@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,269 @@
1
+ // Ported from react-stately (source: .react-spectrum/packages/react-stately/src/table/TableUtils.ts).
2
+ // Verbatim (no React surface).
3
+ import type { Key } from '@react-types/shared';
4
+
5
+ import type { ColumnSize } from './Column';
6
+
7
+ // numbers and percents are considered static. *fr units or a lack of units are considered dynamic.
8
+ export function isStatic(width?: ColumnSize | null): boolean {
9
+ return width != null && (!isNaN(width as number) || String(width).match(/^(\d+)(?=%$)/) !== null);
10
+ }
11
+
12
+ export function parseFractionalUnit(width?: ColumnSize | null): number {
13
+ if (!width || typeof width === 'number') {
14
+ return 1;
15
+ }
16
+ let match = width.match(/^(.+)(?=fr$)/);
17
+ // if width is the incorrect format, just default it to a 1fr
18
+ if (!match) {
19
+ if (process.env.NODE_ENV !== 'production') {
20
+ console.warn(
21
+ `width: ${width} is not a supported format, width should be a number (ex. 150), percentage (ex. '50%') or fr unit (ex. '2fr')`,
22
+ "defaulting to '1fr'",
23
+ );
24
+ }
25
+ return 1;
26
+ }
27
+ return parseFloat(match[0]);
28
+ }
29
+
30
+ export function parseStaticWidth(width: number | string, tableWidth: number): number {
31
+ if (typeof width === 'string') {
32
+ let match = width.match(/^(\d+)(?=%$)/);
33
+ if (!match) {
34
+ throw new Error('Only percentages or numbers are supported for static column widths');
35
+ }
36
+ return tableWidth * (parseFloat(match[0]) / 100);
37
+ }
38
+ return width;
39
+ }
40
+
41
+ export function getMaxWidth(
42
+ maxWidth: number | string | null | undefined,
43
+ tableWidth: number,
44
+ ): number {
45
+ return maxWidth != null ? parseStaticWidth(maxWidth, tableWidth) : Number.MAX_SAFE_INTEGER;
46
+ }
47
+
48
+ // cannot support FR units, we'd need to know everything else in the table to do that
49
+ export function getMinWidth(minWidth: number | string, tableWidth: number): number {
50
+ return minWidth != null ? parseStaticWidth(minWidth, tableWidth) : 0;
51
+ }
52
+
53
+ export interface IColumn {
54
+ minWidth?: number | string;
55
+ maxWidth?: number | string;
56
+ width?: number | string;
57
+ defaultWidth?: number | string;
58
+ key: Key;
59
+ }
60
+
61
+ interface FlexItem {
62
+ frozen: boolean;
63
+ baseSize: number;
64
+ hypotheticalMainSize: number;
65
+ min: number;
66
+ max: number;
67
+ flex: number;
68
+ targetMainSize: number;
69
+ violation: number;
70
+ }
71
+
72
+ /**
73
+ * Implements the flex algorithm described in https://www.w3.org/TR/css-flexbox-1/#layout-algorithm
74
+ * It makes a few constraint/assumptions: 1. All basis values are 0 unless it is a static width,
75
+ * then the basis is the static width 2. All flex grow and shrink values are equal to the FR
76
+ * specified on the column, grow and shrink for the same column are equal 3. We only have one row An
77
+ * example of the setup can be seen here https://jsfiddle.net/snowystinger/wv0ymjaf/61/ where I let
78
+ * the browser figure out the flex of the columns. Note: We differ in one key aspect, all of our
79
+ * column widths must be whole numbers, so we avoid browser sub pixel rounding errors. To do this,
80
+ * we use a cascading rounding algorithm to ensure that the sum of the widths is maintained while
81
+ * distributing the rounding remainder across the columns.
82
+ *
83
+ * As noted in the chrome source code, this algorithm is very accurate, but has the potential to be
84
+ * quadratic. They have deemed this to be acceptable because the number of elements is usually small
85
+ * and the flex factors are usually not high variance. I believe we can make the same assumptions.
86
+ * Particularly once resizing is started, it will convert all columns to the left to static widths,
87
+ * so it will cut down on the number of FR columns.
88
+ *
89
+ * There are likely faster ways to do this, I've chosen to stick to the spec as closely as possible
90
+ * for readability, accuracy, and for the note that this behaving quadratically is unlikely to be a
91
+ * problem.
92
+ *
93
+ * @param availableWidth - The visible width of the table.
94
+ * @param columns - The table defined columns.
95
+ * @param changedColumns - Any columns we want to override, for example, during resizing.
96
+ * @param getDefaultWidth - A function that returns the default width of a column by its index.
97
+ * @param getDefaultMinWidth - A function that returns the default min width of a column by its
98
+ * index.
99
+ */
100
+ export function calculateColumnSizes(
101
+ availableWidth: number,
102
+ columns: IColumn[],
103
+ changedColumns: Map<Key, ColumnSize>,
104
+ getDefaultWidth?: (index: number) => ColumnSize | null | undefined,
105
+ getDefaultMinWidth?: (index: number) => ColumnSize | null | undefined,
106
+ ): number[] {
107
+ let hasNonFrozenItems = false;
108
+ let flexItems: FlexItem[] = columns.map((column, index) => {
109
+ let width: ColumnSize = (
110
+ changedColumns.get(column.key) != null
111
+ ? (changedColumns.get(column.key) ?? '1fr')
112
+ : (column.width ?? column.defaultWidth ?? getDefaultWidth?.(index) ?? '1fr')
113
+ ) as ColumnSize;
114
+ let frozen = false;
115
+ let baseSize = 0;
116
+ let flex = 0;
117
+ let targetMainSize = 0;
118
+ if (isStatic(width)) {
119
+ baseSize = parseStaticWidth(width, availableWidth);
120
+ frozen = true;
121
+ } else {
122
+ flex = parseFractionalUnit(width);
123
+ if (flex <= 0) {
124
+ frozen = true;
125
+ }
126
+ }
127
+
128
+ let min = getMinWidth(column.minWidth ?? getDefaultMinWidth?.(index) ?? 0, availableWidth);
129
+ let max = getMaxWidth(column.maxWidth, availableWidth);
130
+ let hypotheticalMainSize = Math.max(min, Math.min(baseSize, max));
131
+
132
+ // 9.7.1
133
+ // We don't make use of flex basis, it's always 0, so we are always in 'grow' mode.
134
+ // 9.7.2
135
+ if (frozen) {
136
+ targetMainSize = hypotheticalMainSize;
137
+ } else if (baseSize > hypotheticalMainSize) {
138
+ frozen = true;
139
+ targetMainSize = hypotheticalMainSize;
140
+ }
141
+
142
+ // 9.7.3
143
+ if (!frozen) {
144
+ hasNonFrozenItems = true;
145
+ }
146
+ return {
147
+ frozen,
148
+ baseSize,
149
+ hypotheticalMainSize,
150
+ min,
151
+ max,
152
+ flex,
153
+ targetMainSize,
154
+ violation: 0,
155
+ };
156
+ });
157
+
158
+ // 9.7.4
159
+ // 9.7.4.a
160
+ while (hasNonFrozenItems) {
161
+ // 9.7.4.b
162
+ /**
163
+ * Calculate the remaining free space as for initial free space,
164
+ * above (9.7.3). If the sum of the unfrozen flex items’ flex factors is
165
+ * less than one, multiply the initial free space by this sum (of flex factors).
166
+ * If the magnitude of this value is less than the magnitude of
167
+ * the remaining free space, use this as the remaining free space.
168
+ */
169
+ let usedWidth = 0;
170
+ let flexFactors = 0;
171
+ flexItems.forEach((item) => {
172
+ if (item.frozen) {
173
+ usedWidth += item.targetMainSize;
174
+ } else {
175
+ usedWidth += item.baseSize;
176
+ flexFactors += item.flex;
177
+ }
178
+ });
179
+
180
+ let remainingFreeSpace = availableWidth - usedWidth;
181
+ // we only support integer FR's, and because of hasNonFrozenItems, we know that flexFactors > 0
182
+ // so no need to check for flexFactors < 1
183
+ // 9.7.4.c
184
+ /**
185
+ * If the remaining free space is zero
186
+ * - Do nothing.
187
+ * Else // remember, we're always in grow mode
188
+ * - Find the ratio of the item’s flex grow factor to the
189
+ * sum of the flex grow factors of all unfrozen items on
190
+ * the line. Set the item’s target main size to its flex
191
+ * base size plus a fraction of the remaining free space
192
+ * proportional to the ratio.
193
+ */
194
+ if (remainingFreeSpace > 0) {
195
+ flexItems.forEach((item) => {
196
+ if (!item.frozen) {
197
+ let ratio = item.flex / flexFactors;
198
+ item.targetMainSize = item.baseSize + ratio * remainingFreeSpace;
199
+ }
200
+ });
201
+ }
202
+
203
+ // 9.7.4.d
204
+ /**
205
+ * Fix min/max violations. Clamp each non-frozen item’s
206
+ * target main size by its used min and max main sizes
207
+ * and floor its content-box size at zero. If the item’s
208
+ * target main size was made smaller by this, it’s a max
209
+ * violation. If the item’s target main size was made
210
+ * larger by this, it’s a min violation.
211
+ */
212
+ let totalViolation = 0;
213
+ flexItems.forEach((item) => {
214
+ item.violation = 0;
215
+ if (!item.frozen) {
216
+ let { min, max, targetMainSize } = item;
217
+ item.targetMainSize = Math.max(min, Math.min(targetMainSize, max));
218
+
219
+ item.violation = item.targetMainSize - targetMainSize;
220
+ totalViolation += item.violation;
221
+ }
222
+ });
223
+
224
+ // 9.7.4.e
225
+ /**
226
+ * Freeze over-flexed items. The total violation is the
227
+ * sum of the adjustments from the previous step
228
+ * ∑(clamped size - unclamped size). If the total violation is:
229
+ * Zero
230
+ * - Freeze all items.
231
+ *
232
+ * Positive
233
+ * - Freeze all the items with min violations.
234
+ *
235
+ * Negative
236
+ * - Freeze all the items with max violations.
237
+ */
238
+ hasNonFrozenItems = false;
239
+ flexItems.forEach((item) => {
240
+ if (totalViolation === 0 || Math.sign(totalViolation) === Math.sign(item.violation)) {
241
+ item.frozen = true;
242
+ } else if (!item.frozen) {
243
+ hasNonFrozenItems = true;
244
+ }
245
+ });
246
+ }
247
+
248
+ return cascadeRounding(flexItems);
249
+ }
250
+
251
+ function cascadeRounding(flexItems: FlexItem[]): number[] {
252
+ /*
253
+ Given an array of floats that sum to an integer, this rounds the floats
254
+ and returns an array of integers with the same sum.
255
+ */
256
+
257
+ let fpTotal = 0;
258
+ let intTotal = 0;
259
+ let roundedArray: number[] = [];
260
+ flexItems.forEach(function (item) {
261
+ let float = item.targetMainSize;
262
+ let integer = Math.round(float + fpTotal) - intTotal;
263
+ fpTotal += float;
264
+ intTotal += integer;
265
+ roundedArray.push(integer);
266
+ });
267
+
268
+ return roundedArray;
269
+ }
@@ -0,0 +1,202 @@
1
+ // Ported from react-stately (source: .react-spectrum/packages/react-stately/src/table/useTableColumnResizeState.ts).
2
+ // octane adaptations: public-hook slot threading (splitSlot/subSlot) per the binding
3
+ // convention; explicit dependency arrays are kept verbatim (they retain React's exact
4
+ // behavior in octane). Upstream's render-phase "reset uncontrolled widths when the
5
+ // columns changed" adjustment is kept verbatim (React's setState-during-render pattern;
6
+ // octane re-runs the component the same way).
7
+ import type { Key } from '@react-types/shared';
8
+ import { useCallback, useMemo, useState } from 'octane';
9
+
10
+ import { S, splitSlot, subSlot } from '../../internal';
11
+ import type { ColumnSize } from './Column';
12
+ import type { GridNode } from '../grid/GridCollection';
13
+ import { TableColumnLayout } from './TableColumnLayout';
14
+ import type { TableState } from './useTableState';
15
+
16
+ export interface TableColumnResizeStateProps<T> {
17
+ /**
18
+ * Current width of the table or table viewport that the columns
19
+ * should be calculated against.
20
+ */
21
+ tableWidth: number;
22
+ /** A function that is called to find the default width for a given column. */
23
+ getDefaultWidth?: (node: GridNode<T>) => ColumnSize | null | undefined;
24
+ /** A function that is called to find the default minWidth for a given column. */
25
+ getDefaultMinWidth?: (node: GridNode<T>) => ColumnSize | null | undefined;
26
+ }
27
+
28
+ export interface TableColumnResizeState<T> {
29
+ /**
30
+ * Called to update the state that a resize event has occurred.
31
+ * Returns the new widths for all columns based on the resized column.
32
+ */
33
+ updateResizedColumns: (key: Key, width: number) => Map<Key, ColumnSize>;
34
+ /** Callback for when onColumnResize has started. */
35
+ startResize: (key: Key) => void;
36
+ /** Callback for when onColumnResize has ended. */
37
+ endResize: () => void;
38
+ /** Gets the current width for the specified column. */
39
+ getColumnWidth: (key: Key) => number;
40
+ /** Gets the current minWidth for the specified column. */
41
+ getColumnMinWidth: (key: Key) => number;
42
+ /** Gets the current maxWidth for the specified column. */
43
+ getColumnMaxWidth: (key: Key) => number;
44
+ /** Key of the currently resizing column. */
45
+ resizingColumn: Key | null;
46
+ /** A reference to the table state. */
47
+ tableState: TableState<T>;
48
+ /** A map of the current column widths. */
49
+ columnWidths: Map<Key, number>;
50
+ }
51
+
52
+ /**
53
+ * Provides column width state management for a table component with column resizing support.
54
+ * Handles building a map of column widths calculated from the table's width and any provided column
55
+ * width information from the collection. In addition, it tracks the currently resizing column and
56
+ * provides callbacks for updating the widths upon resize operations.
57
+ *
58
+ * @param props - Props for the table.
59
+ * @param state - State for the table, as returned by `useTableState`.
60
+ */
61
+ export function useTableColumnResizeState<T>(
62
+ props: TableColumnResizeStateProps<T>,
63
+ state: TableState<T>,
64
+ ): TableColumnResizeState<T>;
65
+ // Slot-threading form: sibling ported hooks pass their derived sub-slot as the trailing arg.
66
+ export function useTableColumnResizeState<T>(
67
+ props: TableColumnResizeStateProps<T>,
68
+ state: TableState<T>,
69
+ slot: symbol | undefined,
70
+ ): TableColumnResizeState<T>;
71
+ export function useTableColumnResizeState(...args: any[]): TableColumnResizeState<any> {
72
+ const [user, slotArg] = splitSlot(args);
73
+ const slot = slotArg ?? S('useTableColumnResizeState');
74
+ const props = user[0] as TableColumnResizeStateProps<any>;
75
+ const state = user[1] as TableState<any>;
76
+
77
+ let { getDefaultWidth, getDefaultMinWidth, tableWidth = 0 } = props;
78
+
79
+ let [resizingColumn, setResizingColumn] = useState<Key | null>(null, subSlot(slot, 'resizing'));
80
+ let columnLayout = useMemo(
81
+ () =>
82
+ new TableColumnLayout({
83
+ getDefaultWidth,
84
+ getDefaultMinWidth,
85
+ }),
86
+ [getDefaultWidth, getDefaultMinWidth],
87
+ subSlot(slot, 'layout'),
88
+ );
89
+
90
+ let [controlledColumns, uncontrolledColumns] = useMemo(
91
+ () => columnLayout.splitColumnsIntoControlledAndUncontrolled(state.collection.columns),
92
+ [state.collection.columns, columnLayout],
93
+ subSlot(slot, 'split'),
94
+ );
95
+
96
+ // uncontrolled column widths
97
+ let [uncontrolledWidths, setUncontrolledWidths] = useState(
98
+ () => columnLayout.getInitialUncontrolledWidths(uncontrolledColumns),
99
+ subSlot(slot, 'uncontrolled'),
100
+ );
101
+
102
+ // Update uncontrolled widths if the columns changed.
103
+ let [lastColumns, setLastColumns] = useState(state.collection.columns, subSlot(slot, 'last'));
104
+ if (state.collection.columns !== lastColumns) {
105
+ if (
106
+ state.collection.columns.length !== lastColumns.length ||
107
+ state.collection.columns.some((c, i) => c.key !== lastColumns[i].key)
108
+ ) {
109
+ let newUncontrolledWidths = columnLayout.getInitialUncontrolledWidths(uncontrolledColumns);
110
+ setUncontrolledWidths(newUncontrolledWidths);
111
+ }
112
+ setLastColumns(state.collection.columns);
113
+ }
114
+
115
+ // combine columns back into one map that maintains same order as the columns
116
+ let colWidths = useMemo(
117
+ () =>
118
+ columnLayout.recombineColumns(
119
+ state.collection.columns,
120
+ uncontrolledWidths,
121
+ uncontrolledColumns,
122
+ controlledColumns,
123
+ ),
124
+ [
125
+ state.collection.columns,
126
+ uncontrolledWidths,
127
+ uncontrolledColumns,
128
+ controlledColumns,
129
+ columnLayout,
130
+ ],
131
+ subSlot(slot, 'colWidths'),
132
+ );
133
+
134
+ let startResize = useCallback(
135
+ (key: Key) => {
136
+ setResizingColumn(key);
137
+ },
138
+ [setResizingColumn],
139
+ subSlot(slot, 'start'),
140
+ );
141
+
142
+ let updateResizedColumns = useCallback(
143
+ (key: Key, width: number): Map<Key, ColumnSize> => {
144
+ let newSizes = columnLayout.resizeColumnWidth(
145
+ state.collection,
146
+ uncontrolledWidths,
147
+ key,
148
+ width,
149
+ );
150
+ let map = new Map(Array.from(uncontrolledColumns).map(([key]) => [key, newSizes.get(key)!]));
151
+ map.set(key, width);
152
+ setUncontrolledWidths(map);
153
+ return newSizes;
154
+ },
155
+ [
156
+ uncontrolledColumns,
157
+ setUncontrolledWidths,
158
+ columnLayout,
159
+ state.collection,
160
+ uncontrolledWidths,
161
+ ],
162
+ subSlot(slot, 'update'),
163
+ );
164
+
165
+ let endResize = useCallback(
166
+ () => {
167
+ setResizingColumn(null);
168
+ },
169
+ [setResizingColumn],
170
+ subSlot(slot, 'end'),
171
+ );
172
+
173
+ let columnWidths = useMemo(
174
+ () => columnLayout.buildColumnWidths(tableWidth, state.collection, colWidths),
175
+ [tableWidth, state.collection, colWidths, columnLayout],
176
+ subSlot(slot, 'widths'),
177
+ );
178
+
179
+ return useMemo(
180
+ () => ({
181
+ resizingColumn,
182
+ updateResizedColumns,
183
+ startResize,
184
+ endResize,
185
+ getColumnWidth: (key: Key) => columnLayout.getColumnWidth(key),
186
+ getColumnMinWidth: (key: Key) => columnLayout.getColumnMinWidth(key),
187
+ getColumnMaxWidth: (key: Key) => columnLayout.getColumnMaxWidth(key),
188
+ tableState: state,
189
+ columnWidths,
190
+ }),
191
+ [
192
+ columnLayout,
193
+ columnWidths,
194
+ resizingColumn,
195
+ updateResizedColumns,
196
+ startResize,
197
+ endResize,
198
+ state,
199
+ ],
200
+ subSlot(slot, 'result'),
201
+ );
202
+ }
@@ -0,0 +1,235 @@
1
+ // Ported from react-stately (source: .react-spectrum/packages/react-stately/src/table/useTableState.ts).
2
+ // octane adaptations: public-hook slot threading (splitSlot/subSlot) per the binding
3
+ // convention; React element types → `any` descriptors (`children` is a
4
+ // [<TableHeader>, <TableBody>] descriptor pair); explicit dependency arrays are kept
5
+ // verbatim (they retain React's exact behavior in octane).
6
+ import type {
7
+ Expandable,
8
+ Key,
9
+ MultipleSelection,
10
+ Node,
11
+ SelectionMode,
12
+ Sortable,
13
+ SortDescriptor,
14
+ SortDirection,
15
+ } from '@react-types/shared';
16
+ import { useCallback, useMemo, useState } from 'octane';
17
+
18
+ import { S, splitSlot, subSlot } from '../../internal';
19
+ import { type GridState, useGridState } from '../grid/useGridState';
20
+ import { type ITableCollection, TableCollection } from './TableCollection';
21
+ import type { MultipleSelectionState } from '../selection/types';
22
+ import type { MultipleSelectionStateProps } from '../selection/useMultipleSelectionState';
23
+ import { useCollection } from '../collections/useCollection';
24
+ import { useControlledState } from '../utils/useControlledState';
25
+
26
+ export interface TableProps<T> extends MultipleSelection, Sortable, Expandable {
27
+ /** The elements that make up the table. Includes the TableHeader, TableBody, Columns, and Rows. */
28
+ children: [any, any];
29
+ /** A list of row keys to disable. */
30
+ disabledKeys?: Iterable<Key>;
31
+ /**
32
+ * Whether pressing the escape key should clear selection in the table or not.
33
+ *
34
+ * Most experiences should not modify this option as it eliminates a keyboard user's ability to
35
+ * easily clear selection. Only use if the escape key is being handled externally or should not
36
+ * trigger selection clearing contextually.
37
+ *
38
+ * @default 'clearSelection'
39
+ */
40
+ escapeKeyBehavior?: 'clearSelection' | 'none';
41
+ /** Whether selection should occur on press up instead of press down. */
42
+ shouldSelectOnPressUp?: boolean;
43
+ /** The id of the column that displays hierarchical data. */
44
+ treeColumn?: Key;
45
+ }
46
+
47
+ export interface TableState<T> extends GridState<T, ITableCollection<T>> {
48
+ /** A collection of rows and columns in the table. */
49
+ collection: ITableCollection<T>;
50
+ /** Whether the row selection checkboxes should be displayed. */
51
+ showSelectionCheckboxes: boolean;
52
+ /** The current sorted column and direction. */
53
+ sortDescriptor: SortDescriptor | null;
54
+ /** Calls the provided onSortChange handler with the provided column key and sort direction. */
55
+ sort(columnKey: Key, direction?: 'ascending' | 'descending'): void;
56
+ /**
57
+ * Whether keyboard navigation is disabled, such as when the arrow keys should be handled by a
58
+ * component within a cell.
59
+ */
60
+ isKeyboardNavigationDisabled: boolean;
61
+ /**
62
+ * Set whether keyboard navigation is disabled, such as when the arrow keys should be handled by a
63
+ * component within a cell.
64
+ */
65
+ setKeyboardNavigationDisabled: (val: boolean) => void;
66
+ /** A set of keys for items that are expanded. */
67
+ expandedKeys: Set<Key>;
68
+ /** Toggles the expanded state for a row by its key. */
69
+ toggleKey(key: Key): void;
70
+ /** The id of the column that displays hierarchical data. */
71
+ treeColumn: Key | null;
72
+ }
73
+
74
+ export interface CollectionBuilderContext<T> {
75
+ showSelectionCheckboxes: boolean;
76
+ showDragButtons: boolean;
77
+ selectionMode: SelectionMode;
78
+ columns: Node<T>[];
79
+ }
80
+
81
+ export interface TableStateProps<T> extends MultipleSelectionStateProps, Expandable, Sortable {
82
+ /** The elements that make up the table. Includes the TableHeader, TableBody, Columns, and Rows. */
83
+ children?: [any, any];
84
+ /** A pre-constructed collection to use instead of building one from items and children. */
85
+ collection?: ITableCollection<T>;
86
+ /** Whether the row selection checkboxes should be displayed. */
87
+ showSelectionCheckboxes?: boolean;
88
+ /**
89
+ * Whether the row drag button should be displayed.
90
+ *
91
+ * @private
92
+ */
93
+ showDragButtons?: boolean;
94
+ /** @private - Do not use unless you know what you're doing. */
95
+ UNSAFE_selectionState?: MultipleSelectionState;
96
+ /** The id of the column that displays hierarchical data. */
97
+ treeColumn?: Key;
98
+ }
99
+
100
+ const OPPOSITE_SORT_DIRECTION = {
101
+ ascending: 'descending' as SortDirection,
102
+ descending: 'ascending' as SortDirection,
103
+ };
104
+
105
+ /**
106
+ * Provides state management for a table component. Handles building a collection of columns and
107
+ * rows from props. In addition, it tracks row selection and manages sort order changes.
108
+ */
109
+ export function useTableState<T extends object>(props: TableStateProps<T>): TableState<T>;
110
+ // Slot-threading form: sibling ported hooks pass their derived sub-slot as the trailing arg.
111
+ export function useTableState<T extends object>(
112
+ props: TableStateProps<T>,
113
+ slot: symbol | undefined,
114
+ ): TableState<T>;
115
+ export function useTableState(...args: any[]): TableState<any> {
116
+ const [user, slotArg] = splitSlot(args);
117
+ const slot = slotArg ?? S('useTableState');
118
+ const props = user[0] as TableStateProps<any>;
119
+
120
+ let [isKeyboardNavigationDisabled, setKeyboardNavigationDisabled] = useState(
121
+ false,
122
+ subSlot(slot, 'kbdNav'),
123
+ );
124
+ let {
125
+ selectionMode = 'none',
126
+ showSelectionCheckboxes,
127
+ showDragButtons,
128
+ treeColumn = null,
129
+ } = props;
130
+
131
+ let context = useMemo(
132
+ () => ({
133
+ showSelectionCheckboxes: showSelectionCheckboxes && selectionMode !== 'none',
134
+ showDragButtons: showDragButtons,
135
+ selectionMode,
136
+ columns: [],
137
+ }),
138
+ [props.children, showSelectionCheckboxes, selectionMode, showDragButtons],
139
+ subSlot(slot, 'context'),
140
+ );
141
+
142
+ let collection = useCollection<any, ITableCollection<any>>(
143
+ props,
144
+ useCallback(
145
+ (nodes) => new TableCollection(nodes, null, context),
146
+ [context],
147
+ subSlot(slot, 'factory'),
148
+ ),
149
+ context,
150
+ subSlot(slot, 'collection'),
151
+ );
152
+ let { disabledKeys, selectionManager } = useGridState(
153
+ {
154
+ ...props,
155
+ collection,
156
+ disabledBehavior: props.disabledBehavior || 'selection',
157
+ },
158
+ subSlot(slot, 'grid'),
159
+ );
160
+
161
+ let [expandedKeys, setExpandedKeys] = useControlledState(
162
+ props.expandedKeys ? new Set(props.expandedKeys) : undefined,
163
+ props.defaultExpandedKeys ? new Set(props.defaultExpandedKeys) : new Set(),
164
+ props.onExpandedChange,
165
+ subSlot(slot, 'expanded'),
166
+ );
167
+
168
+ return {
169
+ collection,
170
+ disabledKeys,
171
+ selectionManager,
172
+ showSelectionCheckboxes: props.showSelectionCheckboxes || false,
173
+ sortDescriptor: props.sortDescriptor ?? null,
174
+ isKeyboardNavigationDisabled: collection.size === 0 || isKeyboardNavigationDisabled,
175
+ setKeyboardNavigationDisabled,
176
+ sort(columnKey: Key, direction?: 'ascending' | 'descending') {
177
+ props.onSortChange?.({
178
+ column: columnKey,
179
+ direction:
180
+ direction ??
181
+ (props.sortDescriptor?.column === columnKey
182
+ ? OPPOSITE_SORT_DIRECTION[props.sortDescriptor.direction]
183
+ : 'ascending'),
184
+ });
185
+ },
186
+ expandedKeys,
187
+ toggleKey(key) {
188
+ setExpandedKeys((keys) => {
189
+ let newKeys = new Set(keys);
190
+ if (newKeys.has(key)) {
191
+ newKeys.delete(key);
192
+ } else {
193
+ newKeys.add(key);
194
+ }
195
+
196
+ return newKeys;
197
+ });
198
+ },
199
+ treeColumn,
200
+ };
201
+ }
202
+
203
+ /**
204
+ * Filters a collection using the provided filter function and returns a new TableState.
205
+ */
206
+ export function UNSTABLE_useFilteredTableState<T extends object>(
207
+ state: TableState<T>,
208
+ filterFn: ((nodeValue: string, node: Node<T>) => boolean) | null | undefined,
209
+ ): TableState<T>;
210
+ // Slot-threading form: sibling ported hooks pass their derived sub-slot as the trailing arg.
211
+ export function UNSTABLE_useFilteredTableState<T extends object>(
212
+ state: TableState<T>,
213
+ filterFn: ((nodeValue: string, node: Node<T>) => boolean) | null | undefined,
214
+ slot: symbol | undefined,
215
+ ): TableState<T>;
216
+ export function UNSTABLE_useFilteredTableState(...args: any[]): TableState<any> {
217
+ const [user, slotArg] = splitSlot(args);
218
+ const slot = slotArg ?? S('UNSTABLE_useFilteredTableState');
219
+ const state = user[0] as TableState<any>;
220
+ const filterFn = user[1] as ((nodeValue: string, node: Node<any>) => boolean) | null | undefined;
221
+
222
+ let collection = useMemo(
223
+ () => (filterFn ? state.collection.filter!(filterFn) : state.collection),
224
+ [state.collection, filterFn],
225
+ subSlot(slot, 'collection'),
226
+ ) as ITableCollection<any>;
227
+ let selectionManager = state.selectionManager.withCollection(collection);
228
+ // TODO: handle focus key reset? That logic is in useGridState
229
+
230
+ return {
231
+ ...state,
232
+ collection,
233
+ selectionManager,
234
+ };
235
+ }