5440 lines
194 KiB
JavaScript
5440 lines
194 KiB
JavaScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
var React = require('react');
|
|
var jsxRuntime = require('react/jsx-runtime');
|
|
var ReactDOM = require('react-dom');
|
|
var LexicalComposerContext = require('@lexical/react/LexicalComposerContext');
|
|
var lexical = require('lexical');
|
|
|
|
function _interopNamespaceDefault(e) {
|
|
var n = Object.create(null);
|
|
if (e) {
|
|
for (var k in e) {
|
|
n[k] = e[k];
|
|
}
|
|
}
|
|
n.default = e;
|
|
return n;
|
|
}
|
|
|
|
var React__namespace = /*#__PURE__*/_interopNamespaceDefault(React);
|
|
var ReactDOM__namespace = /*#__PURE__*/_interopNamespaceDefault(ReactDOM);
|
|
|
|
function hasWindow() {
|
|
return typeof window !== 'undefined';
|
|
}
|
|
function getNodeName(node) {
|
|
if (isNode(node)) {
|
|
return (node.nodeName || '').toLowerCase();
|
|
}
|
|
// Mocked nodes in testing environments may not be instances of Node. By
|
|
// returning `#document` an infinite loop won't occur.
|
|
// https://github.com/floating-ui/floating-ui/issues/2317
|
|
return '#document';
|
|
}
|
|
function getWindow(node) {
|
|
var _node$ownerDocument;
|
|
return (node == null || (_node$ownerDocument = node.ownerDocument) == null ? void 0 : _node$ownerDocument.defaultView) || window;
|
|
}
|
|
function getDocumentElement(node) {
|
|
var _ref;
|
|
return (_ref = (isNode(node) ? node.ownerDocument : node.document) || window.document) == null ? void 0 : _ref.documentElement;
|
|
}
|
|
function isNode(value) {
|
|
if (!hasWindow()) {
|
|
return false;
|
|
}
|
|
return value instanceof Node || value instanceof getWindow(value).Node;
|
|
}
|
|
function isElement(value) {
|
|
if (!hasWindow()) {
|
|
return false;
|
|
}
|
|
return value instanceof Element || value instanceof getWindow(value).Element;
|
|
}
|
|
function isHTMLElement(value) {
|
|
if (!hasWindow()) {
|
|
return false;
|
|
}
|
|
return value instanceof HTMLElement || value instanceof getWindow(value).HTMLElement;
|
|
}
|
|
function isShadowRoot(value) {
|
|
if (!hasWindow() || typeof ShadowRoot === 'undefined') {
|
|
return false;
|
|
}
|
|
return value instanceof ShadowRoot || value instanceof getWindow(value).ShadowRoot;
|
|
}
|
|
function isOverflowElement(element) {
|
|
const {
|
|
overflow,
|
|
overflowX,
|
|
overflowY,
|
|
display
|
|
} = getComputedStyle$1(element);
|
|
return /auto|scroll|overlay|hidden|clip/.test(overflow + overflowY + overflowX) && !['inline', 'contents'].includes(display);
|
|
}
|
|
function isTableElement(element) {
|
|
return ['table', 'td', 'th'].includes(getNodeName(element));
|
|
}
|
|
function isTopLayer(element) {
|
|
return [':popover-open', ':modal'].some(selector => {
|
|
try {
|
|
return element.matches(selector);
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
});
|
|
}
|
|
function isContainingBlock(elementOrCss) {
|
|
const webkit = isWebKit();
|
|
const css = isElement(elementOrCss) ? getComputedStyle$1(elementOrCss) : elementOrCss;
|
|
|
|
// https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block
|
|
// https://drafts.csswg.org/css-transforms-2/#individual-transforms
|
|
return ['transform', 'translate', 'scale', 'rotate', 'perspective'].some(value => css[value] ? css[value] !== 'none' : false) || (css.containerType ? css.containerType !== 'normal' : false) || !webkit && (css.backdropFilter ? css.backdropFilter !== 'none' : false) || !webkit && (css.filter ? css.filter !== 'none' : false) || ['transform', 'translate', 'scale', 'rotate', 'perspective', 'filter'].some(value => (css.willChange || '').includes(value)) || ['paint', 'layout', 'strict', 'content'].some(value => (css.contain || '').includes(value));
|
|
}
|
|
function getContainingBlock(element) {
|
|
let currentNode = getParentNode(element);
|
|
while (isHTMLElement(currentNode) && !isLastTraversableNode(currentNode)) {
|
|
if (isContainingBlock(currentNode)) {
|
|
return currentNode;
|
|
} else if (isTopLayer(currentNode)) {
|
|
return null;
|
|
}
|
|
currentNode = getParentNode(currentNode);
|
|
}
|
|
return null;
|
|
}
|
|
function isWebKit() {
|
|
if (typeof CSS === 'undefined' || !CSS.supports) return false;
|
|
return CSS.supports('-webkit-backdrop-filter', 'none');
|
|
}
|
|
function isLastTraversableNode(node) {
|
|
return ['html', 'body', '#document'].includes(getNodeName(node));
|
|
}
|
|
function getComputedStyle$1(element) {
|
|
return getWindow(element).getComputedStyle(element);
|
|
}
|
|
function getNodeScroll(element) {
|
|
if (isElement(element)) {
|
|
return {
|
|
scrollLeft: element.scrollLeft,
|
|
scrollTop: element.scrollTop
|
|
};
|
|
}
|
|
return {
|
|
scrollLeft: element.scrollX,
|
|
scrollTop: element.scrollY
|
|
};
|
|
}
|
|
function getParentNode(node) {
|
|
if (getNodeName(node) === 'html') {
|
|
return node;
|
|
}
|
|
const result =
|
|
// Step into the shadow DOM of the parent of a slotted node.
|
|
node.assignedSlot ||
|
|
// DOM Element detected.
|
|
node.parentNode ||
|
|
// ShadowRoot detected.
|
|
isShadowRoot(node) && node.host ||
|
|
// Fallback.
|
|
getDocumentElement(node);
|
|
return isShadowRoot(result) ? result.host : result;
|
|
}
|
|
function getNearestOverflowAncestor(node) {
|
|
const parentNode = getParentNode(node);
|
|
if (isLastTraversableNode(parentNode)) {
|
|
return node.ownerDocument ? node.ownerDocument.body : node.body;
|
|
}
|
|
if (isHTMLElement(parentNode) && isOverflowElement(parentNode)) {
|
|
return parentNode;
|
|
}
|
|
return getNearestOverflowAncestor(parentNode);
|
|
}
|
|
function getOverflowAncestors(node, list, traverseIframes) {
|
|
var _node$ownerDocument2;
|
|
if (list === void 0) {
|
|
list = [];
|
|
}
|
|
if (traverseIframes === void 0) {
|
|
traverseIframes = true;
|
|
}
|
|
const scrollableAncestor = getNearestOverflowAncestor(node);
|
|
const isBody = scrollableAncestor === ((_node$ownerDocument2 = node.ownerDocument) == null ? void 0 : _node$ownerDocument2.body);
|
|
const win = getWindow(scrollableAncestor);
|
|
if (isBody) {
|
|
const frameElement = getFrameElement(win);
|
|
return list.concat(win, win.visualViewport || [], isOverflowElement(scrollableAncestor) ? scrollableAncestor : [], frameElement && traverseIframes ? getOverflowAncestors(frameElement) : []);
|
|
}
|
|
return list.concat(scrollableAncestor, getOverflowAncestors(scrollableAncestor, [], traverseIframes));
|
|
}
|
|
function getFrameElement(win) {
|
|
return win.parent && Object.getPrototypeOf(win.parent) ? win.frameElement : null;
|
|
}
|
|
|
|
/**
|
|
* Custom positioning reference element.
|
|
* @see https://floating-ui.com/docs/virtual-elements
|
|
*/
|
|
|
|
const min = Math.min;
|
|
const max = Math.max;
|
|
const round = Math.round;
|
|
const floor = Math.floor;
|
|
const createCoords = v => ({
|
|
x: v,
|
|
y: v
|
|
});
|
|
const oppositeSideMap = {
|
|
left: 'right',
|
|
right: 'left',
|
|
bottom: 'top',
|
|
top: 'bottom'
|
|
};
|
|
const oppositeAlignmentMap = {
|
|
start: 'end',
|
|
end: 'start'
|
|
};
|
|
function clamp(start, value, end) {
|
|
return max(start, min(value, end));
|
|
}
|
|
function evaluate(value, param) {
|
|
return typeof value === 'function' ? value(param) : value;
|
|
}
|
|
function getSide(placement) {
|
|
return placement.split('-')[0];
|
|
}
|
|
function getAlignment(placement) {
|
|
return placement.split('-')[1];
|
|
}
|
|
function getOppositeAxis(axis) {
|
|
return axis === 'x' ? 'y' : 'x';
|
|
}
|
|
function getAxisLength(axis) {
|
|
return axis === 'y' ? 'height' : 'width';
|
|
}
|
|
function getSideAxis(placement) {
|
|
return ['top', 'bottom'].includes(getSide(placement)) ? 'y' : 'x';
|
|
}
|
|
function getAlignmentAxis(placement) {
|
|
return getOppositeAxis(getSideAxis(placement));
|
|
}
|
|
function getAlignmentSides(placement, rects, rtl) {
|
|
if (rtl === void 0) {
|
|
rtl = false;
|
|
}
|
|
const alignment = getAlignment(placement);
|
|
const alignmentAxis = getAlignmentAxis(placement);
|
|
const length = getAxisLength(alignmentAxis);
|
|
let mainAlignmentSide = alignmentAxis === 'x' ? alignment === (rtl ? 'end' : 'start') ? 'right' : 'left' : alignment === 'start' ? 'bottom' : 'top';
|
|
if (rects.reference[length] > rects.floating[length]) {
|
|
mainAlignmentSide = getOppositePlacement(mainAlignmentSide);
|
|
}
|
|
return [mainAlignmentSide, getOppositePlacement(mainAlignmentSide)];
|
|
}
|
|
function getExpandedPlacements(placement) {
|
|
const oppositePlacement = getOppositePlacement(placement);
|
|
return [getOppositeAlignmentPlacement(placement), oppositePlacement, getOppositeAlignmentPlacement(oppositePlacement)];
|
|
}
|
|
function getOppositeAlignmentPlacement(placement) {
|
|
return placement.replace(/start|end/g, alignment => oppositeAlignmentMap[alignment]);
|
|
}
|
|
function getSideList(side, isStart, rtl) {
|
|
const lr = ['left', 'right'];
|
|
const rl = ['right', 'left'];
|
|
const tb = ['top', 'bottom'];
|
|
const bt = ['bottom', 'top'];
|
|
switch (side) {
|
|
case 'top':
|
|
case 'bottom':
|
|
if (rtl) return isStart ? rl : lr;
|
|
return isStart ? lr : rl;
|
|
case 'left':
|
|
case 'right':
|
|
return isStart ? tb : bt;
|
|
default:
|
|
return [];
|
|
}
|
|
}
|
|
function getOppositeAxisPlacements(placement, flipAlignment, direction, rtl) {
|
|
const alignment = getAlignment(placement);
|
|
let list = getSideList(getSide(placement), direction === 'start', rtl);
|
|
if (alignment) {
|
|
list = list.map(side => side + "-" + alignment);
|
|
if (flipAlignment) {
|
|
list = list.concat(list.map(getOppositeAlignmentPlacement));
|
|
}
|
|
}
|
|
return list;
|
|
}
|
|
function getOppositePlacement(placement) {
|
|
return placement.replace(/left|right|bottom|top/g, side => oppositeSideMap[side]);
|
|
}
|
|
function expandPaddingObject(padding) {
|
|
return {
|
|
top: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
left: 0,
|
|
...padding
|
|
};
|
|
}
|
|
function getPaddingObject(padding) {
|
|
return typeof padding !== 'number' ? expandPaddingObject(padding) : {
|
|
top: padding,
|
|
right: padding,
|
|
bottom: padding,
|
|
left: padding
|
|
};
|
|
}
|
|
function rectToClientRect(rect) {
|
|
const {
|
|
x,
|
|
y,
|
|
width,
|
|
height
|
|
} = rect;
|
|
return {
|
|
width,
|
|
height,
|
|
top: y,
|
|
left: x,
|
|
right: x + width,
|
|
bottom: y + height,
|
|
x,
|
|
y
|
|
};
|
|
}
|
|
|
|
/*!
|
|
* tabbable 6.2.0
|
|
* @license MIT, https://github.com/focus-trap/tabbable/blob/master/LICENSE
|
|
*/
|
|
// NOTE: separate `:not()` selectors has broader browser support than the newer
|
|
// `:not([inert], [inert] *)` (Feb 2023)
|
|
// CAREFUL: JSDom does not support `:not([inert] *)` as a selector; using it causes
|
|
// the entire query to fail, resulting in no nodes found, which will break a lot
|
|
// of things... so we have to rely on JS to identify nodes inside an inert container
|
|
var candidateSelectors = ['input:not([inert])', 'select:not([inert])', 'textarea:not([inert])', 'a[href]:not([inert])', 'button:not([inert])', '[tabindex]:not(slot):not([inert])', 'audio[controls]:not([inert])', 'video[controls]:not([inert])', '[contenteditable]:not([contenteditable="false"]):not([inert])', 'details>summary:first-of-type:not([inert])', 'details:not([inert])'];
|
|
var candidateSelector = /* #__PURE__ */candidateSelectors.join(',');
|
|
var NoElement = typeof Element === 'undefined';
|
|
var matches = NoElement ? function () {} : Element.prototype.matches || Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector;
|
|
var getRootNode = !NoElement && Element.prototype.getRootNode ? function (element) {
|
|
var _element$getRootNode;
|
|
return element === null || element === void 0 ? void 0 : (_element$getRootNode = element.getRootNode) === null || _element$getRootNode === void 0 ? void 0 : _element$getRootNode.call(element);
|
|
} : function (element) {
|
|
return element === null || element === void 0 ? void 0 : element.ownerDocument;
|
|
};
|
|
|
|
/**
|
|
* Determines if a node is inert or in an inert ancestor.
|
|
* @param {Element} [node]
|
|
* @param {boolean} [lookUp] If true and `node` is not inert, looks up at ancestors to
|
|
* see if any of them are inert. If false, only `node` itself is considered.
|
|
* @returns {boolean} True if inert itself or by way of being in an inert ancestor.
|
|
* False if `node` is falsy.
|
|
*/
|
|
var isInert = function isInert(node, lookUp) {
|
|
var _node$getAttribute;
|
|
if (lookUp === void 0) {
|
|
lookUp = true;
|
|
}
|
|
// CAREFUL: JSDom does not support inert at all, so we can't use the `HTMLElement.inert`
|
|
// JS API property; we have to check the attribute, which can either be empty or 'true';
|
|
// if it's `null` (not specified) or 'false', it's an active element
|
|
var inertAtt = node === null || node === void 0 ? void 0 : (_node$getAttribute = node.getAttribute) === null || _node$getAttribute === void 0 ? void 0 : _node$getAttribute.call(node, 'inert');
|
|
var inert = inertAtt === '' || inertAtt === 'true';
|
|
|
|
// NOTE: this could also be handled with `node.matches('[inert], :is([inert] *)')`
|
|
// if it weren't for `matches()` not being a function on shadow roots; the following
|
|
// code works for any kind of node
|
|
// CAREFUL: JSDom does not appear to support certain selectors like `:not([inert] *)`
|
|
// so it likely would not support `:is([inert] *)` either...
|
|
var result = inert || lookUp && node && isInert(node.parentNode); // recursive
|
|
|
|
return result;
|
|
};
|
|
|
|
/**
|
|
* Determines if a node's content is editable.
|
|
* @param {Element} [node]
|
|
* @returns True if it's content-editable; false if it's not or `node` is falsy.
|
|
*/
|
|
var isContentEditable = function isContentEditable(node) {
|
|
var _node$getAttribute2;
|
|
// CAREFUL: JSDom does not support the `HTMLElement.isContentEditable` API so we have
|
|
// to use the attribute directly to check for this, which can either be empty or 'true';
|
|
// if it's `null` (not specified) or 'false', it's a non-editable element
|
|
var attValue = node === null || node === void 0 ? void 0 : (_node$getAttribute2 = node.getAttribute) === null || _node$getAttribute2 === void 0 ? void 0 : _node$getAttribute2.call(node, 'contenteditable');
|
|
return attValue === '' || attValue === 'true';
|
|
};
|
|
|
|
/**
|
|
* @param {Element} el container to check in
|
|
* @param {boolean} includeContainer add container to check
|
|
* @param {(node: Element) => boolean} filter filter candidates
|
|
* @returns {Element[]}
|
|
*/
|
|
var getCandidates = function getCandidates(el, includeContainer, filter) {
|
|
// even if `includeContainer=false`, we still have to check it for inertness because
|
|
// if it's inert, all its children are inert
|
|
if (isInert(el)) {
|
|
return [];
|
|
}
|
|
var candidates = Array.prototype.slice.apply(el.querySelectorAll(candidateSelector));
|
|
if (includeContainer && matches.call(el, candidateSelector)) {
|
|
candidates.unshift(el);
|
|
}
|
|
candidates = candidates.filter(filter);
|
|
return candidates;
|
|
};
|
|
|
|
/**
|
|
* @callback GetShadowRoot
|
|
* @param {Element} element to check for shadow root
|
|
* @returns {ShadowRoot|boolean} ShadowRoot if available or boolean indicating if a shadowRoot is attached but not available.
|
|
*/
|
|
|
|
/**
|
|
* @callback ShadowRootFilter
|
|
* @param {Element} shadowHostNode the element which contains shadow content
|
|
* @returns {boolean} true if a shadow root could potentially contain valid candidates.
|
|
*/
|
|
|
|
/**
|
|
* @typedef {Object} CandidateScope
|
|
* @property {Element} scopeParent contains inner candidates
|
|
* @property {Element[]} candidates list of candidates found in the scope parent
|
|
*/
|
|
|
|
/**
|
|
* @typedef {Object} IterativeOptions
|
|
* @property {GetShadowRoot|boolean} getShadowRoot true if shadow support is enabled; falsy if not;
|
|
* if a function, implies shadow support is enabled and either returns the shadow root of an element
|
|
* or a boolean stating if it has an undisclosed shadow root
|
|
* @property {(node: Element) => boolean} filter filter candidates
|
|
* @property {boolean} flatten if true then result will flatten any CandidateScope into the returned list
|
|
* @property {ShadowRootFilter} shadowRootFilter filter shadow roots;
|
|
*/
|
|
|
|
/**
|
|
* @param {Element[]} elements list of element containers to match candidates from
|
|
* @param {boolean} includeContainer add container list to check
|
|
* @param {IterativeOptions} options
|
|
* @returns {Array.<Element|CandidateScope>}
|
|
*/
|
|
var getCandidatesIteratively = function getCandidatesIteratively(elements, includeContainer, options) {
|
|
var candidates = [];
|
|
var elementsToCheck = Array.from(elements);
|
|
while (elementsToCheck.length) {
|
|
var element = elementsToCheck.shift();
|
|
if (isInert(element, false)) {
|
|
// no need to look up since we're drilling down
|
|
// anything inside this container will also be inert
|
|
continue;
|
|
}
|
|
if (element.tagName === 'SLOT') {
|
|
// add shadow dom slot scope (slot itself cannot be focusable)
|
|
var assigned = element.assignedElements();
|
|
var content = assigned.length ? assigned : element.children;
|
|
var nestedCandidates = getCandidatesIteratively(content, true, options);
|
|
if (options.flatten) {
|
|
candidates.push.apply(candidates, nestedCandidates);
|
|
} else {
|
|
candidates.push({
|
|
scopeParent: element,
|
|
candidates: nestedCandidates
|
|
});
|
|
}
|
|
} else {
|
|
// check candidate element
|
|
var validCandidate = matches.call(element, candidateSelector);
|
|
if (validCandidate && options.filter(element) && (includeContainer || !elements.includes(element))) {
|
|
candidates.push(element);
|
|
}
|
|
|
|
// iterate over shadow content if possible
|
|
var shadowRoot = element.shadowRoot ||
|
|
// check for an undisclosed shadow
|
|
typeof options.getShadowRoot === 'function' && options.getShadowRoot(element);
|
|
|
|
// no inert look up because we're already drilling down and checking for inertness
|
|
// on the way down, so all containers to this root node should have already been
|
|
// vetted as non-inert
|
|
var validShadowRoot = !isInert(shadowRoot, false) && (!options.shadowRootFilter || options.shadowRootFilter(element));
|
|
if (shadowRoot && validShadowRoot) {
|
|
// add shadow dom scope IIF a shadow root node was given; otherwise, an undisclosed
|
|
// shadow exists, so look at light dom children as fallback BUT create a scope for any
|
|
// child candidates found because they're likely slotted elements (elements that are
|
|
// children of the web component element (which has the shadow), in the light dom, but
|
|
// slotted somewhere _inside_ the undisclosed shadow) -- the scope is created below,
|
|
// _after_ we return from this recursive call
|
|
var _nestedCandidates = getCandidatesIteratively(shadowRoot === true ? element.children : shadowRoot.children, true, options);
|
|
if (options.flatten) {
|
|
candidates.push.apply(candidates, _nestedCandidates);
|
|
} else {
|
|
candidates.push({
|
|
scopeParent: element,
|
|
candidates: _nestedCandidates
|
|
});
|
|
}
|
|
} else {
|
|
// there's not shadow so just dig into the element's (light dom) children
|
|
// __without__ giving the element special scope treatment
|
|
elementsToCheck.unshift.apply(elementsToCheck, element.children);
|
|
}
|
|
}
|
|
}
|
|
return candidates;
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
* Determines if the node has an explicitly specified `tabindex` attribute.
|
|
* @param {HTMLElement} node
|
|
* @returns {boolean} True if so; false if not.
|
|
*/
|
|
var hasTabIndex = function hasTabIndex(node) {
|
|
return !isNaN(parseInt(node.getAttribute('tabindex'), 10));
|
|
};
|
|
|
|
/**
|
|
* Determine the tab index of a given node.
|
|
* @param {HTMLElement} node
|
|
* @returns {number} Tab order (negative, 0, or positive number).
|
|
* @throws {Error} If `node` is falsy.
|
|
*/
|
|
var getTabIndex = function getTabIndex(node) {
|
|
if (!node) {
|
|
throw new Error('No node provided');
|
|
}
|
|
if (node.tabIndex < 0) {
|
|
// in Chrome, <details/>, <audio controls/> and <video controls/> elements get a default
|
|
// `tabIndex` of -1 when the 'tabindex' attribute isn't specified in the DOM,
|
|
// yet they are still part of the regular tab order; in FF, they get a default
|
|
// `tabIndex` of 0; since Chrome still puts those elements in the regular tab
|
|
// order, consider their tab index to be 0.
|
|
// Also browsers do not return `tabIndex` correctly for contentEditable nodes;
|
|
// so if they don't have a tabindex attribute specifically set, assume it's 0.
|
|
if ((/^(AUDIO|VIDEO|DETAILS)$/.test(node.tagName) || isContentEditable(node)) && !hasTabIndex(node)) {
|
|
return 0;
|
|
}
|
|
}
|
|
return node.tabIndex;
|
|
};
|
|
|
|
/**
|
|
* Determine the tab index of a given node __for sort order purposes__.
|
|
* @param {HTMLElement} node
|
|
* @param {boolean} [isScope] True for a custom element with shadow root or slot that, by default,
|
|
* has tabIndex -1, but needs to be sorted by document order in order for its content to be
|
|
* inserted into the correct sort position.
|
|
* @returns {number} Tab order (negative, 0, or positive number).
|
|
*/
|
|
var getSortOrderTabIndex = function getSortOrderTabIndex(node, isScope) {
|
|
var tabIndex = getTabIndex(node);
|
|
if (tabIndex < 0 && isScope && !hasTabIndex(node)) {
|
|
return 0;
|
|
}
|
|
return tabIndex;
|
|
};
|
|
var sortOrderedTabbables = function sortOrderedTabbables(a, b) {
|
|
return a.tabIndex === b.tabIndex ? a.documentOrder - b.documentOrder : a.tabIndex - b.tabIndex;
|
|
};
|
|
var isInput = function isInput(node) {
|
|
return node.tagName === 'INPUT';
|
|
};
|
|
var isHiddenInput = function isHiddenInput(node) {
|
|
return isInput(node) && node.type === 'hidden';
|
|
};
|
|
var isDetailsWithSummary = function isDetailsWithSummary(node) {
|
|
var r = node.tagName === 'DETAILS' && Array.prototype.slice.apply(node.children).some(function (child) {
|
|
return child.tagName === 'SUMMARY';
|
|
});
|
|
return r;
|
|
};
|
|
var getCheckedRadio = function getCheckedRadio(nodes, form) {
|
|
for (var i = 0; i < nodes.length; i++) {
|
|
if (nodes[i].checked && nodes[i].form === form) {
|
|
return nodes[i];
|
|
}
|
|
}
|
|
};
|
|
var isTabbableRadio = function isTabbableRadio(node) {
|
|
if (!node.name) {
|
|
return true;
|
|
}
|
|
var radioScope = node.form || getRootNode(node);
|
|
var queryRadios = function queryRadios(name) {
|
|
return radioScope.querySelectorAll('input[type="radio"][name="' + name + '"]');
|
|
};
|
|
var radioSet;
|
|
if (typeof window !== 'undefined' && typeof window.CSS !== 'undefined' && typeof window.CSS.escape === 'function') {
|
|
radioSet = queryRadios(window.CSS.escape(node.name));
|
|
} else {
|
|
try {
|
|
radioSet = queryRadios(node.name);
|
|
} catch (err) {
|
|
// eslint-disable-next-line no-console
|
|
console.error('Looks like you have a radio button with a name attribute containing invalid CSS selector characters and need the CSS.escape polyfill: %s', err.message);
|
|
return false;
|
|
}
|
|
}
|
|
var checked = getCheckedRadio(radioSet, node.form);
|
|
return !checked || checked === node;
|
|
};
|
|
var isRadio = function isRadio(node) {
|
|
return isInput(node) && node.type === 'radio';
|
|
};
|
|
var isNonTabbableRadio = function isNonTabbableRadio(node) {
|
|
return isRadio(node) && !isTabbableRadio(node);
|
|
};
|
|
|
|
// determines if a node is ultimately attached to the window's document
|
|
var isNodeAttached = function isNodeAttached(node) {
|
|
var _nodeRoot;
|
|
// The root node is the shadow root if the node is in a shadow DOM; some document otherwise
|
|
// (but NOT _the_ document; see second 'If' comment below for more).
|
|
// If rootNode is shadow root, it'll have a host, which is the element to which the shadow
|
|
// is attached, and the one we need to check if it's in the document or not (because the
|
|
// shadow, and all nodes it contains, is never considered in the document since shadows
|
|
// behave like self-contained DOMs; but if the shadow's HOST, which is part of the document,
|
|
// is hidden, or is not in the document itself but is detached, it will affect the shadow's
|
|
// visibility, including all the nodes it contains). The host could be any normal node,
|
|
// or a custom element (i.e. web component). Either way, that's the one that is considered
|
|
// part of the document, not the shadow root, nor any of its children (i.e. the node being
|
|
// tested).
|
|
// To further complicate things, we have to look all the way up until we find a shadow HOST
|
|
// that is attached (or find none) because the node might be in nested shadows...
|
|
// If rootNode is not a shadow root, it won't have a host, and so rootNode should be the
|
|
// document (per the docs) and while it's a Document-type object, that document does not
|
|
// appear to be the same as the node's `ownerDocument` for some reason, so it's safer
|
|
// to ignore the rootNode at this point, and use `node.ownerDocument`. Otherwise,
|
|
// using `rootNode.contains(node)` will _always_ be true we'll get false-positives when
|
|
// node is actually detached.
|
|
// NOTE: If `nodeRootHost` or `node` happens to be the `document` itself (which is possible
|
|
// if a tabbable/focusable node was quickly added to the DOM, focused, and then removed
|
|
// from the DOM as in https://github.com/focus-trap/focus-trap-react/issues/905), then
|
|
// `ownerDocument` will be `null`, hence the optional chaining on it.
|
|
var nodeRoot = node && getRootNode(node);
|
|
var nodeRootHost = (_nodeRoot = nodeRoot) === null || _nodeRoot === void 0 ? void 0 : _nodeRoot.host;
|
|
|
|
// in some cases, a detached node will return itself as the root instead of a document or
|
|
// shadow root object, in which case, we shouldn't try to look further up the host chain
|
|
var attached = false;
|
|
if (nodeRoot && nodeRoot !== node) {
|
|
var _nodeRootHost, _nodeRootHost$ownerDo, _node$ownerDocument;
|
|
attached = !!((_nodeRootHost = nodeRootHost) !== null && _nodeRootHost !== void 0 && (_nodeRootHost$ownerDo = _nodeRootHost.ownerDocument) !== null && _nodeRootHost$ownerDo !== void 0 && _nodeRootHost$ownerDo.contains(nodeRootHost) || node !== null && node !== void 0 && (_node$ownerDocument = node.ownerDocument) !== null && _node$ownerDocument !== void 0 && _node$ownerDocument.contains(node));
|
|
while (!attached && nodeRootHost) {
|
|
var _nodeRoot2, _nodeRootHost2, _nodeRootHost2$ownerD;
|
|
// since it's not attached and we have a root host, the node MUST be in a nested shadow DOM,
|
|
// which means we need to get the host's host and check if that parent host is contained
|
|
// in (i.e. attached to) the document
|
|
nodeRoot = getRootNode(nodeRootHost);
|
|
nodeRootHost = (_nodeRoot2 = nodeRoot) === null || _nodeRoot2 === void 0 ? void 0 : _nodeRoot2.host;
|
|
attached = !!((_nodeRootHost2 = nodeRootHost) !== null && _nodeRootHost2 !== void 0 && (_nodeRootHost2$ownerD = _nodeRootHost2.ownerDocument) !== null && _nodeRootHost2$ownerD !== void 0 && _nodeRootHost2$ownerD.contains(nodeRootHost));
|
|
}
|
|
}
|
|
return attached;
|
|
};
|
|
var isZeroArea = function isZeroArea(node) {
|
|
var _node$getBoundingClie = node.getBoundingClientRect(),
|
|
width = _node$getBoundingClie.width,
|
|
height = _node$getBoundingClie.height;
|
|
return width === 0 && height === 0;
|
|
};
|
|
var isHidden = function isHidden(node, _ref) {
|
|
var displayCheck = _ref.displayCheck,
|
|
getShadowRoot = _ref.getShadowRoot;
|
|
// NOTE: visibility will be `undefined` if node is detached from the document
|
|
// (see notes about this further down), which means we will consider it visible
|
|
// (this is legacy behavior from a very long way back)
|
|
// NOTE: we check this regardless of `displayCheck="none"` because this is a
|
|
// _visibility_ check, not a _display_ check
|
|
if (getComputedStyle(node).visibility === 'hidden') {
|
|
return true;
|
|
}
|
|
var isDirectSummary = matches.call(node, 'details>summary:first-of-type');
|
|
var nodeUnderDetails = isDirectSummary ? node.parentElement : node;
|
|
if (matches.call(nodeUnderDetails, 'details:not([open]) *')) {
|
|
return true;
|
|
}
|
|
if (!displayCheck || displayCheck === 'full' || displayCheck === 'legacy-full') {
|
|
if (typeof getShadowRoot === 'function') {
|
|
// figure out if we should consider the node to be in an undisclosed shadow and use the
|
|
// 'non-zero-area' fallback
|
|
var originalNode = node;
|
|
while (node) {
|
|
var parentElement = node.parentElement;
|
|
var rootNode = getRootNode(node);
|
|
if (parentElement && !parentElement.shadowRoot && getShadowRoot(parentElement) === true // check if there's an undisclosed shadow
|
|
) {
|
|
// node has an undisclosed shadow which means we can only treat it as a black box, so we
|
|
// fall back to a non-zero-area test
|
|
return isZeroArea(node);
|
|
} else if (node.assignedSlot) {
|
|
// iterate up slot
|
|
node = node.assignedSlot;
|
|
} else if (!parentElement && rootNode !== node.ownerDocument) {
|
|
// cross shadow boundary
|
|
node = rootNode.host;
|
|
} else {
|
|
// iterate up normal dom
|
|
node = parentElement;
|
|
}
|
|
}
|
|
node = originalNode;
|
|
}
|
|
// else, `getShadowRoot` might be true, but all that does is enable shadow DOM support
|
|
// (i.e. it does not also presume that all nodes might have undisclosed shadows); or
|
|
// it might be a falsy value, which means shadow DOM support is disabled
|
|
|
|
// Since we didn't find it sitting in an undisclosed shadow (or shadows are disabled)
|
|
// now we can just test to see if it would normally be visible or not, provided it's
|
|
// attached to the main document.
|
|
// NOTE: We must consider case where node is inside a shadow DOM and given directly to
|
|
// `isTabbable()` or `isFocusable()` -- regardless of `getShadowRoot` option setting.
|
|
|
|
if (isNodeAttached(node)) {
|
|
// this works wherever the node is: if there's at least one client rect, it's
|
|
// somehow displayed; it also covers the CSS 'display: contents' case where the
|
|
// node itself is hidden in place of its contents; and there's no need to search
|
|
// up the hierarchy either
|
|
return !node.getClientRects().length;
|
|
}
|
|
|
|
// Else, the node isn't attached to the document, which means the `getClientRects()`
|
|
// API will __always__ return zero rects (this can happen, for example, if React
|
|
// is used to render nodes onto a detached tree, as confirmed in this thread:
|
|
// https://github.com/facebook/react/issues/9117#issuecomment-284228870)
|
|
//
|
|
// It also means that even window.getComputedStyle(node).display will return `undefined`
|
|
// because styles are only computed for nodes that are in the document.
|
|
//
|
|
// NOTE: THIS HAS BEEN THE CASE FOR YEARS. It is not new, nor is it caused by tabbable
|
|
// somehow. Though it was never stated officially, anyone who has ever used tabbable
|
|
// APIs on nodes in detached containers has actually implicitly used tabbable in what
|
|
// was later (as of v5.2.0 on Apr 9, 2021) called `displayCheck="none"` mode -- essentially
|
|
// considering __everything__ to be visible because of the innability to determine styles.
|
|
//
|
|
// v6.0.0: As of this major release, the default 'full' option __no longer treats detached
|
|
// nodes as visible with the 'none' fallback.__
|
|
if (displayCheck !== 'legacy-full') {
|
|
return true; // hidden
|
|
}
|
|
// else, fallback to 'none' mode and consider the node visible
|
|
} else if (displayCheck === 'non-zero-area') {
|
|
// NOTE: Even though this tests that the node's client rect is non-zero to determine
|
|
// whether it's displayed, and that a detached node will __always__ have a zero-area
|
|
// client rect, we don't special-case for whether the node is attached or not. In
|
|
// this mode, we do want to consider nodes that have a zero area to be hidden at all
|
|
// times, and that includes attached or not.
|
|
return isZeroArea(node);
|
|
}
|
|
|
|
// visible, as far as we can tell, or per current `displayCheck=none` mode, we assume
|
|
// it's visible
|
|
return false;
|
|
};
|
|
|
|
// form fields (nested) inside a disabled fieldset are not focusable/tabbable
|
|
// unless they are in the _first_ <legend> element of the top-most disabled
|
|
// fieldset
|
|
var isDisabledFromFieldset = function isDisabledFromFieldset(node) {
|
|
if (/^(INPUT|BUTTON|SELECT|TEXTAREA)$/.test(node.tagName)) {
|
|
var parentNode = node.parentElement;
|
|
// check if `node` is contained in a disabled <fieldset>
|
|
while (parentNode) {
|
|
if (parentNode.tagName === 'FIELDSET' && parentNode.disabled) {
|
|
// look for the first <legend> among the children of the disabled <fieldset>
|
|
for (var i = 0; i < parentNode.children.length; i++) {
|
|
var child = parentNode.children.item(i);
|
|
// when the first <legend> (in document order) is found
|
|
if (child.tagName === 'LEGEND') {
|
|
// if its parent <fieldset> is not nested in another disabled <fieldset>,
|
|
// return whether `node` is a descendant of its first <legend>
|
|
return matches.call(parentNode, 'fieldset[disabled] *') ? true : !child.contains(node);
|
|
}
|
|
}
|
|
// the disabled <fieldset> containing `node` has no <legend>
|
|
return true;
|
|
}
|
|
parentNode = parentNode.parentElement;
|
|
}
|
|
}
|
|
|
|
// else, node's tabbable/focusable state should not be affected by a fieldset's
|
|
// enabled/disabled state
|
|
return false;
|
|
};
|
|
var isNodeMatchingSelectorFocusable = function isNodeMatchingSelectorFocusable(options, node) {
|
|
if (node.disabled ||
|
|
// we must do an inert look up to filter out any elements inside an inert ancestor
|
|
// because we're limited in the type of selectors we can use in JSDom (see related
|
|
// note related to `candidateSelectors`)
|
|
isInert(node) || isHiddenInput(node) || isHidden(node, options) ||
|
|
// For a details element with a summary, the summary element gets the focus
|
|
isDetailsWithSummary(node) || isDisabledFromFieldset(node)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
var isNodeMatchingSelectorTabbable = function isNodeMatchingSelectorTabbable(options, node) {
|
|
if (isNonTabbableRadio(node) || getTabIndex(node) < 0 || !isNodeMatchingSelectorFocusable(options, node)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
var isValidShadowRootTabbable = function isValidShadowRootTabbable(shadowHostNode) {
|
|
var tabIndex = parseInt(shadowHostNode.getAttribute('tabindex'), 10);
|
|
if (isNaN(tabIndex) || tabIndex >= 0) {
|
|
return true;
|
|
}
|
|
// If a custom element has an explicit negative tabindex,
|
|
// browsers will not allow tab targeting said element's children.
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* @param {Array.<Element|CandidateScope>} candidates
|
|
* @returns Element[]
|
|
*/
|
|
var sortByOrder = function sortByOrder(candidates) {
|
|
var regularTabbables = [];
|
|
var orderedTabbables = [];
|
|
candidates.forEach(function (item, i) {
|
|
var isScope = !!item.scopeParent;
|
|
var element = isScope ? item.scopeParent : item;
|
|
var candidateTabindex = getSortOrderTabIndex(element, isScope);
|
|
var elements = isScope ? sortByOrder(item.candidates) : element;
|
|
if (candidateTabindex === 0) {
|
|
isScope ? regularTabbables.push.apply(regularTabbables, elements) : regularTabbables.push(element);
|
|
} else {
|
|
orderedTabbables.push({
|
|
documentOrder: i,
|
|
tabIndex: candidateTabindex,
|
|
item: item,
|
|
isScope: isScope,
|
|
content: elements
|
|
});
|
|
}
|
|
});
|
|
return orderedTabbables.sort(sortOrderedTabbables).reduce(function (acc, sortable) {
|
|
sortable.isScope ? acc.push.apply(acc, sortable.content) : acc.push(sortable.content);
|
|
return acc;
|
|
}, []).concat(regularTabbables);
|
|
};
|
|
var tabbable = function tabbable(container, options) {
|
|
options = options || {};
|
|
var candidates;
|
|
if (options.getShadowRoot) {
|
|
candidates = getCandidatesIteratively([container], options.includeContainer, {
|
|
filter: isNodeMatchingSelectorTabbable.bind(null, options),
|
|
flatten: false,
|
|
getShadowRoot: options.getShadowRoot,
|
|
shadowRootFilter: isValidShadowRootTabbable
|
|
});
|
|
} else {
|
|
candidates = getCandidates(container, options.includeContainer, isNodeMatchingSelectorTabbable.bind(null, options));
|
|
}
|
|
return sortByOrder(candidates);
|
|
};
|
|
var focusable = function focusable(container, options) {
|
|
options = options || {};
|
|
var candidates;
|
|
if (options.getShadowRoot) {
|
|
candidates = getCandidatesIteratively([container], options.includeContainer, {
|
|
filter: isNodeMatchingSelectorFocusable.bind(null, options),
|
|
flatten: true,
|
|
getShadowRoot: options.getShadowRoot
|
|
});
|
|
} else {
|
|
candidates = getCandidates(container, options.includeContainer, isNodeMatchingSelectorFocusable.bind(null, options));
|
|
}
|
|
return candidates;
|
|
};
|
|
var isTabbable = function isTabbable(node, options) {
|
|
options = options || {};
|
|
if (!node) {
|
|
throw new Error('No node provided');
|
|
}
|
|
if (matches.call(node, candidateSelector) === false) {
|
|
return false;
|
|
}
|
|
return isNodeMatchingSelectorTabbable(options, node);
|
|
};
|
|
|
|
// Avoid Chrome DevTools blue warning.
|
|
function getPlatform() {
|
|
const uaData = navigator.userAgentData;
|
|
if (uaData != null && uaData.platform) {
|
|
return uaData.platform;
|
|
}
|
|
return navigator.platform;
|
|
}
|
|
function getUserAgent() {
|
|
const uaData = navigator.userAgentData;
|
|
if (uaData && Array.isArray(uaData.brands)) {
|
|
return uaData.brands.map(_ref => {
|
|
let {
|
|
brand,
|
|
version
|
|
} = _ref;
|
|
return brand + "/" + version;
|
|
}).join(' ');
|
|
}
|
|
return navigator.userAgent;
|
|
}
|
|
function isSafari() {
|
|
// Chrome DevTools does not complain about navigator.vendor
|
|
return /apple/i.test(navigator.vendor);
|
|
}
|
|
function isAndroid() {
|
|
const re = /android/i;
|
|
return re.test(getPlatform()) || re.test(getUserAgent());
|
|
}
|
|
function isJSDOM() {
|
|
return getUserAgent().includes('jsdom/');
|
|
}
|
|
|
|
const FOCUSABLE_ATTRIBUTE$1 = 'data-floating-ui-focusable';
|
|
const TYPEABLE_SELECTOR = "input:not([type='hidden']):not([disabled])," + "[contenteditable]:not([contenteditable='false']),textarea:not([disabled])";
|
|
const ARROW_LEFT$1 = 'ArrowLeft';
|
|
const ARROW_RIGHT$1 = 'ArrowRight';
|
|
const ARROW_UP$1 = 'ArrowUp';
|
|
const ARROW_DOWN$1 = 'ArrowDown';
|
|
|
|
function activeElement(doc) {
|
|
let activeElement = doc.activeElement;
|
|
while (((_activeElement = activeElement) == null || (_activeElement = _activeElement.shadowRoot) == null ? void 0 : _activeElement.activeElement) != null) {
|
|
var _activeElement;
|
|
activeElement = activeElement.shadowRoot.activeElement;
|
|
}
|
|
return activeElement;
|
|
}
|
|
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 isEventTargetWithin(event, node) {
|
|
if (node == null) {
|
|
return false;
|
|
}
|
|
if ('composedPath' in event) {
|
|
return event.composedPath().includes(node);
|
|
}
|
|
|
|
// TS thinks `event` is of type never as it assumes all browsers support composedPath, but browsers without shadow dom don't
|
|
const e = event;
|
|
return e.target != null && node.contains(e.target);
|
|
}
|
|
function isRootElement(element) {
|
|
return element.matches('html,body');
|
|
}
|
|
function getDocument$1(node) {
|
|
return (node == null ? void 0 : node.ownerDocument) || document;
|
|
}
|
|
function isTypeableElement(element) {
|
|
return isHTMLElement(element) && element.matches(TYPEABLE_SELECTOR);
|
|
}
|
|
function isTypeableCombobox(element) {
|
|
if (!element) return false;
|
|
return element.getAttribute('role') === 'combobox' && isTypeableElement(element);
|
|
}
|
|
function getFloatingFocusElement(floatingElement) {
|
|
if (!floatingElement) {
|
|
return null;
|
|
}
|
|
// Try to find the element that has `{...getFloatingProps()}` spread on it.
|
|
// This indicates the floating element is acting as a positioning wrapper, and
|
|
// so focus should be managed on the child element with the event handlers and
|
|
// aria props.
|
|
return floatingElement.hasAttribute(FOCUSABLE_ATTRIBUTE$1) ? floatingElement : floatingElement.querySelector("[" + FOCUSABLE_ATTRIBUTE$1 + "]") || floatingElement;
|
|
}
|
|
|
|
function getNodeChildren(nodes, id, onlyOpenChildren) {
|
|
if (onlyOpenChildren === void 0) {
|
|
onlyOpenChildren = true;
|
|
}
|
|
let allChildren = nodes.filter(node => {
|
|
var _node$context;
|
|
return node.parentId === id && ((_node$context = node.context) == null ? void 0 : _node$context.open);
|
|
});
|
|
let currentChildren = allChildren;
|
|
while (currentChildren.length) {
|
|
currentChildren = onlyOpenChildren ? nodes.filter(node => {
|
|
var _currentChildren;
|
|
return (_currentChildren = currentChildren) == null ? void 0 : _currentChildren.some(n => {
|
|
var _node$context2;
|
|
return node.parentId === n.id && ((_node$context2 = node.context) == null ? void 0 : _node$context2.open);
|
|
});
|
|
}) : nodes;
|
|
allChildren = allChildren.concat(currentChildren);
|
|
}
|
|
return allChildren;
|
|
}
|
|
function getDeepestNode(nodes, id) {
|
|
let deepestNodeId;
|
|
let maxDepth = -1;
|
|
function findDeepest(nodeId, depth) {
|
|
if (depth > maxDepth) {
|
|
deepestNodeId = nodeId;
|
|
maxDepth = depth;
|
|
}
|
|
const children = getNodeChildren(nodes, nodeId);
|
|
children.forEach(child => {
|
|
findDeepest(child.id, depth + 1);
|
|
});
|
|
}
|
|
findDeepest(id, 0);
|
|
return nodes.find(node => node.id === deepestNodeId);
|
|
}
|
|
function getNodeAncestors(nodes, id) {
|
|
var _nodes$find;
|
|
let allAncestors = [];
|
|
let currentParentId = (_nodes$find = nodes.find(node => node.id === id)) == null ? void 0 : _nodes$find.parentId;
|
|
while (currentParentId) {
|
|
const currentNode = nodes.find(node => node.id === currentParentId);
|
|
currentParentId = currentNode == null ? void 0 : currentNode.parentId;
|
|
if (currentNode) {
|
|
allAncestors = allAncestors.concat(currentNode);
|
|
}
|
|
}
|
|
return allAncestors;
|
|
}
|
|
|
|
function stopEvent(event) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
}
|
|
function isReactEvent(event) {
|
|
return 'nativeEvent' in event;
|
|
}
|
|
|
|
// License: https://github.com/adobe/react-spectrum/blob/b35d5c02fe900badccd0cf1a8f23bb593419f238/packages/@react-aria/utils/src/isVirtualEvent.ts
|
|
function isVirtualClick(event) {
|
|
// FIXME: Firefox is now emitting a deprecation warning for `mozInputSource`.
|
|
// Try to find a workaround for this. `react-aria` source still has the check.
|
|
if (event.mozInputSource === 0 && event.isTrusted) {
|
|
return true;
|
|
}
|
|
if (isAndroid() && event.pointerType) {
|
|
return event.type === 'click' && event.buttons === 1;
|
|
}
|
|
return event.detail === 0 && !event.pointerType;
|
|
}
|
|
function isVirtualPointerEvent(event) {
|
|
if (isJSDOM()) return false;
|
|
return !isAndroid() && event.width === 0 && event.height === 0 || isAndroid() && event.width === 1 && event.height === 1 && event.pressure === 0 && event.detail === 0 && event.pointerType === 'mouse' ||
|
|
// iOS VoiceOver returns 0.333• for width/height.
|
|
event.width < 1 && event.height < 1 && event.pressure === 0 && event.detail === 0 && event.pointerType === 'touch';
|
|
}
|
|
|
|
var index$1 = typeof document !== 'undefined' ? React.useLayoutEffect : React.useEffect;
|
|
|
|
// https://github.com/mui/material-ui/issues/41190#issuecomment-2040873379
|
|
const SafeReact$1 = {
|
|
...React__namespace
|
|
};
|
|
|
|
function useLatestRef$1(value) {
|
|
const ref = React__namespace.useRef(value);
|
|
index$1(() => {
|
|
ref.current = value;
|
|
});
|
|
return ref;
|
|
}
|
|
const useInsertionEffect = SafeReact$1.useInsertionEffect;
|
|
const useSafeInsertionEffect = useInsertionEffect || (fn => fn());
|
|
function useEffectEvent(callback) {
|
|
const ref = React__namespace.useRef(() => {
|
|
if (process.env.NODE_ENV !== "production") {
|
|
throw new Error('Cannot call an event handler while rendering.');
|
|
}
|
|
});
|
|
useSafeInsertionEffect(() => {
|
|
ref.current = callback;
|
|
});
|
|
return React__namespace.useCallback(function () {
|
|
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
|
|
args[_key] = arguments[_key];
|
|
}
|
|
return ref.current == null ? void 0 : ref.current(...args);
|
|
}, []);
|
|
}
|
|
|
|
function isDifferentGridRow(index, cols, prevRow) {
|
|
return Math.floor(index / cols) !== prevRow;
|
|
}
|
|
function isIndexOutOfListBounds(listRef, index) {
|
|
return index < 0 || index >= listRef.current.length;
|
|
}
|
|
function getMinListIndex(listRef, disabledIndices) {
|
|
return findNonDisabledListIndex(listRef, {
|
|
disabledIndices
|
|
});
|
|
}
|
|
function getMaxListIndex(listRef, disabledIndices) {
|
|
return findNonDisabledListIndex(listRef, {
|
|
decrement: true,
|
|
startingIndex: listRef.current.length,
|
|
disabledIndices
|
|
});
|
|
}
|
|
function findNonDisabledListIndex(listRef, _temp) {
|
|
let {
|
|
startingIndex = -1,
|
|
decrement = false,
|
|
disabledIndices,
|
|
amount = 1
|
|
} = _temp === void 0 ? {} : _temp;
|
|
let index = startingIndex;
|
|
do {
|
|
index += decrement ? -amount : amount;
|
|
} while (index >= 0 && index <= listRef.current.length - 1 && isListIndexDisabled(listRef, index, disabledIndices));
|
|
return index;
|
|
}
|
|
function getGridNavigatedIndex(listRef, _ref) {
|
|
let {
|
|
event,
|
|
orientation,
|
|
loop,
|
|
rtl,
|
|
cols,
|
|
disabledIndices,
|
|
minIndex,
|
|
maxIndex,
|
|
prevIndex,
|
|
stopEvent: stop = false
|
|
} = _ref;
|
|
let nextIndex = prevIndex;
|
|
if (event.key === ARROW_UP$1) {
|
|
stop && stopEvent(event);
|
|
if (prevIndex === -1) {
|
|
nextIndex = maxIndex;
|
|
} else {
|
|
nextIndex = findNonDisabledListIndex(listRef, {
|
|
startingIndex: nextIndex,
|
|
amount: cols,
|
|
decrement: true,
|
|
disabledIndices
|
|
});
|
|
if (loop && (prevIndex - cols < minIndex || nextIndex < 0)) {
|
|
const col = prevIndex % cols;
|
|
const maxCol = maxIndex % cols;
|
|
const offset = maxIndex - (maxCol - col);
|
|
if (maxCol === col) {
|
|
nextIndex = maxIndex;
|
|
} else {
|
|
nextIndex = maxCol > col ? offset : offset - cols;
|
|
}
|
|
}
|
|
}
|
|
if (isIndexOutOfListBounds(listRef, nextIndex)) {
|
|
nextIndex = prevIndex;
|
|
}
|
|
}
|
|
if (event.key === ARROW_DOWN$1) {
|
|
stop && stopEvent(event);
|
|
if (prevIndex === -1) {
|
|
nextIndex = minIndex;
|
|
} else {
|
|
nextIndex = findNonDisabledListIndex(listRef, {
|
|
startingIndex: prevIndex,
|
|
amount: cols,
|
|
disabledIndices
|
|
});
|
|
if (loop && prevIndex + cols > maxIndex) {
|
|
nextIndex = findNonDisabledListIndex(listRef, {
|
|
startingIndex: prevIndex % cols - cols,
|
|
amount: cols,
|
|
disabledIndices
|
|
});
|
|
}
|
|
}
|
|
if (isIndexOutOfListBounds(listRef, nextIndex)) {
|
|
nextIndex = prevIndex;
|
|
}
|
|
}
|
|
|
|
// Remains on the same row/column.
|
|
if (orientation === 'both') {
|
|
const prevRow = floor(prevIndex / cols);
|
|
if (event.key === (rtl ? ARROW_LEFT$1 : ARROW_RIGHT$1)) {
|
|
stop && stopEvent(event);
|
|
if (prevIndex % cols !== cols - 1) {
|
|
nextIndex = findNonDisabledListIndex(listRef, {
|
|
startingIndex: prevIndex,
|
|
disabledIndices
|
|
});
|
|
if (loop && isDifferentGridRow(nextIndex, cols, prevRow)) {
|
|
nextIndex = findNonDisabledListIndex(listRef, {
|
|
startingIndex: prevIndex - prevIndex % cols - 1,
|
|
disabledIndices
|
|
});
|
|
}
|
|
} else if (loop) {
|
|
nextIndex = findNonDisabledListIndex(listRef, {
|
|
startingIndex: prevIndex - prevIndex % cols - 1,
|
|
disabledIndices
|
|
});
|
|
}
|
|
if (isDifferentGridRow(nextIndex, cols, prevRow)) {
|
|
nextIndex = prevIndex;
|
|
}
|
|
}
|
|
if (event.key === (rtl ? ARROW_RIGHT$1 : ARROW_LEFT$1)) {
|
|
stop && stopEvent(event);
|
|
if (prevIndex % cols !== 0) {
|
|
nextIndex = findNonDisabledListIndex(listRef, {
|
|
startingIndex: prevIndex,
|
|
decrement: true,
|
|
disabledIndices
|
|
});
|
|
if (loop && isDifferentGridRow(nextIndex, cols, prevRow)) {
|
|
nextIndex = findNonDisabledListIndex(listRef, {
|
|
startingIndex: prevIndex + (cols - prevIndex % cols),
|
|
decrement: true,
|
|
disabledIndices
|
|
});
|
|
}
|
|
} else if (loop) {
|
|
nextIndex = findNonDisabledListIndex(listRef, {
|
|
startingIndex: prevIndex + (cols - prevIndex % cols),
|
|
decrement: true,
|
|
disabledIndices
|
|
});
|
|
}
|
|
if (isDifferentGridRow(nextIndex, cols, prevRow)) {
|
|
nextIndex = prevIndex;
|
|
}
|
|
}
|
|
const lastRow = floor(maxIndex / cols) === prevRow;
|
|
if (isIndexOutOfListBounds(listRef, nextIndex)) {
|
|
if (loop && lastRow) {
|
|
nextIndex = event.key === (rtl ? ARROW_RIGHT$1 : ARROW_LEFT$1) ? maxIndex : findNonDisabledListIndex(listRef, {
|
|
startingIndex: prevIndex - prevIndex % cols - 1,
|
|
disabledIndices
|
|
});
|
|
} else {
|
|
nextIndex = prevIndex;
|
|
}
|
|
}
|
|
}
|
|
return nextIndex;
|
|
}
|
|
|
|
/** For each cell index, gets the item index that occupies that cell */
|
|
function createGridCellMap(sizes, cols, dense) {
|
|
const cellMap = [];
|
|
let startIndex = 0;
|
|
sizes.forEach((_ref2, index) => {
|
|
let {
|
|
width,
|
|
height
|
|
} = _ref2;
|
|
if (width > cols) {
|
|
if (process.env.NODE_ENV !== "production") {
|
|
throw new Error("[Floating UI]: Invalid grid - item width at index " + index + " is greater than grid columns");
|
|
}
|
|
}
|
|
let itemPlaced = false;
|
|
if (dense) {
|
|
startIndex = 0;
|
|
}
|
|
while (!itemPlaced) {
|
|
const targetCells = [];
|
|
for (let i = 0; i < width; i++) {
|
|
for (let j = 0; j < height; j++) {
|
|
targetCells.push(startIndex + i + j * cols);
|
|
}
|
|
}
|
|
if (startIndex % cols + width <= cols && targetCells.every(cell => cellMap[cell] == null)) {
|
|
targetCells.forEach(cell => {
|
|
cellMap[cell] = index;
|
|
});
|
|
itemPlaced = true;
|
|
} else {
|
|
startIndex++;
|
|
}
|
|
}
|
|
});
|
|
|
|
// convert into a non-sparse array
|
|
return [...cellMap];
|
|
}
|
|
|
|
/** Gets cell index of an item's corner or -1 when index is -1. */
|
|
function getGridCellIndexOfCorner(index, sizes, cellMap, cols, corner) {
|
|
if (index === -1) return -1;
|
|
const firstCellIndex = cellMap.indexOf(index);
|
|
const sizeItem = sizes[index];
|
|
switch (corner) {
|
|
case 'tl':
|
|
return firstCellIndex;
|
|
case 'tr':
|
|
if (!sizeItem) {
|
|
return firstCellIndex;
|
|
}
|
|
return firstCellIndex + sizeItem.width - 1;
|
|
case 'bl':
|
|
if (!sizeItem) {
|
|
return firstCellIndex;
|
|
}
|
|
return firstCellIndex + (sizeItem.height - 1) * cols;
|
|
case 'br':
|
|
return cellMap.lastIndexOf(index);
|
|
}
|
|
}
|
|
|
|
/** Gets all cell indices that correspond to the specified indices */
|
|
function getGridCellIndices(indices, cellMap) {
|
|
return cellMap.flatMap((index, cellIndex) => indices.includes(index) ? [cellIndex] : []);
|
|
}
|
|
function isListIndexDisabled(listRef, index, disabledIndices) {
|
|
if (disabledIndices) {
|
|
return disabledIndices.includes(index);
|
|
}
|
|
const element = listRef.current[index];
|
|
return element == null || element.hasAttribute('disabled') || element.getAttribute('aria-disabled') === 'true';
|
|
}
|
|
|
|
const getTabbableOptions = () => ({
|
|
getShadowRoot: true,
|
|
displayCheck:
|
|
// JSDOM does not support the `tabbable` library. To solve this we can
|
|
// check if `ResizeObserver` is a real function (not polyfilled), which
|
|
// determines if the current environment is JSDOM-like.
|
|
typeof ResizeObserver === 'function' && ResizeObserver.toString().includes('[native code]') ? 'full' : 'none'
|
|
});
|
|
function getTabbableIn(container, dir) {
|
|
const list = tabbable(container, getTabbableOptions());
|
|
const len = list.length;
|
|
if (len === 0) return;
|
|
const active = activeElement(getDocument$1(container));
|
|
const index = list.indexOf(active);
|
|
const nextIndex = index === -1 ? dir === 1 ? 0 : len - 1 : index + dir;
|
|
return list[nextIndex];
|
|
}
|
|
function getNextTabbable(referenceElement) {
|
|
return getTabbableIn(getDocument$1(referenceElement).body, 1) || referenceElement;
|
|
}
|
|
function getPreviousTabbable(referenceElement) {
|
|
return getTabbableIn(getDocument$1(referenceElement).body, -1) || referenceElement;
|
|
}
|
|
function isOutsideEvent(event, container) {
|
|
const containerElement = container || event.currentTarget;
|
|
const relatedTarget = event.relatedTarget;
|
|
return !relatedTarget || !contains(containerElement, relatedTarget);
|
|
}
|
|
function disableFocusInside(container) {
|
|
const tabbableElements = tabbable(container, getTabbableOptions());
|
|
tabbableElements.forEach(element => {
|
|
element.dataset.tabindex = element.getAttribute('tabindex') || '';
|
|
element.setAttribute('tabindex', '-1');
|
|
});
|
|
}
|
|
function enableFocusInside(container) {
|
|
const elements = container.querySelectorAll('[data-tabindex]');
|
|
elements.forEach(element => {
|
|
const tabindex = element.dataset.tabindex;
|
|
delete element.dataset.tabindex;
|
|
if (tabindex) {
|
|
element.setAttribute('tabindex', tabindex);
|
|
} else {
|
|
element.removeAttribute('tabindex');
|
|
}
|
|
});
|
|
}
|
|
|
|
function computeCoordsFromPlacement(_ref, placement, rtl) {
|
|
let {
|
|
reference,
|
|
floating
|
|
} = _ref;
|
|
const sideAxis = getSideAxis(placement);
|
|
const alignmentAxis = getAlignmentAxis(placement);
|
|
const alignLength = getAxisLength(alignmentAxis);
|
|
const side = getSide(placement);
|
|
const isVertical = sideAxis === 'y';
|
|
const commonX = reference.x + reference.width / 2 - floating.width / 2;
|
|
const commonY = reference.y + reference.height / 2 - floating.height / 2;
|
|
const commonAlign = reference[alignLength] / 2 - floating[alignLength] / 2;
|
|
let coords;
|
|
switch (side) {
|
|
case 'top':
|
|
coords = {
|
|
x: commonX,
|
|
y: reference.y - floating.height
|
|
};
|
|
break;
|
|
case 'bottom':
|
|
coords = {
|
|
x: commonX,
|
|
y: reference.y + reference.height
|
|
};
|
|
break;
|
|
case 'right':
|
|
coords = {
|
|
x: reference.x + reference.width,
|
|
y: commonY
|
|
};
|
|
break;
|
|
case 'left':
|
|
coords = {
|
|
x: reference.x - floating.width,
|
|
y: commonY
|
|
};
|
|
break;
|
|
default:
|
|
coords = {
|
|
x: reference.x,
|
|
y: reference.y
|
|
};
|
|
}
|
|
switch (getAlignment(placement)) {
|
|
case 'start':
|
|
coords[alignmentAxis] -= commonAlign * (rtl && isVertical ? -1 : 1);
|
|
break;
|
|
case 'end':
|
|
coords[alignmentAxis] += commonAlign * (rtl && isVertical ? -1 : 1);
|
|
break;
|
|
}
|
|
return coords;
|
|
}
|
|
|
|
/**
|
|
* Computes the `x` and `y` coordinates that will place the floating element
|
|
* next to a given reference element.
|
|
*
|
|
* This export does not have any `platform` interface logic. You will need to
|
|
* write one for the platform you are using Floating UI with.
|
|
*/
|
|
const computePosition$1 = async (reference, floating, config) => {
|
|
const {
|
|
placement = 'bottom',
|
|
strategy = 'absolute',
|
|
middleware = [],
|
|
platform
|
|
} = config;
|
|
const validMiddleware = middleware.filter(Boolean);
|
|
const rtl = await (platform.isRTL == null ? void 0 : platform.isRTL(floating));
|
|
let rects = await platform.getElementRects({
|
|
reference,
|
|
floating,
|
|
strategy
|
|
});
|
|
let {
|
|
x,
|
|
y
|
|
} = computeCoordsFromPlacement(rects, placement, rtl);
|
|
let statefulPlacement = placement;
|
|
let middlewareData = {};
|
|
let resetCount = 0;
|
|
for (let i = 0; i < validMiddleware.length; i++) {
|
|
const {
|
|
name,
|
|
fn
|
|
} = validMiddleware[i];
|
|
const {
|
|
x: nextX,
|
|
y: nextY,
|
|
data,
|
|
reset
|
|
} = await fn({
|
|
x,
|
|
y,
|
|
initialPlacement: placement,
|
|
placement: statefulPlacement,
|
|
strategy,
|
|
middlewareData,
|
|
rects,
|
|
platform,
|
|
elements: {
|
|
reference,
|
|
floating
|
|
}
|
|
});
|
|
x = nextX != null ? nextX : x;
|
|
y = nextY != null ? nextY : y;
|
|
middlewareData = {
|
|
...middlewareData,
|
|
[name]: {
|
|
...middlewareData[name],
|
|
...data
|
|
}
|
|
};
|
|
if (reset && resetCount <= 50) {
|
|
resetCount++;
|
|
if (typeof reset === 'object') {
|
|
if (reset.placement) {
|
|
statefulPlacement = reset.placement;
|
|
}
|
|
if (reset.rects) {
|
|
rects = reset.rects === true ? await platform.getElementRects({
|
|
reference,
|
|
floating,
|
|
strategy
|
|
}) : reset.rects;
|
|
}
|
|
({
|
|
x,
|
|
y
|
|
} = computeCoordsFromPlacement(rects, statefulPlacement, rtl));
|
|
}
|
|
i = -1;
|
|
}
|
|
}
|
|
return {
|
|
x,
|
|
y,
|
|
placement: statefulPlacement,
|
|
strategy,
|
|
middlewareData
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Resolves with an object of overflow side offsets that determine how much the
|
|
* element is overflowing a given clipping boundary on each side.
|
|
* - positive = overflowing the boundary by that number of pixels
|
|
* - negative = how many pixels left before it will overflow
|
|
* - 0 = lies flush with the boundary
|
|
* @see https://floating-ui.com/docs/detectOverflow
|
|
*/
|
|
async function detectOverflow(state, options) {
|
|
var _await$platform$isEle;
|
|
if (options === void 0) {
|
|
options = {};
|
|
}
|
|
const {
|
|
x,
|
|
y,
|
|
platform,
|
|
rects,
|
|
elements,
|
|
strategy
|
|
} = state;
|
|
const {
|
|
boundary = 'clippingAncestors',
|
|
rootBoundary = 'viewport',
|
|
elementContext = 'floating',
|
|
altBoundary = false,
|
|
padding = 0
|
|
} = evaluate(options, state);
|
|
const paddingObject = getPaddingObject(padding);
|
|
const altContext = elementContext === 'floating' ? 'reference' : 'floating';
|
|
const element = elements[altBoundary ? altContext : elementContext];
|
|
const clippingClientRect = rectToClientRect(await platform.getClippingRect({
|
|
element: ((_await$platform$isEle = await (platform.isElement == null ? void 0 : platform.isElement(element))) != null ? _await$platform$isEle : true) ? element : element.contextElement || (await (platform.getDocumentElement == null ? void 0 : platform.getDocumentElement(elements.floating))),
|
|
boundary,
|
|
rootBoundary,
|
|
strategy
|
|
}));
|
|
const rect = elementContext === 'floating' ? {
|
|
x,
|
|
y,
|
|
width: rects.floating.width,
|
|
height: rects.floating.height
|
|
} : rects.reference;
|
|
const offsetParent = await (platform.getOffsetParent == null ? void 0 : platform.getOffsetParent(elements.floating));
|
|
const offsetScale = (await (platform.isElement == null ? void 0 : platform.isElement(offsetParent))) ? (await (platform.getScale == null ? void 0 : platform.getScale(offsetParent))) || {
|
|
x: 1,
|
|
y: 1
|
|
} : {
|
|
x: 1,
|
|
y: 1
|
|
};
|
|
const elementClientRect = rectToClientRect(platform.convertOffsetParentRelativeRectToViewportRelativeRect ? await platform.convertOffsetParentRelativeRectToViewportRelativeRect({
|
|
elements,
|
|
rect,
|
|
offsetParent,
|
|
strategy
|
|
}) : rect);
|
|
return {
|
|
top: (clippingClientRect.top - elementClientRect.top + paddingObject.top) / offsetScale.y,
|
|
bottom: (elementClientRect.bottom - clippingClientRect.bottom + paddingObject.bottom) / offsetScale.y,
|
|
left: (clippingClientRect.left - elementClientRect.left + paddingObject.left) / offsetScale.x,
|
|
right: (elementClientRect.right - clippingClientRect.right + paddingObject.right) / offsetScale.x
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Optimizes the visibility of the floating element by flipping the `placement`
|
|
* in order to keep it in view when the preferred placement(s) will overflow the
|
|
* clipping boundary. Alternative to `autoPlacement`.
|
|
* @see https://floating-ui.com/docs/flip
|
|
*/
|
|
const flip$2 = function (options) {
|
|
if (options === void 0) {
|
|
options = {};
|
|
}
|
|
return {
|
|
name: 'flip',
|
|
options,
|
|
async fn(state) {
|
|
var _middlewareData$arrow, _middlewareData$flip;
|
|
const {
|
|
placement,
|
|
middlewareData,
|
|
rects,
|
|
initialPlacement,
|
|
platform,
|
|
elements
|
|
} = state;
|
|
const {
|
|
mainAxis: checkMainAxis = true,
|
|
crossAxis: checkCrossAxis = true,
|
|
fallbackPlacements: specifiedFallbackPlacements,
|
|
fallbackStrategy = 'bestFit',
|
|
fallbackAxisSideDirection = 'none',
|
|
flipAlignment = true,
|
|
...detectOverflowOptions
|
|
} = evaluate(options, state);
|
|
|
|
// If a reset by the arrow was caused due to an alignment offset being
|
|
// added, we should skip any logic now since `flip()` has already done its
|
|
// work.
|
|
// https://github.com/floating-ui/floating-ui/issues/2549#issuecomment-1719601643
|
|
if ((_middlewareData$arrow = middlewareData.arrow) != null && _middlewareData$arrow.alignmentOffset) {
|
|
return {};
|
|
}
|
|
const side = getSide(placement);
|
|
const initialSideAxis = getSideAxis(initialPlacement);
|
|
const isBasePlacement = getSide(initialPlacement) === initialPlacement;
|
|
const rtl = await (platform.isRTL == null ? void 0 : platform.isRTL(elements.floating));
|
|
const fallbackPlacements = specifiedFallbackPlacements || (isBasePlacement || !flipAlignment ? [getOppositePlacement(initialPlacement)] : getExpandedPlacements(initialPlacement));
|
|
const hasFallbackAxisSideDirection = fallbackAxisSideDirection !== 'none';
|
|
if (!specifiedFallbackPlacements && hasFallbackAxisSideDirection) {
|
|
fallbackPlacements.push(...getOppositeAxisPlacements(initialPlacement, flipAlignment, fallbackAxisSideDirection, rtl));
|
|
}
|
|
const placements = [initialPlacement, ...fallbackPlacements];
|
|
const overflow = await detectOverflow(state, detectOverflowOptions);
|
|
const overflows = [];
|
|
let overflowsData = ((_middlewareData$flip = middlewareData.flip) == null ? void 0 : _middlewareData$flip.overflows) || [];
|
|
if (checkMainAxis) {
|
|
overflows.push(overflow[side]);
|
|
}
|
|
if (checkCrossAxis) {
|
|
const sides = getAlignmentSides(placement, rects, rtl);
|
|
overflows.push(overflow[sides[0]], overflow[sides[1]]);
|
|
}
|
|
overflowsData = [...overflowsData, {
|
|
placement,
|
|
overflows
|
|
}];
|
|
|
|
// One or more sides is overflowing.
|
|
if (!overflows.every(side => side <= 0)) {
|
|
var _middlewareData$flip2, _overflowsData$filter;
|
|
const nextIndex = (((_middlewareData$flip2 = middlewareData.flip) == null ? void 0 : _middlewareData$flip2.index) || 0) + 1;
|
|
const nextPlacement = placements[nextIndex];
|
|
if (nextPlacement) {
|
|
// Try next placement and re-run the lifecycle.
|
|
return {
|
|
data: {
|
|
index: nextIndex,
|
|
overflows: overflowsData
|
|
},
|
|
reset: {
|
|
placement: nextPlacement
|
|
}
|
|
};
|
|
}
|
|
|
|
// First, find the candidates that fit on the mainAxis side of overflow,
|
|
// then find the placement that fits the best on the main crossAxis side.
|
|
let resetPlacement = (_overflowsData$filter = overflowsData.filter(d => d.overflows[0] <= 0).sort((a, b) => a.overflows[1] - b.overflows[1])[0]) == null ? void 0 : _overflowsData$filter.placement;
|
|
|
|
// Otherwise fallback.
|
|
if (!resetPlacement) {
|
|
switch (fallbackStrategy) {
|
|
case 'bestFit':
|
|
{
|
|
var _overflowsData$filter2;
|
|
const placement = (_overflowsData$filter2 = overflowsData.filter(d => {
|
|
if (hasFallbackAxisSideDirection) {
|
|
const currentSideAxis = getSideAxis(d.placement);
|
|
return currentSideAxis === initialSideAxis ||
|
|
// Create a bias to the `y` side axis due to horizontal
|
|
// reading directions favoring greater width.
|
|
currentSideAxis === 'y';
|
|
}
|
|
return true;
|
|
}).map(d => [d.placement, d.overflows.filter(overflow => overflow > 0).reduce((acc, overflow) => acc + overflow, 0)]).sort((a, b) => a[1] - b[1])[0]) == null ? void 0 : _overflowsData$filter2[0];
|
|
if (placement) {
|
|
resetPlacement = placement;
|
|
}
|
|
break;
|
|
}
|
|
case 'initialPlacement':
|
|
resetPlacement = initialPlacement;
|
|
break;
|
|
}
|
|
}
|
|
if (placement !== resetPlacement) {
|
|
return {
|
|
reset: {
|
|
placement: resetPlacement
|
|
}
|
|
};
|
|
}
|
|
}
|
|
return {};
|
|
}
|
|
};
|
|
};
|
|
|
|
// For type backwards-compatibility, the `OffsetOptions` type was also
|
|
// Derivable.
|
|
|
|
async function convertValueToCoords(state, options) {
|
|
const {
|
|
placement,
|
|
platform,
|
|
elements
|
|
} = state;
|
|
const rtl = await (platform.isRTL == null ? void 0 : platform.isRTL(elements.floating));
|
|
const side = getSide(placement);
|
|
const alignment = getAlignment(placement);
|
|
const isVertical = getSideAxis(placement) === 'y';
|
|
const mainAxisMulti = ['left', 'top'].includes(side) ? -1 : 1;
|
|
const crossAxisMulti = rtl && isVertical ? -1 : 1;
|
|
const rawValue = evaluate(options, state);
|
|
|
|
// eslint-disable-next-line prefer-const
|
|
let {
|
|
mainAxis,
|
|
crossAxis,
|
|
alignmentAxis
|
|
} = typeof rawValue === 'number' ? {
|
|
mainAxis: rawValue,
|
|
crossAxis: 0,
|
|
alignmentAxis: null
|
|
} : {
|
|
mainAxis: rawValue.mainAxis || 0,
|
|
crossAxis: rawValue.crossAxis || 0,
|
|
alignmentAxis: rawValue.alignmentAxis
|
|
};
|
|
if (alignment && typeof alignmentAxis === 'number') {
|
|
crossAxis = alignment === 'end' ? alignmentAxis * -1 : alignmentAxis;
|
|
}
|
|
return isVertical ? {
|
|
x: crossAxis * crossAxisMulti,
|
|
y: mainAxis * mainAxisMulti
|
|
} : {
|
|
x: mainAxis * mainAxisMulti,
|
|
y: crossAxis * crossAxisMulti
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Modifies the placement by translating the floating element along the
|
|
* specified axes.
|
|
* A number (shorthand for `mainAxis` or distance), or an axes configuration
|
|
* object may be passed.
|
|
* @see https://floating-ui.com/docs/offset
|
|
*/
|
|
const offset$2 = function (options) {
|
|
if (options === void 0) {
|
|
options = 0;
|
|
}
|
|
return {
|
|
name: 'offset',
|
|
options,
|
|
async fn(state) {
|
|
var _middlewareData$offse, _middlewareData$arrow;
|
|
const {
|
|
x,
|
|
y,
|
|
placement,
|
|
middlewareData
|
|
} = state;
|
|
const diffCoords = await convertValueToCoords(state, options);
|
|
|
|
// If the placement is the same and the arrow caused an alignment offset
|
|
// then we don't need to change the positioning coordinates.
|
|
if (placement === ((_middlewareData$offse = middlewareData.offset) == null ? void 0 : _middlewareData$offse.placement) && (_middlewareData$arrow = middlewareData.arrow) != null && _middlewareData$arrow.alignmentOffset) {
|
|
return {};
|
|
}
|
|
return {
|
|
x: x + diffCoords.x,
|
|
y: y + diffCoords.y,
|
|
data: {
|
|
...diffCoords,
|
|
placement
|
|
}
|
|
};
|
|
}
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Optimizes the visibility of the floating element by shifting it in order to
|
|
* keep it in view when it will overflow the clipping boundary.
|
|
* @see https://floating-ui.com/docs/shift
|
|
*/
|
|
const shift$2 = function (options) {
|
|
if (options === void 0) {
|
|
options = {};
|
|
}
|
|
return {
|
|
name: 'shift',
|
|
options,
|
|
async fn(state) {
|
|
const {
|
|
x,
|
|
y,
|
|
placement
|
|
} = state;
|
|
const {
|
|
mainAxis: checkMainAxis = true,
|
|
crossAxis: checkCrossAxis = false,
|
|
limiter = {
|
|
fn: _ref => {
|
|
let {
|
|
x,
|
|
y
|
|
} = _ref;
|
|
return {
|
|
x,
|
|
y
|
|
};
|
|
}
|
|
},
|
|
...detectOverflowOptions
|
|
} = evaluate(options, state);
|
|
const coords = {
|
|
x,
|
|
y
|
|
};
|
|
const overflow = await detectOverflow(state, detectOverflowOptions);
|
|
const crossAxis = getSideAxis(getSide(placement));
|
|
const mainAxis = getOppositeAxis(crossAxis);
|
|
let mainAxisCoord = coords[mainAxis];
|
|
let crossAxisCoord = coords[crossAxis];
|
|
if (checkMainAxis) {
|
|
const minSide = mainAxis === 'y' ? 'top' : 'left';
|
|
const maxSide = mainAxis === 'y' ? 'bottom' : 'right';
|
|
const min = mainAxisCoord + overflow[minSide];
|
|
const max = mainAxisCoord - overflow[maxSide];
|
|
mainAxisCoord = clamp(min, mainAxisCoord, max);
|
|
}
|
|
if (checkCrossAxis) {
|
|
const minSide = crossAxis === 'y' ? 'top' : 'left';
|
|
const maxSide = crossAxis === 'y' ? 'bottom' : 'right';
|
|
const min = crossAxisCoord + overflow[minSide];
|
|
const max = crossAxisCoord - overflow[maxSide];
|
|
crossAxisCoord = clamp(min, crossAxisCoord, max);
|
|
}
|
|
const limitedCoords = limiter.fn({
|
|
...state,
|
|
[mainAxis]: mainAxisCoord,
|
|
[crossAxis]: crossAxisCoord
|
|
});
|
|
return {
|
|
...limitedCoords,
|
|
data: {
|
|
x: limitedCoords.x - x,
|
|
y: limitedCoords.y - y,
|
|
enabled: {
|
|
[mainAxis]: checkMainAxis,
|
|
[crossAxis]: checkCrossAxis
|
|
}
|
|
}
|
|
};
|
|
}
|
|
};
|
|
};
|
|
|
|
function getCssDimensions(element) {
|
|
const css = getComputedStyle$1(element);
|
|
// In testing environments, the `width` and `height` properties are empty
|
|
// strings for SVG elements, returning NaN. Fallback to `0` in this case.
|
|
let width = parseFloat(css.width) || 0;
|
|
let height = parseFloat(css.height) || 0;
|
|
const hasOffset = isHTMLElement(element);
|
|
const offsetWidth = hasOffset ? element.offsetWidth : width;
|
|
const offsetHeight = hasOffset ? element.offsetHeight : height;
|
|
const shouldFallback = round(width) !== offsetWidth || round(height) !== offsetHeight;
|
|
if (shouldFallback) {
|
|
width = offsetWidth;
|
|
height = offsetHeight;
|
|
}
|
|
return {
|
|
width,
|
|
height,
|
|
$: shouldFallback
|
|
};
|
|
}
|
|
|
|
function unwrapElement(element) {
|
|
return !isElement(element) ? element.contextElement : element;
|
|
}
|
|
|
|
function getScale(element) {
|
|
const domElement = unwrapElement(element);
|
|
if (!isHTMLElement(domElement)) {
|
|
return createCoords(1);
|
|
}
|
|
const rect = domElement.getBoundingClientRect();
|
|
const {
|
|
width,
|
|
height,
|
|
$
|
|
} = getCssDimensions(domElement);
|
|
let x = ($ ? round(rect.width) : rect.width) / width;
|
|
let y = ($ ? round(rect.height) : rect.height) / height;
|
|
|
|
// 0, NaN, or Infinity should always fallback to 1.
|
|
|
|
if (!x || !Number.isFinite(x)) {
|
|
x = 1;
|
|
}
|
|
if (!y || !Number.isFinite(y)) {
|
|
y = 1;
|
|
}
|
|
return {
|
|
x,
|
|
y
|
|
};
|
|
}
|
|
|
|
const noOffsets = /*#__PURE__*/createCoords(0);
|
|
function getVisualOffsets(element) {
|
|
const win = getWindow(element);
|
|
if (!isWebKit() || !win.visualViewport) {
|
|
return noOffsets;
|
|
}
|
|
return {
|
|
x: win.visualViewport.offsetLeft,
|
|
y: win.visualViewport.offsetTop
|
|
};
|
|
}
|
|
function shouldAddVisualOffsets(element, isFixed, floatingOffsetParent) {
|
|
if (isFixed === void 0) {
|
|
isFixed = false;
|
|
}
|
|
if (!floatingOffsetParent || isFixed && floatingOffsetParent !== getWindow(element)) {
|
|
return false;
|
|
}
|
|
return isFixed;
|
|
}
|
|
|
|
function getBoundingClientRect(element, includeScale, isFixedStrategy, offsetParent) {
|
|
if (includeScale === void 0) {
|
|
includeScale = false;
|
|
}
|
|
if (isFixedStrategy === void 0) {
|
|
isFixedStrategy = false;
|
|
}
|
|
const clientRect = element.getBoundingClientRect();
|
|
const domElement = unwrapElement(element);
|
|
let scale = createCoords(1);
|
|
if (includeScale) {
|
|
if (offsetParent) {
|
|
if (isElement(offsetParent)) {
|
|
scale = getScale(offsetParent);
|
|
}
|
|
} else {
|
|
scale = getScale(element);
|
|
}
|
|
}
|
|
const visualOffsets = shouldAddVisualOffsets(domElement, isFixedStrategy, offsetParent) ? getVisualOffsets(domElement) : createCoords(0);
|
|
let x = (clientRect.left + visualOffsets.x) / scale.x;
|
|
let y = (clientRect.top + visualOffsets.y) / scale.y;
|
|
let width = clientRect.width / scale.x;
|
|
let height = clientRect.height / scale.y;
|
|
if (domElement) {
|
|
const win = getWindow(domElement);
|
|
const offsetWin = offsetParent && isElement(offsetParent) ? getWindow(offsetParent) : offsetParent;
|
|
let currentWin = win;
|
|
let currentIFrame = getFrameElement(currentWin);
|
|
while (currentIFrame && offsetParent && offsetWin !== currentWin) {
|
|
const iframeScale = getScale(currentIFrame);
|
|
const iframeRect = currentIFrame.getBoundingClientRect();
|
|
const css = getComputedStyle$1(currentIFrame);
|
|
const left = iframeRect.left + (currentIFrame.clientLeft + parseFloat(css.paddingLeft)) * iframeScale.x;
|
|
const top = iframeRect.top + (currentIFrame.clientTop + parseFloat(css.paddingTop)) * iframeScale.y;
|
|
x *= iframeScale.x;
|
|
y *= iframeScale.y;
|
|
width *= iframeScale.x;
|
|
height *= iframeScale.y;
|
|
x += left;
|
|
y += top;
|
|
currentWin = getWindow(currentIFrame);
|
|
currentIFrame = getFrameElement(currentWin);
|
|
}
|
|
}
|
|
return rectToClientRect({
|
|
width,
|
|
height,
|
|
x,
|
|
y
|
|
});
|
|
}
|
|
|
|
// If <html> has a CSS width greater than the viewport, then this will be
|
|
// incorrect for RTL.
|
|
function getWindowScrollBarX(element, rect) {
|
|
const leftScroll = getNodeScroll(element).scrollLeft;
|
|
if (!rect) {
|
|
return getBoundingClientRect(getDocumentElement(element)).left + leftScroll;
|
|
}
|
|
return rect.left + leftScroll;
|
|
}
|
|
|
|
function getHTMLOffset(documentElement, scroll, ignoreScrollbarX) {
|
|
if (ignoreScrollbarX === void 0) {
|
|
ignoreScrollbarX = false;
|
|
}
|
|
const htmlRect = documentElement.getBoundingClientRect();
|
|
const x = htmlRect.left + scroll.scrollLeft - (ignoreScrollbarX ? 0 :
|
|
// RTL <body> scrollbar.
|
|
getWindowScrollBarX(documentElement, htmlRect));
|
|
const y = htmlRect.top + scroll.scrollTop;
|
|
return {
|
|
x,
|
|
y
|
|
};
|
|
}
|
|
|
|
function convertOffsetParentRelativeRectToViewportRelativeRect(_ref) {
|
|
let {
|
|
elements,
|
|
rect,
|
|
offsetParent,
|
|
strategy
|
|
} = _ref;
|
|
const isFixed = strategy === 'fixed';
|
|
const documentElement = getDocumentElement(offsetParent);
|
|
const topLayer = elements ? isTopLayer(elements.floating) : false;
|
|
if (offsetParent === documentElement || topLayer && isFixed) {
|
|
return rect;
|
|
}
|
|
let scroll = {
|
|
scrollLeft: 0,
|
|
scrollTop: 0
|
|
};
|
|
let scale = createCoords(1);
|
|
const offsets = createCoords(0);
|
|
const isOffsetParentAnElement = isHTMLElement(offsetParent);
|
|
if (isOffsetParentAnElement || !isOffsetParentAnElement && !isFixed) {
|
|
if (getNodeName(offsetParent) !== 'body' || isOverflowElement(documentElement)) {
|
|
scroll = getNodeScroll(offsetParent);
|
|
}
|
|
if (isHTMLElement(offsetParent)) {
|
|
const offsetRect = getBoundingClientRect(offsetParent);
|
|
scale = getScale(offsetParent);
|
|
offsets.x = offsetRect.x + offsetParent.clientLeft;
|
|
offsets.y = offsetRect.y + offsetParent.clientTop;
|
|
}
|
|
}
|
|
const htmlOffset = documentElement && !isOffsetParentAnElement && !isFixed ? getHTMLOffset(documentElement, scroll, true) : createCoords(0);
|
|
return {
|
|
width: rect.width * scale.x,
|
|
height: rect.height * scale.y,
|
|
x: rect.x * scale.x - scroll.scrollLeft * scale.x + offsets.x + htmlOffset.x,
|
|
y: rect.y * scale.y - scroll.scrollTop * scale.y + offsets.y + htmlOffset.y
|
|
};
|
|
}
|
|
|
|
function getClientRects(element) {
|
|
return Array.from(element.getClientRects());
|
|
}
|
|
|
|
// Gets the entire size of the scrollable document area, even extending outside
|
|
// of the `<html>` and `<body>` rect bounds if horizontally scrollable.
|
|
function getDocumentRect(element) {
|
|
const html = getDocumentElement(element);
|
|
const scroll = getNodeScroll(element);
|
|
const body = element.ownerDocument.body;
|
|
const width = max(html.scrollWidth, html.clientWidth, body.scrollWidth, body.clientWidth);
|
|
const height = max(html.scrollHeight, html.clientHeight, body.scrollHeight, body.clientHeight);
|
|
let x = -scroll.scrollLeft + getWindowScrollBarX(element);
|
|
const y = -scroll.scrollTop;
|
|
if (getComputedStyle$1(body).direction === 'rtl') {
|
|
x += max(html.clientWidth, body.clientWidth) - width;
|
|
}
|
|
return {
|
|
width,
|
|
height,
|
|
x,
|
|
y
|
|
};
|
|
}
|
|
|
|
function getViewportRect(element, strategy) {
|
|
const win = getWindow(element);
|
|
const html = getDocumentElement(element);
|
|
const visualViewport = win.visualViewport;
|
|
let width = html.clientWidth;
|
|
let height = html.clientHeight;
|
|
let x = 0;
|
|
let y = 0;
|
|
if (visualViewport) {
|
|
width = visualViewport.width;
|
|
height = visualViewport.height;
|
|
const visualViewportBased = isWebKit();
|
|
if (!visualViewportBased || visualViewportBased && strategy === 'fixed') {
|
|
x = visualViewport.offsetLeft;
|
|
y = visualViewport.offsetTop;
|
|
}
|
|
}
|
|
return {
|
|
width,
|
|
height,
|
|
x,
|
|
y
|
|
};
|
|
}
|
|
|
|
// Returns the inner client rect, subtracting scrollbars if present.
|
|
function getInnerBoundingClientRect(element, strategy) {
|
|
const clientRect = getBoundingClientRect(element, true, strategy === 'fixed');
|
|
const top = clientRect.top + element.clientTop;
|
|
const left = clientRect.left + element.clientLeft;
|
|
const scale = isHTMLElement(element) ? getScale(element) : createCoords(1);
|
|
const width = element.clientWidth * scale.x;
|
|
const height = element.clientHeight * scale.y;
|
|
const x = left * scale.x;
|
|
const y = top * scale.y;
|
|
return {
|
|
width,
|
|
height,
|
|
x,
|
|
y
|
|
};
|
|
}
|
|
function getClientRectFromClippingAncestor(element, clippingAncestor, strategy) {
|
|
let rect;
|
|
if (clippingAncestor === 'viewport') {
|
|
rect = getViewportRect(element, strategy);
|
|
} else if (clippingAncestor === 'document') {
|
|
rect = getDocumentRect(getDocumentElement(element));
|
|
} else if (isElement(clippingAncestor)) {
|
|
rect = getInnerBoundingClientRect(clippingAncestor, strategy);
|
|
} else {
|
|
const visualOffsets = getVisualOffsets(element);
|
|
rect = {
|
|
x: clippingAncestor.x - visualOffsets.x,
|
|
y: clippingAncestor.y - visualOffsets.y,
|
|
width: clippingAncestor.width,
|
|
height: clippingAncestor.height
|
|
};
|
|
}
|
|
return rectToClientRect(rect);
|
|
}
|
|
function hasFixedPositionAncestor(element, stopNode) {
|
|
const parentNode = getParentNode(element);
|
|
if (parentNode === stopNode || !isElement(parentNode) || isLastTraversableNode(parentNode)) {
|
|
return false;
|
|
}
|
|
return getComputedStyle$1(parentNode).position === 'fixed' || hasFixedPositionAncestor(parentNode, stopNode);
|
|
}
|
|
|
|
// A "clipping ancestor" is an `overflow` element with the characteristic of
|
|
// clipping (or hiding) child elements. This returns all clipping ancestors
|
|
// of the given element up the tree.
|
|
function getClippingElementAncestors(element, cache) {
|
|
const cachedResult = cache.get(element);
|
|
if (cachedResult) {
|
|
return cachedResult;
|
|
}
|
|
let result = getOverflowAncestors(element, [], false).filter(el => isElement(el) && getNodeName(el) !== 'body');
|
|
let currentContainingBlockComputedStyle = null;
|
|
const elementIsFixed = getComputedStyle$1(element).position === 'fixed';
|
|
let currentNode = elementIsFixed ? getParentNode(element) : element;
|
|
|
|
// https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block
|
|
while (isElement(currentNode) && !isLastTraversableNode(currentNode)) {
|
|
const computedStyle = getComputedStyle$1(currentNode);
|
|
const currentNodeIsContaining = isContainingBlock(currentNode);
|
|
if (!currentNodeIsContaining && computedStyle.position === 'fixed') {
|
|
currentContainingBlockComputedStyle = null;
|
|
}
|
|
const shouldDropCurrentNode = elementIsFixed ? !currentNodeIsContaining && !currentContainingBlockComputedStyle : !currentNodeIsContaining && computedStyle.position === 'static' && !!currentContainingBlockComputedStyle && ['absolute', 'fixed'].includes(currentContainingBlockComputedStyle.position) || isOverflowElement(currentNode) && !currentNodeIsContaining && hasFixedPositionAncestor(element, currentNode);
|
|
if (shouldDropCurrentNode) {
|
|
// Drop non-containing blocks.
|
|
result = result.filter(ancestor => ancestor !== currentNode);
|
|
} else {
|
|
// Record last containing block for next iteration.
|
|
currentContainingBlockComputedStyle = computedStyle;
|
|
}
|
|
currentNode = getParentNode(currentNode);
|
|
}
|
|
cache.set(element, result);
|
|
return result;
|
|
}
|
|
|
|
// Gets the maximum area that the element is visible in due to any number of
|
|
// clipping ancestors.
|
|
function getClippingRect(_ref) {
|
|
let {
|
|
element,
|
|
boundary,
|
|
rootBoundary,
|
|
strategy
|
|
} = _ref;
|
|
const elementClippingAncestors = boundary === 'clippingAncestors' ? isTopLayer(element) ? [] : getClippingElementAncestors(element, this._c) : [].concat(boundary);
|
|
const clippingAncestors = [...elementClippingAncestors, rootBoundary];
|
|
const firstClippingAncestor = clippingAncestors[0];
|
|
const clippingRect = clippingAncestors.reduce((accRect, clippingAncestor) => {
|
|
const rect = getClientRectFromClippingAncestor(element, clippingAncestor, strategy);
|
|
accRect.top = max(rect.top, accRect.top);
|
|
accRect.right = min(rect.right, accRect.right);
|
|
accRect.bottom = min(rect.bottom, accRect.bottom);
|
|
accRect.left = max(rect.left, accRect.left);
|
|
return accRect;
|
|
}, getClientRectFromClippingAncestor(element, firstClippingAncestor, strategy));
|
|
return {
|
|
width: clippingRect.right - clippingRect.left,
|
|
height: clippingRect.bottom - clippingRect.top,
|
|
x: clippingRect.left,
|
|
y: clippingRect.top
|
|
};
|
|
}
|
|
|
|
function getDimensions(element) {
|
|
const {
|
|
width,
|
|
height
|
|
} = getCssDimensions(element);
|
|
return {
|
|
width,
|
|
height
|
|
};
|
|
}
|
|
|
|
function getRectRelativeToOffsetParent(element, offsetParent, strategy) {
|
|
const isOffsetParentAnElement = isHTMLElement(offsetParent);
|
|
const documentElement = getDocumentElement(offsetParent);
|
|
const isFixed = strategy === 'fixed';
|
|
const rect = getBoundingClientRect(element, true, isFixed, offsetParent);
|
|
let scroll = {
|
|
scrollLeft: 0,
|
|
scrollTop: 0
|
|
};
|
|
const offsets = createCoords(0);
|
|
if (isOffsetParentAnElement || !isOffsetParentAnElement && !isFixed) {
|
|
if (getNodeName(offsetParent) !== 'body' || isOverflowElement(documentElement)) {
|
|
scroll = getNodeScroll(offsetParent);
|
|
}
|
|
if (isOffsetParentAnElement) {
|
|
const offsetRect = getBoundingClientRect(offsetParent, true, isFixed, offsetParent);
|
|
offsets.x = offsetRect.x + offsetParent.clientLeft;
|
|
offsets.y = offsetRect.y + offsetParent.clientTop;
|
|
} else if (documentElement) {
|
|
// If the <body> scrollbar appears on the left (e.g. RTL systems). Use
|
|
// Firefox with layout.scrollbar.side = 3 in about:config to test this.
|
|
offsets.x = getWindowScrollBarX(documentElement);
|
|
}
|
|
}
|
|
const htmlOffset = documentElement && !isOffsetParentAnElement && !isFixed ? getHTMLOffset(documentElement, scroll) : createCoords(0);
|
|
const x = rect.left + scroll.scrollLeft - offsets.x - htmlOffset.x;
|
|
const y = rect.top + scroll.scrollTop - offsets.y - htmlOffset.y;
|
|
return {
|
|
x,
|
|
y,
|
|
width: rect.width,
|
|
height: rect.height
|
|
};
|
|
}
|
|
|
|
function isStaticPositioned(element) {
|
|
return getComputedStyle$1(element).position === 'static';
|
|
}
|
|
|
|
function getTrueOffsetParent(element, polyfill) {
|
|
if (!isHTMLElement(element) || getComputedStyle$1(element).position === 'fixed') {
|
|
return null;
|
|
}
|
|
if (polyfill) {
|
|
return polyfill(element);
|
|
}
|
|
let rawOffsetParent = element.offsetParent;
|
|
|
|
// Firefox returns the <html> element as the offsetParent if it's non-static,
|
|
// while Chrome and Safari return the <body> element. The <body> element must
|
|
// be used to perform the correct calculations even if the <html> element is
|
|
// non-static.
|
|
if (getDocumentElement(element) === rawOffsetParent) {
|
|
rawOffsetParent = rawOffsetParent.ownerDocument.body;
|
|
}
|
|
return rawOffsetParent;
|
|
}
|
|
|
|
// Gets the closest ancestor positioned element. Handles some edge cases,
|
|
// such as table ancestors and cross browser bugs.
|
|
function getOffsetParent(element, polyfill) {
|
|
const win = getWindow(element);
|
|
if (isTopLayer(element)) {
|
|
return win;
|
|
}
|
|
if (!isHTMLElement(element)) {
|
|
let svgOffsetParent = getParentNode(element);
|
|
while (svgOffsetParent && !isLastTraversableNode(svgOffsetParent)) {
|
|
if (isElement(svgOffsetParent) && !isStaticPositioned(svgOffsetParent)) {
|
|
return svgOffsetParent;
|
|
}
|
|
svgOffsetParent = getParentNode(svgOffsetParent);
|
|
}
|
|
return win;
|
|
}
|
|
let offsetParent = getTrueOffsetParent(element, polyfill);
|
|
while (offsetParent && isTableElement(offsetParent) && isStaticPositioned(offsetParent)) {
|
|
offsetParent = getTrueOffsetParent(offsetParent, polyfill);
|
|
}
|
|
if (offsetParent && isLastTraversableNode(offsetParent) && isStaticPositioned(offsetParent) && !isContainingBlock(offsetParent)) {
|
|
return win;
|
|
}
|
|
return offsetParent || getContainingBlock(element) || win;
|
|
}
|
|
|
|
const getElementRects = async function (data) {
|
|
const getOffsetParentFn = this.getOffsetParent || getOffsetParent;
|
|
const getDimensionsFn = this.getDimensions;
|
|
const floatingDimensions = await getDimensionsFn(data.floating);
|
|
return {
|
|
reference: getRectRelativeToOffsetParent(data.reference, await getOffsetParentFn(data.floating), data.strategy),
|
|
floating: {
|
|
x: 0,
|
|
y: 0,
|
|
width: floatingDimensions.width,
|
|
height: floatingDimensions.height
|
|
}
|
|
};
|
|
};
|
|
|
|
function isRTL(element) {
|
|
return getComputedStyle$1(element).direction === 'rtl';
|
|
}
|
|
|
|
const platform = {
|
|
convertOffsetParentRelativeRectToViewportRelativeRect,
|
|
getDocumentElement,
|
|
getClippingRect,
|
|
getOffsetParent,
|
|
getElementRects,
|
|
getClientRects,
|
|
getDimensions,
|
|
getScale,
|
|
isElement,
|
|
isRTL
|
|
};
|
|
|
|
function rectsAreEqual(a, b) {
|
|
return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
|
|
}
|
|
|
|
// https://samthor.au/2021/observing-dom/
|
|
function observeMove(element, onMove) {
|
|
let io = null;
|
|
let timeoutId;
|
|
const root = getDocumentElement(element);
|
|
function cleanup() {
|
|
var _io;
|
|
clearTimeout(timeoutId);
|
|
(_io = io) == null || _io.disconnect();
|
|
io = null;
|
|
}
|
|
function refresh(skip, threshold) {
|
|
if (skip === void 0) {
|
|
skip = false;
|
|
}
|
|
if (threshold === void 0) {
|
|
threshold = 1;
|
|
}
|
|
cleanup();
|
|
const elementRectForRootMargin = element.getBoundingClientRect();
|
|
const {
|
|
left,
|
|
top,
|
|
width,
|
|
height
|
|
} = elementRectForRootMargin;
|
|
if (!skip) {
|
|
onMove();
|
|
}
|
|
if (!width || !height) {
|
|
return;
|
|
}
|
|
const insetTop = floor(top);
|
|
const insetRight = floor(root.clientWidth - (left + width));
|
|
const insetBottom = floor(root.clientHeight - (top + height));
|
|
const insetLeft = floor(left);
|
|
const rootMargin = -insetTop + "px " + -insetRight + "px " + -insetBottom + "px " + -insetLeft + "px";
|
|
const options = {
|
|
rootMargin,
|
|
threshold: max(0, min(1, threshold)) || 1
|
|
};
|
|
let isFirstUpdate = true;
|
|
function handleObserve(entries) {
|
|
const ratio = entries[0].intersectionRatio;
|
|
if (ratio !== threshold) {
|
|
if (!isFirstUpdate) {
|
|
return refresh();
|
|
}
|
|
if (!ratio) {
|
|
// If the reference is clipped, the ratio is 0. Throttle the refresh
|
|
// to prevent an infinite loop of updates.
|
|
timeoutId = setTimeout(() => {
|
|
refresh(false, 1e-7);
|
|
}, 1000);
|
|
} else {
|
|
refresh(false, ratio);
|
|
}
|
|
}
|
|
if (ratio === 1 && !rectsAreEqual(elementRectForRootMargin, element.getBoundingClientRect())) {
|
|
// It's possible that even though the ratio is reported as 1, the
|
|
// element is not actually fully within the IntersectionObserver's root
|
|
// area anymore. This can happen under performance constraints. This may
|
|
// be a bug in the browser's IntersectionObserver implementation. To
|
|
// work around this, we compare the element's bounding rect now with
|
|
// what it was at the time we created the IntersectionObserver. If they
|
|
// are not equal then the element moved, so we refresh.
|
|
refresh();
|
|
}
|
|
isFirstUpdate = false;
|
|
}
|
|
|
|
// Older browsers don't support a `document` as the root and will throw an
|
|
// error.
|
|
try {
|
|
io = new IntersectionObserver(handleObserve, {
|
|
...options,
|
|
// Handle <iframe>s
|
|
root: root.ownerDocument
|
|
});
|
|
} catch (e) {
|
|
io = new IntersectionObserver(handleObserve, options);
|
|
}
|
|
io.observe(element);
|
|
}
|
|
refresh(true);
|
|
return cleanup;
|
|
}
|
|
|
|
/**
|
|
* Automatically updates the position of the floating element when necessary.
|
|
* Should only be called when the floating element is mounted on the DOM or
|
|
* visible on the screen.
|
|
* @returns cleanup function that should be invoked when the floating element is
|
|
* removed from the DOM or hidden from the screen.
|
|
* @see https://floating-ui.com/docs/autoUpdate
|
|
*/
|
|
function autoUpdate(reference, floating, update, options) {
|
|
if (options === void 0) {
|
|
options = {};
|
|
}
|
|
const {
|
|
ancestorScroll = true,
|
|
ancestorResize = true,
|
|
elementResize = typeof ResizeObserver === 'function',
|
|
layoutShift = typeof IntersectionObserver === 'function',
|
|
animationFrame = false
|
|
} = options;
|
|
const referenceEl = unwrapElement(reference);
|
|
const ancestors = ancestorScroll || ancestorResize ? [...(referenceEl ? getOverflowAncestors(referenceEl) : []), ...getOverflowAncestors(floating)] : [];
|
|
ancestors.forEach(ancestor => {
|
|
ancestorScroll && ancestor.addEventListener('scroll', update, {
|
|
passive: true
|
|
});
|
|
ancestorResize && ancestor.addEventListener('resize', update);
|
|
});
|
|
const cleanupIo = referenceEl && layoutShift ? observeMove(referenceEl, update) : null;
|
|
let reobserveFrame = -1;
|
|
let resizeObserver = null;
|
|
if (elementResize) {
|
|
resizeObserver = new ResizeObserver(_ref => {
|
|
let [firstEntry] = _ref;
|
|
if (firstEntry && firstEntry.target === referenceEl && resizeObserver) {
|
|
// Prevent update loops when using the `size` middleware.
|
|
// https://github.com/floating-ui/floating-ui/issues/1740
|
|
resizeObserver.unobserve(floating);
|
|
cancelAnimationFrame(reobserveFrame);
|
|
reobserveFrame = requestAnimationFrame(() => {
|
|
var _resizeObserver;
|
|
(_resizeObserver = resizeObserver) == null || _resizeObserver.observe(floating);
|
|
});
|
|
}
|
|
update();
|
|
});
|
|
if (referenceEl && !animationFrame) {
|
|
resizeObserver.observe(referenceEl);
|
|
}
|
|
resizeObserver.observe(floating);
|
|
}
|
|
let frameId;
|
|
let prevRefRect = animationFrame ? getBoundingClientRect(reference) : null;
|
|
if (animationFrame) {
|
|
frameLoop();
|
|
}
|
|
function frameLoop() {
|
|
const nextRefRect = getBoundingClientRect(reference);
|
|
if (prevRefRect && !rectsAreEqual(prevRefRect, nextRefRect)) {
|
|
update();
|
|
}
|
|
prevRefRect = nextRefRect;
|
|
frameId = requestAnimationFrame(frameLoop);
|
|
}
|
|
update();
|
|
return () => {
|
|
var _resizeObserver2;
|
|
ancestors.forEach(ancestor => {
|
|
ancestorScroll && ancestor.removeEventListener('scroll', update);
|
|
ancestorResize && ancestor.removeEventListener('resize', update);
|
|
});
|
|
cleanupIo == null || cleanupIo();
|
|
(_resizeObserver2 = resizeObserver) == null || _resizeObserver2.disconnect();
|
|
resizeObserver = null;
|
|
if (animationFrame) {
|
|
cancelAnimationFrame(frameId);
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Modifies the placement by translating the floating element along the
|
|
* specified axes.
|
|
* A number (shorthand for `mainAxis` or distance), or an axes configuration
|
|
* object may be passed.
|
|
* @see https://floating-ui.com/docs/offset
|
|
*/
|
|
const offset$1 = offset$2;
|
|
|
|
/**
|
|
* Optimizes the visibility of the floating element by shifting it in order to
|
|
* keep it in view when it will overflow the clipping boundary.
|
|
* @see https://floating-ui.com/docs/shift
|
|
*/
|
|
const shift$1 = shift$2;
|
|
|
|
/**
|
|
* Optimizes the visibility of the floating element by flipping the `placement`
|
|
* in order to keep it in view when the preferred placement(s) will overflow the
|
|
* clipping boundary. Alternative to `autoPlacement`.
|
|
* @see https://floating-ui.com/docs/flip
|
|
*/
|
|
const flip$1 = flip$2;
|
|
|
|
/**
|
|
* Computes the `x` and `y` coordinates that will place the floating element
|
|
* next to a given reference element.
|
|
*/
|
|
const computePosition = (reference, floating, options) => {
|
|
// This caches the expensive `getClippingElementAncestors` function so that
|
|
// multiple lifecycle resets re-use the same result. It only lives for a
|
|
// single call. If other functions become expensive, we can add them as well.
|
|
const cache = new Map();
|
|
const mergedOptions = {
|
|
platform,
|
|
...options
|
|
};
|
|
const platformWithCache = {
|
|
...mergedOptions.platform,
|
|
_c: cache
|
|
};
|
|
return computePosition$1(reference, floating, {
|
|
...mergedOptions,
|
|
platform: platformWithCache
|
|
});
|
|
};
|
|
|
|
var index = typeof document !== 'undefined' ? React.useLayoutEffect : React.useEffect;
|
|
|
|
// Fork of `fast-deep-equal` that only does the comparisons we need and compares
|
|
// functions
|
|
function deepEqual(a, b) {
|
|
if (a === b) {
|
|
return true;
|
|
}
|
|
if (typeof a !== typeof b) {
|
|
return false;
|
|
}
|
|
if (typeof a === 'function' && a.toString() === b.toString()) {
|
|
return true;
|
|
}
|
|
let length;
|
|
let i;
|
|
let keys;
|
|
if (a && b && typeof a === 'object') {
|
|
if (Array.isArray(a)) {
|
|
length = a.length;
|
|
if (length !== b.length) return false;
|
|
for (i = length; i-- !== 0;) {
|
|
if (!deepEqual(a[i], b[i])) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
keys = Object.keys(a);
|
|
length = keys.length;
|
|
if (length !== Object.keys(b).length) {
|
|
return false;
|
|
}
|
|
for (i = length; i-- !== 0;) {
|
|
if (!{}.hasOwnProperty.call(b, keys[i])) {
|
|
return false;
|
|
}
|
|
}
|
|
for (i = length; i-- !== 0;) {
|
|
const key = keys[i];
|
|
if (key === '_owner' && a.$$typeof) {
|
|
continue;
|
|
}
|
|
if (!deepEqual(a[key], b[key])) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
return a !== a && b !== b;
|
|
}
|
|
|
|
function getDPR(element) {
|
|
if (typeof window === 'undefined') {
|
|
return 1;
|
|
}
|
|
const win = element.ownerDocument.defaultView || window;
|
|
return win.devicePixelRatio || 1;
|
|
}
|
|
|
|
function roundByDPR(element, value) {
|
|
const dpr = getDPR(element);
|
|
return Math.round(value * dpr) / dpr;
|
|
}
|
|
|
|
function useLatestRef(value) {
|
|
const ref = React__namespace.useRef(value);
|
|
index(() => {
|
|
ref.current = value;
|
|
});
|
|
return ref;
|
|
}
|
|
|
|
/**
|
|
* Provides data to position a floating element.
|
|
* @see https://floating-ui.com/docs/useFloating
|
|
*/
|
|
function useFloating$1(options) {
|
|
if (options === void 0) {
|
|
options = {};
|
|
}
|
|
const {
|
|
placement = 'bottom',
|
|
strategy = 'absolute',
|
|
middleware = [],
|
|
platform,
|
|
elements: {
|
|
reference: externalReference,
|
|
floating: externalFloating
|
|
} = {},
|
|
transform = true,
|
|
whileElementsMounted,
|
|
open
|
|
} = options;
|
|
const [data, setData] = React__namespace.useState({
|
|
x: 0,
|
|
y: 0,
|
|
strategy,
|
|
placement,
|
|
middlewareData: {},
|
|
isPositioned: false
|
|
});
|
|
const [latestMiddleware, setLatestMiddleware] = React__namespace.useState(middleware);
|
|
if (!deepEqual(latestMiddleware, middleware)) {
|
|
setLatestMiddleware(middleware);
|
|
}
|
|
const [_reference, _setReference] = React__namespace.useState(null);
|
|
const [_floating, _setFloating] = React__namespace.useState(null);
|
|
const setReference = React__namespace.useCallback(node => {
|
|
if (node !== referenceRef.current) {
|
|
referenceRef.current = node;
|
|
_setReference(node);
|
|
}
|
|
}, []);
|
|
const setFloating = React__namespace.useCallback(node => {
|
|
if (node !== floatingRef.current) {
|
|
floatingRef.current = node;
|
|
_setFloating(node);
|
|
}
|
|
}, []);
|
|
const referenceEl = externalReference || _reference;
|
|
const floatingEl = externalFloating || _floating;
|
|
const referenceRef = React__namespace.useRef(null);
|
|
const floatingRef = React__namespace.useRef(null);
|
|
const dataRef = React__namespace.useRef(data);
|
|
const hasWhileElementsMounted = whileElementsMounted != null;
|
|
const whileElementsMountedRef = useLatestRef(whileElementsMounted);
|
|
const platformRef = useLatestRef(platform);
|
|
const openRef = useLatestRef(open);
|
|
const update = React__namespace.useCallback(() => {
|
|
if (!referenceRef.current || !floatingRef.current) {
|
|
return;
|
|
}
|
|
const config = {
|
|
placement,
|
|
strategy,
|
|
middleware: latestMiddleware
|
|
};
|
|
if (platformRef.current) {
|
|
config.platform = platformRef.current;
|
|
}
|
|
computePosition(referenceRef.current, floatingRef.current, config).then(data => {
|
|
const fullData = {
|
|
...data,
|
|
// The floating element's position may be recomputed while it's closed
|
|
// but still mounted (such as when transitioning out). To ensure
|
|
// `isPositioned` will be `false` initially on the next open, avoid
|
|
// setting it to `true` when `open === false` (must be specified).
|
|
isPositioned: openRef.current !== false
|
|
};
|
|
if (isMountedRef.current && !deepEqual(dataRef.current, fullData)) {
|
|
dataRef.current = fullData;
|
|
ReactDOM__namespace.flushSync(() => {
|
|
setData(fullData);
|
|
});
|
|
}
|
|
});
|
|
}, [latestMiddleware, placement, strategy, platformRef, openRef]);
|
|
index(() => {
|
|
if (open === false && dataRef.current.isPositioned) {
|
|
dataRef.current.isPositioned = false;
|
|
setData(data => ({
|
|
...data,
|
|
isPositioned: false
|
|
}));
|
|
}
|
|
}, [open]);
|
|
const isMountedRef = React__namespace.useRef(false);
|
|
index(() => {
|
|
isMountedRef.current = true;
|
|
return () => {
|
|
isMountedRef.current = false;
|
|
};
|
|
}, []);
|
|
index(() => {
|
|
if (referenceEl) referenceRef.current = referenceEl;
|
|
if (floatingEl) floatingRef.current = floatingEl;
|
|
if (referenceEl && floatingEl) {
|
|
if (whileElementsMountedRef.current) {
|
|
return whileElementsMountedRef.current(referenceEl, floatingEl, update);
|
|
}
|
|
update();
|
|
}
|
|
}, [referenceEl, floatingEl, update, whileElementsMountedRef, hasWhileElementsMounted]);
|
|
const refs = React__namespace.useMemo(() => ({
|
|
reference: referenceRef,
|
|
floating: floatingRef,
|
|
setReference,
|
|
setFloating
|
|
}), [setReference, setFloating]);
|
|
const elements = React__namespace.useMemo(() => ({
|
|
reference: referenceEl,
|
|
floating: floatingEl
|
|
}), [referenceEl, floatingEl]);
|
|
const floatingStyles = React__namespace.useMemo(() => {
|
|
const initialStyles = {
|
|
position: strategy,
|
|
left: 0,
|
|
top: 0
|
|
};
|
|
if (!elements.floating) {
|
|
return initialStyles;
|
|
}
|
|
const x = roundByDPR(elements.floating, data.x);
|
|
const y = roundByDPR(elements.floating, data.y);
|
|
if (transform) {
|
|
return {
|
|
...initialStyles,
|
|
transform: "translate(" + x + "px, " + y + "px)",
|
|
...(getDPR(elements.floating) >= 1.5 && {
|
|
willChange: 'transform'
|
|
})
|
|
};
|
|
}
|
|
return {
|
|
position: strategy,
|
|
left: x,
|
|
top: y
|
|
};
|
|
}, [strategy, transform, elements.floating, data.x, data.y]);
|
|
return React__namespace.useMemo(() => ({
|
|
...data,
|
|
update,
|
|
refs,
|
|
elements,
|
|
floatingStyles
|
|
}), [data, update, refs, elements, floatingStyles]);
|
|
}
|
|
|
|
/**
|
|
* Modifies the placement by translating the floating element along the
|
|
* specified axes.
|
|
* A number (shorthand for `mainAxis` or distance), or an axes configuration
|
|
* object may be passed.
|
|
* @see https://floating-ui.com/docs/offset
|
|
*/
|
|
const offset = (options, deps) => ({
|
|
...offset$1(options),
|
|
options: [options, deps]
|
|
});
|
|
|
|
/**
|
|
* Optimizes the visibility of the floating element by shifting it in order to
|
|
* keep it in view when it will overflow the clipping boundary.
|
|
* @see https://floating-ui.com/docs/shift
|
|
*/
|
|
const shift = (options, deps) => ({
|
|
...shift$1(options),
|
|
options: [options, deps]
|
|
});
|
|
|
|
/**
|
|
* Optimizes the visibility of the floating element by flipping the `placement`
|
|
* in order to keep it in view when the preferred placement(s) will overflow the
|
|
* clipping boundary. Alternative to `autoPlacement`.
|
|
* @see https://floating-ui.com/docs/flip
|
|
*/
|
|
const flip = (options, deps) => ({
|
|
...flip$1(options),
|
|
options: [options, deps]
|
|
});
|
|
|
|
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';
|
|
|
|
// https://github.com/mui/material-ui/issues/41190#issuecomment-2040873379
|
|
const SafeReact = {
|
|
...React__namespace
|
|
};
|
|
|
|
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__namespace.useState(() => serverHandoffComplete ? genId() : undefined);
|
|
index$1(() => {
|
|
if (id == null) {
|
|
setId(genId());
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, []);
|
|
React__namespace.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);
|
|
}
|
|
}
|
|
|
|
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__namespace.createContext(null);
|
|
const FloatingTreeContext = /*#__PURE__*/React__namespace.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__namespace.useContext(FloatingNodeContext)) == null ? void 0 : _React$useContext.id) || null;
|
|
};
|
|
|
|
/**
|
|
* Returns the nearest floating tree context, if available.
|
|
*/
|
|
const useFloatingTree = () => React__namespace.useContext(FloatingTreeContext);
|
|
|
|
function createAttribute(name) {
|
|
return "data-floating-ui-" + name;
|
|
}
|
|
|
|
function clearTimeoutIfSet(timeoutRef) {
|
|
if (timeoutRef.current !== -1) {
|
|
clearTimeout(timeoutRef.current);
|
|
timeoutRef.current = -1;
|
|
}
|
|
}
|
|
|
|
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 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
|
|
let counterMap = /*#__PURE__*/new WeakMap();
|
|
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 currentCounterValue = counterMap.get(node) || 0;
|
|
const counterValue = controlAttribute ? currentCounterValue + 1 : currentCounterValue;
|
|
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 currentCounterValue = counterMap.get(element) || 0;
|
|
const counterValue = controlAttribute ? currentCounterValue - 1 : currentCounterValue;
|
|
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) {
|
|
counterMap = new WeakMap();
|
|
counterMap = new WeakMap();
|
|
uncontrolledElementsSet = new WeakSet();
|
|
markerMap = {};
|
|
}
|
|
};
|
|
}
|
|
function markOthers(avoidElements, ariaHidden, inert) {
|
|
if (ariaHidden === void 0) {
|
|
ariaHidden = false;
|
|
}
|
|
if (inert === void 0) {
|
|
inert = false;
|
|
}
|
|
const body = getDocument(avoidElements[0]).body;
|
|
return applyAttributeToOthers(avoidElements.concat(Array.from(body.querySelectorAll('[aria-live]'))), body, ariaHidden, inert);
|
|
}
|
|
|
|
const HIDDEN_STYLES = {
|
|
border: 0,
|
|
clip: 'rect(0 0 0 0)',
|
|
height: '1px',
|
|
margin: '-1px',
|
|
overflow: 'hidden',
|
|
padding: 0,
|
|
position: 'fixed',
|
|
whiteSpace: 'nowrap',
|
|
width: '1px',
|
|
top: 0,
|
|
left: 0
|
|
};
|
|
const FocusGuard = /*#__PURE__*/React__namespace.forwardRef(function FocusGuard(props, ref) {
|
|
const [role, setRole] = React__namespace.useState();
|
|
index$1(() => {
|
|
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__*/jsxRuntime.jsx("span", {
|
|
...props,
|
|
...restProps
|
|
});
|
|
});
|
|
|
|
const PortalContext = /*#__PURE__*/React__namespace.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__namespace.useState(null);
|
|
const portalNodeRef = React__namespace.useRef(null);
|
|
index$1(() => {
|
|
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]);
|
|
index$1(() => {
|
|
// 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]);
|
|
index$1(() => {
|
|
// 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 && !isElement(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__namespace.useState(null);
|
|
const beforeOutsideRef = React__namespace.useRef(null);
|
|
const afterOutsideRef = React__namespace.useRef(null);
|
|
const beforeInsideRef = React__namespace.useRef(null);
|
|
const afterInsideRef = React__namespace.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__namespace.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__namespace.useEffect(() => {
|
|
if (!portalNode) return;
|
|
if (open) return;
|
|
enableFocusInside(portalNode);
|
|
}, [open, portalNode]);
|
|
return /*#__PURE__*/jsxRuntime.jsxs(PortalContext.Provider, {
|
|
value: React__namespace.useMemo(() => ({
|
|
preserveTabOrder,
|
|
beforeOutsideRef,
|
|
afterOutsideRef,
|
|
beforeInsideRef,
|
|
afterInsideRef,
|
|
portalNode,
|
|
setFocusManagerState
|
|
}), [preserveTabOrder, portalNode]),
|
|
children: [shouldRenderGuards && portalNode && /*#__PURE__*/jsxRuntime.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__*/jsxRuntime.jsx("span", {
|
|
"aria-owns": portalNode.id,
|
|
style: HIDDEN_STYLES
|
|
}), portalNode && /*#__PURE__*/ReactDOM__namespace.createPortal(children, portalNode), shouldRenderGuards && portalNode && /*#__PURE__*/jsxRuntime.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__namespace.useContext(PortalContext);
|
|
|
|
function useLiteMergeRefs(refs) {
|
|
return React__namespace.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 addPreviouslyFocusedElement(element) {
|
|
previouslyFocusedElements = previouslyFocusedElements.filter(el => el.isConnected);
|
|
if (element && getNodeName(element) !== 'body') {
|
|
previouslyFocusedElements.push(element);
|
|
if (previouslyFocusedElements.length > LIST_LIMIT) {
|
|
previouslyFocusedElements = previouslyFocusedElements.slice(-20);
|
|
}
|
|
}
|
|
}
|
|
function getPreviouslyFocusedElement() {
|
|
return previouslyFocusedElements.slice().reverse().find(el => el.isConnected);
|
|
}
|
|
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__namespace.forwardRef(function VisuallyHiddenDismiss(props, ref) {
|
|
return /*#__PURE__*/jsxRuntime.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$1(order);
|
|
const initialFocusRef = useLatestRef$1(initialFocus);
|
|
const returnFocusRef = useLatestRef$1(returnFocus);
|
|
const tree = useFloatingTree();
|
|
const portalContext = usePortalContext();
|
|
const startDismissButtonRef = React__namespace.useRef(null);
|
|
const endDismissButtonRef = React__namespace.useRef(null);
|
|
const preventReturnFocusRef = React__namespace.useRef(false);
|
|
const isPointerDownRef = React__namespace.useRef(false);
|
|
const tabbableIndexRef = React__namespace.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__namespace.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(floatingFocusElement, activeElement(getDocument$1(floatingFocusElement))) && getTabbableContent().length === 0 && !isUntrappedTypeableCombobox) {
|
|
stopEvent(event);
|
|
}
|
|
const els = getTabbableElements();
|
|
const target = getTarget(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__namespace.useEffect(() => {
|
|
if (disabled) return;
|
|
if (!floating) return;
|
|
function handleFocusIn(event) {
|
|
const target = getTarget(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__namespace.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;
|
|
queueMicrotask(() => {
|
|
const nodeId = getNodeId();
|
|
const movedToUnrelatedNode = !(contains(domReference, relatedTarget) || contains(floating, relatedTarget) || contains(relatedTarget, floating) || contains(portalContext == null ? void 0 : portalContext.portalNode, relatedTarget) || relatedTarget != null && relatedTarget.hasAttribute(createAttribute('focus-guard')) || tree && (getNodeChildren(tree.nodesRef.current, nodeId).find(node => {
|
|
var _node$context, _node$context2;
|
|
return contains((_node$context = node.context) == null ? void 0 : _node$context.elements.floating, relatedTarget) || contains((_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 && movedToUnrelatedNode && 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();
|
|
}
|
|
}
|
|
|
|
// 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');
|
|
}
|
|
});
|
|
}
|
|
if (floating && isHTMLElement(domReference)) {
|
|
domReference.addEventListener('focusout', handleFocusOutside);
|
|
domReference.addEventListener('pointerdown', handlePointerDown);
|
|
floating.addEventListener('focusout', handleFocusOutside);
|
|
return () => {
|
|
domReference.removeEventListener('focusout', handleFocusOutside);
|
|
domReference.removeEventListener('pointerdown', handlePointerDown);
|
|
floating.removeEventListener('focusout', handleFocusOutside);
|
|
};
|
|
}
|
|
}, [disabled, domReference, floating, floatingFocusElement, modal, tree, portalContext, onOpenChange, closeOnFocusOut, restoreFocus, getTabbableContent, isUntrappedTypeableCombobox, getNodeId, orderRef]);
|
|
const beforeGuardRef = React__namespace.useRef(null);
|
|
const afterGuardRef = React__namespace.useRef(null);
|
|
const mergedBeforeGuardRef = useLiteMergeRefs([beforeGuardRef, portalContext == null ? void 0 : portalContext.beforeInsideRef]);
|
|
const mergedAfterGuardRef = useLiteMergeRefs([afterGuardRef, portalContext == null ? void 0 : portalContext.afterInsideRef]);
|
|
React__namespace.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 ancestorFloatingNodes = tree && !modal ? ancestors.map(node => {
|
|
var _node$context6;
|
|
return (_node$context6 = node.context) == null ? void 0 : _node$context6.elements.floating;
|
|
}) : [];
|
|
const rootAncestorComboboxDomReference = (_ancestors$find = ancestors.find(node => {
|
|
var _node$context7;
|
|
return isTypeableCombobox(((_node$context7 = node.context) == null ? void 0 : _node$context7.elements.domReference) || null);
|
|
})) == null || (_ancestors$find = _ancestors$find.context) == null ? void 0 : _ancestors$find.elements.domReference;
|
|
const insideElements = [floating, rootAncestorComboboxDomReference, ...portalNodes, ...ancestorFloatingNodes, ...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]);
|
|
index$1(() => {
|
|
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(floatingFocusElement, previouslyFocusedElement);
|
|
if (!ignoreInitialFocus && !focusAlreadyInsideFloatingEl && open) {
|
|
enqueueFocus(elToFocus, {
|
|
preventScroll: elToFocus === floatingFocusElement
|
|
});
|
|
}
|
|
});
|
|
}, [disabled, open, floatingFocusElement, ignoreInitialFocus, getTabbableElements, initialFocusRef]);
|
|
index$1(() => {
|
|
if (disabled || !floatingFocusElement) return;
|
|
let preventReturnFocusScroll = false;
|
|
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;
|
|
preventReturnFocusScroll = true;
|
|
} 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;
|
|
preventReturnFocusScroll = true;
|
|
} 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(floating, activeEl) || tree && getNodeChildren(tree.nodesRef.current, getNodeId()).some(node => {
|
|
var _node$context8;
|
|
return contains((_node$context8 = node.context) == null ? void 0 : _node$context8.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: preventReturnFocusScroll
|
|
});
|
|
}
|
|
fallbackEl.remove();
|
|
});
|
|
};
|
|
}, [disabled, floating, floatingFocusElement, returnFocusRef, dataRef, events, tree, isInsidePortal, domReference, getNodeId]);
|
|
React__namespace.useEffect(() => {
|
|
// The `returnFocus` cleanup behavior is inside a microtask; ensure we
|
|
// wait for it to complete before resetting the flag.
|
|
queueMicrotask(() => {
|
|
preventReturnFocusRef.current = false;
|
|
});
|
|
}, [disabled]);
|
|
|
|
// Synchronize the `context` & `modal` value to the FloatingPortal context.
|
|
// It will decide whether or not it needs to render its own guards.
|
|
index$1(() => {
|
|
if (disabled) return;
|
|
if (!portalContext) return;
|
|
portalContext.setFocusManagerState({
|
|
modal,
|
|
closeOnFocusOut,
|
|
open,
|
|
onOpenChange,
|
|
domReference
|
|
});
|
|
return () => {
|
|
portalContext.setFocusManagerState(null);
|
|
};
|
|
}, [disabled, portalContext, modal, open, onOpenChange, closeOnFocusOut, domReference]);
|
|
index$1(() => {
|
|
if (disabled) return;
|
|
if (!floatingFocusElement) return;
|
|
handleTabIndex(floatingFocusElement, orderRef);
|
|
}, [disabled, floatingFocusElement, orderRef]);
|
|
function renderDismissButton(location) {
|
|
if (disabled || !visuallyHiddenDismiss || !modal) {
|
|
return null;
|
|
}
|
|
return /*#__PURE__*/jsxRuntime.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__*/jsxRuntime.jsxs(jsxRuntime.Fragment, {
|
|
children: [shouldRenderGuards && /*#__PURE__*/jsxRuntime.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__*/jsxRuntime.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;
|
|
function enableScrollLock() {
|
|
const isIOS = /iP(hone|ad|od)|iOS/.test(getPlatform());
|
|
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';
|
|
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]: ''
|
|
});
|
|
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__namespace.forwardRef(function FloatingOverlay(props, ref) {
|
|
const {
|
|
lockScroll = false,
|
|
...rest
|
|
} = props;
|
|
index$1(() => {
|
|
if (!lockScroll) return;
|
|
lockCount++;
|
|
if (lockCount === 1) {
|
|
cleanup = enableScrollLock();
|
|
}
|
|
return () => {
|
|
lockCount--;
|
|
if (lockCount === 0) {
|
|
cleanup();
|
|
}
|
|
};
|
|
}, [lockScroll]);
|
|
return /*#__PURE__*/jsxRuntime.jsx("div", {
|
|
ref: ref,
|
|
...rest,
|
|
style: {
|
|
position: 'fixed',
|
|
overflow: 'auto',
|
|
top: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
left: 0,
|
|
...rest.style
|
|
}
|
|
});
|
|
});
|
|
|
|
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 insideReactTreeRef = React__namespace.useRef(false);
|
|
const endedOrStartedInsideRef = React__namespace.useRef(false);
|
|
const {
|
|
escapeKey: escapeKeyBubbles,
|
|
outsidePress: outsidePressBubbles
|
|
} = normalizeProp(bubbles);
|
|
const {
|
|
escapeKey: escapeKeyCapture,
|
|
outsidePress: outsidePressCapture
|
|
} = normalizeProp(capture);
|
|
const isComposingRef = React__namespace.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(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(event)) == null || _getTarget.removeEventListener('keydown', callback);
|
|
};
|
|
(_getTarget2 = getTarget(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 = insideReactTreeRef.current;
|
|
insideReactTreeRef.current = 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(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(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(targetRootAncestor, marker))) {
|
|
return;
|
|
}
|
|
|
|
// Check if the click occurred on the scrollbar
|
|
if (isHTMLElement(target) && floating) {
|
|
const lastTraversableNode = isLastTraversableNode(target);
|
|
const style = getComputedStyle$1(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(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(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(event)) == null || _getTarget3.removeEventListener(outsidePressEvent, callback);
|
|
};
|
|
(_getTarget4 = getTarget(event)) == null || _getTarget4.addEventListener(outsidePressEvent, callback);
|
|
});
|
|
React__namespace.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__namespace.useEffect(() => {
|
|
insideReactTreeRef.current = false;
|
|
}, [outsidePress, outsidePressEvent]);
|
|
const reference = React__namespace.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__namespace.useMemo(() => ({
|
|
onKeyDown: closeOnEscapeKeyDown,
|
|
onMouseDown() {
|
|
endedOrStartedInsideRef.current = true;
|
|
},
|
|
onMouseUp() {
|
|
endedOrStartedInsideRef.current = true;
|
|
},
|
|
[captureHandlerKeys[outsidePressEvent]]: () => {
|
|
insideReactTreeRef.current = true;
|
|
}
|
|
}), [closeOnEscapeKeyDown, outsidePressEvent]);
|
|
return React__namespace.useMemo(() => enabled ? {
|
|
reference,
|
|
floating
|
|
} : {}, [enabled, reference, floating]);
|
|
}
|
|
|
|
function useFloatingRootContext(options) {
|
|
const {
|
|
open = false,
|
|
onOpenChange: onOpenChangeProp,
|
|
elements: elementsProp
|
|
} = options;
|
|
const floatingId = useId();
|
|
const dataRef = React__namespace.useRef({});
|
|
const [events] = React__namespace.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__namespace.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__namespace.useMemo(() => ({
|
|
setPositionReference
|
|
}), []);
|
|
const elements = React__namespace.useMemo(() => ({
|
|
reference: positionReference || elementsProp.reference || null,
|
|
floating: elementsProp.floating || null,
|
|
domReference: elementsProp.reference
|
|
}), [positionReference, elementsProp.reference, elementsProp.floating]);
|
|
return React__namespace.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__namespace.useState(null);
|
|
const [positionReference, _setPositionReference] = React__namespace.useState(null);
|
|
const optionDomReference = computedElements == null ? void 0 : computedElements.domReference;
|
|
const domReference = optionDomReference || _domReference;
|
|
const domReferenceRef = React__namespace.useRef(null);
|
|
const tree = useFloatingTree();
|
|
index$1(() => {
|
|
if (domReference) {
|
|
domReferenceRef.current = domReference;
|
|
}
|
|
}, [domReference]);
|
|
const position = useFloating$1({
|
|
...options,
|
|
elements: {
|
|
...computedElements,
|
|
...(positionReference && {
|
|
reference: positionReference
|
|
})
|
|
}
|
|
});
|
|
const setPositionReference = React__namespace.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__namespace.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__namespace.useMemo(() => ({
|
|
...position.refs,
|
|
setReference,
|
|
setPositionReference,
|
|
domReference: domReferenceRef
|
|
}), [position.refs, setReference, setPositionReference]);
|
|
const elements = React__namespace.useMemo(() => ({
|
|
...position.elements,
|
|
domReference: domReference
|
|
}), [position.elements, domReference]);
|
|
const context = React__namespace.useMemo(() => ({
|
|
...position,
|
|
...rootContext,
|
|
refs,
|
|
elements,
|
|
nodeId
|
|
}), [position, refs, elements, nodeId, rootContext]);
|
|
index$1(() => {
|
|
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__namespace.useMemo(() => ({
|
|
...position,
|
|
context,
|
|
refs,
|
|
elements
|
|
}), [position, refs, elements, context]);
|
|
}
|
|
|
|
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__namespace.useCallback(userProps => mergeProps(userProps, propsList, 'reference'),
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
referenceDeps);
|
|
const getFloatingProps = React__namespace.useCallback(userProps => mergeProps(userProps, propsList, 'floating'),
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
floatingDeps);
|
|
const getItemProps = React__namespace.useCallback(userProps => mergeProps(userProps, propsList, 'item'),
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
itemDeps);
|
|
return React__namespace.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$1(floatingFocusElement);
|
|
const parentId = useFloatingParentNodeId();
|
|
const tree = useFloatingTree();
|
|
index$1(() => {
|
|
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__namespace.useRef(focusItemOnOpen);
|
|
const indexRef = React__namespace.useRef(selectedIndex != null ? selectedIndex : -1);
|
|
const keyRef = React__namespace.useRef(null);
|
|
const isPointerModalityRef = React__namespace.useRef(true);
|
|
const previousOnNavigateRef = React__namespace.useRef(onNavigate);
|
|
const previousMountedRef = React__namespace.useRef(!!elements.floating);
|
|
const previousOpenRef = React__namespace.useRef(open);
|
|
const forceSyncFocusRef = React__namespace.useRef(false);
|
|
const forceScrollIntoViewRef = React__namespace.useRef(false);
|
|
const disabledIndicesRef = useLatestRef$1(disabledIndices);
|
|
const latestOpenRef = useLatestRef$1(open);
|
|
const scrollItemIntoViewRef = useLatestRef$1(scrollItemIntoView);
|
|
const selectedIndexRef = useLatestRef$1(selectedIndex);
|
|
const [activeId, setActiveId] = React__namespace.useState();
|
|
const [virtualId, setVirtualId] = React__namespace.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.
|
|
index$1(() => {
|
|
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.
|
|
index$1(() => {
|
|
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.
|
|
index$1(() => {
|
|
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(node.context.elements.floating, activeEl));
|
|
if (parent && !treeContainsActiveEl && isPointerModalityRef.current) {
|
|
parent.focus({
|
|
preventScroll: true
|
|
});
|
|
}
|
|
}, [enabled, elements.floating, tree, parentId, virtual]);
|
|
index$1(() => {
|
|
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]);
|
|
index$1(() => {
|
|
previousOnNavigateRef.current = onNavigate;
|
|
previousOpenRef.current = open;
|
|
previousMountedRef.current = !!elements.floating;
|
|
});
|
|
index$1(() => {
|
|
if (!open) {
|
|
keyRef.current = null;
|
|
}
|
|
}, [open]);
|
|
const hasActiveIndex = activeIndex != null;
|
|
const item = React__namespace.useMemo(() => {
|
|
function syncCurrentTarget(currentTarget) {
|
|
if (!open) 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
|
|
...(focusItemOnHover && {
|
|
onMouseMove(_ref3) {
|
|
let {
|
|
currentTarget
|
|
} = _ref3;
|
|
forceSyncFocusRef.current = true;
|
|
forceScrollIntoViewRef.current = false;
|
|
syncCurrentTarget(currentTarget);
|
|
},
|
|
onPointerLeave(_ref4) {
|
|
let {
|
|
pointerType
|
|
} = _ref4;
|
|
if (!isPointerModalityRef.current || pointerType === 'touch') {
|
|
return;
|
|
}
|
|
forceSyncFocusRef.current = true;
|
|
indexRef.current = -1;
|
|
onNavigate();
|
|
if (!virtual) {
|
|
var _floatingFocusElement;
|
|
(_floatingFocusElement = floatingFocusElementRef.current) == null || _floatingFocusElement.focus({
|
|
preventScroll: true
|
|
});
|
|
}
|
|
}
|
|
})
|
|
};
|
|
return props;
|
|
}, [open, floatingFocusElementRef, focusItemOnHover, listRef, onNavigate, virtual]);
|
|
const getParentOrientation = React__namespace.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([...(disabledIndices || listRef.current.map((_, index) => isListIndexDisabled(listRef, index) ? 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__namespace.useMemo(() => {
|
|
return virtual && open && hasActiveIndex && {
|
|
'aria-activedescendant': virtualId || activeId
|
|
};
|
|
}, [virtual, open, hasActiveIndex, virtualId, activeId]);
|
|
const floating = React__namespace.useMemo(() => {
|
|
return {
|
|
'aria-orientation': orientation === 'both' ? undefined : orientation,
|
|
...(!typeableComboboxReference ? ariaActiveDescendantProp : {}),
|
|
onKeyDown: commonOnKeyDown,
|
|
onPointerMove() {
|
|
isPointerModalityRef.current = true;
|
|
}
|
|
};
|
|
}, [ariaActiveDescendantProp, commonOnKeyDown, orientation, typeableComboboxReference]);
|
|
const reference = React__namespace.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__namespace.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__namespace.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__namespace.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__namespace.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__namespace.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':
|
|
return {
|
|
...commonProps,
|
|
'aria-selected': active && selected
|
|
};
|
|
case 'combobox':
|
|
{
|
|
return {
|
|
...commonProps,
|
|
'aria-selected': selected
|
|
};
|
|
}
|
|
}
|
|
return {};
|
|
}, [floatingId, role]);
|
|
return React__namespace.useMemo(() => enabled ? {
|
|
reference,
|
|
floating,
|
|
item
|
|
} : {}, [enabled, reference, floating, item]);
|
|
}
|
|
|
|
/**
|
|
* 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__namespace.useRef(-1);
|
|
const stringRef = React__namespace.useRef('');
|
|
const prevIndexRef = React__namespace.useRef((_ref = selectedIndex != null ? selectedIndex : activeIndex) != null ? _ref : -1);
|
|
const matchIndexRef = React__namespace.useRef(null);
|
|
const onMatch = useEffectEvent(unstable_onMatch);
|
|
const onTypingChange = useEffectEvent(unstable_onTypingChange);
|
|
const findMatchRef = useLatestRef$1(findMatch);
|
|
const ignoreKeysRef = useLatestRef$1(ignoreKeys);
|
|
index$1(() => {
|
|
if (open) {
|
|
clearTimeoutIfSet(timeoutIdRef);
|
|
matchIndexRef.current = null;
|
|
stringRef.current = '';
|
|
}
|
|
}, [open]);
|
|
index$1(() => {
|
|
// 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__namespace.useMemo(() => ({
|
|
onKeyDown
|
|
}), [onKeyDown]);
|
|
const floating = React__namespace.useMemo(() => {
|
|
return {
|
|
onKeyDown,
|
|
onKeyUp(event) {
|
|
if (event.key === ' ') {
|
|
setTypingChange(false);
|
|
}
|
|
}
|
|
};
|
|
}, [onKeyDown, setTypingChange]);
|
|
return React__namespace.useMemo(() => enabled ? {
|
|
reference,
|
|
floating
|
|
} : {}, [enabled, reference, floating]);
|
|
}
|
|
|
|
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
*/
|
|
|
|
class MenuOption {
|
|
constructor(key) {
|
|
this.key = key;
|
|
this.ref = {
|
|
current: null
|
|
};
|
|
this.setRefElement = this.setRefElement.bind(this);
|
|
}
|
|
setRefElement(element) {
|
|
this.ref = {
|
|
current: element
|
|
};
|
|
}
|
|
}
|
|
class NodeContextMenuOption extends MenuOption {
|
|
constructor(title, options) {
|
|
super(title);
|
|
this.type = 'item';
|
|
this.title = title;
|
|
this.disabled = options.disabled ?? false;
|
|
this.icon = options.icon ?? null;
|
|
this.$onSelect = options.$onSelect;
|
|
if (options.$showOn) {
|
|
this.$showOn = options.$showOn;
|
|
}
|
|
}
|
|
}
|
|
class NodeContextMenuSeparator extends MenuOption {
|
|
constructor(options) {
|
|
super('_separator');
|
|
this.type = 'separator';
|
|
if (options && options.$showOn) {
|
|
this.$showOn = options.$showOn;
|
|
}
|
|
}
|
|
}
|
|
const ContextMenuSeparatorItem = /*#__PURE__*/React.forwardRef(({
|
|
className,
|
|
disabled,
|
|
...props
|
|
}, ref) => {
|
|
return /*#__PURE__*/jsxRuntime.jsx("hr", {
|
|
className: className
|
|
});
|
|
});
|
|
const ContextMenuItem = /*#__PURE__*/React.forwardRef(({
|
|
className,
|
|
label,
|
|
disabled,
|
|
icon,
|
|
...props
|
|
}, ref) => {
|
|
return /*#__PURE__*/jsxRuntime.jsxs("button", {
|
|
...props,
|
|
className: className,
|
|
ref: ref,
|
|
role: "menuitem",
|
|
disabled: disabled,
|
|
children: [icon, label]
|
|
});
|
|
});
|
|
const NodeContextMenuPlugin = /*#__PURE__*/React.forwardRef(({
|
|
items,
|
|
className,
|
|
itemClassName,
|
|
separatorClassName
|
|
}, forwardedRef) => {
|
|
const [editor] = LexicalComposerContext.useLexicalComposerContext();
|
|
const [activeIndex, setActiveIndex] = React.useState(null);
|
|
const [isOpen, setIsOpen] = React.useState(false);
|
|
const listItemsRef = React.useRef([]);
|
|
const listContentRef = React.useRef([]);
|
|
const {
|
|
refs,
|
|
floatingStyles,
|
|
context
|
|
} = useFloating({
|
|
middleware: [offset({
|
|
alignmentAxis: 4,
|
|
mainAxis: 5
|
|
}), flip({
|
|
fallbackPlacements: ['left-start']
|
|
}), shift({
|
|
padding: 10
|
|
})],
|
|
onOpenChange: setIsOpen,
|
|
open: isOpen,
|
|
placement: 'right-start',
|
|
strategy: 'fixed',
|
|
whileElementsMounted: autoUpdate
|
|
});
|
|
const role = useRole(context, {
|
|
role: 'menu'
|
|
});
|
|
const dismiss = useDismiss(context);
|
|
const listNavigation = useListNavigation(context, {
|
|
activeIndex,
|
|
listRef: listItemsRef,
|
|
onNavigate: setActiveIndex
|
|
});
|
|
const typeahead = useTypeahead(context, {
|
|
activeIndex,
|
|
enabled: isOpen,
|
|
listRef: listContentRef,
|
|
onMatch: setActiveIndex
|
|
});
|
|
const {
|
|
getFloatingProps,
|
|
getItemProps
|
|
} = useInteractions([role, dismiss, listNavigation, typeahead]);
|
|
const [renderItems, setRenderItems] = React.useState([]);
|
|
React.useEffect(() => {
|
|
function onContextMenu(e) {
|
|
e.preventDefault();
|
|
refs.setPositionReference({
|
|
getBoundingClientRect() {
|
|
return {
|
|
bottom: e.clientY,
|
|
height: 0,
|
|
left: e.clientX,
|
|
right: e.clientX,
|
|
top: e.clientY,
|
|
width: 0,
|
|
x: e.clientX,
|
|
y: e.clientY
|
|
};
|
|
}
|
|
});
|
|
let visibleItems = [];
|
|
if (items) {
|
|
editor.read(() => {
|
|
const node = lexical.$getNearestNodeFromDOMNode(e.target);
|
|
if (node) {
|
|
visibleItems = items.filter(option => option.$showOn ? option.$showOn(node) : true);
|
|
}
|
|
});
|
|
}
|
|
const renderableItems = visibleItems.map((option, index) => {
|
|
if (option.type === 'separator') {
|
|
return {
|
|
className: separatorClassName,
|
|
key: option.key + '-' + index,
|
|
type: option.type
|
|
};
|
|
} else {
|
|
return {
|
|
className: itemClassName,
|
|
disabled: option.disabled,
|
|
icon: option.icon,
|
|
key: option.key,
|
|
label: option.title,
|
|
onClick: () => editor.update(() => option.$onSelect()),
|
|
title: option.title,
|
|
type: option.type
|
|
};
|
|
}
|
|
});
|
|
listContentRef.current = renderableItems.map(item => item.key);
|
|
setRenderItems(renderableItems);
|
|
setIsOpen(true);
|
|
}
|
|
return editor.registerRootListener((rootElement, prevRootElement) => {
|
|
if (prevRootElement !== null) {
|
|
prevRootElement.removeEventListener('contextmenu', onContextMenu);
|
|
}
|
|
if (rootElement !== null) {
|
|
rootElement.addEventListener('contextmenu', onContextMenu);
|
|
}
|
|
});
|
|
}, [items, itemClassName, separatorClassName, refs, editor]);
|
|
return /*#__PURE__*/jsxRuntime.jsx(FloatingPortal, {
|
|
children: isOpen && /*#__PURE__*/jsxRuntime.jsx(FloatingOverlay, {
|
|
lockScroll: true,
|
|
children: /*#__PURE__*/jsxRuntime.jsx(FloatingFocusManager, {
|
|
context: context,
|
|
initialFocus: refs.floating,
|
|
children: /*#__PURE__*/jsxRuntime.jsx("div", {
|
|
className: className,
|
|
ref: refs.setFloating,
|
|
style: floatingStyles,
|
|
...getFloatingProps(),
|
|
children: renderItems.map((item, index) => {
|
|
if (item.type === 'item') {
|
|
return /*#__PURE__*/jsxRuntime.jsx(ContextMenuItem, {
|
|
...getItemProps({
|
|
...item,
|
|
onClick() {
|
|
item.onClick();
|
|
setIsOpen(false);
|
|
},
|
|
onMouseUp() {
|
|
item.onClick();
|
|
setIsOpen(false);
|
|
},
|
|
ref(node) {
|
|
listItemsRef.current[index] = node;
|
|
},
|
|
tabIndex: activeIndex === index ? 0 : -1
|
|
})
|
|
});
|
|
} else if (item.type === 'separator') {
|
|
return /*#__PURE__*/jsxRuntime.jsx(ContextMenuSeparatorItem, {
|
|
...getItemProps({
|
|
...item,
|
|
ref(node) {
|
|
listItemsRef.current[index] = node;
|
|
},
|
|
tabIndex: activeIndex === index ? 0 : -1
|
|
})
|
|
});
|
|
}
|
|
})
|
|
})
|
|
})
|
|
})
|
|
});
|
|
});
|
|
|
|
exports.NodeContextMenuOption = NodeContextMenuOption;
|
|
exports.NodeContextMenuPlugin = NodeContextMenuPlugin;
|
|
exports.NodeContextMenuSeparator = NodeContextMenuSeparator;
|