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:
co-authored by
Claude Opus 5
parent
d2245ad122
commit
b4a82a4dbe
@@ -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,7 +332,8 @@ const menu = () => ({
|
||||
this.control()?.setAttribute('aria-expanded', 'true')
|
||||
this.label()
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.$nextTick(() =>
|
||||
requestAnimationFrame(() => {
|
||||
if (this.field()) {
|
||||
this.lookUp()
|
||||
|
||||
@@ -337,7 +341,8 @@ const menu = () => ({
|
||||
}
|
||||
|
||||
this.focusItem(focus)
|
||||
})
|
||||
}),
|
||||
)
|
||||
},
|
||||
|
||||
/** Focus in the field, its text selected, and the first row it leaves highlighted. */
|
||||
|
||||
@@ -123,11 +123,13 @@ document.addEventListener('alpine:init', () => {
|
||||
this.closedAt = performance.now()
|
||||
|
||||
if (refocus) {
|
||||
// Back to whatever opened the view: the icon button, or the field itself. A tick
|
||||
// later, because the icon button is only on screen again once the view has closed.
|
||||
// Back to whatever opened the view: the icon button, or the field itself. A frame
|
||||
// after the tick, as `expand()` waits: the icon button is `x-show`n, and Alpine only
|
||||
// shows it on that frame, so a focus in the tick reaches a hidden button, which
|
||||
// Firefox and WebKit refuse (in Chrome the trap's own return had covered for it).
|
||||
const back = this.$refs.trigger ?? this.$refs.input
|
||||
|
||||
this.$nextTick(() => back.focus())
|
||||
this.$nextTick(() => requestAnimationFrame(() => back.focus()))
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user