4589 lines
165 KiB
JavaScript
4589 lines
165 KiB
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, isNode, 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],[role="status"],output'))), 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/floating-ui/issues/2454
|
||
queueMicrotask(() => {
|
||
portalNodeRef.current = null;
|
||
});
|
||
};
|
||
}, [portalNode]);
|
||
useModernLayoutEffect(() => {
|
||
// Wait for the uniqueId to be generated before creating the portal node in
|
||
// React <18 (using `useFloatingId` instead of the native `useId`).
|
||
// https://github.com/floating-ui/floating-ui/issues/2778
|
||
if (!uniqueId) return;
|
||
if (portalNodeRef.current) return;
|
||
const existingIdRoot = id ? document.getElementById(id) : null;
|
||
if (!existingIdRoot) return;
|
||
const subRoot = document.createElement('div');
|
||
subRoot.id = uniqueId;
|
||
subRoot.setAttribute(attr, '');
|
||
existingIdRoot.appendChild(subRoot);
|
||
portalNodeRef.current = subRoot;
|
||
setPortalNode(subRoot);
|
||
}, [id, uniqueId]);
|
||
useModernLayoutEffect(() => {
|
||
// Wait for the root to exist before creating the portal node. The root must
|
||
// be stored in state, not a ref, for this to work reactively.
|
||
if (root === null) return;
|
||
if (!uniqueId) return;
|
||
if (portalNodeRef.current) return;
|
||
let container = root || (portalContext == null ? void 0 : portalContext.portalNode);
|
||
if (container && !isNode(container)) container = container.current;
|
||
container = container || document.body;
|
||
let idWrapper = null;
|
||
if (id) {
|
||
idWrapper = document.createElement('div');
|
||
idWrapper.id = id;
|
||
container.appendChild(idWrapper);
|
||
}
|
||
const subRoot = document.createElement('div');
|
||
subRoot.id = uniqueId;
|
||
subRoot.setAttribute(attr, '');
|
||
container = idWrapper || container;
|
||
container.appendChild(subRoot);
|
||
portalNodeRef.current = subRoot;
|
||
setPortalNode(subRoot);
|
||
}, [id, root, uniqueId, portalContext]);
|
||
return portalNode;
|
||
}
|
||
/**
|
||
* Portals the floating element into a given container element — by default,
|
||
* outside of the app root and into the body.
|
||
* This is necessary to ensure the floating element can appear outside any
|
||
* potential parent containers that cause clipping (such as `overflow: hidden`),
|
||
* while retaining its location in the React tree.
|
||
* @see https://floating-ui.com/docs/FloatingPortal
|
||
*/
|
||
function FloatingPortal(props) {
|
||
const {
|
||
children,
|
||
id,
|
||
root,
|
||
preserveTabOrder = true
|
||
} = props;
|
||
const portalNode = useFloatingPortalNode({
|
||
id,
|
||
root
|
||
});
|
||
const [focusManagerState, setFocusManagerState] = React.useState(null);
|
||
const beforeOutsideRef = React.useRef(null);
|
||
const afterOutsideRef = React.useRef(null);
|
||
const beforeInsideRef = React.useRef(null);
|
||
const afterInsideRef = React.useRef(null);
|
||
const modal = focusManagerState == null ? void 0 : focusManagerState.modal;
|
||
const open = focusManagerState == null ? void 0 : focusManagerState.open;
|
||
const shouldRenderGuards =
|
||
// The FocusManager and therefore floating element are currently open/
|
||
// rendered.
|
||
!!focusManagerState &&
|
||
// Guards are only for non-modal focus management.
|
||
!focusManagerState.modal &&
|
||
// Don't render if unmount is transitioning.
|
||
focusManagerState.open && preserveTabOrder && !!(root || portalNode);
|
||
|
||
// https://codesandbox.io/s/tabbable-portal-f4tng?file=/src/TabbablePortal.tsx
|
||
React.useEffect(() => {
|
||
if (!portalNode || !preserveTabOrder || modal) {
|
||
return;
|
||
}
|
||
|
||
// Make sure elements inside the portal element are tabbable only when the
|
||
// portal has already been focused, either by tabbing into a focus trap
|
||
// element outside or using the mouse.
|
||
function onFocus(event) {
|
||
if (portalNode && isOutsideEvent(event)) {
|
||
const focusing = event.type === 'focusin';
|
||
const manageFocus = focusing ? enableFocusInside : disableFocusInside;
|
||
manageFocus(portalNode);
|
||
}
|
||
}
|
||
// Listen to the event on the capture phase so they run before the focus
|
||
// trap elements onFocus prop is called.
|
||
portalNode.addEventListener('focusin', onFocus, true);
|
||
portalNode.addEventListener('focusout', onFocus, true);
|
||
return () => {
|
||
portalNode.removeEventListener('focusin', onFocus, true);
|
||
portalNode.removeEventListener('focusout', onFocus, true);
|
||
};
|
||
}, [portalNode, preserveTabOrder, modal]);
|
||
React.useEffect(() => {
|
||
if (!portalNode) return;
|
||
if (open) return;
|
||
enableFocusInside(portalNode);
|
||
}, [open, portalNode]);
|
||
return /*#__PURE__*/jsxs(PortalContext.Provider, {
|
||
value: React.useMemo(() => ({
|
||
preserveTabOrder,
|
||
beforeOutsideRef,
|
||
afterOutsideRef,
|
||
beforeInsideRef,
|
||
afterInsideRef,
|
||
portalNode,
|
||
setFocusManagerState
|
||
}), [preserveTabOrder, portalNode]),
|
||
children: [shouldRenderGuards && portalNode && /*#__PURE__*/jsx(FocusGuard, {
|
||
"data-type": "outside",
|
||
ref: beforeOutsideRef,
|
||
onFocus: event => {
|
||
if (isOutsideEvent(event, portalNode)) {
|
||
var _beforeInsideRef$curr;
|
||
(_beforeInsideRef$curr = beforeInsideRef.current) == null || _beforeInsideRef$curr.focus();
|
||
} else {
|
||
const domReference = focusManagerState ? focusManagerState.domReference : null;
|
||
const prevTabbable = getPreviousTabbable(domReference);
|
||
prevTabbable == null || prevTabbable.focus();
|
||
}
|
||
}
|
||
}), shouldRenderGuards && portalNode && /*#__PURE__*/jsx("span", {
|
||
"aria-owns": portalNode.id,
|
||
style: HIDDEN_STYLES
|
||
}), portalNode && /*#__PURE__*/ReactDOM.createPortal(children, portalNode), shouldRenderGuards && portalNode && /*#__PURE__*/jsx(FocusGuard, {
|
||
"data-type": "outside",
|
||
ref: afterOutsideRef,
|
||
onFocus: event => {
|
||
if (isOutsideEvent(event, portalNode)) {
|
||
var _afterInsideRef$curre;
|
||
(_afterInsideRef$curre = afterInsideRef.current) == null || _afterInsideRef$curre.focus();
|
||
} else {
|
||
const domReference = focusManagerState ? focusManagerState.domReference : null;
|
||
const nextTabbable = getNextTabbable(domReference);
|
||
nextTabbable == null || nextTabbable.focus();
|
||
(focusManagerState == null ? void 0 : focusManagerState.closeOnFocusOut) && (focusManagerState == null ? void 0 : focusManagerState.onOpenChange(false, event.nativeEvent, 'focus-out'));
|
||
}
|
||
}
|
||
})]
|
||
});
|
||
}
|
||
const usePortalContext = () => React.useContext(PortalContext);
|
||
|
||
function useLiteMergeRefs(refs) {
|
||
return React.useMemo(() => {
|
||
return value => {
|
||
refs.forEach(ref => {
|
||
if (ref) {
|
||
ref.current = value;
|
||
}
|
||
});
|
||
};
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, refs);
|
||
}
|
||
|
||
const LIST_LIMIT = 20;
|
||
let previouslyFocusedElements = [];
|
||
function clearDisconnectedPreviouslyFocusedElements() {
|
||
previouslyFocusedElements = previouslyFocusedElements.filter(el => el.isConnected);
|
||
}
|
||
function addPreviouslyFocusedElement(element) {
|
||
clearDisconnectedPreviouslyFocusedElements();
|
||
if (element && getNodeName(element) !== 'body') {
|
||
previouslyFocusedElements.push(element);
|
||
if (previouslyFocusedElements.length > LIST_LIMIT) {
|
||
previouslyFocusedElements = previouslyFocusedElements.slice(-20);
|
||
}
|
||
}
|
||
}
|
||
function getPreviouslyFocusedElement() {
|
||
clearDisconnectedPreviouslyFocusedElements();
|
||
return previouslyFocusedElements[previouslyFocusedElements.length - 1];
|
||
}
|
||
function getFirstTabbableElement(container) {
|
||
const tabbableOptions = getTabbableOptions();
|
||
if (isTabbable(container, tabbableOptions)) {
|
||
return container;
|
||
}
|
||
return tabbable(container, tabbableOptions)[0] || container;
|
||
}
|
||
function handleTabIndex(floatingFocusElement, orderRef) {
|
||
var _floatingFocusElement;
|
||
if (!orderRef.current.includes('floating') && !((_floatingFocusElement = floatingFocusElement.getAttribute('role')) != null && _floatingFocusElement.includes('dialog'))) {
|
||
return;
|
||
}
|
||
const options = getTabbableOptions();
|
||
const focusableElements = focusable(floatingFocusElement, options);
|
||
const tabbableContent = focusableElements.filter(element => {
|
||
const dataTabIndex = element.getAttribute('data-tabindex') || '';
|
||
return isTabbable(element, options) || element.hasAttribute('data-tabindex') && !dataTabIndex.startsWith('-');
|
||
});
|
||
const tabIndex = floatingFocusElement.getAttribute('tabindex');
|
||
if (orderRef.current.includes('floating') || tabbableContent.length === 0) {
|
||
if (tabIndex !== '0') {
|
||
floatingFocusElement.setAttribute('tabindex', '0');
|
||
}
|
||
} else if (tabIndex !== '-1' || floatingFocusElement.hasAttribute('data-tabindex') && floatingFocusElement.getAttribute('data-tabindex') !== '-1') {
|
||
floatingFocusElement.setAttribute('tabindex', '-1');
|
||
floatingFocusElement.setAttribute('data-tabindex', '-1');
|
||
}
|
||
}
|
||
const VisuallyHiddenDismiss = /*#__PURE__*/React.forwardRef(function VisuallyHiddenDismiss(props, ref) {
|
||
return /*#__PURE__*/jsx("button", {
|
||
...props,
|
||
type: "button",
|
||
ref: ref,
|
||
tabIndex: -1,
|
||
style: HIDDEN_STYLES
|
||
});
|
||
});
|
||
/**
|
||
* Provides focus management for the floating element.
|
||
* @see https://floating-ui.com/docs/FloatingFocusManager
|
||
*/
|
||
function FloatingFocusManager(props) {
|
||
const {
|
||
context,
|
||
children,
|
||
disabled = false,
|
||
order = ['content'],
|
||
guards: _guards = true,
|
||
initialFocus = 0,
|
||
returnFocus = true,
|
||
restoreFocus = false,
|
||
modal = true,
|
||
visuallyHiddenDismiss = false,
|
||
closeOnFocusOut = true,
|
||
outsideElementsInert = false,
|
||
getInsideElements: _getInsideElements = () => []
|
||
} = props;
|
||
const {
|
||
open,
|
||
onOpenChange,
|
||
events,
|
||
dataRef,
|
||
elements: {
|
||
domReference,
|
||
floating
|
||
}
|
||
} = context;
|
||
const getNodeId = useEffectEvent(() => {
|
||
var _dataRef$current$floa;
|
||
return (_dataRef$current$floa = dataRef.current.floatingContext) == null ? void 0 : _dataRef$current$floa.nodeId;
|
||
});
|
||
const getInsideElements = useEffectEvent(_getInsideElements);
|
||
const ignoreInitialFocus = typeof initialFocus === 'number' && initialFocus < 0;
|
||
// If the reference is a combobox and is typeable (e.g. input/textarea),
|
||
// there are different focus semantics. The guards should not be rendered, but
|
||
// aria-hidden should be applied to all nodes still. Further, the visually
|
||
// hidden dismiss button should only appear at the end of the list, not the
|
||
// start.
|
||
const isUntrappedTypeableCombobox = isTypeableCombobox(domReference) && ignoreInitialFocus;
|
||
|
||
// Force the guards to be rendered if the `inert` attribute is not supported.
|
||
const inertSupported = supportsInert();
|
||
const guards = inertSupported ? _guards : true;
|
||
const useInert = !guards || inertSupported && outsideElementsInert;
|
||
const orderRef = useLatestRef(order);
|
||
const initialFocusRef = useLatestRef(initialFocus);
|
||
const returnFocusRef = useLatestRef(returnFocus);
|
||
const tree = useFloatingTree();
|
||
const portalContext = usePortalContext();
|
||
const startDismissButtonRef = React.useRef(null);
|
||
const endDismissButtonRef = React.useRef(null);
|
||
const preventReturnFocusRef = React.useRef(false);
|
||
const isPointerDownRef = React.useRef(false);
|
||
const tabbableIndexRef = React.useRef(-1);
|
||
const blurTimeoutRef = React.useRef(-1);
|
||
const isInsidePortal = portalContext != null;
|
||
const floatingFocusElement = getFloatingFocusElement(floating);
|
||
const getTabbableContent = useEffectEvent(function (container) {
|
||
if (container === void 0) {
|
||
container = floatingFocusElement;
|
||
}
|
||
return container ? tabbable(container, getTabbableOptions()) : [];
|
||
});
|
||
const getTabbableElements = useEffectEvent(container => {
|
||
const content = getTabbableContent(container);
|
||
return orderRef.current.map(type => {
|
||
if (domReference && type === 'reference') {
|
||
return domReference;
|
||
}
|
||
if (floatingFocusElement && type === 'floating') {
|
||
return floatingFocusElement;
|
||
}
|
||
return content;
|
||
}).filter(Boolean).flat();
|
||
});
|
||
React.useEffect(() => {
|
||
if (disabled) return;
|
||
if (!modal) return;
|
||
function onKeyDown(event) {
|
||
if (event.key === 'Tab') {
|
||
// The focus guards have nothing to focus, so we need to stop the event.
|
||
if (contains$1(floatingFocusElement, activeElement(getDocument$1(floatingFocusElement))) && getTabbableContent().length === 0 && !isUntrappedTypeableCombobox) {
|
||
stopEvent(event);
|
||
}
|
||
const els = getTabbableElements();
|
||
const target = getTarget$1(event);
|
||
if (orderRef.current[0] === 'reference' && target === domReference) {
|
||
stopEvent(event);
|
||
if (event.shiftKey) {
|
||
enqueueFocus(els[els.length - 1]);
|
||
} else {
|
||
enqueueFocus(els[1]);
|
||
}
|
||
}
|
||
if (orderRef.current[1] === 'floating' && target === floatingFocusElement && event.shiftKey) {
|
||
stopEvent(event);
|
||
enqueueFocus(els[0]);
|
||
}
|
||
}
|
||
}
|
||
const doc = getDocument$1(floatingFocusElement);
|
||
doc.addEventListener('keydown', onKeyDown);
|
||
return () => {
|
||
doc.removeEventListener('keydown', onKeyDown);
|
||
};
|
||
}, [disabled, domReference, floatingFocusElement, modal, orderRef, isUntrappedTypeableCombobox, getTabbableContent, getTabbableElements]);
|
||
React.useEffect(() => {
|
||
if (disabled) return;
|
||
if (!floating) return;
|
||
function handleFocusIn(event) {
|
||
const target = getTarget$1(event);
|
||
const tabbableContent = getTabbableContent();
|
||
const tabbableIndex = tabbableContent.indexOf(target);
|
||
if (tabbableIndex !== -1) {
|
||
tabbableIndexRef.current = tabbableIndex;
|
||
}
|
||
}
|
||
floating.addEventListener('focusin', handleFocusIn);
|
||
return () => {
|
||
floating.removeEventListener('focusin', handleFocusIn);
|
||
};
|
||
}, [disabled, floating, getTabbableContent]);
|
||
React.useEffect(() => {
|
||
if (disabled) return;
|
||
if (!closeOnFocusOut) return;
|
||
|
||
// In Safari, buttons lose focus when pressing them.
|
||
function handlePointerDown() {
|
||
isPointerDownRef.current = true;
|
||
setTimeout(() => {
|
||
isPointerDownRef.current = false;
|
||
});
|
||
}
|
||
function handleFocusOutside(event) {
|
||
const relatedTarget = event.relatedTarget;
|
||
const currentTarget = event.currentTarget;
|
||
const target = getTarget$1(event);
|
||
queueMicrotask(() => {
|
||
const nodeId = getNodeId();
|
||
const movedToUnrelatedNode = !(contains$1(domReference, relatedTarget) || contains$1(floating, relatedTarget) || contains$1(relatedTarget, floating) || contains$1(portalContext == null ? void 0 : portalContext.portalNode, relatedTarget) || relatedTarget != null && relatedTarget.hasAttribute(createAttribute('focus-guard')) || tree && (getNodeChildren$1(tree.nodesRef.current, nodeId).find(node => {
|
||
var _node$context, _node$context2;
|
||
return contains$1((_node$context = node.context) == null ? void 0 : _node$context.elements.floating, relatedTarget) || contains$1((_node$context2 = node.context) == null ? void 0 : _node$context2.elements.domReference, relatedTarget);
|
||
}) || getNodeAncestors(tree.nodesRef.current, nodeId).find(node => {
|
||
var _node$context3, _node$context4, _node$context5;
|
||
return [(_node$context3 = node.context) == null ? void 0 : _node$context3.elements.floating, getFloatingFocusElement((_node$context4 = node.context) == null ? void 0 : _node$context4.elements.floating)].includes(relatedTarget) || ((_node$context5 = node.context) == null ? void 0 : _node$context5.elements.domReference) === relatedTarget;
|
||
})));
|
||
if (currentTarget === domReference && floatingFocusElement) {
|
||
handleTabIndex(floatingFocusElement, orderRef);
|
||
}
|
||
|
||
// Restore focus to the previous tabbable element index to prevent
|
||
// focus from being lost outside the floating tree.
|
||
if (restoreFocus && currentTarget !== domReference && !(target != null && target.isConnected) && activeElement(getDocument$1(floatingFocusElement)) === getDocument$1(floatingFocusElement).body) {
|
||
// Let `FloatingPortal` effect knows that focus is still inside the
|
||
// floating tree.
|
||
if (isHTMLElement(floatingFocusElement)) {
|
||
floatingFocusElement.focus();
|
||
}
|
||
const prevTabbableIndex = tabbableIndexRef.current;
|
||
const tabbableContent = getTabbableContent();
|
||
const nodeToFocus = tabbableContent[prevTabbableIndex] || tabbableContent[tabbableContent.length - 1] || floatingFocusElement;
|
||
if (isHTMLElement(nodeToFocus)) {
|
||
nodeToFocus.focus();
|
||
}
|
||
}
|
||
|
||
// https://github.com/floating-ui/floating-ui/issues/3060
|
||
if (dataRef.current.insideReactTree) {
|
||
dataRef.current.insideReactTree = false;
|
||
return;
|
||
}
|
||
|
||
// Focus did not move inside the floating tree, and there are no tabbable
|
||
// portal guards to handle closing.
|
||
if ((isUntrappedTypeableCombobox ? true : !modal) && relatedTarget && movedToUnrelatedNode && !isPointerDownRef.current &&
|
||
// Fix React 18 Strict Mode returnFocus due to double rendering.
|
||
relatedTarget !== getPreviouslyFocusedElement()) {
|
||
preventReturnFocusRef.current = true;
|
||
onOpenChange(false, event, 'focus-out');
|
||
}
|
||
});
|
||
}
|
||
const shouldHandleBlurCapture = Boolean(!tree && portalContext);
|
||
function markInsideReactTree() {
|
||
clearTimeoutIfSet(blurTimeoutRef);
|
||
dataRef.current.insideReactTree = true;
|
||
blurTimeoutRef.current = window.setTimeout(() => {
|
||
dataRef.current.insideReactTree = false;
|
||
});
|
||
}
|
||
if (floating && isHTMLElement(domReference)) {
|
||
domReference.addEventListener('focusout', handleFocusOutside);
|
||
domReference.addEventListener('pointerdown', handlePointerDown);
|
||
floating.addEventListener('focusout', handleFocusOutside);
|
||
if (shouldHandleBlurCapture) {
|
||
floating.addEventListener('focusout', markInsideReactTree, true);
|
||
}
|
||
return () => {
|
||
domReference.removeEventListener('focusout', handleFocusOutside);
|
||
domReference.removeEventListener('pointerdown', handlePointerDown);
|
||
floating.removeEventListener('focusout', handleFocusOutside);
|
||
if (shouldHandleBlurCapture) {
|
||
floating.removeEventListener('focusout', markInsideReactTree, true);
|
||
}
|
||
};
|
||
}
|
||
}, [disabled, domReference, floating, floatingFocusElement, modal, tree, portalContext, onOpenChange, closeOnFocusOut, restoreFocus, getTabbableContent, isUntrappedTypeableCombobox, getNodeId, orderRef, dataRef]);
|
||
const beforeGuardRef = React.useRef(null);
|
||
const afterGuardRef = React.useRef(null);
|
||
const mergedBeforeGuardRef = useLiteMergeRefs([beforeGuardRef, portalContext == null ? void 0 : portalContext.beforeInsideRef]);
|
||
const mergedAfterGuardRef = useLiteMergeRefs([afterGuardRef, portalContext == null ? void 0 : portalContext.afterInsideRef]);
|
||
React.useEffect(() => {
|
||
var _portalContext$portal, _ancestors$find;
|
||
if (disabled) return;
|
||
if (!floating) return;
|
||
|
||
// Don't hide portals nested within the parent portal.
|
||
const portalNodes = Array.from((portalContext == null || (_portalContext$portal = portalContext.portalNode) == null ? void 0 : _portalContext$portal.querySelectorAll("[" + createAttribute('portal') + "]")) || []);
|
||
const ancestors = tree ? getNodeAncestors(tree.nodesRef.current, getNodeId()) : [];
|
||
const rootAncestorComboboxDomReference = (_ancestors$find = ancestors.find(node => {
|
||
var _node$context6;
|
||
return isTypeableCombobox(((_node$context6 = node.context) == null ? void 0 : _node$context6.elements.domReference) || null);
|
||
})) == null || (_ancestors$find = _ancestors$find.context) == null ? void 0 : _ancestors$find.elements.domReference;
|
||
const insideElements = [floating, rootAncestorComboboxDomReference, ...portalNodes, ...getInsideElements(), startDismissButtonRef.current, endDismissButtonRef.current, beforeGuardRef.current, afterGuardRef.current, portalContext == null ? void 0 : portalContext.beforeOutsideRef.current, portalContext == null ? void 0 : portalContext.afterOutsideRef.current, orderRef.current.includes('reference') || isUntrappedTypeableCombobox ? domReference : null].filter(x => x != null);
|
||
const cleanup = modal || isUntrappedTypeableCombobox ? markOthers(insideElements, !useInert, useInert) : markOthers(insideElements);
|
||
return () => {
|
||
cleanup();
|
||
};
|
||
}, [disabled, domReference, floating, modal, orderRef, portalContext, isUntrappedTypeableCombobox, guards, useInert, tree, getNodeId, getInsideElements]);
|
||
useModernLayoutEffect(() => {
|
||
if (disabled || !isHTMLElement(floatingFocusElement)) return;
|
||
const doc = getDocument$1(floatingFocusElement);
|
||
const previouslyFocusedElement = activeElement(doc);
|
||
|
||
// Wait for any layout effect state setters to execute to set `tabIndex`.
|
||
queueMicrotask(() => {
|
||
const focusableElements = getTabbableElements(floatingFocusElement);
|
||
const initialFocusValue = initialFocusRef.current;
|
||
const elToFocus = (typeof initialFocusValue === 'number' ? focusableElements[initialFocusValue] : initialFocusValue.current) || floatingFocusElement;
|
||
const focusAlreadyInsideFloatingEl = contains$1(floatingFocusElement, previouslyFocusedElement);
|
||
if (!ignoreInitialFocus && !focusAlreadyInsideFloatingEl && open) {
|
||
enqueueFocus(elToFocus, {
|
||
preventScroll: elToFocus === floatingFocusElement
|
||
});
|
||
}
|
||
});
|
||
}, [disabled, open, floatingFocusElement, ignoreInitialFocus, getTabbableElements, initialFocusRef]);
|
||
useModernLayoutEffect(() => {
|
||
if (disabled || !floatingFocusElement) return;
|
||
const doc = getDocument$1(floatingFocusElement);
|
||
const previouslyFocusedElement = activeElement(doc);
|
||
addPreviouslyFocusedElement(previouslyFocusedElement);
|
||
|
||
// Dismissing via outside press should always ignore `returnFocus` to
|
||
// prevent unwanted scrolling.
|
||
function onOpenChange(_ref) {
|
||
let {
|
||
reason,
|
||
event,
|
||
nested
|
||
} = _ref;
|
||
if (['hover', 'safe-polygon'].includes(reason) && event.type === 'mouseleave') {
|
||
preventReturnFocusRef.current = true;
|
||
}
|
||
if (reason !== 'outside-press') return;
|
||
if (nested) {
|
||
preventReturnFocusRef.current = false;
|
||
} else if (isVirtualClick(event) || isVirtualPointerEvent(event)) {
|
||
preventReturnFocusRef.current = false;
|
||
} else {
|
||
let isPreventScrollSupported = false;
|
||
document.createElement('div').focus({
|
||
get preventScroll() {
|
||
isPreventScrollSupported = true;
|
||
return false;
|
||
}
|
||
});
|
||
if (isPreventScrollSupported) {
|
||
preventReturnFocusRef.current = false;
|
||
} else {
|
||
preventReturnFocusRef.current = true;
|
||
}
|
||
}
|
||
}
|
||
events.on('openchange', onOpenChange);
|
||
const fallbackEl = doc.createElement('span');
|
||
fallbackEl.setAttribute('tabindex', '-1');
|
||
fallbackEl.setAttribute('aria-hidden', 'true');
|
||
Object.assign(fallbackEl.style, HIDDEN_STYLES);
|
||
if (isInsidePortal && domReference) {
|
||
domReference.insertAdjacentElement('afterend', fallbackEl);
|
||
}
|
||
function getReturnElement() {
|
||
if (typeof returnFocusRef.current === 'boolean') {
|
||
const el = domReference || getPreviouslyFocusedElement();
|
||
return el && el.isConnected ? el : fallbackEl;
|
||
}
|
||
return returnFocusRef.current.current || fallbackEl;
|
||
}
|
||
return () => {
|
||
events.off('openchange', onOpenChange);
|
||
const activeEl = activeElement(doc);
|
||
const isFocusInsideFloatingTree = contains$1(floating, activeEl) || tree && getNodeChildren$1(tree.nodesRef.current, getNodeId(), false).some(node => {
|
||
var _node$context7;
|
||
return contains$1((_node$context7 = node.context) == null ? void 0 : _node$context7.elements.floating, activeEl);
|
||
});
|
||
const returnElement = getReturnElement();
|
||
queueMicrotask(() => {
|
||
// This is `returnElement`, if it's tabbable, or its first tabbable child.
|
||
const tabbableReturnElement = getFirstTabbableElement(returnElement);
|
||
if (
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
returnFocusRef.current && !preventReturnFocusRef.current && isHTMLElement(tabbableReturnElement) && (
|
||
// If the focus moved somewhere else after mount, avoid returning focus
|
||
// since it likely entered a different element which should be
|
||
// respected: https://github.com/floating-ui/floating-ui/issues/2607
|
||
tabbableReturnElement !== activeEl && activeEl !== doc.body ? isFocusInsideFloatingTree : true)) {
|
||
tabbableReturnElement.focus({
|
||
preventScroll: true
|
||
});
|
||
}
|
||
fallbackEl.remove();
|
||
});
|
||
};
|
||
}, [disabled, floating, floatingFocusElement, returnFocusRef, dataRef, events, tree, isInsidePortal, domReference, getNodeId]);
|
||
React.useEffect(() => {
|
||
// The `returnFocus` cleanup behavior is inside a microtask; ensure we
|
||
// wait for it to complete before resetting the flag.
|
||
queueMicrotask(() => {
|
||
preventReturnFocusRef.current = false;
|
||
});
|
||
return () => {
|
||
queueMicrotask(clearDisconnectedPreviouslyFocusedElements);
|
||
};
|
||
}, [disabled]);
|
||
|
||
// Synchronize the `context` & `modal` value to the FloatingPortal context.
|
||
// It will decide whether or not it needs to render its own guards.
|
||
useModernLayoutEffect(() => {
|
||
if (disabled) return;
|
||
if (!portalContext) return;
|
||
portalContext.setFocusManagerState({
|
||
modal,
|
||
closeOnFocusOut,
|
||
open,
|
||
onOpenChange,
|
||
domReference
|
||
});
|
||
return () => {
|
||
portalContext.setFocusManagerState(null);
|
||
};
|
||
}, [disabled, portalContext, modal, open, onOpenChange, closeOnFocusOut, domReference]);
|
||
useModernLayoutEffect(() => {
|
||
if (disabled) return;
|
||
if (!floatingFocusElement) return;
|
||
handleTabIndex(floatingFocusElement, orderRef);
|
||
}, [disabled, floatingFocusElement, orderRef]);
|
||
function renderDismissButton(location) {
|
||
if (disabled || !visuallyHiddenDismiss || !modal) {
|
||
return null;
|
||
}
|
||
return /*#__PURE__*/jsx(VisuallyHiddenDismiss, {
|
||
ref: location === 'start' ? startDismissButtonRef : endDismissButtonRef,
|
||
onClick: event => onOpenChange(false, event.nativeEvent),
|
||
children: typeof visuallyHiddenDismiss === 'string' ? visuallyHiddenDismiss : 'Dismiss'
|
||
});
|
||
}
|
||
const shouldRenderGuards = !disabled && guards && (modal ? !isUntrappedTypeableCombobox : true) && (isInsidePortal || modal);
|
||
return /*#__PURE__*/jsxs(Fragment, {
|
||
children: [shouldRenderGuards && /*#__PURE__*/jsx(FocusGuard, {
|
||
"data-type": "inside",
|
||
ref: mergedBeforeGuardRef,
|
||
onFocus: event => {
|
||
if (modal) {
|
||
const els = getTabbableElements();
|
||
enqueueFocus(order[0] === 'reference' ? els[0] : els[els.length - 1]);
|
||
} else if (portalContext != null && portalContext.preserveTabOrder && portalContext.portalNode) {
|
||
preventReturnFocusRef.current = false;
|
||
if (isOutsideEvent(event, portalContext.portalNode)) {
|
||
const nextTabbable = getNextTabbable(domReference);
|
||
nextTabbable == null || nextTabbable.focus();
|
||
} else {
|
||
var _portalContext$before;
|
||
(_portalContext$before = portalContext.beforeOutsideRef.current) == null || _portalContext$before.focus();
|
||
}
|
||
}
|
||
}
|
||
}), !isUntrappedTypeableCombobox && renderDismissButton('start'), children, renderDismissButton('end'), shouldRenderGuards && /*#__PURE__*/jsx(FocusGuard, {
|
||
"data-type": "inside",
|
||
ref: mergedAfterGuardRef,
|
||
onFocus: event => {
|
||
if (modal) {
|
||
enqueueFocus(getTabbableElements()[0]);
|
||
} else if (portalContext != null && portalContext.preserveTabOrder && portalContext.portalNode) {
|
||
if (closeOnFocusOut) {
|
||
preventReturnFocusRef.current = true;
|
||
}
|
||
if (isOutsideEvent(event, portalContext.portalNode)) {
|
||
const prevTabbable = getPreviousTabbable(domReference);
|
||
prevTabbable == null || prevTabbable.focus();
|
||
} else {
|
||
var _portalContext$afterO;
|
||
(_portalContext$afterO = portalContext.afterOutsideRef.current) == null || _portalContext$afterO.focus();
|
||
}
|
||
}
|
||
}
|
||
})]
|
||
});
|
||
}
|
||
|
||
let lockCount = 0;
|
||
const scrollbarProperty = '--floating-ui-scrollbar-width';
|
||
function enableScrollLock() {
|
||
const platform = getPlatform();
|
||
const isIOS = /iP(hone|ad|od)|iOS/.test(platform) ||
|
||
// iPads can claim to be MacIntel
|
||
platform === 'MacIntel' && navigator.maxTouchPoints > 1;
|
||
const bodyStyle = document.body.style;
|
||
// RTL <body> scrollbar
|
||
const scrollbarX = Math.round(document.documentElement.getBoundingClientRect().left) + document.documentElement.scrollLeft;
|
||
const paddingProp = scrollbarX ? 'paddingLeft' : 'paddingRight';
|
||
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
|
||
const scrollX = bodyStyle.left ? parseFloat(bodyStyle.left) : window.scrollX;
|
||
const scrollY = bodyStyle.top ? parseFloat(bodyStyle.top) : window.scrollY;
|
||
bodyStyle.overflow = 'hidden';
|
||
bodyStyle.setProperty(scrollbarProperty, scrollbarWidth + "px");
|
||
if (scrollbarWidth) {
|
||
bodyStyle[paddingProp] = scrollbarWidth + "px";
|
||
}
|
||
|
||
// Only iOS doesn't respect `overflow: hidden` on document.body, and this
|
||
// technique has fewer side effects.
|
||
if (isIOS) {
|
||
var _window$visualViewpor, _window$visualViewpor2;
|
||
// iOS 12 does not support `visualViewport`.
|
||
const offsetLeft = ((_window$visualViewpor = window.visualViewport) == null ? void 0 : _window$visualViewpor.offsetLeft) || 0;
|
||
const offsetTop = ((_window$visualViewpor2 = window.visualViewport) == null ? void 0 : _window$visualViewpor2.offsetTop) || 0;
|
||
Object.assign(bodyStyle, {
|
||
position: 'fixed',
|
||
top: -(scrollY - Math.floor(offsetTop)) + "px",
|
||
left: -(scrollX - Math.floor(offsetLeft)) + "px",
|
||
right: '0'
|
||
});
|
||
}
|
||
return () => {
|
||
Object.assign(bodyStyle, {
|
||
overflow: '',
|
||
[paddingProp]: ''
|
||
});
|
||
bodyStyle.removeProperty(scrollbarProperty);
|
||
if (isIOS) {
|
||
Object.assign(bodyStyle, {
|
||
position: '',
|
||
top: '',
|
||
left: '',
|
||
right: ''
|
||
});
|
||
window.scrollTo(scrollX, scrollY);
|
||
}
|
||
};
|
||
}
|
||
let cleanup = () => {};
|
||
|
||
/**
|
||
* Provides base styling for a fixed overlay element to dim content or block
|
||
* pointer events behind a floating element.
|
||
* It's a regular `<div>`, so it can be styled via any CSS solution you prefer.
|
||
* @see https://floating-ui.com/docs/FloatingOverlay
|
||
*/
|
||
const FloatingOverlay = /*#__PURE__*/React.forwardRef(function FloatingOverlay(props, ref) {
|
||
const {
|
||
lockScroll = false,
|
||
...rest
|
||
} = props;
|
||
useModernLayoutEffect(() => {
|
||
if (!lockScroll) return;
|
||
lockCount++;
|
||
if (lockCount === 1) {
|
||
cleanup = enableScrollLock();
|
||
}
|
||
return () => {
|
||
lockCount--;
|
||
if (lockCount === 0) {
|
||
cleanup();
|
||
}
|
||
};
|
||
}, [lockScroll]);
|
||
return /*#__PURE__*/jsx("div", {
|
||
ref: ref,
|
||
...rest,
|
||
style: {
|
||
position: 'fixed',
|
||
overflow: 'auto',
|
||
top: 0,
|
||
right: 0,
|
||
bottom: 0,
|
||
left: 0,
|
||
...rest.style
|
||
}
|
||
});
|
||
});
|
||
|
||
function isButtonTarget(event) {
|
||
return isHTMLElement(event.target) && event.target.tagName === 'BUTTON';
|
||
}
|
||
function isAnchorTarget(event) {
|
||
return isHTMLElement(event.target) && event.target.tagName === 'A';
|
||
}
|
||
function isSpaceIgnored(element) {
|
||
return isTypeableElement(element);
|
||
}
|
||
/**
|
||
* Opens or closes the floating element when clicking the reference element.
|
||
* @see https://floating-ui.com/docs/useClick
|
||
*/
|
||
function useClick(context, props) {
|
||
if (props === void 0) {
|
||
props = {};
|
||
}
|
||
const {
|
||
open,
|
||
onOpenChange,
|
||
dataRef,
|
||
elements: {
|
||
domReference
|
||
}
|
||
} = context;
|
||
const {
|
||
enabled = true,
|
||
event: eventOption = 'click',
|
||
toggle = true,
|
||
ignoreMouse = false,
|
||
keyboardHandlers = true,
|
||
stickIfOpen = true
|
||
} = props;
|
||
const pointerTypeRef = React.useRef();
|
||
const didKeyDownRef = React.useRef(false);
|
||
const reference = React.useMemo(() => ({
|
||
onPointerDown(event) {
|
||
pointerTypeRef.current = event.pointerType;
|
||
},
|
||
onMouseDown(event) {
|
||
const pointerType = pointerTypeRef.current;
|
||
|
||
// Ignore all buttons except for the "main" button.
|
||
// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button
|
||
if (event.button !== 0) return;
|
||
if (eventOption === 'click') return;
|
||
if (isMouseLikePointerType(pointerType, true) && ignoreMouse) return;
|
||
if (open && toggle && (dataRef.current.openEvent && stickIfOpen ? dataRef.current.openEvent.type === 'mousedown' : true)) {
|
||
onOpenChange(false, event.nativeEvent, 'click');
|
||
} else {
|
||
// Prevent stealing focus from the floating element
|
||
event.preventDefault();
|
||
onOpenChange(true, event.nativeEvent, 'click');
|
||
}
|
||
},
|
||
onClick(event) {
|
||
const pointerType = pointerTypeRef.current;
|
||
if (eventOption === 'mousedown' && pointerTypeRef.current) {
|
||
pointerTypeRef.current = undefined;
|
||
return;
|
||
}
|
||
if (isMouseLikePointerType(pointerType, true) && ignoreMouse) return;
|
||
if (open && toggle && (dataRef.current.openEvent && stickIfOpen ? dataRef.current.openEvent.type === 'click' : true)) {
|
||
onOpenChange(false, event.nativeEvent, 'click');
|
||
} else {
|
||
onOpenChange(true, event.nativeEvent, 'click');
|
||
}
|
||
},
|
||
onKeyDown(event) {
|
||
pointerTypeRef.current = undefined;
|
||
if (event.defaultPrevented || !keyboardHandlers || isButtonTarget(event)) {
|
||
return;
|
||
}
|
||
if (event.key === ' ' && !isSpaceIgnored(domReference)) {
|
||
// Prevent scrolling
|
||
event.preventDefault();
|
||
didKeyDownRef.current = true;
|
||
}
|
||
if (isAnchorTarget(event)) {
|
||
return;
|
||
}
|
||
if (event.key === 'Enter') {
|
||
if (open && toggle) {
|
||
onOpenChange(false, event.nativeEvent, 'click');
|
||
} else {
|
||
onOpenChange(true, event.nativeEvent, 'click');
|
||
}
|
||
}
|
||
},
|
||
onKeyUp(event) {
|
||
if (event.defaultPrevented || !keyboardHandlers || isButtonTarget(event) || isSpaceIgnored(domReference)) {
|
||
return;
|
||
}
|
||
if (event.key === ' ' && didKeyDownRef.current) {
|
||
didKeyDownRef.current = false;
|
||
if (open && toggle) {
|
||
onOpenChange(false, event.nativeEvent, 'click');
|
||
} else {
|
||
onOpenChange(true, event.nativeEvent, 'click');
|
||
}
|
||
}
|
||
}
|
||
}), [dataRef, domReference, eventOption, ignoreMouse, keyboardHandlers, onOpenChange, open, stickIfOpen, toggle]);
|
||
return React.useMemo(() => enabled ? {
|
||
reference
|
||
} : {}, [enabled, reference]);
|
||
}
|
||
|
||
function createVirtualElement(domElement, data) {
|
||
let offsetX = null;
|
||
let offsetY = null;
|
||
let isAutoUpdateEvent = false;
|
||
return {
|
||
contextElement: domElement || undefined,
|
||
getBoundingClientRect() {
|
||
var _data$dataRef$current;
|
||
const domRect = (domElement == null ? void 0 : domElement.getBoundingClientRect()) || {
|
||
width: 0,
|
||
height: 0,
|
||
x: 0,
|
||
y: 0
|
||
};
|
||
const isXAxis = data.axis === 'x' || data.axis === 'both';
|
||
const isYAxis = data.axis === 'y' || data.axis === 'both';
|
||
const canTrackCursorOnAutoUpdate = ['mouseenter', 'mousemove'].includes(((_data$dataRef$current = data.dataRef.current.openEvent) == null ? void 0 : _data$dataRef$current.type) || '') && data.pointerType !== 'touch';
|
||
let width = domRect.width;
|
||
let height = domRect.height;
|
||
let x = domRect.x;
|
||
let y = domRect.y;
|
||
if (offsetX == null && data.x && isXAxis) {
|
||
offsetX = domRect.x - data.x;
|
||
}
|
||
if (offsetY == null && data.y && isYAxis) {
|
||
offsetY = domRect.y - data.y;
|
||
}
|
||
x -= offsetX || 0;
|
||
y -= offsetY || 0;
|
||
width = 0;
|
||
height = 0;
|
||
if (!isAutoUpdateEvent || canTrackCursorOnAutoUpdate) {
|
||
width = data.axis === 'y' ? domRect.width : 0;
|
||
height = data.axis === 'x' ? domRect.height : 0;
|
||
x = isXAxis && data.x != null ? data.x : x;
|
||
y = isYAxis && data.y != null ? data.y : y;
|
||
} else if (isAutoUpdateEvent && !canTrackCursorOnAutoUpdate) {
|
||
height = data.axis === 'x' ? domRect.height : height;
|
||
width = data.axis === 'y' ? domRect.width : width;
|
||
}
|
||
isAutoUpdateEvent = true;
|
||
return {
|
||
width,
|
||
height,
|
||
x,
|
||
y,
|
||
top: y,
|
||
right: x + width,
|
||
bottom: y + height,
|
||
left: x
|
||
};
|
||
}
|
||
};
|
||
}
|
||
function isMouseBasedEvent(event) {
|
||
return event != null && event.clientX != null;
|
||
}
|
||
/**
|
||
* Positions the floating element relative to a client point (in the viewport),
|
||
* such as the mouse position. By default, it follows the mouse cursor.
|
||
* @see https://floating-ui.com/docs/useClientPoint
|
||
*/
|
||
function useClientPoint(context, props) {
|
||
if (props === void 0) {
|
||
props = {};
|
||
}
|
||
const {
|
||
open,
|
||
dataRef,
|
||
elements: {
|
||
floating,
|
||
domReference
|
||
},
|
||
refs
|
||
} = context;
|
||
const {
|
||
enabled = true,
|
||
axis = 'both',
|
||
x = null,
|
||
y = null
|
||
} = props;
|
||
const initialRef = React.useRef(false);
|
||
const cleanupListenerRef = React.useRef(null);
|
||
const [pointerType, setPointerType] = React.useState();
|
||
const [reactive, setReactive] = React.useState([]);
|
||
const setReference = useEffectEvent((x, y) => {
|
||
if (initialRef.current) return;
|
||
|
||
// Prevent setting if the open event was not a mouse-like one
|
||
// (e.g. focus to open, then hover over the reference element).
|
||
// Only apply if the event exists.
|
||
if (dataRef.current.openEvent && !isMouseBasedEvent(dataRef.current.openEvent)) {
|
||
return;
|
||
}
|
||
refs.setPositionReference(createVirtualElement(domReference, {
|
||
x,
|
||
y,
|
||
axis,
|
||
dataRef,
|
||
pointerType
|
||
}));
|
||
});
|
||
const handleReferenceEnterOrMove = useEffectEvent(event => {
|
||
if (x != null || y != null) return;
|
||
if (!open) {
|
||
setReference(event.clientX, event.clientY);
|
||
} else if (!cleanupListenerRef.current) {
|
||
// If there's no cleanup, there's no listener, but we want to ensure
|
||
// we add the listener if the cursor landed on the floating element and
|
||
// then back on the reference (i.e. it's interactive).
|
||
setReactive([]);
|
||
}
|
||
});
|
||
|
||
// If the pointer is a mouse-like pointer, we want to continue following the
|
||
// mouse even if the floating element is transitioning out. On touch
|
||
// devices, this is undesirable because the floating element will move to
|
||
// the dismissal touch point.
|
||
const openCheck = isMouseLikePointerType(pointerType) ? floating : open;
|
||
const addListener = React.useCallback(() => {
|
||
// Explicitly specified `x`/`y` coordinates shouldn't add a listener.
|
||
if (!openCheck || !enabled || x != null || y != null) return;
|
||
const win = getWindow(floating);
|
||
function handleMouseMove(event) {
|
||
const target = getTarget$1(event);
|
||
if (!contains$1(floating, target)) {
|
||
setReference(event.clientX, event.clientY);
|
||
} else {
|
||
win.removeEventListener('mousemove', handleMouseMove);
|
||
cleanupListenerRef.current = null;
|
||
}
|
||
}
|
||
if (!dataRef.current.openEvent || isMouseBasedEvent(dataRef.current.openEvent)) {
|
||
win.addEventListener('mousemove', handleMouseMove);
|
||
const cleanup = () => {
|
||
win.removeEventListener('mousemove', handleMouseMove);
|
||
cleanupListenerRef.current = null;
|
||
};
|
||
cleanupListenerRef.current = cleanup;
|
||
return cleanup;
|
||
}
|
||
refs.setPositionReference(domReference);
|
||
}, [openCheck, enabled, x, y, floating, dataRef, refs, domReference, setReference]);
|
||
React.useEffect(() => {
|
||
return addListener();
|
||
}, [addListener, reactive]);
|
||
React.useEffect(() => {
|
||
if (enabled && !floating) {
|
||
initialRef.current = false;
|
||
}
|
||
}, [enabled, floating]);
|
||
React.useEffect(() => {
|
||
if (!enabled && open) {
|
||
initialRef.current = true;
|
||
}
|
||
}, [enabled, open]);
|
||
useModernLayoutEffect(() => {
|
||
if (enabled && (x != null || y != null)) {
|
||
initialRef.current = false;
|
||
setReference(x, y);
|
||
}
|
||
}, [enabled, x, y, setReference]);
|
||
const reference = React.useMemo(() => {
|
||
function setPointerTypeRef(_ref) {
|
||
let {
|
||
pointerType
|
||
} = _ref;
|
||
setPointerType(pointerType);
|
||
}
|
||
return {
|
||
onPointerDown: setPointerTypeRef,
|
||
onPointerEnter: setPointerTypeRef,
|
||
onMouseMove: handleReferenceEnterOrMove,
|
||
onMouseEnter: handleReferenceEnterOrMove
|
||
};
|
||
}, [handleReferenceEnterOrMove]);
|
||
return React.useMemo(() => enabled ? {
|
||
reference
|
||
} : {}, [enabled, reference]);
|
||
}
|
||
|
||
const bubbleHandlerKeys = {
|
||
pointerdown: 'onPointerDown',
|
||
mousedown: 'onMouseDown',
|
||
click: 'onClick'
|
||
};
|
||
const captureHandlerKeys = {
|
||
pointerdown: 'onPointerDownCapture',
|
||
mousedown: 'onMouseDownCapture',
|
||
click: 'onClickCapture'
|
||
};
|
||
const normalizeProp = normalizable => {
|
||
var _normalizable$escapeK, _normalizable$outside;
|
||
return {
|
||
escapeKey: typeof normalizable === 'boolean' ? normalizable : (_normalizable$escapeK = normalizable == null ? void 0 : normalizable.escapeKey) != null ? _normalizable$escapeK : false,
|
||
outsidePress: typeof normalizable === 'boolean' ? normalizable : (_normalizable$outside = normalizable == null ? void 0 : normalizable.outsidePress) != null ? _normalizable$outside : true
|
||
};
|
||
};
|
||
/**
|
||
* Closes the floating element when a dismissal is requested — by default, when
|
||
* the user presses the `escape` key or outside of the floating element.
|
||
* @see https://floating-ui.com/docs/useDismiss
|
||
*/
|
||
function useDismiss(context, props) {
|
||
if (props === void 0) {
|
||
props = {};
|
||
}
|
||
const {
|
||
open,
|
||
onOpenChange,
|
||
elements,
|
||
dataRef
|
||
} = context;
|
||
const {
|
||
enabled = true,
|
||
escapeKey = true,
|
||
outsidePress: unstable_outsidePress = true,
|
||
outsidePressEvent = 'pointerdown',
|
||
referencePress = false,
|
||
referencePressEvent = 'pointerdown',
|
||
ancestorScroll = false,
|
||
bubbles,
|
||
capture
|
||
} = props;
|
||
const tree = useFloatingTree();
|
||
const outsidePressFn = useEffectEvent(typeof unstable_outsidePress === 'function' ? unstable_outsidePress : () => false);
|
||
const outsidePress = typeof unstable_outsidePress === 'function' ? outsidePressFn : unstable_outsidePress;
|
||
const endedOrStartedInsideRef = React.useRef(false);
|
||
const {
|
||
escapeKey: escapeKeyBubbles,
|
||
outsidePress: outsidePressBubbles
|
||
} = normalizeProp(bubbles);
|
||
const {
|
||
escapeKey: escapeKeyCapture,
|
||
outsidePress: outsidePressCapture
|
||
} = normalizeProp(capture);
|
||
const isComposingRef = React.useRef(false);
|
||
const closeOnEscapeKeyDown = useEffectEvent(event => {
|
||
var _dataRef$current$floa;
|
||
if (!open || !enabled || !escapeKey || event.key !== 'Escape') {
|
||
return;
|
||
}
|
||
|
||
// Wait until IME is settled. Pressing `Escape` while composing should
|
||
// close the compose menu, but not the floating element.
|
||
if (isComposingRef.current) {
|
||
return;
|
||
}
|
||
const nodeId = (_dataRef$current$floa = dataRef.current.floatingContext) == null ? void 0 : _dataRef$current$floa.nodeId;
|
||
const children = tree ? getNodeChildren$1(tree.nodesRef.current, nodeId) : [];
|
||
if (!escapeKeyBubbles) {
|
||
event.stopPropagation();
|
||
if (children.length > 0) {
|
||
let shouldDismiss = true;
|
||
children.forEach(child => {
|
||
var _child$context;
|
||
if ((_child$context = child.context) != null && _child$context.open && !child.context.dataRef.current.__escapeKeyBubbles) {
|
||
shouldDismiss = false;
|
||
return;
|
||
}
|
||
});
|
||
if (!shouldDismiss) {
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
onOpenChange(false, isReactEvent(event) ? event.nativeEvent : event, 'escape-key');
|
||
});
|
||
const closeOnEscapeKeyDownCapture = useEffectEvent(event => {
|
||
var _getTarget2;
|
||
const callback = () => {
|
||
var _getTarget;
|
||
closeOnEscapeKeyDown(event);
|
||
(_getTarget = getTarget$1(event)) == null || _getTarget.removeEventListener('keydown', callback);
|
||
};
|
||
(_getTarget2 = getTarget$1(event)) == null || _getTarget2.addEventListener('keydown', callback);
|
||
});
|
||
const closeOnPressOutside = useEffectEvent(event => {
|
||
var _dataRef$current$floa2;
|
||
// Given developers can stop the propagation of the synthetic event,
|
||
// we can only be confident with a positive value.
|
||
const insideReactTree = dataRef.current.insideReactTree;
|
||
dataRef.current.insideReactTree = false;
|
||
|
||
// When click outside is lazy (`click` event), handle dragging.
|
||
// Don't close if:
|
||
// - The click started inside the floating element.
|
||
// - The click ended inside the floating element.
|
||
const endedOrStartedInside = endedOrStartedInsideRef.current;
|
||
endedOrStartedInsideRef.current = false;
|
||
if (outsidePressEvent === 'click' && endedOrStartedInside) {
|
||
return;
|
||
}
|
||
if (insideReactTree) {
|
||
return;
|
||
}
|
||
if (typeof outsidePress === 'function' && !outsidePress(event)) {
|
||
return;
|
||
}
|
||
const target = getTarget$1(event);
|
||
const inertSelector = "[" + createAttribute('inert') + "]";
|
||
const markers = getDocument$1(elements.floating).querySelectorAll(inertSelector);
|
||
let targetRootAncestor = isElement(target) ? target : null;
|
||
while (targetRootAncestor && !isLastTraversableNode(targetRootAncestor)) {
|
||
const nextParent = getParentNode(targetRootAncestor);
|
||
if (isLastTraversableNode(nextParent) || !isElement(nextParent)) {
|
||
break;
|
||
}
|
||
targetRootAncestor = nextParent;
|
||
}
|
||
|
||
// Check if the click occurred on a third-party element injected after the
|
||
// floating element rendered.
|
||
if (markers.length && isElement(target) && !isRootElement(target) &&
|
||
// Clicked on a direct ancestor (e.g. FloatingOverlay).
|
||
!contains$1(target, elements.floating) &&
|
||
// If the target root element contains none of the markers, then the
|
||
// element was injected after the floating element rendered.
|
||
Array.from(markers).every(marker => !contains$1(targetRootAncestor, marker))) {
|
||
return;
|
||
}
|
||
|
||
// Check if the click occurred on the scrollbar
|
||
if (isHTMLElement(target) && floating) {
|
||
const lastTraversableNode = isLastTraversableNode(target);
|
||
const style = getComputedStyle(target);
|
||
const scrollRe = /auto|scroll/;
|
||
const isScrollableX = lastTraversableNode || scrollRe.test(style.overflowX);
|
||
const isScrollableY = lastTraversableNode || scrollRe.test(style.overflowY);
|
||
const canScrollX = isScrollableX && target.clientWidth > 0 && target.scrollWidth > target.clientWidth;
|
||
const canScrollY = isScrollableY && target.clientHeight > 0 && target.scrollHeight > target.clientHeight;
|
||
const isRTL = style.direction === 'rtl';
|
||
|
||
// Check click position relative to scrollbar.
|
||
// In some browsers it is possible to change the <body> (or window)
|
||
// scrollbar to the left side, but is very rare and is difficult to
|
||
// check for. Plus, for modal dialogs with backdrops, it is more
|
||
// important that the backdrop is checked but not so much the window.
|
||
const pressedVerticalScrollbar = canScrollY && (isRTL ? event.offsetX <= target.offsetWidth - target.clientWidth : event.offsetX > target.clientWidth);
|
||
const pressedHorizontalScrollbar = canScrollX && event.offsetY > target.clientHeight;
|
||
if (pressedVerticalScrollbar || pressedHorizontalScrollbar) {
|
||
return;
|
||
}
|
||
}
|
||
const nodeId = (_dataRef$current$floa2 = dataRef.current.floatingContext) == null ? void 0 : _dataRef$current$floa2.nodeId;
|
||
const targetIsInsideChildren = tree && getNodeChildren$1(tree.nodesRef.current, nodeId).some(node => {
|
||
var _node$context;
|
||
return isEventTargetWithin(event, (_node$context = node.context) == null ? void 0 : _node$context.elements.floating);
|
||
});
|
||
if (isEventTargetWithin(event, elements.floating) || isEventTargetWithin(event, elements.domReference) || targetIsInsideChildren) {
|
||
return;
|
||
}
|
||
const children = tree ? getNodeChildren$1(tree.nodesRef.current, nodeId) : [];
|
||
if (children.length > 0) {
|
||
let shouldDismiss = true;
|
||
children.forEach(child => {
|
||
var _child$context2;
|
||
if ((_child$context2 = child.context) != null && _child$context2.open && !child.context.dataRef.current.__outsidePressBubbles) {
|
||
shouldDismiss = false;
|
||
return;
|
||
}
|
||
});
|
||
if (!shouldDismiss) {
|
||
return;
|
||
}
|
||
}
|
||
onOpenChange(false, event, 'outside-press');
|
||
});
|
||
const closeOnPressOutsideCapture = useEffectEvent(event => {
|
||
var _getTarget4;
|
||
const callback = () => {
|
||
var _getTarget3;
|
||
closeOnPressOutside(event);
|
||
(_getTarget3 = getTarget$1(event)) == null || _getTarget3.removeEventListener(outsidePressEvent, callback);
|
||
};
|
||
(_getTarget4 = getTarget$1(event)) == null || _getTarget4.addEventListener(outsidePressEvent, callback);
|
||
});
|
||
React.useEffect(() => {
|
||
if (!open || !enabled) {
|
||
return;
|
||
}
|
||
dataRef.current.__escapeKeyBubbles = escapeKeyBubbles;
|
||
dataRef.current.__outsidePressBubbles = outsidePressBubbles;
|
||
let compositionTimeout = -1;
|
||
function onScroll(event) {
|
||
onOpenChange(false, event, 'ancestor-scroll');
|
||
}
|
||
function handleCompositionStart() {
|
||
window.clearTimeout(compositionTimeout);
|
||
isComposingRef.current = true;
|
||
}
|
||
function handleCompositionEnd() {
|
||
// Safari fires `compositionend` before `keydown`, so we need to wait
|
||
// until the next tick to set `isComposing` to `false`.
|
||
// https://bugs.webkit.org/show_bug.cgi?id=165004
|
||
compositionTimeout = window.setTimeout(() => {
|
||
isComposingRef.current = false;
|
||
},
|
||
// 0ms or 1ms don't work in Safari. 5ms appears to consistently work.
|
||
// Only apply to WebKit for the test to remain 0ms.
|
||
isWebKit() ? 5 : 0);
|
||
}
|
||
const doc = getDocument$1(elements.floating);
|
||
if (escapeKey) {
|
||
doc.addEventListener('keydown', escapeKeyCapture ? closeOnEscapeKeyDownCapture : closeOnEscapeKeyDown, escapeKeyCapture);
|
||
doc.addEventListener('compositionstart', handleCompositionStart);
|
||
doc.addEventListener('compositionend', handleCompositionEnd);
|
||
}
|
||
outsidePress && doc.addEventListener(outsidePressEvent, outsidePressCapture ? closeOnPressOutsideCapture : closeOnPressOutside, outsidePressCapture);
|
||
let ancestors = [];
|
||
if (ancestorScroll) {
|
||
if (isElement(elements.domReference)) {
|
||
ancestors = getOverflowAncestors(elements.domReference);
|
||
}
|
||
if (isElement(elements.floating)) {
|
||
ancestors = ancestors.concat(getOverflowAncestors(elements.floating));
|
||
}
|
||
if (!isElement(elements.reference) && elements.reference && elements.reference.contextElement) {
|
||
ancestors = ancestors.concat(getOverflowAncestors(elements.reference.contextElement));
|
||
}
|
||
}
|
||
|
||
// Ignore the visual viewport for scrolling dismissal (allow pinch-zoom)
|
||
ancestors = ancestors.filter(ancestor => {
|
||
var _doc$defaultView;
|
||
return ancestor !== ((_doc$defaultView = doc.defaultView) == null ? void 0 : _doc$defaultView.visualViewport);
|
||
});
|
||
ancestors.forEach(ancestor => {
|
||
ancestor.addEventListener('scroll', onScroll, {
|
||
passive: true
|
||
});
|
||
});
|
||
return () => {
|
||
if (escapeKey) {
|
||
doc.removeEventListener('keydown', escapeKeyCapture ? closeOnEscapeKeyDownCapture : closeOnEscapeKeyDown, escapeKeyCapture);
|
||
doc.removeEventListener('compositionstart', handleCompositionStart);
|
||
doc.removeEventListener('compositionend', handleCompositionEnd);
|
||
}
|
||
outsidePress && doc.removeEventListener(outsidePressEvent, outsidePressCapture ? closeOnPressOutsideCapture : closeOnPressOutside, outsidePressCapture);
|
||
ancestors.forEach(ancestor => {
|
||
ancestor.removeEventListener('scroll', onScroll);
|
||
});
|
||
window.clearTimeout(compositionTimeout);
|
||
};
|
||
}, [dataRef, elements, escapeKey, outsidePress, outsidePressEvent, open, onOpenChange, ancestorScroll, enabled, escapeKeyBubbles, outsidePressBubbles, closeOnEscapeKeyDown, escapeKeyCapture, closeOnEscapeKeyDownCapture, closeOnPressOutside, outsidePressCapture, closeOnPressOutsideCapture]);
|
||
React.useEffect(() => {
|
||
dataRef.current.insideReactTree = false;
|
||
}, [dataRef, outsidePress, outsidePressEvent]);
|
||
const reference = React.useMemo(() => ({
|
||
onKeyDown: closeOnEscapeKeyDown,
|
||
...(referencePress && {
|
||
[bubbleHandlerKeys[referencePressEvent]]: event => {
|
||
onOpenChange(false, event.nativeEvent, 'reference-press');
|
||
},
|
||
...(referencePressEvent !== 'click' && {
|
||
onClick(event) {
|
||
onOpenChange(false, event.nativeEvent, 'reference-press');
|
||
}
|
||
})
|
||
})
|
||
}), [closeOnEscapeKeyDown, onOpenChange, referencePress, referencePressEvent]);
|
||
const floating = React.useMemo(() => ({
|
||
onKeyDown: closeOnEscapeKeyDown,
|
||
onMouseDown() {
|
||
endedOrStartedInsideRef.current = true;
|
||
},
|
||
onMouseUp() {
|
||
endedOrStartedInsideRef.current = true;
|
||
},
|
||
[captureHandlerKeys[outsidePressEvent]]: () => {
|
||
dataRef.current.insideReactTree = true;
|
||
}
|
||
}), [closeOnEscapeKeyDown, outsidePressEvent, dataRef]);
|
||
return React.useMemo(() => enabled ? {
|
||
reference,
|
||
floating
|
||
} : {}, [enabled, reference, floating]);
|
||
}
|
||
|
||
function useFloatingRootContext(options) {
|
||
const {
|
||
open = false,
|
||
onOpenChange: onOpenChangeProp,
|
||
elements: elementsProp
|
||
} = options;
|
||
const floatingId = useId();
|
||
const dataRef = React.useRef({});
|
||
const [events] = React.useState(() => createEventEmitter());
|
||
const nested = useFloatingParentNodeId() != null;
|
||
if (process.env.NODE_ENV !== "production") {
|
||
const optionDomReference = elementsProp.reference;
|
||
if (optionDomReference && !isElement(optionDomReference)) {
|
||
error('Cannot pass a virtual element to the `elements.reference` option,', 'as it must be a real DOM element. Use `refs.setPositionReference()`', 'instead.');
|
||
}
|
||
}
|
||
const [positionReference, setPositionReference] = React.useState(elementsProp.reference);
|
||
const onOpenChange = useEffectEvent((open, event, reason) => {
|
||
dataRef.current.openEvent = open ? event : undefined;
|
||
events.emit('openchange', {
|
||
open,
|
||
event,
|
||
reason,
|
||
nested
|
||
});
|
||
onOpenChangeProp == null || onOpenChangeProp(open, event, reason);
|
||
});
|
||
const refs = React.useMemo(() => ({
|
||
setPositionReference
|
||
}), []);
|
||
const elements = React.useMemo(() => ({
|
||
reference: positionReference || elementsProp.reference || null,
|
||
floating: elementsProp.floating || null,
|
||
domReference: elementsProp.reference
|
||
}), [positionReference, elementsProp.reference, elementsProp.floating]);
|
||
return React.useMemo(() => ({
|
||
dataRef,
|
||
open,
|
||
onOpenChange,
|
||
elements,
|
||
events,
|
||
floatingId,
|
||
refs
|
||
}), [open, onOpenChange, elements, events, floatingId, refs]);
|
||
}
|
||
|
||
/**
|
||
* Provides data to position a floating element and context to add interactions.
|
||
* @see https://floating-ui.com/docs/useFloating
|
||
*/
|
||
function useFloating(options) {
|
||
if (options === void 0) {
|
||
options = {};
|
||
}
|
||
const {
|
||
nodeId
|
||
} = options;
|
||
const internalRootContext = useFloatingRootContext({
|
||
...options,
|
||
elements: {
|
||
reference: null,
|
||
floating: null,
|
||
...options.elements
|
||
}
|
||
});
|
||
const rootContext = options.rootContext || internalRootContext;
|
||
const computedElements = rootContext.elements;
|
||
const [_domReference, setDomReference] = React.useState(null);
|
||
const [positionReference, _setPositionReference] = React.useState(null);
|
||
const optionDomReference = computedElements == null ? void 0 : computedElements.domReference;
|
||
const domReference = optionDomReference || _domReference;
|
||
const domReferenceRef = React.useRef(null);
|
||
const tree = useFloatingTree();
|
||
useModernLayoutEffect(() => {
|
||
if (domReference) {
|
||
domReferenceRef.current = domReference;
|
||
}
|
||
}, [domReference]);
|
||
const position = useFloating$1({
|
||
...options,
|
||
elements: {
|
||
...computedElements,
|
||
...(positionReference && {
|
||
reference: positionReference
|
||
})
|
||
}
|
||
});
|
||
const setPositionReference = React.useCallback(node => {
|
||
const computedPositionReference = isElement(node) ? {
|
||
getBoundingClientRect: () => node.getBoundingClientRect(),
|
||
getClientRects: () => node.getClientRects(),
|
||
contextElement: node
|
||
} : node;
|
||
// Store the positionReference in state if the DOM reference is specified externally via the
|
||
// `elements.reference` option. This ensures that it won't be overridden on future renders.
|
||
_setPositionReference(computedPositionReference);
|
||
position.refs.setReference(computedPositionReference);
|
||
}, [position.refs]);
|
||
const setReference = React.useCallback(node => {
|
||
if (isElement(node) || node === null) {
|
||
domReferenceRef.current = node;
|
||
setDomReference(node);
|
||
}
|
||
|
||
// Backwards-compatibility for passing a virtual element to `reference`
|
||
// after it has set the DOM reference.
|
||
if (isElement(position.refs.reference.current) || position.refs.reference.current === null ||
|
||
// Don't allow setting virtual elements using the old technique back to
|
||
// `null` to support `positionReference` + an unstable `reference`
|
||
// callback ref.
|
||
node !== null && !isElement(node)) {
|
||
position.refs.setReference(node);
|
||
}
|
||
}, [position.refs]);
|
||
const refs = React.useMemo(() => ({
|
||
...position.refs,
|
||
setReference,
|
||
setPositionReference,
|
||
domReference: domReferenceRef
|
||
}), [position.refs, setReference, setPositionReference]);
|
||
const elements = React.useMemo(() => ({
|
||
...position.elements,
|
||
domReference: domReference
|
||
}), [position.elements, domReference]);
|
||
const context = React.useMemo(() => ({
|
||
...position,
|
||
...rootContext,
|
||
refs,
|
||
elements,
|
||
nodeId
|
||
}), [position, refs, elements, nodeId, rootContext]);
|
||
useModernLayoutEffect(() => {
|
||
rootContext.dataRef.current.floatingContext = context;
|
||
const node = tree == null ? void 0 : tree.nodesRef.current.find(node => node.id === nodeId);
|
||
if (node) {
|
||
node.context = context;
|
||
}
|
||
});
|
||
return React.useMemo(() => ({
|
||
...position,
|
||
context,
|
||
refs,
|
||
elements
|
||
}), [position, refs, elements, context]);
|
||
}
|
||
|
||
function isMacSafari() {
|
||
return isMac() && isSafari();
|
||
}
|
||
/**
|
||
* Opens the floating element while the reference element has focus, like CSS
|
||
* `:focus`.
|
||
* @see https://floating-ui.com/docs/useFocus
|
||
*/
|
||
function useFocus(context, props) {
|
||
if (props === void 0) {
|
||
props = {};
|
||
}
|
||
const {
|
||
open,
|
||
onOpenChange,
|
||
events,
|
||
dataRef,
|
||
elements
|
||
} = context;
|
||
const {
|
||
enabled = true,
|
||
visibleOnly = true
|
||
} = props;
|
||
const blockFocusRef = React.useRef(false);
|
||
const timeoutRef = React.useRef(-1);
|
||
const keyboardModalityRef = React.useRef(true);
|
||
React.useEffect(() => {
|
||
if (!enabled) return;
|
||
const win = getWindow(elements.domReference);
|
||
|
||
// If the reference was focused and the user left the tab/window, and the
|
||
// floating element was not open, the focus should be blocked when they
|
||
// return to the tab/window.
|
||
function onBlur() {
|
||
if (!open && isHTMLElement(elements.domReference) && elements.domReference === activeElement(getDocument$1(elements.domReference))) {
|
||
blockFocusRef.current = true;
|
||
}
|
||
}
|
||
function onKeyDown() {
|
||
keyboardModalityRef.current = true;
|
||
}
|
||
function onPointerDown() {
|
||
keyboardModalityRef.current = false;
|
||
}
|
||
win.addEventListener('blur', onBlur);
|
||
if (isMacSafari()) {
|
||
win.addEventListener('keydown', onKeyDown, true);
|
||
win.addEventListener('pointerdown', onPointerDown, true);
|
||
}
|
||
return () => {
|
||
win.removeEventListener('blur', onBlur);
|
||
if (isMacSafari()) {
|
||
win.removeEventListener('keydown', onKeyDown, true);
|
||
win.removeEventListener('pointerdown', onPointerDown, true);
|
||
}
|
||
};
|
||
}, [elements.domReference, open, enabled]);
|
||
React.useEffect(() => {
|
||
if (!enabled) return;
|
||
function onOpenChange(_ref) {
|
||
let {
|
||
reason
|
||
} = _ref;
|
||
if (reason === 'reference-press' || reason === 'escape-key') {
|
||
blockFocusRef.current = true;
|
||
}
|
||
}
|
||
events.on('openchange', onOpenChange);
|
||
return () => {
|
||
events.off('openchange', onOpenChange);
|
||
};
|
||
}, [events, enabled]);
|
||
React.useEffect(() => {
|
||
return () => {
|
||
clearTimeoutIfSet(timeoutRef);
|
||
};
|
||
}, []);
|
||
const reference = React.useMemo(() => ({
|
||
onMouseLeave() {
|
||
blockFocusRef.current = false;
|
||
},
|
||
onFocus(event) {
|
||
if (blockFocusRef.current) return;
|
||
const target = getTarget$1(event.nativeEvent);
|
||
if (visibleOnly && isElement(target)) {
|
||
// Safari fails to match `:focus-visible` if focus was initially
|
||
// outside the document.
|
||
if (isMacSafari() && !event.relatedTarget) {
|
||
if (!keyboardModalityRef.current && !isTypeableElement(target)) {
|
||
return;
|
||
}
|
||
} else if (!matchesFocusVisible(target)) {
|
||
return;
|
||
}
|
||
}
|
||
onOpenChange(true, event.nativeEvent, 'focus');
|
||
},
|
||
onBlur(event) {
|
||
blockFocusRef.current = false;
|
||
const relatedTarget = event.relatedTarget;
|
||
const nativeEvent = event.nativeEvent;
|
||
|
||
// Hit the non-modal focus management portal guard. Focus will be
|
||
// moved into the floating element immediately after.
|
||
const movedToFocusGuard = isElement(relatedTarget) && relatedTarget.hasAttribute(createAttribute('focus-guard')) && relatedTarget.getAttribute('data-type') === 'outside';
|
||
|
||
// Wait for the window blur listener to fire.
|
||
timeoutRef.current = window.setTimeout(() => {
|
||
var _dataRef$current$floa;
|
||
const activeEl = activeElement(elements.domReference ? elements.domReference.ownerDocument : document);
|
||
|
||
// Focus left the page, keep it open.
|
||
if (!relatedTarget && activeEl === elements.domReference) return;
|
||
|
||
// When focusing the reference element (e.g. regular click), then
|
||
// clicking into the floating element, prevent it from hiding.
|
||
// Note: it must be focusable, e.g. `tabindex="-1"`.
|
||
// We can not rely on relatedTarget to point to the correct element
|
||
// as it will only point to the shadow host of the newly focused element
|
||
// and not the element that actually has received focus if it is located
|
||
// inside a shadow root.
|
||
if (contains$1((_dataRef$current$floa = dataRef.current.floatingContext) == null ? void 0 : _dataRef$current$floa.refs.floating.current, activeEl) || contains$1(elements.domReference, activeEl) || movedToFocusGuard) {
|
||
return;
|
||
}
|
||
onOpenChange(false, nativeEvent, 'focus');
|
||
});
|
||
}
|
||
}), [dataRef, elements.domReference, onOpenChange, visibleOnly]);
|
||
return React.useMemo(() => enabled ? {
|
||
reference
|
||
} : {}, [enabled, reference]);
|
||
}
|
||
|
||
function mergeProps(userProps, propsList, elementKey) {
|
||
const map = new Map();
|
||
const isItem = elementKey === 'item';
|
||
let domUserProps = userProps;
|
||
if (isItem && userProps) {
|
||
const {
|
||
[ACTIVE_KEY]: _,
|
||
[SELECTED_KEY]: __,
|
||
...validProps
|
||
} = userProps;
|
||
domUserProps = validProps;
|
||
}
|
||
return {
|
||
...(elementKey === 'floating' && {
|
||
tabIndex: -1,
|
||
[FOCUSABLE_ATTRIBUTE]: ''
|
||
}),
|
||
...domUserProps,
|
||
...propsList.map(value => {
|
||
const propsOrGetProps = value ? value[elementKey] : null;
|
||
if (typeof propsOrGetProps === 'function') {
|
||
return userProps ? propsOrGetProps(userProps) : null;
|
||
}
|
||
return propsOrGetProps;
|
||
}).concat(userProps).reduce((acc, props) => {
|
||
if (!props) {
|
||
return acc;
|
||
}
|
||
Object.entries(props).forEach(_ref => {
|
||
let [key, value] = _ref;
|
||
if (isItem && [ACTIVE_KEY, SELECTED_KEY].includes(key)) {
|
||
return;
|
||
}
|
||
if (key.indexOf('on') === 0) {
|
||
if (!map.has(key)) {
|
||
map.set(key, []);
|
||
}
|
||
if (typeof value === 'function') {
|
||
var _map$get;
|
||
(_map$get = map.get(key)) == null || _map$get.push(value);
|
||
acc[key] = function () {
|
||
var _map$get2;
|
||
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
|
||
args[_key] = arguments[_key];
|
||
}
|
||
return (_map$get2 = map.get(key)) == null ? void 0 : _map$get2.map(fn => fn(...args)).find(val => val !== undefined);
|
||
};
|
||
}
|
||
} else {
|
||
acc[key] = value;
|
||
}
|
||
});
|
||
return acc;
|
||
}, {})
|
||
};
|
||
}
|
||
/**
|
||
* Merges an array of interaction hooks' props into prop getters, allowing
|
||
* event handler functions to be composed together without overwriting one
|
||
* another.
|
||
* @see https://floating-ui.com/docs/useInteractions
|
||
*/
|
||
function useInteractions(propsList) {
|
||
if (propsList === void 0) {
|
||
propsList = [];
|
||
}
|
||
const referenceDeps = propsList.map(key => key == null ? void 0 : key.reference);
|
||
const floatingDeps = propsList.map(key => key == null ? void 0 : key.floating);
|
||
const itemDeps = propsList.map(key => key == null ? void 0 : key.item);
|
||
const getReferenceProps = React.useCallback(userProps => mergeProps(userProps, propsList, 'reference'),
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
referenceDeps);
|
||
const getFloatingProps = React.useCallback(userProps => mergeProps(userProps, propsList, 'floating'),
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
floatingDeps);
|
||
const getItemProps = React.useCallback(userProps => mergeProps(userProps, propsList, 'item'),
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
itemDeps);
|
||
return React.useMemo(() => ({
|
||
getReferenceProps,
|
||
getFloatingProps,
|
||
getItemProps
|
||
}), [getReferenceProps, getFloatingProps, getItemProps]);
|
||
}
|
||
|
||
const ESCAPE = 'Escape';
|
||
function doSwitch(orientation, vertical, horizontal) {
|
||
switch (orientation) {
|
||
case 'vertical':
|
||
return vertical;
|
||
case 'horizontal':
|
||
return horizontal;
|
||
default:
|
||
return vertical || horizontal;
|
||
}
|
||
}
|
||
function isMainOrientationKey(key, orientation) {
|
||
const vertical = key === ARROW_UP || key === ARROW_DOWN;
|
||
const horizontal = key === ARROW_LEFT || key === ARROW_RIGHT;
|
||
return doSwitch(orientation, vertical, horizontal);
|
||
}
|
||
function isMainOrientationToEndKey(key, orientation, rtl) {
|
||
const vertical = key === ARROW_DOWN;
|
||
const horizontal = rtl ? key === ARROW_LEFT : key === ARROW_RIGHT;
|
||
return doSwitch(orientation, vertical, horizontal) || key === 'Enter' || key === ' ' || key === '';
|
||
}
|
||
function isCrossOrientationOpenKey(key, orientation, rtl) {
|
||
const vertical = rtl ? key === ARROW_LEFT : key === ARROW_RIGHT;
|
||
const horizontal = key === ARROW_DOWN;
|
||
return doSwitch(orientation, vertical, horizontal);
|
||
}
|
||
function isCrossOrientationCloseKey(key, orientation, rtl, cols) {
|
||
const vertical = rtl ? key === ARROW_RIGHT : key === ARROW_LEFT;
|
||
const horizontal = key === ARROW_UP;
|
||
if (orientation === 'both' || orientation === 'horizontal' && cols && cols > 1) {
|
||
return key === ESCAPE;
|
||
}
|
||
return doSwitch(orientation, vertical, horizontal);
|
||
}
|
||
/**
|
||
* Adds arrow key-based navigation of a list of items, either using real DOM
|
||
* focus or virtual focus.
|
||
* @see https://floating-ui.com/docs/useListNavigation
|
||
*/
|
||
function useListNavigation(context, props) {
|
||
const {
|
||
open,
|
||
onOpenChange,
|
||
elements,
|
||
floatingId
|
||
} = context;
|
||
const {
|
||
listRef,
|
||
activeIndex,
|
||
onNavigate: unstable_onNavigate = () => {},
|
||
enabled = true,
|
||
selectedIndex = null,
|
||
allowEscape = false,
|
||
loop = false,
|
||
nested = false,
|
||
rtl = false,
|
||
virtual = false,
|
||
focusItemOnOpen = 'auto',
|
||
focusItemOnHover = true,
|
||
openOnArrowKeyDown = true,
|
||
disabledIndices = undefined,
|
||
orientation = 'vertical',
|
||
parentOrientation,
|
||
cols = 1,
|
||
scrollItemIntoView = true,
|
||
virtualItemRef,
|
||
itemSizes,
|
||
dense = false
|
||
} = props;
|
||
if (process.env.NODE_ENV !== "production") {
|
||
if (allowEscape) {
|
||
if (!loop) {
|
||
warn('`useListNavigation` looping must be enabled to allow escaping.');
|
||
}
|
||
if (!virtual) {
|
||
warn('`useListNavigation` must be virtual to allow escaping.');
|
||
}
|
||
}
|
||
if (orientation === 'vertical' && cols > 1) {
|
||
warn('In grid list navigation mode (`cols` > 1), the `orientation` should', 'be either "horizontal" or "both".');
|
||
}
|
||
}
|
||
const floatingFocusElement = getFloatingFocusElement(elements.floating);
|
||
const floatingFocusElementRef = useLatestRef(floatingFocusElement);
|
||
const parentId = useFloatingParentNodeId();
|
||
const tree = useFloatingTree();
|
||
useModernLayoutEffect(() => {
|
||
context.dataRef.current.orientation = orientation;
|
||
}, [context, orientation]);
|
||
const onNavigate = useEffectEvent(() => {
|
||
unstable_onNavigate(indexRef.current === -1 ? null : indexRef.current);
|
||
});
|
||
const typeableComboboxReference = isTypeableCombobox(elements.domReference);
|
||
const focusItemOnOpenRef = React.useRef(focusItemOnOpen);
|
||
const indexRef = React.useRef(selectedIndex != null ? selectedIndex : -1);
|
||
const keyRef = React.useRef(null);
|
||
const isPointerModalityRef = React.useRef(true);
|
||
const previousOnNavigateRef = React.useRef(onNavigate);
|
||
const previousMountedRef = React.useRef(!!elements.floating);
|
||
const previousOpenRef = React.useRef(open);
|
||
const forceSyncFocusRef = React.useRef(false);
|
||
const forceScrollIntoViewRef = React.useRef(false);
|
||
const disabledIndicesRef = useLatestRef(disabledIndices);
|
||
const latestOpenRef = useLatestRef(open);
|
||
const scrollItemIntoViewRef = useLatestRef(scrollItemIntoView);
|
||
const selectedIndexRef = useLatestRef(selectedIndex);
|
||
const [activeId, setActiveId] = React.useState();
|
||
const [virtualId, setVirtualId] = React.useState();
|
||
const focusItem = useEffectEvent(() => {
|
||
function runFocus(item) {
|
||
if (virtual) {
|
||
var _item$id;
|
||
if ((_item$id = item.id) != null && _item$id.endsWith('-fui-option')) {
|
||
item.id = floatingId + "-" + Math.random().toString(16).slice(2, 10);
|
||
}
|
||
setActiveId(item.id);
|
||
tree == null || tree.events.emit('virtualfocus', item);
|
||
if (virtualItemRef) {
|
||
virtualItemRef.current = item;
|
||
}
|
||
} else {
|
||
enqueueFocus(item, {
|
||
sync: forceSyncFocusRef.current,
|
||
preventScroll: true
|
||
});
|
||
}
|
||
}
|
||
const initialItem = listRef.current[indexRef.current];
|
||
const forceScrollIntoView = forceScrollIntoViewRef.current;
|
||
if (initialItem) {
|
||
runFocus(initialItem);
|
||
}
|
||
const scheduler = forceSyncFocusRef.current ? v => v() : requestAnimationFrame;
|
||
scheduler(() => {
|
||
const waitedItem = listRef.current[indexRef.current] || initialItem;
|
||
if (!waitedItem) return;
|
||
if (!initialItem) {
|
||
runFocus(waitedItem);
|
||
}
|
||
const scrollIntoViewOptions = scrollItemIntoViewRef.current;
|
||
const shouldScrollIntoView = scrollIntoViewOptions && item && (forceScrollIntoView || !isPointerModalityRef.current);
|
||
if (shouldScrollIntoView) {
|
||
// JSDOM doesn't support `.scrollIntoView()` but it's widely supported
|
||
// by all browsers.
|
||
waitedItem.scrollIntoView == null || waitedItem.scrollIntoView(typeof scrollIntoViewOptions === 'boolean' ? {
|
||
block: 'nearest',
|
||
inline: 'nearest'
|
||
} : scrollIntoViewOptions);
|
||
}
|
||
});
|
||
});
|
||
|
||
// Sync `selectedIndex` to be the `activeIndex` upon opening the floating
|
||
// element. Also, reset `activeIndex` upon closing the floating element.
|
||
useModernLayoutEffect(() => {
|
||
if (!enabled) return;
|
||
if (open && elements.floating) {
|
||
if (focusItemOnOpenRef.current && selectedIndex != null) {
|
||
// Regardless of the pointer modality, we want to ensure the selected
|
||
// item comes into view when the floating element is opened.
|
||
forceScrollIntoViewRef.current = true;
|
||
indexRef.current = selectedIndex;
|
||
onNavigate();
|
||
}
|
||
} else if (previousMountedRef.current) {
|
||
// Since the user can specify `onNavigate` conditionally
|
||
// (onNavigate: open ? setActiveIndex : setSelectedIndex),
|
||
// we store and call the previous function.
|
||
indexRef.current = -1;
|
||
previousOnNavigateRef.current();
|
||
}
|
||
}, [enabled, open, elements.floating, selectedIndex, onNavigate]);
|
||
|
||
// Sync `activeIndex` to be the focused item while the floating element is
|
||
// open.
|
||
useModernLayoutEffect(() => {
|
||
if (!enabled) return;
|
||
if (!open) return;
|
||
if (!elements.floating) return;
|
||
if (activeIndex == null) {
|
||
forceSyncFocusRef.current = false;
|
||
if (selectedIndexRef.current != null) {
|
||
return;
|
||
}
|
||
|
||
// Reset while the floating element was open (e.g. the list changed).
|
||
if (previousMountedRef.current) {
|
||
indexRef.current = -1;
|
||
focusItem();
|
||
}
|
||
|
||
// Initial sync.
|
||
if ((!previousOpenRef.current || !previousMountedRef.current) && focusItemOnOpenRef.current && (keyRef.current != null || focusItemOnOpenRef.current === true && keyRef.current == null)) {
|
||
let runs = 0;
|
||
const waitForListPopulated = () => {
|
||
if (listRef.current[0] == null) {
|
||
// Avoid letting the browser paint if possible on the first try,
|
||
// otherwise use rAF. Don't try more than twice, since something
|
||
// is wrong otherwise.
|
||
if (runs < 2) {
|
||
const scheduler = runs ? requestAnimationFrame : queueMicrotask;
|
||
scheduler(waitForListPopulated);
|
||
}
|
||
runs++;
|
||
} else {
|
||
indexRef.current = keyRef.current == null || isMainOrientationToEndKey(keyRef.current, orientation, rtl) || nested ? getMinListIndex(listRef, disabledIndicesRef.current) : getMaxListIndex(listRef, disabledIndicesRef.current);
|
||
keyRef.current = null;
|
||
onNavigate();
|
||
}
|
||
};
|
||
waitForListPopulated();
|
||
}
|
||
} else if (!isIndexOutOfListBounds(listRef, activeIndex)) {
|
||
indexRef.current = activeIndex;
|
||
focusItem();
|
||
forceScrollIntoViewRef.current = false;
|
||
}
|
||
}, [enabled, open, elements.floating, activeIndex, selectedIndexRef, nested, listRef, orientation, rtl, onNavigate, focusItem, disabledIndicesRef]);
|
||
|
||
// Ensure the parent floating element has focus when a nested child closes
|
||
// to allow arrow key navigation to work after the pointer leaves the child.
|
||
useModernLayoutEffect(() => {
|
||
var _nodes$find;
|
||
if (!enabled || elements.floating || !tree || virtual || !previousMountedRef.current) {
|
||
return;
|
||
}
|
||
const nodes = tree.nodesRef.current;
|
||
const parent = (_nodes$find = nodes.find(node => node.id === parentId)) == null || (_nodes$find = _nodes$find.context) == null ? void 0 : _nodes$find.elements.floating;
|
||
const activeEl = activeElement(getDocument$1(elements.floating));
|
||
const treeContainsActiveEl = nodes.some(node => node.context && contains$1(node.context.elements.floating, activeEl));
|
||
if (parent && !treeContainsActiveEl && isPointerModalityRef.current) {
|
||
parent.focus({
|
||
preventScroll: true
|
||
});
|
||
}
|
||
}, [enabled, elements.floating, tree, parentId, virtual]);
|
||
useModernLayoutEffect(() => {
|
||
if (!enabled) return;
|
||
if (!tree) return;
|
||
if (!virtual) return;
|
||
if (parentId) return;
|
||
function handleVirtualFocus(item) {
|
||
setVirtualId(item.id);
|
||
if (virtualItemRef) {
|
||
virtualItemRef.current = item;
|
||
}
|
||
}
|
||
tree.events.on('virtualfocus', handleVirtualFocus);
|
||
return () => {
|
||
tree.events.off('virtualfocus', handleVirtualFocus);
|
||
};
|
||
}, [enabled, tree, virtual, parentId, virtualItemRef]);
|
||
useModernLayoutEffect(() => {
|
||
previousOnNavigateRef.current = onNavigate;
|
||
previousOpenRef.current = open;
|
||
previousMountedRef.current = !!elements.floating;
|
||
});
|
||
useModernLayoutEffect(() => {
|
||
if (!open) {
|
||
keyRef.current = null;
|
||
focusItemOnOpenRef.current = focusItemOnOpen;
|
||
}
|
||
}, [open, focusItemOnOpen]);
|
||
const hasActiveIndex = activeIndex != null;
|
||
const item = React.useMemo(() => {
|
||
function syncCurrentTarget(currentTarget) {
|
||
if (!latestOpenRef.current) return;
|
||
const index = listRef.current.indexOf(currentTarget);
|
||
if (index !== -1 && indexRef.current !== index) {
|
||
indexRef.current = index;
|
||
onNavigate();
|
||
}
|
||
}
|
||
const props = {
|
||
onFocus(_ref) {
|
||
let {
|
||
currentTarget
|
||
} = _ref;
|
||
forceSyncFocusRef.current = true;
|
||
syncCurrentTarget(currentTarget);
|
||
},
|
||
onClick: _ref2 => {
|
||
let {
|
||
currentTarget
|
||
} = _ref2;
|
||
return currentTarget.focus({
|
||
preventScroll: true
|
||
});
|
||
},
|
||
// Safari
|
||
onMouseMove(_ref3) {
|
||
let {
|
||
currentTarget
|
||
} = _ref3;
|
||
forceSyncFocusRef.current = true;
|
||
forceScrollIntoViewRef.current = false;
|
||
if (focusItemOnHover) {
|
||
syncCurrentTarget(currentTarget);
|
||
}
|
||
},
|
||
onPointerLeave(_ref4) {
|
||
let {
|
||
pointerType
|
||
} = _ref4;
|
||
if (!isPointerModalityRef.current || pointerType === 'touch') {
|
||
return;
|
||
}
|
||
forceSyncFocusRef.current = true;
|
||
if (!focusItemOnHover) {
|
||
return;
|
||
}
|
||
indexRef.current = -1;
|
||
onNavigate();
|
||
if (!virtual) {
|
||
var _floatingFocusElement;
|
||
(_floatingFocusElement = floatingFocusElementRef.current) == null || _floatingFocusElement.focus({
|
||
preventScroll: true
|
||
});
|
||
}
|
||
}
|
||
};
|
||
return props;
|
||
}, [latestOpenRef, floatingFocusElementRef, focusItemOnHover, listRef, onNavigate, virtual]);
|
||
const getParentOrientation = React.useCallback(() => {
|
||
var _tree$nodesRef$curren;
|
||
return parentOrientation != null ? parentOrientation : tree == null || (_tree$nodesRef$curren = tree.nodesRef.current.find(node => node.id === parentId)) == null || (_tree$nodesRef$curren = _tree$nodesRef$curren.context) == null || (_tree$nodesRef$curren = _tree$nodesRef$curren.dataRef) == null ? void 0 : _tree$nodesRef$curren.current.orientation;
|
||
}, [parentId, tree, parentOrientation]);
|
||
const commonOnKeyDown = useEffectEvent(event => {
|
||
isPointerModalityRef.current = false;
|
||
forceSyncFocusRef.current = true;
|
||
|
||
// When composing a character, Chrome fires ArrowDown twice. Firefox/Safari
|
||
// don't appear to suffer from this. `event.isComposing` is avoided due to
|
||
// Safari not supporting it properly (although it's not needed in the first
|
||
// place for Safari, just avoiding any possible issues).
|
||
if (event.which === 229) {
|
||
return;
|
||
}
|
||
|
||
// If the floating element is animating out, ignore navigation. Otherwise,
|
||
// the `activeIndex` gets set to 0 despite not being open so the next time
|
||
// the user ArrowDowns, the first item won't be focused.
|
||
if (!latestOpenRef.current && event.currentTarget === floatingFocusElementRef.current) {
|
||
return;
|
||
}
|
||
if (nested && isCrossOrientationCloseKey(event.key, orientation, rtl, cols)) {
|
||
// If the nested list's close key is also the parent navigation key,
|
||
// let the parent navigate. Otherwise, stop propagating the event.
|
||
if (!isMainOrientationKey(event.key, getParentOrientation())) {
|
||
stopEvent(event);
|
||
}
|
||
onOpenChange(false, event.nativeEvent, 'list-navigation');
|
||
if (isHTMLElement(elements.domReference)) {
|
||
if (virtual) {
|
||
tree == null || tree.events.emit('virtualfocus', elements.domReference);
|
||
} else {
|
||
elements.domReference.focus();
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
const currentIndex = indexRef.current;
|
||
const minIndex = getMinListIndex(listRef, disabledIndices);
|
||
const maxIndex = getMaxListIndex(listRef, disabledIndices);
|
||
if (!typeableComboboxReference) {
|
||
if (event.key === 'Home') {
|
||
stopEvent(event);
|
||
indexRef.current = minIndex;
|
||
onNavigate();
|
||
}
|
||
if (event.key === 'End') {
|
||
stopEvent(event);
|
||
indexRef.current = maxIndex;
|
||
onNavigate();
|
||
}
|
||
}
|
||
|
||
// Grid navigation.
|
||
if (cols > 1) {
|
||
const sizes = itemSizes || Array.from({
|
||
length: listRef.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(listRef, index, disabledIndices));
|
||
// last enabled index
|
||
const maxGridIndex = cellMap.reduce((foundIndex, index, cellIndex) => index != null && !isListIndexDisabled(listRef, index, disabledIndices) ? cellIndex : foundIndex, -1);
|
||
const index = cellMap[getGridNavigatedIndex({
|
||
current: cellMap.map(itemIndex => itemIndex != null ? listRef.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) || listRef.current.map((_, index) => isListIndexDisabled(listRef, index, disabledIndices) ? index : undefined)), undefined], cellMap),
|
||
minIndex: minGridIndex,
|
||
maxIndex: maxGridIndex,
|
||
prevIndex: getGridCellIndexOfCorner(indexRef.current > maxIndex ? minIndex : indexRef.current, 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 === (rtl ? ARROW_LEFT : ARROW_RIGHT) ? 'tr' : 'tl'),
|
||
stopEvent: true
|
||
})];
|
||
if (index != null) {
|
||
indexRef.current = index;
|
||
onNavigate();
|
||
}
|
||
if (orientation === 'both') {
|
||
return;
|
||
}
|
||
}
|
||
if (isMainOrientationKey(event.key, orientation)) {
|
||
stopEvent(event);
|
||
|
||
// Reset the index if no item is focused.
|
||
if (open && !virtual && activeElement(event.currentTarget.ownerDocument) === event.currentTarget) {
|
||
indexRef.current = isMainOrientationToEndKey(event.key, orientation, rtl) ? minIndex : maxIndex;
|
||
onNavigate();
|
||
return;
|
||
}
|
||
if (isMainOrientationToEndKey(event.key, orientation, rtl)) {
|
||
if (loop) {
|
||
indexRef.current = currentIndex >= maxIndex ? allowEscape && currentIndex !== listRef.current.length ? -1 : minIndex : findNonDisabledListIndex(listRef, {
|
||
startingIndex: currentIndex,
|
||
disabledIndices
|
||
});
|
||
} else {
|
||
indexRef.current = Math.min(maxIndex, findNonDisabledListIndex(listRef, {
|
||
startingIndex: currentIndex,
|
||
disabledIndices
|
||
}));
|
||
}
|
||
} else {
|
||
if (loop) {
|
||
indexRef.current = currentIndex <= minIndex ? allowEscape && currentIndex !== -1 ? listRef.current.length : maxIndex : findNonDisabledListIndex(listRef, {
|
||
startingIndex: currentIndex,
|
||
decrement: true,
|
||
disabledIndices
|
||
});
|
||
} else {
|
||
indexRef.current = Math.max(minIndex, findNonDisabledListIndex(listRef, {
|
||
startingIndex: currentIndex,
|
||
decrement: true,
|
||
disabledIndices
|
||
}));
|
||
}
|
||
}
|
||
if (isIndexOutOfListBounds(listRef, indexRef.current)) {
|
||
indexRef.current = -1;
|
||
}
|
||
onNavigate();
|
||
}
|
||
});
|
||
const ariaActiveDescendantProp = React.useMemo(() => {
|
||
return virtual && open && hasActiveIndex && {
|
||
'aria-activedescendant': virtualId || activeId
|
||
};
|
||
}, [virtual, open, hasActiveIndex, virtualId, activeId]);
|
||
const floating = React.useMemo(() => {
|
||
return {
|
||
'aria-orientation': orientation === 'both' ? undefined : orientation,
|
||
...(!typeableComboboxReference ? ariaActiveDescendantProp : {}),
|
||
onKeyDown: commonOnKeyDown,
|
||
onPointerMove() {
|
||
isPointerModalityRef.current = true;
|
||
}
|
||
};
|
||
}, [ariaActiveDescendantProp, commonOnKeyDown, orientation, typeableComboboxReference]);
|
||
const reference = React.useMemo(() => {
|
||
function checkVirtualMouse(event) {
|
||
if (focusItemOnOpen === 'auto' && isVirtualClick(event.nativeEvent)) {
|
||
focusItemOnOpenRef.current = true;
|
||
}
|
||
}
|
||
function checkVirtualPointer(event) {
|
||
// `pointerdown` fires first, reset the state then perform the checks.
|
||
focusItemOnOpenRef.current = focusItemOnOpen;
|
||
if (focusItemOnOpen === 'auto' && isVirtualPointerEvent(event.nativeEvent)) {
|
||
focusItemOnOpenRef.current = true;
|
||
}
|
||
}
|
||
return {
|
||
...ariaActiveDescendantProp,
|
||
onKeyDown(event) {
|
||
isPointerModalityRef.current = false;
|
||
const isArrowKey = event.key.startsWith('Arrow');
|
||
const isHomeOrEndKey = ['Home', 'End'].includes(event.key);
|
||
const isMoveKey = isArrowKey || isHomeOrEndKey;
|
||
const isCrossOpenKey = isCrossOrientationOpenKey(event.key, orientation, rtl);
|
||
const isCrossCloseKey = isCrossOrientationCloseKey(event.key, orientation, rtl, cols);
|
||
const isParentCrossOpenKey = isCrossOrientationOpenKey(event.key, getParentOrientation(), rtl);
|
||
const isMainKey = isMainOrientationKey(event.key, orientation);
|
||
const isNavigationKey = (nested ? isParentCrossOpenKey : isMainKey) || event.key === 'Enter' || event.key.trim() === '';
|
||
if (virtual && open) {
|
||
const rootNode = tree == null ? void 0 : tree.nodesRef.current.find(node => node.parentId == null);
|
||
const deepestNode = tree && rootNode ? getDeepestNode(tree.nodesRef.current, rootNode.id) : null;
|
||
if (isMoveKey && deepestNode && virtualItemRef) {
|
||
const eventObject = new KeyboardEvent('keydown', {
|
||
key: event.key,
|
||
bubbles: true
|
||
});
|
||
if (isCrossOpenKey || isCrossCloseKey) {
|
||
var _deepestNode$context, _deepestNode$context2;
|
||
const isCurrentTarget = ((_deepestNode$context = deepestNode.context) == null ? void 0 : _deepestNode$context.elements.domReference) === event.currentTarget;
|
||
const dispatchItem = isCrossCloseKey && !isCurrentTarget ? (_deepestNode$context2 = deepestNode.context) == null ? void 0 : _deepestNode$context2.elements.domReference : isCrossOpenKey ? listRef.current.find(item => (item == null ? void 0 : item.id) === activeId) : null;
|
||
if (dispatchItem) {
|
||
stopEvent(event);
|
||
dispatchItem.dispatchEvent(eventObject);
|
||
setVirtualId(undefined);
|
||
}
|
||
}
|
||
if ((isMainKey || isHomeOrEndKey) && deepestNode.context) {
|
||
if (deepestNode.context.open && deepestNode.parentId && event.currentTarget !== deepestNode.context.elements.domReference) {
|
||
var _deepestNode$context$;
|
||
stopEvent(event);
|
||
(_deepestNode$context$ = deepestNode.context.elements.domReference) == null || _deepestNode$context$.dispatchEvent(eventObject);
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
return commonOnKeyDown(event);
|
||
}
|
||
// If a floating element should not open on arrow key down, avoid
|
||
// setting `activeIndex` while it's closed.
|
||
if (!open && !openOnArrowKeyDown && isArrowKey) {
|
||
return;
|
||
}
|
||
if (isNavigationKey) {
|
||
const isParentMainKey = isMainOrientationKey(event.key, getParentOrientation());
|
||
keyRef.current = nested && isParentMainKey ? null : event.key;
|
||
}
|
||
if (nested) {
|
||
if (isParentCrossOpenKey) {
|
||
stopEvent(event);
|
||
if (open) {
|
||
indexRef.current = getMinListIndex(listRef, disabledIndicesRef.current);
|
||
onNavigate();
|
||
} else {
|
||
onOpenChange(true, event.nativeEvent, 'list-navigation');
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
if (isMainKey) {
|
||
if (selectedIndex != null) {
|
||
indexRef.current = selectedIndex;
|
||
}
|
||
stopEvent(event);
|
||
if (!open && openOnArrowKeyDown) {
|
||
onOpenChange(true, event.nativeEvent, 'list-navigation');
|
||
} else {
|
||
commonOnKeyDown(event);
|
||
}
|
||
if (open) {
|
||
onNavigate();
|
||
}
|
||
}
|
||
},
|
||
onFocus() {
|
||
if (open && !virtual) {
|
||
indexRef.current = -1;
|
||
onNavigate();
|
||
}
|
||
},
|
||
onPointerDown: checkVirtualPointer,
|
||
onPointerEnter: checkVirtualPointer,
|
||
onMouseDown: checkVirtualMouse,
|
||
onClick: checkVirtualMouse
|
||
};
|
||
}, [activeId, ariaActiveDescendantProp, cols, commonOnKeyDown, disabledIndicesRef, focusItemOnOpen, listRef, nested, onNavigate, onOpenChange, open, openOnArrowKeyDown, orientation, getParentOrientation, rtl, selectedIndex, tree, virtual, virtualItemRef]);
|
||
return React.useMemo(() => enabled ? {
|
||
reference,
|
||
floating,
|
||
item
|
||
} : {}, [enabled, reference, floating, item]);
|
||
}
|
||
|
||
const componentRoleToAriaRoleMap = /*#__PURE__*/new Map([['select', 'listbox'], ['combobox', 'listbox'], ['label', false]]);
|
||
|
||
/**
|
||
* Adds base screen reader props to the reference and floating elements for a
|
||
* given floating element `role`.
|
||
* @see https://floating-ui.com/docs/useRole
|
||
*/
|
||
function useRole(context, props) {
|
||
var _elements$domReferenc, _componentRoleToAriaR;
|
||
if (props === void 0) {
|
||
props = {};
|
||
}
|
||
const {
|
||
open,
|
||
elements,
|
||
floatingId: defaultFloatingId
|
||
} = context;
|
||
const {
|
||
enabled = true,
|
||
role = 'dialog'
|
||
} = props;
|
||
const defaultReferenceId = useId();
|
||
const referenceId = ((_elements$domReferenc = elements.domReference) == null ? void 0 : _elements$domReferenc.id) || defaultReferenceId;
|
||
const floatingId = React.useMemo(() => {
|
||
var _getFloatingFocusElem;
|
||
return ((_getFloatingFocusElem = getFloatingFocusElement(elements.floating)) == null ? void 0 : _getFloatingFocusElem.id) || defaultFloatingId;
|
||
}, [elements.floating, defaultFloatingId]);
|
||
const ariaRole = (_componentRoleToAriaR = componentRoleToAriaRoleMap.get(role)) != null ? _componentRoleToAriaR : role;
|
||
const parentId = useFloatingParentNodeId();
|
||
const isNested = parentId != null;
|
||
const reference = React.useMemo(() => {
|
||
if (ariaRole === 'tooltip' || role === 'label') {
|
||
return {
|
||
["aria-" + (role === 'label' ? 'labelledby' : 'describedby')]: open ? floatingId : undefined
|
||
};
|
||
}
|
||
return {
|
||
'aria-expanded': open ? 'true' : 'false',
|
||
'aria-haspopup': ariaRole === 'alertdialog' ? 'dialog' : ariaRole,
|
||
'aria-controls': open ? floatingId : undefined,
|
||
...(ariaRole === 'listbox' && {
|
||
role: 'combobox'
|
||
}),
|
||
...(ariaRole === 'menu' && {
|
||
id: referenceId
|
||
}),
|
||
...(ariaRole === 'menu' && isNested && {
|
||
role: 'menuitem'
|
||
}),
|
||
...(role === 'select' && {
|
||
'aria-autocomplete': 'none'
|
||
}),
|
||
...(role === 'combobox' && {
|
||
'aria-autocomplete': 'list'
|
||
})
|
||
};
|
||
}, [ariaRole, floatingId, isNested, open, referenceId, role]);
|
||
const floating = React.useMemo(() => {
|
||
const floatingProps = {
|
||
id: floatingId,
|
||
...(ariaRole && {
|
||
role: ariaRole
|
||
})
|
||
};
|
||
if (ariaRole === 'tooltip' || role === 'label') {
|
||
return floatingProps;
|
||
}
|
||
return {
|
||
...floatingProps,
|
||
...(ariaRole === 'menu' && {
|
||
'aria-labelledby': referenceId
|
||
})
|
||
};
|
||
}, [ariaRole, floatingId, referenceId, role]);
|
||
const item = React.useCallback(_ref => {
|
||
let {
|
||
active,
|
||
selected
|
||
} = _ref;
|
||
const commonProps = {
|
||
role: 'option',
|
||
...(active && {
|
||
id: floatingId + "-fui-option"
|
||
})
|
||
};
|
||
|
||
// For `menu`, we are unable to tell if the item is a `menuitemradio`
|
||
// or `menuitemcheckbox`. For backwards-compatibility reasons, also
|
||
// avoid defaulting to `menuitem` as it may overwrite custom role props.
|
||
switch (role) {
|
||
case 'select':
|
||
case 'combobox':
|
||
return {
|
||
...commonProps,
|
||
'aria-selected': selected
|
||
};
|
||
}
|
||
return {};
|
||
}, [floatingId, role]);
|
||
return React.useMemo(() => enabled ? {
|
||
reference,
|
||
floating,
|
||
item
|
||
} : {}, [enabled, reference, floating, item]);
|
||
}
|
||
|
||
// Converts a JS style key like `backgroundColor` to a CSS transition-property
|
||
// like `background-color`.
|
||
const camelCaseToKebabCase = str => str.replace(/[A-Z]+(?![a-z])|[A-Z]/g, ($, ofs) => (ofs ? '-' : '') + $.toLowerCase());
|
||
function execWithArgsOrReturn(valueOrFn, args) {
|
||
return typeof valueOrFn === 'function' ? valueOrFn(args) : valueOrFn;
|
||
}
|
||
function useDelayUnmount(open, durationMs) {
|
||
const [isMounted, setIsMounted] = React.useState(open);
|
||
if (open && !isMounted) {
|
||
setIsMounted(true);
|
||
}
|
||
React.useEffect(() => {
|
||
if (!open && isMounted) {
|
||
const timeout = setTimeout(() => setIsMounted(false), durationMs);
|
||
return () => clearTimeout(timeout);
|
||
}
|
||
}, [open, isMounted, durationMs]);
|
||
return isMounted;
|
||
}
|
||
/**
|
||
* Provides a status string to apply CSS transitions to a floating element,
|
||
* correctly handling placement-aware transitions.
|
||
* @see https://floating-ui.com/docs/useTransition#usetransitionstatus
|
||
*/
|
||
function useTransitionStatus(context, props) {
|
||
if (props === void 0) {
|
||
props = {};
|
||
}
|
||
const {
|
||
open,
|
||
elements: {
|
||
floating
|
||
}
|
||
} = context;
|
||
const {
|
||
duration = 250
|
||
} = props;
|
||
const isNumberDuration = typeof duration === 'number';
|
||
const closeDuration = (isNumberDuration ? duration : duration.close) || 0;
|
||
const [status, setStatus] = React.useState('unmounted');
|
||
const isMounted = useDelayUnmount(open, closeDuration);
|
||
if (!isMounted && status === 'close') {
|
||
setStatus('unmounted');
|
||
}
|
||
useModernLayoutEffect(() => {
|
||
if (!floating) return;
|
||
if (open) {
|
||
setStatus('initial');
|
||
const frame = requestAnimationFrame(() => {
|
||
// Ensure it opens before paint. With `FloatingDelayGroup`,
|
||
// this avoids a flicker when moving between floating elements
|
||
// to ensure one is always open with no missing frames.
|
||
ReactDOM.flushSync(() => {
|
||
setStatus('open');
|
||
});
|
||
});
|
||
return () => {
|
||
cancelAnimationFrame(frame);
|
||
};
|
||
}
|
||
setStatus('close');
|
||
}, [open, floating]);
|
||
return {
|
||
isMounted,
|
||
status
|
||
};
|
||
}
|
||
/**
|
||
* Provides styles to apply CSS transitions to a floating element, correctly
|
||
* handling placement-aware transitions. Wrapper around `useTransitionStatus`.
|
||
* @see https://floating-ui.com/docs/useTransition#usetransitionstyles
|
||
*/
|
||
function useTransitionStyles(context, props) {
|
||
if (props === void 0) {
|
||
props = {};
|
||
}
|
||
const {
|
||
initial: unstable_initial = {
|
||
opacity: 0
|
||
},
|
||
open: unstable_open,
|
||
close: unstable_close,
|
||
common: unstable_common,
|
||
duration = 250
|
||
} = props;
|
||
const placement = context.placement;
|
||
const side = placement.split('-')[0];
|
||
const fnArgs = React.useMemo(() => ({
|
||
side,
|
||
placement
|
||
}), [side, placement]);
|
||
const isNumberDuration = typeof duration === 'number';
|
||
const openDuration = (isNumberDuration ? duration : duration.open) || 0;
|
||
const closeDuration = (isNumberDuration ? duration : duration.close) || 0;
|
||
const [styles, setStyles] = React.useState(() => ({
|
||
...execWithArgsOrReturn(unstable_common, fnArgs),
|
||
...execWithArgsOrReturn(unstable_initial, fnArgs)
|
||
}));
|
||
const {
|
||
isMounted,
|
||
status
|
||
} = useTransitionStatus(context, {
|
||
duration
|
||
});
|
||
const initialRef = useLatestRef(unstable_initial);
|
||
const openRef = useLatestRef(unstable_open);
|
||
const closeRef = useLatestRef(unstable_close);
|
||
const commonRef = useLatestRef(unstable_common);
|
||
useModernLayoutEffect(() => {
|
||
const initialStyles = execWithArgsOrReturn(initialRef.current, fnArgs);
|
||
const closeStyles = execWithArgsOrReturn(closeRef.current, fnArgs);
|
||
const commonStyles = execWithArgsOrReturn(commonRef.current, fnArgs);
|
||
const openStyles = execWithArgsOrReturn(openRef.current, fnArgs) || Object.keys(initialStyles).reduce((acc, key) => {
|
||
acc[key] = '';
|
||
return acc;
|
||
}, {});
|
||
if (status === 'initial') {
|
||
setStyles(styles => ({
|
||
transitionProperty: styles.transitionProperty,
|
||
...commonStyles,
|
||
...initialStyles
|
||
}));
|
||
}
|
||
if (status === 'open') {
|
||
setStyles({
|
||
transitionProperty: Object.keys(openStyles).map(camelCaseToKebabCase).join(','),
|
||
transitionDuration: openDuration + "ms",
|
||
...commonStyles,
|
||
...openStyles
|
||
});
|
||
}
|
||
if (status === 'close') {
|
||
const styles = closeStyles || initialStyles;
|
||
setStyles({
|
||
transitionProperty: Object.keys(styles).map(camelCaseToKebabCase).join(','),
|
||
transitionDuration: closeDuration + "ms",
|
||
...commonStyles,
|
||
...styles
|
||
});
|
||
}
|
||
}, [closeDuration, closeRef, initialRef, openRef, commonRef, openDuration, status, fnArgs]);
|
||
return {
|
||
isMounted,
|
||
styles
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Provides a matching callback that can be used to focus an item as the user
|
||
* types, often used in tandem with `useListNavigation()`.
|
||
* @see https://floating-ui.com/docs/useTypeahead
|
||
*/
|
||
function useTypeahead(context, props) {
|
||
var _ref;
|
||
const {
|
||
open,
|
||
dataRef
|
||
} = context;
|
||
const {
|
||
listRef,
|
||
activeIndex,
|
||
onMatch: unstable_onMatch,
|
||
onTypingChange: unstable_onTypingChange,
|
||
enabled = true,
|
||
findMatch = null,
|
||
resetMs = 750,
|
||
ignoreKeys = [],
|
||
selectedIndex = null
|
||
} = props;
|
||
const timeoutIdRef = React.useRef(-1);
|
||
const stringRef = React.useRef('');
|
||
const prevIndexRef = React.useRef((_ref = selectedIndex != null ? selectedIndex : activeIndex) != null ? _ref : -1);
|
||
const matchIndexRef = React.useRef(null);
|
||
const onMatch = useEffectEvent(unstable_onMatch);
|
||
const onTypingChange = useEffectEvent(unstable_onTypingChange);
|
||
const findMatchRef = useLatestRef(findMatch);
|
||
const ignoreKeysRef = useLatestRef(ignoreKeys);
|
||
useModernLayoutEffect(() => {
|
||
if (open) {
|
||
clearTimeoutIfSet(timeoutIdRef);
|
||
matchIndexRef.current = null;
|
||
stringRef.current = '';
|
||
}
|
||
}, [open]);
|
||
useModernLayoutEffect(() => {
|
||
// Sync arrow key navigation but not typeahead navigation.
|
||
if (open && stringRef.current === '') {
|
||
var _ref2;
|
||
prevIndexRef.current = (_ref2 = selectedIndex != null ? selectedIndex : activeIndex) != null ? _ref2 : -1;
|
||
}
|
||
}, [open, selectedIndex, activeIndex]);
|
||
const setTypingChange = useEffectEvent(value => {
|
||
if (value) {
|
||
if (!dataRef.current.typing) {
|
||
dataRef.current.typing = value;
|
||
onTypingChange(value);
|
||
}
|
||
} else {
|
||
if (dataRef.current.typing) {
|
||
dataRef.current.typing = value;
|
||
onTypingChange(value);
|
||
}
|
||
}
|
||
});
|
||
const onKeyDown = useEffectEvent(event => {
|
||
function getMatchingIndex(list, orderedList, string) {
|
||
const str = findMatchRef.current ? findMatchRef.current(orderedList, string) : orderedList.find(text => (text == null ? void 0 : text.toLocaleLowerCase().indexOf(string.toLocaleLowerCase())) === 0);
|
||
return str ? list.indexOf(str) : -1;
|
||
}
|
||
const listContent = listRef.current;
|
||
if (stringRef.current.length > 0 && stringRef.current[0] !== ' ') {
|
||
if (getMatchingIndex(listContent, listContent, stringRef.current) === -1) {
|
||
setTypingChange(false);
|
||
} else if (event.key === ' ') {
|
||
stopEvent(event);
|
||
}
|
||
}
|
||
if (listContent == null || ignoreKeysRef.current.includes(event.key) ||
|
||
// Character key.
|
||
event.key.length !== 1 ||
|
||
// Modifier key.
|
||
event.ctrlKey || event.metaKey || event.altKey) {
|
||
return;
|
||
}
|
||
if (open && event.key !== ' ') {
|
||
stopEvent(event);
|
||
setTypingChange(true);
|
||
}
|
||
|
||
// Bail out if the list contains a word like "llama" or "aaron". TODO:
|
||
// allow it in this case, too.
|
||
const allowRapidSuccessionOfFirstLetter = listContent.every(text => {
|
||
var _text$, _text$2;
|
||
return text ? ((_text$ = text[0]) == null ? void 0 : _text$.toLocaleLowerCase()) !== ((_text$2 = text[1]) == null ? void 0 : _text$2.toLocaleLowerCase()) : true;
|
||
});
|
||
|
||
// Allows the user to cycle through items that start with the same letter
|
||
// in rapid succession.
|
||
if (allowRapidSuccessionOfFirstLetter && stringRef.current === event.key) {
|
||
stringRef.current = '';
|
||
prevIndexRef.current = matchIndexRef.current;
|
||
}
|
||
stringRef.current += event.key;
|
||
clearTimeoutIfSet(timeoutIdRef);
|
||
timeoutIdRef.current = window.setTimeout(() => {
|
||
stringRef.current = '';
|
||
prevIndexRef.current = matchIndexRef.current;
|
||
setTypingChange(false);
|
||
}, resetMs);
|
||
const prevIndex = prevIndexRef.current;
|
||
const index = getMatchingIndex(listContent, [...listContent.slice((prevIndex || 0) + 1), ...listContent.slice(0, (prevIndex || 0) + 1)], stringRef.current);
|
||
if (index !== -1) {
|
||
onMatch(index);
|
||
matchIndexRef.current = index;
|
||
} else if (event.key !== ' ') {
|
||
stringRef.current = '';
|
||
setTypingChange(false);
|
||
}
|
||
});
|
||
const reference = React.useMemo(() => ({
|
||
onKeyDown
|
||
}), [onKeyDown]);
|
||
const floating = React.useMemo(() => {
|
||
return {
|
||
onKeyDown,
|
||
onKeyUp(event) {
|
||
if (event.key === ' ') {
|
||
setTypingChange(false);
|
||
}
|
||
}
|
||
};
|
||
}, [onKeyDown, setTypingChange]);
|
||
return React.useMemo(() => enabled ? {
|
||
reference,
|
||
floating
|
||
} : {}, [enabled, reference, floating]);
|
||
}
|
||
|
||
function getArgsWithCustomFloatingHeight(state, height) {
|
||
return {
|
||
...state,
|
||
rects: {
|
||
...state.rects,
|
||
floating: {
|
||
...state.rects.floating,
|
||
height
|
||
}
|
||
}
|
||
};
|
||
}
|
||
/**
|
||
* Positions the floating element such that an inner element inside of it is
|
||
* anchored to the reference element.
|
||
* @see https://floating-ui.com/docs/inner
|
||
* @deprecated
|
||
*/
|
||
const inner = props => ({
|
||
name: 'inner',
|
||
options: props,
|
||
async fn(state) {
|
||
const {
|
||
listRef,
|
||
overflowRef,
|
||
onFallbackChange,
|
||
offset: innerOffset = 0,
|
||
index = 0,
|
||
minItemsVisible = 4,
|
||
referenceOverflowThreshold = 0,
|
||
scrollRef,
|
||
...detectOverflowOptions
|
||
} = evaluate(props, state);
|
||
const {
|
||
rects,
|
||
elements: {
|
||
floating
|
||
}
|
||
} = state;
|
||
const item = listRef.current[index];
|
||
const scrollEl = (scrollRef == null ? void 0 : scrollRef.current) || floating;
|
||
|
||
// Valid combinations:
|
||
// 1. Floating element is the scrollRef and has a border (default)
|
||
// 2. Floating element is not the scrollRef, floating element has a border
|
||
// 3. Floating element is not the scrollRef, scrollRef has a border
|
||
// Floating > {...getFloatingProps()} wrapper > scrollRef > items is not
|
||
// allowed as VoiceOver doesn't work.
|
||
const clientTop = floating.clientTop || scrollEl.clientTop;
|
||
const floatingIsBordered = floating.clientTop !== 0;
|
||
const scrollElIsBordered = scrollEl.clientTop !== 0;
|
||
const floatingIsScrollEl = floating === scrollEl;
|
||
if (process.env.NODE_ENV !== "production") {
|
||
if (!state.placement.startsWith('bottom')) {
|
||
warn('`placement` side must be "bottom" when using the `inner`', 'middleware.');
|
||
}
|
||
}
|
||
if (!item) {
|
||
return {};
|
||
}
|
||
const nextArgs = {
|
||
...state,
|
||
...(await offset(-item.offsetTop - floating.clientTop - rects.reference.height / 2 - item.offsetHeight / 2 - innerOffset).fn(state))
|
||
};
|
||
const overflow = await detectOverflow(getArgsWithCustomFloatingHeight(nextArgs, scrollEl.scrollHeight + clientTop + floating.clientTop), detectOverflowOptions);
|
||
const refOverflow = await detectOverflow(nextArgs, {
|
||
...detectOverflowOptions,
|
||
elementContext: 'reference'
|
||
});
|
||
const diffY = max(0, overflow.top);
|
||
const nextY = nextArgs.y + diffY;
|
||
const isScrollable = scrollEl.scrollHeight > scrollEl.clientHeight;
|
||
const rounder = isScrollable ? v => v : round;
|
||
const maxHeight = rounder(max(0, scrollEl.scrollHeight + (floatingIsBordered && floatingIsScrollEl || scrollElIsBordered ? clientTop * 2 : 0) - diffY - max(0, overflow.bottom)));
|
||
scrollEl.style.maxHeight = maxHeight + "px";
|
||
scrollEl.scrollTop = diffY;
|
||
|
||
// There is not enough space, fallback to standard anchored positioning
|
||
if (onFallbackChange) {
|
||
const shouldFallback = scrollEl.offsetHeight < item.offsetHeight * min(minItemsVisible, listRef.current.length) - 1 || refOverflow.top >= -referenceOverflowThreshold || refOverflow.bottom >= -referenceOverflowThreshold;
|
||
ReactDOM.flushSync(() => onFallbackChange(shouldFallback));
|
||
}
|
||
if (overflowRef) {
|
||
overflowRef.current = await detectOverflow(getArgsWithCustomFloatingHeight({
|
||
...nextArgs,
|
||
y: nextY
|
||
}, scrollEl.offsetHeight + clientTop + floating.clientTop), detectOverflowOptions);
|
||
}
|
||
return {
|
||
y: nextY
|
||
};
|
||
}
|
||
});
|
||
/**
|
||
* Changes the `inner` middleware's `offset` upon a `wheel` event to
|
||
* expand the floating element's height, revealing more list items.
|
||
* @see https://floating-ui.com/docs/inner
|
||
* @deprecated
|
||
*/
|
||
function useInnerOffset(context, props) {
|
||
const {
|
||
open,
|
||
elements
|
||
} = context;
|
||
const {
|
||
enabled = true,
|
||
overflowRef,
|
||
scrollRef,
|
||
onChange: unstable_onChange
|
||
} = props;
|
||
const onChange = useEffectEvent(unstable_onChange);
|
||
const controlledScrollingRef = React.useRef(false);
|
||
const prevScrollTopRef = React.useRef(null);
|
||
const initialOverflowRef = React.useRef(null);
|
||
React.useEffect(() => {
|
||
if (!enabled) return;
|
||
function onWheel(e) {
|
||
if (e.ctrlKey || !el || overflowRef.current == null) {
|
||
return;
|
||
}
|
||
const dY = e.deltaY;
|
||
const isAtTop = overflowRef.current.top >= -0.5;
|
||
const isAtBottom = overflowRef.current.bottom >= -0.5;
|
||
const remainingScroll = el.scrollHeight - el.clientHeight;
|
||
const sign = dY < 0 ? -1 : 1;
|
||
const method = dY < 0 ? 'max' : 'min';
|
||
if (el.scrollHeight <= el.clientHeight) {
|
||
return;
|
||
}
|
||
if (!isAtTop && dY > 0 || !isAtBottom && dY < 0) {
|
||
e.preventDefault();
|
||
ReactDOM.flushSync(() => {
|
||
onChange(d => d + Math[method](dY, remainingScroll * sign));
|
||
});
|
||
} else if (/firefox/i.test(getUserAgent())) {
|
||
// Needed to propagate scrolling during momentum scrolling phase once
|
||
// it gets limited by the boundary. UX improvement, not critical.
|
||
el.scrollTop += dY;
|
||
}
|
||
}
|
||
const el = (scrollRef == null ? void 0 : scrollRef.current) || elements.floating;
|
||
if (open && el) {
|
||
el.addEventListener('wheel', onWheel);
|
||
|
||
// Wait for the position to be ready.
|
||
requestAnimationFrame(() => {
|
||
prevScrollTopRef.current = el.scrollTop;
|
||
if (overflowRef.current != null) {
|
||
initialOverflowRef.current = {
|
||
...overflowRef.current
|
||
};
|
||
}
|
||
});
|
||
return () => {
|
||
prevScrollTopRef.current = null;
|
||
initialOverflowRef.current = null;
|
||
el.removeEventListener('wheel', onWheel);
|
||
};
|
||
}
|
||
}, [enabled, open, elements.floating, overflowRef, scrollRef, onChange]);
|
||
const floating = React.useMemo(() => ({
|
||
onKeyDown() {
|
||
controlledScrollingRef.current = true;
|
||
},
|
||
onWheel() {
|
||
controlledScrollingRef.current = false;
|
||
},
|
||
onPointerMove() {
|
||
controlledScrollingRef.current = false;
|
||
},
|
||
onScroll() {
|
||
const el = (scrollRef == null ? void 0 : scrollRef.current) || elements.floating;
|
||
if (!overflowRef.current || !el || !controlledScrollingRef.current) {
|
||
return;
|
||
}
|
||
if (prevScrollTopRef.current !== null) {
|
||
const scrollDiff = el.scrollTop - prevScrollTopRef.current;
|
||
if (overflowRef.current.bottom < -0.5 && scrollDiff < -1 || overflowRef.current.top < -0.5 && scrollDiff > 1) {
|
||
ReactDOM.flushSync(() => onChange(d => d + scrollDiff));
|
||
}
|
||
}
|
||
|
||
// [Firefox] Wait for the height change to have been applied.
|
||
requestAnimationFrame(() => {
|
||
prevScrollTopRef.current = el.scrollTop;
|
||
});
|
||
}
|
||
}), [elements.floating, onChange, overflowRef, scrollRef]);
|
||
return React.useMemo(() => enabled ? {
|
||
floating
|
||
} : {}, [enabled, floating]);
|
||
}
|
||
|
||
function getNodeChildren(nodes, id, onlyOpenChildren) {
|
||
if (onlyOpenChildren === void 0) {
|
||
onlyOpenChildren = true;
|
||
}
|
||
const directChildren = nodes.filter(node => {
|
||
var _node$context;
|
||
return node.parentId === id && (!onlyOpenChildren || ((_node$context = node.context) == null ? void 0 : _node$context.open));
|
||
});
|
||
return directChildren.flatMap(child => [child, ...getNodeChildren(nodes, child.id, onlyOpenChildren)]);
|
||
}
|
||
|
||
function isPointInPolygon(point, polygon) {
|
||
const [x, y] = point;
|
||
let isInside = false;
|
||
const length = polygon.length;
|
||
for (let i = 0, j = length - 1; i < length; j = i++) {
|
||
const [xi, yi] = polygon[i] || [0, 0];
|
||
const [xj, yj] = polygon[j] || [0, 0];
|
||
const intersect = yi >= y !== yj >= y && x <= (xj - xi) * (y - yi) / (yj - yi) + xi;
|
||
if (intersect) {
|
||
isInside = !isInside;
|
||
}
|
||
}
|
||
return isInside;
|
||
}
|
||
function isInside(point, rect) {
|
||
return point[0] >= rect.x && point[0] <= rect.x + rect.width && point[1] >= rect.y && point[1] <= rect.y + rect.height;
|
||
}
|
||
/**
|
||
* Generates a safe polygon area that the user can traverse without closing the
|
||
* floating element once leaving the reference element.
|
||
* @see https://floating-ui.com/docs/useHover#safepolygon
|
||
*/
|
||
function safePolygon(options) {
|
||
if (options === void 0) {
|
||
options = {};
|
||
}
|
||
const {
|
||
buffer = 0.5,
|
||
blockPointerEvents = false,
|
||
requireIntent = true
|
||
} = options;
|
||
const timeoutRef = {
|
||
current: -1
|
||
};
|
||
let hasLanded = false;
|
||
let lastX = null;
|
||
let lastY = null;
|
||
let lastCursorTime = typeof performance !== 'undefined' ? performance.now() : 0;
|
||
function getCursorSpeed(x, y) {
|
||
const currentTime = performance.now();
|
||
const elapsedTime = currentTime - lastCursorTime;
|
||
if (lastX === null || lastY === null || elapsedTime === 0) {
|
||
lastX = x;
|
||
lastY = y;
|
||
lastCursorTime = currentTime;
|
||
return null;
|
||
}
|
||
const deltaX = x - lastX;
|
||
const deltaY = y - lastY;
|
||
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
||
const speed = distance / elapsedTime; // px / ms
|
||
|
||
lastX = x;
|
||
lastY = y;
|
||
lastCursorTime = currentTime;
|
||
return speed;
|
||
}
|
||
const fn = _ref => {
|
||
let {
|
||
x,
|
||
y,
|
||
placement,
|
||
elements,
|
||
onClose,
|
||
nodeId,
|
||
tree
|
||
} = _ref;
|
||
return function onMouseMove(event) {
|
||
function close() {
|
||
clearTimeoutIfSet(timeoutRef);
|
||
onClose();
|
||
}
|
||
clearTimeoutIfSet(timeoutRef);
|
||
if (!elements.domReference || !elements.floating || placement == null || x == null || y == null) {
|
||
return;
|
||
}
|
||
const {
|
||
clientX,
|
||
clientY
|
||
} = event;
|
||
const clientPoint = [clientX, clientY];
|
||
const target = getTarget(event);
|
||
const isLeave = event.type === 'mouseleave';
|
||
const isOverFloatingEl = contains(elements.floating, target);
|
||
const isOverReferenceEl = contains(elements.domReference, target);
|
||
const refRect = elements.domReference.getBoundingClientRect();
|
||
const rect = elements.floating.getBoundingClientRect();
|
||
const side = placement.split('-')[0];
|
||
const cursorLeaveFromRight = x > rect.right - rect.width / 2;
|
||
const cursorLeaveFromBottom = y > rect.bottom - rect.height / 2;
|
||
const isOverReferenceRect = isInside(clientPoint, refRect);
|
||
const isFloatingWider = rect.width > refRect.width;
|
||
const isFloatingTaller = rect.height > refRect.height;
|
||
const left = (isFloatingWider ? refRect : rect).left;
|
||
const right = (isFloatingWider ? refRect : rect).right;
|
||
const top = (isFloatingTaller ? refRect : rect).top;
|
||
const bottom = (isFloatingTaller ? refRect : rect).bottom;
|
||
if (isOverFloatingEl) {
|
||
hasLanded = true;
|
||
if (!isLeave) {
|
||
return;
|
||
}
|
||
}
|
||
if (isOverReferenceEl) {
|
||
hasLanded = false;
|
||
}
|
||
if (isOverReferenceEl && !isLeave) {
|
||
hasLanded = true;
|
||
return;
|
||
}
|
||
|
||
// Prevent overlapping floating element from being stuck in an open-close
|
||
// loop: https://github.com/floating-ui/floating-ui/issues/1910
|
||
if (isLeave && isElement(event.relatedTarget) && contains(elements.floating, event.relatedTarget)) {
|
||
return;
|
||
}
|
||
|
||
// If any nested child is open, abort.
|
||
if (tree && getNodeChildren(tree.nodesRef.current, nodeId).length) {
|
||
return;
|
||
}
|
||
|
||
// If the pointer is leaving from the opposite side, the "buffer" logic
|
||
// creates a point where the floating element remains open, but should be
|
||
// ignored.
|
||
// A constant of 1 handles floating point rounding errors.
|
||
if (side === 'top' && y >= refRect.bottom - 1 || side === 'bottom' && y <= refRect.top + 1 || side === 'left' && x >= refRect.right - 1 || side === 'right' && x <= refRect.left + 1) {
|
||
return close();
|
||
}
|
||
|
||
// Ignore when the cursor is within the rectangular trough between the
|
||
// two elements. Since the triangle is created from the cursor point,
|
||
// which can start beyond the ref element's edge, traversing back and
|
||
// forth from the ref to the floating element can cause it to close. This
|
||
// ensures it always remains open in that case.
|
||
let rectPoly = [];
|
||
switch (side) {
|
||
case 'top':
|
||
rectPoly = [[left, refRect.top + 1], [left, rect.bottom - 1], [right, rect.bottom - 1], [right, refRect.top + 1]];
|
||
break;
|
||
case 'bottom':
|
||
rectPoly = [[left, rect.top + 1], [left, refRect.bottom - 1], [right, refRect.bottom - 1], [right, rect.top + 1]];
|
||
break;
|
||
case 'left':
|
||
rectPoly = [[rect.right - 1, bottom], [rect.right - 1, top], [refRect.left + 1, top], [refRect.left + 1, bottom]];
|
||
break;
|
||
case 'right':
|
||
rectPoly = [[refRect.right - 1, bottom], [refRect.right - 1, top], [rect.left + 1, top], [rect.left + 1, bottom]];
|
||
break;
|
||
}
|
||
function getPolygon(_ref2) {
|
||
let [x, y] = _ref2;
|
||
switch (side) {
|
||
case 'top':
|
||
{
|
||
const cursorPointOne = [isFloatingWider ? x + buffer / 2 : cursorLeaveFromRight ? x + buffer * 4 : x - buffer * 4, y + buffer + 1];
|
||
const cursorPointTwo = [isFloatingWider ? x - buffer / 2 : cursorLeaveFromRight ? x + buffer * 4 : x - buffer * 4, y + buffer + 1];
|
||
const commonPoints = [[rect.left, cursorLeaveFromRight ? rect.bottom - buffer : isFloatingWider ? rect.bottom - buffer : rect.top], [rect.right, cursorLeaveFromRight ? isFloatingWider ? rect.bottom - buffer : rect.top : rect.bottom - buffer]];
|
||
return [cursorPointOne, cursorPointTwo, ...commonPoints];
|
||
}
|
||
case 'bottom':
|
||
{
|
||
const cursorPointOne = [isFloatingWider ? x + buffer / 2 : cursorLeaveFromRight ? x + buffer * 4 : x - buffer * 4, y - buffer];
|
||
const cursorPointTwo = [isFloatingWider ? x - buffer / 2 : cursorLeaveFromRight ? x + buffer * 4 : x - buffer * 4, y - buffer];
|
||
const commonPoints = [[rect.left, cursorLeaveFromRight ? rect.top + buffer : isFloatingWider ? rect.top + buffer : rect.bottom], [rect.right, cursorLeaveFromRight ? isFloatingWider ? rect.top + buffer : rect.bottom : rect.top + buffer]];
|
||
return [cursorPointOne, cursorPointTwo, ...commonPoints];
|
||
}
|
||
case 'left':
|
||
{
|
||
const cursorPointOne = [x + buffer + 1, isFloatingTaller ? y + buffer / 2 : cursorLeaveFromBottom ? y + buffer * 4 : y - buffer * 4];
|
||
const cursorPointTwo = [x + buffer + 1, isFloatingTaller ? y - buffer / 2 : cursorLeaveFromBottom ? y + buffer * 4 : y - buffer * 4];
|
||
const commonPoints = [[cursorLeaveFromBottom ? rect.right - buffer : isFloatingTaller ? rect.right - buffer : rect.left, rect.top], [cursorLeaveFromBottom ? isFloatingTaller ? rect.right - buffer : rect.left : rect.right - buffer, rect.bottom]];
|
||
return [...commonPoints, cursorPointOne, cursorPointTwo];
|
||
}
|
||
case 'right':
|
||
{
|
||
const cursorPointOne = [x - buffer, isFloatingTaller ? y + buffer / 2 : cursorLeaveFromBottom ? y + buffer * 4 : y - buffer * 4];
|
||
const cursorPointTwo = [x - buffer, isFloatingTaller ? y - buffer / 2 : cursorLeaveFromBottom ? y + buffer * 4 : y - buffer * 4];
|
||
const commonPoints = [[cursorLeaveFromBottom ? rect.left + buffer : isFloatingTaller ? rect.left + buffer : rect.right, rect.top], [cursorLeaveFromBottom ? isFloatingTaller ? rect.left + buffer : rect.right : rect.left + buffer, rect.bottom]];
|
||
return [cursorPointOne, cursorPointTwo, ...commonPoints];
|
||
}
|
||
}
|
||
}
|
||
if (isPointInPolygon([clientX, clientY], rectPoly)) {
|
||
return;
|
||
}
|
||
if (hasLanded && !isOverReferenceRect) {
|
||
return close();
|
||
}
|
||
if (!isLeave && requireIntent) {
|
||
const cursorSpeed = getCursorSpeed(event.clientX, event.clientY);
|
||
const cursorSpeedThreshold = 0.1;
|
||
if (cursorSpeed !== null && cursorSpeed < cursorSpeedThreshold) {
|
||
return close();
|
||
}
|
||
}
|
||
if (!isPointInPolygon([clientX, clientY], getPolygon([x, y]))) {
|
||
close();
|
||
} else if (!hasLanded && requireIntent) {
|
||
timeoutRef.current = window.setTimeout(close, 40);
|
||
}
|
||
};
|
||
};
|
||
fn.__options = {
|
||
blockPointerEvents
|
||
};
|
||
return fn;
|
||
}
|
||
|
||
export { Composite, CompositeItem, FloatingArrow, FloatingDelayGroup, FloatingFocusManager, FloatingList, FloatingNode, FloatingOverlay, FloatingPortal, FloatingTree, NextFloatingDelayGroup, inner, safePolygon, useClick, useClientPoint, useDelayGroup, useDelayGroupContext, useDismiss, useFloating, useFloatingNodeId, useFloatingParentNodeId, useFloatingPortalNode, useFloatingRootContext, useFloatingTree, useFocus, useHover, useId, useInnerOffset, useInteractions, useListItem, useListNavigation, useMergeRefs, useNextDelayGroup, useRole, useTransitionStatus, useTransitionStyles, useTypeahead };
|