diff --git a/src/components/onboarding/tour.tsx b/src/components/onboarding/tour.tsx index 5e46b4a3b..c2de9a77a 100644 --- a/src/components/onboarding/tour.tsx +++ b/src/components/onboarding/tour.tsx @@ -225,13 +225,28 @@ export function OnboardingTour({ }, [stop]); // Focus management — moves focus to the primary action when the - // step changes so keyboard users can hit Enter to advance. + // step changes so keyboard users can hit Enter to advance. The + // tooltip ref + `previousFocus` carry the focus-trap contract: tab + // cycles inside the tooltip, and on close the originally-focused + // element (e.g. the Settings → Account "Restart" button) regains + // focus. const primaryRef = useRef(null); + const tooltipRef = useRef(null); + const previousFocusRef = useRef(null); + useEffect(() => { if (!stop) return; primaryRef.current?.focus(); }, [stop]); + useEffect(() => { + if (typeof document === "undefined") return; + previousFocusRef.current = document.activeElement as HTMLElement | null; + return () => { + previousFocusRef.current?.focus?.(); + }; + }, []); + // Keyboard shortcuts: Esc = skip, ArrowLeft / ArrowRight = nav, // Enter = next/done. We attach to window so the listener works // regardless of which element holds focus inside the tooltip. @@ -268,6 +283,29 @@ export function OnboardingTour({ if (e.key === "Escape") { e.preventDefault(); handleSkip(); + } else if (e.key === "Tab") { + // Trap focus inside the tooltip so the user can't tab onto the + // dimmed page underneath while the tour is active. + const root = tooltipRef.current; + if (!root) return; + const focusables = Array.from( + root.querySelectorAll( + 'button:not([disabled]), [href], [tabindex]:not([tabindex="-1"])', + ), + ); + if (focusables.length === 0) return; + const first = focusables[0]; + const last = focusables[focusables.length - 1]; + const active = document.activeElement as HTMLElement | null; + if (e.shiftKey) { + if (active === first || !root.contains(active)) { + e.preventDefault(); + last.focus(); + } + } else if (active === last) { + e.preventDefault(); + first.focus(); + } } else if (e.key === "ArrowRight" || e.key === "Enter") { // Don't hijack Enter inside form inputs (none exist in the // tour itself but be defensive against future content). @@ -328,7 +366,7 @@ export function OnboardingTour({