From 5edb00bd9a56e47fc653370fc91d02a93a3fa4dd Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Thu, 2 Apr 2026 19:24:41 -0700 Subject: [PATCH] Fix virtual scrolling MenuList font family dropdowns shrinking when wider content is unmounted (#3987) * Fix virtual scrolling MenuList dropdowns shrinking when wider content goes away * Code review fixes * Fix small CI workflow bug * Stop scrolling in dropdowns from horizontally scrolling the control bar * Use more robust way of getting commit hash in CI workflow --- .github/workflows/build.yml | 2 +- .../components/floating-menus/MenuList.svelte | 38 ++++++++++++++++++- .../src/components/layout/FloatingMenu.svelte | 2 +- frontend/src/utility-functions/input.ts | 2 +- 4 files changed, 40 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9742dcb6..74a5c39d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -162,7 +162,7 @@ jobs: REF="master" ENVIRONMENT="graphite-dev (Production)" else - REF="${{ inputs.checkout_ref || github.head_ref || github.ref_name }}" + REF="$(git rev-parse HEAD)" ENVIRONMENT="graphite-dev (Preview)" fi DEPLOY_ID=$(gh api \ diff --git a/frontend/src/components/floating-menus/MenuList.svelte b/frontend/src/components/floating-menus/MenuList.svelte index 3e62fd0b..66cccf0c 100644 --- a/frontend/src/components/floating-menus/MenuList.svelte +++ b/frontend/src/components/floating-menus/MenuList.svelte @@ -47,6 +47,8 @@ let virtualScrollingEntriesStart = 0; let keydownListenerAdded = false; let destroyed = false; + let maxMenuWidth = 0; + let resizeObserver: ResizeObserver | undefined = undefined; // eslint-disable-next-line svelte/prefer-svelte-reactivity -- `loadedFonts` reactivity is driven by `loadedFontsGeneration`, not the Set itself let loadedFonts = new Set(); let loadedFontsGeneration = 0; @@ -77,6 +79,7 @@ }); onDestroy(() => { removeEventListener("keydown", keydown); + resizeObserver?.disconnect(); // Set the destroyed status in the closure kept by the awaited `tick()` in `onMount` in case that delayed run occurs after the component is destroyed destroyed = true; }); @@ -144,6 +147,15 @@ keydownListenerAdded = false; } + // For virtual scrolling menus, observe width changes so the menu only grows and never shrinks while open + if (open && virtualScrolling) { + startMenuWidthObserver(); + } else if (resizeObserver) { + resizeObserver.disconnect(); + resizeObserver = undefined; + maxMenuWidth = 0; + } + highlighted = activeEntry; dispatch("open", open); @@ -156,14 +168,38 @@ }); } - function watchEntriesHash(_entriesHash: bigint) { + function watchEntriesHash(_: bigint) { reactiveEntries = entries; } function watchRemeasureWidth(_: MenuListEntry[][], __: boolean) { + // Skip re-measurement for virtual scrolling menus since ResizeObserver handles their width + if (virtualScrolling) return; + self?.measureAndEmitNaturalWidth(); } + async function startMenuWidthObserver() { + await tick(); + // Guard against the menu having closed during the tick + if (!open) return; + + const floatingMenuContentDiv = self?.div()?.querySelector("[data-floating-menu-content]"); + if (!(floatingMenuContentDiv instanceof HTMLElement)) return; + + maxMenuWidth = 0; + + resizeObserver?.disconnect(); + resizeObserver = new ResizeObserver(() => { + const width = floatingMenuContentDiv.scrollWidth; + if (width > maxMenuWidth) { + maxMenuWidth = width; + floatingMenuContentDiv.style.minWidth = `${maxMenuWidth}px`; + } + }); + resizeObserver.observe(floatingMenuContentDiv); + } + function onScroll(e: Event) { if (!virtualScrollingEntryHeight) return; virtualScrollingEntriesStart = e.target instanceof HTMLElement ? e.target.scrollTop : 0; diff --git a/frontend/src/components/layout/FloatingMenu.svelte b/frontend/src/components/layout/FloatingMenu.svelte index d9c2fc04..489013f1 100644 --- a/frontend/src/components/layout/FloatingMenu.svelte +++ b/frontend/src/components/layout/FloatingMenu.svelte @@ -492,7 +492,7 @@
{/if} {#if displayContainer} -
+
diff --git a/frontend/src/utility-functions/input.ts b/frontend/src/utility-functions/input.ts index dd0b129c..d2004843 100644 --- a/frontend/src/utility-functions/input.ts +++ b/frontend/src/utility-functions/input.ts @@ -225,7 +225,7 @@ export function onWheelScroll(e: WheelEvent, editor: EditorWrapper) { // Redirect vertical scroll wheel movement into a horizontal scroll on a horizontally scrollable element // There seems to be no possible way to properly employ the browser's smooth scrolling interpolation - const horizontalScrollableElement = e.target instanceof Element && e.target.closest("[data-scrollable-x]"); + const horizontalScrollableElement = e.target instanceof Element && !e.target.closest("[data-scrollable-y]") && e.target.closest("[data-scrollable-x]"); if (horizontalScrollableElement && e.deltaY !== 0) { horizontalScrollableElement.scrollTo(horizontalScrollableElement.scrollLeft + e.deltaY, 0); return;