diff --git a/resources/js/menu.js b/resources/js/menu.js index 47a9c79c..28a2ba69 100644 --- a/resources/js/menu.js +++ b/resources/js/menu.js @@ -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. */ diff --git a/resources/js/search.js b/resources/js/search.js index 2521a275..3a97723c 100644 --- a/resources/js/search.js +++ b/resources/js/search.js @@ -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())) } },