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.

374 lines (366 loc) 13.4 kB
"use strict"; var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default; Object.defineProperty(exports, "__esModule", { value: true }); exports.getDelay = getDelay; exports.useHover = useHover; var React = _interopRequireWildcard(require("react")); var _dom = require("@floating-ui/utils/dom"); var _useTimeout = require("@base-ui-components/utils/useTimeout"); var _useLatestRef = require("@base-ui-components/utils/useLatestRef"); var _useEventCallback = require("@base-ui-components/utils/useEventCallback"); var _useIsoLayoutEffect = require("@base-ui-components/utils/useIsoLayoutEffect"); var _utils = require("../utils"); var _FloatingTree = require("../components/FloatingTree"); var _createAttribute = require("../utils/createAttribute"); const safePolygonIdentifier = (0, _createAttribute.createAttribute)('safe-polygon'); function getDelay(value, prop, pointerType) { if (pointerType && !(0, _utils.isMouseLikePointerType)(pointerType)) { return 0; } if (typeof value === 'number') { return value; } if (typeof value === 'function') { const result = value(); if (typeof result === 'number') { return result; } return result?.[prop]; } return value?.[prop]; } function getRestMs(value) { if (typeof value === 'function') { return value(); } return value; } /** * Opens the floating element while hovering over the reference element, like * CSS `:hover`. * @see https://floating-ui.com/docs/useHover */ function useHover(context, props = {}) { const { open, onOpenChange, dataRef, events, elements } = context; const { enabled = true, delay = 0, handleClose = null, mouseOnly = false, restMs = 0, move = true } = props; const tree = (0, _FloatingTree.useFloatingTree)(); const parentId = (0, _FloatingTree.useFloatingParentNodeId)(); const handleCloseRef = (0, _useLatestRef.useLatestRef)(handleClose); const delayRef = (0, _useLatestRef.useLatestRef)(delay); const openRef = (0, _useLatestRef.useLatestRef)(open); const restMsRef = (0, _useLatestRef.useLatestRef)(restMs); const pointerTypeRef = React.useRef(undefined); const timeout = (0, _useTimeout.useTimeout)(); const handlerRef = React.useRef(undefined); const restTimeout = (0, _useTimeout.useTimeout)(); const blockMouseMoveRef = React.useRef(true); const performedPointerEventsMutationRef = React.useRef(false); const unbindMouseMoveRef = React.useRef(() => {}); const restTimeoutPendingRef = React.useRef(false); const isHoverOpen = (0, _useEventCallback.useEventCallback)(() => { const type = dataRef.current.openEvent?.type; return type?.includes('mouse') && type !== 'mousedown'; }); // When closing before opening, clear the delay timeouts to cancel it // from showing. React.useEffect(() => { if (!enabled) { return undefined; } function onOpenChangeLocal({ open: newOpen }) { if (!newOpen) { timeout.clear(); restTimeout.clear(); blockMouseMoveRef.current = true; restTimeoutPendingRef.current = false; } } events.on('openchange', onOpenChangeLocal); return () => { events.off('openchange', onOpenChangeLocal); }; }, [enabled, events, timeout, restTimeout]); React.useEffect(() => { if (!enabled) { return undefined; } if (!handleCloseRef.current) { return undefined; } if (!open) { return undefined; } function onLeave(event) { if (isHoverOpen()) { onOpenChange(false, event, 'hover'); } } const html = (0, _utils.getDocument)(elements.floating).documentElement; html.addEventListener('mouseleave', onLeave); return () => { html.removeEventListener('mouseleave', onLeave); }; }, [elements.floating, open, onOpenChange, enabled, handleCloseRef, isHoverOpen]); const closeWithDelay = React.useCallback((event, runElseBranch = true, reason = 'hover') => { const closeDelay = getDelay(delayRef.current, 'close', pointerTypeRef.current); if (closeDelay && !handlerRef.current) { timeout.start(closeDelay, () => onOpenChange(false, event, reason)); } else if (runElseBranch) { timeout.clear(); onOpenChange(false, event, reason); } }, [delayRef, onOpenChange, timeout]); const cleanupMouseMoveHandler = (0, _useEventCallback.useEventCallback)(() => { unbindMouseMoveRef.current(); handlerRef.current = undefined; }); const clearPointerEvents = (0, _useEventCallback.useEventCallback)(() => { if (performedPointerEventsMutationRef.current) { const body = (0, _utils.getDocument)(elements.floating).body; body.style.pointerEvents = ''; body.removeAttribute(safePolygonIdentifier); performedPointerEventsMutationRef.current = false; } }); const isClickLikeOpenEvent = (0, _useEventCallback.useEventCallback)(() => { return dataRef.current.openEvent ? ['click', 'mousedown'].includes(dataRef.current.openEvent.type) : false; }); // Registering the mouse events on the reference directly to bypass React's // delegation system. If the cursor was on a disabled element and then entered // the reference (no gap), `mouseenter` doesn't fire in the delegation system. React.useEffect(() => { if (!enabled) { return undefined; } function onReferenceMouseEnter(event) { timeout.clear(); blockMouseMoveRef.current = false; if (mouseOnly && !(0, _utils.isMouseLikePointerType)(pointerTypeRef.current) || getRestMs(restMsRef.current) > 0 && !getDelay(delayRef.current, 'open')) { return; } const openDelay = getDelay(delayRef.current, 'open', pointerTypeRef.current); if (openDelay) { timeout.start(openDelay, () => { if (!openRef.current) { onOpenChange(true, event, 'hover'); } }); } else if (!open) { onOpenChange(true, event, 'hover'); } } function onReferenceMouseLeave(event) { if (isClickLikeOpenEvent()) { clearPointerEvents(); return; } unbindMouseMoveRef.current(); const doc = (0, _utils.getDocument)(elements.floating); restTimeout.clear(); restTimeoutPendingRef.current = false; if (handleCloseRef.current && dataRef.current.floatingContext) { // Prevent clearing `onScrollMouseLeave` timeout. if (!open) { timeout.clear(); } handlerRef.current = handleCloseRef.current({ ...dataRef.current.floatingContext, tree, x: event.clientX, y: event.clientY, onClose() { clearPointerEvents(); cleanupMouseMoveHandler(); if (!isClickLikeOpenEvent()) { closeWithDelay(event, true, 'safe-polygon'); } } }); const handler = handlerRef.current; doc.addEventListener('mousemove', handler); unbindMouseMoveRef.current = () => { doc.removeEventListener('mousemove', handler); }; return; } // Allow interactivity without `safePolygon` on touch devices. With a // pointer, a short close delay is an alternative, so it should work // consistently. const shouldClose = pointerTypeRef.current === 'touch' ? !(0, _utils.contains)(elements.floating, event.relatedTarget) : true; if (shouldClose) { closeWithDelay(event); } } // 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; } handleCloseRef.current?.({ ...dataRef.current.floatingContext, tree, x: event.clientX, y: event.clientY, onClose() { clearPointerEvents(); cleanupMouseMoveHandler(); if (!isClickLikeOpenEvent()) { closeWithDelay(event); } } })(event); } function onFloatingMouseEnter() { timeout.clear(); } function onFloatingMouseLeave(event) { if (!isClickLikeOpenEvent()) { closeWithDelay(event, false); } } if ((0, _dom.isElement)(elements.domReference)) { const reference = elements.domReference; const floating = elements.floating; if (open) { reference.addEventListener('mouseleave', onScrollMouseLeave); } if (move) { reference.addEventListener('mousemove', onReferenceMouseEnter, { once: true }); } reference.addEventListener('mouseenter', onReferenceMouseEnter); reference.addEventListener('mouseleave', onReferenceMouseLeave); if (floating) { floating.addEventListener('mouseleave', onScrollMouseLeave); floating.addEventListener('mouseenter', onFloatingMouseEnter); floating.addEventListener('mouseleave', onFloatingMouseLeave); } return () => { if (open) { reference.removeEventListener('mouseleave', onScrollMouseLeave); } if (move) { reference.removeEventListener('mousemove', onReferenceMouseEnter); } reference.removeEventListener('mouseenter', onReferenceMouseEnter); reference.removeEventListener('mouseleave', onReferenceMouseLeave); if (floating) { floating.removeEventListener('mouseleave', onScrollMouseLeave); floating.removeEventListener('mouseenter', onFloatingMouseEnter); floating.removeEventListener('mouseleave', onFloatingMouseLeave); } }; } return undefined; }, [elements, enabled, context, mouseOnly, move, closeWithDelay, cleanupMouseMoveHandler, clearPointerEvents, onOpenChange, open, openRef, tree, delayRef, handleCloseRef, dataRef, isClickLikeOpenEvent, restMsRef, timeout, restTimeout]); // Block pointer-events of every element other than the reference and floating // while the floating element is open and has a `handleClose` handler. Also // handles nested floating elements. // https://github.com/floating-ui/floating-ui/issues/1722 (0, _useIsoLayoutEffect.useIsoLayoutEffect)(() => { if (!enabled) { return undefined; } // eslint-disable-next-line no-underscore-dangle if (open && handleCloseRef.current?.__options?.blockPointerEvents && isHoverOpen()) { performedPointerEventsMutationRef.current = true; const floatingEl = elements.floating; if ((0, _dom.isElement)(elements.domReference) && floatingEl) { const body = (0, _utils.getDocument)(elements.floating).body; body.setAttribute(safePolygonIdentifier, ''); const ref = elements.domReference; 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, parentId, elements, tree, handleCloseRef, isHoverOpen]); (0, _useIsoLayoutEffect.useIsoLayoutEffect)(() => { if (!open) { pointerTypeRef.current = undefined; restTimeoutPendingRef.current = false; cleanupMouseMoveHandler(); clearPointerEvents(); } }, [open, cleanupMouseMoveHandler, clearPointerEvents]); React.useEffect(() => { return () => { cleanupMouseMoveHandler(); timeout.clear(); restTimeout.clear(); clearPointerEvents(); }; }, [enabled, elements.domReference, cleanupMouseMoveHandler, clearPointerEvents, timeout, restTimeout]); const reference = React.useMemo(() => { function setPointerRef(event) { pointerTypeRef.current = event.pointerType; } return { onPointerDown: setPointerRef, onPointerEnter: setPointerRef, onMouseMove(event) { const { nativeEvent } = event; function handleMouseMove() { if (!blockMouseMoveRef.current && !openRef.current) { onOpenChange(true, nativeEvent, 'hover'); } } if (mouseOnly && !(0, _utils.isMouseLikePointerType)(pointerTypeRef.current)) { return; } if (open || getRestMs(restMsRef.current) === 0) { return; } // Ignore insignificant movements to account for tremors. if (restTimeoutPendingRef.current && event.movementX ** 2 + event.movementY ** 2 < 2) { return; } restTimeout.clear(); if (pointerTypeRef.current === 'touch') { handleMouseMove(); } else { restTimeoutPendingRef.current = true; restTimeout.start(getRestMs(restMsRef.current), handleMouseMove); } } }; }, [mouseOnly, onOpenChange, open, openRef, restMsRef, restTimeout]); return React.useMemo(() => enabled ? { reference } : {}, [enabled, reference]); }