UNPKG

@base-ui-components/react

Version:

Base UI is a library of headless ('unstyled') React components and low-level hooks. You gain complete control over your app's CSS and accessibility features.

149 lines (142 loc) 5.91 kB
"use strict"; var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default; Object.defineProperty(exports, "__esModule", { value: true }); exports.useFocus = useFocus; var React = _interopRequireWildcard(require("react")); var _dom = require("@floating-ui/utils/dom"); var _detectBrowser = require("@base-ui-components/utils/detectBrowser"); var _useTimeout = require("@base-ui-components/utils/useTimeout"); var _utils = require("../utils"); var _createBaseUIEventDetails = require("../../utils/createBaseUIEventDetails"); var _reasons = require("../../utils/reasons"); var _createAttribute = require("../utils/createAttribute"); const isMacSafari = _detectBrowser.isMac && _detectBrowser.isSafari; /** * Opens the floating element while the reference element has focus, like CSS * `:focus`. * @see https://floating-ui.com/docs/useFocus */ function useFocus(context, props = {}) { const store = 'rootStore' in context ? context.rootStore : context; const { events, dataRef } = store.context; const { enabled = true, visibleOnly = true } = props; const blockFocusRef = React.useRef(false); const timeout = (0, _useTimeout.useTimeout)(); const keyboardModalityRef = React.useRef(true); React.useEffect(() => { const domReference = store.select('domReferenceElement'); if (!enabled) { return undefined; } const win = (0, _dom.getWindow)(domReference); // If the reference was focused and the user left the tab/window, and the // floating element was not open, the focus should be blocked when they // return to the tab/window. function onBlur() { if (!store.select('open') && (0, _dom.isHTMLElement)(domReference) && domReference === (0, _utils.activeElement)((0, _utils.getDocument)(domReference))) { blockFocusRef.current = true; } } function onKeyDown() { keyboardModalityRef.current = true; } function onPointerDown() { keyboardModalityRef.current = false; } win.addEventListener('blur', onBlur); if (isMacSafari) { win.addEventListener('keydown', onKeyDown, true); win.addEventListener('pointerdown', onPointerDown, true); } return () => { win.removeEventListener('blur', onBlur); if (isMacSafari) { win.removeEventListener('keydown', onKeyDown, true); win.removeEventListener('pointerdown', onPointerDown, true); } }; }, [store, enabled]); React.useEffect(() => { if (!enabled) { return undefined; } function onOpenChangeLocal(details) { if (details.reason === _reasons.REASONS.triggerPress || details.reason === _reasons.REASONS.escapeKey) { blockFocusRef.current = true; } } events.on('openchange', onOpenChangeLocal); return () => { events.off('openchange', onOpenChangeLocal); }; }, [events, enabled]); const reference = React.useMemo(() => ({ onMouseLeave() { blockFocusRef.current = false; }, onFocus(event) { if (blockFocusRef.current) { return; } const target = (0, _utils.getTarget)(event.nativeEvent); if (visibleOnly && (0, _dom.isElement)(target)) { // Safari fails to match `:focus-visible` if focus was initially // outside the document. if (isMacSafari && !event.relatedTarget) { if (!keyboardModalityRef.current && !(0, _utils.isTypeableElement)(target)) { return; } } else if (!(0, _utils.matchesFocusVisible)(target)) { return; } } store.setOpen(true, (0, _createBaseUIEventDetails.createChangeEventDetails)(_reasons.REASONS.triggerFocus, event.nativeEvent, event.currentTarget)); }, onBlur(event) { blockFocusRef.current = false; const relatedTarget = event.relatedTarget; const nativeEvent = event.nativeEvent; // Hit the non-modal focus management portal guard. Focus will be // moved into the floating element immediately after. const movedToFocusGuard = (0, _dom.isElement)(relatedTarget) && relatedTarget.hasAttribute((0, _createAttribute.createAttribute)('focus-guard')) && relatedTarget.getAttribute('data-type') === 'outside'; // Wait for the window blur listener to fire. timeout.start(0, () => { const domReference = store.select('domReferenceElement'); const activeEl = (0, _utils.activeElement)(domReference ? domReference.ownerDocument : document); // Focus left the page, keep it open. if (!relatedTarget && activeEl === domReference) { return; } // When focusing the reference element (e.g. regular click), then // clicking into the floating element, prevent it from hiding. // Note: it must be focusable, e.g. `tabindex="-1"`. // We can not rely on relatedTarget to point to the correct element // as it will only point to the shadow host of the newly focused element // and not the element that actually has received focus if it is located // inside a shadow root. if ((0, _utils.contains)(dataRef.current.floatingContext?.refs.floating.current, activeEl) || (0, _utils.contains)(domReference, activeEl) || movedToFocusGuard) { return; } // If the next focused element is one of the triggers, do not close // the floating element. The focus handler of that trigger will // handle the open state. if (store.context.triggerElements.hasElement(event.relatedTarget)) { return; } store.setOpen(false, (0, _createBaseUIEventDetails.createChangeEventDetails)(_reasons.REASONS.triggerFocus, nativeEvent)); }); } }), [dataRef, store, visibleOnly, timeout]); return React.useMemo(() => enabled ? { reference, trigger: reference } : {}, [enabled, reference]); }