@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
JavaScript
;
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]);
}