UNPKG

@coreui/react-pro

Version:

UI Components Library for React.js

161 lines (158 loc) 7.63 kB
import React, { useRef, useEffect, cloneElement } from 'react'; import { focusableChildren, mergeRefs } from './utils.js'; const CFocusTrap = ({ active = true, additionalContainer, children, focusFirstElement = false, onActivate, onDeactivate, restoreFocus = true, }) => { const containerRef = useRef(null); const prevFocusedRef = useRef(null); const isActiveRef = useRef(false); const lastTabNavDirectionRef = useRef('forward'); const tabEventSourceRef = useRef(null); useEffect(() => { var _a; const container = containerRef.current; const _additionalContainer = (additionalContainer === null || additionalContainer === void 0 ? void 0 : additionalContainer.current) || null; if (!active || !container) { if (isActiveRef.current) { // Deactivate cleanup if (restoreFocus && ((_a = prevFocusedRef.current) === null || _a === void 0 ? void 0 : _a.isConnected)) { prevFocusedRef.current.focus({ preventScroll: true }); } onDeactivate === null || onDeactivate === void 0 ? void 0 : onDeactivate(); isActiveRef.current = false; prevFocusedRef.current = null; } return; } // Remember focused element BEFORE we move focus into the trap prevFocusedRef.current = document.activeElement; // Activating… isActiveRef.current = true; // Set initial focus if (focusFirstElement) { const elements = focusableChildren(container); if (elements.length > 0) { elements[0].focus({ preventScroll: true }); } else { // Fallback to container if no focusable elements container.focus({ preventScroll: true }); } } else { container.focus({ preventScroll: true }); } onActivate === null || onActivate === void 0 ? void 0 : onActivate(); const handleFocusIn = (event) => { var _a; // Only handle focus events from tab navigation if (containerRef.current !== tabEventSourceRef.current) { return; } const target = event.target; // Allow focus within container if (target === document || target === container || container.contains(target)) { return; } // Allow focus within additional elements if (_additionalContainer && (target === _additionalContainer || _additionalContainer.contains(target))) { return; } // Focus escaped, bring it back const elements = focusableChildren(container); if (elements.length === 0) { container.focus({ preventScroll: true }); } else if (lastTabNavDirectionRef.current === 'backward') { (_a = elements.at(-1)) === null || _a === void 0 ? void 0 : _a.focus({ preventScroll: true }); } else { elements[0].focus({ preventScroll: true }); } }; const handleKeyDown = (event) => { var _a, _b; if (event.key !== 'Tab') { return; } tabEventSourceRef.current = container; lastTabNavDirectionRef.current = event.shiftKey ? 'backward' : 'forward'; if (!_additionalContainer) { return; } const containerElements = focusableChildren(container); const additionalElements = focusableChildren(_additionalContainer); if (containerElements.length === 0 && additionalElements.length === 0) { // No focusable elements, prevent tab event.preventDefault(); return; } const activeElement = document.activeElement; const isInContainer = containerElements.includes(activeElement); const isInAdditional = additionalElements.includes(activeElement); // Handle tab navigation between container and additional elements if (isInContainer) { const index = containerElements.indexOf(activeElement); if (!event.shiftKey && index === containerElements.length - 1 && additionalElements.length > 0) { // Tab forward from last container element to first additional element event.preventDefault(); additionalElements[0].focus({ preventScroll: true }); } else if (event.shiftKey && index === 0 && additionalElements.length > 0) { // Tab backward from first container element to last additional element event.preventDefault(); (_a = additionalElements.at(-1)) === null || _a === void 0 ? void 0 : _a.focus({ preventScroll: true }); } } else if (isInAdditional) { const index = additionalElements.indexOf(activeElement); if (!event.shiftKey && index === additionalElements.length - 1 && containerElements.length > 0) { // Tab forward from last additional element to first container element event.preventDefault(); containerElements[0].focus({ preventScroll: true }); } else if (event.shiftKey && index === 0 && containerElements.length > 0) { // Tab backward from first additional element to last container element event.preventDefault(); (_b = containerElements.at(-1)) === null || _b === void 0 ? void 0 : _b.focus({ preventScroll: true }); } } }; // Add event listeners container.addEventListener('keydown', handleKeyDown, true); if (_additionalContainer) { _additionalContainer.addEventListener('keydown', handleKeyDown, true); } document.addEventListener('focusin', handleFocusIn, true); // Cleanup function return () => { var _a; container.removeEventListener('keydown', handleKeyDown, true); if (_additionalContainer) { _additionalContainer.removeEventListener('keydown', handleKeyDown, true); } document.removeEventListener('focusin', handleFocusIn, true); // On unmount (also considered deactivation) if (restoreFocus && ((_a = prevFocusedRef.current) === null || _a === void 0 ? void 0 : _a.isConnected)) { prevFocusedRef.current.focus({ preventScroll: true }); } if (isActiveRef.current) { onDeactivate === null || onDeactivate === void 0 ? void 0 : onDeactivate(); isActiveRef.current = false; } prevFocusedRef.current = null; }; }, [active, additionalContainer, focusFirstElement, onActivate, onDeactivate, restoreFocus]); // Attach our ref to the ONLY child — no extra wrappers const onlyChild = React.Children.only(children); const childRef = onlyChild.ref; const mergedRef = mergeRefs(childRef, (node) => { containerRef.current = node; }); return cloneElement(onlyChild, { ref: mergedRef }); }; export { CFocusTrap }; //# sourceMappingURL=CFocusTrap.js.map