diff --git a/.changeset/run-menu-scroll.md b/.changeset/run-menu-scroll.md deleted file mode 100644 index 693444f1..00000000 --- a/.changeset/run-menu-scroll.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"aicodeman": patch ---- - -The Run dropdown scrolls. It opens upward from the toolbar with no height limit, so with every CLI, the custom endpoint entries ("Claude Code (llama.cpp)" and so on), Terminal and the saved URLs it grew taller than the room above the toolbar: its top ran off-screen and the entries up there could not be reached, worst on a phone. It is now capped to the space between the header and the toolbar (`dvh`, with a `vh` fallback) and scrolls inside that, keeping the scroll from chaining to the page, and its sub-lists (history, saved URLs) are no longer squashed to nothing as the menu fills. diff --git a/src/web/public/styles.css b/src/web/public/styles.css index 7398bdff..839b4aa1 100644 --- a/src/web/public/styles.css +++ b/src/web/public/styles.css @@ -5355,7 +5355,8 @@ html[data-skin="og"] .welcome-primary:hover { z-index inside the toolbar. When the popover is open, raise the toolbar above the CJK input (z-index 52) so the popover is interactable. */ .toolbar:has(.case-settings-popover:not(.hidden)), -.toolbar:has(.case-combobox-list:not(.hidden)) { +.toolbar:has(.case-combobox-list:not(.hidden)), +.toolbar:has(.run-mode-menu.active) { z-index: 100; } @@ -5651,11 +5652,19 @@ html[data-skin="og"] .welcome-primary:hover { endpoint entries, Terminal and the saved URLs it can be taller than the room above the toolbar: its top then sits off-screen with nothing to scroll, and the entries up there cannot be reached (worst on a phone). Cap it to the space between the header and the - toolbar and scroll inside it. `dvh` so an on-screen keyboard or a collapsing browser bar - shrinks the cap with it; the `vh` line is the fallback for browsers without it. */ - max-height: calc(100vh - var(--header-height) - var(--toolbar-height) - 16px); - max-height: calc(100dvh - var(--header-height) - var(--toolbar-height) - 16px); + toolbar and scroll inside it. The cap follows --app-height, not `dvh`: the viewport meta + has no interactive-widget, so `dvh` does not shrink for an on-screen keyboard, while + MobileDetection always sets --app-height to the visual viewport height and KeyboardHandler + keeps it there while the keyboard is up. Both safe areas come off too, because the phone + header and toolbar grow by them in the iPhone home-screen app. The second line is the one + that applies (a declaration carrying var() is never dropped at parse time, so the first + line is not a working fallback for it; it stays only as the plain-viewport form). */ + max-height: calc(100vh - var(--header-height) - var(--safe-area-top) - var(--toolbar-height) - var(--safe-area-bottom) - 16px); + max-height: calc(var(--app-height, 100dvh) - var(--header-height) - var(--safe-area-top) - var(--toolbar-height) - var(--safe-area-bottom) - 16px); overflow-y: auto; + /* overflow-y: auto computes overflow-x to auto as well, so a long nowrap custom-endpoint + label would show a horizontal scrollbar that touch-action: pan-y cannot pan. */ + overflow-x: hidden; overscroll-behavior: contain; -webkit-overflow-scrolling: touch; touch-action: pan-y; diff --git a/test/run-mode-menu-scroll.test.ts b/test/run-mode-menu-scroll.test.ts index 94b807b5..0ee8243d 100644 --- a/test/run-mode-menu-scroll.test.ts +++ b/test/run-mode-menu-scroll.test.ts @@ -8,7 +8,12 @@ import { readFileSync } from 'node:fs'; import { describe, expect, it } from 'vitest'; -const css = readFileSync(new URL('../src/web/public/styles.css', import.meta.url), 'utf8'); +// Comments stripped: the rule's own comment names `overflow-y: auto` and `touch-action: pan-y`, +// which would otherwise satisfy the assertions below after the declarations were deleted. +const css = readFileSync(new URL('../src/web/public/styles.css', import.meta.url), 'utf8').replace( + /\/\*[\s\S]*?\*\//g, + '' +); /** The declaration block of the FIRST rule whose selector is exactly `selector`. */ function rule(selector: string): string { @@ -27,17 +32,32 @@ describe('Run dropdown scrolls when it is taller than the room above the toolbar expect(menu).toMatch(/touch-action:\s*pan-y/); }); - it('is capped to the space between the header and the toolbar, in dvh with a vh fallback', () => { + it('is capped to the space between the header and the toolbar, following --app-height and the safe areas', () => { const caps = [...menu.matchAll(/max-height:\s*([^;]+);/g)].map((m) => m[1]); expect(caps).toHaveLength(2); expect(caps[0]).toContain('100vh'); expect(caps[1]).toContain('100dvh'); + // dvh does not shrink for the on-screen keyboard; --app-height (the visual viewport) does. + expect(caps[1]).toContain('var(--app-height'); for (const cap of caps) { expect(cap).toContain('var(--header-height)'); expect(cap).toContain('var(--toolbar-height)'); + // The phone header and toolbar grow by the safe areas in the iPhone home-screen app. + expect(cap).toContain('var(--safe-area-top)'); + expect(cap).toContain('var(--safe-area-bottom)'); } }); + it('never scrolls sideways (a long custom-endpoint label must not add a horizontal scrollbar)', () => { + expect(menu).toMatch(/overflow-x:\s*hidden/); + }); + + it('raises the toolbar while the menu is open, so its last rows clear the keyboard bar and the CJK input', () => { + // The menu is trapped in the toolbar's stacking context (backdrop-filter), so only the + // toolbar's own z-index can lift it over the accessory bar (z 51) and #cjkInput (z 52). + expect(css).toMatch(/\.toolbar:has\(\.run-mode-menu\.active\)[\s\S]*?\{\s*z-index:\s*100;/); + }); + it('does not let scrolling children (history, saved URLs) be squashed to nothing', () => { expect(rule('.run-mode-menu > *')).toMatch(/flex-shrink:\s*0/); });