@react-stately/grid
Version:
Spectrum UI components in React
123 lines (114 loc) • 4.59 kB
text/typescript
import {getChildNodes, getFirstItem, getLastItem} from '@react-stately/collections';
import {GridCollection, GridNode} from '@react-types/grid';
import {Key} from '@react-types/shared';
import {MultipleSelectionState, MultipleSelectionStateProps, SelectionManager, useMultipleSelectionState} from '@react-stately/selection';
import {useEffect, useMemo, useRef} from 'react';
export interface GridState<T, C extends GridCollection<T>> {
collection: C,
/** A set of keys for rows that are disabled. */
disabledKeys: Set<Key>,
/** A selection manager to read and update row selection state. */
selectionManager: SelectionManager,
/** Whether keyboard navigation is disabled, such as when the arrow keys should be handled by a component within a cell. */
isKeyboardNavigationDisabled: boolean
}
export interface GridStateOptions<T, C extends GridCollection<T>> extends MultipleSelectionStateProps {
collection: C,
disabledKeys?: Iterable<Key>,
focusMode?: 'row' | 'cell',
/** @private - do not use unless you know what you're doing. */
UNSAFE_selectionState?: MultipleSelectionState
}
/**
* Provides state management for a grid component. Handles row selection and focusing a grid cell's focusable child if applicable.
*/
export function useGridState<T extends object, C extends GridCollection<T>>(props: GridStateOptions<T, C>): GridState<T, C> {
let {collection, focusMode} = props;
// eslint-disable-next-line react-hooks/rules-of-hooks
let selectionState = props.UNSAFE_selectionState || useMultipleSelectionState(props);
let disabledKeys = useMemo(() =>
props.disabledKeys ? new Set(props.disabledKeys) : new Set<Key>()
, [props.disabledKeys]);
let setFocusedKey = selectionState.setFocusedKey;
selectionState.setFocusedKey = (key, child) => {
// If focusMode is cell and an item is focused, focus a child cell instead.
if (focusMode === 'cell' && key != null) {
let item = collection.getItem(key);
if (item?.type === 'item') {
let children = getChildNodes(item, collection);
if (child === 'last') {
key = getLastItem(children)?.key ?? null;
} else {
key = getFirstItem(children)?.key ?? null;
}
}
}
setFocusedKey(key, child);
};
let selectionManager = useMemo(() =>
new SelectionManager(collection, selectionState)
, [collection, selectionState]
);
// Reset focused key if that item is deleted from the collection.
const cachedCollection = useRef<C | null>(null);
useEffect(() => {
if (selectionState.focusedKey != null && cachedCollection.current && !collection.getItem(selectionState.focusedKey)) {
const node = cachedCollection.current.getItem(selectionState.focusedKey);
const parentNode =
node?.parentKey != null && (node.type === 'cell' || node.type === 'rowheader' || node.type === 'column') ?
cachedCollection.current.getItem(node.parentKey) :
node;
if (!parentNode) {
selectionState.setFocusedKey(null);
return;
}
const cachedRows = cachedCollection.current.rows;
const rows = collection.rows;
const diff = cachedRows.length - rows.length;
let index = Math.min(
(
diff > 1 ?
Math.max(parentNode.index - diff + 1, 0) :
parentNode.index
),
rows.length - 1);
let newRow: GridNode<T> | null = null;
while (index >= 0) {
if (!selectionManager.isDisabled(rows[index].key) && rows[index].type !== 'headerrow') {
newRow = rows[index];
break;
}
// Find next, not disabled row.
if (index < rows.length - 1) {
index++;
// Otherwise, find previous, not disabled row.
} else {
if (index > parentNode.index) {
index = parentNode.index;
}
index--;
}
}
if (newRow) {
const childNodes = newRow.hasChildNodes ? [...getChildNodes(newRow, collection)] : [];
const keyToFocus =
newRow.hasChildNodes &&
parentNode !== node &&
node &&
node.index < childNodes.length ?
childNodes[node.index].key :
newRow.key;
selectionState.setFocusedKey(keyToFocus);
} else {
selectionState.setFocusedKey(null);
}
}
cachedCollection.current = collection;
}, [collection, selectionManager, selectionState, selectionState.focusedKey]);
return {
collection,
disabledKeys,
isKeyboardNavigationDisabled: false,
selectionManager
};
}