UNPKG

@base-ui-components/react

Version:

Base UI is a library of headless ('unstyled') React components and low-level hooks. You gain complete control over your app's CSS and accessibility features.

220 lines (218 loc) 9.74 kB
"use strict"; 'use client'; var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default; Object.defineProperty(exports, "__esModule", { value: true }); exports.useCompositeRoot = useCompositeRoot; var React = _interopRequireWildcard(require("react")); var _isElementDisabled = require("@base-ui-components/utils/isElementDisabled"); var _useEventCallback = require("@base-ui-components/utils/useEventCallback"); var _useMergedRefs = require("@base-ui-components/utils/useMergedRefs"); var _composite = require("../composite"); var _constants = require("../constants"); const EMPTY_ARRAY = []; function useCompositeRoot(params) { const { itemSizes, cols = 1, loop = true, dense = false, orientation = 'both', direction, highlightedIndex: externalHighlightedIndex, onHighlightedIndexChange: externalSetHighlightedIndex, rootRef: externalRef, enableHomeAndEndKeys = false, stopEventPropagation = false, disabledIndices, modifierKeys = EMPTY_ARRAY } = params; const [internalHighlightedIndex, internalSetHighlightedIndex] = React.useState(0); const isGrid = cols > 1; const rootRef = React.useRef(null); const mergedRef = (0, _useMergedRefs.useMergedRefs)(rootRef, externalRef); const elementsRef = React.useRef([]); const hasSetDefaultIndexRef = React.useRef(false); const highlightedIndex = externalHighlightedIndex ?? internalHighlightedIndex; const onHighlightedIndexChange = (0, _useEventCallback.useEventCallback)((index, shouldScrollIntoView = false) => { (externalSetHighlightedIndex ?? internalSetHighlightedIndex)(index); if (shouldScrollIntoView) { const newActiveItem = elementsRef.current[index]; (0, _composite.scrollIntoViewIfNeeded)(rootRef.current, newActiveItem, direction, orientation); } }); const onMapChange = (0, _useEventCallback.useEventCallback)(map => { if (map.size === 0 || hasSetDefaultIndexRef.current) { return; } hasSetDefaultIndexRef.current = true; const sortedElements = Array.from(map.keys()); const activeItem = sortedElements.find(compositeElement => compositeElement?.hasAttribute(_constants.ACTIVE_COMPOSITE_ITEM)) ?? null; // Set the default highlighted index of an arbitrary composite item. const activeIndex = activeItem ? sortedElements.indexOf(activeItem) : -1; if (activeIndex !== -1) { onHighlightedIndexChange(activeIndex); } (0, _composite.scrollIntoViewIfNeeded)(rootRef.current, activeItem, direction, orientation); }); const props = React.useMemo(() => ({ 'aria-orientation': orientation === 'both' ? undefined : orientation, ref: mergedRef, onFocus(event) { const element = rootRef.current; if (!element || !(0, _composite.isNativeInput)(event.target)) { return; } event.target.setSelectionRange(0, event.target.value.length ?? 0); }, onKeyDown(event) { const RELEVANT_KEYS = enableHomeAndEndKeys ? _composite.ALL_KEYS : _composite.ARROW_KEYS; if (!RELEVANT_KEYS.has(event.key)) { return; } if (isModifierKeySet(event, modifierKeys)) { return; } const element = rootRef.current; if (!element) { return; } const isRtl = direction === 'rtl'; const horizontalForwardKey = isRtl ? _composite.ARROW_LEFT : _composite.ARROW_RIGHT; const forwardKey = { horizontal: horizontalForwardKey, vertical: _composite.ARROW_DOWN, both: horizontalForwardKey }[orientation]; const horizontalBackwardKey = isRtl ? _composite.ARROW_RIGHT : _composite.ARROW_LEFT; const backwardKey = { horizontal: horizontalBackwardKey, vertical: _composite.ARROW_UP, both: horizontalBackwardKey }[orientation]; if ((0, _composite.isNativeInput)(event.target) && !(0, _isElementDisabled.isElementDisabled)(event.target)) { const selectionStart = event.target.selectionStart; const selectionEnd = event.target.selectionEnd; const textContent = event.target.value ?? ''; // return to native textbox behavior when // 1 - Shift is held to make a text selection, or if there already is a text selection if (selectionStart == null || event.shiftKey || selectionStart !== selectionEnd) { return; } // 2 - arrow-ing forward and not in the last position of the text if (event.key !== backwardKey && selectionStart < textContent.length) { return; } // 3 -arrow-ing backward and not in the first position of the text if (event.key !== forwardKey && selectionStart > 0) { return; } } let nextIndex = highlightedIndex; const minIndex = (0, _composite.getMinListIndex)(elementsRef, disabledIndices); const maxIndex = (0, _composite.getMaxListIndex)(elementsRef, disabledIndices); if (isGrid) { const sizes = itemSizes || Array.from({ length: elementsRef.current.length }, () => ({ width: 1, height: 1 })); // To calculate movements on the grid, we use hypothetical cell indices // as if every item was 1x1, then convert back to real indices. const cellMap = (0, _composite.createGridCellMap)(sizes, cols, dense); const minGridIndex = cellMap.findIndex(index => index != null && !(0, _composite.isListIndexDisabled)(elementsRef, index, disabledIndices)); // last enabled index const maxGridIndex = cellMap.reduce((foundIndex, index, cellIndex) => index != null && !(0, _composite.isListIndexDisabled)(elementsRef, index, disabledIndices) ? cellIndex : foundIndex, -1); nextIndex = cellMap[(0, _composite.getGridNavigatedIndex)({ current: cellMap.map(itemIndex => itemIndex ? elementsRef.current[itemIndex] : null) }, { event, orientation, loop, cols, // treat undefined (empty grid spaces) as disabled indices so we // don't end up in them disabledIndices: (0, _composite.getGridCellIndices)([...(disabledIndices || elementsRef.current.map((_, index) => (0, _composite.isListIndexDisabled)(elementsRef, index) ? index : undefined)), undefined], cellMap), minIndex: minGridIndex, maxIndex: maxGridIndex, prevIndex: (0, _composite.getGridCellIndexOfCorner)(highlightedIndex > maxIndex ? minIndex : highlightedIndex, 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. // eslint-disable-next-line no-nested-ternary event.key === _composite.ARROW_DOWN ? 'bl' : event.key === _composite.ARROW_RIGHT ? 'tr' : 'tl'), rtl: isRtl })]; // navigated cell will never be nullish } const forwardKeys = { horizontal: [horizontalForwardKey], vertical: [_composite.ARROW_DOWN], both: [horizontalForwardKey, _composite.ARROW_DOWN] }[orientation]; const backwardKeys = { horizontal: [horizontalBackwardKey], vertical: [_composite.ARROW_UP], both: [horizontalBackwardKey, _composite.ARROW_UP] }[orientation]; const preventedKeys = isGrid ? RELEVANT_KEYS : { horizontal: enableHomeAndEndKeys ? _composite.HORIZONTAL_KEYS_WITH_EXTRA_KEYS : _composite.HORIZONTAL_KEYS, vertical: enableHomeAndEndKeys ? _composite.VERTICAL_KEYS_WITH_EXTRA_KEYS : _composite.VERTICAL_KEYS, both: RELEVANT_KEYS }[orientation]; if (enableHomeAndEndKeys) { if (event.key === _composite.HOME) { nextIndex = minIndex; } else if (event.key === _composite.END) { nextIndex = maxIndex; } } if (nextIndex === highlightedIndex && (forwardKeys.includes(event.key) || backwardKeys.includes(event.key))) { if (loop && nextIndex === maxIndex && forwardKeys.includes(event.key)) { nextIndex = minIndex; } else if (loop && nextIndex === minIndex && backwardKeys.includes(event.key)) { nextIndex = maxIndex; } else { nextIndex = (0, _composite.findNonDisabledListIndex)(elementsRef, { startingIndex: nextIndex, decrement: backwardKeys.includes(event.key), disabledIndices }); } } if (nextIndex !== highlightedIndex && !(0, _composite.isIndexOutOfListBounds)(elementsRef, nextIndex)) { if (stopEventPropagation) { event.stopPropagation(); } if (preventedKeys.has(event.key)) { event.preventDefault(); } onHighlightedIndexChange(nextIndex, true); // Wait for FocusManager `returnFocus` to execute. queueMicrotask(() => { elementsRef.current[nextIndex]?.focus(); }); } } }), [cols, dense, direction, disabledIndices, elementsRef, enableHomeAndEndKeys, highlightedIndex, isGrid, itemSizes, loop, mergedRef, modifierKeys, onHighlightedIndexChange, orientation, stopEventPropagation]); return React.useMemo(() => ({ props, highlightedIndex, onHighlightedIndexChange, elementsRef, disabledIndices, onMapChange }), [props, highlightedIndex, onHighlightedIndexChange, elementsRef, disabledIndices, onMapChange]); } function isModifierKeySet(event, ignoredModifierKeys) { for (const key of _composite.MODIFIER_KEYS.values()) { if (ignoredModifierKeys.includes(key)) { continue; } if (event.getModifierState(key)) { return true; } } return false; }