UNPKG

@floating-ui/react

Version:
546 lines (522 loc) 18.2 kB
import { isShadowRoot, isHTMLElement } from '@floating-ui/utils/dom'; import * as React from 'react'; import { useLayoutEffect } from 'react'; import { floor } from '@floating-ui/utils'; import { tabbable } from 'tabbable'; // 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 isMac() { return getPlatform().toLowerCase().startsWith('mac') && !navigator.maxTouchPoints; } function isJSDOM() { return getUserAgent().includes('jsdom/'); } const FOCUSABLE_ATTRIBUTE = 'data-floating-ui-focusable'; const TYPEABLE_SELECTOR = "input:not([type='hidden']):not([disabled])," + "[contenteditable]:not([contenteditable='false']),textarea:not([disabled])"; const ARROW_LEFT = 'ArrowLeft'; const ARROW_RIGHT = 'ArrowRight'; const ARROW_UP = 'ArrowUp'; const ARROW_DOWN = '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(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 matchesFocusVisible(element) { // We don't want to block focus from working with `visibleOnly` // (JSDOM doesn't match `:focus-visible` when the element has `:focus`) if (!element || isJSDOM()) return true; try { return element.matches(':focus-visible'); } catch (_e) { return true; } } 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) ? floatingElement : floatingElement.querySelector("[" + FOCUSABLE_ATTRIBUTE + "]") || floatingElement; } function getNodeChildren(nodes, id) { 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 = 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); }); }); 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'; } function isMouseLikePointerType(pointerType, strict) { // On some Linux machines with Chromium, mouse inputs return a `pointerType` // of "pen": https://github.com/floating-ui/floating-ui/issues/2015 const values = ['mouse', 'pen']; if (!strict) { values.push('', undefined); } return values.includes(pointerType); } var isClient = typeof document !== 'undefined'; var noop = function noop() {}; var index = isClient ? useLayoutEffect : noop; // https://github.com/mui/material-ui/issues/41190#issuecomment-2040873379 const SafeReact = { ...React }; function useLatestRef(value) { const ref = React.useRef(value); index(() => { ref.current = value; }); return ref; } const useInsertionEffect = SafeReact.useInsertionEffect; const useSafeInsertionEffect = useInsertionEffect || (fn => fn()); function useEffectEvent(callback) { const ref = React.useRef(() => { if (process.env.NODE_ENV !== "production") { throw new Error('Cannot call an event handler while rendering.'); } }); useSafeInsertionEffect(() => { ref.current = callback; }); return React.useCallback(function () { for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } return ref.current == null ? void 0 : ref.current(...args); }, []); } 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) { 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) { 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 : ARROW_RIGHT)) { 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 : ARROW_LEFT)) { 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 : ARROW_LEFT) ? 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 (typeof disabledIndices === 'function') { return disabledIndices(index); } else 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(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(referenceElement).body, 1) || referenceElement; } function getPreviousTabbable(referenceElement) { return getTabbableIn(getDocument(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'); } }); } export { activeElement, contains, createGridCellMap, disableFocusInside, enableFocusInside, findNonDisabledListIndex, getDeepestNode, getDocument, getFloatingFocusElement, getGridCellIndexOfCorner, getGridCellIndices, getGridNavigatedIndex, getMaxListIndex, getMinListIndex, getNextTabbable, getNodeAncestors, getNodeChildren, getPlatform, getPreviousTabbable, getTabbableOptions, getTarget, getUserAgent, isAndroid, isDifferentGridRow, isEventTargetWithin, isIndexOutOfListBounds, isJSDOM, isListIndexDisabled, isMac, isMouseLikePointerType, isOutsideEvent, isReactEvent, isRootElement, isSafari, isTypeableCombobox, isTypeableElement, isVirtualClick, isVirtualPointerEvent, matchesFocusVisible, stopEvent, useEffectEvent, useLatestRef, index as useModernLayoutEffect };