@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
JavaScript
'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;
}
;