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.

192 lines (189 loc) 7.7 kB
"use strict"; var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default; Object.defineProperty(exports, "__esModule", { value: true }); exports.getDelay = getDelay; exports.useHoverFloatingInteraction = useHoverFloatingInteraction; var React = _interopRequireWildcard(require("react")); var _dom = require("@floating-ui/utils/dom"); var _useStableCallback = require("@base-ui-components/utils/useStableCallback"); var _useIsoLayoutEffect = require("@base-ui-components/utils/useIsoLayoutEffect"); var _utils = require("../utils"); var _createBaseUIEventDetails = require("../../utils/createBaseUIEventDetails"); var _reasons = require("../../utils/reasons"); var _FloatingTree = require("../components/FloatingTree"); var _useHoverInteractionSharedState = require("./useHoverInteractionSharedState"); const clickLikeEvents = new Set(['click', 'mousedown']); /** * Provides hover interactions that should be attached to the floating element. */ function useHoverFloatingInteraction(context, parameters = {}) { const store = 'rootStore' in context ? context.rootStore : context; const open = store.useState('open'); const floatingElement = store.useState('floatingElement'); const domReferenceElement = store.useState('domReferenceElement'); const { dataRef } = store.context; const { enabled = true, closeDelay: closeDelayProp = 0, externalTree } = parameters; const { pointerTypeRef, interactedInsideRef, handlerRef, performedPointerEventsMutationRef, unbindMouseMoveRef, restTimeoutPendingRef, openChangeTimeout: openChangeTimeout, handleCloseOptionsRef } = (0, _useHoverInteractionSharedState.useHoverInteractionSharedState)(store); const tree = (0, _FloatingTree.useFloatingTree)(externalTree); const parentId = (0, _FloatingTree.useFloatingParentNodeId)(); const isClickLikeOpenEvent = (0, _useStableCallback.useStableCallback)(() => { if (interactedInsideRef.current) { return true; } return dataRef.current.openEvent ? clickLikeEvents.has(dataRef.current.openEvent.type) : false; }); const isHoverOpen = (0, _useStableCallback.useStableCallback)(() => { const type = dataRef.current.openEvent?.type; return type?.includes('mouse') && type !== 'mousedown'; }); const closeWithDelay = React.useCallback((event, runElseBranch = true) => { const closeDelay = getDelay(closeDelayProp, pointerTypeRef.current); if (closeDelay && !handlerRef.current) { openChangeTimeout.start(closeDelay, () => store.setOpen(false, (0, _createBaseUIEventDetails.createChangeEventDetails)(_reasons.REASONS.triggerHover, event))); } else if (runElseBranch) { openChangeTimeout.clear(); store.setOpen(false, (0, _createBaseUIEventDetails.createChangeEventDetails)(_reasons.REASONS.triggerHover, event)); } }, [closeDelayProp, handlerRef, store, pointerTypeRef, openChangeTimeout]); const cleanupMouseMoveHandler = (0, _useStableCallback.useStableCallback)(() => { unbindMouseMoveRef.current(); handlerRef.current = undefined; }); const clearPointerEvents = (0, _useStableCallback.useStableCallback)(() => { if (performedPointerEventsMutationRef.current) { const body = (0, _utils.getDocument)(floatingElement).body; body.style.pointerEvents = ''; body.removeAttribute(_useHoverInteractionSharedState.safePolygonIdentifier); performedPointerEventsMutationRef.current = false; } }); const handleInteractInside = (0, _useStableCallback.useStableCallback)(event => { const target = (0, _utils.getTarget)(event); if (!(0, _useHoverInteractionSharedState.isInteractiveElement)(target)) { interactedInsideRef.current = false; return; } interactedInsideRef.current = true; }); (0, _useIsoLayoutEffect.useIsoLayoutEffect)(() => { if (!open) { pointerTypeRef.current = undefined; restTimeoutPendingRef.current = false; interactedInsideRef.current = false; cleanupMouseMoveHandler(); clearPointerEvents(); } }, [open, pointerTypeRef, restTimeoutPendingRef, interactedInsideRef, cleanupMouseMoveHandler, clearPointerEvents]); React.useEffect(() => { return () => { cleanupMouseMoveHandler(); }; }, [cleanupMouseMoveHandler]); React.useEffect(() => { return clearPointerEvents; }, [clearPointerEvents]); (0, _useIsoLayoutEffect.useIsoLayoutEffect)(() => { if (!enabled) { return undefined; } if (open && handleCloseOptionsRef.current?.blockPointerEvents && isHoverOpen() && (0, _dom.isElement)(domReferenceElement) && floatingElement) { performedPointerEventsMutationRef.current = true; const body = (0, _utils.getDocument)(floatingElement).body; body.setAttribute(_useHoverInteractionSharedState.safePolygonIdentifier, ''); const ref = domReferenceElement; const floatingEl = floatingElement; const parentFloating = tree?.nodesRef.current.find(node => node.id === parentId)?.context?.elements.floating; if (parentFloating) { parentFloating.style.pointerEvents = ''; } body.style.pointerEvents = 'none'; ref.style.pointerEvents = 'auto'; floatingEl.style.pointerEvents = 'auto'; return () => { body.style.pointerEvents = ''; ref.style.pointerEvents = ''; floatingEl.style.pointerEvents = ''; }; } return undefined; }, [enabled, open, domReferenceElement, floatingElement, handleCloseOptionsRef, isHoverOpen, tree, parentId, performedPointerEventsMutationRef]); React.useEffect(() => { if (!enabled) { return undefined; } // Ensure the floating element closes after scrolling even if the pointer // did not move. // https://github.com/floating-ui/floating-ui/discussions/1692 function onScrollMouseLeave(event) { if (isClickLikeOpenEvent()) { return; } if (!dataRef.current.floatingContext) { return; } const triggerElements = store.context.triggerElements; if (event.relatedTarget && triggerElements.hasElement(event.relatedTarget)) { // If the mouse is leaving the reference element to another trigger, don't explicitly close the popup // as it will be moved. return; } clearPointerEvents(); cleanupMouseMoveHandler(); if (!isClickLikeOpenEvent()) { closeWithDelay(event); } } function onFloatingMouseEnter(event) { openChangeTimeout.clear(); clearPointerEvents(); handlerRef.current?.(event); cleanupMouseMoveHandler(); } function onFloatingMouseLeave(event) { if (!isClickLikeOpenEvent()) { closeWithDelay(event, false); } } const floating = floatingElement; if (floating) { floating.addEventListener('mouseleave', onScrollMouseLeave); floating.addEventListener('mouseenter', onFloatingMouseEnter); floating.addEventListener('mouseleave', onFloatingMouseLeave); floating.addEventListener('pointerdown', handleInteractInside, true); } return () => { if (floating) { floating.removeEventListener('mouseleave', onScrollMouseLeave); floating.removeEventListener('mouseenter', onFloatingMouseEnter); floating.removeEventListener('mouseleave', onFloatingMouseLeave); floating.removeEventListener('pointerdown', handleInteractInside, true); } }; }); } function getDelay(value, pointerType) { if (pointerType && !(0, _utils.isMouseLikePointerType)(pointerType)) { return 0; } if (typeof value === 'function') { return value(); } return value; }