"use client"; import { useEffect, useRef, type ReactNode } from "react"; interface FocusTrapProps { children: ReactNode; /** When false, the trap is inactive (e.g. when the panel is hidden) */ active?: boolean; } const FOCUSABLE_SELECTORS = [ "a[href]", "button:not([disabled])", "input:not([disabled])", "select:not([disabled])", "textarea:not([disabled])", '[tabindex]:not([tabindex="-1"])', ].join(", "); /** * Traps keyboard focus within its children when `active` is true. * Use for modals, drawers, and other overlay patterns. * Note: Radix Dialog handles focus trapping natively — use this only for * custom overlay components that don't use Radix primitives. */ export function FocusTrap({ children, active = true }: FocusTrapProps) { const containerRef = useRef(null); useEffect(() => { if (!active) return; const container = containerRef.current; if (!container) return; const focusable = () => Array.from(container.querySelectorAll(FOCUSABLE_SELECTORS)).filter( (el) => !el.closest("[aria-hidden='true']") ); const handleKeyDown = (e: KeyboardEvent) => { if (e.key !== "Tab") return; const els = focusable(); if (els.length === 0) return; const first = els[0]; const last = els[els.length - 1]; if (e.shiftKey) { if (document.activeElement === first) { e.preventDefault(); last.focus(); } } else { if (document.activeElement === last) { e.preventDefault(); first.focus(); } } }; // Move focus into the trap on mount const els = focusable(); if (els.length > 0 && !container.contains(document.activeElement)) { els[0].focus(); } document.addEventListener("keydown", handleKeyDown); return () => document.removeEventListener("keydown", handleKeyDown); }, [active]); return
{children}
; }