@chromvoid/headless-ui 0.1.0

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 (191) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +99 -0
  3. package/dist/a11y-contracts/index.d.ts +23 -0
  4. package/dist/a11y-contracts/index.js +1 -0
  5. package/dist/accordion/index.d.ts +78 -0
  6. package/dist/accordion/index.js +264 -0
  7. package/dist/adapters/index.d.ts +9 -0
  8. package/dist/adapters/index.js +1 -0
  9. package/dist/alert/index.d.ts +33 -0
  10. package/dist/alert/index.js +54 -0
  11. package/dist/alert-dialog/index.d.ts +69 -0
  12. package/dist/alert-dialog/index.js +94 -0
  13. package/dist/badge/index.d.ts +48 -0
  14. package/dist/badge/index.js +89 -0
  15. package/dist/breadcrumb/index.d.ts +55 -0
  16. package/dist/breadcrumb/index.js +77 -0
  17. package/dist/button/index.d.ts +46 -0
  18. package/dist/button/index.js +86 -0
  19. package/dist/callout/index.d.ts +41 -0
  20. package/dist/callout/index.js +63 -0
  21. package/dist/card/index.d.ts +54 -0
  22. package/dist/card/index.js +103 -0
  23. package/dist/carousel/index.d.ts +98 -0
  24. package/dist/carousel/index.js +243 -0
  25. package/dist/checkbox/index.d.ts +50 -0
  26. package/dist/checkbox/index.js +87 -0
  27. package/dist/combobox/index.d.ts +114 -0
  28. package/dist/combobox/index.js +431 -0
  29. package/dist/command-palette/index.d.ts +73 -0
  30. package/dist/command-palette/index.js +147 -0
  31. package/dist/context-menu/index.d.ts +111 -0
  32. package/dist/context-menu/index.js +372 -0
  33. package/dist/copy-button/index.d.ts +62 -0
  34. package/dist/copy-button/index.js +183 -0
  35. package/dist/core/index.d.ts +20 -0
  36. package/dist/core/index.js +2 -0
  37. package/dist/core/selection.d.ts +5 -0
  38. package/dist/core/selection.js +39 -0
  39. package/dist/core/value-range.d.ts +49 -0
  40. package/dist/core/value-range.js +134 -0
  41. package/dist/date-picker/index.d.ts +210 -0
  42. package/dist/date-picker/index.js +895 -0
  43. package/dist/dialog/index.d.ts +95 -0
  44. package/dist/dialog/index.js +153 -0
  45. package/dist/disclosure/index.d.ts +52 -0
  46. package/dist/disclosure/index.js +159 -0
  47. package/dist/drawer/index.d.ts +30 -0
  48. package/dist/drawer/index.js +39 -0
  49. package/dist/feed/index.d.ts +77 -0
  50. package/dist/feed/index.js +260 -0
  51. package/dist/grid/index.d.ts +103 -0
  52. package/dist/grid/index.js +415 -0
  53. package/dist/index.d.ts +51 -0
  54. package/dist/index.js +51 -0
  55. package/dist/input/index.d.ts +86 -0
  56. package/dist/input/index.js +156 -0
  57. package/dist/interactions/composite-navigation.d.ts +69 -0
  58. package/dist/interactions/composite-navigation.js +169 -0
  59. package/dist/interactions/index.d.ts +15 -0
  60. package/dist/interactions/index.js +4 -0
  61. package/dist/interactions/keyboard-intents.d.ts +16 -0
  62. package/dist/interactions/keyboard-intents.js +33 -0
  63. package/dist/interactions/overlay-focus.d.ts +40 -0
  64. package/dist/interactions/overlay-focus.js +93 -0
  65. package/dist/interactions/typeahead.d.ts +20 -0
  66. package/dist/interactions/typeahead.js +41 -0
  67. package/dist/landmarks/index.d.ts +39 -0
  68. package/dist/landmarks/index.js +58 -0
  69. package/dist/link/index.d.ts +34 -0
  70. package/dist/link/index.js +39 -0
  71. package/dist/listbox/index.d.ts +92 -0
  72. package/dist/listbox/index.js +337 -0
  73. package/dist/menu/index.d.ts +132 -0
  74. package/dist/menu/index.js +541 -0
  75. package/dist/menu-button/index.d.ts +71 -0
  76. package/dist/menu-button/index.js +121 -0
  77. package/dist/meter/index.d.ts +45 -0
  78. package/dist/meter/index.js +106 -0
  79. package/dist/number/index.d.ts +113 -0
  80. package/dist/number/index.js +252 -0
  81. package/dist/popover/index.d.ts +70 -0
  82. package/dist/popover/index.js +126 -0
  83. package/dist/progress/index.d.ts +49 -0
  84. package/dist/progress/index.js +79 -0
  85. package/dist/radio-group/index.d.ts +61 -0
  86. package/dist/radio-group/index.js +150 -0
  87. package/dist/select/index.d.ts +92 -0
  88. package/dist/select/index.js +239 -0
  89. package/dist/sidebar/index.d.ts +74 -0
  90. package/dist/sidebar/index.js +186 -0
  91. package/dist/slider/index.d.ts +61 -0
  92. package/dist/slider/index.js +150 -0
  93. package/dist/slider-multi-thumb/index.d.ts +70 -0
  94. package/dist/slider-multi-thumb/index.js +222 -0
  95. package/dist/spinbutton/index.d.ts +75 -0
  96. package/dist/spinbutton/index.js +214 -0
  97. package/dist/spinner/index.d.ts +1 -0
  98. package/dist/spinner/index.js +1 -0
  99. package/dist/spinner/spinner.d.ts +23 -0
  100. package/dist/spinner/spinner.js +25 -0
  101. package/dist/switch/index.d.ts +40 -0
  102. package/dist/switch/index.js +61 -0
  103. package/dist/table/index.d.ts +117 -0
  104. package/dist/table/index.js +377 -0
  105. package/dist/tabs/index.d.ts +63 -0
  106. package/dist/tabs/index.js +174 -0
  107. package/dist/textarea/index.d.ts +68 -0
  108. package/dist/textarea/index.js +137 -0
  109. package/dist/toast/index.d.ts +67 -0
  110. package/dist/toast/index.js +145 -0
  111. package/dist/toolbar/index.d.ts +59 -0
  112. package/dist/toolbar/index.js +139 -0
  113. package/dist/tooltip/index.d.ts +52 -0
  114. package/dist/tooltip/index.js +169 -0
  115. package/dist/treegrid/index.d.ts +101 -0
  116. package/dist/treegrid/index.js +463 -0
  117. package/dist/treeview/index.d.ts +68 -0
  118. package/dist/treeview/index.js +370 -0
  119. package/dist/window-splitter/index.d.ts +65 -0
  120. package/dist/window-splitter/index.js +204 -0
  121. package/package.json +92 -0
  122. package/specs/ADR-001-headless-architecture.md +461 -0
  123. package/specs/ADR-002-repo-release-model.md +108 -0
  124. package/specs/ADR-003-public-api-versioning.md +136 -0
  125. package/specs/ADR-004-focus-selection-policy.md +117 -0
  126. package/specs/IMPLEMENTATION-ROADMAP.md +237 -0
  127. package/specs/ISSUE-BACKLOG.md +681 -0
  128. package/specs/RELEASE-CANDIDATE.md +30 -0
  129. package/specs/components/accordion.md +130 -0
  130. package/specs/components/alert-dialog.md +72 -0
  131. package/specs/components/alert.md +65 -0
  132. package/specs/components/badge.md +220 -0
  133. package/specs/components/breadcrumb.md +74 -0
  134. package/specs/components/button.md +115 -0
  135. package/specs/components/callout.md +195 -0
  136. package/specs/components/card.md +280 -0
  137. package/specs/components/carousel.md +140 -0
  138. package/specs/components/checkbox.md +172 -0
  139. package/specs/components/combobox.md +423 -0
  140. package/specs/components/command-palette.md +92 -0
  141. package/specs/components/context-menu.md +556 -0
  142. package/specs/components/copy-button.md +293 -0
  143. package/specs/components/date-picker.md +400 -0
  144. package/specs/components/dialog.md +298 -0
  145. package/specs/components/disclosure.md +257 -0
  146. package/specs/components/drawer.md +353 -0
  147. package/specs/components/feed.md +265 -0
  148. package/specs/components/grid.md +186 -0
  149. package/specs/components/input.md +254 -0
  150. package/specs/components/landmarks.md +136 -0
  151. package/specs/components/link.md +134 -0
  152. package/specs/components/listbox.md +351 -0
  153. package/specs/components/menu-button.md +76 -0
  154. package/specs/components/menu.md +623 -0
  155. package/specs/components/meter.md +149 -0
  156. package/specs/components/number.md +393 -0
  157. package/specs/components/popover.md +252 -0
  158. package/specs/components/progress.md +188 -0
  159. package/specs/components/radio-group.md +151 -0
  160. package/specs/components/select.md +144 -0
  161. package/specs/components/sidebar.md +321 -0
  162. package/specs/components/slider-multi-thumb.md +78 -0
  163. package/specs/components/slider.md +84 -0
  164. package/specs/components/spinbutton.md +140 -0
  165. package/specs/components/spinner.md +132 -0
  166. package/specs/components/switch.md +175 -0
  167. package/specs/components/table.md +403 -0
  168. package/specs/components/tabs.md +265 -0
  169. package/specs/components/textarea.md +185 -0
  170. package/specs/components/toast.md +198 -0
  171. package/specs/components/toolbar.md +278 -0
  172. package/specs/components/tooltip.md +252 -0
  173. package/specs/components/treegrid.md +281 -0
  174. package/specs/components/treeview.md +91 -0
  175. package/specs/components/window-splitter.md +297 -0
  176. package/specs/ops/git-shard-sync.md +107 -0
  177. package/specs/ops/release-checklist.md +76 -0
  178. package/specs/release/GAP-TO-GREEN-ISSUES.md +88 -0
  179. package/specs/release/api-freeze-candidate.md +54 -0
  180. package/specs/release/changelog-automation.md +76 -0
  181. package/specs/release/changelog.generated.md +53 -0
  182. package/specs/release/changelog.patch.generated.md +46 -0
  183. package/specs/release/consumer-integration.md +53 -0
  184. package/specs/release/migration-notes-pre-v1.md +40 -0
  185. package/specs/release/mvp-changelog.md +57 -0
  186. package/specs/release/release-notes-template.md +61 -0
  187. package/specs/release/release-rehearsal.md +113 -0
  188. package/specs/release/semver-deprecation-dry-run.md +89 -0
  189. package/specs/release/shard-release-drill-report.md +50 -0
  190. package/specs/release/shard-release-follow-ups.md +31 -0
  191. package/specs/signals.md +208 -0
@@ -0,0 +1,463 @@
1
+ import { action, atom, computed } from '@reatom/core';
2
+ const cellKey = (rowId, colId) => `${rowId}::${colId}`;
3
+ const collectDescendantIds = (metaById, rowId) => {
4
+ const descendants = [];
5
+ const queue = [...(metaById.get(rowId)?.childIds ?? [])];
6
+ while (queue.length > 0) {
7
+ const current = queue.shift();
8
+ if (!current)
9
+ continue;
10
+ descendants.push(current);
11
+ queue.push(...(metaById.get(current)?.childIds ?? []));
12
+ }
13
+ return descendants;
14
+ };
15
+ const buildRowsMeta = (rows) => {
16
+ const rowById = new Map();
17
+ const metaById = new Map();
18
+ const rootIds = [];
19
+ const orderedIds = [];
20
+ const visit = (siblings, parentId, level) => {
21
+ const setSize = siblings.length;
22
+ siblings.forEach((row, index) => {
23
+ const childIds = (row.children ?? []).map((child) => child.id);
24
+ const rowIndex = orderedIds.length + 1;
25
+ orderedIds.push(row.id);
26
+ rowById.set(row.id, row);
27
+ metaById.set(row.id, {
28
+ id: row.id,
29
+ parentId,
30
+ childIds,
31
+ level,
32
+ posInSet: index + 1,
33
+ setSize,
34
+ rowIndex,
35
+ disabled: row.disabled === true,
36
+ });
37
+ if (parentId == null) {
38
+ rootIds.push(row.id);
39
+ }
40
+ if (childIds.length > 0) {
41
+ visit(row.children ?? [], row.id, level + 1);
42
+ }
43
+ });
44
+ };
45
+ visit(rows, null, 1);
46
+ return { rowById, metaById, rootIds };
47
+ };
48
+ export function createTreegrid(options) {
49
+ const idBase = options.idBase ?? 'treegrid';
50
+ const selectionMode = options.selectionMode ?? 'single';
51
+ const columns = [...options.columns];
52
+ const { rowById, metaById, rootIds } = buildRowsMeta(options.rows);
53
+ const allRowIds = [...metaById.keys()];
54
+ const allRowIdSet = new Set(allRowIds);
55
+ const enabledRowIdSet = new Set(allRowIds.filter((rowId) => metaById.get(rowId)?.disabled !== true));
56
+ const columnById = new Map(columns.map((column, index) => [column.id, { column, index }]));
57
+ const disabledCellSet = new Set((options.disabledCells ?? []).map((cell) => cellKey(cell.rowId, cell.colId)));
58
+ const isBranchRow = (rowId) => (metaById.get(rowId)?.childIds.length ?? 0) > 0;
59
+ const isRowExpanded = (rowId) => expandedRowIdsAtom().has(rowId);
60
+ const hasCell = (rowId, colId) => rowById.has(rowId) && columnById.has(colId);
61
+ const getColumnIndex = (colId) => columnById.get(colId)?.index ?? -1;
62
+ const colAriaIndex = (colId) => {
63
+ const info = columnById.get(colId);
64
+ if (!info)
65
+ return 1;
66
+ return info.column.index ?? info.index + 1;
67
+ };
68
+ const isCellDisabled = (rowId, colId) => {
69
+ const rowMeta = metaById.get(rowId);
70
+ const columnInfo = columnById.get(colId);
71
+ if (!rowMeta || !columnInfo)
72
+ return true;
73
+ if (rowMeta.disabled)
74
+ return true;
75
+ if (columnInfo.column.disabled)
76
+ return true;
77
+ if (disabledCellSet.has(cellKey(rowId, colId)))
78
+ return true;
79
+ return false;
80
+ };
81
+ const expandedRowIdsAtom = atom(new Set((options.initialExpandedRowIds ?? []).filter((rowId) => allRowIdSet.has(rowId) && isBranchRow(rowId))), `${idBase}.expandedRowIds`);
82
+ const selectedRowIdsAtom = atom(new Set((options.initialSelectedRowIds ?? [])
83
+ .filter((rowId) => enabledRowIdSet.has(rowId))
84
+ .slice(0, selectionMode === 'single' ? 1 : undefined)), `${idBase}.selectedRowIds`);
85
+ const getVisibleRowIds = () => {
86
+ const expanded = expandedRowIdsAtom();
87
+ const visible = [];
88
+ const visit = (rowId) => {
89
+ if (!metaById.has(rowId))
90
+ return;
91
+ visible.push(rowId);
92
+ if (!expanded.has(rowId))
93
+ return;
94
+ const childIds = metaById.get(rowId)?.childIds ?? [];
95
+ for (const childId of childIds) {
96
+ visit(childId);
97
+ }
98
+ };
99
+ for (const rootId of rootIds) {
100
+ visit(rootId);
101
+ }
102
+ return visible;
103
+ };
104
+ const getVisibleEnabledRowIds = () => getVisibleRowIds().filter((rowId) => enabledRowIdSet.has(rowId));
105
+ const firstEnabledCellInRow = (rowId) => {
106
+ for (const column of columns) {
107
+ if (!isCellDisabled(rowId, column.id)) {
108
+ return { rowId, colId: column.id };
109
+ }
110
+ }
111
+ return null;
112
+ };
113
+ const lastEnabledCellInRow = (rowId) => {
114
+ for (let index = columns.length - 1; index >= 0; index -= 1) {
115
+ const colId = columns[index]?.id;
116
+ if (colId != null && !isCellDisabled(rowId, colId)) {
117
+ return { rowId, colId };
118
+ }
119
+ }
120
+ return null;
121
+ };
122
+ const firstEnabledCellInVisibleRows = () => {
123
+ for (const rowId of getVisibleEnabledRowIds()) {
124
+ const cell = firstEnabledCellInRow(rowId);
125
+ if (cell)
126
+ return cell;
127
+ }
128
+ return null;
129
+ };
130
+ const lastEnabledCellInVisibleRows = () => {
131
+ const visible = getVisibleEnabledRowIds();
132
+ for (let index = visible.length - 1; index >= 0; index -= 1) {
133
+ const rowId = visible[index];
134
+ if (!rowId)
135
+ continue;
136
+ const cell = lastEnabledCellInRow(rowId);
137
+ if (cell)
138
+ return cell;
139
+ }
140
+ return null;
141
+ };
142
+ const normalizeCell = (cell) => {
143
+ if (cell == null)
144
+ return firstEnabledCellInVisibleRows();
145
+ if (!hasCell(cell.rowId, cell.colId))
146
+ return firstEnabledCellInVisibleRows();
147
+ if (!getVisibleRowIds().includes(cell.rowId))
148
+ return firstEnabledCellInVisibleRows();
149
+ if (isCellDisabled(cell.rowId, cell.colId))
150
+ return firstEnabledCellInVisibleRows();
151
+ return cell;
152
+ };
153
+ const activeCellIdAtom = atom(normalizeCell(options.initialActiveCellId ?? null), `${idBase}.activeCellId`);
154
+ const setActiveCell = action((cell) => {
155
+ const normalized = normalizeCell(cell);
156
+ if (!normalized) {
157
+ activeCellIdAtom.set(null);
158
+ return;
159
+ }
160
+ activeCellIdAtom.set(normalized);
161
+ }, `${idBase}.setActiveCell`);
162
+ const moveToAdjacentRow = (direction) => {
163
+ const current = normalizeCell(activeCellIdAtom());
164
+ if (!current)
165
+ return;
166
+ const visibleRows = getVisibleEnabledRowIds();
167
+ const currentRowIndex = visibleRows.indexOf(current.rowId);
168
+ if (currentRowIndex < 0)
169
+ return;
170
+ for (let nextIndex = currentRowIndex + direction; nextIndex >= 0 && nextIndex < visibleRows.length; nextIndex += direction) {
171
+ const rowId = visibleRows[nextIndex];
172
+ if (!rowId)
173
+ continue;
174
+ if (!isCellDisabled(rowId, current.colId)) {
175
+ setActiveCell({ rowId, colId: current.colId });
176
+ return;
177
+ }
178
+ }
179
+ };
180
+ const moveWithinRow = (direction) => {
181
+ const current = normalizeCell(activeCellIdAtom());
182
+ if (!current)
183
+ return;
184
+ const currentColIndex = getColumnIndex(current.colId);
185
+ if (currentColIndex < 0)
186
+ return;
187
+ for (let nextCol = currentColIndex + direction; nextCol >= 0 && nextCol < columns.length; nextCol += direction) {
188
+ const colId = columns[nextCol]?.id;
189
+ if (colId != null && !isCellDisabled(current.rowId, colId)) {
190
+ setActiveCell({ rowId: current.rowId, colId });
191
+ return;
192
+ }
193
+ }
194
+ };
195
+ const moveUp = action(() => {
196
+ moveToAdjacentRow(-1);
197
+ }, `${idBase}.moveUp`);
198
+ const moveDown = action(() => {
199
+ moveToAdjacentRow(1);
200
+ }, `${idBase}.moveDown`);
201
+ const moveRowStart = action(() => {
202
+ const current = normalizeCell(activeCellIdAtom());
203
+ if (!current)
204
+ return;
205
+ const first = firstEnabledCellInRow(current.rowId);
206
+ if (first) {
207
+ setActiveCell(first);
208
+ }
209
+ }, `${idBase}.moveRowStart`);
210
+ const moveRowEnd = action(() => {
211
+ const current = normalizeCell(activeCellIdAtom());
212
+ if (!current)
213
+ return;
214
+ const last = lastEnabledCellInRow(current.rowId);
215
+ if (last) {
216
+ setActiveCell(last);
217
+ }
218
+ }, `${idBase}.moveRowEnd`);
219
+ const moveGridStart = action(() => {
220
+ const first = firstEnabledCellInVisibleRows();
221
+ if (first) {
222
+ setActiveCell(first);
223
+ }
224
+ }, `${idBase}.moveGridStart`);
225
+ const moveGridEnd = action(() => {
226
+ const last = lastEnabledCellInVisibleRows();
227
+ if (last) {
228
+ setActiveCell(last);
229
+ }
230
+ }, `${idBase}.moveGridEnd`);
231
+ const expandRow = action((rowId) => {
232
+ if (!isBranchRow(rowId))
233
+ return;
234
+ if (expandedRowIdsAtom().has(rowId))
235
+ return;
236
+ const next = new Set(expandedRowIdsAtom());
237
+ next.add(rowId);
238
+ expandedRowIdsAtom.set(next);
239
+ }, `${idBase}.expandRow`);
240
+ const collapseRow = action((rowId) => {
241
+ if (!isBranchRow(rowId))
242
+ return;
243
+ if (!expandedRowIdsAtom().has(rowId))
244
+ return;
245
+ const next = new Set(expandedRowIdsAtom());
246
+ next.delete(rowId);
247
+ expandedRowIdsAtom.set(next);
248
+ const active = activeCellIdAtom();
249
+ if (!active || active.rowId === rowId)
250
+ return;
251
+ const descendants = collectDescendantIds(metaById, rowId);
252
+ if (descendants.includes(active.rowId)) {
253
+ if (!isCellDisabled(rowId, active.colId)) {
254
+ setActiveCell({ rowId, colId: active.colId });
255
+ }
256
+ else {
257
+ const fallback = firstEnabledCellInRow(rowId);
258
+ if (fallback) {
259
+ setActiveCell(fallback);
260
+ }
261
+ }
262
+ }
263
+ }, `${idBase}.collapseRow`);
264
+ const toggleRowExpanded = action((rowId) => {
265
+ if (expandedRowIdsAtom().has(rowId)) {
266
+ collapseRow(rowId);
267
+ }
268
+ else {
269
+ expandRow(rowId);
270
+ }
271
+ }, `${idBase}.toggleRowExpanded`);
272
+ const selectRow = action((rowId) => {
273
+ if (!enabledRowIdSet.has(rowId))
274
+ return;
275
+ if (selectionMode === 'single') {
276
+ selectedRowIdsAtom.set(new Set([rowId]));
277
+ return;
278
+ }
279
+ selectedRowIdsAtom.set(new Set([rowId]));
280
+ }, `${idBase}.selectRow`);
281
+ const toggleRowSelection = action((rowId) => {
282
+ if (!enabledRowIdSet.has(rowId))
283
+ return;
284
+ if (selectionMode === 'single') {
285
+ selectedRowIdsAtom.set(new Set([rowId]));
286
+ return;
287
+ }
288
+ const next = new Set(selectedRowIdsAtom());
289
+ if (next.has(rowId)) {
290
+ next.delete(rowId);
291
+ }
292
+ else {
293
+ next.add(rowId);
294
+ }
295
+ selectedRowIdsAtom.set(next);
296
+ }, `${idBase}.toggleRowSelection`);
297
+ const moveLeft = action(() => {
298
+ const current = normalizeCell(activeCellIdAtom());
299
+ if (!current)
300
+ return;
301
+ const rowMeta = metaById.get(current.rowId);
302
+ if (!rowMeta)
303
+ return;
304
+ if (isBranchRow(current.rowId) && isRowExpanded(current.rowId)) {
305
+ collapseRow(current.rowId);
306
+ return;
307
+ }
308
+ if (rowMeta.parentId != null) {
309
+ const parentId = rowMeta.parentId;
310
+ if (!isCellDisabled(parentId, current.colId)) {
311
+ setActiveCell({ rowId: parentId, colId: current.colId });
312
+ }
313
+ else {
314
+ const fallback = firstEnabledCellInRow(parentId);
315
+ if (fallback) {
316
+ setActiveCell(fallback);
317
+ }
318
+ }
319
+ return;
320
+ }
321
+ moveWithinRow(-1);
322
+ }, `${idBase}.moveLeft`);
323
+ const moveRight = action(() => {
324
+ const current = normalizeCell(activeCellIdAtom());
325
+ if (!current)
326
+ return;
327
+ if (isBranchRow(current.rowId) && !isRowExpanded(current.rowId)) {
328
+ expandRow(current.rowId);
329
+ return;
330
+ }
331
+ if (isBranchRow(current.rowId) && isRowExpanded(current.rowId)) {
332
+ const firstChildId = metaById.get(current.rowId)?.childIds[0];
333
+ if (firstChildId != null && !isCellDisabled(firstChildId, current.colId)) {
334
+ setActiveCell({ rowId: firstChildId, colId: current.colId });
335
+ return;
336
+ }
337
+ if (firstChildId != null) {
338
+ const fallback = firstEnabledCellInRow(firstChildId);
339
+ if (fallback) {
340
+ setActiveCell(fallback);
341
+ return;
342
+ }
343
+ }
344
+ }
345
+ moveWithinRow(1);
346
+ }, `${idBase}.moveRight`);
347
+ const handleKeyDown = action((event) => {
348
+ const ctrlOrMeta = event.ctrlKey === true || event.metaKey === true;
349
+ switch (event.key) {
350
+ case 'ArrowUp':
351
+ moveUp();
352
+ return;
353
+ case 'ArrowDown':
354
+ moveDown();
355
+ return;
356
+ case 'ArrowLeft':
357
+ moveLeft();
358
+ return;
359
+ case 'ArrowRight':
360
+ moveRight();
361
+ return;
362
+ case 'Home':
363
+ if (ctrlOrMeta) {
364
+ moveGridStart();
365
+ }
366
+ else {
367
+ moveRowStart();
368
+ }
369
+ return;
370
+ case 'End':
371
+ if (ctrlOrMeta) {
372
+ moveGridEnd();
373
+ }
374
+ else {
375
+ moveRowEnd();
376
+ }
377
+ return;
378
+ default:
379
+ return;
380
+ }
381
+ }, `${idBase}.handleKeyDown`);
382
+ const rowCountAtom = computed(() => allRowIds.length, `${idBase}.rowCount`);
383
+ const columnCountAtom = computed(() => columns.length, `${idBase}.columnCount`);
384
+ const actions = {
385
+ moveUp,
386
+ moveDown,
387
+ moveLeft,
388
+ moveRight,
389
+ moveRowStart,
390
+ moveRowEnd,
391
+ expandRow,
392
+ collapseRow,
393
+ toggleRowExpanded,
394
+ selectRow,
395
+ toggleRowSelection,
396
+ handleKeyDown,
397
+ };
398
+ const cellDomId = (rowId, colId) => `${idBase}-cell-${rowId}-${colId}`;
399
+ const contracts = {
400
+ getTreegridProps() {
401
+ return {
402
+ id: `${idBase}-root`,
403
+ role: 'treegrid',
404
+ tabindex: '-1',
405
+ 'aria-label': options.ariaLabel,
406
+ 'aria-labelledby': options.ariaLabelledBy,
407
+ 'aria-multiselectable': selectionMode === 'multiple' ? 'true' : 'false',
408
+ 'aria-rowcount': rowCountAtom(),
409
+ 'aria-colcount': columnCountAtom(),
410
+ };
411
+ },
412
+ getRowProps(rowId) {
413
+ const rowMeta = metaById.get(rowId);
414
+ if (!rowMeta) {
415
+ throw new Error(`Unknown treegrid row id: ${rowId}`);
416
+ }
417
+ const isBranch = rowMeta.childIds.length > 0;
418
+ const expanded = isBranch ? expandedRowIdsAtom().has(rowId) : undefined;
419
+ return {
420
+ id: `${idBase}-row-${rowId}`,
421
+ role: 'row',
422
+ 'aria-level': rowMeta.level,
423
+ 'aria-posinset': rowMeta.posInSet,
424
+ 'aria-setsize': rowMeta.setSize,
425
+ 'aria-rowindex': rowById.get(rowId)?.index ?? rowMeta.rowIndex,
426
+ 'aria-expanded': expanded == null ? undefined : expanded ? 'true' : 'false',
427
+ 'aria-selected': selectedRowIdsAtom().has(rowId) ? 'true' : 'false',
428
+ 'aria-disabled': rowMeta.disabled ? 'true' : undefined,
429
+ };
430
+ },
431
+ getCellProps(rowId, colId) {
432
+ if (!hasCell(rowId, colId)) {
433
+ throw new Error(`Unknown treegrid cell id: ${rowId}:${colId}`);
434
+ }
435
+ const activeCell = activeCellIdAtom();
436
+ const isActive = activeCell?.rowId === rowId && activeCell.colId === colId;
437
+ const isDisabled = isCellDisabled(rowId, colId);
438
+ const role = columnById.get(colId)?.column.cellRole ?? 'gridcell';
439
+ return {
440
+ id: cellDomId(rowId, colId),
441
+ role,
442
+ tabindex: isActive && !isDisabled ? '0' : '-1',
443
+ 'aria-colindex': colAriaIndex(colId),
444
+ 'aria-selected': selectedRowIdsAtom().has(rowId) ? 'true' : 'false',
445
+ 'aria-disabled': isDisabled ? 'true' : undefined,
446
+ 'data-active': isActive ? 'true' : 'false',
447
+ onFocus: () => setActiveCell({ rowId, colId }),
448
+ };
449
+ },
450
+ };
451
+ const state = {
452
+ activeCellId: activeCellIdAtom,
453
+ expandedRowIds: expandedRowIdsAtom,
454
+ selectedRowIds: selectedRowIdsAtom,
455
+ rowCount: rowCountAtom,
456
+ columnCount: columnCountAtom,
457
+ };
458
+ return {
459
+ state,
460
+ actions,
461
+ contracts,
462
+ };
463
+ }
@@ -0,0 +1,68 @@
1
+ import { type Atom } from '@reatom/core';
2
+ export type TreeSelectionMode = 'single' | 'multiple';
3
+ export interface TreeNode {
4
+ id: string;
5
+ label?: string;
6
+ disabled?: boolean;
7
+ children?: readonly TreeNode[];
8
+ }
9
+ export interface CreateTreeviewOptions {
10
+ nodes: readonly TreeNode[];
11
+ idBase?: string;
12
+ ariaLabel?: string;
13
+ selectionMode?: TreeSelectionMode;
14
+ initialExpandedIds?: readonly string[];
15
+ initialActiveId?: string | null;
16
+ initialSelectedIds?: readonly string[];
17
+ }
18
+ export interface TreeviewState {
19
+ activeId: Atom<string | null>;
20
+ selectedIds: Atom<string[]>;
21
+ expandedIds: Atom<string[]>;
22
+ }
23
+ export interface TreeProps {
24
+ role: 'tree';
25
+ tabindex: '0';
26
+ 'aria-label'?: string;
27
+ 'aria-multiselectable'?: 'true';
28
+ }
29
+ export interface TreeItemProps {
30
+ id: string;
31
+ role: 'treeitem';
32
+ tabindex: '0' | '-1';
33
+ 'aria-level': number;
34
+ 'aria-posinset': number;
35
+ 'aria-setsize': number;
36
+ 'aria-expanded'?: 'true' | 'false';
37
+ 'aria-selected': 'true' | 'false';
38
+ 'aria-disabled'?: 'true';
39
+ 'data-active': 'true' | 'false';
40
+ 'data-expanded'?: 'true' | 'false';
41
+ }
42
+ export interface TreeviewActions {
43
+ setActive(id: string | null): void;
44
+ moveNext(): void;
45
+ movePrev(): void;
46
+ moveFirst(): void;
47
+ moveLast(): void;
48
+ expand(id: string): void;
49
+ collapse(id: string): void;
50
+ toggleExpanded(id: string): void;
51
+ expandActive(): void;
52
+ collapseActive(): void;
53
+ select(id: string): void;
54
+ toggleSelected(id: string): void;
55
+ clearSelected(): void;
56
+ handleKeyDown(event: Pick<KeyboardEvent, 'key' | 'shiftKey' | 'ctrlKey' | 'metaKey' | 'altKey'>): void;
57
+ }
58
+ export interface TreeviewContracts {
59
+ getTreeProps(): TreeProps;
60
+ getItemProps(id: string): TreeItemProps;
61
+ getVisibleNodeIds(): readonly string[];
62
+ }
63
+ export interface TreeviewModel {
64
+ readonly state: TreeviewState;
65
+ readonly actions: TreeviewActions;
66
+ readonly contracts: TreeviewContracts;
67
+ }
68
+ export declare function createTreeview(options: CreateTreeviewOptions): TreeviewModel;