UNPKG

@floating-ui/react

Version:
1,526 lines (1,488 loc) 169 kB
import * as React from 'react'; import { useModernLayoutEffect, useEffectEvent, getMinListIndex, getMaxListIndex, createGridCellMap, isListIndexDisabled, getGridNavigatedIndex, getGridCellIndexOfCorner, getGridCellIndices, findNonDisabledListIndex, isIndexOutOfListBounds, useLatestRef, getDocument as getDocument$1, isMouseLikePointerType, contains as contains$1, isSafari, enableFocusInside, isOutsideEvent, getPreviousTabbable, getNextTabbable, disableFocusInside, isTypeableCombobox, getFloatingFocusElement, getTabbableOptions, getNodeAncestors, activeElement, getNodeChildren as getNodeChildren$1, stopEvent, getTarget as getTarget$1, isVirtualClick, isVirtualPointerEvent, getPlatform, isTypeableElement, isReactEvent, isRootElement, isEventTargetWithin, matchesFocusVisible, isMac, getDeepestNode, getUserAgent } from '@floating-ui/react/utils'; import { jsx, jsxs, Fragment } from 'react/jsx-runtime'; import { getComputedStyle, isElement, isShadowRoot, getNodeName, isHTMLElement, getWindow, isLastTraversableNode, getParentNode, isWebKit } from '@floating-ui/utils/dom'; import { tabbable, isTabbable, focusable } from 'tabbable'; import * as ReactDOM from 'react-dom'; import { getOverflowAncestors, useFloating as useFloating$1, offset, detectOverflow } from '@floating-ui/react-dom'; export { arrow, autoPlacement, autoUpdate, computePosition, detectOverflow, flip, getOverflowAncestors, hide, inline, limitShift, offset, platform, shift, size } from '@floating-ui/react-dom'; import { evaluate, max, round, min } from '@floating-ui/utils'; /** * Merges an array of refs into a single memoized callback ref or `null`. * @see https://floating-ui.com/docs/react-utils#usemergerefs */ function useMergeRefs(refs) { const cleanupRef = React.useRef(undefined); const refEffect = React.useCallback(instance => { const cleanups = refs.map(ref => { if (ref == null) { return; } if (typeof ref === 'function') { const refCallback = ref; const refCleanup = refCallback(instance); return typeof refCleanup === 'function' ? refCleanup : () => { refCallback(null); }; } ref.current = instance; return () => { ref.current = null; }; }); return () => { cleanups.forEach(refCleanup => refCleanup == null ? void 0 : refCleanup()); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, refs); return React.useMemo(() => { if (refs.every(ref => ref == null)) { return null; } return value => { if (cleanupRef.current) { cleanupRef.current(); cleanupRef.current = undefined; } if (value != null) { cleanupRef.current = refEffect(value); } }; // eslint-disable-next-line react-hooks/exhaustive-deps }, refs); } function sortByDocumentPosition(a, b) { const position = a.compareDocumentPosition(b); if (position & Node.DOCUMENT_POSITION_FOLLOWING || position & Node.DOCUMENT_POSITION_CONTAINED_BY) { return -1; } if (position & Node.DOCUMENT_POSITION_PRECEDING || position & Node.DOCUMENT_POSITION_CONTAINS) { return 1; } return 0; } const FloatingListContext = /*#__PURE__*/React.createContext({ register: () => {}, unregister: () => {}, map: /*#__PURE__*/new Map(), elementsRef: { current: [] } }); /** * Provides context for a list of items within the floating element. * @see https://floating-ui.com/docs/FloatingList */ function FloatingList(props) { const { children, elementsRef, labelsRef } = props; const [nodes, setNodes] = React.useState(() => new Set()); const register = React.useCallback(node => { setNodes(prevSet => new Set(prevSet).add(node)); }, []); const unregister = React.useCallback(node => { setNodes(prevSet => { const set = new Set(prevSet); set.delete(node); return set; }); }, []); const map = React.useMemo(() => { const newMap = new Map(); const sortedNodes = Array.from(nodes.keys()).sort(sortByDocumentPosition); sortedNodes.forEach((node, index) => { newMap.set(node, index); }); return newMap; }, [nodes]); return /*#__PURE__*/jsx(FloatingListContext.Provider, { value: React.useMemo(() => ({ register, unregister, map, elementsRef, labelsRef }), [register, unregister, map, elementsRef, labelsRef]), children: children }); } /** * Used to register a list item and its index (DOM position) in the * `FloatingList`. * @see https://floating-ui.com/docs/FloatingList#uselistitem */ function useListItem(props) { if (props === void 0) { props = {}; } const { label } = props; const { register, unregister, map, elementsRef, labelsRef } = React.useContext(FloatingListContext); const [index, setIndex] = React.useState(null); const componentRef = React.useRef(null); const ref = React.useCallback(node => { componentRef.current = node; if (index !== null) { elementsRef.current[index] = node; if (labelsRef) { var _node$textContent; const isLabelDefined = label !== undefined; labelsRef.current[index] = isLabelDefined ? label : (_node$textContent = node == null ? void 0 : node.textContent) != null ? _node$textContent : null; } } }, [index, elementsRef, labelsRef, label]); useModernLayoutEffect(() => { const node = componentRef.current; if (node) { register(node); return () => { unregister(node); }; } }, [register, unregister]); useModernLayoutEffect(() => { const index = componentRef.current ? map.get(componentRef.current) : null; if (index != null) { setIndex(index); } }, [map]); return React.useMemo(() => ({ ref, index: index == null ? -1 : index }), [index, ref]); } const FOCUSABLE_ATTRIBUTE = 'data-floating-ui-focusable'; const ACTIVE_KEY = 'active'; const SELECTED_KEY = 'selected'; const ARROW_LEFT = 'ArrowLeft'; const ARROW_RIGHT = 'ArrowRight'; const ARROW_UP = 'ArrowUp'; const ARROW_DOWN = 'ArrowDown'; function renderJsx(render, computedProps) { if (typeof render === 'function') { return render(computedProps); } if (render) { return /*#__PURE__*/React.cloneElement(render, computedProps); } return /*#__PURE__*/jsx("div", { ...computedProps }); } const CompositeContext = /*#__PURE__*/React.createContext({ activeIndex: 0, onNavigate: () => {} }); const horizontalKeys = [ARROW_LEFT, ARROW_RIGHT]; const verticalKeys = [ARROW_UP, ARROW_DOWN]; const allKeys = [...horizontalKeys, ...verticalKeys]; /** * Creates a single tab stop whose items are navigated by arrow keys, which * provides list navigation outside of floating element contexts. * * This is useful to enable navigation of a list of items that aren’t part of a * floating element. A menubar is an example of a composite, with each reference * element being an item. * @see https://floating-ui.com/docs/Composite */ const Composite = /*#__PURE__*/React.forwardRef(function Composite(props, forwardedRef) { const { render, orientation = 'both', loop = true, rtl = false, cols = 1, disabledIndices, activeIndex: externalActiveIndex, onNavigate: externalSetActiveIndex, itemSizes, dense = false, ...domProps } = props; const [internalActiveIndex, internalSetActiveIndex] = React.useState(0); const activeIndex = externalActiveIndex != null ? externalActiveIndex : internalActiveIndex; const onNavigate = useEffectEvent(externalSetActiveIndex != null ? externalSetActiveIndex : internalSetActiveIndex); const elementsRef = React.useRef([]); const renderElementProps = render && typeof render !== 'function' ? render.props : {}; const contextValue = React.useMemo(() => ({ activeIndex, onNavigate }), [activeIndex, onNavigate]); const isGrid = cols > 1; function handleKeyDown(event) { if (!allKeys.includes(event.key)) return; let nextIndex = activeIndex; const minIndex = getMinListIndex(elementsRef, disabledIndices); const maxIndex = getMaxListIndex(elementsRef, disabledIndices); const horizontalEndKey = rtl ? ARROW_LEFT : ARROW_RIGHT; const horizontalStartKey = rtl ? ARROW_RIGHT : ARROW_LEFT; if (isGrid) { const sizes = itemSizes || Array.from({ length: elementsRef.current.length }, () => ({ width: 1, height: 1 })); // To calculate movements on the grid, we use hypothetical cell indices // as if every item was 1x1, then convert back to real indices. const cellMap = createGridCellMap(sizes, cols, dense); const minGridIndex = cellMap.findIndex(index => index != null && !isListIndexDisabled(elementsRef, index, disabledIndices)); // last enabled index const maxGridIndex = cellMap.reduce((foundIndex, index, cellIndex) => index != null && !isListIndexDisabled(elementsRef, index, disabledIndices) ? cellIndex : foundIndex, -1); const maybeNextIndex = cellMap[getGridNavigatedIndex({ current: cellMap.map(itemIndex => itemIndex ? elementsRef.current[itemIndex] : null) }, { event, orientation, loop, rtl, cols, // treat undefined (empty grid spaces) as disabled indices so we // don't end up in them disabledIndices: getGridCellIndices([...((typeof disabledIndices !== 'function' ? disabledIndices : null) || elementsRef.current.map((_, index) => isListIndexDisabled(elementsRef, index, disabledIndices) ? index : undefined)), undefined], cellMap), minIndex: minGridIndex, maxIndex: maxGridIndex, prevIndex: getGridCellIndexOfCorner(activeIndex > maxIndex ? minIndex : activeIndex, sizes, cellMap, cols, // use a corner matching the edge closest to the direction we're // moving in so we don't end up in the same item. Prefer // top/left over bottom/right. event.key === ARROW_DOWN ? 'bl' : event.key === horizontalEndKey ? 'tr' : 'tl') })]; if (maybeNextIndex != null) { nextIndex = maybeNextIndex; } } const toEndKeys = { horizontal: [horizontalEndKey], vertical: [ARROW_DOWN], both: [horizontalEndKey, ARROW_DOWN] }[orientation]; const toStartKeys = { horizontal: [horizontalStartKey], vertical: [ARROW_UP], both: [horizontalStartKey, ARROW_UP] }[orientation]; const preventedKeys = isGrid ? allKeys : { horizontal: horizontalKeys, vertical: verticalKeys, both: allKeys }[orientation]; if (nextIndex === activeIndex && [...toEndKeys, ...toStartKeys].includes(event.key)) { if (loop && nextIndex === maxIndex && toEndKeys.includes(event.key)) { nextIndex = minIndex; } else if (loop && nextIndex === minIndex && toStartKeys.includes(event.key)) { nextIndex = maxIndex; } else { nextIndex = findNonDisabledListIndex(elementsRef, { startingIndex: nextIndex, decrement: toStartKeys.includes(event.key), disabledIndices }); } } if (nextIndex !== activeIndex && !isIndexOutOfListBounds(elementsRef, nextIndex)) { var _elementsRef$current$; event.stopPropagation(); if (preventedKeys.includes(event.key)) { event.preventDefault(); } onNavigate(nextIndex); (_elementsRef$current$ = elementsRef.current[nextIndex]) == null || _elementsRef$current$.focus(); } } const computedProps = { ...domProps, ...renderElementProps, ref: forwardedRef, 'aria-orientation': orientation === 'both' ? undefined : orientation, onKeyDown(e) { domProps.onKeyDown == null || domProps.onKeyDown(e); renderElementProps.onKeyDown == null || renderElementProps.onKeyDown(e); handleKeyDown(e); } }; return /*#__PURE__*/jsx(CompositeContext.Provider, { value: contextValue, children: /*#__PURE__*/jsx(FloatingList, { elementsRef: elementsRef, children: renderJsx(render, computedProps) }) }); }); /** * @see https://floating-ui.com/docs/Composite */ const CompositeItem = /*#__PURE__*/React.forwardRef(function CompositeItem(props, forwardedRef) { const { render, ...domProps } = props; const renderElementProps = render && typeof render !== 'function' ? render.props : {}; const { activeIndex, onNavigate } = React.useContext(CompositeContext); const { ref, index } = useListItem(); const mergedRef = useMergeRefs([ref, forwardedRef, renderElementProps.ref]); const isActive = activeIndex === index; const computedProps = { ...domProps, ...renderElementProps, ref: mergedRef, tabIndex: isActive ? 0 : -1, 'data-active': isActive ? '' : undefined, onFocus(e) { domProps.onFocus == null || domProps.onFocus(e); renderElementProps.onFocus == null || renderElementProps.onFocus(e); onNavigate(index); } }; return renderJsx(render, computedProps); }); // https://github.com/mui/material-ui/issues/41190#issuecomment-2040873379 const SafeReact = { ...React }; let serverHandoffComplete = false; let count = 0; const genId = () => // Ensure the id is unique with multiple independent versions of Floating UI // on <React 18 "floating-ui-" + Math.random().toString(36).slice(2, 6) + count++; function useFloatingId() { const [id, setId] = React.useState(() => serverHandoffComplete ? genId() : undefined); useModernLayoutEffect(() => { if (id == null) { setId(genId()); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); React.useEffect(() => { serverHandoffComplete = true; }, []); return id; } const useReactId = SafeReact.useId; /** * Uses React 18's built-in `useId()` when available, or falls back to a * slightly less performant (requiring a double render) implementation for * earlier React versions. * @see https://floating-ui.com/docs/react-utils#useid */ const useId = useReactId || useFloatingId; let devMessageSet; if (process.env.NODE_ENV !== "production") { devMessageSet = /*#__PURE__*/new Set(); } function warn() { var _devMessageSet; for (var _len = arguments.length, messages = new Array(_len), _key = 0; _key < _len; _key++) { messages[_key] = arguments[_key]; } const message = "Floating UI: " + messages.join(' '); if (!((_devMessageSet = devMessageSet) != null && _devMessageSet.has(message))) { var _devMessageSet2; (_devMessageSet2 = devMessageSet) == null || _devMessageSet2.add(message); console.warn(message); } } function error() { var _devMessageSet3; for (var _len2 = arguments.length, messages = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { messages[_key2] = arguments[_key2]; } const message = "Floating UI: " + messages.join(' '); if (!((_devMessageSet3 = devMessageSet) != null && _devMessageSet3.has(message))) { var _devMessageSet4; (_devMessageSet4 = devMessageSet) == null || _devMessageSet4.add(message); console.error(message); } } /** * Renders a pointing arrow triangle. * @see https://floating-ui.com/docs/FloatingArrow */ const FloatingArrow = /*#__PURE__*/React.forwardRef(function FloatingArrow(props, ref) { const { context: { placement, elements: { floating }, middlewareData: { arrow, shift } }, width = 14, height = 7, tipRadius = 0, strokeWidth = 0, staticOffset, stroke, d, style: { transform, ...restStyle } = {}, ...rest } = props; if (process.env.NODE_ENV !== "production") { if (!ref) { warn('The `ref` prop is required for `FloatingArrow`.'); } } const clipPathId = useId(); const [isRTL, setIsRTL] = React.useState(false); // https://github.com/floating-ui/floating-ui/issues/2932 useModernLayoutEffect(() => { if (!floating) return; const isRTL = getComputedStyle(floating).direction === 'rtl'; if (isRTL) { setIsRTL(true); } }, [floating]); if (!floating) { return null; } const [side, alignment] = placement.split('-'); const isVerticalSide = side === 'top' || side === 'bottom'; let computedStaticOffset = staticOffset; if (isVerticalSide && shift != null && shift.x || !isVerticalSide && shift != null && shift.y) { computedStaticOffset = null; } // Strokes must be double the border width, this ensures the stroke's width // works as you'd expect. const computedStrokeWidth = strokeWidth * 2; const halfStrokeWidth = computedStrokeWidth / 2; const svgX = width / 2 * (tipRadius / -8 + 1); const svgY = height / 2 * tipRadius / 4; const isCustomShape = !!d; const yOffsetProp = computedStaticOffset && alignment === 'end' ? 'bottom' : 'top'; let xOffsetProp = computedStaticOffset && alignment === 'end' ? 'right' : 'left'; if (computedStaticOffset && isRTL) { xOffsetProp = alignment === 'end' ? 'left' : 'right'; } const arrowX = (arrow == null ? void 0 : arrow.x) != null ? computedStaticOffset || arrow.x : ''; const arrowY = (arrow == null ? void 0 : arrow.y) != null ? computedStaticOffset || arrow.y : ''; const dValue = d || 'M0,0' + (" H" + width) + (" L" + (width - svgX) + "," + (height - svgY)) + (" Q" + width / 2 + "," + height + " " + svgX + "," + (height - svgY)) + ' Z'; const rotation = { top: isCustomShape ? 'rotate(180deg)' : '', left: isCustomShape ? 'rotate(90deg)' : 'rotate(-90deg)', bottom: isCustomShape ? '' : 'rotate(180deg)', right: isCustomShape ? 'rotate(-90deg)' : 'rotate(90deg)' }[side]; return /*#__PURE__*/jsxs("svg", { ...rest, "aria-hidden": true, ref: ref, width: isCustomShape ? width : width + computedStrokeWidth, height: width, viewBox: "0 0 " + width + " " + (height > width ? height : width), style: { position: 'absolute', pointerEvents: 'none', [xOffsetProp]: arrowX, [yOffsetProp]: arrowY, [side]: isVerticalSide || isCustomShape ? '100%' : "calc(100% - " + computedStrokeWidth / 2 + "px)", transform: [rotation, transform].filter(t => !!t).join(' '), ...restStyle }, children: [computedStrokeWidth > 0 && /*#__PURE__*/jsx("path", { clipPath: "url(#" + clipPathId + ")", fill: "none", stroke: stroke // Account for the stroke on the fill path rendered below. , strokeWidth: computedStrokeWidth + (d ? 0 : 1), d: dValue }), /*#__PURE__*/jsx("path", { stroke: computedStrokeWidth && !d ? rest.fill : 'none', d: dValue }), /*#__PURE__*/jsx("clipPath", { id: clipPathId, children: /*#__PURE__*/jsx("rect", { x: -halfStrokeWidth, y: halfStrokeWidth * (isCustomShape ? -1 : 1), width: width + computedStrokeWidth, height: width }) })] }); }); function createEventEmitter() { const map = new Map(); return { emit(event, data) { var _map$get; (_map$get = map.get(event)) == null || _map$get.forEach(listener => listener(data)); }, on(event, listener) { if (!map.has(event)) { map.set(event, new Set()); } map.get(event).add(listener); }, off(event, listener) { var _map$get2; (_map$get2 = map.get(event)) == null || _map$get2.delete(listener); } }; } const FloatingNodeContext = /*#__PURE__*/React.createContext(null); const FloatingTreeContext = /*#__PURE__*/React.createContext(null); /** * Returns the parent node id for nested floating elements, if available. * Returns `null` for top-level floating elements. */ const useFloatingParentNodeId = () => { var _React$useContext; return ((_React$useContext = React.useContext(FloatingNodeContext)) == null ? void 0 : _React$useContext.id) || null; }; /** * Returns the nearest floating tree context, if available. */ const useFloatingTree = () => React.useContext(FloatingTreeContext); /** * Registers a node into the `FloatingTree`, returning its id. * @see https://floating-ui.com/docs/FloatingTree */ function useFloatingNodeId(customParentId) { const id = useId(); const tree = useFloatingTree(); const reactParentId = useFloatingParentNodeId(); const parentId = customParentId || reactParentId; useModernLayoutEffect(() => { if (!id) return; const node = { id, parentId }; tree == null || tree.addNode(node); return () => { tree == null || tree.removeNode(node); }; }, [tree, id, parentId]); return id; } /** * Provides parent node context for nested floating elements. * @see https://floating-ui.com/docs/FloatingTree */ function FloatingNode(props) { const { children, id } = props; const parentId = useFloatingParentNodeId(); return /*#__PURE__*/jsx(FloatingNodeContext.Provider, { value: React.useMemo(() => ({ id, parentId }), [id, parentId]), children: children }); } /** * Provides context for nested floating elements when they are not children of * each other on the DOM. * This is not necessary in all cases, except when there must be explicit communication between parent and child floating elements. It is necessary for: * - The `bubbles` option in the `useDismiss()` Hook * - Nested virtual list navigation * - Nested floating elements that each open on hover * - Custom communication between parent and child floating elements * @see https://floating-ui.com/docs/FloatingTree */ function FloatingTree(props) { const { children } = props; const nodesRef = React.useRef([]); const addNode = React.useCallback(node => { nodesRef.current = [...nodesRef.current, node]; }, []); const removeNode = React.useCallback(node => { nodesRef.current = nodesRef.current.filter(n => n !== node); }, []); const [events] = React.useState(() => createEventEmitter()); return /*#__PURE__*/jsx(FloatingTreeContext.Provider, { value: React.useMemo(() => ({ nodesRef, addNode, removeNode, events }), [addNode, removeNode, events]), children: children }); } function createAttribute(name) { return "data-floating-ui-" + name; } function clearTimeoutIfSet(timeoutRef) { if (timeoutRef.current !== -1) { clearTimeout(timeoutRef.current); timeoutRef.current = -1; } } const safePolygonIdentifier = /*#__PURE__*/createAttribute('safe-polygon'); function getDelay(value, prop, pointerType) { if (pointerType && !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 == null ? void 0 : result[prop]; } return value == null ? void 0 : 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) { if (props === void 0) { props = {}; } const { open, onOpenChange, dataRef, events, elements } = context; const { enabled = true, delay = 0, handleClose = null, mouseOnly = false, restMs = 0, move = true } = props; const tree = useFloatingTree(); const parentId = useFloatingParentNodeId(); const handleCloseRef = useLatestRef(handleClose); const delayRef = useLatestRef(delay); const openRef = useLatestRef(open); const restMsRef = useLatestRef(restMs); const pointerTypeRef = React.useRef(); const timeoutRef = React.useRef(-1); const handlerRef = React.useRef(); const restTimeoutRef = React.useRef(-1); const blockMouseMoveRef = React.useRef(true); const performedPointerEventsMutationRef = React.useRef(false); const unbindMouseMoveRef = React.useRef(() => {}); const restTimeoutPendingRef = React.useRef(false); const isHoverOpen = useEffectEvent(() => { var _dataRef$current$open; const type = (_dataRef$current$open = dataRef.current.openEvent) == null ? void 0 : _dataRef$current$open.type; return (type == null ? void 0 : type.includes('mouse')) && type !== 'mousedown'; }); // When closing before opening, clear the delay timeouts to cancel it // from showing. React.useEffect(() => { if (!enabled) return; function onOpenChange(_ref) { let { open } = _ref; if (!open) { clearTimeoutIfSet(timeoutRef); clearTimeoutIfSet(restTimeoutRef); blockMouseMoveRef.current = true; restTimeoutPendingRef.current = false; } } events.on('openchange', onOpenChange); return () => { events.off('openchange', onOpenChange); }; }, [enabled, events]); React.useEffect(() => { if (!enabled) return; if (!handleCloseRef.current) return; if (!open) return; function onLeave(event) { if (isHoverOpen()) { onOpenChange(false, event, 'hover'); } } const html = getDocument$1(elements.floating).documentElement; html.addEventListener('mouseleave', onLeave); return () => { html.removeEventListener('mouseleave', onLeave); }; }, [elements.floating, open, onOpenChange, enabled, handleCloseRef, isHoverOpen]); const closeWithDelay = React.useCallback(function (event, runElseBranch, reason) { if (runElseBranch === void 0) { runElseBranch = true; } if (reason === void 0) { reason = 'hover'; } const closeDelay = getDelay(delayRef.current, 'close', pointerTypeRef.current); if (closeDelay && !handlerRef.current) { clearTimeoutIfSet(timeoutRef); timeoutRef.current = window.setTimeout(() => onOpenChange(false, event, reason), closeDelay); } else if (runElseBranch) { clearTimeoutIfSet(timeoutRef); onOpenChange(false, event, reason); } }, [delayRef, onOpenChange]); const cleanupMouseMoveHandler = useEffectEvent(() => { unbindMouseMoveRef.current(); handlerRef.current = undefined; }); const clearPointerEvents = useEffectEvent(() => { if (performedPointerEventsMutationRef.current) { const body = getDocument$1(elements.floating).body; body.style.pointerEvents = ''; body.removeAttribute(safePolygonIdentifier); performedPointerEventsMutationRef.current = false; } }); const isClickLikeOpenEvent = useEffectEvent(() => { 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; function onReferenceMouseEnter(event) { clearTimeoutIfSet(timeoutRef); blockMouseMoveRef.current = false; if (mouseOnly && !isMouseLikePointerType(pointerTypeRef.current) || getRestMs(restMsRef.current) > 0 && !getDelay(delayRef.current, 'open')) { return; } const openDelay = getDelay(delayRef.current, 'open', pointerTypeRef.current); if (openDelay) { timeoutRef.current = window.setTimeout(() => { if (!openRef.current) { onOpenChange(true, event, 'hover'); } }, openDelay); } else if (!open) { onOpenChange(true, event, 'hover'); } } function onReferenceMouseLeave(event) { if (isClickLikeOpenEvent()) { clearPointerEvents(); return; } unbindMouseMoveRef.current(); const doc = getDocument$1(elements.floating); clearTimeoutIfSet(restTimeoutRef); restTimeoutPendingRef.current = false; if (handleCloseRef.current && dataRef.current.floatingContext) { // Prevent clearing `onScrollMouseLeave` timeout. if (!open) { clearTimeoutIfSet(timeoutRef); } 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' ? !contains$1(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 == null || handleCloseRef.current({ ...dataRef.current.floatingContext, tree, x: event.clientX, y: event.clientY, onClose() { clearPointerEvents(); cleanupMouseMoveHandler(); if (!isClickLikeOpenEvent()) { closeWithDelay(event); } } })(event); } function onFloatingMouseEnter() { clearTimeoutIfSet(timeoutRef); } function onFloatingMouseLeave(event) { if (!isClickLikeOpenEvent()) { closeWithDelay(event, false); } } if (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); } }; } }, [elements, enabled, context, mouseOnly, move, closeWithDelay, cleanupMouseMoveHandler, clearPointerEvents, onOpenChange, open, openRef, tree, delayRef, handleCloseRef, dataRef, isClickLikeOpenEvent, restMsRef]); // 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 useModernLayoutEffect(() => { var _handleCloseRef$curre; if (!enabled) return; if (open && (_handleCloseRef$curre = handleCloseRef.current) != null && (_handleCloseRef$curre = _handleCloseRef$curre.__options) != null && _handleCloseRef$curre.blockPointerEvents && isHoverOpen()) { performedPointerEventsMutationRef.current = true; const floatingEl = elements.floating; if (isElement(elements.domReference) && floatingEl) { var _tree$nodesRef$curren; const body = getDocument$1(elements.floating).body; body.setAttribute(safePolygonIdentifier, ''); const ref = elements.domReference; const parentFloating = tree == null || (_tree$nodesRef$curren = tree.nodesRef.current.find(node => node.id === parentId)) == null || (_tree$nodesRef$curren = _tree$nodesRef$curren.context) == null ? void 0 : _tree$nodesRef$curren.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 = ''; }; } } }, [enabled, open, parentId, elements, tree, handleCloseRef, isHoverOpen]); useModernLayoutEffect(() => { if (!open) { pointerTypeRef.current = undefined; restTimeoutPendingRef.current = false; cleanupMouseMoveHandler(); clearPointerEvents(); } }, [open, cleanupMouseMoveHandler, clearPointerEvents]); React.useEffect(() => { return () => { cleanupMouseMoveHandler(); clearTimeoutIfSet(timeoutRef); clearTimeoutIfSet(restTimeoutRef); clearPointerEvents(); }; }, [enabled, elements.domReference, cleanupMouseMoveHandler, clearPointerEvents]); 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 && !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; } clearTimeoutIfSet(restTimeoutRef); if (pointerTypeRef.current === 'touch') { handleMouseMove(); } else { restTimeoutPendingRef.current = true; restTimeoutRef.current = window.setTimeout(handleMouseMove, getRestMs(restMsRef.current)); } } }; }, [mouseOnly, onOpenChange, open, openRef, restMsRef]); return React.useMemo(() => enabled ? { reference } : {}, [enabled, reference]); } const NOOP = () => {}; const FloatingDelayGroupContext = /*#__PURE__*/React.createContext({ delay: 0, initialDelay: 0, timeoutMs: 0, currentId: null, setCurrentId: NOOP, setState: NOOP, isInstantPhase: false }); /** * @deprecated * Use the return value of `useDelayGroup()` instead. */ const useDelayGroupContext = () => React.useContext(FloatingDelayGroupContext); /** * Provides context for a group of floating elements that should share a * `delay`. * @see https://floating-ui.com/docs/FloatingDelayGroup */ function FloatingDelayGroup(props) { const { children, delay, timeoutMs = 0 } = props; const [state, setState] = React.useReducer((prev, next) => ({ ...prev, ...next }), { delay, timeoutMs, initialDelay: delay, currentId: null, isInstantPhase: false }); const initialCurrentIdRef = React.useRef(null); const setCurrentId = React.useCallback(currentId => { setState({ currentId }); }, []); useModernLayoutEffect(() => { if (state.currentId) { if (initialCurrentIdRef.current === null) { initialCurrentIdRef.current = state.currentId; } else if (!state.isInstantPhase) { setState({ isInstantPhase: true }); } } else { if (state.isInstantPhase) { setState({ isInstantPhase: false }); } initialCurrentIdRef.current = null; } }, [state.currentId, state.isInstantPhase]); return /*#__PURE__*/jsx(FloatingDelayGroupContext.Provider, { value: React.useMemo(() => ({ ...state, setState, setCurrentId }), [state, setCurrentId]), children: children }); } /** * Enables grouping when called inside a component that's a child of a * `FloatingDelayGroup`. * @see https://floating-ui.com/docs/FloatingDelayGroup */ function useDelayGroup(context, options) { if (options === void 0) { options = {}; } const { open, onOpenChange, floatingId } = context; const { id: optionId, enabled = true } = options; const id = optionId != null ? optionId : floatingId; const groupContext = useDelayGroupContext(); const { currentId, setCurrentId, initialDelay, setState, timeoutMs } = groupContext; useModernLayoutEffect(() => { if (!enabled) return; if (!currentId) return; setState({ delay: { open: 1, close: getDelay(initialDelay, 'close') } }); if (currentId !== id) { onOpenChange(false); } }, [enabled, id, onOpenChange, setState, currentId, initialDelay]); useModernLayoutEffect(() => { function unset() { onOpenChange(false); setState({ delay: initialDelay, currentId: null }); } if (!enabled) return; if (!currentId) return; if (!open && currentId === id) { if (timeoutMs) { const timeout = window.setTimeout(unset, timeoutMs); return () => { clearTimeout(timeout); }; } unset(); } }, [enabled, open, setState, currentId, id, onOpenChange, initialDelay, timeoutMs]); useModernLayoutEffect(() => { if (!enabled) return; if (setCurrentId === NOOP || !open) return; setCurrentId(id); }, [enabled, open, setCurrentId, id]); return groupContext; } const NextFloatingDelayGroupContext = /*#__PURE__*/React.createContext({ hasProvider: false, timeoutMs: 0, delayRef: { current: 0 }, initialDelayRef: { current: 0 }, timeoutIdRef: { current: -1 }, currentIdRef: { current: null }, currentContextRef: { current: null } }); /** * Experimental next version of `FloatingDelayGroup` to become the default * in the future. This component is not yet stable. * Provides context for a group of floating elements that should share a * `delay`. Unlike `FloatingDelayGroup`, `useNextDelayGroup` with this * component does not cause a re-render of unrelated consumers of the * context when the delay changes. * @see https://floating-ui.com/docs/FloatingDelayGroup */ function NextFloatingDelayGroup(props) { const { children, delay, timeoutMs = 0 } = props; const delayRef = React.useRef(delay); const initialDelayRef = React.useRef(delay); const currentIdRef = React.useRef(null); const currentContextRef = React.useRef(null); const timeoutIdRef = React.useRef(-1); return /*#__PURE__*/jsx(NextFloatingDelayGroupContext.Provider, { value: React.useMemo(() => ({ hasProvider: true, delayRef, initialDelayRef, currentIdRef, timeoutMs, currentContextRef, timeoutIdRef }), [timeoutMs]), children: children }); } /** * Enables grouping when called inside a component that's a child of a * `NextFloatingDelayGroup`. * @see https://floating-ui.com/docs/FloatingDelayGroup */ function useNextDelayGroup(context, options) { if (options === void 0) { options = {}; } const { open, onOpenChange, floatingId } = context; const { enabled = true } = options; const groupContext = React.useContext(NextFloatingDelayGroupContext); const { currentIdRef, delayRef, timeoutMs, initialDelayRef, currentContextRef, hasProvider, timeoutIdRef } = groupContext; const [isInstantPhase, setIsInstantPhase] = React.useState(false); useModernLayoutEffect(() => { function unset() { var _currentContextRef$cu; setIsInstantPhase(false); (_currentContextRef$cu = currentContextRef.current) == null || _currentContextRef$cu.setIsInstantPhase(false); currentIdRef.current = null; currentContextRef.current = null; delayRef.current = initialDelayRef.current; } if (!enabled) return; if (!currentIdRef.current) return; if (!open && currentIdRef.current === floatingId) { setIsInstantPhase(false); if (timeoutMs) { timeoutIdRef.current = window.setTimeout(unset, timeoutMs); return () => { clearTimeout(timeoutIdRef.current); }; } unset(); } }, [enabled, open, floatingId, currentIdRef, delayRef, timeoutMs, initialDelayRef, currentContextRef, timeoutIdRef]); useModernLayoutEffect(() => { if (!enabled) return; if (!open) return; const prevContext = currentContextRef.current; const prevId = currentIdRef.current; currentContextRef.current = { onOpenChange, setIsInstantPhase }; currentIdRef.current = floatingId; delayRef.current = { open: 0, close: getDelay(initialDelayRef.current, 'close') }; if (prevId !== null && prevId !== floatingId) { clearTimeoutIfSet(timeoutIdRef); setIsInstantPhase(true); prevContext == null || prevContext.setIsInstantPhase(true); prevContext == null || prevContext.onOpenChange(false); } else { setIsInstantPhase(false); prevContext == null || prevContext.setIsInstantPhase(false); } }, [enabled, open, floatingId, onOpenChange, currentIdRef, delayRef, timeoutMs, initialDelayRef, currentContextRef, timeoutIdRef]); useModernLayoutEffect(() => { return () => { currentContextRef.current = null; }; }, [currentContextRef]); return React.useMemo(() => ({ hasProvider, delayRef, isInstantPhase }), [hasProvider, delayRef, isInstantPhase]); } let rafId = 0; function enqueueFocus(el, options) { if (options === void 0) { options = {}; } const { preventScroll = false, cancelPrevious = true, sync = false } = options; cancelPrevious && cancelAnimationFrame(rafId); const exec = () => el == null ? void 0 : el.focus({ preventScroll }); if (sync) { exec(); } else { rafId = requestAnimationFrame(exec); } } function contains(parent, child) { if (!parent || !child) { return false; } const rootNode = child.getRootNode == null ? void 0 : child.getRootNode(); // First, attempt with faster native method if (parent.contains(child)) { return true; } // then fallback to custom implementation with Shadow DOM support if (rootNode && isShadowRoot(rootNode)) { let next = child; while (next) { if (parent === next) { return true; } // @ts-ignore next = next.parentNode || next.host; } } // Give up, the result is false return false; } function getTarget(event) { if ('composedPath' in event) { return event.composedPath()[0]; } // TS thinks `event` is of type never as it assumes all browsers support // `composedPath()`, but browsers without shadow DOM don't. return event.target; } function getDocument(node) { return (node == null ? void 0 : node.ownerDocument) || document; } // Modified to add conditional `aria-hidden` support: // https://github.com/theKashey/aria-hidden/blob/9220c8f4a4fd35f63bee5510a9f41a37264382d4/src/index.ts const counters = { inert: /*#__PURE__*/new WeakMap(), 'aria-hidden': /*#__PURE__*/new WeakMap(), none: /*#__PURE__*/new WeakMap() }; function getCounterMap(control) { if (control === 'inert') return counters.inert; if (control === 'aria-hidden') return counters['aria-hidden']; return counters.none; } let uncontrolledElementsSet = /*#__PURE__*/new WeakSet(); let markerMap = {}; let lockCount$1 = 0; const supportsInert = () => typeof HTMLElement !== 'undefined' && 'inert' in HTMLElement.prototype; const unwrapHost = node => node && (node.host || unwrapHost(node.parentNode)); const correctElements = (parent, targets) => targets.map(target => { if (parent.contains(target)) { return target; } const correctedTarget = unwrapHost(target); if (parent.contains(correctedTarget)) { return correctedTarget; } return null; }).filter(x => x != null); function applyAttributeToOthers(uncorrectedAvoidElements, body, ariaHidden, inert) { const markerName = 'data-floating-ui-inert'; const controlAttribute = inert ? 'inert' : ariaHidden ? 'aria-hidden' : null; const avoidElements = correctElements(body, uncorrectedAvoidElements); const elementsToKeep = new Set(); const elementsToStop = new Set(avoidElements); const hiddenElements = []; if (!markerMap[markerName]) { markerMap[markerName] = new WeakMap(); } const markerCounter = markerMap[markerName]; avoidElements.forEach(keep); deep(body); elementsToKeep.clear(); function keep(el) { if (!el || elementsToKeep.has(el)) { return; } elementsToKeep.add(el); el.parentNode && keep(el.parentNode); } function deep(parent) { if (!parent || elementsToStop.has(parent)) { return; } [].forEach.call(parent.children, node => { if (getNodeName(node) === 'script') return; if (elementsToKeep.has(node)) { deep(node); } else { const attr = controlAttribute ? node.getAttribute(controlAttribute) : null; const alreadyHidden = attr !== null && attr !== 'false'; const counterMap = getCounterMap(controlAttribute); const counterValue = (counterMap.get(node) || 0) + 1; const markerValue = (markerCounter.get(node) || 0) + 1; counterMap.set(node, counterValue); markerCounter.set(node, markerValue); hiddenElements.push(node); if (counterValue === 1 && alreadyHidden) { uncontrolledElementsSet.add(node); } if (markerValue === 1) { node.setAttribute(markerName, ''); } if (!alreadyHidden && controlAttribute) { node.setAttribute(controlAttribute, controlAttribute === 'inert' ? '' : 'true'); } } }); } lockCount$1++; return () => { hiddenElements.forEach(element => { const counterMap = getCounterMap(controlAttribute); const currentCounterValue = counterMap.get(element) || 0; const counterValue = currentCounterValue - 1; const markerValue = (markerCounter.get(element) || 0) - 1; counterMap.set(element, counterValue); markerCounter.set(element, markerValue); if (!counterValue) { if (!uncontrolledElementsSet.has(element) && controlAttribute) { element.removeAttribute(controlAttribute); } uncontrolledElementsSet.delete(element); } if (!markerValue) { element.removeAttribute(markerName); } }); lockCount$1--; if (!lockCount$1) { counters.inert = new WeakMap(); counters['aria-hidden'] = new WeakMap(); counters.none = new WeakMap(); uncontrolledElementsSet = new WeakSet(); markerMap = {}; } }; } function markOthers(avoidElements, ariaHidden, inert) { if (ariaHidden === void 0) { ariaHidden = false; } if (inert === void 0) { inert = false; } const body = getDocument(avoidElements[0]).body; return applyAttributeToOthers(avoidElements.concat(Array.from(body.querySelectorAll('[aria-live]'))), body, ariaHidden, inert); } const HIDDEN_STYLES = { border: 0, clip: 'rect(0 0 0 0)', height: '1px', margin: '-1px', overflow: 'hidden', padding: 0, position: 'fixed', whiteSpace: 'nowrap', width: '1px', top: 0, left: 0 }; const FocusGuard = /*#__PURE__*/React.forwardRef(function FocusGuard(props, ref) { const [role, setRole] = React.useState(); useModernLayoutEffect(() => { if (isSafari()) { // Unlike other screen readers such as NVDA and JAWS, the virtual cursor // on VoiceOver does trigger the onFocus event, so we can use the focus // trap element. On Safari, only buttons trigger the onFocus event. // NB: "group" role in the Sandbox no longer appears to work, must be a // button role. setRole('button'); } }, []); const restProps = { ref, tabIndex: 0, // Role is only for VoiceOver role, 'aria-hidden': role ? undefined : true, [createAttribute('focus-guard')]: '', style: HIDDEN_STYLES }; return /*#__PURE__*/jsx("span", { ...props, ...restProps }); }); const PortalContext = /*#__PURE__*/React.createContext(null); const attr = /*#__PURE__*/createAttribute('portal'); /** * @see https://floating-ui.com/docs/FloatingPortal#usefloatingportalnode */ function useFloatingPortalNode(props) { if (props === void 0) { props = {}; } const { id, root } = props; const uniqueId = useId(); const portalContext = usePortalContext(); const [portalNode, setPortalNode] = React.useState(null); const portalNodeRef = React.useRef(null); useModernLayoutEffect(() => { return () => { portalNode == null || portalNode.remove(); // Allow the subsequent layout effects to create a new node on updates. // The portal node will still be cleaned up on unmount. // https://github.com/floating-ui/floati