Focus what x-show reveals on the frame it shows

A sheet-at-compact menu focused its first item, or its filter field, in
$nextTick, and the icon entry point of <x-search> handed focus back to
its trigger the same way. Both targets are x-show'n, and Alpine shows an
element on the animation frame after its condition turns true, not in the
tick: the focus reached an element still display: none, which Firefox and
WebKit refuse. The sheet's focus trap then put the focus on the drag
handle, and a closed search left it on the page body; in Chrome the
frame or the trap's own return happened to cover for it. The comment in
openSheet() assumed Alpine holds $nextTick until the transition begins,
which it only does with x-transition, and the sheet uses CSS transitions.

Both now wait for that frame after the tick, running after Alpine's own
show in it, as search.js's expand() already did.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Andreas Reinhold / reini
2026-09-16 12:23:15 +02:00
co-authored by Claude Opus 5
parent d2245ad122
commit b4a82a4dbe
2 changed files with 19 additions and 12 deletions
+14 -9
View File
@@ -317,8 +317,11 @@ const menu = () => ({
},
/**
* The sheet shows a frame or two after `open` turns true, once its transition has begun, and
* Alpine holds `$nextTick` until then. Its focus trap starts on a timer of its own and keeps a
* The sheet's panel is `x-show`n, and Alpine shows an element on the animation frame after
* `open` turns true, not in the tick: an item focused in `$nextTick` is still `display: none`,
* which Firefox and WebKit refuse to focus (Chrome only got there because its frame came
* first). So the focus waits for that frame too, and runs after Alpine's own show in it, as
* search.js's `expand()` does. The sheet's focus trap starts on a timer of its own and keeps a
* focus already inside it, so the item (or the field) M3 asks to be focused first wins over
* the drag handle the trap would otherwise pick.
*/
@@ -329,15 +332,17 @@ const menu = () => ({
this.control()?.setAttribute('aria-expanded', 'true')
this.label()
this.$nextTick(() => {
if (this.field()) {
this.lookUp()
this.$nextTick(() =>
requestAnimationFrame(() => {
if (this.field()) {
this.lookUp()
return
}
return
}
this.focusItem(focus)
})
this.focusItem(focus)
}),
)
},
/** Focus in the field, its text selected, and the first row it leaves highlighted. */