Fix dropdown menus spawning offset in scrolled panels, a regression from upgrading to Svelte 5.54 (#4047)

* Fix dropdown menus spawning offset in scrolled panels, a regression from upgrading to Svelte 5.54

Regression introduced in #3933 by upgrading from Svelte 5.47.1 to 5.54.1

* Code review

* Fix unrelated typo

* Add assets build size to build link comment
This commit is contained in:
Keavon Chambers 2026-04-24 12:17:27 -07:00 committed by GitHub
parent c9c76df40c
commit 7bb01c9651
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 52 additions and 31 deletions

View File

@ -204,9 +204,19 @@ jobs:
exit 0
fi
size_of() { find frontend/dist/assets "$@" -printf '%s\n' | awk '{s+=$1} END {printf "%.2f MB", s/1048576}'; }
WASM_SIZE=$(size_of -name '*.wasm')
JS_SIZE=$(size_of -name '*.js')
CSS_SIZE=$(size_of -name '*.css')
FONT_SIZE=$(size_of \( -name '*.woff2' -o -name '*.woff' -o -name '*.ttf' -o -name '*.otf' \))
IMAGE_SIZE=$(size_of \( -name '*.png' -o -name '*.jpg' -o -name '*.svg' \))
ALL_SIZE=$(size_of -type f)
COMMENT_BODY="| 📦 **Web Build Complete for** $(git rev-parse HEAD) |
|-|
| $CF_URL |"
| $CF_URL |
Wasm: **$WASM_SIZE** — JS: **$JS_SIZE** — CSS: **$CSS_SIZE** — Fonts: **$FONT_SIZE** — Images: **$IMAGE_SIZE** — All Assets: **$ALL_SIZE**"
if [ "${{ github.ref }}" = "refs/tags/latest-stable" ]; then
# Push tag: skip commenting (commit was already commented on master merge)

View File

@ -62,8 +62,6 @@
let measuringOngoingGuard = false;
let minWidthParentWidth = 0;
let pointerStillDown = false;
let floatingMenuBounds = new DOMRect();
let floatingMenuContentBounds = new DOMRect();
$: watchOpenChange(open);
@ -189,9 +187,9 @@
if (!self || !floatingMenuContainer || !floatingMenuContent || !floatingMenuContentDiv) return;
const windowBounds = document.documentElement.getBoundingClientRect();
floatingMenuBounds = self.getBoundingClientRect();
const floatingMenuBounds = self.getBoundingClientRect();
const floatingMenuContainerBounds = floatingMenuContainer.getBoundingClientRect();
floatingMenuContentBounds = floatingMenuContentDiv.getBoundingClientRect();
const floatingMenuContentBounds = floatingMenuContentDiv.getBoundingClientRect();
const overflowingLeft = floatingMenuContentBounds.left - windowEdgeMargin <= windowBounds.left;
const overflowingRight = floatingMenuContentBounds.right + windowEdgeMargin >= windowBounds.right;
@ -210,22 +208,30 @@
else if (direction === "Right" && overflowingRight) direction = "Left";
}
// These are set imperatively, not through reactive Svelte style bindings, because that would cause `afterUpdate()` to call this function recursively forever.
// CSS custom properties on the container are used instead of direct `.style` on the content because Svelte's `set_style` can replace the content's entire
// inline style when its managed `style` attribute updates, which would wipe out any manually-set properties like `top` and `left`.
floatingMenuContainer.style.removeProperty("--content-top");
floatingMenuContainer.style.removeProperty("--content-bottom");
floatingMenuContainer.style.removeProperty("--content-left");
floatingMenuContainer.style.removeProperty("--content-right");
floatingMenuContainer.style.removeProperty("--content-border-top-left-radius");
floatingMenuContainer.style.removeProperty("--content-border-top-right-radius");
floatingMenuContainer.style.removeProperty("--content-border-bottom-left-radius");
floatingMenuContainer.style.removeProperty("--content-border-bottom-right-radius");
const inParentFloatingMenu = Boolean(floatingMenuContainer.closest("[data-floating-menu-content]"));
const noPosition = Boolean(floatingMenuContainer.closest("[data-floating-menu-no-position]"));
if (!inParentFloatingMenu && !noPosition) {
// Required to correctly position content when scrolled (it has a `position: fixed` to prevent clipping)
// We use `.style` on a div (instead of a style DOM attribute binding) because the binding causes the `afterUpdate()` hook to call the function we're in recursively forever
let tailOffset = 0;
if (type === "Popover") tailOffset = 10;
if (type === "Tooltip") tailOffset = direction === "Bottom" ? 20 : 10;
if (direction === "Bottom") floatingMenuContentDiv.style.top = `${tailOffset + floatingMenuBounds.y}px`;
if (direction === "Top") floatingMenuContentDiv.style.bottom = `${tailOffset + (windowBounds.height - floatingMenuBounds.y)}px`;
if (direction === "Right") floatingMenuContentDiv.style.left = `${tailOffset + floatingMenuBounds.x}px`;
if (direction === "Left") floatingMenuContentDiv.style.right = `${tailOffset + (windowBounds.width - floatingMenuBounds.x)}px`;
if (direction === "Bottom") floatingMenuContainer.style.setProperty("--content-top", `${tailOffset + floatingMenuBounds.y}px`);
if (direction === "Top") floatingMenuContainer.style.setProperty("--content-bottom", `${tailOffset + (windowBounds.height - floatingMenuBounds.y)}px`);
if (direction === "Right") floatingMenuContainer.style.setProperty("--content-left", `${tailOffset + floatingMenuBounds.x}px`);
if (direction === "Left") floatingMenuContainer.style.setProperty("--content-right", `${tailOffset + (windowBounds.width - floatingMenuBounds.x)}px`);
// Required to correctly position tail when scrolled (it has a `position: fixed` to prevent clipping)
// We use `.style` on a div (instead of a style DOM attribute binding) because the binding causes the `afterUpdate()` hook to call the function we're in recursively forever
if (tail && direction === "Bottom") tail.style.top = `${floatingMenuBounds.y}px`;
if (tail && direction === "Top") tail.style.bottom = `${windowBounds.height - floatingMenuBounds.y}px`;
if (tail && direction === "Right") tail.style.left = `${floatingMenuBounds.x}px`;
@ -239,45 +245,42 @@
if (direction === "Top" || direction === "Bottom") {
zeroedBorderVertical = direction === "Top" ? "Bottom" : "Top";
// We use `.style` on a div (instead of a style DOM attribute binding) because the binding causes the `afterUpdate()` hook to call the function we're in recursively forever
if (overflowingLeft) {
floatingMenuContentDiv.style.left = `${windowEdgeMargin}px`;
floatingMenuContainer.style.setProperty("--content-left", `${windowEdgeMargin}px`);
if (windowBounds.left + floatingMenuContainerBounds.left === 12) zeroedBorderHorizontal = "Left";
}
if (overflowingRight) {
floatingMenuContentDiv.style.right = `${windowEdgeMargin}px`;
floatingMenuContainer.style.setProperty("--content-right", `${windowEdgeMargin}px`);
if (windowBounds.right - floatingMenuContainerBounds.right === 12) zeroedBorderHorizontal = "Right";
}
}
if (direction === "Left" || direction === "Right") {
zeroedBorderHorizontal = direction === "Left" ? "Right" : "Left";
// We use `.style` on a div (instead of a style DOM attribute binding) because the binding causes the `afterUpdate()` hook to call the function we're in recursively forever
if (overflowingTop) {
floatingMenuContentDiv.style.top = `${windowEdgeMargin}px`;
floatingMenuContainer.style.setProperty("--content-top", `${windowEdgeMargin}px`);
if (windowBounds.top + floatingMenuContainerBounds.top === 12) zeroedBorderVertical = "Top";
}
if (overflowingBottom) {
floatingMenuContentDiv.style.bottom = `${windowEdgeMargin}px`;
floatingMenuContainer.style.setProperty("--content-bottom", `${windowEdgeMargin}px`);
if (windowBounds.bottom - floatingMenuContainerBounds.bottom === 12) zeroedBorderVertical = "Bottom";
}
}
// Remove the rounded corner from the content where the tail perfectly meets the corner
if (displayTail && windowEdgeMargin === 6 && zeroedBorderVertical && zeroedBorderHorizontal) {
// We use `.style` on a div (instead of a style DOM attribute binding) because the binding causes the `afterUpdate()` hook to call the function we're in recursively forever
switch (`${zeroedBorderVertical}${zeroedBorderHorizontal}`) {
case "TopLeft":
floatingMenuContentDiv.style.borderTopLeftRadius = "0";
floatingMenuContainer.style.setProperty("--content-border-top-left-radius", "0");
break;
case "TopRight":
floatingMenuContentDiv.style.borderTopRightRadius = "0";
floatingMenuContainer.style.setProperty("--content-border-top-right-radius", "0");
break;
case "BottomLeft":
floatingMenuContentDiv.style.borderBottomLeftRadius = "0";
floatingMenuContainer.style.setProperty("--content-border-bottom-left-radius", "0");
break;
case "BottomRight":
floatingMenuContentDiv.style.borderBottomRightRadius = "0";
floatingMenuContainer.style.setProperty("--content-border-bottom-right-radius", "0");
break;
default:
break;
@ -537,12 +540,20 @@
box-shadow: rgba(var(--color-0-black-rgb), 0.5) 0 2px 4px;
border: 1px solid var(--color-3-darkgray);
border-radius: 4px;
border-top-left-radius: var(--content-border-top-left-radius, 4px);
border-top-right-radius: var(--content-border-top-right-radius, 4px);
border-bottom-left-radius: var(--content-border-bottom-left-radius, 4px);
border-bottom-right-radius: var(--content-border-bottom-right-radius, 4px);
color: var(--color-e-nearwhite);
font-size: inherit;
padding: 8px;
z-index: 0;
// Draw over the application without being clipped by the containing panel's `overflow: hidden`
position: fixed;
top: var(--content-top, auto);
bottom: var(--content-bottom, auto);
left: var(--content-left, auto);
right: var(--content-right, auto);
// Counteract the rightward shift caused by the border
margin-left: -1px;
}

View File

@ -682,9 +682,9 @@ fn string_length(_: impl Ctx, string: String) -> f64 {
string.graphemes(true).count() as f64
}
/// Splits a string into a list of substrings based on the specified delimeter. This is the inverse of the **String Join** node.
/// Splits a string into a list of substrings based on the specified delimiter. This is the inverse of the **String Join** node.
///
/// For example, splitting "a, b, c" with delimeter ", " produces `["a", "b", "c"]`.
/// For example, splitting "a, b, c" with delimiter ", " produces `["a", "b", "c"]`.
#[node_macro::node(category("Text"))]
fn string_split(
_: impl Ctx,
@ -692,15 +692,15 @@ fn string_split(
string: String,
/// The character(s) that separate the substrings. These are not included in the outputs.
#[default("\\n")]
delimeter: String,
/// Whether to convert escape sequences found in the delimeter into their corresponding characters:
delimiter: String,
/// Whether to convert escape sequences found in the delimiter into their corresponding characters:
/// "\n" (newline), "\r" (carriage return), "\t" (tab), "\0" (null), and "\\" (backslash).
#[default(true)]
delimeter_escaping: bool,
delimiter_escaping: bool,
) -> Vec<String> {
let delimeter = if delimeter_escaping { unescape_string(delimeter) } else { delimeter };
let delimiter = if delimiter_escaping { unescape_string(delimiter) } else { delimiter };
string.split(&delimeter).map(str::to_string).collect()
string.split(&delimiter).map(str::to_string).collect()
}
/// Joins a list of strings together with a separator between each pair. This is the inverse of the **String Split** node.