@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,377 @@
1
+ import { action, atom, computed } from '@reatom/core';
2
+ const rowIndexById = (rows) => new Map(rows.map((row, index) => [row.id, row.index ?? index + 1]));
3
+ const columnIndexById = (columns) => new Map(columns.map((column, index) => [column.id, column.index ?? index + 1]));
4
+ const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
5
+ export function createTable(options) {
6
+ if (!options.ariaLabel && !options.ariaLabelledBy) {
7
+ throw new Error('Table requires either ariaLabel or ariaLabelledBy for accessibility');
8
+ }
9
+ const idBase = options.idBase ?? 'table';
10
+ const selectable = options.selectable ?? false;
11
+ const interactive = options.interactive ?? false;
12
+ const pageSize = Math.max(options.pageSize ?? 10, 1);
13
+ const columnIds = new Set(options.columns.map((column) => column.id));
14
+ const rowIds = new Set(options.rows.map((row) => row.id));
15
+ const rowIdsList = options.rows.map((row) => row.id);
16
+ const rowIndexMap = rowIndexById(options.rows);
17
+ const columnIndexMap = columnIndexById(options.columns);
18
+ const sortColumnIdAtom = atom(options.initialSortColumnId ?? null, `${idBase}.sortColumnId`);
19
+ const sortDirectionAtom = atom(options.initialSortDirection ?? 'none', `${idBase}.sortDirection`);
20
+ const rowCountAtom = computed(() => Math.max(options.totalRowCount ?? options.rows.length, options.rows.length), `${idBase}.rowCount`);
21
+ const columnCountAtom = computed(() => Math.max(options.totalColumnCount ?? options.columns.length, options.columns.length), `${idBase}.columnCount`);
22
+ const sortBy = action((columnId, direction) => {
23
+ if (direction === 'none') {
24
+ sortColumnIdAtom.set(null);
25
+ sortDirectionAtom.set('none');
26
+ return;
27
+ }
28
+ if (!columnIds.has(columnId))
29
+ return;
30
+ sortColumnIdAtom.set(columnId);
31
+ sortDirectionAtom.set(direction);
32
+ }, `${idBase}.sortBy`);
33
+ const clearSort = action(() => {
34
+ sortColumnIdAtom.set(null);
35
+ sortDirectionAtom.set('none');
36
+ }, `${idBase}.clearSort`);
37
+ const resolveInitialSelectedRowIds = () => {
38
+ if (selectable === false)
39
+ return new Set();
40
+ const initial = (options.initialSelectedRowIds ?? []).filter((id) => rowIds.has(id));
41
+ if (selectable === 'single') {
42
+ return initial.length > 0 ? new Set([initial[0]]) : new Set();
43
+ }
44
+ return new Set(initial);
45
+ };
46
+ const selectedRowIdsAtom = atom(resolveInitialSelectedRowIds(), `${idBase}.selectedRowIds`);
47
+ const selectRow = action((rowId) => {
48
+ if (selectable === false)
49
+ return;
50
+ if (!rowIds.has(rowId))
51
+ return;
52
+ if (selectable === 'single') {
53
+ selectedRowIdsAtom.set(new Set([rowId]));
54
+ }
55
+ else {
56
+ const next = new Set(selectedRowIdsAtom());
57
+ next.add(rowId);
58
+ selectedRowIdsAtom.set(next);
59
+ }
60
+ }, `${idBase}.selectRow`);
61
+ const deselectRow = action((rowId) => {
62
+ if (selectable === false)
63
+ return;
64
+ const current = selectedRowIdsAtom();
65
+ if (!current.has(rowId))
66
+ return;
67
+ const next = new Set(current);
68
+ next.delete(rowId);
69
+ selectedRowIdsAtom.set(next);
70
+ }, `${idBase}.deselectRow`);
71
+ const toggleRowSelection = action((rowId) => {
72
+ if (selectable === false)
73
+ return;
74
+ if (!rowIds.has(rowId))
75
+ return;
76
+ const current = selectedRowIdsAtom();
77
+ if (current.has(rowId)) {
78
+ const next = new Set(current);
79
+ next.delete(rowId);
80
+ selectedRowIdsAtom.set(next);
81
+ }
82
+ else {
83
+ if (selectable === 'single') {
84
+ selectedRowIdsAtom.set(new Set([rowId]));
85
+ }
86
+ else {
87
+ const next = new Set(current);
88
+ next.add(rowId);
89
+ selectedRowIdsAtom.set(next);
90
+ }
91
+ }
92
+ }, `${idBase}.toggleRowSelection`);
93
+ const selectAllRows = action(() => {
94
+ if (selectable !== 'multi')
95
+ return;
96
+ selectedRowIdsAtom.set(new Set(rowIdsList));
97
+ }, `${idBase}.selectAllRows`);
98
+ const clearSelection = action(() => {
99
+ if (selectable === false)
100
+ return;
101
+ selectedRowIdsAtom.set(new Set());
102
+ }, `${idBase}.clearSelection`);
103
+ const resolveInitialFocus = (index, max) => {
104
+ if (!interactive)
105
+ return null;
106
+ if (index != null)
107
+ return clamp(index, 0, max);
108
+ return 0;
109
+ };
110
+ const focusedRowIndexAtom = atom(resolveInitialFocus(options.initialFocusedRowIndex, options.rows.length - 1), `${idBase}.focusedRowIndex`);
111
+ const focusedColumnIndexAtom = atom(resolveInitialFocus(options.initialFocusedColumnIndex, options.columns.length - 1), `${idBase}.focusedColumnIndex`);
112
+ const moveFocus = action((direction) => {
113
+ if (!interactive)
114
+ return;
115
+ const row = focusedRowIndexAtom();
116
+ const col = focusedColumnIndexAtom();
117
+ if (row == null || col == null)
118
+ return;
119
+ const maxRow = options.rows.length - 1;
120
+ const maxCol = options.columns.length - 1;
121
+ switch (direction) {
122
+ case 'up':
123
+ focusedRowIndexAtom.set(Math.max(0, row - 1));
124
+ break;
125
+ case 'down':
126
+ focusedRowIndexAtom.set(Math.min(maxRow, row + 1));
127
+ break;
128
+ case 'left':
129
+ focusedColumnIndexAtom.set(Math.max(0, col - 1));
130
+ break;
131
+ case 'right':
132
+ focusedColumnIndexAtom.set(Math.min(maxCol, col + 1));
133
+ break;
134
+ }
135
+ }, `${idBase}.moveFocus`);
136
+ const moveFocusToStart = action(() => {
137
+ if (!interactive)
138
+ return;
139
+ focusedRowIndexAtom.set(0);
140
+ focusedColumnIndexAtom.set(0);
141
+ }, `${idBase}.moveFocusToStart`);
142
+ const moveFocusToEnd = action(() => {
143
+ if (!interactive)
144
+ return;
145
+ focusedRowIndexAtom.set(options.rows.length - 1);
146
+ focusedColumnIndexAtom.set(options.columns.length - 1);
147
+ }, `${idBase}.moveFocusToEnd`);
148
+ const moveFocusToRowStart = action(() => {
149
+ if (!interactive)
150
+ return;
151
+ focusedColumnIndexAtom.set(0);
152
+ }, `${idBase}.moveFocusToRowStart`);
153
+ const moveFocusToRowEnd = action(() => {
154
+ if (!interactive)
155
+ return;
156
+ focusedColumnIndexAtom.set(options.columns.length - 1);
157
+ }, `${idBase}.moveFocusToRowEnd`);
158
+ const setFocusedCell = action((rowIndex, columnIndex) => {
159
+ if (!interactive)
160
+ return;
161
+ focusedRowIndexAtom.set(clamp(rowIndex, 0, options.rows.length - 1));
162
+ focusedColumnIndexAtom.set(clamp(columnIndex, 0, options.columns.length - 1));
163
+ }, `${idBase}.setFocusedCell`);
164
+ const pageUpAction = action(() => {
165
+ if (!interactive)
166
+ return;
167
+ const row = focusedRowIndexAtom();
168
+ if (row == null)
169
+ return;
170
+ focusedRowIndexAtom.set(Math.max(0, row - pageSize));
171
+ }, `${idBase}.pageUp`);
172
+ const pageDownAction = action(() => {
173
+ if (!interactive)
174
+ return;
175
+ const row = focusedRowIndexAtom();
176
+ if (row == null)
177
+ return;
178
+ focusedRowIndexAtom.set(Math.min(options.rows.length - 1, row + pageSize));
179
+ }, `${idBase}.pageDown`);
180
+ const handleKeyDown = action((event) => {
181
+ if (!interactive)
182
+ return;
183
+ const ctrlOrMeta = event.ctrlKey === true || event.metaKey === true;
184
+ switch (event.key) {
185
+ case 'ArrowUp':
186
+ moveFocus('up');
187
+ return;
188
+ case 'ArrowDown':
189
+ moveFocus('down');
190
+ return;
191
+ case 'ArrowLeft':
192
+ moveFocus('left');
193
+ return;
194
+ case 'ArrowRight':
195
+ moveFocus('right');
196
+ return;
197
+ case 'Home':
198
+ if (ctrlOrMeta) {
199
+ moveFocusToStart();
200
+ }
201
+ else {
202
+ moveFocusToRowStart();
203
+ }
204
+ return;
205
+ case 'End':
206
+ if (ctrlOrMeta) {
207
+ moveFocusToEnd();
208
+ }
209
+ else {
210
+ moveFocusToRowEnd();
211
+ }
212
+ return;
213
+ case 'PageUp':
214
+ pageUpAction();
215
+ return;
216
+ case 'PageDown':
217
+ pageDownAction();
218
+ return;
219
+ case ' ': {
220
+ if (selectable === false)
221
+ return;
222
+ const focusedRow = focusedRowIndexAtom();
223
+ if (focusedRow == null)
224
+ return;
225
+ const rowId = rowIdsList[focusedRow];
226
+ if (rowId != null) {
227
+ toggleRowSelection(rowId);
228
+ }
229
+ return;
230
+ }
231
+ case 'a': {
232
+ if (ctrlOrMeta && selectable === 'multi') {
233
+ selectAllRows();
234
+ }
235
+ return;
236
+ }
237
+ default:
238
+ return;
239
+ }
240
+ }, `${idBase}.handleKeyDown`);
241
+ const actions = {
242
+ sortBy,
243
+ clearSort,
244
+ selectRow,
245
+ deselectRow,
246
+ toggleRowSelection,
247
+ selectAllRows,
248
+ clearSelection,
249
+ moveFocus,
250
+ moveFocusToStart,
251
+ moveFocusToEnd,
252
+ moveFocusToRowStart,
253
+ moveFocusToRowEnd,
254
+ setFocusedCell,
255
+ pageUp: pageUpAction,
256
+ pageDown: pageDownAction,
257
+ handleKeyDown,
258
+ };
259
+ const getRowPositionalIndex = (rowId) => {
260
+ const idx = rowIdsList.indexOf(rowId);
261
+ return idx >= 0 ? idx : -1;
262
+ };
263
+ const getColumnPositionalIndex = (columnId) => {
264
+ for (let i = 0; i < options.columns.length; i++) {
265
+ if (options.columns[i].id === columnId)
266
+ return i;
267
+ }
268
+ return -1;
269
+ };
270
+ const contracts = {
271
+ getTableProps() {
272
+ const props = {
273
+ id: `${idBase}-root`,
274
+ role: interactive ? 'grid' : 'table',
275
+ 'aria-label': options.ariaLabel,
276
+ 'aria-labelledby': options.ariaLabelledBy,
277
+ 'aria-rowcount': rowCountAtom(),
278
+ 'aria-colcount': columnCountAtom(),
279
+ };
280
+ if (selectable === 'multi') {
281
+ props['aria-multiselectable'] = 'true';
282
+ }
283
+ if (interactive) {
284
+ props.tabindex = '0';
285
+ }
286
+ return props;
287
+ },
288
+ getRowProps(rowId) {
289
+ if (!rowIds.has(rowId)) {
290
+ throw new Error(`Unknown table row id: ${rowId}`);
291
+ }
292
+ const props = {
293
+ id: `${idBase}-row-${rowId}`,
294
+ role: 'row',
295
+ 'aria-rowindex': rowIndexMap.get(rowId) ?? 1,
296
+ };
297
+ if (selectable !== false) {
298
+ props['aria-selected'] = selectedRowIdsAtom().has(rowId) ? 'true' : 'false';
299
+ }
300
+ return props;
301
+ },
302
+ getCellProps(rowId, columnId, span) {
303
+ if (!rowIds.has(rowId)) {
304
+ throw new Error(`Unknown table row id for cell: ${rowId}`);
305
+ }
306
+ if (!columnIds.has(columnId)) {
307
+ throw new Error(`Unknown table column id for cell: ${columnId}`);
308
+ }
309
+ const props = {
310
+ id: `${idBase}-cell-${rowId}-${columnId}`,
311
+ role: interactive ? 'gridcell' : 'cell',
312
+ 'aria-colindex': columnIndexMap.get(columnId) ?? 1,
313
+ 'aria-colspan': span?.colspan,
314
+ 'aria-rowspan': span?.rowspan,
315
+ };
316
+ if (interactive) {
317
+ const rowPos = getRowPositionalIndex(rowId);
318
+ const colPos = getColumnPositionalIndex(columnId);
319
+ const focusedRow = focusedRowIndexAtom();
320
+ const focusedCol = focusedColumnIndexAtom();
321
+ const isFocused = rowPos === focusedRow && colPos === focusedCol;
322
+ props.tabindex = isFocused ? '0' : '-1';
323
+ props['data-active'] = isFocused ? 'true' : 'false';
324
+ }
325
+ return props;
326
+ },
327
+ getColumnHeaderProps(columnId) {
328
+ if (!columnIds.has(columnId)) {
329
+ throw new Error(`Unknown table column id for header: ${columnId}`);
330
+ }
331
+ const isSorted = sortColumnIdAtom() === columnId;
332
+ const sortDirection = sortDirectionAtom();
333
+ const props = {
334
+ id: `${idBase}-column-header-${columnId}`,
335
+ role: 'columnheader',
336
+ 'aria-colindex': columnIndexMap.get(columnId) ?? 1,
337
+ 'aria-sort': isSorted ? sortDirection : 'none',
338
+ };
339
+ if (interactive) {
340
+ const colPos = getColumnPositionalIndex(columnId);
341
+ const focusedCol = focusedColumnIndexAtom();
342
+ props.tabindex = focusedCol === colPos && focusedRowIndexAtom() === null ? '0' : '-1';
343
+ }
344
+ return props;
345
+ },
346
+ getRowHeaderProps(rowId, columnId) {
347
+ if (!rowIds.has(rowId)) {
348
+ throw new Error(`Unknown table row id for row header: ${rowId}`);
349
+ }
350
+ if (!columnIds.has(columnId)) {
351
+ throw new Error(`Unknown table column id for row header: ${columnId}`);
352
+ }
353
+ return {
354
+ id: `${idBase}-row-header-${rowId}-${columnId}`,
355
+ role: 'rowheader',
356
+ 'aria-rowindex': rowIndexMap.get(rowId) ?? 1,
357
+ 'aria-colindex': columnIndexMap.get(columnId) ?? 1,
358
+ };
359
+ },
360
+ };
361
+ const state = {
362
+ rowCount: rowCountAtom,
363
+ columnCount: columnCountAtom,
364
+ sortColumnId: sortColumnIdAtom,
365
+ sortDirection: sortDirectionAtom,
366
+ selectedRowIds: selectedRowIdsAtom,
367
+ focusedRowIndex: focusedRowIndexAtom,
368
+ focusedColumnIndex: focusedColumnIndexAtom,
369
+ selectable,
370
+ interactive,
371
+ };
372
+ return {
373
+ state,
374
+ actions,
375
+ contracts,
376
+ };
377
+ }
@@ -0,0 +1,63 @@
1
+ import { type Atom } from '@reatom/core';
2
+ export type TabsOrientation = 'horizontal' | 'vertical';
3
+ export type TabsActivationMode = 'automatic' | 'manual';
4
+ export interface TabItem {
5
+ id: string;
6
+ disabled?: boolean;
7
+ }
8
+ export interface CreateTabsOptions {
9
+ tabs: readonly TabItem[];
10
+ idBase?: string;
11
+ ariaLabel?: string;
12
+ orientation?: TabsOrientation;
13
+ activationMode?: TabsActivationMode;
14
+ initialActiveTabId?: string | null;
15
+ initialSelectedTabId?: string | null;
16
+ }
17
+ export interface TabsState {
18
+ activeTabId: Atom<string | null>;
19
+ selectedTabId: Atom<string | null>;
20
+ }
21
+ export interface TabListProps {
22
+ id: string;
23
+ role: 'tablist';
24
+ 'aria-orientation': TabsOrientation;
25
+ 'aria-label'?: string;
26
+ }
27
+ export interface TabProps {
28
+ id: string;
29
+ role: 'tab';
30
+ tabindex: '0' | '-1';
31
+ 'aria-selected': 'true' | 'false';
32
+ 'aria-controls': string;
33
+ 'aria-disabled'?: 'true';
34
+ 'data-active': 'true' | 'false';
35
+ 'data-selected': 'true' | 'false';
36
+ }
37
+ export interface TabPanelProps {
38
+ id: string;
39
+ role: 'tabpanel';
40
+ tabindex: '0' | '-1';
41
+ 'aria-labelledby': string;
42
+ hidden: boolean;
43
+ }
44
+ export interface TabsActions {
45
+ setActive(id: string | null): void;
46
+ select(id: string): void;
47
+ moveNext(): void;
48
+ movePrev(): void;
49
+ moveFirst(): void;
50
+ moveLast(): void;
51
+ handleKeyDown(event: Pick<KeyboardEvent, 'key' | 'shiftKey' | 'ctrlKey' | 'metaKey' | 'altKey'>): void;
52
+ }
53
+ export interface TabsContracts {
54
+ getTabListProps(): TabListProps;
55
+ getTabProps(id: string): TabProps;
56
+ getPanelProps(id: string): TabPanelProps;
57
+ }
58
+ export interface TabsModel {
59
+ readonly state: TabsState;
60
+ readonly actions: TabsActions;
61
+ readonly contracts: TabsContracts;
62
+ }
63
+ export declare function createTabs(options: CreateTabsOptions): TabsModel;
@@ -0,0 +1,174 @@
1
+ import { action, atom } from '@reatom/core';
2
+ import { mapListboxKeyboardIntent } from '../interactions/keyboard-intents.js';
3
+ export function createTabs(options) {
4
+ const idBase = options.idBase ?? 'tabs';
5
+ const orientation = options.orientation ?? 'horizontal';
6
+ const activationMode = options.activationMode ?? 'automatic';
7
+ const tabById = new Map(options.tabs.map((tab) => [tab.id, tab]));
8
+ const enabledTabIds = options.tabs.filter((tab) => !tab.disabled).map((tab) => tab.id);
9
+ const resolveInitial = (candidate) => {
10
+ if (candidate != null && enabledTabIds.includes(candidate)) {
11
+ return candidate;
12
+ }
13
+ return enabledTabIds[0] ?? null;
14
+ };
15
+ const initialSelected = resolveInitial(options.initialSelectedTabId);
16
+ const initialActive = resolveInitial(options.initialActiveTabId ?? initialSelected);
17
+ const activeTabIdAtom = atom(initialActive, `${idBase}.activeTabId`);
18
+ const selectedTabIdAtom = atom(initialSelected, `${idBase}.selectedTabId`);
19
+ if (selectedTabIdAtom() == null && activeTabIdAtom() != null) {
20
+ selectedTabIdAtom.set(activeTabIdAtom());
21
+ }
22
+ const tabListId = `${idBase}-tablist`;
23
+ const tabDomId = (id) => `${idBase}-tab-${id}`;
24
+ const panelDomId = (id) => `${idBase}-panel-${id}`;
25
+ const applyAutoActivation = () => {
26
+ if (activationMode === 'automatic' && activeTabIdAtom() != null) {
27
+ selectedTabIdAtom.set(activeTabIdAtom());
28
+ }
29
+ };
30
+ const move = (direction) => {
31
+ if (enabledTabIds.length === 0) {
32
+ activeTabIdAtom.set(null);
33
+ return;
34
+ }
35
+ const activeTabId = activeTabIdAtom();
36
+ if (activeTabId == null || !enabledTabIds.includes(activeTabId)) {
37
+ activeTabIdAtom.set(enabledTabIds[0] ?? null);
38
+ applyAutoActivation();
39
+ return;
40
+ }
41
+ const currentIndex = enabledTabIds.indexOf(activeTabId);
42
+ const nextIndex = (currentIndex + direction + enabledTabIds.length) % enabledTabIds.length;
43
+ activeTabIdAtom.set(enabledTabIds[nextIndex] ?? null);
44
+ applyAutoActivation();
45
+ };
46
+ const setActive = action((id) => {
47
+ if (id == null) {
48
+ activeTabIdAtom.set(null);
49
+ return;
50
+ }
51
+ if (!enabledTabIds.includes(id))
52
+ return;
53
+ activeTabIdAtom.set(id);
54
+ applyAutoActivation();
55
+ }, `${idBase}.setActive`);
56
+ const select = action((id) => {
57
+ if (!enabledTabIds.includes(id))
58
+ return;
59
+ activeTabIdAtom.set(id);
60
+ selectedTabIdAtom.set(id);
61
+ }, `${idBase}.select`);
62
+ const moveNext = action(() => {
63
+ move(1);
64
+ }, `${idBase}.moveNext`);
65
+ const movePrev = action(() => {
66
+ move(-1);
67
+ }, `${idBase}.movePrev`);
68
+ const moveFirst = action(() => {
69
+ activeTabIdAtom.set(enabledTabIds[0] ?? null);
70
+ applyAutoActivation();
71
+ }, `${idBase}.moveFirst`);
72
+ const moveLast = action(() => {
73
+ activeTabIdAtom.set(enabledTabIds[enabledTabIds.length - 1] ?? null);
74
+ applyAutoActivation();
75
+ }, `${idBase}.moveLast`);
76
+ const handleKeyDown = action((event) => {
77
+ const intent = mapListboxKeyboardIntent(event, {
78
+ orientation,
79
+ selectionMode: 'single',
80
+ rangeSelectionEnabled: false,
81
+ });
82
+ if (intent == null)
83
+ return;
84
+ switch (intent) {
85
+ case 'NAV_NEXT':
86
+ moveNext();
87
+ return;
88
+ case 'NAV_PREV':
89
+ movePrev();
90
+ return;
91
+ case 'NAV_FIRST':
92
+ moveFirst();
93
+ return;
94
+ case 'NAV_LAST':
95
+ moveLast();
96
+ return;
97
+ case 'ACTIVATE':
98
+ case 'TOGGLE_SELECTION': {
99
+ const activeTabId = activeTabIdAtom();
100
+ if (activeTabId != null) {
101
+ select(activeTabId);
102
+ }
103
+ return;
104
+ }
105
+ default:
106
+ return;
107
+ }
108
+ }, `${idBase}.handleKeyDown`);
109
+ const actions = {
110
+ setActive,
111
+ select,
112
+ moveNext,
113
+ movePrev,
114
+ moveFirst,
115
+ moveLast,
116
+ handleKeyDown,
117
+ };
118
+ const contracts = {
119
+ getTabListProps() {
120
+ const props = {
121
+ id: tabListId,
122
+ role: 'tablist',
123
+ 'aria-orientation': orientation,
124
+ };
125
+ if (options.ariaLabel != null) {
126
+ props['aria-label'] = options.ariaLabel;
127
+ }
128
+ return props;
129
+ },
130
+ getTabProps(id) {
131
+ const tab = tabById.get(id);
132
+ if (!tab) {
133
+ throw new Error(`Unknown tab id: ${id}`);
134
+ }
135
+ const isActive = activeTabIdAtom() === id;
136
+ const isSelected = selectedTabIdAtom() === id;
137
+ const props = {
138
+ id: tabDomId(id),
139
+ role: 'tab',
140
+ tabindex: isActive ? '0' : '-1',
141
+ 'aria-selected': isSelected ? 'true' : 'false',
142
+ 'aria-controls': panelDomId(id),
143
+ 'data-active': isActive ? 'true' : 'false',
144
+ 'data-selected': isSelected ? 'true' : 'false',
145
+ };
146
+ if (tab.disabled) {
147
+ props['aria-disabled'] = 'true';
148
+ }
149
+ return props;
150
+ },
151
+ getPanelProps(id) {
152
+ if (!tabById.has(id)) {
153
+ throw new Error(`Unknown tab id for panel: ${id}`);
154
+ }
155
+ const isSelected = selectedTabIdAtom() === id;
156
+ return {
157
+ id: panelDomId(id),
158
+ role: 'tabpanel',
159
+ tabindex: isSelected ? '0' : '-1',
160
+ 'aria-labelledby': tabDomId(id),
161
+ hidden: !isSelected,
162
+ };
163
+ },
164
+ };
165
+ const state = {
166
+ activeTabId: activeTabIdAtom,
167
+ selectedTabId: selectedTabIdAtom,
168
+ };
169
+ return {
170
+ state,
171
+ actions,
172
+ contracts,
173
+ };
174
+ }
@@ -0,0 +1,68 @@
1
+ import { type Atom } from '@reatom/core';
2
+ export type TextareaResize = 'none' | 'vertical';
3
+ export interface CreateTextareaOptions {
4
+ idBase?: string;
5
+ value?: string;
6
+ disabled?: boolean;
7
+ readonly?: boolean;
8
+ required?: boolean;
9
+ placeholder?: string;
10
+ rows?: number;
11
+ cols?: number;
12
+ minLength?: number;
13
+ maxLength?: number;
14
+ resize?: TextareaResize;
15
+ onInput?: (value: string) => void;
16
+ }
17
+ export interface TextareaState {
18
+ value: Atom<string>;
19
+ disabled: Atom<boolean>;
20
+ readonly: Atom<boolean>;
21
+ required: Atom<boolean>;
22
+ placeholder: Atom<string>;
23
+ rows: Atom<number>;
24
+ cols: Atom<number>;
25
+ minLength: Atom<number | undefined>;
26
+ maxLength: Atom<number | undefined>;
27
+ resize: Atom<TextareaResize>;
28
+ focused: Atom<boolean>;
29
+ filled(): boolean;
30
+ }
31
+ export interface TextareaActions {
32
+ setValue(value: string): void;
33
+ setDisabled(disabled: boolean): void;
34
+ setReadonly(readonly: boolean): void;
35
+ setRequired(required: boolean): void;
36
+ setPlaceholder(placeholder: string): void;
37
+ setRows(rows: number | undefined): void;
38
+ setCols(cols: number | undefined): void;
39
+ setMinLength(minLength: number | undefined): void;
40
+ setMaxLength(maxLength: number | undefined): void;
41
+ setResize(resize: TextareaResize): void;
42
+ setFocused(focused: boolean): void;
43
+ handleInput(value: string): void;
44
+ }
45
+ export interface TextareaProps {
46
+ id: string;
47
+ 'aria-disabled'?: 'true';
48
+ 'aria-readonly'?: 'true';
49
+ 'aria-required'?: 'true';
50
+ placeholder?: string;
51
+ disabled?: boolean;
52
+ readonly?: boolean;
53
+ required?: boolean;
54
+ tabindex: '0' | '-1';
55
+ rows: number;
56
+ cols: number;
57
+ minlength?: number;
58
+ maxlength?: number;
59
+ }
60
+ export interface TextareaContracts {
61
+ getTextareaProps(): TextareaProps;
62
+ }
63
+ export interface TextareaModel {
64
+ readonly state: TextareaState;
65
+ readonly actions: TextareaActions;
66
+ readonly contracts: TextareaContracts;
67
+ }
68
+ export declare function createTextarea(options?: CreateTextareaOptions): TextareaModel;