fix(utils): update to track pointer instead of keyboard

This commit is contained in:
Brandy Smith
2025-07-02 13:29:00 -04:00
parent 0ded8ba19f
commit f1dc0b2b06

View File

@@ -22,51 +22,49 @@ export interface FocusVisibleUtility {
export const startFocusVisible = (rootEl?: HTMLElement): FocusVisibleUtility => {
let currentFocus: Element[] = [];
let keyboardMode = false;
let lastPointerType: string | null = null;
// Tracks if the last interaction was a pointer event (mouse, touch, pen)
// Used to distinguish between pointer and keyboard navigation for focus styling
let hadPointerEvent = false;
const ref = rootEl ? rootEl.shadowRoot! : document;
const root = rootEl ? rootEl : document.body;
// Adds or removes the focused class for styling
const setFocus = (elements: Element[]) => {
currentFocus.forEach((el) => el.classList.remove(ION_FOCUSED));
elements.forEach((el) => el.classList.add(ION_FOCUSED));
currentFocus = elements;
};
// Do not set focus on pointer interactions
const pointerDown = (ev: Event) => {
const pointerEvent = ev as PointerEvent;
lastPointerType = pointerEvent.pointerType;
keyboardMode = false;
setFocus([]);
if (ev instanceof PointerEvent && ev.pointerType !== '') {
hadPointerEvent = true;
// Reset after the event loop so only the immediate focusin is suppressed
setTimeout(() => { hadPointerEvent = false; }, 0);
}
};
// Clear hadPointerEvent so keyboard navigation shows focus
// Also, clear focus if the key is not a navigation key
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;
hadPointerEvent = false;
// If it's not a focus key, clear focus immediately
const keyboardEvent = ev as KeyboardEvent;
if (!FOCUS_KEYS.includes(keyboardEvent.key)) {
setFocus([]);
}
};
// Set focus if the last interaction was NOT a pointer event
// This works around iOS/Safari bugs where keydown is not fired for Tab
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);
}
if (target.classList.contains(ION_FOCUSABLE) && !hadPointerEvent) {
const toFocus = ev.composedPath().filter((el): el is HTMLElement =>
el instanceof HTMLElement && el.classList.contains(ION_FOCUSABLE)
);
setFocus(toFocus);
}
};
@@ -80,16 +78,12 @@ export const startFocusVisible = (rootEl?: HTMLElement): FocusVisibleUtility =>
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);
};
return {