️(frontend) add visible focus ring on restore focus

Show focus ring when restoring focus, keyboard navigation only
This commit is contained in:
Cyril
2026-01-15 10:23:58 +01:00
parent 716e11b5b3
commit cfc6b90bd7
2 changed files with 46 additions and 2 deletions
+43 -1
View File
@@ -26,8 +26,21 @@ export function useRestoreFocus(
const prevIsOpenRef = useRef(false)
const triggerRef = useRef<HTMLElement | null>(null)
const cleanupRef = useRef<(() => void) | null>(null)
const lastInteractionRef = useRef<'keyboard' | 'mouse' | null>(null)
useEffect(() => {
// Track last interaction type (like native :focus-visible behavior)
const handleKeyDown = () => {
lastInteractionRef.current = 'keyboard'
}
const handleMouseDown = () => {
lastInteractionRef.current = 'mouse'
}
document.addEventListener('keydown', handleKeyDown)
document.addEventListener('mousedown', handleMouseDown)
const wasOpen = prevIsOpenRef.current
// Just opened
@@ -41,7 +54,28 @@ export function useRestoreFocus(
if (wasOpen && !isOpen) {
const trigger = triggerRef.current
if (trigger && document.contains(trigger)) {
const focus = () => trigger.focus({ preventScroll })
const focus = () => {
trigger.focus({ preventScroll })
// Only show focus ring if last interaction was keyboard (like native :focus-visible)
if (lastInteractionRef.current === 'keyboard') {
trigger.setAttribute('data-focus-visible', '')
// Remove focus ring when focus moves to another element
const handleFocusChange = (e: FocusEvent) => {
if (e.target !== trigger && document.contains(trigger)) {
trigger.removeAttribute('data-focus-visible')
}
}
document.addEventListener('focusin', handleFocusChange, {
once: true,
})
// Store cleanup for unmount case
cleanupRef.current = () => {
if (document.contains(trigger)) {
trigger.removeAttribute('data-focus-visible')
}
}
}
}
if (restoreFocusRaf) requestAnimationFrame(focus)
else focus()
}
@@ -50,6 +84,14 @@ export function useRestoreFocus(
}
prevIsOpenRef.current = isOpen
// Cleanup: remove focus ring if component unmounts before focus changes
return () => {
document.removeEventListener('keydown', handleKeyDown)
document.removeEventListener('mousedown', handleMouseDown)
cleanupRef.current?.()
cleanupRef.current = null
}
}, [
isOpen,
onClosed,
+3 -1
View File
@@ -25,7 +25,9 @@ body,
[data-rac][data-focus-visible]:not(label, .react-aria-Select),
:is(a, button, input[type='text'], select, textarea):not(
[data-rac]
):focus-visible {
):focus-visible,
/* Show focus ring when data-focus-visible is set programmatically (e.g., when restoring focus) */
[data-focus-visible]:is(a, button, input[type='text'], select, textarea):focus {
outline: 2px solid var(--colors-focus-ring);
outline-offset: 1px;
}