Files
ashutoshpythoncs@gmail.com b564857c0b claude-code
2026-03-31 18:58:05 +05:30

73 lines
1.9 KiB
TypeScript

"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<HTMLDivElement>(null);
useEffect(() => {
if (!active) return;
const container = containerRef.current;
if (!container) return;
const focusable = () =>
Array.from(container.querySelectorAll<HTMLElement>(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 <div ref={containerRef}>{children}</div>;
}