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.

272 lines (267 loc) 8.07 kB
import { hasComputedStyleMapSupport } from '../utils/hasComputedStyleMapSupport.js'; import { ownerWindow } from '../utils/owner.js'; export const ARROW_UP = 'ArrowUp'; export const ARROW_DOWN = 'ArrowDown'; export const ARROW_LEFT = 'ArrowLeft'; export const ARROW_RIGHT = 'ArrowRight'; export const HOME = 'Home'; export const END = 'End'; export const HORIZONTAL_KEYS = [ARROW_LEFT, ARROW_RIGHT]; export const HORIZONTAL_KEYS_WITH_EXTRA_KEYS = [ARROW_LEFT, ARROW_RIGHT, HOME, END]; export const VERTICAL_KEYS = [ARROW_UP, ARROW_DOWN]; export const VERTICAL_KEYS_WITH_EXTRA_KEYS = [ARROW_UP, ARROW_DOWN, HOME, END]; export const ARROW_KEYS = [...HORIZONTAL_KEYS, ...VERTICAL_KEYS]; export const ALL_KEYS = [...ARROW_KEYS, HOME, END]; function stopEvent(event) { event.preventDefault(); event.stopPropagation(); } export function isDifferentRow(index, cols, prevRow) { return Math.floor(index / cols) !== prevRow; } export function isIndexOutOfBounds(listRef, index) { return index < 0 || index >= listRef.current.length; } export function getMinIndex(listRef, disabledIndices) { return findNonDisabledIndex(listRef, { disabledIndices }); } export function getMaxIndex(listRef, disabledIndices) { return findNonDisabledIndex(listRef, { decrement: true, startingIndex: listRef.current.length, disabledIndices }); } export function findNonDisabledIndex(listRef, { startingIndex = -1, decrement = false, disabledIndices, amount = 1 } = {}) { const list = listRef.current; let index = startingIndex; do { index += decrement ? -amount : amount; } while (index >= 0 && index <= list.length - 1 && isDisabled(list, index, disabledIndices)); return index; } export function getGridNavigatedIndex(elementsRef, { event, orientation, loop, cols, disabledIndices, minIndex, maxIndex, prevIndex, rtl, stopEvent: stop = false }) { let nextIndex = prevIndex; if (event.key === ARROW_UP) { if (stop) { stopEvent(event); } if (prevIndex === -1) { nextIndex = maxIndex; } else { nextIndex = findNonDisabledIndex(elementsRef, { 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 (isIndexOutOfBounds(elementsRef, nextIndex)) { nextIndex = prevIndex; } } if (event.key === ARROW_DOWN) { if (stop) { stopEvent(event); } if (prevIndex === -1) { nextIndex = minIndex; } else { nextIndex = findNonDisabledIndex(elementsRef, { startingIndex: prevIndex, amount: cols, disabledIndices }); if (loop && prevIndex + cols > maxIndex) { nextIndex = findNonDisabledIndex(elementsRef, { startingIndex: prevIndex % cols - cols, amount: cols, disabledIndices }); } } if (isIndexOutOfBounds(elementsRef, nextIndex)) { nextIndex = prevIndex; } } // Remains on the same row/column. if (orientation === 'both') { const nextKey = rtl ? ARROW_LEFT : ARROW_RIGHT; const prevKey = rtl ? ARROW_RIGHT : ARROW_LEFT; const prevRow = Math.floor(prevIndex / cols); if (event.key === nextKey) { if (stop) { stopEvent(event); } if (prevIndex % cols !== cols - 1) { nextIndex = findNonDisabledIndex(elementsRef, { startingIndex: prevIndex, disabledIndices }); if (loop && isDifferentRow(nextIndex, cols, prevRow)) { nextIndex = findNonDisabledIndex(elementsRef, { startingIndex: prevIndex - prevIndex % cols - 1, disabledIndices }); } } else if (loop) { nextIndex = findNonDisabledIndex(elementsRef, { startingIndex: prevIndex - prevIndex % cols - 1, disabledIndices }); } if (isDifferentRow(nextIndex, cols, prevRow)) { nextIndex = prevIndex; } } if (event.key === prevKey) { if (stop) { stopEvent(event); } if (prevIndex % cols !== 0) { nextIndex = findNonDisabledIndex(elementsRef, { startingIndex: prevIndex, decrement: true, disabledIndices }); if (loop && isDifferentRow(nextIndex, cols, prevRow)) { nextIndex = findNonDisabledIndex(elementsRef, { startingIndex: prevIndex + (cols - prevIndex % cols), decrement: true, disabledIndices }); } } else if (loop) { nextIndex = findNonDisabledIndex(elementsRef, { startingIndex: prevIndex + (cols - prevIndex % cols), decrement: true, disabledIndices }); } if (isDifferentRow(nextIndex, cols, prevRow)) { nextIndex = prevIndex; } } const lastRow = Math.floor(maxIndex / cols) === prevRow; if (isIndexOutOfBounds(elementsRef, nextIndex)) { if (loop && lastRow) { nextIndex = event.key === prevKey ? maxIndex : findNonDisabledIndex(elementsRef, { startingIndex: prevIndex - prevIndex % cols - 1, disabledIndices }); } else { nextIndex = prevIndex; } } } return nextIndex; } /** For each cell index, gets the item index that occupies that cell */ export function buildCellMap(sizes, cols, dense) { const cellMap = []; let startIndex = 0; sizes.forEach(({ width, height }, index) => { if (width > cols) { if (process.env.NODE_ENV !== 'production') { throw new Error(`[Base 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 += 1) { for (let j = 0; j < height; j += 1) { 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 += 1; } } }); // convert into a non-sparse array return [...cellMap]; } /** Gets cell index of an item's corner or -1 when index is -1. */ export function getCellIndexOfCorner(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); default: return -1; } } /** Gets all cell indices that correspond to the specified indices */ export function getCellIndices(indices, cellMap) { return cellMap.flatMap((index, cellIndex) => indices.includes(index) ? [cellIndex] : []); } export function isDisabled(list, index, disabledIndices) { if (disabledIndices) { return disabledIndices.includes(index); } const element = list[index]; return element == null || element.hasAttribute('disabled') || element.getAttribute('aria-disabled') === 'true'; } export function getTextDirection(element) { if (hasComputedStyleMapSupport()) { const direction = element.computedStyleMap().get('direction'); return direction?.value; } return ownerWindow(element).getComputedStyle(element).direction; }