From 0ded8ba19f71f04d83cdb053efd66333c9155b22 Mon Sep 17 00:00:00 2001 From: Brandy Smith <6577830+brandyscarney@users.noreply.github.com> Date: Tue, 1 Jul 2025 18:53:03 -0400 Subject: [PATCH] fix(utils): update focus visible to work with keydown again --- core/src/utils/focus-visible.ts | 56 +++++++++++++++++++++++++++++---- 1 file changed, 50 insertions(+), 6 deletions(-) diff --git a/core/src/utils/focus-visible.ts b/core/src/utils/focus-visible.ts index 851e32f1ab..f1411e6996 100644 --- a/core/src/utils/focus-visible.ts +++ b/core/src/utils/focus-visible.ts @@ -1,5 +1,19 @@ const ION_FOCUSED = 'ion-focused'; const ION_FOCUSABLE = 'ion-focusable'; +const FOCUS_KEYS = [ + 'Tab', + 'ArrowDown', + 'Space', + 'Escape', + ' ', + 'Shift', + 'Enter', + 'ArrowLeft', + 'ArrowRight', + 'ArrowUp', + 'Home', + 'End', +]; export interface FocusVisibleUtility { destroy: () => void; @@ -8,6 +22,8 @@ export interface FocusVisibleUtility { export const startFocusVisible = (rootEl?: HTMLElement): FocusVisibleUtility => { let currentFocus: Element[] = []; + let keyboardMode = false; + let lastPointerType: string | null = null; const ref = rootEl ? rootEl.shadowRoot! : document; const root = rootEl ? rootEl : document.body; @@ -18,16 +34,40 @@ export const startFocusVisible = (rootEl?: HTMLElement): FocusVisibleUtility => currentFocus = elements; }; - const pointerDown = () => { + const pointerDown = (ev: Event) => { + const pointerEvent = ev as PointerEvent; + lastPointerType = pointerEvent.pointerType; + keyboardMode = false; setFocus([]); }; - const onFocusin = (ev: Event) => { - const toFocus = ev.composedPath().filter((el): el is HTMLElement => { - return el instanceof HTMLElement && el.classList.contains(ION_FOCUSABLE); - }); + const onKeydown = (ev: Event) => { + const keyboardEvent = ev as KeyboardEvent; + // Always set keyboard mode to true when any key is pressed + // This handles the WebKit Tab key bug where keydown might not fire + keyboardMode = true; - setFocus(toFocus); + // If it's not a focus key, clear focus immediately + if (!FOCUS_KEYS.includes(keyboardEvent.key)) { + setFocus([]); + } + }; + + const onFocusin = (ev: Event) => { + // Check if this focus event is likely from keyboard navigation + // We can detect this by checking if there was a recent keydown event + // or if the focus target is a focusable element that typically gets focus via keyboard + const target = ev.target as HTMLElement; + + if (target.classList.contains(ION_FOCUSABLE)) { + // If we're in keyboard mode or this looks like keyboard navigation + if (keyboardMode || !lastPointerType) { + const toFocus = ev.composedPath().filter((el): el is HTMLElement => { + return el instanceof HTMLElement && el.classList.contains(ION_FOCUSABLE); + }); + setFocus(toFocus); + } + } }; const onFocusout = () => { @@ -36,14 +76,18 @@ export const startFocusVisible = (rootEl?: HTMLElement): FocusVisibleUtility => } }; + ref.addEventListener('keydown', onKeydown); ref.addEventListener('focusin', onFocusin); ref.addEventListener('focusout', onFocusout); + ref.addEventListener('pointerdown', pointerDown, { passive: true }); ref.addEventListener('touchstart', pointerDown, { passive: true }); ref.addEventListener('mousedown', pointerDown); const destroy = () => { + ref.removeEventListener('keydown', onKeydown); ref.removeEventListener('focusin', onFocusin); ref.removeEventListener('focusout', onFocusout); + ref.removeEventListener('pointerdown', pointerDown); ref.removeEventListener('touchstart', pointerDown); ref.removeEventListener('mousedown', pointerDown); };