Scroll a menu that is too long for the window
The popover was fit-content with overflow visible, so a long menu ran past the edge of the top layer, where the page's own scrolling cannot reach it. It now caps at 18rem — less on a short window — and scrolls, as M3's menu behaviour asks, and the keyboard brings the item it moves to into view. Plan step 11, actions.md ACT-04. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
co-authored by
Claude Opus 5
parent
6e24ff2cf8
commit
c1ca157341
@@ -183,10 +183,16 @@ document.addEventListener('alpine:init', () => {
|
|||||||
return [...this.$refs.menu.querySelectorAll(ITEMS)].filter((item) => item.getAttribute('aria-disabled') !== 'true')
|
return [...this.$refs.menu.querySelectorAll(ITEMS)].filter((item) => item.getAttribute('aria-disabled') !== 'true')
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** The menu scrolls when it is too long for the window, so the item taken has to be shown. */
|
||||||
focusItem(which) {
|
focusItem(which) {
|
||||||
const items = this.items()
|
const items = this.items()
|
||||||
|
|
||||||
;(which === 'last' ? items.at(-1) : items[0])?.focus()
|
this.reach(which === 'last' ? items.at(-1) : items[0])
|
||||||
|
},
|
||||||
|
|
||||||
|
reach(item) {
|
||||||
|
item?.focus()
|
||||||
|
item?.scrollIntoView({ block: 'nearest' })
|
||||||
},
|
},
|
||||||
|
|
||||||
navigate(event) {
|
navigate(event) {
|
||||||
@@ -195,7 +201,7 @@ document.addEventListener('alpine:init', () => {
|
|||||||
|
|
||||||
const move = (index) => {
|
const move = (index) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
items[(index + items.length) % items.length]?.focus()
|
this.reach(items[(index + items.length) % items.length])
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (event.key) {
|
switch (event.key) {
|
||||||
@@ -226,7 +232,7 @@ document.addEventListener('alpine:init', () => {
|
|||||||
|
|
||||||
if (match) {
|
if (match) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
match.focus()
|
this.reach(match)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -32,6 +32,10 @@
|
|||||||
of the loop iteration around it, which would give every child component after the menu the
|
of the loop iteration around it, which would give every child component after the menu the
|
||||||
same key.
|
same key.
|
||||||
|
|
||||||
|
A menu too long for the window scrolls, as M3 asks, rather than running off the edge of the
|
||||||
|
top layer where nothing can reach it: 18rem at most, and less on a short window. The arrow
|
||||||
|
keys, Home, End and typeahead bring the item they move to into view.
|
||||||
|
|
||||||
The container is Expressive's standard menu (surface-container-low, 16px corner, elevation
|
The container is Expressive's standard menu (surface-container-low, 16px corner, elevation
|
||||||
2), or `vibrant` in tertiary-container — StandardMenuTokens and VibrantMenuTokens from
|
2), or `vibrant` in tertiary-container — StandardMenuTokens and VibrantMenuTokens from
|
||||||
androidx Compose Material 3 (Apache-2.0). --}}
|
androidx Compose Material 3 (Apache-2.0). --}}
|
||||||
@@ -67,7 +71,7 @@
|
|||||||
x-on:keydown="navigate($event)"
|
x-on:keydown="navigate($event)"
|
||||||
x-on:click="activate($event)"
|
x-on:click="activate($event)"
|
||||||
@class([
|
@class([
|
||||||
'm-0 min-w-28 max-w-70 overflow-visible border-0 p-1 rounded-corner-lg shadow-elevation-2 [inset:auto]',
|
'm-0 min-w-28 max-w-70 max-h-[min(18rem,calc(100dvh-2rem))] overflow-y-auto border-0 p-1 rounded-corner-lg shadow-elevation-2 [inset:auto]',
|
||||||
'my-1 [position-try-fallbacks:flip-block,flip-inline,flip-block_flip-inline]',
|
'my-1 [position-try-fallbacks:flip-block,flip-inline,flip-block_flip-inline]',
|
||||||
'opacity-0 transition-[opacity,translate,display,overlay] transition-discrete duration-(--md-sys-motion-effects-fast-duration) ease-effects-fast open:opacity-100 starting:open:opacity-0',
|
'opacity-0 transition-[opacity,translate,display,overlay] transition-discrete duration-(--md-sys-motion-effects-fast-duration) ease-effects-fast open:opacity-100 starting:open:opacity-0',
|
||||||
'bg-surface-container-low text-on-surface' => ! $vibrant,
|
'bg-surface-container-low text-on-surface' => ! $vibrant,
|
||||||
|
|||||||
@@ -23,6 +23,12 @@ it('opens a popover menu from its trigger', function () {
|
|||||||
->toContain('[position-area:bottom_span-right]');
|
->toContain('[position-area:bottom_span-right]');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('scrolls a menu too long for the window instead of running off it', function () {
|
||||||
|
expect((string) $this->blade('<x-menu><x-slot:trigger><button>x</button></x-slot:trigger></x-menu>'))
|
||||||
|
->toContain('max-h-[min(18rem,calc(100dvh-2rem))] overflow-y-auto')
|
||||||
|
->not->toContain('overflow-visible');
|
||||||
|
});
|
||||||
|
|
||||||
it('opens at the position asked for, in the vibrant colours on request', function () {
|
it('opens at the position asked for, in the vibrant colours on request', function () {
|
||||||
$html = (string) $this->blade('<x-menu position="top-end" vibrant><x-slot:trigger><button>x</button></x-slot:trigger></x-menu>');
|
$html = (string) $this->blade('<x-menu position="top-end" vibrant><x-slot:trigger><button>x</button></x-slot:trigger></x-menu>');
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user