@floating-ui/react
Version:
Floating UI for React
1,526 lines (1,488 loc) • 169 kB
JavaScript
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