@coreui/react-pro
Version: 
UI Components Library for React.js
161 lines (158 loc) • 7.63 kB
JavaScript
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