UNPKG

@nex-ui/react

Version:

🎉 A beautiful, modern, and reliable React component library.

122 lines (119 loc) • 4.49 kB
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 };