UNPKG

@floating-ui/react

Version:
1,574 lines (1,535 loc) 177 kB
import * as React from 'react'; import { useLayoutEffect, useEffect, useRef } from 'react'; import { stopEvent, getDocument, isMouseLikePointerType, contains, activeElement, isSafari, isTypeableCombobox, isVirtualClick, isVirtualPointerEvent, getTarget, getPlatform, isTypeableElement, isReactEvent, isRootElement, isEventTargetWithin, isMac, getUserAgent as getUserAgent$1 } from '@floating-ui/react/utils'; import { floor, evaluate, max, round, min } from '@floating-ui/utils'; import { jsx, jsxs, Fragment } from 'react/jsx-runtime'; import { getComputedStyle, isElement, getNodeName, isHTMLElement, getWindow, isWebKit, isLastTraversableNode, getParentNode } from '@floating-ui/utils/dom'; import { tabbable, isTabbable } 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'; /** * 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); } // https://github.com/mui/material-ui/issues/41190#issuecomment-2040873379 const SafeReact = { ...React }; const useInsertionEffect = SafeReact.useInsertionEffect; const useSafeInsertionEffect = useInsertionEffect || (fn => fn()); function useEffectEvent(callback) { const ref = React.useRef(() => { if (process.env.NODE_ENV !== "production") { throw new Error('Cannot call an event handler while rendering.'); } }); useSafeInsertionEffect(() => { ref.current = callback; }); return React.useCallback(function () { for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } return ref.current == null ? void 0 : ref.current(...args); }, []); } const ARROW_UP = 'ArrowUp'; const ARROW_DOWN = 'ArrowDown'; const ARROW_LEFT = 'ArrowLeft'; const ARROW_RIGHT = 'ArrowRight'; function isDifferentRow(index, cols, prevRow) { return Math.floor(index / cols) !== prevRow; } function isIndexOutOfBounds(listRef, index) { return index < 0 || index >= listRef.current.length; } function getMinIndex(listRef, disabledIndices) { return findNonDisabledIndex(listRef, { disabledIndices }); } function getMaxIndex(listRef, disabledIndices) { return findNonDisabledIndex(listRef, { decrement: true, startingIndex: listRef.current.length, disabledIndices }); } function findNonDisabledIndex(listRef, _temp) { let { startingIndex = -1, decrement = false, disabledIndices, amount = 1 } = _temp === void 0 ? {} : _temp; const list = listRef.current; let index = startingIndex; do { index += decrement ? -amount : amount; } while (index >= 0 && index <= list.length - 1 && isDisabled(list, index, disabledIndices)); return index; } function getGridNavigatedIndex(elementsRef, _ref) { let { event, orientation, loop, rtl, cols, disabledIndices, minIndex, maxIndex, prevIndex, stopEvent: stop = false } = _ref; let nextIndex = prevIndex; if (event.key === ARROW_UP) { stop && stopEvent(event); if (prevIndex === -1) { nextIndex = maxIndex; } else { nextIndex = findNonDisabledIndex(elementsRef, { startingIndex: nextIndex, amount: cols, decrement: true, disabledIndices }); if (loop && (prevIndex - cols < minIndex || nextIndex < 0)) { const col = prevIndex % cols; const maxCol = maxIndex % cols; const offset = maxIndex - (maxCol - col); if (maxCol === col) { nextIndex = maxIndex; } else { nextIndex = maxCol > col ? offset : offset - cols; } } } if (isIndexOutOfBounds(elementsRef, nextIndex)) { nextIndex = prevIndex; } } if (event.key === ARROW_DOWN) { stop && stopEvent(event); if (prevIndex === -1) { nextIndex = minIndex; } else { nextIndex = findNonDisabledIndex(elementsRef, { startingIndex: prevIndex, amount: cols, disabledIndices }); if (loop && prevIndex + cols > maxIndex) { nextIndex = findNonDisabledIndex(elementsRef, { startingIndex: prevIndex % cols - cols, amount: cols, disabledIndices }); } } if (isIndexOutOfBounds(elementsRef, nextIndex)) { nextIndex = prevIndex; } } // Remains on the same row/column. if (orientation === 'both') { const prevRow = floor(prevIndex / cols); if (event.key === (rtl ? ARROW_LEFT : ARROW_RIGHT)) { stop && stopEvent(event); if (prevIndex % cols !== cols - 1) { nextIndex = findNonDisabledIndex(elementsRef, { startingIndex: prevIndex, disabledIndices }); if (loop && isDifferentRow(nextIndex, cols, prevRow)) { nextIndex = findNonDisabledIndex(elementsRef, { startingIndex: prevIndex - prevIndex % cols - 1, disabledIndices }); } } else if (loop) { nextIndex = findNonDisabledIndex(elementsRef, { startingIndex: prevIndex - prevIndex % cols - 1, disabledIndices }); } if (isDifferentRow(nextIndex, cols, prevRow)) { nextIndex = prevIndex; } } if (event.key === (rtl ? ARROW_RIGHT : ARROW_LEFT)) { stop && stopEvent(event); if (prevIndex % cols !== 0) { nextIndex = findNonDisabledIndex(elementsRef, { startingIndex: prevIndex, decrement: true, disabledIndices }); if (loop && isDifferentRow(nextIndex, cols, prevRow)) { nextIndex = findNonDisabledIndex(elementsRef, { startingIndex: prevIndex + (cols - prevIndex % cols), decrement: true, disabledIndices }); } } else if (loop) { nextIndex = findNonDisabledIndex(elementsRef, { startingIndex: prevIndex + (cols - prevIndex % cols), decrement: true, disabledIndices }); } if (isDifferentRow(nextIndex, cols, prevRow)) { nextIndex = prevIndex; } } const lastRow = floor(maxIndex / cols) === prevRow; if (isIndexOutOfBounds(elementsRef, nextIndex)) { if (loop && lastRow) { nextIndex = event.key === (rtl ? ARROW_RIGHT : ARROW_LEFT) ? maxIndex : findNonDisabledIndex(elementsRef, { startingIndex: prevIndex - prevIndex % cols - 1, disabledIndices }); } else { nextIndex = prevIndex; } } } return nextIndex; } /** For each cell index, gets the item index that occupies that cell */ function buildCellMap(sizes, cols, dense) { const cellMap = []; let startIndex = 0; sizes.forEach((_ref2, index) => { let { width, height } = _ref2; if (width > cols) { if (process.env.NODE_ENV !== "production") { throw new Error("[Floating UI]: Invalid grid - item width at index " + index + " is greater than grid columns"); } } let itemPlaced = false; if (dense) { startIndex = 0; } while (!itemPlaced) { const targetCells = []; for (let i = 0; i < width; i++) { for (let j = 0; j < height; j++) { targetCells.push(startIndex + i + j * cols); } } if (startIndex % cols + width <= cols && targetCells.every(cell => cellMap[cell] == null)) { targetCells.forEach(cell => { cellMap[cell] = index; }); itemPlaced = true; } else { startIndex++; } } }); // convert into a non-sparse array return [...cellMap]; } /** Gets cell index of an item's corner or -1 when index is -1. */ function getCellIndexOfCorner(index, sizes, cellMap, cols, corner) { if (index === -1) return -1; const firstCellIndex = cellMap.indexOf(index); const sizeItem = sizes[index]; switch (corner) { case 'tl': return firstCellIndex; case 'tr': if (!sizeItem) { return firstCellIndex; } return firstCellIndex + sizeItem.width - 1; case 'bl': if (!sizeItem) { return firstCellIndex; } return firstCellIndex + (sizeItem.height - 1) * cols; case 'br': return cellMap.lastIndexOf(index); } } /** Gets all cell indices that correspond to the specified indices */ function getCellIndices(indices, cellMap) { return cellMap.flatMap((index, cellIndex) => indices.includes(index) ? [cellIndex] : []); } function isDisabled(list, index, disabledIndices) { if (disabledIndices) { return disabledIndices.includes(index); } const element = list[index]; return element == null || element.hasAttribute('disabled') || element.getAttribute('aria-disabled') === 'true'; } var index = typeof document !== 'undefined' ? useLayoutEffect : useEffect; 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$1, setIndex] = React.useState(null); const componentRef = React.useRef(null); const ref = React.useCallback(node => { componentRef.current = node; if (index$1 !== null) { elementsRef.current[index$1] = node; if (labelsRef) { var _node$textContent; const isLabelDefined = label !== undefined; labelsRef.current[index$1] = isLabelDefined ? label : (_node$textContent = node == null ? void 0 : node.textContent) != null ? _node$textContent : null; } } }, [index$1, elementsRef, labelsRef, label]); index(() => { const node = componentRef.current; if (node) { register(node); return () => { unregister(node); }; } }, [register, unregister]); index(() => { const index = componentRef.current ? map.get(componentRef.current) : null; if (index != null) { setIndex(index); } }, [map]); return React.useMemo(() => ({ ref, index: index$1 == null ? -1 : index$1 }), [index$1, ref]); } 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 = getMinIndex(elementsRef, disabledIndices); const maxIndex = getMaxIndex(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 = buildCellMap(sizes, cols, dense); const minGridIndex = cellMap.findIndex(index => index != null && !isDisabled(elementsRef.current, index, disabledIndices)); // last enabled index const maxGridIndex = cellMap.reduce((foundIndex, index, cellIndex) => index != null && !isDisabled(elementsRef.current, 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: getCellIndices([...(disabledIndices || elementsRef.current.map((_, index) => isDisabled(elementsRef.current, index) ? index : undefined)), undefined], cellMap), minIndex: minGridIndex, maxIndex: maxGridIndex, prevIndex: getCellIndexOfCorner(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 = findNonDisabledIndex(elementsRef, { startingIndex: nextIndex, decrement: toStartKeys.includes(event.key), disabledIndices }); } } if (nextIndex !== activeIndex && !isIndexOutOfBounds(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); }); 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); index(() => { 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 index(() => { 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; index(() => { 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; } } function useLatestRef(value) { const ref = useRef(value); index(() => { ref.current = value; }); return ref; } 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 = React.useCallback(() => { 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'; }, [dataRef]); // 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(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(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(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(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 index(() => { var _handleCloseRef$curre; if (!enabled) return; if (open && (_handleCloseRef$curre = handleCloseRef.current) != null && _handleCloseRef$curre.__options.blockPointerEvents && isHoverOpen()) { performedPointerEventsMutationRef.current = true; const floatingEl = elements.floating; if (isElement(elements.domReference) && floatingEl) { var _tree$nodesRef$curren; const body = getDocument(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]); index(() => { 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 }); }, []); index(() => { 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; index(() => { 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]); index(() => { 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]); index(() => { 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); index(() => { 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]); index(() => { 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]); index(() => { 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 getAncestors(nodes, id) { var _nodes$find; let allAncestors = []; let currentParentId = (_nodes$find = nodes.find(node => node.id === id)) == null ? void 0 : _nodes$