From d94b69c88e1fad68b4f8667228ed8dea9c521390 Mon Sep 17 00:00:00 2001 From: Steffen Deusch Date: Sat, 18 Jan 2025 16:27:59 +0100 Subject: [PATCH] Handle entering FocusWrap from outside (#3641) When FocusWrap was used, but the previous focus was outside of the wrapped element, the browser would try to focus the boundary element, causing us to focus the last element instead. This is fixed by checking the relatedTarget of the event, and specially handling focus coming from outside. Fixes #3636. --- assets/js/phoenix_live_view/hooks.js | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/assets/js/phoenix_live_view/hooks.js b/assets/js/phoenix_live_view/hooks.js index bda38cd62d..f69765794f 100644 --- a/assets/js/phoenix_live_view/hooks.js +++ b/assets/js/phoenix_live_view/hooks.js @@ -47,8 +47,26 @@ let Hooks = { mounted(){ this.focusStart = this.el.firstElementChild this.focusEnd = this.el.lastElementChild - this.focusStart.addEventListener("focus", () => ARIA.focusLast(this.el)) - this.focusEnd.addEventListener("focus", () => ARIA.focusFirst(this.el)) + this.focusStart.addEventListener("focus", (e) => { + if(!e.relatedTarget || !this.el.contains(e.relatedTarget)){ + // Handle focus entering from outside (e.g. Tab when body is focused) + // https://github.com/phoenixframework/phoenix_live_view/issues/3636 + const nextFocus = e.target.nextElementSibling + ARIA.attemptFocus(nextFocus) || ARIA.focusFirst(nextFocus) + } else { + ARIA.focusLast(this.el) + } + }) + this.focusEnd.addEventListener("focus", (e) => { + if(!e.relatedTarget || !this.el.contains(e.relatedTarget)){ + // Handle focus entering from outside (e.g. Shift+Tab when body is focused) + // https://github.com/phoenixframework/phoenix_live_view/issues/3636 + const nextFocus = e.target.previousElementSibling + ARIA.attemptFocus(nextFocus) || ARIA.focusLast(nextFocus) + } else { + ARIA.focusFirst(this.el) + } + }) this.el.addEventListener("phx:show-end", () => this.el.focus()) if(window.getComputedStyle(this.el).display !== "none"){ ARIA.focusFirst(this.el)