@coreui/react-pro
Version:
UI Components Library for React.js
178 lines (175 loc) • 8.5 kB
JavaScript
import React, { useRef, useEffect, cloneElement } from 'react';
import { focusableChildren, getChildRef, 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 });
return;
}
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';
const containerElements = focusableChildren(container);
const additionalElements = _additionalContainer ? focusableChildren(_additionalContainer) : [];
if (containerElements.length === 0 && additionalElements.length === 0) {
// No focusable elements, prevent tab
event.preventDefault();
return;
}
const focusableElements = [...containerElements, ...additionalElements];
const firstFocusableElement = focusableElements[0];
const lastFocusableElement = focusableElements.at(-1);
const activeElement = document.activeElement;
if (event.shiftKey && activeElement === firstFocusableElement) {
event.preventDefault();
lastFocusableElement.focus();
return;
}
if (!event.shiftKey && activeElement === lastFocusableElement) {
event.preventDefault();
firstFocusableElement.focus();
return;
}
if (!_additionalContainer) {
return;
}
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);
// Handle different ref access patterns between React versions
// React 19+: ref is accessed via element.props.ref
// React 18 and earlier: ref is accessed via element.ref
const childRef = getChildRef(onlyChild);
const mergedRef = mergeRefs(childRef, (node) => {
containerRef.current = node;
});
return cloneElement(onlyChild, { ref: mergedRef });
};
export { CFocusTrap };
//# sourceMappingURL=CFocusTrap.js.map