@nex-ui/react
Version:
🎉 A beautiful, modern, and reliable React component library.
122 lines (119 loc) • 4.49 kB
JavaScript
import { jsxs, Fragment as Fragment$1, jsx } from 'react/jsx-runtime';
import { mergeRefs, ownerDocument, addEventListener, __DEV__ } from '@nex-ui/utils';
import { useRef, useEffect, isValidElement, Fragment, cloneElement } from 'react';
import { useLatest } from '@nex-ui/hooks';
import { getTabbable } from './getTabbable.mjs';
const FocusTrap = ({ children, active, paused, restoreFocus = true })=>{
const rootRef = useRef(null);
const sentinelStartRef = useRef(null);
const sentinelEndRef = useRef(null);
const lastKeydownRef = useRef(null);
const restoredNode = useRef(null);
const ignoreNextFocus = useRef(false);
const pausedRef = useLatest(paused);
const mergedRefs = mergeRefs(rootRef, children?.props.ref);
useEffect(()=>{
if (!active || !rootRef.current) {
return;
}
const doc = ownerDocument(rootRef.current);
if (!rootRef.current.contains(doc.activeElement)) {
// If the focus is not inside the focus trap, focus the root element
rootRef.current?.focus();
}
return ()=>{
const node = restoredNode.current;
if (restoreFocus && node) {
ignoreNextFocus.current = true;
node.focus();
}
};
}, [
active,
restoreFocus
]);
useEffect(()=>{
if (!active || !rootRef.current) {
return;
}
const doc = ownerDocument(rootRef.current);
const trapFocus = ()=>{
const rootElement = rootRef.current;
if (!rootElement || pausedRef.current) return;
if (ignoreNextFocus.current) {
ignoreNextFocus.current = false;
return;
}
// The focus is already inside
if (rootElement.contains(doc.activeElement)) return;
if (doc.activeElement !== sentinelEndRef.current && doc.activeElement !== sentinelStartRef.current) {
return;
}
const tabbable = getTabbable(rootElement);
// one of the sentinel nodes was focused, so move the focus
// to the first/last tabbable element inside the focus trap.
if (tabbable.length && lastKeydownRef.current) {
const shiftPressed = lastKeydownRef.current.key === 'Tab' && lastKeydownRef.current.shiftKey;
const focusNext = tabbable[0];
const focusPrevious = tabbable[tabbable.length - 1];
if (shiftPressed) {
focusPrevious.focus();
} else {
focusNext.focus();
}
} else {
// no tabbable elements in the trap focus
rootElement.focus();
}
};
const removeFocusEventListener = addEventListener(doc.body, 'focus', trapFocus, true);
const removeKeydownEventListener = addEventListener(doc.body, 'keydown', (e)=>{
if (e.key !== 'Tab' || pausedRef.current) {
return;
}
lastKeydownRef.current = e;
}, true);
return ()=>{
removeFocusEventListener();
removeKeydownEventListener();
};
}, [
active,
pausedRef
]);
if (!/*#__PURE__*/ isValidElement(children)) {
return null;
}
if (__DEV__ && children.type === Fragment) {
console.error('[Nex UI] FocusTrap: FocusTrap cannot use a Fragment as its child container.');
return null;
}
const handleFocus = (e)=>{
if (restoredNode.current === null) {
restoredNode.current = e.relatedTarget;
}
const childrenPropsHandler = children.props.onFocus;
if (childrenPropsHandler) {
childrenPropsHandler(e);
}
};
return /*#__PURE__*/ jsxs(Fragment$1, {
children: [
/*#__PURE__*/ jsx("div", {
tabIndex: active && !pausedRef.current ? 0 : -1,
ref: sentinelStartRef
}),
/*#__PURE__*/ cloneElement(children, {
...children.props,
ref: mergedRefs,
onFocus: handleFocus
}),
/*#__PURE__*/ jsx("div", {
tabIndex: active && !pausedRef.current ? 0 : -1,
ref: sentinelEndRef
})
]
});
};
FocusTrap.displayName = 'FocusTrap';
export { FocusTrap };