64 lines
1.8 KiB
TypeScript
64 lines
1.8 KiB
TypeScript
import { useEffect, RefObject } from 'react';
|
|
|
|
/**
|
|
* Hook to trap focus within a container element
|
|
* Useful for modals, drawers, and overlays
|
|
*
|
|
* @param containerRef - Reference to the container element
|
|
* @param isActive - Whether the focus trap is active
|
|
*/
|
|
export function useFocusTrap(
|
|
containerRef: RefObject<HTMLElement>,
|
|
isActive: boolean
|
|
) {
|
|
useEffect(() => {
|
|
if (!isActive || !containerRef.current) return;
|
|
|
|
const container = containerRef.current;
|
|
|
|
// Get all focusable elements
|
|
const focusableElements = container.querySelectorAll<HTMLElement>(
|
|
'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
|
|
);
|
|
|
|
if (focusableElements.length === 0) return;
|
|
|
|
const firstElement = focusableElements[0];
|
|
const lastElement = focusableElements[focusableElements.length - 1];
|
|
|
|
// Store the element that had focus before the trap
|
|
const previouslyFocusedElement = document.activeElement as HTMLElement;
|
|
|
|
// Focus the first element
|
|
firstElement.focus();
|
|
|
|
const handleTabKey = (e: KeyboardEvent) => {
|
|
if (e.key !== 'Tab') return;
|
|
|
|
if (e.shiftKey) {
|
|
// Shift + Tab
|
|
if (document.activeElement === firstElement) {
|
|
e.preventDefault();
|
|
lastElement.focus();
|
|
}
|
|
} else {
|
|
// Tab
|
|
if (document.activeElement === lastElement) {
|
|
e.preventDefault();
|
|
firstElement.focus();
|
|
}
|
|
}
|
|
};
|
|
|
|
container.addEventListener('keydown', handleTabKey);
|
|
|
|
// Cleanup: restore focus to previously focused element
|
|
return () => {
|
|
container.removeEventListener('keydown', handleTabKey);
|
|
if (previouslyFocusedElement) {
|
|
previouslyFocusedElement.focus();
|
|
}
|
|
};
|
|
}, [containerRef, isActive]);
|
|
}
|