@octanejs/aria 0.0.5 → 0.0.7

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.
Files changed (124) hide show
  1. package/package.json +7 -7
  2. package/src/components/Table.ts +2618 -0
  3. package/src/components/Tree.ts +1450 -0
  4. package/src/components/TreeDropTargetDelegate.ts +363 -0
  5. package/src/components/index.ts +57 -0
  6. package/src/components/utils.ts +3 -6
  7. package/src/grid/GridKeyboardDelegate.ts +514 -0
  8. package/src/grid/useGrid.ts +263 -0
  9. package/src/grid/useGridCell.ts +346 -0
  10. package/src/grid/useGridRow.ts +108 -0
  11. package/src/grid/useGridRowGroup.ts +29 -0
  12. package/src/grid/utils.ts +24 -0
  13. package/src/index.ts +38 -0
  14. package/src/intl/autocomplete/index.ts +9 -2
  15. package/src/intl/breadcrumbs/index.ts +9 -2
  16. package/src/intl/combobox/index.ts +9 -2
  17. package/src/intl/compileMessages.ts +356 -0
  18. package/src/intl/grid/index.ts +9 -2
  19. package/src/intl/gridlist/index.ts +9 -2
  20. package/src/intl/menu/index.ts +9 -2
  21. package/src/intl/numberfield/index.ts +9 -2
  22. package/src/intl/overlays/index.ts +9 -2
  23. package/src/intl/searchfield/index.ts +9 -2
  24. package/src/intl/spinbutton/index.ts +9 -2
  25. package/src/intl/table/ar-AE.json +13 -0
  26. package/src/intl/table/bg-BG.json +13 -0
  27. package/src/intl/table/cs-CZ.json +13 -0
  28. package/src/intl/table/da-DK.json +13 -0
  29. package/src/intl/table/de-DE.json +13 -0
  30. package/src/intl/table/el-GR.json +13 -0
  31. package/src/intl/table/en-US.json +13 -0
  32. package/src/intl/table/es-ES.json +13 -0
  33. package/src/intl/table/et-EE.json +13 -0
  34. package/src/intl/table/fi-FI.json +13 -0
  35. package/src/intl/table/fr-FR.json +13 -0
  36. package/src/intl/table/he-IL.json +13 -0
  37. package/src/intl/table/hr-HR.json +13 -0
  38. package/src/intl/table/hu-HU.json +13 -0
  39. package/src/intl/table/index.ts +82 -0
  40. package/src/intl/table/it-IT.json +13 -0
  41. package/src/intl/table/ja-JP.json +13 -0
  42. package/src/intl/table/ko-KR.json +13 -0
  43. package/src/intl/table/lt-LT.json +13 -0
  44. package/src/intl/table/lv-LV.json +13 -0
  45. package/src/intl/table/nb-NO.json +13 -0
  46. package/src/intl/table/nl-NL.json +13 -0
  47. package/src/intl/table/pl-PL.json +13 -0
  48. package/src/intl/table/pt-BR.json +13 -0
  49. package/src/intl/table/pt-PT.json +13 -0
  50. package/src/intl/table/ro-RO.json +13 -0
  51. package/src/intl/table/ru-RU.json +13 -0
  52. package/src/intl/table/sk-SK.json +13 -0
  53. package/src/intl/table/sl-SI.json +13 -0
  54. package/src/intl/table/sr-SP.json +13 -0
  55. package/src/intl/table/sv-SE.json +13 -0
  56. package/src/intl/table/tr-TR.json +13 -0
  57. package/src/intl/table/uk-UA.json +13 -0
  58. package/src/intl/table/zh-CN.json +13 -0
  59. package/src/intl/table/zh-TW.json +13 -0
  60. package/src/intl/tag/index.ts +9 -2
  61. package/src/intl/tree/ar-AE.json +4 -0
  62. package/src/intl/tree/bg-BG.json +4 -0
  63. package/src/intl/tree/cs-CZ.json +4 -0
  64. package/src/intl/tree/da-DK.json +4 -0
  65. package/src/intl/tree/de-DE.json +4 -0
  66. package/src/intl/tree/el-GR.json +4 -0
  67. package/src/intl/tree/en-US.json +4 -0
  68. package/src/intl/tree/es-ES.json +4 -0
  69. package/src/intl/tree/et-EE.json +4 -0
  70. package/src/intl/tree/fi-FI.json +4 -0
  71. package/src/intl/tree/fr-FR.json +4 -0
  72. package/src/intl/tree/he-IL.json +4 -0
  73. package/src/intl/tree/hr-HR.json +4 -0
  74. package/src/intl/tree/hu-HU.json +4 -0
  75. package/src/intl/tree/index.ts +82 -0
  76. package/src/intl/tree/it-IT.json +4 -0
  77. package/src/intl/tree/ja-JP.json +4 -0
  78. package/src/intl/tree/ko-KR.json +4 -0
  79. package/src/intl/tree/lt-LT.json +4 -0
  80. package/src/intl/tree/lv-LV.json +4 -0
  81. package/src/intl/tree/nb-NO.json +4 -0
  82. package/src/intl/tree/nl-NL.json +4 -0
  83. package/src/intl/tree/pl-PL.json +4 -0
  84. package/src/intl/tree/pt-BR.json +4 -0
  85. package/src/intl/tree/pt-PT.json +4 -0
  86. package/src/intl/tree/ro-RO.json +4 -0
  87. package/src/intl/tree/ru-RU.json +4 -0
  88. package/src/intl/tree/sk-SK.json +4 -0
  89. package/src/intl/tree/sl-SI.json +4 -0
  90. package/src/intl/tree/sr-SP.json +4 -0
  91. package/src/intl/tree/sv-SE.json +4 -0
  92. package/src/intl/tree/tr-TR.json +4 -0
  93. package/src/intl/tree/uk-UA.json +4 -0
  94. package/src/intl/tree/zh-CN.json +4 -0
  95. package/src/intl/tree/zh-TW.json +4 -0
  96. package/src/stately/grid/GridCollection.ts +200 -0
  97. package/src/stately/grid/useGridState.ts +160 -0
  98. package/src/stately/index.ts +43 -0
  99. package/src/stately/table/Cell.ts +49 -0
  100. package/src/stately/table/Column.ts +121 -0
  101. package/src/stately/table/Row.ts +145 -0
  102. package/src/stately/table/TableBody.ts +66 -0
  103. package/src/stately/table/TableCollection.ts +390 -0
  104. package/src/stately/table/TableColumnLayout.ts +148 -0
  105. package/src/stately/table/TableHeader.ts +65 -0
  106. package/src/stately/table/TableUtils.ts +269 -0
  107. package/src/stately/table/useTableColumnResizeState.ts +202 -0
  108. package/src/stately/table/useTableState.ts +235 -0
  109. package/src/stately/table/useTreeGridState.ts +248 -0
  110. package/src/table/TableKeyboardDelegate.ts +205 -0
  111. package/src/table/useTable.ts +175 -0
  112. package/src/table/useTableCell.ts +81 -0
  113. package/src/table/useTableColumnHeader.ts +155 -0
  114. package/src/table/useTableColumnResize.ts +374 -0
  115. package/src/table/useTableHeaderRow.ts +57 -0
  116. package/src/table/useTableRow.ts +198 -0
  117. package/src/table/useTableRowGroup.ts +13 -0
  118. package/src/table/useTableSelectionCheckbox.ts +105 -0
  119. package/src/table/utils.ts +42 -0
  120. package/src/tree/useTree.ts +76 -0
  121. package/src/tree/useTreeItem.ts +109 -0
  122. package/src/utils/mergeRefs.ts +1 -4
  123. package/src/utils/useFormReset.ts +1 -2
  124. package/src/utils/useObjectRef.ts +2 -8
@@ -0,0 +1,248 @@
1
+ // Ported from react-stately (source: .react-spectrum/packages/react-stately/src/table/useTreeGridState.ts).
2
+ // octane adaptations: public-hook slot threading (splitSlot/subSlot) per the binding
3
+ // convention; React element types → `any` descriptors; upstream's implicitly-typed
4
+ // `nodes` parameter is typed as `Iterable<GridNode<T>>`; the feature flag import points
5
+ // at the ported `../flags`; explicit dependency arrays are kept verbatim (they retain
6
+ // React's exact behavior in octane).
7
+ import type { Key } from '@react-types/shared';
8
+ import { useMemo } from 'octane';
9
+
10
+ import { S, splitSlot, subSlot } from '../../internal';
11
+ import { CollectionBuilder } from '../collections/CollectionBuilder';
12
+ import type { GridNode } from '../grid/GridCollection';
13
+ import { TableCollection } from './TableCollection';
14
+ import { tableNestedRows } from '../flags';
15
+ import { type TableState, type TableStateProps, useTableState } from './useTableState';
16
+ import { useControlledState } from '../utils/useControlledState';
17
+
18
+ export interface TreeGridState<T> extends Omit<TableState<T>, 'expandedKeys'> {
19
+ /** A set of keys for items that are expanded. */
20
+ expandedKeys: 'all' | Set<Key>;
21
+ /** Toggles the expanded state for a row by its key. */
22
+ toggleKey(key: Key): void;
23
+ /** The key map containing nodes representing the collection's tree grid structure. */
24
+ keyMap: Map<Key, GridNode<T>>;
25
+ /** The number of leaf columns provided by the user. */
26
+ userColumnCount: number;
27
+ }
28
+
29
+ export interface TreeGridStateProps<T> extends Omit<TableStateProps<T>, 'collection'> {
30
+ /** The currently expanded keys in the collection (controlled). */
31
+ UNSTABLE_expandedKeys?: 'all' | Iterable<Key>;
32
+ /** The initial expanded keys in the collection (uncontrolled). */
33
+ UNSTABLE_defaultExpandedKeys?: 'all' | Iterable<Key>;
34
+ /** Handler that is called when items are expanded or collapsed. */
35
+ UNSTABLE_onExpandedChange?: (keys: Set<Key>) => any;
36
+ }
37
+
38
+ /**
39
+ * Provides state management for a tree grid component. Handles building a collection of columns and
40
+ * rows from props. In addition, it tracks and manages expanded rows, row selection, and sort order
41
+ * changes.
42
+ */
43
+ export function UNSTABLE_useTreeGridState<T extends object>(
44
+ props: TreeGridStateProps<T>,
45
+ ): TreeGridState<T>;
46
+ // Slot-threading form: sibling ported hooks pass their derived sub-slot as the trailing arg.
47
+ export function UNSTABLE_useTreeGridState<T extends object>(
48
+ props: TreeGridStateProps<T>,
49
+ slot: symbol | undefined,
50
+ ): TreeGridState<T>;
51
+ export function UNSTABLE_useTreeGridState(...args: any[]): TreeGridState<any> {
52
+ const [user, slotArg] = splitSlot(args);
53
+ const slot = slotArg ?? S('UNSTABLE_useTreeGridState');
54
+ const props = user[0] as TreeGridStateProps<any>;
55
+
56
+ let {
57
+ selectionMode = 'none',
58
+ showSelectionCheckboxes,
59
+ showDragButtons,
60
+ UNSTABLE_expandedKeys: propExpandedKeys,
61
+ UNSTABLE_defaultExpandedKeys: propDefaultExpandedKeys,
62
+ UNSTABLE_onExpandedChange,
63
+ children,
64
+ } = props;
65
+
66
+ if (!tableNestedRows()) {
67
+ throw new Error('Feature flag for table nested rows must be enabled to use useTreeGridState.');
68
+ }
69
+
70
+ let [expandedKeys, setExpandedKeys] = useControlledState(
71
+ propExpandedKeys ? convertExpanded(propExpandedKeys) : undefined,
72
+ propDefaultExpandedKeys ? convertExpanded(propDefaultExpandedKeys) : new Set(),
73
+ UNSTABLE_onExpandedChange,
74
+ subSlot(slot, 'expanded'),
75
+ );
76
+
77
+ let context = useMemo(
78
+ () => ({
79
+ showSelectionCheckboxes: showSelectionCheckboxes && selectionMode !== 'none',
80
+ showDragButtons: showDragButtons,
81
+ selectionMode,
82
+ columns: [],
83
+ }),
84
+ [children, showSelectionCheckboxes, selectionMode, showDragButtons],
85
+ subSlot(slot, 'context'),
86
+ );
87
+
88
+ let builder = useMemo(() => new CollectionBuilder<any>(), [], subSlot(slot, 'builder'));
89
+ let nodes = useMemo(
90
+ () => builder.build({ children: children as any }, context),
91
+ [builder, children, context],
92
+ subSlot(slot, 'nodes'),
93
+ );
94
+ let treeGridCollection = useMemo(
95
+ () => {
96
+ return generateTreeGridCollection<any>(nodes, {
97
+ showSelectionCheckboxes,
98
+ showDragButtons,
99
+ expandedKeys,
100
+ });
101
+ },
102
+ [nodes, showSelectionCheckboxes, showDragButtons, expandedKeys],
103
+ subSlot(slot, 'treeGrid'),
104
+ );
105
+
106
+ let onToggle = (key: Key) => {
107
+ setExpandedKeys(toggleKey(expandedKeys, key, treeGridCollection));
108
+ };
109
+
110
+ let collection = useMemo(
111
+ () => {
112
+ return new TableCollection(treeGridCollection.tableNodes, null, context);
113
+ },
114
+ [context, treeGridCollection.tableNodes],
115
+ subSlot(slot, 'collection'),
116
+ );
117
+
118
+ let tableState = useTableState({ ...props, collection }, subSlot(slot, 'table'));
119
+ return {
120
+ ...tableState,
121
+ keyMap: treeGridCollection.keyMap,
122
+ userColumnCount: treeGridCollection.userColumnCount,
123
+ expandedKeys,
124
+ toggleKey: onToggle,
125
+ treeColumn: tableState.treeColumn ?? collection.rowHeaderColumnKeys.keys().next().value ?? null,
126
+ };
127
+ }
128
+
129
+ function toggleKey<T>(
130
+ currentExpandedKeys: 'all' | Set<Key>,
131
+ key: Key,
132
+ collection: TreeGridCollection<T>,
133
+ ): Set<Key> {
134
+ let updatedExpandedKeys: Set<Key>;
135
+ if (currentExpandedKeys === 'all') {
136
+ updatedExpandedKeys = new Set(
137
+ collection.flattenedRows
138
+ .filter(
139
+ (row) =>
140
+ row.props.UNSTABLE_childItems || row.props.children.length > collection.userColumnCount,
141
+ )
142
+ .map((row) => row.key),
143
+ );
144
+ updatedExpandedKeys.delete(key);
145
+ } else {
146
+ updatedExpandedKeys = new Set(currentExpandedKeys);
147
+ if (updatedExpandedKeys.has(key)) {
148
+ updatedExpandedKeys.delete(key);
149
+ } else {
150
+ updatedExpandedKeys.add(key);
151
+ }
152
+ }
153
+
154
+ return updatedExpandedKeys;
155
+ }
156
+
157
+ function convertExpanded(expanded: 'all' | Iterable<Key>): 'all' | Set<Key> {
158
+ if (!expanded) {
159
+ return new Set<Key>();
160
+ }
161
+
162
+ return expanded === 'all' ? 'all' : new Set(expanded);
163
+ }
164
+
165
+ interface TreeGridCollectionOptions {
166
+ showSelectionCheckboxes?: boolean;
167
+ showDragButtons?: boolean;
168
+ expandedKeys: 'all' | Set<Key>;
169
+ }
170
+
171
+ interface TreeGridCollection<T> {
172
+ keyMap: Map<Key, GridNode<T>>;
173
+ tableNodes: GridNode<T>[];
174
+ flattenedRows: GridNode<T>[];
175
+ userColumnCount: number;
176
+ }
177
+ function generateTreeGridCollection<T>(
178
+ nodes: Iterable<GridNode<T>>,
179
+ opts: TreeGridCollectionOptions,
180
+ ): TreeGridCollection<T> {
181
+ let { expandedKeys = new Set() } = opts;
182
+
183
+ let body: GridNode<T> | null = null;
184
+ let flattenedRows: GridNode<T>[] = [];
185
+ let userColumnCount = 0;
186
+ let originalColumns: GridNode<T>[] = [];
187
+ let keyMap = new Map();
188
+
189
+ let topLevelRows: GridNode<T>[] = [];
190
+ let visit = (node: GridNode<T>) => {
191
+ switch (node.type) {
192
+ case 'body':
193
+ body = node;
194
+ keyMap.set(body.key, body);
195
+ break;
196
+ case 'column':
197
+ if (!node.hasChildNodes) {
198
+ userColumnCount++;
199
+ }
200
+ break;
201
+ case 'item':
202
+ topLevelRows.push(node);
203
+ return;
204
+ }
205
+
206
+ for (let child of node.childNodes) {
207
+ visit(child);
208
+ }
209
+ };
210
+
211
+ for (let node of nodes) {
212
+ if (node.type === 'column') {
213
+ originalColumns.push(node);
214
+ }
215
+ visit(node);
216
+ }
217
+
218
+ // Update each grid node in the treegrid table with values specific to a treegrid structure. Also store a set of flattened row nodes for TableCollection to consume
219
+ let visitNode = (node: GridNode<T>) => {
220
+ if (node.type === 'item') {
221
+ flattenedRows.push(node);
222
+ }
223
+
224
+ keyMap.set(node.key, node);
225
+
226
+ for (let child of node.childNodes) {
227
+ if (!(child.type === 'item' && expandedKeys !== 'all' && !expandedKeys.has(node.key))) {
228
+ if (child.type === 'item') {
229
+ visitNode(child);
230
+ } else {
231
+ // We enforce that the cells come before rows so can just reuse cell index
232
+ visitNode(child);
233
+ }
234
+ }
235
+ }
236
+ };
237
+
238
+ for (let node of topLevelRows) {
239
+ visitNode(node as GridNode<T>);
240
+ }
241
+
242
+ return {
243
+ keyMap,
244
+ userColumnCount,
245
+ flattenedRows,
246
+ tableNodes: [...originalColumns, { ...body!, childNodes: flattenedRows }],
247
+ };
248
+ }
@@ -0,0 +1,205 @@
1
+ // Ported from react-aria (source: .react-spectrum/packages/react-aria/src/table/TableKeyboardDelegate.ts).
2
+ // octane adaptations:
3
+ // - `getChildNodes`/`getFirstItem` come from the ported stately collections helpers and
4
+ // `ITableCollection` from the ported stately table collection.
5
+ // - Pure class (no hooks) — no slot threading involved.
6
+ import { getChildNodes, getFirstItem } from '../stately/collections/getChildNodes';
7
+ import { GridKeyboardDelegate } from '../grid/GridKeyboardDelegate';
8
+ import type { ITableCollection } from '../stately/table/TableCollection';
9
+ import type { Key, Node } from '@react-types/shared';
10
+
11
+ export class TableKeyboardDelegate<T> extends GridKeyboardDelegate<T, ITableCollection<T>> {
12
+ protected isCell(node: Node<T>): boolean {
13
+ return node.type === 'cell' || node.type === 'rowheader' || node.type === 'column';
14
+ }
15
+
16
+ getKeyBelow(key: Key, options?: { includeDisabled?: boolean }): Key | null {
17
+ let startItem = this.collection.getItem(key);
18
+ if (!startItem) {
19
+ return null;
20
+ }
21
+
22
+ // If focus was on a column, then focus the first child column if any,
23
+ // or find the corresponding cell in the first row.
24
+ if (startItem.type === 'column') {
25
+ let child = getFirstItem(getChildNodes(startItem, this.collection));
26
+ if (child) {
27
+ return child.key;
28
+ }
29
+
30
+ let firstKey = this.getFirstKey();
31
+ if (firstKey == null) {
32
+ return null;
33
+ }
34
+
35
+ let firstItem = this.collection.getItem(firstKey);
36
+ if (!firstItem) {
37
+ return null;
38
+ }
39
+
40
+ return super.getKeyForItemInRowByIndex(firstKey, startItem.index);
41
+ }
42
+
43
+ return super.getKeyBelow(key, options);
44
+ }
45
+
46
+ getKeyAbove(key: Key, options?: { includeDisabled?: boolean }): Key | null {
47
+ let startItem = this.collection.getItem(key);
48
+ if (!startItem) {
49
+ return null;
50
+ }
51
+
52
+ // If focus was on a column, focus the parent column if any
53
+ if (startItem.type === 'column') {
54
+ let parent =
55
+ startItem.parentKey != null ? this.collection.getItem(startItem.parentKey) : null;
56
+ if (parent && parent.type === 'column') {
57
+ return parent.key;
58
+ }
59
+
60
+ return null;
61
+ }
62
+
63
+ // only return above row key if not header row
64
+ let superKey = super.getKeyAbove(key, options);
65
+ let superItem = superKey != null ? this.collection.getItem(superKey) : null;
66
+ if (superItem && superItem.type !== 'headerrow') {
67
+ return superKey;
68
+ }
69
+
70
+ // If no item was found, and focus was on a cell, then focus the
71
+ // corresponding column header.
72
+ if (this.isCell(startItem)) {
73
+ return this.collection.columns[startItem.index].key;
74
+ }
75
+
76
+ // If focus was on a row, then focus the first column header.
77
+ return this.collection.columns[0].key;
78
+ }
79
+
80
+ private findNextColumnKey(column: Node<T>): Key | null {
81
+ // Search following columns
82
+ let key = this.findNextKey(column.key, (item) => item.type === 'column');
83
+ if (key != null) {
84
+ return key;
85
+ }
86
+
87
+ // Wrap around to the first column
88
+ let row = this.collection.headerRows[column.level];
89
+ for (let item of getChildNodes(row, this.collection)) {
90
+ if (item.type === 'column') {
91
+ return item.key;
92
+ }
93
+ }
94
+
95
+ return null;
96
+ }
97
+
98
+ private findPreviousColumnKey(column: Node<T>): Key | null {
99
+ // Search previous columns
100
+ let key = this.findPreviousKey(column.key, (item) => item.type === 'column');
101
+ if (key != null) {
102
+ return key;
103
+ }
104
+
105
+ // Wrap around to the last column
106
+ let row = this.collection.headerRows[column.level];
107
+ let childNodes = [...getChildNodes(row, this.collection)];
108
+ for (let i = childNodes.length - 1; i >= 0; i--) {
109
+ let item = childNodes[i];
110
+ if (item.type === 'column') {
111
+ return item.key;
112
+ }
113
+ }
114
+
115
+ return null;
116
+ }
117
+
118
+ getKeyRightOf(key: Key): Key | null {
119
+ let item = this.collection.getItem(key);
120
+ if (!item) {
121
+ return null;
122
+ }
123
+
124
+ // If focus was on a column, then focus the next column
125
+ if (item.type === 'column') {
126
+ return this.direction === 'rtl'
127
+ ? this.findPreviousColumnKey(item)
128
+ : this.findNextColumnKey(item);
129
+ }
130
+
131
+ return super.getKeyRightOf(key);
132
+ }
133
+
134
+ getKeyLeftOf(key: Key): Key | null {
135
+ let item = this.collection.getItem(key);
136
+ if (!item) {
137
+ return null;
138
+ }
139
+
140
+ // If focus was on a column, then focus the previous column
141
+ if (item.type === 'column') {
142
+ return this.direction === 'rtl'
143
+ ? this.findNextColumnKey(item)
144
+ : this.findPreviousColumnKey(item);
145
+ }
146
+
147
+ return super.getKeyLeftOf(key);
148
+ }
149
+
150
+ getKeyForSearch(search: string, fromKey?: Key): Key | null {
151
+ if (!this.collator) {
152
+ return null;
153
+ }
154
+
155
+ let collection = this.collection;
156
+ let key = fromKey ?? this.getFirstKey();
157
+ if (key == null) {
158
+ return null;
159
+ }
160
+
161
+ // If the starting key is a cell, search from its parent row.
162
+ let startItem = collection.getItem(key);
163
+ if (startItem?.type === 'cell') {
164
+ key = startItem.parentKey ?? null;
165
+ }
166
+
167
+ let hasWrapped = false;
168
+ while (key != null) {
169
+ let item = collection.getItem(key);
170
+ if (!item) {
171
+ return null;
172
+ }
173
+
174
+ if (item.textValue) {
175
+ let substring = item.textValue.slice(0, search.length);
176
+ if (this.collator.compare(substring, search) === 0) {
177
+ return item.key;
178
+ }
179
+ }
180
+
181
+ // Check each of the row header cells in this row for a match
182
+ for (let cell of getChildNodes(item, this.collection)) {
183
+ let column = collection.columns[cell.index];
184
+ if (collection.rowHeaderColumnKeys.has(column.key) && cell.textValue) {
185
+ let substring = cell.textValue.slice(0, search.length);
186
+ if (this.collator.compare(substring, search) === 0) {
187
+ // If we started on a cell, end on the matching cell. Otherwise, end on the row.
188
+ let fromItem = fromKey != null ? collection.getItem(fromKey) : startItem;
189
+ return fromItem?.type === 'cell' ? cell.key : item.key;
190
+ }
191
+ }
192
+ }
193
+
194
+ key = this.getKeyBelow(key);
195
+
196
+ // Wrap around when reaching the end of the collection
197
+ if (key == null && !hasWrapped) {
198
+ key = this.getFirstKey();
199
+ hasWrapped = true;
200
+ }
201
+ }
202
+
203
+ return null;
204
+ }
205
+ }
@@ -0,0 +1,175 @@
1
+ // Ported from react-aria (source: .react-spectrum/packages/react-aria/src/table/useTable.ts).
2
+ // octane adaptations:
3
+ // - `TableState`/`TreeGridState` come from the ported stately table hooks; the grid-area
4
+ // imports resolve to the ported src/grid/ modules.
5
+ // - The Parcel glob intl import becomes the generated src/intl/table index (verbatim
6
+ // dictionaries).
7
+ // - Public-hook slot threading (splitSlot/subSlot); explicit dependency arrays are kept
8
+ // verbatim.
9
+ import { announce } from '../live-announcer/LiveAnnouncer';
10
+
11
+ import { GridAria, GridProps, useGrid } from '../grid/useGrid';
12
+ import { gridIds } from './utils';
13
+ import intlMessages from '../intl/table';
14
+ import type { Key, LayoutDelegate, Rect, RefObject, Size } from '@react-types/shared';
15
+ import { mergeProps } from '../utils/mergeProps';
16
+ import { TableKeyboardDelegate } from './TableKeyboardDelegate';
17
+ import type { TableState } from '../stately/table/useTableState';
18
+ import type { TreeGridState } from '../stately/table/useTreeGridState';
19
+ import { useCollator } from '../i18n/useCollator';
20
+ import { useDescription } from '../utils/useDescription';
21
+ import { useId } from '../utils/useId';
22
+ import { useLocale } from '../i18n/I18nProvider';
23
+ import { useLocalizedStringFormatter } from '../i18n/useLocalizedStringFormatter';
24
+ import { useMemo } from 'octane';
25
+ import { useUpdateEffect } from '../utils/useUpdateEffect';
26
+
27
+ import { S, splitSlot, subSlot } from '../internal';
28
+
29
+ export interface AriaTableProps extends GridProps {
30
+ /**
31
+ * The layout object for the table. Computes what content is visible and how to position and style
32
+ * them.
33
+ */
34
+ layoutDelegate?: LayoutDelegate;
35
+ /** @deprecated - Use layoutDelegate instead. */
36
+ layout?: DeprecatedLayout;
37
+ }
38
+
39
+ interface DeprecatedLayout {
40
+ getLayoutInfo(key: Key): DeprecatedLayoutInfo;
41
+ getContentSize(): Size;
42
+ virtualizer: DeprecatedVirtualizer;
43
+ }
44
+
45
+ interface DeprecatedLayoutInfo {
46
+ rect: Rect;
47
+ }
48
+
49
+ interface DeprecatedVirtualizer {
50
+ visibleRect: Rect;
51
+ }
52
+
53
+ /**
54
+ * Provides the behavior and accessibility implementation for a table component. A table displays
55
+ * data in rows and columns and enables a user to navigate its contents via directional navigation
56
+ * keys, and optionally supports row selection and sorting.
57
+ *
58
+ * @param props - Props for the table.
59
+ * @param state - State for the table, as returned by `useTableState`.
60
+ * @param ref - The ref attached to the table element.
61
+ */
62
+ export function useTable<T>(
63
+ props: AriaTableProps,
64
+ state: TableState<T> | TreeGridState<T>,
65
+ ref: RefObject<HTMLElement | null>,
66
+ ): GridAria;
67
+ // Slot-threading form: sibling ported hooks pass their derived sub-slot as the trailing arg.
68
+ export function useTable<T>(
69
+ props: AriaTableProps,
70
+ state: TableState<T> | TreeGridState<T>,
71
+ ref: RefObject<HTMLElement | null>,
72
+ slot: symbol | undefined,
73
+ ): GridAria;
74
+ export function useTable(...args: any[]): GridAria {
75
+ const [user, slotArg] = splitSlot(args);
76
+ const slot = slotArg ?? S('useTable');
77
+ const props = user[0] as AriaTableProps;
78
+ const state = user[1] as TableState<any> | TreeGridState<any>;
79
+ const ref = user[2] as RefObject<HTMLElement | null>;
80
+
81
+ let { keyboardDelegate, isVirtualized, layoutDelegate, layout } = props;
82
+
83
+ // By default, a KeyboardDelegate is provided which uses the DOM to query layout information (e.g. for page up/page down).
84
+ // When virtualized, the layout object will be passed in as a prop and override this.
85
+ let collator = useCollator({ usage: 'search', sensitivity: 'base' }, subSlot(slot, 'collator'));
86
+ let { direction } = useLocale(subSlot(slot, 'locale'));
87
+ let disabledBehavior = state.selectionManager.disabledBehavior;
88
+ let delegate = useMemo(
89
+ () =>
90
+ keyboardDelegate ||
91
+ new TableKeyboardDelegate({
92
+ collection: state.collection,
93
+ disabledKeys: state.disabledKeys,
94
+ disabledBehavior,
95
+ ref,
96
+ direction,
97
+ collator,
98
+ layoutDelegate,
99
+ layout,
100
+ }),
101
+ [
102
+ keyboardDelegate,
103
+ state.collection,
104
+ state.disabledKeys,
105
+ disabledBehavior,
106
+ ref,
107
+ direction,
108
+ collator,
109
+ layoutDelegate,
110
+ layout,
111
+ ],
112
+ subSlot(slot, 'delegate'),
113
+ );
114
+ let id = useId(props.id, subSlot(slot, 'id'));
115
+ gridIds.set(state as TableState<any>, id);
116
+
117
+ let { gridProps } = useGrid(
118
+ {
119
+ ...props,
120
+ id,
121
+ keyboardDelegate: delegate,
122
+ },
123
+ state,
124
+ ref,
125
+ subSlot(slot, 'grid'),
126
+ );
127
+
128
+ // Override to include header rows
129
+ if (isVirtualized) {
130
+ gridProps['aria-rowcount'] = state.collection.size + state.collection.headerRows.length;
131
+ }
132
+
133
+ if (state.treeColumn != null) {
134
+ gridProps.role = 'treegrid';
135
+ }
136
+
137
+ let { column, direction: sortDirection } = state.sortDescriptor || {};
138
+ let stringFormatter = useLocalizedStringFormatter(
139
+ intlMessages,
140
+ '@react-aria/table',
141
+ subSlot(slot, 'strings'),
142
+ );
143
+ let sortDescription = useMemo(
144
+ () => {
145
+ let columnName = state.collection.columns.find((c) => c.key === column)?.textValue ?? '';
146
+ return sortDirection && column
147
+ ? stringFormatter.format(`${sortDirection}Sort`, { columnName })
148
+ : undefined;
149
+ },
150
+ [sortDirection, column, state.collection.columns],
151
+ subSlot(slot, 'sortDescription'),
152
+ );
153
+
154
+ let descriptionProps = useDescription(sortDescription, subSlot(slot, 'description'));
155
+
156
+ // Only announce after initial render, tabbing to the table will tell you the initial sort info already
157
+ useUpdateEffect(
158
+ () => {
159
+ if (sortDescription) {
160
+ announce(sortDescription, 'assertive', 500);
161
+ }
162
+ },
163
+ [sortDescription],
164
+ subSlot(slot, 'announceSort'),
165
+ );
166
+
167
+ return {
168
+ gridProps: mergeProps(gridProps, descriptionProps, {
169
+ // merge sort description with long press information
170
+ 'aria-describedby': [descriptionProps['aria-describedby'], gridProps['aria-describedby']]
171
+ .filter(Boolean)
172
+ .join(' '),
173
+ }),
174
+ };
175
+ }
@@ -0,0 +1,81 @@
1
+ // Ported from react-aria (source: .react-spectrum/packages/react-aria/src/table/useTableCell.ts).
2
+ // octane adaptations:
3
+ // - `TableState`/`GridNode` come from the ported stately hooks; `useGridCell` from the
4
+ // ported src/grid/; `DOMAttributes` is a local structural prop-bag alias.
5
+ // - Public-hook slot threading (splitSlot/subSlot).
6
+ import type { FocusableElement, RefObject } from '@react-types/shared';
7
+ import { getCellId } from './utils';
8
+ import type { GridNode } from '../stately/grid/GridCollection';
9
+ import type { TableState } from '../stately/table/useTableState';
10
+ import { useGridCell } from '../grid/useGridCell';
11
+
12
+ import { S, splitSlot, subSlot } from '../internal';
13
+
14
+ // octane adaptation: minimal structural DOMAttributes (upstream's drags React's synthetic
15
+ // handler types along).
16
+ type DOMAttributes = Record<string, any>;
17
+
18
+ export interface AriaTableCellProps {
19
+ /**
20
+ * An object representing the table cell. Contains all the relevant information that makes up the
21
+ * row header.
22
+ */
23
+ node: GridNode<unknown>;
24
+ /** Whether the cell is contained in a virtual scroller. */
25
+ isVirtualized?: boolean;
26
+ /** Whether selection should occur on press up instead of press down. */
27
+ shouldSelectOnPressUp?: boolean;
28
+ /**
29
+ * Handler that is called when a user performs an action on the cell.
30
+ * Please use onCellAction at the collection level instead.
31
+ *
32
+ * @deprecated
33
+ */
34
+ onAction?: () => void;
35
+ }
36
+
37
+ export interface TableCellAria {
38
+ /** Props for the table cell element. */
39
+ gridCellProps: DOMAttributes;
40
+ /** Whether the cell is currently in a pressed state. */
41
+ isPressed: boolean;
42
+ }
43
+
44
+ /**
45
+ * Provides the behavior and accessibility implementation for a cell in a table.
46
+ *
47
+ * @param props - Props for the cell.
48
+ * @param state - State of the table, as returned by `useTableState`.
49
+ * @param ref - The ref attached to the cell element.
50
+ */
51
+ export function useTableCell<T>(
52
+ props: AriaTableCellProps,
53
+ state: TableState<T>,
54
+ ref: RefObject<FocusableElement | null>,
55
+ ): TableCellAria;
56
+ // Slot-threading form: sibling ported hooks pass their derived sub-slot as the trailing arg.
57
+ export function useTableCell<T>(
58
+ props: AriaTableCellProps,
59
+ state: TableState<T>,
60
+ ref: RefObject<FocusableElement | null>,
61
+ slot: symbol | undefined,
62
+ ): TableCellAria;
63
+ export function useTableCell(...args: any[]): TableCellAria {
64
+ const [user, slotArg] = splitSlot(args);
65
+ const slot = slotArg ?? S('useTableCell');
66
+ const props = user[0] as AriaTableCellProps;
67
+ const state = user[1] as TableState<any>;
68
+ const ref = user[2] as RefObject<FocusableElement | null>;
69
+
70
+ let { gridCellProps, isPressed } = useGridCell(props, state, ref, subSlot(slot, 'gridCell'));
71
+ let columnKey = props.node.column?.key;
72
+ if (columnKey != null && state.collection.rowHeaderColumnKeys.has(columnKey)) {
73
+ gridCellProps.role = 'rowheader';
74
+ gridCellProps.id = getCellId(state, props.node.parentKey!, columnKey);
75
+ }
76
+
77
+ return {
78
+ gridCellProps,
79
+ isPressed,
80
+ };
81
+ }