Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bug(input_text_area): Auto resized text areas resize on visibility change #1569

Merged
merged 11 commits into from
Jul 26, 2024
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

* Require shinyswatch >= 0.7.0 and updated examples accordingly. (#1558)

* `input_text_area(autoresize=True)` now resizes properly even when it's not visible when initially rendered (e.g. in a closed accordion or a hidden tab). (#1560)

### Bug fixes

### Deprecations
Expand Down
42 changes: 39 additions & 3 deletions js/text-area/textarea-autoresize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,46 @@ function onDelegatedEvent(
});
}

// Use a single intersectionObserver as they are slow to create / use.
let textAreaIntersectionObserver: null | IntersectionObserver = null;

function callUpdateHeightWhenTargetIsVisible(target: HTMLTextAreaElement) {
if (textAreaIntersectionObserver === null) {
// Create a single observer to watch for the textarea becoming visible
textAreaIntersectionObserver = new IntersectionObserver(
(entries, observer) => {
entries.forEach((entry) => {
// Quit if the entry is not visible
if (!entry.isIntersecting) {
return;
}
// If the entry is visible (even if it's just a single pixel)
// Stop observing the target
textAreaIntersectionObserver!.unobserve(entry.target);

// Update the height of the textarea
update_height(entry.target as HTMLTextAreaElement);
});
}
);
}

textAreaIntersectionObserver.observe(target);
}

function update_height(target: HTMLTextAreaElement) {
// Automatically resize the textarea to fit its content.
target.style.height = "auto";
target.style.height = target.scrollHeight + "px";
if (target.scrollHeight > 0) {
// Automatically resize the textarea to fit its content.
target.style.height = "auto";
target.style.height = target.scrollHeight + "px";
} else {
// The textarea is not visible on the page, therefore it has a 0 scroll height.

// If we should autoresize the text area height, then we can wait for the textarea to
// become visible and call `update_height` again. Hopefully the scroll height is no
// longer 0
callUpdateHeightWhenTargetIsVisible(target);
}
}

// Update on change
Expand Down
25 changes: 23 additions & 2 deletions shiny/www/py-shiny/text-area/textarea-autoresize.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,30 @@ function onDelegatedEvent(eventName, selector, callback) {
}
});
}
var textAreaIntersectionObserver = null;
function callUpdateHeightWhenTargetIsVisible(target) {
if (textAreaIntersectionObserver === null) {
textAreaIntersectionObserver = new IntersectionObserver(
(entries, observer) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) {
return;
}
textAreaIntersectionObserver.unobserve(entry.target);
update_height(entry.target);
});
}
);
}
textAreaIntersectionObserver.observe(target);
}
function update_height(target) {
target.style.height = "auto";
target.style.height = target.scrollHeight + "px";
if (target.scrollHeight > 0) {
target.style.height = "auto";
target.style.height = target.scrollHeight + "px";
} else {
callUpdateHeightWhenTargetIsVisible(target);
}
}
onDelegatedEvent(
"input",
Expand Down
26 changes: 26 additions & 0 deletions tests/playwright/shiny/inputs/input_text_area/autoresize/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from shiny.express import render, ui

with ui.navset_card_tab(id="tab"):

with ui.nav_panel("Tab 1"):
"Tab 1 content"
with ui.nav_panel("Text Area"):
ui.input_text_area(
id="test_text_area",
label="A text area input",
autoresize=True,
value="a\nb\nc\nd\ne",
)

ui.input_text_area(
id="test_text_area2",
label="A second text area input",
autoresize=True,
value="a\nb\nc\nd\ne",
rows=4,
)


@render.code
def text():
return "Loaded"
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from playwright.sync_api import Page

from shiny.playwright import controller
from shiny.playwright.expect import expect_to_have_style
from shiny.run import ShinyAppProc


def test_accordion(page: Page, local_app: ShinyAppProc, is_webkit: bool) -> None:
page.goto(local_app.url)

text = controller.OutputCode(page, "text")
tab = controller.NavsetTab(page, "tab")

test_text_area = controller.InputTextArea(page, "test_text_area")
test_text_area_w_rows = controller.InputTextArea(page, "test_text_area2")

text.expect_value("Loaded")

# Make sure the `rows` is respected
test_text_area_w_rows.expect_rows("4")
# Make sure the placeholder row value of `1` is set
test_text_area.expect_rows("1")

tab.set("Text Area")

test_text_area.expect_autoresize(True)
test_text_area.expect_value("a\nb\nc\nd\ne")

if is_webkit:
# Skip the rest of the test for webkit.
# Heights are not consistent with chrome and firefox
return
expect_to_have_style(test_text_area.loc, "height", "125px")
expect_to_have_style(test_text_area_w_rows.loc, "height", "125px")

# Make sure the `rows` is consistent
test_text_area.expect_rows("1")
test_text_area_w_rows.expect_rows("4")

# Reset the text area to a single row and make sure the area shrink to appropriate size
test_text_area.set("single row")
test_text_area_w_rows.set("single row")

# 1 row
expect_to_have_style(test_text_area.loc, "height", "35px")
# 4 rows
expect_to_have_style(test_text_area_w_rows.loc, "height", "102px")
Loading