@ankhorage/zora 25.8.13 → 25.8.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/README.md +1 -1
- package/dist/features/matrix-grid/adapters/inbound/MatrixGrid.d.ts.map +1 -1
- package/dist/features/matrix-grid/adapters/inbound/MatrixGrid.js +11 -8
- package/dist/features/matrix-grid/adapters/inbound/MatrixGrid.js.map +1 -1
- package/dist/features/matrix-grid/adapters/inbound/useMatrixGridKeyboardFocus.d.ts +1 -1
- package/dist/features/matrix-grid/adapters/inbound/useMatrixGridKeyboardFocus.d.ts.map +1 -1
- package/dist/features/matrix-grid/adapters/inbound/useMatrixGridKeyboardFocus.js +25 -5
- package/dist/features/matrix-grid/adapters/inbound/useMatrixGridKeyboardFocus.js.map +1 -1
- package/dist/features/matrix-grid/application/resolveMatrixGridSelection.d.ts +1 -1
- package/dist/features/matrix-grid/application/resolveMatrixGridSelection.d.ts.map +1 -1
- package/dist/features/matrix-grid/application/resolveMatrixGridSelection.js +5 -2
- package/dist/features/matrix-grid/application/resolveMatrixGridSelection.js.map +1 -1
- package/dist/types/matrix-grid.d.ts +2 -0
- package/dist/types/matrix-grid.d.ts.map +1 -1
- package/dist/types/matrix-grid.js.map +1 -1
- package/package.json +2 -2
- package/src/features/matrix-grid/adapters/inbound/MatrixGrid.protected-focus.test.tsx +252 -0
- package/src/features/matrix-grid/adapters/inbound/MatrixGrid.test.tsx +79 -0
- package/src/features/matrix-grid/adapters/inbound/MatrixGrid.tsx +16 -5
- package/src/features/matrix-grid/adapters/inbound/useMatrixGridKeyboardFocus.ts +31 -4
- package/src/features/matrix-grid/application/resolveMatrixGridSelection.test.ts +20 -13
- package/src/features/matrix-grid/application/resolveMatrixGridSelection.ts +3 -0
- package/src/types/matrix-grid.ts +2 -0
- package/web-dist/components/matrix-grid/MatrixGrid.js +41 -16
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
# @ankhorage/zora
|
|
5
5
|
|
|
6
|
-
         
|
|
7
7
|
|
|
8
8
|
Opinionated React Native and React Native Web UI kit built on @ankhorage/surface.
|
|
9
9
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MatrixGrid.d.ts","sourceRoot":"","sources":["../../../../../src/features/matrix-grid/adapters/inbound/MatrixGrid.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;
|
|
1
|
+
{"version":3,"file":"MatrixGrid.d.ts","sourceRoot":"","sources":["../../../../../src/features/matrix-grid/adapters/inbound/MatrixGrid.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAuOrE,gGAAgG;AAChG,eAAO,MAAM,UAAU,uDAAsC,CAAC"}
|
|
@@ -12,7 +12,7 @@ import { resolveMatrixGridSelection } from '../../application/resolveMatrixGridS
|
|
|
12
12
|
import { MatrixGridKeyboardProxy } from './MatrixGridKeyboardProxy';
|
|
13
13
|
import { useMatrixGridKeyboardFocus } from './useMatrixGridKeyboardFocus';
|
|
14
14
|
/*** Compose the canonical GridView around released sparse matrix placement and culling geometry. */
|
|
15
|
-
function MatrixGridInner({ accessibilityLabel = 'Matrix grid', cells, height, interactionPolicy, layout, onSelectionChange, onViewportChange, overscanPixels = 160, readOnly = false, renderCell, selectedCellIds = [], selectionMode = 'single', testID, viewport: controlledViewport, width, zoomX = 1, zoomY = 1, }) {
|
|
15
|
+
function MatrixGridInner({ accessibilityLabel = 'Matrix grid', cells, height, interactionPolicy, layout, onSelectionChange, onViewportChange, overscanPixels = 160, protectedCellIds = [], readOnly = false, renderCell, selectedCellIds = [], selectionMode = 'single', testID, viewport: controlledViewport, width, zoomX = 1, zoomY = 1, }) {
|
|
16
16
|
const { theme } = useZoraTheme();
|
|
17
17
|
const contentSize = React.useMemo(() => resolveMatrixGridContentSize(layout), [layout]);
|
|
18
18
|
const initialViewport = React.useMemo(() => ({ height, offsetX: 0, offsetY: 0, pixelsPerUnitX: zoomX, pixelsPerUnitY: zoomY, width }), [height, width, zoomX, zoomY]);
|
|
@@ -22,18 +22,20 @@ function MatrixGridInner({ accessibilityLabel = 'Matrix grid', cells, height, in
|
|
|
22
22
|
const cellPlacements = React.useMemo(() => getVisibleMatrixCells(layout, cells, viewport, overscanPixels), [cells, layout, overscanPixels, viewport]);
|
|
23
23
|
const [visibleIds, setVisibleIds] = React.useState([]);
|
|
24
24
|
const anchorId = React.useRef(null);
|
|
25
|
+
const protectedCellIdSet = React.useMemo(() => new Set(protectedCellIds), [protectedCellIds]);
|
|
26
|
+
const isNavigable = React.useCallback((id) => !protectedCellIdSet.has(id), [protectedCellIdSet]);
|
|
25
27
|
const handleViewportChange = React.useCallback((proposal) => {
|
|
26
28
|
if (controlledViewport === undefined)
|
|
27
29
|
setUncontrolledViewport(proposal);
|
|
28
30
|
onViewportChange?.(proposal);
|
|
29
31
|
}, [controlledViewport, onViewportChange]);
|
|
30
32
|
const activateCell = React.useCallback((cell, event) => {
|
|
31
|
-
if (interactionPolicy === 'passive' || readOnly)
|
|
33
|
+
if (interactionPolicy === 'passive' || readOnly || protectedCellIdSet.has(cell.id))
|
|
32
34
|
return;
|
|
33
35
|
const intent = isRangeSelectionEvent(event)
|
|
34
36
|
? 'range'
|
|
35
37
|
: resolveSelectionEventIntent(event, 'pointer');
|
|
36
|
-
onSelectionChange?.(resolveMatrixGridSelection(sparseIndex.entries, selectedCellIds, cell.id, anchorId.current, intent, selectionMode));
|
|
38
|
+
onSelectionChange?.(resolveMatrixGridSelection(sparseIndex.entries, selectedCellIds, cell.id, anchorId.current, intent, selectionMode, protectedCellIdSet));
|
|
37
39
|
if (intent !== 'range')
|
|
38
40
|
anchorId.current = cell.id;
|
|
39
41
|
}, [
|
|
@@ -43,6 +45,7 @@ function MatrixGridInner({ accessibilityLabel = 'Matrix grid', cells, height, in
|
|
|
43
45
|
selectedCellIds,
|
|
44
46
|
selectionMode,
|
|
45
47
|
sparseIndex.entries,
|
|
48
|
+
protectedCellIdSet,
|
|
46
49
|
]);
|
|
47
50
|
const handleKeyboardNavigation = React.useCallback((targetId, originId, shiftKey) => {
|
|
48
51
|
anchorId.current ??= originId;
|
|
@@ -52,7 +55,7 @@ function MatrixGridInner({ accessibilityLabel = 'Matrix grid', cells, height, in
|
|
|
52
55
|
if (cell)
|
|
53
56
|
activateCell(getMatrixCellPlacement(layout, cell), { shiftKey: true });
|
|
54
57
|
}, [activateCell, layout, sparseIndex]);
|
|
55
|
-
const keyboard = useMatrixGridKeyboardFocus(sparseIndex, interactionPolicy === 'passive', visibleIds, handleKeyboardNavigation);
|
|
58
|
+
const keyboard = useMatrixGridKeyboardFocus(sparseIndex, interactionPolicy === 'passive', visibleIds, isNavigable, handleKeyboardNavigation);
|
|
56
59
|
const focusedPlacement = React.useMemo(() => {
|
|
57
60
|
const focusedCell = keyboard.focusedId === null ? undefined : sparseIndex.cellById.get(keyboard.focusedId);
|
|
58
61
|
return focusedCell ? getMatrixCellPlacement(layout, focusedCell) : undefined;
|
|
@@ -66,12 +69,12 @@ function MatrixGridInner({ accessibilityLabel = 'Matrix grid', cells, height, in
|
|
|
66
69
|
const cell = cellById.get(item.id);
|
|
67
70
|
if (!cell)
|
|
68
71
|
return null;
|
|
69
|
-
return renderMatrixCell(cell, selectedCellIds, theme.semantics.neutral.divider, theme.colors.primary, renderCell, activateCell, keyboard, interactionPolicy === 'passive', readOnly, testID);
|
|
72
|
+
return renderMatrixCell(cell, selectedCellIds, theme.semantics.neutral.divider, theme.colors.primary, renderCell, activateCell, keyboard, interactionPolicy === 'passive', protectedCellIdSet.has(cell.id), readOnly, testID);
|
|
70
73
|
}} testID={testID} viewport={viewport} width={width} onViewportChange={handleViewportChange}/>
|
|
71
74
|
</View>);
|
|
72
75
|
}
|
|
73
76
|
/*** Render one positioned sparse cell with native/web accessible selection semantics. */
|
|
74
|
-
function renderMatrixCell(cell, selectedCellIds, dividerColor, selectedColor, renderCell, activateCell, keyboard, passive, readOnly, testID) {
|
|
77
|
+
function renderMatrixCell(cell, selectedCellIds, dividerColor, selectedColor, renderCell, activateCell, keyboard, passive, protectedCell, readOnly, testID) {
|
|
75
78
|
const selected = selectedCellIds.includes(cell.id);
|
|
76
79
|
const canInteract = keyboard.tabStopId === cell.id;
|
|
77
80
|
return (<MatrixGridKeyboardProxy onKeyDown={(event) => {
|
|
@@ -79,12 +82,12 @@ function renderMatrixCell(cell, selectedCellIds, dividerColor, selectedColor, re
|
|
|
79
82
|
return true;
|
|
80
83
|
if (!['Enter', ' '].includes(event.key))
|
|
81
84
|
return false;
|
|
82
|
-
if (passive || readOnly)
|
|
85
|
+
if (passive || protectedCell || readOnly)
|
|
83
86
|
return false;
|
|
84
87
|
activateCell(cell, event);
|
|
85
88
|
return true;
|
|
86
89
|
}}>
|
|
87
|
-
<NativePressable key={cell.id} accessibilityActions={[{ name: 'activate' }]} accessibilityLabel={`Cell ${cell.id}`} accessibilityRole="button" accessibilityState={{ disabled: passive, selected }} disabled={passive} focusable={Platform.OS !== 'web' || canInteract} style={[
|
|
90
|
+
<NativePressable key={cell.id} accessibilityActions={[{ name: 'activate' }]} accessibilityLabel={`Cell ${cell.id}`} accessibilityRole="button" accessibilityState={{ disabled: passive || protectedCell, selected }} disabled={passive || protectedCell} focusable={!protectedCell && (Platform.OS !== 'web' || canInteract)} style={[
|
|
88
91
|
styles.cell,
|
|
89
92
|
{
|
|
90
93
|
borderColor: selected || keyboard.focusedId === cell.id ? selectedColor : dividerColor,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MatrixGrid.js","sourceRoot":"","sources":["../../../../../src/features/matrix-grid/adapters/inbound/MatrixGrid.tsx"],"names":[],"mappings":"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","sourcesContent":["import {\n getMatrixCellPlacement,\n getVisibleMatrixCells,\n type GridMatrixCellPlacement,\n type GridViewport,\n} from '@ankhorage/grid-view';\nimport { isRecord } from '@ankhorage/utility/object';\nimport React from 'react';\nimport { Platform, Pressable as NativePressable, StyleSheet, View } from 'react-native';\n\nimport type { MatrixGridProps } from '../../../../types/matrix-grid';\nimport { GridView } from '../../../grid-view/public';\nimport { resolveSelectionEventIntent } from '../../../selection/public';\nimport { withZoraThemeScope } from '../../../theme/adapters/inbound/withZoraThemeScope';\nimport { useZoraTheme } from '../../../theme/composition/useZoraTheme';\nimport { createMatrixGridSparseIndex } from '../../application/createMatrixGridSparseIndex';\nimport { resolveMatrixGridContentSize } from '../../application/resolveMatrixGridContentSize';\nimport { resolveMatrixGridSelection } from '../../application/resolveMatrixGridSelection';\nimport { MatrixGridKeyboardProxy } from './MatrixGridKeyboardProxy';\nimport { useMatrixGridKeyboardFocus } from './useMatrixGridKeyboardFocus';\n\n/*** Compose the canonical GridView around released sparse matrix placement and culling geometry. */\nfunction MatrixGridInner({\n accessibilityLabel = 'Matrix grid',\n cells,\n height,\n interactionPolicy,\n layout,\n onSelectionChange,\n onViewportChange,\n overscanPixels = 160,\n readOnly = false,\n renderCell,\n selectedCellIds = [],\n selectionMode = 'single',\n testID,\n viewport: controlledViewport,\n width,\n zoomX = 1,\n zoomY = 1,\n}: MatrixGridProps) {\n const { theme } = useZoraTheme();\n const contentSize = React.useMemo(() => resolveMatrixGridContentSize(layout), [layout]);\n const initialViewport = React.useMemo<GridViewport>(\n () => ({ height, offsetX: 0, offsetY: 0, pixelsPerUnitX: zoomX, pixelsPerUnitY: zoomY, width }),\n [height, width, zoomX, zoomY],\n );\n const [uncontrolledViewport, setUncontrolledViewport] = React.useState(initialViewport);\n const viewport = controlledViewport ?? uncontrolledViewport;\n const sparseIndex = React.useMemo(\n () => createMatrixGridSparseIndex(layout, cells),\n [cells, layout],\n );\n const cellPlacements = React.useMemo(\n () => getVisibleMatrixCells(layout, cells, viewport, overscanPixels),\n [cells, layout, overscanPixels, viewport],\n );\n const [visibleIds, setVisibleIds] = React.useState<readonly string[]>([]);\n const anchorId = React.useRef<string | null>(null);\n\n const handleViewportChange = React.useCallback(\n (proposal: GridViewport) => {\n if (controlledViewport === undefined) setUncontrolledViewport(proposal);\n onViewportChange?.(proposal);\n },\n [controlledViewport, onViewportChange],\n );\n\n const activateCell = React.useCallback(\n (cell: GridMatrixCellPlacement, event: unknown) => {\n if (interactionPolicy === 'passive' || readOnly) return;\n const intent = isRangeSelectionEvent(event)\n ? 'range'\n : resolveSelectionEventIntent(event, 'pointer');\n onSelectionChange?.(\n resolveMatrixGridSelection(\n sparseIndex.entries,\n selectedCellIds,\n cell.id,\n anchorId.current,\n intent,\n selectionMode,\n ),\n );\n if (intent !== 'range') anchorId.current = cell.id;\n },\n [\n interactionPolicy,\n onSelectionChange,\n readOnly,\n selectedCellIds,\n selectionMode,\n sparseIndex.entries,\n ],\n );\n const handleKeyboardNavigation = React.useCallback(\n (targetId: string, originId: string, shiftKey: boolean) => {\n anchorId.current ??= originId;\n if (!shiftKey) return;\n const cell = sparseIndex.cellById.get(targetId);\n if (cell) activateCell(getMatrixCellPlacement(layout, cell), { shiftKey: true });\n },\n [activateCell, layout, sparseIndex],\n );\n const keyboard = useMatrixGridKeyboardFocus(\n sparseIndex,\n interactionPolicy === 'passive',\n visibleIds,\n handleKeyboardNavigation,\n );\n const focusedPlacement = React.useMemo(() => {\n const focusedCell =\n keyboard.focusedId === null ? undefined : sparseIndex.cellById.get(keyboard.focusedId);\n return focusedCell ? getMatrixCellPlacement(layout, focusedCell) : undefined;\n }, [keyboard.focusedId, layout, sparseIndex.cellById]);\n const gridItems = React.useMemo(\n () =>\n focusedPlacement && !cellPlacements.some((cell) => cell.id === focusedPlacement.id)\n ? [...cellPlacements, focusedPlacement]\n : cellPlacements,\n [cellPlacements, focusedPlacement],\n );\n const cellById = React.useMemo(\n () => new Map(gridItems.map((cell) => [cell.id, cell])),\n [gridItems],\n );\n\n return (\n <View accessibilityLabel={accessibilityLabel}>\n <GridView\n contentHeight={contentSize.height}\n contentWidth={contentSize.width}\n height={height}\n interactionPolicy={interactionPolicy}\n items={gridItems}\n focusedItemId={keyboard.focusedId ?? undefined}\n overscanPixels={0}\n onVisibleItemIdsChange={setVisibleIds}\n renderItem={(item) => {\n const cell = cellById.get(item.id);\n if (!cell) return null;\n return renderMatrixCell(\n cell,\n selectedCellIds,\n theme.semantics.neutral.divider,\n theme.colors.primary,\n renderCell,\n activateCell,\n keyboard,\n interactionPolicy === 'passive',\n readOnly,\n testID,\n );\n }}\n testID={testID}\n viewport={viewport}\n width={width}\n onViewportChange={handleViewportChange}\n />\n </View>\n );\n}\n\n/*** Render one positioned sparse cell with native/web accessible selection semantics. */\nfunction renderMatrixCell(\n cell: GridMatrixCellPlacement,\n selectedCellIds: readonly string[],\n dividerColor: string,\n selectedColor: string,\n renderCell: MatrixGridProps['renderCell'],\n activateCell: (cell: GridMatrixCellPlacement, event: unknown) => void,\n keyboard: ReturnType<typeof useMatrixGridKeyboardFocus>,\n passive: boolean,\n readOnly: boolean,\n testID: string | undefined,\n) {\n const selected = selectedCellIds.includes(cell.id);\n const canInteract = keyboard.tabStopId === cell.id;\n return (\n <MatrixGridKeyboardProxy\n onKeyDown={(event) => {\n if (keyboard.onKeyDown(cell.id, event.key, event.shiftKey)) return true;\n if (!['Enter', ' '].includes(event.key)) return false;\n if (passive || readOnly) return false;\n activateCell(cell, event);\n return true;\n }}\n >\n <NativePressable\n key={cell.id}\n accessibilityActions={[{ name: 'activate' }]}\n accessibilityLabel={`Cell ${cell.id}`}\n accessibilityRole=\"button\"\n accessibilityState={{ disabled: passive, selected }}\n disabled={passive}\n focusable={Platform.OS !== 'web' || canInteract}\n style={[\n styles.cell,\n {\n borderColor: selected || keyboard.focusedId === cell.id ? selectedColor : dividerColor,\n height: '100%',\n width: '100%',\n },\n ]}\n testID={testID === undefined ? undefined : `${testID}-cell-${cell.id}`}\n onAccessibilityAction={() => activateCell(cell, {})}\n onFocus={() => keyboard.onFocus(cell.id)}\n onPress={(event) => activateCell(cell, event)}\n ref={(node) => keyboard.registerCell(cell.id, node)}\n >\n {renderCell(cell)}\n </NativePressable>\n </MatrixGridKeyboardProxy>\n );\n}\n\n/*** Recognize range intent without changing the shared replace-or-toggle selection contract. */\nfunction isRangeSelectionEvent(event: unknown): boolean {\n if (!isRecord(event)) return false;\n if (event.shiftKey === true) return true;\n return (\n (isRecord(event.nativeEvent) && event.nativeEvent.shiftKey === true) ||\n (isRecord(event.originalEvent) && event.originalEvent.shiftKey === true)\n );\n}\n\nconst styles = StyleSheet.create({\n cell: { borderWidth: 1, height: '100%', overflow: 'hidden', width: '100%' },\n});\n\n/*** A code-first generic sparse matrix grid; formulas and domain vocabulary remain external. */\nexport const MatrixGrid = withZoraThemeScope(MatrixGridInner);\n"]}
|
|
1
|
+
{"version":3,"file":"MatrixGrid.js","sourceRoot":"","sources":["../../../../../src/features/matrix-grid/adapters/inbound/MatrixGrid.tsx"],"names":[],"mappings":"AAAA,OAAO,EACL,sBAAsB,EACtB,qBAAqB,GAGtB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AACrD,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,QAAQ,EAAE,SAAS,IAAI,eAAe,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAGxF,OAAO,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AACrD,OAAO,EAAE,2BAA2B,EAAE,MAAM,2BAA2B,CAAC;AACxE,OAAO,EAAE,kBAAkB,EAAE,MAAM,oDAAoD,CAAC;AACxF,OAAO,EAAE,YAAY,EAAE,MAAM,yCAAyC,CAAC;AACvE,OAAO,EAAE,2BAA2B,EAAE,MAAM,+CAA+C,CAAC;AAC5F,OAAO,EAAE,4BAA4B,EAAE,MAAM,gDAAgD,CAAC;AAC9F,OAAO,EAAE,0BAA0B,EAAE,MAAM,8CAA8C,CAAC;AAC1F,OAAO,EAAE,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AACpE,OAAO,EAAE,0BAA0B,EAAE,MAAM,8BAA8B,CAAC;AAE1E,oGAAoG;AACpG,SAAS,eAAe,CAAC,EACvB,kBAAkB,GAAG,aAAa,EAClC,KAAK,EACL,MAAM,EACN,iBAAiB,EACjB,MAAM,EACN,iBAAiB,EACjB,gBAAgB,EAChB,cAAc,GAAG,GAAG,EACpB,gBAAgB,GAAG,EAAE,EACrB,QAAQ,GAAG,KAAK,EAChB,UAAU,EACV,eAAe,GAAG,EAAE,EACpB,aAAa,GAAG,QAAQ,EACxB,MAAM,EACN,QAAQ,EAAE,kBAAkB,EAC5B,KAAK,EACL,KAAK,GAAG,CAAC,EACT,KAAK,GAAG,CAAC,GACO;IAChB,MAAM,EAAE,KAAK,EAAE,GAAG,YAAY,EAAE,CAAC;IACjC,MAAM,WAAW,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,4BAA4B,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;IACxF,MAAM,eAAe,GAAG,KAAK,CAAC,OAAO,CACnC,GAAG,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,cAAc,EAAE,KAAK,EAAE,cAAc,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,EAC/F,CAAC,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,CAC9B,CAAC;IACF,MAAM,CAAC,oBAAoB,EAAE,uBAAuB,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAC;IACxF,MAAM,QAAQ,GAAG,kBAAkB,IAAI,oBAAoB,CAAC;IAC5D,MAAM,WAAW,GAAG,KAAK,CAAC,OAAO,CAC/B,GAAG,EAAE,CAAC,2BAA2B,CAAC,MAAM,EAAE,KAAK,CAAC,EAChD,CAAC,KAAK,EAAE,MAAM,CAAC,CAChB,CAAC;IACF,MAAM,cAAc,GAAG,KAAK,CAAC,OAAO,CAClC,GAAG,EAAE,CAAC,qBAAqB,CAAC,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,cAAc,CAAC,EACpE,CAAC,KAAK,EAAE,MAAM,EAAE,cAAc,EAAE,QAAQ,CAAC,CAC1C,CAAC;IACF,MAAM,CAAC,UAAU,EAAE,aAAa,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAoB,EAAE,CAAC,CAAC;IAC1E,MAAM,QAAQ,GAAG,KAAK,CAAC,MAAM,CAAgB,IAAI,CAAC,CAAC;IACnD,MAAM,kBAAkB,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,IAAI,GAAG,CAAC,gBAAgB,CAAC,EAAE,CAAC,gBAAgB,CAAC,CAAC,CAAC;IAC9F,MAAM,WAAW,GAAG,KAAK,CAAC,WAAW,CACnC,CAAC,EAAU,EAAE,EAAE,CAAC,CAAC,kBAAkB,CAAC,GAAG,CAAC,EAAE,CAAC,EAC3C,CAAC,kBAAkB,CAAC,CACrB,CAAC;IAEF,MAAM,oBAAoB,GAAG,KAAK,CAAC,WAAW,CAC5C,CAAC,QAAsB,EAAE,EAAE;QACzB,IAAI,kBAAkB,KAAK,SAAS;YAAE,uBAAuB,CAAC,QAAQ,CAAC,CAAC;QACxE,gBAAgB,EAAE,CAAC,QAAQ,CAAC,CAAC;IAC/B,CAAC,EACD,CAAC,kBAAkB,EAAE,gBAAgB,CAAC,CACvC,CAAC;IAEF,MAAM,YAAY,GAAG,KAAK,CAAC,WAAW,CACpC,CAAC,IAA6B,EAAE,KAAc,EAAE,EAAE;QAChD,IAAI,iBAAiB,KAAK,SAAS,IAAI,QAAQ,IAAI,kBAAkB,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;YAAE,OAAO;QAC3F,MAAM,MAAM,GAAG,qBAAqB,CAAC,KAAK,CAAC;YACzC,CAAC,CAAC,OAAO;YACT,CAAC,CAAC,2BAA2B,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;QAClD,iBAAiB,EAAE,CACjB,0BAA0B,CACxB,WAAW,CAAC,OAAO,EACnB,eAAe,EACf,IAAI,CAAC,EAAE,EACP,QAAQ,CAAC,OAAO,EAChB,MAAM,EACN,aAAa,EACb,kBAAkB,CACnB,CACF,CAAC;QACF,IAAI,MAAM,KAAK,OAAO;YAAE,QAAQ,CAAC,OAAO,GAAG,IAAI,CAAC,EAAE,CAAC;IACrD,CAAC,EACD;QACE,iBAAiB;QACjB,iBAAiB;QACjB,QAAQ;QACR,eAAe;QACf,aAAa;QACb,WAAW,CAAC,OAAO;QACnB,kBAAkB;KACnB,CACF,CAAC;IACF,MAAM,wBAAwB,GAAG,KAAK,CAAC,WAAW,CAChD,CAAC,QAAgB,EAAE,QAAgB,EAAE,QAAiB,EAAE,EAAE;QACxD,QAAQ,CAAC,OAAO,KAAK,QAAQ,CAAC;QAC9B,IAAI,CAAC,QAAQ;YAAE,OAAO;QACtB,MAAM,IAAI,GAAG,WAAW,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAChD,IAAI,IAAI;YAAE,YAAY,CAAC,sBAAsB,CAAC,MAAM,EAAE,IAAI,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;IACnF,CAAC,EACD,CAAC,YAAY,EAAE,MAAM,EAAE,WAAW,CAAC,CACpC,CAAC;IACF,MAAM,QAAQ,GAAG,0BAA0B,CACzC,WAAW,EACX,iBAAiB,KAAK,SAAS,EAC/B,UAAU,EACV,WAAW,EACX,wBAAwB,CACzB,CAAC;IACF,MAAM,gBAAgB,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE;QAC1C,MAAM,WAAW,GACf,QAAQ,CAAC,SAAS,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;QACzF,OAAO,WAAW,CAAC,CAAC,CAAC,sBAAsB,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAC/E,CAAC,EAAE,CAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAC,CAAC;IACvD,MAAM,SAAS,GAAG,KAAK,CAAC,OAAO,CAC7B,GAAG,EAAE,CACH,gBAAgB,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,KAAK,gBAAgB,CAAC,EAAE,CAAC;QACjF,CAAC,CAAC,CAAC,GAAG,cAAc,EAAE,gBAAgB,CAAC;QACvC,CAAC,CAAC,cAAc,EACpB,CAAC,cAAc,EAAE,gBAAgB,CAAC,CACnC,CAAC;IACF,MAAM,QAAQ,GAAG,KAAK,CAAC,OAAO,CAC5B,GAAG,EAAE,CAAC,IAAI,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC,CAAC,EACvD,CAAC,SAAS,CAAC,CACZ,CAAC;IAEF,OAAO,CACL,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC,kBAAkB,CAAC,CAC3C;MAAA,CAAC,QAAQ,CACP,aAAa,CAAC,CAAC,WAAW,CAAC,MAAM,CAAC,CAClC,YAAY,CAAC,CAAC,WAAW,CAAC,KAAK,CAAC,CAChC,MAAM,CAAC,CAAC,MAAM,CAAC,CACf,iBAAiB,CAAC,CAAC,iBAAiB,CAAC,CACrC,KAAK,CAAC,CAAC,SAAS,CAAC,CACjB,aAAa,CAAC,CAAC,QAAQ,CAAC,SAAS,IAAI,SAAS,CAAC,CAC/C,cAAc,CAAC,CAAC,CAAC,CAAC,CAClB,sBAAsB,CAAC,CAAC,aAAa,CAAC,CACtC,UAAU,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE;YACnB,MAAM,IAAI,GAAG,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YACnC,IAAI,CAAC,IAAI;gBAAE,OAAO,IAAI,CAAC;YACvB,OAAO,gBAAgB,CACrB,IAAI,EACJ,eAAe,EACf,KAAK,CAAC,SAAS,CAAC,OAAO,CAAC,OAAO,EAC/B,KAAK,CAAC,MAAM,CAAC,OAAO,EACpB,UAAU,EACV,YAAY,EACZ,QAAQ,EACR,iBAAiB,KAAK,SAAS,EAC/B,kBAAkB,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,EAC/B,QAAQ,EACR,MAAM,CACP,CAAC;QACJ,CAAC,CAAC,CACF,MAAM,CAAC,CAAC,MAAM,CAAC,CACf,QAAQ,CAAC,CAAC,QAAQ,CAAC,CACnB,KAAK,CAAC,CAAC,KAAK,CAAC,CACb,gBAAgB,CAAC,CAAC,oBAAoB,CAAC,EAE3C;IAAA,EAAE,IAAI,CAAC,CACR,CAAC;AACJ,CAAC;AAED,yFAAyF;AACzF,SAAS,gBAAgB,CACvB,IAA6B,EAC7B,eAAkC,EAClC,YAAoB,EACpB,aAAqB,EACrB,UAAyC,EACzC,YAAqE,EACrE,QAAuD,EACvD,OAAgB,EAChB,aAAsB,EACtB,QAAiB,EACjB,MAA0B;IAE1B,MAAM,QAAQ,GAAG,eAAe,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACnD,MAAM,WAAW,GAAG,QAAQ,CAAC,SAAS,KAAK,IAAI,CAAC,EAAE,CAAC;IACnD,OAAO,CACL,CAAC,uBAAuB,CACtB,SAAS,CAAC,CAAC,CAAC,KAAK,EAAE,EAAE;YACnB,IAAI,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,EAAE,KAAK,CAAC,GAAG,EAAE,KAAK,CAAC,QAAQ,CAAC;gBAAE,OAAO,IAAI,CAAC;YACxE,IAAI,CAAC,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC;gBAAE,OAAO,KAAK,CAAC;YACtD,IAAI,OAAO,IAAI,aAAa,IAAI,QAAQ;gBAAE,OAAO,KAAK,CAAC;YACvD,YAAY,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;YAC1B,OAAO,IAAI,CAAC;QACd,CAAC,CAAC,CAEF;MAAA,CAAC,eAAe,CACd,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CACb,oBAAoB,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,CAAC,CAC7C,kBAAkB,CAAC,CAAC,QAAQ,IAAI,CAAC,EAAE,EAAE,CAAC,CACtC,iBAAiB,CAAC,QAAQ,CAC1B,kBAAkB,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,IAAI,aAAa,EAAE,QAAQ,EAAE,CAAC,CACrE,QAAQ,CAAC,CAAC,OAAO,IAAI,aAAa,CAAC,CACnC,SAAS,CAAC,CAAC,CAAC,aAAa,IAAI,CAAC,QAAQ,CAAC,EAAE,KAAK,KAAK,IAAI,WAAW,CAAC,CAAC,CACpE,KAAK,CAAC,CAAC;YACL,MAAM,CAAC,IAAI;YACX;gBACE,WAAW,EAAE,QAAQ,IAAI,QAAQ,CAAC,SAAS,KAAK,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,YAAY;gBACtF,MAAM,EAAE,MAAM;gBACd,KAAK,EAAE,MAAM;aACd;SACF,CAAC,CACF,MAAM,CAAC,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,MAAM,SAAS,IAAI,CAAC,EAAE,EAAE,CAAC,CACvE,qBAAqB,CAAC,CAAC,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CACpD,OAAO,CAAC,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CACzC,OAAO,CAAC,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAC9C,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,QAAQ,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC,CAEpD;QAAA,CAAC,UAAU,CAAC,IAAI,CAAC,CACnB;MAAA,EAAE,eAAe,CACnB;IAAA,EAAE,uBAAuB,CAAC,CAC3B,CAAC;AACJ,CAAC;AAED,gGAAgG;AAChG,SAAS,qBAAqB,CAAC,KAAc;IAC3C,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACnC,IAAI,KAAK,CAAC,QAAQ,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IACzC,OAAO,CACL,CAAC,QAAQ,CAAC,KAAK,CAAC,WAAW,CAAC,IAAI,KAAK,CAAC,WAAW,CAAC,QAAQ,KAAK,IAAI,CAAC;QACpE,CAAC,QAAQ,CAAC,KAAK,CAAC,aAAa,CAAC,IAAI,KAAK,CAAC,aAAa,CAAC,QAAQ,KAAK,IAAI,CAAC,CACzE,CAAC;AACJ,CAAC;AAED,MAAM,MAAM,GAAG,UAAU,CAAC,MAAM,CAAC;IAC/B,IAAI,EAAE,EAAE,WAAW,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE;CAC5E,CAAC,CAAC;AAEH,gGAAgG;AAChG,MAAM,CAAC,MAAM,UAAU,GAAG,kBAAkB,CAAC,eAAe,CAAC,CAAC","sourcesContent":["import {\n getMatrixCellPlacement,\n getVisibleMatrixCells,\n type GridMatrixCellPlacement,\n type GridViewport,\n} from '@ankhorage/grid-view';\nimport { isRecord } from '@ankhorage/utility/object';\nimport React from 'react';\nimport { Platform, Pressable as NativePressable, StyleSheet, View } from 'react-native';\n\nimport type { MatrixGridProps } from '../../../../types/matrix-grid';\nimport { GridView } from '../../../grid-view/public';\nimport { resolveSelectionEventIntent } from '../../../selection/public';\nimport { withZoraThemeScope } from '../../../theme/adapters/inbound/withZoraThemeScope';\nimport { useZoraTheme } from '../../../theme/composition/useZoraTheme';\nimport { createMatrixGridSparseIndex } from '../../application/createMatrixGridSparseIndex';\nimport { resolveMatrixGridContentSize } from '../../application/resolveMatrixGridContentSize';\nimport { resolveMatrixGridSelection } from '../../application/resolveMatrixGridSelection';\nimport { MatrixGridKeyboardProxy } from './MatrixGridKeyboardProxy';\nimport { useMatrixGridKeyboardFocus } from './useMatrixGridKeyboardFocus';\n\n/*** Compose the canonical GridView around released sparse matrix placement and culling geometry. */\nfunction MatrixGridInner({\n accessibilityLabel = 'Matrix grid',\n cells,\n height,\n interactionPolicy,\n layout,\n onSelectionChange,\n onViewportChange,\n overscanPixels = 160,\n protectedCellIds = [],\n readOnly = false,\n renderCell,\n selectedCellIds = [],\n selectionMode = 'single',\n testID,\n viewport: controlledViewport,\n width,\n zoomX = 1,\n zoomY = 1,\n}: MatrixGridProps) {\n const { theme } = useZoraTheme();\n const contentSize = React.useMemo(() => resolveMatrixGridContentSize(layout), [layout]);\n const initialViewport = React.useMemo<GridViewport>(\n () => ({ height, offsetX: 0, offsetY: 0, pixelsPerUnitX: zoomX, pixelsPerUnitY: zoomY, width }),\n [height, width, zoomX, zoomY],\n );\n const [uncontrolledViewport, setUncontrolledViewport] = React.useState(initialViewport);\n const viewport = controlledViewport ?? uncontrolledViewport;\n const sparseIndex = React.useMemo(\n () => createMatrixGridSparseIndex(layout, cells),\n [cells, layout],\n );\n const cellPlacements = React.useMemo(\n () => getVisibleMatrixCells(layout, cells, viewport, overscanPixels),\n [cells, layout, overscanPixels, viewport],\n );\n const [visibleIds, setVisibleIds] = React.useState<readonly string[]>([]);\n const anchorId = React.useRef<string | null>(null);\n const protectedCellIdSet = React.useMemo(() => new Set(protectedCellIds), [protectedCellIds]);\n const isNavigable = React.useCallback(\n (id: string) => !protectedCellIdSet.has(id),\n [protectedCellIdSet],\n );\n\n const handleViewportChange = React.useCallback(\n (proposal: GridViewport) => {\n if (controlledViewport === undefined) setUncontrolledViewport(proposal);\n onViewportChange?.(proposal);\n },\n [controlledViewport, onViewportChange],\n );\n\n const activateCell = React.useCallback(\n (cell: GridMatrixCellPlacement, event: unknown) => {\n if (interactionPolicy === 'passive' || readOnly || protectedCellIdSet.has(cell.id)) return;\n const intent = isRangeSelectionEvent(event)\n ? 'range'\n : resolveSelectionEventIntent(event, 'pointer');\n onSelectionChange?.(\n resolveMatrixGridSelection(\n sparseIndex.entries,\n selectedCellIds,\n cell.id,\n anchorId.current,\n intent,\n selectionMode,\n protectedCellIdSet,\n ),\n );\n if (intent !== 'range') anchorId.current = cell.id;\n },\n [\n interactionPolicy,\n onSelectionChange,\n readOnly,\n selectedCellIds,\n selectionMode,\n sparseIndex.entries,\n protectedCellIdSet,\n ],\n );\n const handleKeyboardNavigation = React.useCallback(\n (targetId: string, originId: string, shiftKey: boolean) => {\n anchorId.current ??= originId;\n if (!shiftKey) return;\n const cell = sparseIndex.cellById.get(targetId);\n if (cell) activateCell(getMatrixCellPlacement(layout, cell), { shiftKey: true });\n },\n [activateCell, layout, sparseIndex],\n );\n const keyboard = useMatrixGridKeyboardFocus(\n sparseIndex,\n interactionPolicy === 'passive',\n visibleIds,\n isNavigable,\n handleKeyboardNavigation,\n );\n const focusedPlacement = React.useMemo(() => {\n const focusedCell =\n keyboard.focusedId === null ? undefined : sparseIndex.cellById.get(keyboard.focusedId);\n return focusedCell ? getMatrixCellPlacement(layout, focusedCell) : undefined;\n }, [keyboard.focusedId, layout, sparseIndex.cellById]);\n const gridItems = React.useMemo(\n () =>\n focusedPlacement && !cellPlacements.some((cell) => cell.id === focusedPlacement.id)\n ? [...cellPlacements, focusedPlacement]\n : cellPlacements,\n [cellPlacements, focusedPlacement],\n );\n const cellById = React.useMemo(\n () => new Map(gridItems.map((cell) => [cell.id, cell])),\n [gridItems],\n );\n\n return (\n <View accessibilityLabel={accessibilityLabel}>\n <GridView\n contentHeight={contentSize.height}\n contentWidth={contentSize.width}\n height={height}\n interactionPolicy={interactionPolicy}\n items={gridItems}\n focusedItemId={keyboard.focusedId ?? undefined}\n overscanPixels={0}\n onVisibleItemIdsChange={setVisibleIds}\n renderItem={(item) => {\n const cell = cellById.get(item.id);\n if (!cell) return null;\n return renderMatrixCell(\n cell,\n selectedCellIds,\n theme.semantics.neutral.divider,\n theme.colors.primary,\n renderCell,\n activateCell,\n keyboard,\n interactionPolicy === 'passive',\n protectedCellIdSet.has(cell.id),\n readOnly,\n testID,\n );\n }}\n testID={testID}\n viewport={viewport}\n width={width}\n onViewportChange={handleViewportChange}\n />\n </View>\n );\n}\n\n/*** Render one positioned sparse cell with native/web accessible selection semantics. */\nfunction renderMatrixCell(\n cell: GridMatrixCellPlacement,\n selectedCellIds: readonly string[],\n dividerColor: string,\n selectedColor: string,\n renderCell: MatrixGridProps['renderCell'],\n activateCell: (cell: GridMatrixCellPlacement, event: unknown) => void,\n keyboard: ReturnType<typeof useMatrixGridKeyboardFocus>,\n passive: boolean,\n protectedCell: boolean,\n readOnly: boolean,\n testID: string | undefined,\n) {\n const selected = selectedCellIds.includes(cell.id);\n const canInteract = keyboard.tabStopId === cell.id;\n return (\n <MatrixGridKeyboardProxy\n onKeyDown={(event) => {\n if (keyboard.onKeyDown(cell.id, event.key, event.shiftKey)) return true;\n if (!['Enter', ' '].includes(event.key)) return false;\n if (passive || protectedCell || readOnly) return false;\n activateCell(cell, event);\n return true;\n }}\n >\n <NativePressable\n key={cell.id}\n accessibilityActions={[{ name: 'activate' }]}\n accessibilityLabel={`Cell ${cell.id}`}\n accessibilityRole=\"button\"\n accessibilityState={{ disabled: passive || protectedCell, selected }}\n disabled={passive || protectedCell}\n focusable={!protectedCell && (Platform.OS !== 'web' || canInteract)}\n style={[\n styles.cell,\n {\n borderColor: selected || keyboard.focusedId === cell.id ? selectedColor : dividerColor,\n height: '100%',\n width: '100%',\n },\n ]}\n testID={testID === undefined ? undefined : `${testID}-cell-${cell.id}`}\n onAccessibilityAction={() => activateCell(cell, {})}\n onFocus={() => keyboard.onFocus(cell.id)}\n onPress={(event) => activateCell(cell, event)}\n ref={(node) => keyboard.registerCell(cell.id, node)}\n >\n {renderCell(cell)}\n </NativePressable>\n </MatrixGridKeyboardProxy>\n );\n}\n\n/*** Recognize range intent without changing the shared replace-or-toggle selection contract. */\nfunction isRangeSelectionEvent(event: unknown): boolean {\n if (!isRecord(event)) return false;\n if (event.shiftKey === true) return true;\n return (\n (isRecord(event.nativeEvent) && event.nativeEvent.shiftKey === true) ||\n (isRecord(event.originalEvent) && event.originalEvent.shiftKey === true)\n );\n}\n\nconst styles = StyleSheet.create({\n cell: { borderWidth: 1, height: '100%', overflow: 'hidden', width: '100%' },\n});\n\n/*** A code-first generic sparse matrix grid; formulas and domain vocabulary remain external. */\nexport const MatrixGrid = withZoraThemeScope(MatrixGridInner);\n"]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { MatrixGridSparseIndex } from '../../application/createMatrixGridSparseIndex';
|
|
2
2
|
/*** Keep one roving focus target and restore it after GridView virtualizes a sparse cell. */
|
|
3
|
-
export declare function useMatrixGridKeyboardFocus(index: MatrixGridSparseIndex, disabled: boolean, visibleIds: readonly string[], onNavigate: (targetId: string, originId: string, shiftKey: boolean) => void): {
|
|
3
|
+
export declare function useMatrixGridKeyboardFocus(index: MatrixGridSparseIndex, disabled: boolean, visibleIds: readonly string[], isNavigable: (id: string) => boolean, onNavigate: (targetId: string, originId: string, shiftKey: boolean) => void): {
|
|
4
4
|
focusedId: string | null;
|
|
5
5
|
onFocus: (id: string) => void;
|
|
6
6
|
onKeyDown: (id: string, key: string, shiftKey: boolean) => boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useMatrixGridKeyboardFocus.d.ts","sourceRoot":"","sources":["../../../../../src/features/matrix-grid/adapters/inbound/useMatrixGridKeyboardFocus.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,+CAA+C,CAAC;AAG3F,6FAA6F;AAC7F,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,qBAAqB,EAC5B,QAAQ,EAAE,OAAO,EACjB,UAAU,EAAE,SAAS,MAAM,EAAE,EAC7B,UAAU,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,KAAK,IAAI;;
|
|
1
|
+
{"version":3,"file":"useMatrixGridKeyboardFocus.d.ts","sourceRoot":"","sources":["../../../../../src/features/matrix-grid/adapters/inbound/useMatrixGridKeyboardFocus.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,+CAA+C,CAAC;AAG3F,6FAA6F;AAC7F,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,qBAAqB,EAC5B,QAAQ,EAAE,OAAO,EACjB,UAAU,EAAE,SAAS,MAAM,EAAE,EAC7B,WAAW,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,EACpC,UAAU,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,KAAK,IAAI;;kBAsBpC,MAAM;oBAmBtC,MAAM,OAAO,MAAM,YAAY,OAAO,KAAG,OAAO;uBAbX,MAAM,QAAQ,aAAa,GAAG,IAAI;;EAqC/E;AAkBD,UAAU,aAAa;IACrB,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB"}
|
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { getMatrixGridNextFocusId } from '../../application/getMatrixGridNextFocusId';
|
|
3
3
|
/*** Keep one roving focus target and restore it after GridView virtualizes a sparse cell. */
|
|
4
|
-
export function useMatrixGridKeyboardFocus(index, disabled, visibleIds, onNavigate) {
|
|
4
|
+
export function useMatrixGridKeyboardFocus(index, disabled, visibleIds, isNavigable, onNavigate) {
|
|
5
5
|
const [focusedId, setFocusedId] = React.useState(null);
|
|
6
6
|
const focusedRef = React.useRef(null);
|
|
7
7
|
const pendingFocusRef = React.useRef(null);
|
|
8
8
|
const cellRefs = React.useRef(new Map());
|
|
9
9
|
const visibleIdSet = React.useMemo(() => new Set(visibleIds), [visibleIds]);
|
|
10
|
-
const tabStopId = focusedId !== null && visibleIdSet.has(focusedId)
|
|
10
|
+
const tabStopId = focusedId !== null && visibleIdSet.has(focusedId) && isNavigable(focusedId)
|
|
11
11
|
? focusedId
|
|
12
|
-
: visibleIds.find((id) => index.positionById.has(id));
|
|
12
|
+
: visibleIds.find((id) => index.positionById.has(id) && isNavigable(id));
|
|
13
|
+
React.useEffect(() => {
|
|
14
|
+
if (focusedRef.current !== null && !isNavigable(focusedRef.current)) {
|
|
15
|
+
focusedRef.current = null;
|
|
16
|
+
setFocusedId(null);
|
|
17
|
+
}
|
|
18
|
+
if (pendingFocusRef.current !== null && !isNavigable(pendingFocusRef.current)) {
|
|
19
|
+
pendingFocusRef.current = null;
|
|
20
|
+
}
|
|
21
|
+
}, [isNavigable]);
|
|
13
22
|
const onFocus = React.useCallback((id) => {
|
|
14
23
|
pendingFocusRef.current = null;
|
|
15
24
|
focusedRef.current = id;
|
|
@@ -31,7 +40,7 @@ export function useMatrixGridKeyboardFocus(index, disabled, visibleIds, onNaviga
|
|
|
31
40
|
return false;
|
|
32
41
|
}
|
|
33
42
|
const originId = focusedRef.current ?? id;
|
|
34
|
-
const nextId =
|
|
43
|
+
const nextId = resolveNextNavigableId(index, originId, key, isNavigable);
|
|
35
44
|
if (nextId === null || nextId === originId)
|
|
36
45
|
return true;
|
|
37
46
|
focusedRef.current = nextId;
|
|
@@ -44,7 +53,18 @@ export function useMatrixGridKeyboardFocus(index, disabled, visibleIds, onNaviga
|
|
|
44
53
|
mounted.focus();
|
|
45
54
|
}
|
|
46
55
|
return true;
|
|
47
|
-
}, [disabled, index, onNavigate]);
|
|
56
|
+
}, [disabled, index, isNavigable, onNavigate]);
|
|
48
57
|
return { focusedId, onFocus, onKeyDown, registerCell, tabStopId };
|
|
49
58
|
}
|
|
59
|
+
/*** Skip protected sparse cells while retaining the nearest deterministic directional navigation. */
|
|
60
|
+
function resolveNextNavigableId(index, originId, key, isNavigable) {
|
|
61
|
+
let candidate = getMatrixGridNextFocusId(index, originId, key);
|
|
62
|
+
while (candidate !== null && candidate !== originId && !isNavigable(candidate)) {
|
|
63
|
+
const nextCandidate = getMatrixGridNextFocusId(index, candidate, key);
|
|
64
|
+
if (nextCandidate === candidate)
|
|
65
|
+
return originId;
|
|
66
|
+
candidate = nextCandidate;
|
|
67
|
+
}
|
|
68
|
+
return candidate;
|
|
69
|
+
}
|
|
50
70
|
//# sourceMappingURL=useMatrixGridKeyboardFocus.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useMatrixGridKeyboardFocus.js","sourceRoot":"","sources":["../../../../../src/features/matrix-grid/adapters/inbound/useMatrixGridKeyboardFocus.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B,OAAO,EAAE,wBAAwB,EAAE,MAAM,4CAA4C,CAAC;AAEtF,6FAA6F;AAC7F,MAAM,UAAU,0BAA0B,CACxC,KAA4B,EAC5B,QAAiB,EACjB,UAA6B,EAC7B,UAA2E;IAE3E,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAgB,IAAI,CAAC,CAAC;IACtE,MAAM,UAAU,GAAG,KAAK,CAAC,MAAM,CAAgB,IAAI,CAAC,CAAC;IACrD,MAAM,eAAe,GAAG,KAAK,CAAC,MAAM,CAAgB,IAAI,CAAC,CAAC;IAC1D,MAAM,QAAQ,GAAG,KAAK,CAAC,MAAM,CAAC,IAAI,GAAG,EAAyB,CAAC,CAAC;IAChE,MAAM,YAAY,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,IAAI,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC;IAC5E,MAAM,SAAS,GACb,SAAS,KAAK,IAAI,IAAI,YAAY,CAAC,GAAG,CAAC,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"useMatrixGridKeyboardFocus.js","sourceRoot":"","sources":["../../../../../src/features/matrix-grid/adapters/inbound/useMatrixGridKeyboardFocus.ts"],"names":[],"mappings":"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","sourcesContent":["import React from 'react';\n\nimport type { MatrixGridSparseIndex } from '../../application/createMatrixGridSparseIndex';\nimport { getMatrixGridNextFocusId } from '../../application/getMatrixGridNextFocusId';\n\n/*** Keep one roving focus target and restore it after GridView virtualizes a sparse cell. */\nexport function useMatrixGridKeyboardFocus(\n index: MatrixGridSparseIndex,\n disabled: boolean,\n visibleIds: readonly string[],\n isNavigable: (id: string) => boolean,\n onNavigate: (targetId: string, originId: string, shiftKey: boolean) => void,\n) {\n const [focusedId, setFocusedId] = React.useState<string | null>(null);\n const focusedRef = React.useRef<string | null>(null);\n const pendingFocusRef = React.useRef<string | null>(null);\n const cellRefs = React.useRef(new Map<string, FocusableCell>());\n const visibleIdSet = React.useMemo(() => new Set(visibleIds), [visibleIds]);\n const tabStopId =\n focusedId !== null && visibleIdSet.has(focusedId) && isNavigable(focusedId)\n ? focusedId\n : visibleIds.find((id) => index.positionById.has(id) && isNavigable(id));\n\n React.useEffect(() => {\n if (focusedRef.current !== null && !isNavigable(focusedRef.current)) {\n focusedRef.current = null;\n setFocusedId(null);\n }\n if (pendingFocusRef.current !== null && !isNavigable(pendingFocusRef.current)) {\n pendingFocusRef.current = null;\n }\n }, [isNavigable]);\n\n const onFocus = React.useCallback((id: string) => {\n pendingFocusRef.current = null;\n focusedRef.current = id;\n setFocusedId(id);\n }, []);\n\n const registerCell = React.useCallback((id: string, node: FocusableCell | null) => {\n if (node === null) {\n cellRefs.current.delete(id);\n return;\n }\n cellRefs.current.set(id, node);\n if (pendingFocusRef.current === id) {\n pendingFocusRef.current = null;\n node.focus();\n }\n }, []);\n\n const onKeyDown = React.useCallback(\n (id: string, key: string, shiftKey: boolean): boolean => {\n if (disabled || !['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(key)) {\n return false;\n }\n const originId = focusedRef.current ?? id;\n const nextId = resolveNextNavigableId(index, originId, key, isNavigable);\n if (nextId === null || nextId === originId) return true;\n\n focusedRef.current = nextId;\n pendingFocusRef.current = nextId;\n setFocusedId(nextId);\n onNavigate(nextId, originId, shiftKey);\n\n const mounted = cellRefs.current.get(nextId);\n if (mounted) {\n pendingFocusRef.current = null;\n mounted.focus();\n }\n return true;\n },\n [disabled, index, isNavigable, onNavigate],\n );\n\n return { focusedId, onFocus, onKeyDown, registerCell, tabStopId };\n}\n\n/*** Skip protected sparse cells while retaining the nearest deterministic directional navigation. */\nfunction resolveNextNavigableId(\n index: MatrixGridSparseIndex,\n originId: string,\n key: string,\n isNavigable: (id: string) => boolean,\n): string | null {\n let candidate = getMatrixGridNextFocusId(index, originId, key);\n while (candidate !== null && candidate !== originId && !isNavigable(candidate)) {\n const nextCandidate = getMatrixGridNextFocusId(index, candidate, key);\n if (nextCandidate === candidate) return originId;\n candidate = nextCandidate;\n }\n return candidate;\n}\n\ninterface FocusableCell {\n focus: () => void;\n}\n"]}
|
|
@@ -2,7 +2,7 @@ import { type SelectionIntent } from '@ankhorage/utility/selection';
|
|
|
2
2
|
import type { SelectionMode } from '../../../types/selection';
|
|
3
3
|
type MatrixGridSelectionIntent = SelectionIntent | 'range';
|
|
4
4
|
/*** Apply ZORA's canonical selection intent while retaining the configured selection mode. */
|
|
5
|
-
export declare function resolveMatrixGridSelection(cells: readonly MatrixGridSelectionCell[], selectedIds: readonly string[], cellId: string, anchorId: string | null, intent: MatrixGridSelectionIntent, mode: SelectionMode): readonly string[];
|
|
5
|
+
export declare function resolveMatrixGridSelection(cells: readonly MatrixGridSelectionCell[], selectedIds: readonly string[], cellId: string, anchorId: string | null, intent: MatrixGridSelectionIntent, mode: SelectionMode, protectedCellIds: ReadonlySet<string>): readonly string[];
|
|
6
6
|
type MatrixGridSelectionCell = Readonly<{
|
|
7
7
|
columnIndex: number;
|
|
8
8
|
id: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolveMatrixGridSelection.d.ts","sourceRoot":"","sources":["../../../../src/features/matrix-grid/application/resolveMatrixGridSelection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwB,KAAK,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAE1F,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAE9D,KAAK,yBAAyB,GAAG,eAAe,GAAG,OAAO,CAAC;AAE3D,8FAA8F;AAC9F,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,SAAS,uBAAuB,EAAE,EACzC,WAAW,EAAE,SAAS,MAAM,EAAE,EAC9B,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,GAAG,IAAI,EACvB,MAAM,EAAE,yBAAyB,EACjC,IAAI,EAAE,aAAa,
|
|
1
|
+
{"version":3,"file":"resolveMatrixGridSelection.d.ts","sourceRoot":"","sources":["../../../../src/features/matrix-grid/application/resolveMatrixGridSelection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwB,KAAK,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAE1F,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAE9D,KAAK,yBAAyB,GAAG,eAAe,GAAG,OAAO,CAAC;AAE3D,8FAA8F;AAC9F,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,SAAS,uBAAuB,EAAE,EACzC,WAAW,EAAE,SAAS,MAAM,EAAE,EAC9B,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,GAAG,IAAI,EACvB,MAAM,EAAE,yBAAyB,EACjC,IAAI,EAAE,aAAa,EACnB,gBAAgB,EAAE,WAAW,CAAC,MAAM,CAAC,GACpC,SAAS,MAAM,EAAE,CAmBnB;AAED,KAAK,uBAAuB,GAAG,QAAQ,CAAC;IACtC,WAAW,EAAE,MAAM,CAAC;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC,CAAC"}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { applySelectionIntent } from '@ankhorage/utility/selection';
|
|
2
2
|
/*** Apply ZORA's canonical selection intent while retaining the configured selection mode. */
|
|
3
|
-
export function resolveMatrixGridSelection(cells, selectedIds, cellId, anchorId, intent, mode) {
|
|
3
|
+
export function resolveMatrixGridSelection(cells, selectedIds, cellId, anchorId, intent, mode, protectedCellIds) {
|
|
4
|
+
if (protectedCellIds.has(cellId))
|
|
5
|
+
return selectedIds;
|
|
4
6
|
if (mode === 'single')
|
|
5
7
|
return applySelectionIntent(selectedIds, cellId, 'replace');
|
|
6
8
|
if (intent !== 'range')
|
|
@@ -10,7 +12,8 @@ export function resolveMatrixGridSelection(cells, selectedIds, cellId, anchorId,
|
|
|
10
12
|
if (!anchor || !target)
|
|
11
13
|
return applySelectionIntent(selectedIds, cellId, 'replace');
|
|
12
14
|
return cells
|
|
13
|
-
.filter((cell) =>
|
|
15
|
+
.filter((cell) => !protectedCellIds.has(cell.id) &&
|
|
16
|
+
cell.rowIndex >= Math.min(anchor.rowIndex, target.rowIndex) &&
|
|
14
17
|
cell.rowIndex <= Math.max(anchor.rowIndex, target.rowIndex) &&
|
|
15
18
|
cell.columnIndex >= Math.min(anchor.columnIndex, target.columnIndex) &&
|
|
16
19
|
cell.columnIndex <= Math.max(anchor.columnIndex, target.columnIndex))
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolveMatrixGridSelection.js","sourceRoot":"","sources":["../../../../src/features/matrix-grid/application/resolveMatrixGridSelection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAwB,MAAM,8BAA8B,CAAC;AAM1F,8FAA8F;AAC9F,MAAM,UAAU,0BAA0B,CACxC,KAAyC,EACzC,WAA8B,EAC9B,MAAc,EACd,QAAuB,EACvB,MAAiC,EACjC,IAAmB;
|
|
1
|
+
{"version":3,"file":"resolveMatrixGridSelection.js","sourceRoot":"","sources":["../../../../src/features/matrix-grid/application/resolveMatrixGridSelection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAwB,MAAM,8BAA8B,CAAC;AAM1F,8FAA8F;AAC9F,MAAM,UAAU,0BAA0B,CACxC,KAAyC,EACzC,WAA8B,EAC9B,MAAc,EACd,QAAuB,EACvB,MAAiC,EACjC,IAAmB,EACnB,gBAAqC;IAErC,IAAI,gBAAgB,CAAC,GAAG,CAAC,MAAM,CAAC;QAAE,OAAO,WAAW,CAAC;IACrD,IAAI,IAAI,KAAK,QAAQ;QAAE,OAAO,oBAAoB,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC;IACnF,IAAI,MAAM,KAAK,OAAO;QAAE,OAAO,oBAAoB,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IAEjF,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,KAAK,QAAQ,CAAC,CAAC;IAC1D,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,KAAK,MAAM,CAAC,CAAC;IACxD,IAAI,CAAC,MAAM,IAAI,CAAC,MAAM;QAAE,OAAO,oBAAoB,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC;IAEpF,OAAO,KAAK;SACT,MAAM,CACL,CAAC,IAAI,EAAE,EAAE,CACP,CAAC,gBAAgB,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;QAC9B,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC;QAC3D,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC;QAC3D,IAAI,CAAC,WAAW,IAAI,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,WAAW,CAAC;QACpE,IAAI,CAAC,WAAW,IAAI,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,WAAW,CAAC,CACvE;SACA,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AAC5B,CAAC","sourcesContent":["import { applySelectionIntent, type SelectionIntent } from '@ankhorage/utility/selection';\n\nimport type { SelectionMode } from '../../../types/selection';\n\ntype MatrixGridSelectionIntent = SelectionIntent | 'range';\n\n/*** Apply ZORA's canonical selection intent while retaining the configured selection mode. */\nexport function resolveMatrixGridSelection(\n cells: readonly MatrixGridSelectionCell[],\n selectedIds: readonly string[],\n cellId: string,\n anchorId: string | null,\n intent: MatrixGridSelectionIntent,\n mode: SelectionMode,\n protectedCellIds: ReadonlySet<string>,\n): readonly string[] {\n if (protectedCellIds.has(cellId)) return selectedIds;\n if (mode === 'single') return applySelectionIntent(selectedIds, cellId, 'replace');\n if (intent !== 'range') return applySelectionIntent(selectedIds, cellId, intent);\n\n const anchor = cells.find((cell) => cell.id === anchorId);\n const target = cells.find((cell) => cell.id === cellId);\n if (!anchor || !target) return applySelectionIntent(selectedIds, cellId, 'replace');\n\n return cells\n .filter(\n (cell) =>\n !protectedCellIds.has(cell.id) &&\n cell.rowIndex >= Math.min(anchor.rowIndex, target.rowIndex) &&\n cell.rowIndex <= Math.max(anchor.rowIndex, target.rowIndex) &&\n cell.columnIndex >= Math.min(anchor.columnIndex, target.columnIndex) &&\n cell.columnIndex <= Math.max(anchor.columnIndex, target.columnIndex),\n )\n .map((cell) => cell.id);\n}\n\ntype MatrixGridSelectionCell = Readonly<{\n columnIndex: number;\n id: string;\n rowIndex: number;\n}>;\n"]}
|
|
@@ -15,6 +15,8 @@ export interface MatrixGridProps extends ZoraBaseProps {
|
|
|
15
15
|
readonly overscanPixels?: number;
|
|
16
16
|
/** Retains focus navigation while suppressing cell-selection mutations. */
|
|
17
17
|
readonly readOnly?: boolean;
|
|
18
|
+
/** Cells that remain visible but cannot receive focus or change the controlled selection. */
|
|
19
|
+
readonly protectedCellIds?: readonly string[];
|
|
18
20
|
readonly selectedCellIds?: readonly string[];
|
|
19
21
|
readonly selectionMode?: SelectionMode;
|
|
20
22
|
readonly onSelectionChange?: (ids: readonly string[]) => void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"matrix-grid.d.ts","sourceRoot":"","sources":["../../src/types/matrix-grid.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,cAAc,EACd,uBAAuB,EACvB,gBAAgB,EAChB,YAAY,EACb,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AAC5C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,mGAAmG;AACnG,MAAM,WAAW,eAAgB,SAAQ,aAAa;IACpD,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAClC,QAAQ,CAAC,KAAK,EAAE,SAAS,cAAc,EAAE,CAAC;IAC1C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,wFAAwF;IACxF,QAAQ,CAAC,QAAQ,CAAC,EAAE,YAAY,CAAC;IACjC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,2EAA2E;IAC3E,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,eAAe,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC7C,QAAQ,CAAC,aAAa,CAAC,EAAE,aAAa,CAAC;IACvC,QAAQ,CAAC,iBAAiB,CAAC,EAAE,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,KAAK,IAAI,CAAC;IAC9D,QAAQ,CAAC,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,YAAY,KAAK,IAAI,CAAC;IAC7D,QAAQ,CAAC,UAAU,EAAE,CAAC,IAAI,EAAE,uBAAuB,KAAK,KAAK,CAAC,SAAS,CAAC;IACxE,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;CACtC"}
|
|
1
|
+
{"version":3,"file":"matrix-grid.d.ts","sourceRoot":"","sources":["../../src/types/matrix-grid.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,cAAc,EACd,uBAAuB,EACvB,gBAAgB,EAChB,YAAY,EACb,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AAC5C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,mGAAmG;AACnG,MAAM,WAAW,eAAgB,SAAQ,aAAa;IACpD,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAClC,QAAQ,CAAC,KAAK,EAAE,SAAS,cAAc,EAAE,CAAC;IAC1C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,wFAAwF;IACxF,QAAQ,CAAC,QAAQ,CAAC,EAAE,YAAY,CAAC;IACjC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,2EAA2E;IAC3E,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,6FAA6F;IAC7F,QAAQ,CAAC,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9C,QAAQ,CAAC,eAAe,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC7C,QAAQ,CAAC,aAAa,CAAC,EAAE,aAAa,CAAC;IACvC,QAAQ,CAAC,iBAAiB,CAAC,EAAE,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,KAAK,IAAI,CAAC;IAC9D,QAAQ,CAAC,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,YAAY,KAAK,IAAI,CAAC;IAC7D,QAAQ,CAAC,UAAU,EAAE,CAAC,IAAI,EAAE,uBAAuB,KAAK,KAAK,CAAC,SAAS,CAAC;IACxE,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;CACtC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"matrix-grid.js","sourceRoot":"","sources":["../../src/types/matrix-grid.ts"],"names":[],"mappings":"","sourcesContent":["import type {\n GridMatrixCell,\n GridMatrixCellPlacement,\n GridMatrixLayout,\n GridViewport,\n} from '@ankhorage/grid-view';\nimport type React from 'react';\n\nimport type { ZoraBaseProps } from './base';\nimport type { SelectionMode } from './selection';\n\n/** Code-first virtualized sparse-matrix presentation over the published grid-view geometry API. */\nexport interface MatrixGridProps extends ZoraBaseProps {\n readonly layout: GridMatrixLayout;\n readonly cells: readonly GridMatrixCell[];\n readonly width: number;\n readonly height: number;\n /** A supplied viewport is controlled by the caller, including independent axis zoom. */\n readonly viewport?: GridViewport;\n readonly zoomX?: number;\n readonly zoomY?: number;\n readonly overscanPixels?: number;\n /** Retains focus navigation while suppressing cell-selection mutations. */\n readonly readOnly?: boolean;\n readonly selectedCellIds?: readonly string[];\n readonly selectionMode?: SelectionMode;\n readonly onSelectionChange?: (ids: readonly string[]) => void;\n readonly onViewportChange?: (viewport: GridViewport) => void;\n readonly renderCell: (cell: GridMatrixCellPlacement) => React.ReactNode;\n readonly accessibilityLabel?: string;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"matrix-grid.js","sourceRoot":"","sources":["../../src/types/matrix-grid.ts"],"names":[],"mappings":"","sourcesContent":["import type {\n GridMatrixCell,\n GridMatrixCellPlacement,\n GridMatrixLayout,\n GridViewport,\n} from '@ankhorage/grid-view';\nimport type React from 'react';\n\nimport type { ZoraBaseProps } from './base';\nimport type { SelectionMode } from './selection';\n\n/** Code-first virtualized sparse-matrix presentation over the published grid-view geometry API. */\nexport interface MatrixGridProps extends ZoraBaseProps {\n readonly layout: GridMatrixLayout;\n readonly cells: readonly GridMatrixCell[];\n readonly width: number;\n readonly height: number;\n /** A supplied viewport is controlled by the caller, including independent axis zoom. */\n readonly viewport?: GridViewport;\n readonly zoomX?: number;\n readonly zoomY?: number;\n readonly overscanPixels?: number;\n /** Retains focus navigation while suppressing cell-selection mutations. */\n readonly readOnly?: boolean;\n /** Cells that remain visible but cannot receive focus or change the controlled selection. */\n readonly protectedCellIds?: readonly string[];\n readonly selectedCellIds?: readonly string[];\n readonly selectionMode?: SelectionMode;\n readonly onSelectionChange?: (ids: readonly string[]) => void;\n readonly onViewportChange?: (viewport: GridViewport) => void;\n readonly renderCell: (cell: GridMatrixCellPlacement) => React.ReactNode;\n readonly accessibilityLabel?: string;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ankhorage/zora",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "25.8.
|
|
4
|
+
"version": "25.8.14",
|
|
5
5
|
"description": "Opinionated React Native and React Native Web UI kit built on @ankhorage/surface.",
|
|
6
6
|
"homepage": "https://github.com/ankhorage/zora#readme",
|
|
7
7
|
"bugs": {
|
|
@@ -129,7 +129,7 @@
|
|
|
129
129
|
"format": "ankhorage-prettier --write .",
|
|
130
130
|
"format:check": "ankhorage-prettier --check .",
|
|
131
131
|
"prepack": "bun run build",
|
|
132
|
-
"test": "bun run build && bun test src examples/grid-workspaces/time --isolate",
|
|
132
|
+
"test": "bun run build && bun test src examples/grid-workspaces/time examples/grid-workspaces/plugins --isolate",
|
|
133
133
|
"typecheck": "bun x tsc --noEmit -p tsconfig.json && bun x tsc --noEmit -p tsconfig.scripts.json",
|
|
134
134
|
"version-packages": "ankhorage-changeset version"
|
|
135
135
|
},
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { pathToFileURL } from 'node:url';
|
|
3
|
+
|
|
4
|
+
import { expect, test } from 'bun:test';
|
|
5
|
+
import { Window } from 'happy-dom';
|
|
6
|
+
import React, { act } from 'react';
|
|
7
|
+
import { createRoot } from 'react-dom/client';
|
|
8
|
+
|
|
9
|
+
const browserKeys = [
|
|
10
|
+
'window',
|
|
11
|
+
'document',
|
|
12
|
+
'Node',
|
|
13
|
+
'navigator',
|
|
14
|
+
'ShadowRoot',
|
|
15
|
+
'IS_REACT_ACT_ENVIRONMENT',
|
|
16
|
+
] as const;
|
|
17
|
+
const webDistRoot = join(import.meta.dir, '../../../../../web-dist');
|
|
18
|
+
const load = (path: string) => import(pathToFileURL(join(webDistRoot, path)).href);
|
|
19
|
+
|
|
20
|
+
interface MatrixCell {
|
|
21
|
+
readonly columnId: string;
|
|
22
|
+
readonly id: string;
|
|
23
|
+
readonly rowId: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface MatrixLayout {
|
|
27
|
+
readonly columns: readonly { readonly id: string; readonly size: number }[];
|
|
28
|
+
readonly rows: readonly { readonly id: string; readonly size: number }[];
|
|
29
|
+
}
|
|
30
|
+
type MatrixGridComponent = React.ComponentType<{
|
|
31
|
+
readonly cells: readonly MatrixCell[];
|
|
32
|
+
readonly height: number;
|
|
33
|
+
readonly layout: MatrixLayout;
|
|
34
|
+
readonly protectedCellIds?: readonly string[];
|
|
35
|
+
readonly renderCell: (cell: MatrixCell) => React.ReactNode;
|
|
36
|
+
readonly testID: string;
|
|
37
|
+
readonly width: number;
|
|
38
|
+
}>;
|
|
39
|
+
type ZoraProviderComponent = React.ComponentType<{ readonly children: React.ReactNode }>;
|
|
40
|
+
|
|
41
|
+
/*** Mount MatrixGrid through React Native Web and restore the ambient DOM after each assertion. */
|
|
42
|
+
async function withMatrixDom(assertion: (host: HTMLElement, browser: Window) => Promise<void>) {
|
|
43
|
+
const browser = new Window();
|
|
44
|
+
const previous = browserKeys.map(
|
|
45
|
+
(key) => [key, Object.getOwnPropertyDescriptor(globalThis, key)] as const,
|
|
46
|
+
);
|
|
47
|
+
Object.assign(globalThis, {
|
|
48
|
+
IS_REACT_ACT_ENVIRONMENT: true,
|
|
49
|
+
window: browser,
|
|
50
|
+
document: browser.document,
|
|
51
|
+
Node: browser.Node,
|
|
52
|
+
navigator: browser.navigator,
|
|
53
|
+
ShadowRoot: browser.ShadowRoot,
|
|
54
|
+
});
|
|
55
|
+
const host = document.createElement('div');
|
|
56
|
+
document.body.appendChild(host);
|
|
57
|
+
try {
|
|
58
|
+
await assertion(host, browser);
|
|
59
|
+
} finally {
|
|
60
|
+
browser.close();
|
|
61
|
+
for (const [key, descriptor] of previous) {
|
|
62
|
+
if (descriptor) Object.defineProperty(globalThis, key, descriptor);
|
|
63
|
+
else Reflect.deleteProperty(globalThis, key);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/*** Load the compiled MatrixGrid and provider used by React Native Web interaction tests. */
|
|
69
|
+
async function loadMatrixGrid() {
|
|
70
|
+
const { MatrixGrid } = (await load('components/matrix-grid/MatrixGrid.js')) as {
|
|
71
|
+
MatrixGrid: MatrixGridComponent;
|
|
72
|
+
};
|
|
73
|
+
const { ZoraProvider } = (await load('runtime/ZoraProvider.js')) as {
|
|
74
|
+
ZoraProvider: ZoraProviderComponent;
|
|
75
|
+
};
|
|
76
|
+
return { MatrixGrid, ZoraProvider };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
test('retains focus when protected terminal cells block all four arrow directions', async () => {
|
|
80
|
+
await withMatrixDom(async (host, browser) => {
|
|
81
|
+
const root = createRoot(host);
|
|
82
|
+
const { MatrixGrid, ZoraProvider } = await loadMatrixGrid();
|
|
83
|
+
const layout = {
|
|
84
|
+
columns: Array.from({ length: 3 }, (_, index) => ({ id: `column-${index}`, size: 20 })),
|
|
85
|
+
rows: Array.from({ length: 3 }, (_, index) => ({ id: `row-${index}`, size: 20 })),
|
|
86
|
+
};
|
|
87
|
+
const cells = [
|
|
88
|
+
{ columnId: 'column-1', id: 'up', rowId: 'row-0' },
|
|
89
|
+
{ columnId: 'column-0', id: 'left', rowId: 'row-1' },
|
|
90
|
+
{ columnId: 'column-1', id: 'origin', rowId: 'row-1' },
|
|
91
|
+
{ columnId: 'column-2', id: 'right', rowId: 'row-1' },
|
|
92
|
+
{ columnId: 'column-1', id: 'down', rowId: 'row-2' },
|
|
93
|
+
];
|
|
94
|
+
try {
|
|
95
|
+
act(() =>
|
|
96
|
+
root.render(
|
|
97
|
+
<ZoraProvider>
|
|
98
|
+
<MatrixGrid
|
|
99
|
+
cells={cells}
|
|
100
|
+
height={60}
|
|
101
|
+
layout={layout}
|
|
102
|
+
protectedCellIds={['left', 'right', 'up', 'down']}
|
|
103
|
+
testID="terminal-protected-matrix"
|
|
104
|
+
width={60}
|
|
105
|
+
renderCell={(cell) => <span>{cell.id}</span>}
|
|
106
|
+
/>
|
|
107
|
+
</ZoraProvider>,
|
|
108
|
+
),
|
|
109
|
+
);
|
|
110
|
+
const origin = host.querySelector<HTMLElement>(
|
|
111
|
+
'[data-testid="terminal-protected-matrix-cell-origin"]',
|
|
112
|
+
);
|
|
113
|
+
if (!origin) throw new Error('Missing terminal-protected focus origin.');
|
|
114
|
+
act(() => origin.focus());
|
|
115
|
+
for (const key of ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown']) {
|
|
116
|
+
act(() => {
|
|
117
|
+
void origin.dispatchEvent(new browser.KeyboardEvent('keydown', { bubbles: true, key }));
|
|
118
|
+
});
|
|
119
|
+
expect(document.activeElement).toBe(origin);
|
|
120
|
+
}
|
|
121
|
+
} finally {
|
|
122
|
+
act(() => root.unmount());
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('skips consecutive protected cells and retains the origin when a direction has no navigable cell', async () => {
|
|
128
|
+
await withMatrixDom(async (host, browser) => {
|
|
129
|
+
const root = createRoot(host);
|
|
130
|
+
const { MatrixGrid, ZoraProvider } = await loadMatrixGrid();
|
|
131
|
+
const layout = {
|
|
132
|
+
columns: Array.from({ length: 4 }, (_, index) => ({ id: `column-${index}`, size: 20 })),
|
|
133
|
+
rows: [{ id: 'row-0', size: 20 }],
|
|
134
|
+
};
|
|
135
|
+
const cells = ['a', 'b', 'c', 'd'].map((id, index) => ({
|
|
136
|
+
columnId: `column-${index}`,
|
|
137
|
+
id,
|
|
138
|
+
rowId: 'row-0',
|
|
139
|
+
}));
|
|
140
|
+
const render = (protectedCellIds: readonly string[], testID: string) => (
|
|
141
|
+
<ZoraProvider>
|
|
142
|
+
<MatrixGrid
|
|
143
|
+
cells={cells}
|
|
144
|
+
height={20}
|
|
145
|
+
layout={layout}
|
|
146
|
+
protectedCellIds={protectedCellIds}
|
|
147
|
+
testID={testID}
|
|
148
|
+
width={80}
|
|
149
|
+
renderCell={(cell) => <span>{cell.id}</span>}
|
|
150
|
+
/>
|
|
151
|
+
</ZoraProvider>
|
|
152
|
+
);
|
|
153
|
+
try {
|
|
154
|
+
act(() => root.render(render(['b', 'c'], 'consecutive-protected-matrix')));
|
|
155
|
+
const a = host.querySelector<HTMLElement>(
|
|
156
|
+
'[data-testid="consecutive-protected-matrix-cell-a"]',
|
|
157
|
+
);
|
|
158
|
+
const d = host.querySelector<HTMLElement>(
|
|
159
|
+
'[data-testid="consecutive-protected-matrix-cell-d"]',
|
|
160
|
+
);
|
|
161
|
+
if (!a || !d) throw new Error('Missing consecutive-protected cells.');
|
|
162
|
+
act(() => {
|
|
163
|
+
a.focus();
|
|
164
|
+
void a.dispatchEvent(
|
|
165
|
+
new browser.KeyboardEvent('keydown', { bubbles: true, key: 'ArrowRight' }),
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
expect(document.activeElement).toBe(d);
|
|
169
|
+
|
|
170
|
+
act(() => root.render(render(['b', 'c', 'd'], 'blocked-direction-matrix')));
|
|
171
|
+
const blockedOrigin = host.querySelector<HTMLElement>(
|
|
172
|
+
'[data-testid="blocked-direction-matrix-cell-a"]',
|
|
173
|
+
);
|
|
174
|
+
if (!blockedOrigin) throw new Error('Missing blocked-direction focus origin.');
|
|
175
|
+
act(() => {
|
|
176
|
+
blockedOrigin.focus();
|
|
177
|
+
void blockedOrigin.dispatchEvent(
|
|
178
|
+
new browser.KeyboardEvent('keydown', { bubbles: true, key: 'ArrowRight' }),
|
|
179
|
+
);
|
|
180
|
+
});
|
|
181
|
+
expect(document.activeElement).toBe(blockedOrigin);
|
|
182
|
+
} finally {
|
|
183
|
+
act(() => root.unmount());
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('reconciles roving focus when protected cells change and leaves fully protected matrices without a tab stop', async () => {
|
|
189
|
+
await withMatrixDom(async (host, browser) => {
|
|
190
|
+
const root = createRoot(host);
|
|
191
|
+
const { MatrixGrid, ZoraProvider } = await loadMatrixGrid();
|
|
192
|
+
const layout = {
|
|
193
|
+
columns: Array.from({ length: 3 }, (_, index) => ({ id: `column-${index}`, size: 20 })),
|
|
194
|
+
rows: [{ id: 'row-0', size: 20 }],
|
|
195
|
+
};
|
|
196
|
+
const cells = ['a', 'b', 'c'].map((id, index) => ({
|
|
197
|
+
columnId: `column-${index}`,
|
|
198
|
+
id,
|
|
199
|
+
rowId: 'row-0',
|
|
200
|
+
}));
|
|
201
|
+
let setProtectedCellIds: React.Dispatch<React.SetStateAction<readonly string[]>> | undefined;
|
|
202
|
+
|
|
203
|
+
function DynamicProtectionMatrix() {
|
|
204
|
+
const [protectedCellIds, setProtected] = React.useState<readonly string[]>([]);
|
|
205
|
+
setProtectedCellIds = setProtected;
|
|
206
|
+
return (
|
|
207
|
+
<ZoraProvider>
|
|
208
|
+
<MatrixGrid
|
|
209
|
+
cells={cells}
|
|
210
|
+
height={20}
|
|
211
|
+
layout={layout}
|
|
212
|
+
protectedCellIds={protectedCellIds}
|
|
213
|
+
testID="dynamic-protected-matrix"
|
|
214
|
+
width={60}
|
|
215
|
+
renderCell={(cell) => <span>{cell.id}</span>}
|
|
216
|
+
/>
|
|
217
|
+
</ZoraProvider>
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
act(() => root.render(<DynamicProtectionMatrix />));
|
|
223
|
+
const a = host.querySelector<HTMLElement>('[data-testid="dynamic-protected-matrix-cell-a"]');
|
|
224
|
+
if (!a) throw new Error('Missing dynamic-protection focus origin.');
|
|
225
|
+
act(() => a.focus());
|
|
226
|
+
act(() => setProtectedCellIds?.(['a']));
|
|
227
|
+
const protectedA = host.querySelector<HTMLElement>(
|
|
228
|
+
'[data-testid="dynamic-protected-matrix-cell-a"]',
|
|
229
|
+
);
|
|
230
|
+
const b = host.querySelector<HTMLElement>('[data-testid="dynamic-protected-matrix-cell-b"]');
|
|
231
|
+
if (!protectedA || !b) throw new Error('Missing dynamic-protection cells.');
|
|
232
|
+
expect(protectedA.getAttribute('aria-disabled')).toBe('true');
|
|
233
|
+
expect(protectedA.getAttribute('tabindex')).not.toBe('0');
|
|
234
|
+
expect(b.getAttribute('tabindex')).toBe('0');
|
|
235
|
+
act(() => {
|
|
236
|
+
b.focus();
|
|
237
|
+
void b.dispatchEvent(
|
|
238
|
+
new browser.KeyboardEvent('keydown', { bubbles: true, key: 'ArrowRight' }),
|
|
239
|
+
);
|
|
240
|
+
});
|
|
241
|
+
const c = host.querySelector<HTMLElement>('[data-testid="dynamic-protected-matrix-cell-c"]');
|
|
242
|
+
expect(document.activeElement).toBe(c);
|
|
243
|
+
|
|
244
|
+
act(() => setProtectedCellIds?.(['a', 'b', 'c']));
|
|
245
|
+
expect(
|
|
246
|
+
host.querySelectorAll('[data-testid^="dynamic-protected-matrix-cell-"][tabindex="0"]'),
|
|
247
|
+
).toHaveLength(0);
|
|
248
|
+
} finally {
|
|
249
|
+
act(() => root.unmount());
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
});
|
|
@@ -67,6 +67,7 @@ type MatrixGridComponent = React.ComponentType<{
|
|
|
67
67
|
readonly layout: typeof layout;
|
|
68
68
|
readonly onSelectionChange?: (ids: readonly string[]) => void;
|
|
69
69
|
readonly onViewportChange?: (viewport: unknown) => void;
|
|
70
|
+
readonly protectedCellIds?: readonly string[];
|
|
70
71
|
readonly renderCell: (cell: { readonly id: string }) => React.ReactNode;
|
|
71
72
|
readonly readOnly?: boolean;
|
|
72
73
|
readonly selectedCellIds?: readonly string[];
|
|
@@ -320,6 +321,84 @@ test('navigates sparse cells by keyboard and applies range and modifier selectio
|
|
|
320
321
|
});
|
|
321
322
|
});
|
|
322
323
|
|
|
324
|
+
test('keeps protected cells disabled, unselected, and out of keyboard ranges', async () => {
|
|
325
|
+
await withMatrixDom(async (host, browser) => {
|
|
326
|
+
const selections: (readonly string[])[] = [];
|
|
327
|
+
const root = createRoot(host);
|
|
328
|
+
const { MatrixGrid } = (await load('components/matrix-grid/MatrixGrid.js')) as {
|
|
329
|
+
MatrixGrid: MatrixGridComponent;
|
|
330
|
+
};
|
|
331
|
+
const { ZoraProvider } = (await load('runtime/ZoraProvider.js')) as {
|
|
332
|
+
ZoraProvider: ZoraProviderComponent;
|
|
333
|
+
};
|
|
334
|
+
const protectedLayout = {
|
|
335
|
+
columns: [
|
|
336
|
+
{ id: 'column-0', size: 20 },
|
|
337
|
+
{ id: 'column-1', size: 20 },
|
|
338
|
+
{ id: 'column-2', size: 20 },
|
|
339
|
+
],
|
|
340
|
+
rows: [{ id: 'row-0', size: 20 }],
|
|
341
|
+
};
|
|
342
|
+
const protectedCells = [
|
|
343
|
+
{ columnId: 'column-0', id: 'a1', rowId: 'row-0' },
|
|
344
|
+
{ columnId: 'column-1', id: 'b1', rowId: 'row-0' },
|
|
345
|
+
{ columnId: 'column-2', id: 'c1', rowId: 'row-0' },
|
|
346
|
+
];
|
|
347
|
+
|
|
348
|
+
function ProtectedMatrix() {
|
|
349
|
+
const [selectedIds, setSelectedIds] = React.useState<readonly string[]>(['a1']);
|
|
350
|
+
return (
|
|
351
|
+
<ZoraProvider>
|
|
352
|
+
<MatrixGrid
|
|
353
|
+
cells={protectedCells}
|
|
354
|
+
height={40}
|
|
355
|
+
layout={protectedLayout}
|
|
356
|
+
protectedCellIds={['b1']}
|
|
357
|
+
selectedCellIds={selectedIds}
|
|
358
|
+
selectionMode="multi"
|
|
359
|
+
testID="protected-matrix"
|
|
360
|
+
width={60}
|
|
361
|
+
onSelectionChange={(ids) => {
|
|
362
|
+
selections.push(ids);
|
|
363
|
+
setSelectedIds(ids);
|
|
364
|
+
}}
|
|
365
|
+
renderCell={(cell) => <span>{cell.id}</span>}
|
|
366
|
+
/>
|
|
367
|
+
</ZoraProvider>
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
try {
|
|
372
|
+
act(() => root.render(<ProtectedMatrix />));
|
|
373
|
+
const a1 = host.querySelector<HTMLElement>('[data-testid="protected-matrix-cell-a1"]');
|
|
374
|
+
const b1 = host.querySelector<HTMLElement>('[data-testid="protected-matrix-cell-b1"]');
|
|
375
|
+
if (!a1 || !b1) throw new Error('Missing protected matrix cells.');
|
|
376
|
+
expect(b1.getAttribute('aria-disabled')).toBe('true');
|
|
377
|
+
act(() => {
|
|
378
|
+
void b1.dispatchEvent(new browser.MouseEvent('click', { bubbles: true }));
|
|
379
|
+
void b1.dispatchEvent(
|
|
380
|
+
new browser.KeyboardEvent('keydown', { bubbles: true, key: 'Enter' }),
|
|
381
|
+
);
|
|
382
|
+
void b1.dispatchEvent(new browser.KeyboardEvent('keydown', { bubbles: true, key: ' ' }));
|
|
383
|
+
a1.focus();
|
|
384
|
+
void a1.dispatchEvent(
|
|
385
|
+
new browser.KeyboardEvent('keydown', {
|
|
386
|
+
bubbles: true,
|
|
387
|
+
key: 'ArrowRight',
|
|
388
|
+
shiftKey: true,
|
|
389
|
+
}),
|
|
390
|
+
);
|
|
391
|
+
});
|
|
392
|
+
const c1 = host.querySelector<HTMLElement>('[data-testid="protected-matrix-cell-c1"]');
|
|
393
|
+
expect(c1).not.toBeNull();
|
|
394
|
+
expect(document.activeElement).toBe(c1);
|
|
395
|
+
expect(selections).toEqual([['a1', 'c1']]);
|
|
396
|
+
} finally {
|
|
397
|
+
act(() => root.unmount());
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
|
|
323
402
|
test('does not capture keyboard input from interactive cell content and respects read-only selection', async () => {
|
|
324
403
|
await withMatrixDom(async (host, browser) => {
|
|
325
404
|
const selections: (readonly string[])[] = [];
|
|
@@ -29,6 +29,7 @@ function MatrixGridInner({
|
|
|
29
29
|
onSelectionChange,
|
|
30
30
|
onViewportChange,
|
|
31
31
|
overscanPixels = 160,
|
|
32
|
+
protectedCellIds = [],
|
|
32
33
|
readOnly = false,
|
|
33
34
|
renderCell,
|
|
34
35
|
selectedCellIds = [],
|
|
@@ -57,6 +58,11 @@ function MatrixGridInner({
|
|
|
57
58
|
);
|
|
58
59
|
const [visibleIds, setVisibleIds] = React.useState<readonly string[]>([]);
|
|
59
60
|
const anchorId = React.useRef<string | null>(null);
|
|
61
|
+
const protectedCellIdSet = React.useMemo(() => new Set(protectedCellIds), [protectedCellIds]);
|
|
62
|
+
const isNavigable = React.useCallback(
|
|
63
|
+
(id: string) => !protectedCellIdSet.has(id),
|
|
64
|
+
[protectedCellIdSet],
|
|
65
|
+
);
|
|
60
66
|
|
|
61
67
|
const handleViewportChange = React.useCallback(
|
|
62
68
|
(proposal: GridViewport) => {
|
|
@@ -68,7 +74,7 @@ function MatrixGridInner({
|
|
|
68
74
|
|
|
69
75
|
const activateCell = React.useCallback(
|
|
70
76
|
(cell: GridMatrixCellPlacement, event: unknown) => {
|
|
71
|
-
if (interactionPolicy === 'passive' || readOnly) return;
|
|
77
|
+
if (interactionPolicy === 'passive' || readOnly || protectedCellIdSet.has(cell.id)) return;
|
|
72
78
|
const intent = isRangeSelectionEvent(event)
|
|
73
79
|
? 'range'
|
|
74
80
|
: resolveSelectionEventIntent(event, 'pointer');
|
|
@@ -80,6 +86,7 @@ function MatrixGridInner({
|
|
|
80
86
|
anchorId.current,
|
|
81
87
|
intent,
|
|
82
88
|
selectionMode,
|
|
89
|
+
protectedCellIdSet,
|
|
83
90
|
),
|
|
84
91
|
);
|
|
85
92
|
if (intent !== 'range') anchorId.current = cell.id;
|
|
@@ -91,6 +98,7 @@ function MatrixGridInner({
|
|
|
91
98
|
selectedCellIds,
|
|
92
99
|
selectionMode,
|
|
93
100
|
sparseIndex.entries,
|
|
101
|
+
protectedCellIdSet,
|
|
94
102
|
],
|
|
95
103
|
);
|
|
96
104
|
const handleKeyboardNavigation = React.useCallback(
|
|
@@ -106,6 +114,7 @@ function MatrixGridInner({
|
|
|
106
114
|
sparseIndex,
|
|
107
115
|
interactionPolicy === 'passive',
|
|
108
116
|
visibleIds,
|
|
117
|
+
isNavigable,
|
|
109
118
|
handleKeyboardNavigation,
|
|
110
119
|
);
|
|
111
120
|
const focusedPlacement = React.useMemo(() => {
|
|
@@ -148,6 +157,7 @@ function MatrixGridInner({
|
|
|
148
157
|
activateCell,
|
|
149
158
|
keyboard,
|
|
150
159
|
interactionPolicy === 'passive',
|
|
160
|
+
protectedCellIdSet.has(cell.id),
|
|
151
161
|
readOnly,
|
|
152
162
|
testID,
|
|
153
163
|
);
|
|
@@ -171,6 +181,7 @@ function renderMatrixCell(
|
|
|
171
181
|
activateCell: (cell: GridMatrixCellPlacement, event: unknown) => void,
|
|
172
182
|
keyboard: ReturnType<typeof useMatrixGridKeyboardFocus>,
|
|
173
183
|
passive: boolean,
|
|
184
|
+
protectedCell: boolean,
|
|
174
185
|
readOnly: boolean,
|
|
175
186
|
testID: string | undefined,
|
|
176
187
|
) {
|
|
@@ -181,7 +192,7 @@ function renderMatrixCell(
|
|
|
181
192
|
onKeyDown={(event) => {
|
|
182
193
|
if (keyboard.onKeyDown(cell.id, event.key, event.shiftKey)) return true;
|
|
183
194
|
if (!['Enter', ' '].includes(event.key)) return false;
|
|
184
|
-
if (passive || readOnly) return false;
|
|
195
|
+
if (passive || protectedCell || readOnly) return false;
|
|
185
196
|
activateCell(cell, event);
|
|
186
197
|
return true;
|
|
187
198
|
}}
|
|
@@ -191,9 +202,9 @@ function renderMatrixCell(
|
|
|
191
202
|
accessibilityActions={[{ name: 'activate' }]}
|
|
192
203
|
accessibilityLabel={`Cell ${cell.id}`}
|
|
193
204
|
accessibilityRole="button"
|
|
194
|
-
accessibilityState={{ disabled: passive, selected }}
|
|
195
|
-
disabled={passive}
|
|
196
|
-
focusable={Platform.OS !== 'web' || canInteract}
|
|
205
|
+
accessibilityState={{ disabled: passive || protectedCell, selected }}
|
|
206
|
+
disabled={passive || protectedCell}
|
|
207
|
+
focusable={!protectedCell && (Platform.OS !== 'web' || canInteract)}
|
|
197
208
|
style={[
|
|
198
209
|
styles.cell,
|
|
199
210
|
{
|
|
@@ -8,6 +8,7 @@ export function useMatrixGridKeyboardFocus(
|
|
|
8
8
|
index: MatrixGridSparseIndex,
|
|
9
9
|
disabled: boolean,
|
|
10
10
|
visibleIds: readonly string[],
|
|
11
|
+
isNavigable: (id: string) => boolean,
|
|
11
12
|
onNavigate: (targetId: string, originId: string, shiftKey: boolean) => void,
|
|
12
13
|
) {
|
|
13
14
|
const [focusedId, setFocusedId] = React.useState<string | null>(null);
|
|
@@ -16,9 +17,19 @@ export function useMatrixGridKeyboardFocus(
|
|
|
16
17
|
const cellRefs = React.useRef(new Map<string, FocusableCell>());
|
|
17
18
|
const visibleIdSet = React.useMemo(() => new Set(visibleIds), [visibleIds]);
|
|
18
19
|
const tabStopId =
|
|
19
|
-
focusedId !== null && visibleIdSet.has(focusedId)
|
|
20
|
+
focusedId !== null && visibleIdSet.has(focusedId) && isNavigable(focusedId)
|
|
20
21
|
? focusedId
|
|
21
|
-
: visibleIds.find((id) => index.positionById.has(id));
|
|
22
|
+
: visibleIds.find((id) => index.positionById.has(id) && isNavigable(id));
|
|
23
|
+
|
|
24
|
+
React.useEffect(() => {
|
|
25
|
+
if (focusedRef.current !== null && !isNavigable(focusedRef.current)) {
|
|
26
|
+
focusedRef.current = null;
|
|
27
|
+
setFocusedId(null);
|
|
28
|
+
}
|
|
29
|
+
if (pendingFocusRef.current !== null && !isNavigable(pendingFocusRef.current)) {
|
|
30
|
+
pendingFocusRef.current = null;
|
|
31
|
+
}
|
|
32
|
+
}, [isNavigable]);
|
|
22
33
|
|
|
23
34
|
const onFocus = React.useCallback((id: string) => {
|
|
24
35
|
pendingFocusRef.current = null;
|
|
@@ -44,7 +55,7 @@ export function useMatrixGridKeyboardFocus(
|
|
|
44
55
|
return false;
|
|
45
56
|
}
|
|
46
57
|
const originId = focusedRef.current ?? id;
|
|
47
|
-
const nextId =
|
|
58
|
+
const nextId = resolveNextNavigableId(index, originId, key, isNavigable);
|
|
48
59
|
if (nextId === null || nextId === originId) return true;
|
|
49
60
|
|
|
50
61
|
focusedRef.current = nextId;
|
|
@@ -59,12 +70,28 @@ export function useMatrixGridKeyboardFocus(
|
|
|
59
70
|
}
|
|
60
71
|
return true;
|
|
61
72
|
},
|
|
62
|
-
[disabled, index, onNavigate],
|
|
73
|
+
[disabled, index, isNavigable, onNavigate],
|
|
63
74
|
);
|
|
64
75
|
|
|
65
76
|
return { focusedId, onFocus, onKeyDown, registerCell, tabStopId };
|
|
66
77
|
}
|
|
67
78
|
|
|
79
|
+
/*** Skip protected sparse cells while retaining the nearest deterministic directional navigation. */
|
|
80
|
+
function resolveNextNavigableId(
|
|
81
|
+
index: MatrixGridSparseIndex,
|
|
82
|
+
originId: string,
|
|
83
|
+
key: string,
|
|
84
|
+
isNavigable: (id: string) => boolean,
|
|
85
|
+
): string | null {
|
|
86
|
+
let candidate = getMatrixGridNextFocusId(index, originId, key);
|
|
87
|
+
while (candidate !== null && candidate !== originId && !isNavigable(candidate)) {
|
|
88
|
+
const nextCandidate = getMatrixGridNextFocusId(index, candidate, key);
|
|
89
|
+
if (nextCandidate === candidate) return originId;
|
|
90
|
+
candidate = nextCandidate;
|
|
91
|
+
}
|
|
92
|
+
return candidate;
|
|
93
|
+
}
|
|
94
|
+
|
|
68
95
|
interface FocusableCell {
|
|
69
96
|
focus: () => void;
|
|
70
97
|
}
|
|
@@ -51,21 +51,28 @@ const cells = [
|
|
|
51
51
|
|
|
52
52
|
describe('resolveMatrixGridSelection', () => {
|
|
53
53
|
test('preserves the canonical single and multi selection intent rules', () => {
|
|
54
|
-
expect(
|
|
55
|
-
'b1',
|
|
56
|
-
]);
|
|
57
|
-
expect(
|
|
58
|
-
'a1',
|
|
59
|
-
|
|
60
|
-
]);
|
|
54
|
+
expect(
|
|
55
|
+
resolveMatrixGridSelection(cells, ['a1'], 'b1', 'a1', 'toggle', 'single', new Set()),
|
|
56
|
+
).toEqual(['b1']);
|
|
57
|
+
expect(
|
|
58
|
+
resolveMatrixGridSelection(cells, ['a1'], 'b1', 'a1', 'toggle', 'multi', new Set()),
|
|
59
|
+
).toEqual(['a1', 'b1']);
|
|
61
60
|
});
|
|
62
61
|
|
|
63
62
|
test('uses the selection contract for a sparse rectangular shift range', () => {
|
|
64
|
-
expect(
|
|
65
|
-
'a1',
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
63
|
+
expect(
|
|
64
|
+
resolveMatrixGridSelection(cells, ['a1'], 'b2', 'a1', 'range', 'multi', new Set()),
|
|
65
|
+
).toEqual(['a1', 'b1', 'a2', 'b2']);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('excludes protected cells from direct and range selection proposals', () => {
|
|
69
|
+
const protectedCellIds = new Set(['b1', 'a2']);
|
|
70
|
+
|
|
71
|
+
expect(
|
|
72
|
+
resolveMatrixGridSelection(cells, ['a1'], 'b1', 'a1', 'replace', 'multi', protectedCellIds),
|
|
73
|
+
).toEqual(['a1']);
|
|
74
|
+
expect(
|
|
75
|
+
resolveMatrixGridSelection(cells, ['a1'], 'b2', 'a1', 'range', 'multi', protectedCellIds),
|
|
76
|
+
).toEqual(['a1', 'b2']);
|
|
70
77
|
});
|
|
71
78
|
});
|
|
@@ -12,7 +12,9 @@ export function resolveMatrixGridSelection(
|
|
|
12
12
|
anchorId: string | null,
|
|
13
13
|
intent: MatrixGridSelectionIntent,
|
|
14
14
|
mode: SelectionMode,
|
|
15
|
+
protectedCellIds: ReadonlySet<string>,
|
|
15
16
|
): readonly string[] {
|
|
17
|
+
if (protectedCellIds.has(cellId)) return selectedIds;
|
|
16
18
|
if (mode === 'single') return applySelectionIntent(selectedIds, cellId, 'replace');
|
|
17
19
|
if (intent !== 'range') return applySelectionIntent(selectedIds, cellId, intent);
|
|
18
20
|
|
|
@@ -23,6 +25,7 @@ export function resolveMatrixGridSelection(
|
|
|
23
25
|
return cells
|
|
24
26
|
.filter(
|
|
25
27
|
(cell) =>
|
|
28
|
+
!protectedCellIds.has(cell.id) &&
|
|
26
29
|
cell.rowIndex >= Math.min(anchor.rowIndex, target.rowIndex) &&
|
|
27
30
|
cell.rowIndex <= Math.max(anchor.rowIndex, target.rowIndex) &&
|
|
28
31
|
cell.columnIndex >= Math.min(anchor.columnIndex, target.columnIndex) &&
|
package/src/types/matrix-grid.ts
CHANGED
|
@@ -22,6 +22,8 @@ export interface MatrixGridProps extends ZoraBaseProps {
|
|
|
22
22
|
readonly overscanPixels?: number;
|
|
23
23
|
/** Retains focus navigation while suppressing cell-selection mutations. */
|
|
24
24
|
readonly readOnly?: boolean;
|
|
25
|
+
/** Cells that remain visible but cannot receive focus or change the controlled selection. */
|
|
26
|
+
readonly protectedCellIds?: readonly string[];
|
|
25
27
|
readonly selectedCellIds?: readonly string[];
|
|
26
28
|
readonly selectionMode?: SelectionMode;
|
|
27
29
|
readonly onSelectionChange?: (ids: readonly string[]) => void;
|
|
@@ -110,7 +110,9 @@ function resolveMatrixGridContentSize(layout) {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
// src/features/matrix-grid/application/resolveMatrixGridSelection.ts
|
|
113
|
-
function resolveMatrixGridSelection(cells, selectedIds, cellId, anchorId, intent, mode) {
|
|
113
|
+
function resolveMatrixGridSelection(cells, selectedIds, cellId, anchorId, intent, mode, protectedCellIds) {
|
|
114
|
+
if (protectedCellIds.has(cellId))
|
|
115
|
+
return selectedIds;
|
|
114
116
|
if (mode === "single")
|
|
115
117
|
return applySelectionIntent(selectedIds, cellId, "replace");
|
|
116
118
|
if (intent !== "range")
|
|
@@ -119,7 +121,7 @@ function resolveMatrixGridSelection(cells, selectedIds, cellId, anchorId, intent
|
|
|
119
121
|
const target = cells.find((cell) => cell.id === cellId);
|
|
120
122
|
if (!anchor || !target)
|
|
121
123
|
return applySelectionIntent(selectedIds, cellId, "replace");
|
|
122
|
-
return cells.filter((cell) => cell.rowIndex >= Math.min(anchor.rowIndex, target.rowIndex) && cell.rowIndex <= Math.max(anchor.rowIndex, target.rowIndex) && cell.columnIndex >= Math.min(anchor.columnIndex, target.columnIndex) && cell.columnIndex <= Math.max(anchor.columnIndex, target.columnIndex)).map((cell) => cell.id);
|
|
124
|
+
return cells.filter((cell) => !protectedCellIds.has(cell.id) && cell.rowIndex >= Math.min(anchor.rowIndex, target.rowIndex) && cell.rowIndex <= Math.max(anchor.rowIndex, target.rowIndex) && cell.columnIndex >= Math.min(anchor.columnIndex, target.columnIndex) && cell.columnIndex <= Math.max(anchor.columnIndex, target.columnIndex)).map((cell) => cell.id);
|
|
123
125
|
}
|
|
124
126
|
|
|
125
127
|
// src/features/matrix-grid/adapters/inbound/MatrixGridKeyboardProxy.web.tsx
|
|
@@ -214,13 +216,22 @@ function getAxisCoordinate2(entry, axis) {
|
|
|
214
216
|
}
|
|
215
217
|
|
|
216
218
|
// src/features/matrix-grid/adapters/inbound/useMatrixGridKeyboardFocus.ts
|
|
217
|
-
function useMatrixGridKeyboardFocus(index, disabled, visibleIds, onNavigate) {
|
|
219
|
+
function useMatrixGridKeyboardFocus(index, disabled, visibleIds, isNavigable, onNavigate) {
|
|
218
220
|
const [focusedId, setFocusedId] = React.useState(null);
|
|
219
221
|
const focusedRef = React.useRef(null);
|
|
220
222
|
const pendingFocusRef = React.useRef(null);
|
|
221
223
|
const cellRefs = React.useRef(new Map);
|
|
222
224
|
const visibleIdSet = React.useMemo(() => new Set(visibleIds), [visibleIds]);
|
|
223
|
-
const tabStopId = focusedId !== null && visibleIdSet.has(focusedId) ? focusedId : visibleIds.find((id) => index.positionById.has(id));
|
|
225
|
+
const tabStopId = focusedId !== null && visibleIdSet.has(focusedId) && isNavigable(focusedId) ? focusedId : visibleIds.find((id) => index.positionById.has(id) && isNavigable(id));
|
|
226
|
+
React.useEffect(() => {
|
|
227
|
+
if (focusedRef.current !== null && !isNavigable(focusedRef.current)) {
|
|
228
|
+
focusedRef.current = null;
|
|
229
|
+
setFocusedId(null);
|
|
230
|
+
}
|
|
231
|
+
if (pendingFocusRef.current !== null && !isNavigable(pendingFocusRef.current)) {
|
|
232
|
+
pendingFocusRef.current = null;
|
|
233
|
+
}
|
|
234
|
+
}, [isNavigable]);
|
|
224
235
|
const onFocus = React.useCallback((id) => {
|
|
225
236
|
pendingFocusRef.current = null;
|
|
226
237
|
focusedRef.current = id;
|
|
@@ -242,7 +253,7 @@ function useMatrixGridKeyboardFocus(index, disabled, visibleIds, onNavigate) {
|
|
|
242
253
|
return false;
|
|
243
254
|
}
|
|
244
255
|
const originId = focusedRef.current ?? id;
|
|
245
|
-
const nextId =
|
|
256
|
+
const nextId = resolveNextNavigableId(index, originId, key, isNavigable);
|
|
246
257
|
if (nextId === null || nextId === originId)
|
|
247
258
|
return true;
|
|
248
259
|
focusedRef.current = nextId;
|
|
@@ -255,9 +266,19 @@ function useMatrixGridKeyboardFocus(index, disabled, visibleIds, onNavigate) {
|
|
|
255
266
|
mounted.focus();
|
|
256
267
|
}
|
|
257
268
|
return true;
|
|
258
|
-
}, [disabled, index, onNavigate]);
|
|
269
|
+
}, [disabled, index, isNavigable, onNavigate]);
|
|
259
270
|
return { focusedId, onFocus, onKeyDown, registerCell, tabStopId };
|
|
260
271
|
}
|
|
272
|
+
function resolveNextNavigableId(index, originId, key, isNavigable) {
|
|
273
|
+
let candidate = getMatrixGridNextFocusId(index, originId, key);
|
|
274
|
+
while (candidate !== null && candidate !== originId && !isNavigable(candidate)) {
|
|
275
|
+
const nextCandidate = getMatrixGridNextFocusId(index, candidate, key);
|
|
276
|
+
if (nextCandidate === candidate)
|
|
277
|
+
return originId;
|
|
278
|
+
candidate = nextCandidate;
|
|
279
|
+
}
|
|
280
|
+
return candidate;
|
|
281
|
+
}
|
|
261
282
|
|
|
262
283
|
// src/features/matrix-grid/adapters/inbound/MatrixGrid.tsx
|
|
263
284
|
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
@@ -270,6 +291,7 @@ function MatrixGridInner({
|
|
|
270
291
|
onSelectionChange,
|
|
271
292
|
onViewportChange,
|
|
272
293
|
overscanPixels = 160,
|
|
294
|
+
protectedCellIds = [],
|
|
273
295
|
readOnly = false,
|
|
274
296
|
renderCell,
|
|
275
297
|
selectedCellIds = [],
|
|
@@ -289,16 +311,18 @@ function MatrixGridInner({
|
|
|
289
311
|
const cellPlacements = React2.useMemo(() => getVisibleMatrixCells(layout, cells, viewport, overscanPixels), [cells, layout, overscanPixels, viewport]);
|
|
290
312
|
const [visibleIds, setVisibleIds] = React2.useState([]);
|
|
291
313
|
const anchorId = React2.useRef(null);
|
|
314
|
+
const protectedCellIdSet = React2.useMemo(() => new Set(protectedCellIds), [protectedCellIds]);
|
|
315
|
+
const isNavigable = React2.useCallback((id) => !protectedCellIdSet.has(id), [protectedCellIdSet]);
|
|
292
316
|
const handleViewportChange = React2.useCallback((proposal) => {
|
|
293
317
|
if (controlledViewport === undefined)
|
|
294
318
|
setUncontrolledViewport(proposal);
|
|
295
319
|
onViewportChange?.(proposal);
|
|
296
320
|
}, [controlledViewport, onViewportChange]);
|
|
297
321
|
const activateCell = React2.useCallback((cell, event) => {
|
|
298
|
-
if (interactionPolicy === "passive" || readOnly)
|
|
322
|
+
if (interactionPolicy === "passive" || readOnly || protectedCellIdSet.has(cell.id))
|
|
299
323
|
return;
|
|
300
324
|
const intent = isRangeSelectionEvent(event) ? "range" : resolveSelectionEventIntent(event, "pointer");
|
|
301
|
-
onSelectionChange?.(resolveMatrixGridSelection(sparseIndex.entries, selectedCellIds, cell.id, anchorId.current, intent, selectionMode));
|
|
325
|
+
onSelectionChange?.(resolveMatrixGridSelection(sparseIndex.entries, selectedCellIds, cell.id, anchorId.current, intent, selectionMode, protectedCellIdSet));
|
|
302
326
|
if (intent !== "range")
|
|
303
327
|
anchorId.current = cell.id;
|
|
304
328
|
}, [
|
|
@@ -307,7 +331,8 @@ function MatrixGridInner({
|
|
|
307
331
|
readOnly,
|
|
308
332
|
selectedCellIds,
|
|
309
333
|
selectionMode,
|
|
310
|
-
sparseIndex.entries
|
|
334
|
+
sparseIndex.entries,
|
|
335
|
+
protectedCellIdSet
|
|
311
336
|
]);
|
|
312
337
|
const handleKeyboardNavigation = React2.useCallback((targetId, originId, shiftKey) => {
|
|
313
338
|
anchorId.current ??= originId;
|
|
@@ -317,7 +342,7 @@ function MatrixGridInner({
|
|
|
317
342
|
if (cell)
|
|
318
343
|
activateCell(getMatrixCellPlacement(layout, cell), { shiftKey: true });
|
|
319
344
|
}, [activateCell, layout, sparseIndex]);
|
|
320
|
-
const keyboard = useMatrixGridKeyboardFocus(sparseIndex, interactionPolicy === "passive", visibleIds, handleKeyboardNavigation);
|
|
345
|
+
const keyboard = useMatrixGridKeyboardFocus(sparseIndex, interactionPolicy === "passive", visibleIds, isNavigable, handleKeyboardNavigation);
|
|
321
346
|
const focusedPlacement = React2.useMemo(() => {
|
|
322
347
|
const focusedCell = keyboard.focusedId === null ? undefined : sparseIndex.cellById.get(keyboard.focusedId);
|
|
323
348
|
return focusedCell ? getMatrixCellPlacement(layout, focusedCell) : undefined;
|
|
@@ -339,7 +364,7 @@ function MatrixGridInner({
|
|
|
339
364
|
const cell = cellById.get(item.id);
|
|
340
365
|
if (!cell)
|
|
341
366
|
return null;
|
|
342
|
-
return renderMatrixCell(cell, selectedCellIds, theme.semantics.neutral.divider, theme.colors.primary, renderCell, activateCell, keyboard, interactionPolicy === "passive", readOnly, testID);
|
|
367
|
+
return renderMatrixCell(cell, selectedCellIds, theme.semantics.neutral.divider, theme.colors.primary, renderCell, activateCell, keyboard, interactionPolicy === "passive", protectedCellIdSet.has(cell.id), readOnly, testID);
|
|
343
368
|
},
|
|
344
369
|
testID,
|
|
345
370
|
viewport,
|
|
@@ -348,7 +373,7 @@ function MatrixGridInner({
|
|
|
348
373
|
})
|
|
349
374
|
});
|
|
350
375
|
}
|
|
351
|
-
function renderMatrixCell(cell, selectedCellIds, dividerColor, selectedColor, renderCell, activateCell, keyboard, passive, readOnly, testID) {
|
|
376
|
+
function renderMatrixCell(cell, selectedCellIds, dividerColor, selectedColor, renderCell, activateCell, keyboard, passive, protectedCell, readOnly, testID) {
|
|
352
377
|
const selected = selectedCellIds.includes(cell.id);
|
|
353
378
|
const canInteract = keyboard.tabStopId === cell.id;
|
|
354
379
|
return /* @__PURE__ */ jsx2(MatrixGridKeyboardProxy, {
|
|
@@ -357,7 +382,7 @@ function renderMatrixCell(cell, selectedCellIds, dividerColor, selectedColor, re
|
|
|
357
382
|
return true;
|
|
358
383
|
if (!["Enter", " "].includes(event.key))
|
|
359
384
|
return false;
|
|
360
|
-
if (passive || readOnly)
|
|
385
|
+
if (passive || protectedCell || readOnly)
|
|
361
386
|
return false;
|
|
362
387
|
activateCell(cell, event);
|
|
363
388
|
return true;
|
|
@@ -366,9 +391,9 @@ function renderMatrixCell(cell, selectedCellIds, dividerColor, selectedColor, re
|
|
|
366
391
|
accessibilityActions: [{ name: "activate" }],
|
|
367
392
|
accessibilityLabel: `Cell ${cell.id}`,
|
|
368
393
|
accessibilityRole: "button",
|
|
369
|
-
accessibilityState: { disabled: passive, selected },
|
|
370
|
-
disabled: passive,
|
|
371
|
-
focusable: import_react_native.Platform.OS !== "web" || canInteract,
|
|
394
|
+
accessibilityState: { disabled: passive || protectedCell, selected },
|
|
395
|
+
disabled: passive || protectedCell,
|
|
396
|
+
focusable: !protectedCell && (import_react_native.Platform.OS !== "web" || canInteract),
|
|
372
397
|
style: [
|
|
373
398
|
styles.cell,
|
|
374
399
|
{
|