@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,198 @@
1
+ // Ported from react-aria (source: .react-spectrum/packages/react-aria/src/table/useTableRow.ts).
2
+ // octane adaptations:
3
+ // - Handlers receive NATIVE events: the treegrid expansion keydown handler takes the
4
+ // native KeyboardEvent.
5
+ // - `TableState`/`TreeGridState`/`ITableCollection` come from the ported stately hooks;
6
+ // `useGridRow` from the ported src/grid/; React's `HTMLAttributes` and `DOMAttributes`
7
+ // collapse to a structural prop bag.
8
+ // - The Parcel glob intl import becomes the generated src/intl/table index (verbatim
9
+ // dictionaries).
10
+ // - Public-hook slot threading (splitSlot/subSlot).
11
+ import type { AriaButtonProps } from '../button/useButton';
12
+
13
+ import type { Collection, FocusableElement, Node, RefObject } from '@react-types/shared';
14
+ import { getRowLabelledBy } from './utils';
15
+ import { GridRowAria, GridRowProps, useGridRow } from '../grid/useGridRow';
16
+ import intlMessages from '../intl/table';
17
+ import type { ITableCollection } from '../stately/table/TableCollection';
18
+ import { mergeProps } from '../utils/mergeProps';
19
+ import type { TableState } from '../stately/table/useTableState';
20
+ import type { TreeGridState } from '../stately/table/useTreeGridState';
21
+ import { useLabels } from '../utils/useLabels';
22
+ import { useLocale } from '../i18n/I18nProvider';
23
+ import { useLocalizedStringFormatter } from '../i18n/useLocalizedStringFormatter';
24
+ import { useSyntheticLinkProps } from '../utils/openLink';
25
+
26
+ import { S, splitSlot, subSlot } from '../internal';
27
+
28
+ const EXPANSION_KEYS = {
29
+ expand: {
30
+ ltr: 'ArrowRight',
31
+ rtl: 'ArrowLeft',
32
+ },
33
+ collapse: {
34
+ ltr: 'ArrowLeft',
35
+ rtl: 'ArrowRight',
36
+ },
37
+ };
38
+
39
+ export interface TableRowAria extends GridRowAria {
40
+ expandButtonProps: AriaButtonProps;
41
+ }
42
+
43
+ /**
44
+ * Provides the behavior and accessibility implementation for a row in a table.
45
+ *
46
+ * @param props - Props for the row.
47
+ * @param state - State of the table, as returned by `useTableState`.
48
+ */
49
+ export function useTableRow<T>(
50
+ props: GridRowProps<T>,
51
+ state: TableState<T> | TreeGridState<T>,
52
+ ref: RefObject<FocusableElement | null>,
53
+ ): TableRowAria;
54
+ // Slot-threading form: sibling ported hooks pass their derived sub-slot as the trailing arg.
55
+ export function useTableRow<T>(
56
+ props: GridRowProps<T>,
57
+ state: TableState<T> | TreeGridState<T>,
58
+ ref: RefObject<FocusableElement | null>,
59
+ slot: symbol | undefined,
60
+ ): TableRowAria;
61
+ export function useTableRow(...args: any[]): TableRowAria {
62
+ const [user, slotArg] = splitSlot(args);
63
+ const slot = slotArg ?? S('useTableRow');
64
+ const props = user[0] as GridRowProps<any>;
65
+ const state = user[1] as TableState<any> | TreeGridState<any>;
66
+ const ref = user[2] as RefObject<FocusableElement | null>;
67
+
68
+ let { node, isVirtualized } = props;
69
+ let { rowProps, ...states } = useGridRow<any, ITableCollection<any>, TableState<any>>(
70
+ props,
71
+ state as TableState<any>,
72
+ ref,
73
+ subSlot(slot, 'gridRow'),
74
+ );
75
+ let { direction } = useLocale(subSlot(slot, 'locale'));
76
+
77
+ if (isVirtualized && state.treeColumn == null) {
78
+ rowProps['aria-rowindex'] = node.index + 1 + state.collection.headerRows.length; // aria-rowindex is 1 based
79
+ } else {
80
+ delete rowProps['aria-rowindex'];
81
+ }
82
+
83
+ let isExpanded =
84
+ state.treeColumn != null && (state.expandedKeys === 'all' || state.expandedKeys.has(node.key));
85
+ let stringFormatter = useLocalizedStringFormatter(
86
+ intlMessages,
87
+ '@react-aria/table',
88
+ subSlot(slot, 'strings'),
89
+ );
90
+ let labelProps = useLabels(
91
+ {
92
+ 'aria-label': isExpanded
93
+ ? stringFormatter.format('collapse')
94
+ : stringFormatter.format('expand'),
95
+ 'aria-labelledby': getRowLabelledBy(state as TableState<any>, node.key),
96
+ },
97
+ undefined,
98
+ subSlot(slot, 'labels'),
99
+ );
100
+
101
+ let treeGridRowProps: Record<string, any> = {};
102
+ let expandButtonProps: AriaButtonProps = {};
103
+ if (state.treeColumn != null) {
104
+ let treeNode = state.collection.getItem(node.key);
105
+ if (treeNode != null) {
106
+ let lastChild = getLastChild(state.collection, node);
107
+ let hasChildRows =
108
+ treeNode.props?.hasChildRows ||
109
+ treeNode.props?.UNSTABLE_childItems ||
110
+ lastChild?.type !== 'cell';
111
+ let parent = state.collection.getItem(node.parentKey!)!;
112
+ let isParentBody = parent.type === 'tablebody' || parent.type === 'body';
113
+ let lastSibling = getLastChild(state.collection, parent)!;
114
+ while (lastSibling && lastSibling.type !== 'item' && lastSibling.prevKey != null) {
115
+ lastSibling = state.collection.getItem(lastSibling.prevKey)!;
116
+ }
117
+
118
+ treeGridRowProps = {
119
+ onKeyDown: (e: KeyboardEvent) => {
120
+ if (
121
+ e.key === EXPANSION_KEYS['expand'][direction] &&
122
+ state.selectionManager.focusedKey === treeNode.key &&
123
+ hasChildRows &&
124
+ state.expandedKeys !== 'all' &&
125
+ !state.expandedKeys.has(treeNode.key)
126
+ ) {
127
+ state.toggleKey(treeNode.key);
128
+ e.stopPropagation();
129
+ } else if (
130
+ e.key === EXPANSION_KEYS['collapse'][direction] &&
131
+ state.selectionManager.focusedKey === treeNode.key
132
+ ) {
133
+ if (state.expandedKeys !== 'all') {
134
+ if (hasChildRows && state.expandedKeys.has(treeNode.key)) {
135
+ state.toggleKey(treeNode.key);
136
+ e.stopPropagation();
137
+ } else if (
138
+ !state.expandedKeys.has(treeNode.key) &&
139
+ treeNode.parentKey != null &&
140
+ treeNode.level > 0
141
+ ) {
142
+ // Item is a leaf or already collapsed, move focus to parent
143
+ state.selectionManager.setFocusedKey(treeNode.parentKey);
144
+ e.stopPropagation();
145
+ }
146
+ } else if (state.expandedKeys === 'all') {
147
+ state.toggleKey(treeNode.key);
148
+ e.stopPropagation();
149
+ }
150
+ }
151
+ },
152
+ 'aria-expanded': hasChildRows
153
+ ? state.expandedKeys === 'all' || state.expandedKeys.has(node.key)
154
+ : undefined,
155
+ 'aria-level': treeNode.level + 1,
156
+ 'aria-posinset': treeNode.index - (isParentBody ? 0 : state.collection.columnCount) + 1,
157
+ 'aria-setsize': lastSibling.index - (isParentBody ? 0 : state.collection.columnCount) + 1,
158
+ };
159
+
160
+ expandButtonProps = {
161
+ isDisabled: states.isDisabled,
162
+ onPress: () => {
163
+ if (!states.isDisabled) {
164
+ state.toggleKey(node.key);
165
+ state.selectionManager.setFocused(true);
166
+ state.selectionManager.setFocusedKey(node.key);
167
+ }
168
+ },
169
+ excludeFromTabOrder: true,
170
+ preventFocusOnPress: true,
171
+ // @ts-ignore
172
+ 'data-react-aria-prevent-focus': true,
173
+ ...labelProps,
174
+ };
175
+ }
176
+ }
177
+
178
+ let syntheticLinkProps = useSyntheticLinkProps(node.props);
179
+ let linkProps = states.hasAction ? syntheticLinkProps : {};
180
+ return {
181
+ rowProps: {
182
+ ...mergeProps(rowProps, treeGridRowProps, linkProps),
183
+ 'aria-labelledby': getRowLabelledBy(state as TableState<any>, node.key),
184
+ },
185
+ expandButtonProps,
186
+ ...states,
187
+ };
188
+ }
189
+
190
+ function getLastChild(collection: Collection<Node<unknown>>, node: Node<unknown>) {
191
+ if ('lastChildKey' in node) {
192
+ return (node as any).lastChildKey != null
193
+ ? collection.getItem((node as any).lastChildKey)
194
+ : null;
195
+ } else {
196
+ return Array.from(node.childNodes).findLast((item) => item.parentKey === node.key);
197
+ }
198
+ }
@@ -0,0 +1,13 @@
1
+ // Ported from react-aria (source: .react-spectrum/packages/react-aria/src/table/useTableRowGroup.ts).
2
+ // octane adaptations: composes the ported src/grid/useGridRowGroup; the optional trailing
3
+ // slot is forwarded (uniform calling convention).
4
+ import { GridRowGroupAria, useGridRowGroup } from '../grid/useGridRowGroup';
5
+
6
+ import { restSlot } from '../internal';
7
+
8
+ export function useTableRowGroup(): GridRowGroupAria;
9
+ // Slot-threading form: sibling ported hooks pass their derived sub-slot as the trailing arg.
10
+ export function useTableRowGroup(slot: symbol | undefined): GridRowGroupAria;
11
+ export function useTableRowGroup(...args: any[]): GridRowGroupAria {
12
+ return useGridRowGroup(restSlot(args));
13
+ }
@@ -0,0 +1,105 @@
1
+ // Ported from react-aria (source: .react-spectrum/packages/react-aria/src/table/useTableSelectionCheckbox.ts).
2
+ // octane adaptations:
3
+ // - `TableState` comes from the ported stately table hook; `useGridSelectionCheckbox`
4
+ // from the ported src/grid/.
5
+ // - The checkbox `onChange` is the value-level AriaCheckboxProps callback (not DOM
6
+ // wiring), so it ports unchanged — consumers wire it to the native checkbox `change`
7
+ // event.
8
+ // - The Parcel glob intl import becomes the generated src/intl/table index (verbatim
9
+ // dictionaries).
10
+ // - Public-hook slot threading (splitSlot/subSlot).
11
+ import type { AriaCheckboxProps } from '../checkbox/useCheckbox';
12
+ import { getRowLabelledBy } from './utils';
13
+ import intlMessages from '../intl/table';
14
+ import type { Key } from '@react-types/shared';
15
+ import type { TableState } from '../stately/table/useTableState';
16
+ import { useGridSelectionCheckbox } from '../grid/useGridSelectionCheckbox';
17
+ import { useLocalizedStringFormatter } from '../i18n/useLocalizedStringFormatter';
18
+
19
+ import { S, splitSlot, subSlot } from '../internal';
20
+
21
+ export interface AriaTableSelectionCheckboxProps {
22
+ /** A unique key for the checkbox. */
23
+ key: Key;
24
+ }
25
+
26
+ export interface TableSelectionCheckboxAria {
27
+ /** Props for the row selection checkbox element. */
28
+ checkboxProps: AriaCheckboxProps;
29
+ }
30
+
31
+ export interface TableSelectAllCheckboxAria {
32
+ /** Props for the select all checkbox element. */
33
+ checkboxProps: AriaCheckboxProps;
34
+ }
35
+
36
+ /**
37
+ * Provides the behavior and accessibility implementation for a selection checkbox in a table.
38
+ *
39
+ * @param props - Props for the selection checkbox.
40
+ * @param state - State of the table, as returned by `useTableState`.
41
+ */
42
+ export function useTableSelectionCheckbox<T>(
43
+ props: AriaTableSelectionCheckboxProps,
44
+ state: TableState<T>,
45
+ ): TableSelectionCheckboxAria;
46
+ // Slot-threading form: sibling ported hooks pass their derived sub-slot as the trailing arg.
47
+ export function useTableSelectionCheckbox<T>(
48
+ props: AriaTableSelectionCheckboxProps,
49
+ state: TableState<T>,
50
+ slot: symbol | undefined,
51
+ ): TableSelectionCheckboxAria;
52
+ export function useTableSelectionCheckbox(...args: any[]): TableSelectionCheckboxAria {
53
+ const [user, slotArg] = splitSlot(args);
54
+ const slot = slotArg ?? S('useTableSelectionCheckbox');
55
+ const props = user[0] as AriaTableSelectionCheckboxProps;
56
+ const state = user[1] as TableState<any>;
57
+
58
+ let { key } = props;
59
+ const { checkboxProps } = useGridSelectionCheckbox(props, state, subSlot(slot, 'gridCheckbox'));
60
+
61
+ return {
62
+ checkboxProps: {
63
+ ...checkboxProps,
64
+ 'aria-labelledby': `${checkboxProps.id} ${getRowLabelledBy(state, key)}`,
65
+ },
66
+ };
67
+ }
68
+
69
+ /**
70
+ * Provides the behavior and accessibility implementation for the select all checkbox in a table.
71
+ *
72
+ * @param props - Props for the select all checkbox.
73
+ * @param state - State of the table, as returned by `useTableState`.
74
+ */
75
+ export function useTableSelectAllCheckbox<T>(state: TableState<T>): TableSelectAllCheckboxAria;
76
+ // Slot-threading form: sibling ported hooks pass their derived sub-slot as the trailing arg.
77
+ export function useTableSelectAllCheckbox<T>(
78
+ state: TableState<T>,
79
+ slot: symbol | undefined,
80
+ ): TableSelectAllCheckboxAria;
81
+ export function useTableSelectAllCheckbox(...args: any[]): TableSelectAllCheckboxAria {
82
+ const [user, slotArg] = splitSlot(args);
83
+ const slot = slotArg ?? S('useTableSelectAllCheckbox');
84
+ const state = user[0] as TableState<any>;
85
+
86
+ let { isEmpty, isSelectAll, selectionMode } = state.selectionManager;
87
+ const stringFormatter = useLocalizedStringFormatter(
88
+ intlMessages,
89
+ '@react-aria/table',
90
+ subSlot(slot, 'strings'),
91
+ );
92
+
93
+ return {
94
+ checkboxProps: {
95
+ 'aria-label': stringFormatter.format(selectionMode === 'single' ? 'select' : 'selectAll'),
96
+ isSelected: isSelectAll,
97
+ isDisabled:
98
+ selectionMode !== 'multiple' ||
99
+ state.collection.size === 0 ||
100
+ (state.collection.rows.length === 1 && state.collection.rows[0].type === 'loader'),
101
+ isIndeterminate: !isEmpty && !isSelectAll,
102
+ onChange: () => state.selectionManager.toggleSelectAll(),
103
+ },
104
+ };
105
+ }
@@ -0,0 +1,42 @@
1
+ // Ported from react-aria (source: .react-spectrum/packages/react-aria/src/table/utils.ts).
2
+ // octane adaptations: `TableState` type comes from the ported stately table hook.
3
+ import type { Key } from '@react-types/shared';
4
+ import type { TableState } from '../stately/table/useTableState';
5
+
6
+ export const gridIds: WeakMap<TableState<unknown>, string> = new WeakMap<
7
+ TableState<unknown>,
8
+ string
9
+ >();
10
+
11
+ function normalizeKey(key: Key): string {
12
+ if (typeof key === 'string') {
13
+ return key.replace(/\s*/g, '');
14
+ }
15
+
16
+ return '' + key;
17
+ }
18
+
19
+ export function getColumnHeaderId<T>(state: TableState<T>, columnKey: Key): string {
20
+ let gridId = gridIds.get(state);
21
+ if (!gridId) {
22
+ throw new Error('Unknown grid');
23
+ }
24
+
25
+ return `${gridId}-${normalizeKey(columnKey)}`;
26
+ }
27
+
28
+ export function getCellId<T>(state: TableState<T>, rowKey: Key, columnKey: Key): string {
29
+ let gridId = gridIds.get(state);
30
+ if (!gridId) {
31
+ throw new Error('Unknown grid');
32
+ }
33
+
34
+ return `${gridId}-${normalizeKey(rowKey)}-${normalizeKey(columnKey)}`;
35
+ }
36
+
37
+ export function getRowLabelledBy<T>(state: TableState<T>, rowKey: Key): string {
38
+ // A row is labelled by it's row headers.
39
+ return [...state.collection.rowHeaderColumnKeys]
40
+ .map((columnKey) => getCellId(state, rowKey, columnKey))
41
+ .join(' ');
42
+ }
@@ -0,0 +1,76 @@
1
+ // Ported from react-aria (source: .react-spectrum/packages/react-aria/src/tree/useTree.ts).
2
+ // octane adaptations:
3
+ // - `TreeState` comes from the ported stately tree hook; `DOMAttributes` is a local
4
+ // structural prop-bag alias (upstream's is typed over React's synthetic handlers).
5
+ // - Public-hook slot threading (splitSlot/subSlot).
6
+ import type { KeyboardDelegate, RefObject } from '@react-types/shared';
7
+ import {
8
+ type AriaGridListOptions,
9
+ type AriaGridListProps,
10
+ type GridListProps,
11
+ useGridList,
12
+ } from '../gridlist/useGridList';
13
+ import type { TreeState } from '../stately/tree/useTreeState';
14
+
15
+ import { S, splitSlot, subSlot } from '../internal';
16
+
17
+ // octane adaptation: minimal structural DOMAttributes (upstream's drags React's synthetic
18
+ // handler types along).
19
+ type DOMAttributes = Record<string, any>;
20
+
21
+ export interface TreeProps<T> extends GridListProps<T> {}
22
+
23
+ export interface AriaTreeProps<T> extends AriaGridListProps<T> {}
24
+ export interface AriaTreeOptions<T> extends Omit<
25
+ AriaGridListOptions<T>,
26
+ 'children' | 'shouldFocusWrap'
27
+ > {
28
+ /**
29
+ * An optional keyboard delegate implementation for type to select,
30
+ * to override the default.
31
+ */
32
+ keyboardDelegate?: KeyboardDelegate;
33
+ }
34
+
35
+ export interface TreeAria {
36
+ /** Props for the treegrid element. */
37
+ gridProps: DOMAttributes;
38
+ }
39
+
40
+ /**
41
+ * Provides the behavior and accessibility implementation for a single column treegrid component
42
+ * with interactive children. A tree grid provides users with a way to navigate nested hierarchical
43
+ * information.
44
+ *
45
+ * @param props - Props for the treegrid.
46
+ * @param state - State for the treegrid, as returned by `useTreeState`.
47
+ * @param ref - The ref attached to the treegrid element.
48
+ */
49
+ export function useTree<T>(
50
+ props: AriaTreeOptions<T>,
51
+ state: TreeState<T>,
52
+ ref: RefObject<HTMLElement | null>,
53
+ ): TreeAria;
54
+ // Slot-threading form: sibling ported hooks pass their derived sub-slot as the trailing arg.
55
+ export function useTree<T>(
56
+ props: AriaTreeOptions<T>,
57
+ state: TreeState<T>,
58
+ ref: RefObject<HTMLElement | null>,
59
+ slot: symbol | undefined,
60
+ ): TreeAria;
61
+ export function useTree(...args: any[]): TreeAria {
62
+ const [user, slotArg] = splitSlot(args);
63
+ const slot = slotArg ?? S('useTree');
64
+ const props = user[0] as AriaTreeOptions<any>;
65
+ const state = user[1] as TreeState<any>;
66
+ const ref = user[2] as RefObject<HTMLElement | null>;
67
+
68
+ // TreeState is a structural superset of ListState (collection/disabledKeys/selectionManager),
69
+ // exactly like upstream's call into useGridList.
70
+ let { gridProps } = useGridList(props as any, state as any, ref, subSlot(slot, 'gridList'));
71
+ gridProps.role = 'treegrid';
72
+
73
+ return {
74
+ gridProps,
75
+ };
76
+ }
@@ -0,0 +1,109 @@
1
+ // Ported from react-aria (source: .react-spectrum/packages/react-aria/src/tree/useTreeItem.ts).
2
+ // octane adaptations:
3
+ // - The Parcel glob intl import becomes the generated src/intl/tree index (verbatim
4
+ // locale JSONs from the pinned checkout).
5
+ // - `TreeState` comes from the ported stately tree hook; `DOMAttributes` is a local
6
+ // structural prop-bag alias (upstream's is typed over React's synthetic handlers).
7
+ // - Public-hook slot threading (splitSlot/subSlot).
8
+ import type { FocusableElement, Node, RefObject } from '@react-types/shared';
9
+ import type { AriaButtonProps } from '../button/useButton';
10
+ import {
11
+ type AriaGridListItemOptions,
12
+ type GridListItemAria,
13
+ useGridListItem,
14
+ } from '../gridlist/useGridListItem';
15
+ import { useLocalizedStringFormatter } from '../i18n/useLocalizedStringFormatter';
16
+ import intlMessages from '../intl/tree';
17
+ import type { TreeState } from '../stately/tree/useTreeState';
18
+ import { useLabels } from '../utils/useLabels';
19
+
20
+ import { S, splitSlot, subSlot } from '../internal';
21
+
22
+ // octane adaptation: minimal structural DOMAttributes (upstream's drags React's synthetic
23
+ // handler types along).
24
+ type DOMAttributes = Record<string, any>;
25
+
26
+ export interface AriaTreeItemOptions extends Omit<AriaGridListItemOptions, 'isVirtualized'> {
27
+ /**
28
+ * An object representing the treegrid item. Contains all the relevant information that makes up
29
+ * the treegrid row.
30
+ */
31
+ node: Node<unknown>;
32
+ }
33
+
34
+ export interface TreeItemAria extends GridListItemAria {
35
+ /** Props for the tree grid row element. */
36
+ rowProps: DOMAttributes;
37
+ /** Props for the tree grid cell element within the tree grid list row. */
38
+ gridCellProps: DOMAttributes;
39
+ /** Props for the tree grid row description element, if any. */
40
+ descriptionProps: DOMAttributes;
41
+ /** Props for the tree grid row expand button. */
42
+ expandButtonProps: AriaButtonProps;
43
+ }
44
+
45
+ /**
46
+ * Provides the behavior and accessibility implementation for a row in a tree grid list.
47
+ *
48
+ * @param props - Props for the row.
49
+ * @param state - State of the parent list, as returned by `useTreeState`.
50
+ * @param ref - The ref attached to the row element.
51
+ */
52
+ export function useTreeItem<T>(
53
+ props: AriaTreeItemOptions,
54
+ state: TreeState<T>,
55
+ ref: RefObject<FocusableElement | null>,
56
+ ): TreeItemAria;
57
+ // Slot-threading form: sibling ported hooks pass their derived sub-slot as the trailing arg.
58
+ export function useTreeItem<T>(
59
+ props: AriaTreeItemOptions,
60
+ state: TreeState<T>,
61
+ ref: RefObject<FocusableElement | null>,
62
+ slot: symbol | undefined,
63
+ ): TreeItemAria;
64
+ export function useTreeItem(...args: any[]): TreeItemAria {
65
+ const [user, slotArg] = splitSlot(args);
66
+ const slot = slotArg ?? S('useTreeItem');
67
+ const props = user[0] as AriaTreeItemOptions;
68
+ const state = user[1] as TreeState<any>;
69
+ const ref = user[2] as RefObject<FocusableElement | null>;
70
+
71
+ let { node } = props;
72
+ let gridListAria = useGridListItem(props, state, ref, subSlot(slot, 'gridListItem'));
73
+ let isExpanded = gridListAria.rowProps['aria-expanded'] === true;
74
+ let stringFormatter = useLocalizedStringFormatter(
75
+ intlMessages,
76
+ '@react-aria/tree',
77
+ subSlot(slot, 'strings'),
78
+ );
79
+ let labelProps = useLabels(
80
+ {
81
+ 'aria-label': isExpanded
82
+ ? stringFormatter.format('collapse')
83
+ : stringFormatter.format('expand'),
84
+ 'aria-labelledby': gridListAria.rowProps.id,
85
+ },
86
+ undefined,
87
+ subSlot(slot, 'labels'),
88
+ );
89
+
90
+ let expandButtonProps = {
91
+ onPress: () => {
92
+ if (!gridListAria.isDisabled) {
93
+ state.toggleKey(node.key);
94
+ state.selectionManager.setFocused(true);
95
+ state.selectionManager.setFocusedKey(node.key);
96
+ }
97
+ },
98
+ excludeFromTabOrder: true,
99
+ preventFocusOnPress: true,
100
+ 'data-react-aria-prevent-focus': true,
101
+ ...labelProps,
102
+ };
103
+
104
+ // TODO: should it return a state specifically for isExpanded? Or is aria attribute sufficient?
105
+ return {
106
+ ...gridListAria,
107
+ expandButtonProps,
108
+ };
109
+ }
@@ -3,10 +3,7 @@
3
3
  // (callback ref with optional cleanup, or object ref), passed as ordinary props.
4
4
 
5
5
  export type MergableRef<T> =
6
- | ((value: T | null) => void | (() => void))
7
- | { current: T | null }
8
- | null
9
- | undefined;
6
+ ((value: T | null) => void | (() => void)) | { current: T | null } | null | undefined;
10
7
 
11
8
  /**
12
9
  * Merges multiple refs into one. Works with either callback or object refs.
@@ -25,8 +25,7 @@ export function useFormReset(...args: any[]): void {
25
25
  const [user, slotArg] = splitSlot(args);
26
26
  const slot = slotArg ?? S('useFormReset');
27
27
  const ref = user[0] as
28
- | { current: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | null }
29
- | undefined;
28
+ { current: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | null } | undefined;
30
29
  const initialValue = user[1];
31
30
  const onReset = user[2] as (value: any) => void;
32
31
 
@@ -21,20 +21,14 @@ export function useObjectRef<T>(
21
21
  // Slot-threading form: sibling ported hooks pass their derived sub-slot as the trailing arg.
22
22
  export function useObjectRef<T>(
23
23
  ref:
24
- | ((instance: T | null) => (() => void) | void)
25
- | MutableRefObject<T | null>
26
- | null
27
- | undefined,
24
+ ((instance: T | null) => (() => void) | void) | MutableRefObject<T | null> | null | undefined,
28
25
  slot: symbol | undefined,
29
26
  ): MutableRefObject<T | null>;
30
27
  export function useObjectRef(...args: any[]): any {
31
28
  const [user, slotArg] = splitSlot(args);
32
29
  const slot = slotArg ?? S('useObjectRef');
33
30
  const ref = user[0] as
34
- | ((instance: any) => (() => void) | void)
35
- | MutableRefObject<any>
36
- | null
37
- | undefined;
31
+ ((instance: any) => (() => void) | void) | MutableRefObject<any> | null | undefined;
38
32
 
39
33
  const objRef: MutableRefObject<any> = useRef(null, subSlot(slot, 'obj'));
40
34
  const cleanupRef: MutableRefObject<(() => void) | void> = useRef(