@nex-ui/react
Version:
🎉 A beautiful, modern, and reliable React component library.
119 lines (115 loc) • 4.34 kB
JavaScript
;
var jsxRuntime = require('react/jsx-runtime');
var utils = require('@nex-ui/utils');
var react = require('react');
var hooks = require('@nex-ui/hooks');
var getTabbable = require('./getTabbable.cjs');
const FocusTrap = ({ children, active, paused, restoreFocus = true })=>{
const rootRef = react.useRef(null);
const sentinelStartRef = react.useRef(null);
const sentinelEndRef = react.useRef(null);
const lastKeydownRef = react.useRef(null);
const pausedRef = hooks.useLatest(paused);
const restoredNode = react.useRef(null);
const ignoreNextFocus = react.useRef(false);
const mergedRefs = utils.mergeRefs(rootRef, children?.props.ref);
react.useEffect(()=>{
if (!active || !rootRef.current) {
return;
}
if (!rootRef.current.contains(document.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
]);
react.useEffect(()=>{
if (!active || !rootRef.current) {
return;
}
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(document.activeElement)) return;
if (document.activeElement !== sentinelEndRef.current && document.activeElement !== sentinelStartRef.current) {
return;
}
const tabbable = getTabbable.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 = utils.addEventListener(document.body, 'focus', trapFocus, true);
const removeKeydownEventListener = utils.addEventListener(document.body, 'keydown', (e)=>{
lastKeydownRef.current = e;
}, true);
return ()=>{
removeFocusEventListener();
removeKeydownEventListener();
};
}, [
active,
pausedRef
]);
if (!/*#__PURE__*/ react.isValidElement(children)) {
return null;
}
if (utils.__DEV__ && children.type === react.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__*/ jsxRuntime.jsxs(jsxRuntime.Fragment, {
children: [
/*#__PURE__*/ jsxRuntime.jsx("div", {
tabIndex: active && !paused ? 0 : -1,
ref: sentinelStartRef
}),
/*#__PURE__*/ react.cloneElement(children, {
...children.props,
ref: mergedRefs,
onFocus: handleFocus
}),
/*#__PURE__*/ jsxRuntime.jsx("div", {
tabIndex: active && !paused ? 0 : -1,
ref: sentinelEndRef
})
]
});
};
FocusTrap.displayName = 'FocusTrap';
exports.FocusTrap = FocusTrap;