@supunlakmal/hooks
Version:
A collection of reusable React hooks
92 lines • 4.3 kB
JavaScript
import { useRef, useEffect, useCallback } from 'react';
// Selectors for focusable elements
const FOCUSABLE_SELECTORS = [
'a[href]',
'button:not([disabled])',
'input:not([disabled]):not([type="hidden"])',
'select:not([disabled])',
'textarea:not([disabled])',
'[tabindex]:not([tabindex="-1"])',
'[contenteditable="true"]',
].join(', ');
/**
* Traps focus within a specified container element.
*
* @param containerRef - Ref object pointing to the container element.
* @param isActive - Boolean indicating if the focus trap should be active.
* @param initialFocusRef - Optional ref object pointing to the element that should receive focus initially.
*/
export const useFocusTrap = (containerRef, isActive, initialFocusRef) => {
const previousActiveElement = useRef(null);
const getFocusableElements = useCallback(() => {
if (!containerRef.current)
return [];
// Query all potentially focusable elements within the container
return Array.from(containerRef.current.querySelectorAll(FOCUSABLE_SELECTORS)).filter((el) => el.offsetParent !== null); // Ensure they are visible/rendered
}, [containerRef]);
const handleKeyDown = useCallback((event) => {
if (event.key !== 'Tab' || !isActive || !containerRef.current)
return;
const focusableElements = getFocusableElements();
if (focusableElements.length === 0)
return;
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
const currentFocusedElement = document.activeElement;
if (event.shiftKey) {
// Shift + Tab: Move focus backwards
if (currentFocusedElement === firstElement ||
!containerRef.current.contains(currentFocusedElement)) {
// If focus is on the first element or outside the trap, wrap to the last
event.preventDefault();
lastElement.focus();
}
// Otherwise, allow default browser behavior (tabbing within the trap)
}
else {
// Tab: Move focus forwards
if (currentFocusedElement === lastElement ||
!containerRef.current.contains(currentFocusedElement)) {
// If focus is on the last element or outside the trap, wrap to the first
event.preventDefault();
firstElement.focus();
}
// Otherwise, allow default browser behavior (tabbing within the trap)
}
}, [isActive, containerRef, getFocusableElements]);
useEffect(() => {
var _a;
if (isActive && containerRef.current) {
// Save the element that was focused before the trap became active
previousActiveElement.current = document.activeElement;
// Focus the initial element if provided, otherwise the first focusable element
const targetFocusElement = (_a = initialFocusRef === null || initialFocusRef === void 0 ? void 0 : initialFocusRef.current) !== null && _a !== void 0 ? _a : getFocusableElements()[0];
targetFocusElement === null || targetFocusElement === void 0 ? void 0 : targetFocusElement.focus();
document.addEventListener('keydown', handleKeyDown);
}
else {
// If deactivated, remove the listener
document.removeEventListener('keydown', handleKeyDown);
// Restore focus to the previously active element
if (previousActiveElement.current instanceof HTMLElement) {
previousActiveElement.current.focus();
}
previousActiveElement.current = null; // Clear the ref
}
// Cleanup function
return () => {
document.removeEventListener('keydown', handleKeyDown);
// Ensure focus is restored if component unmounts while trap is active
if (isActive && previousActiveElement.current instanceof HTMLElement) {
previousActiveElement.current.focus();
}
};
}, [
isActive,
containerRef,
initialFocusRef,
handleKeyDown,
getFocusableElements,
]);
};
//# sourceMappingURL=useFocusTrap.js.map