@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.
- package/LICENSE +21 -0
- package/README.md +99 -0
- package/dist/a11y-contracts/index.d.ts +23 -0
- package/dist/a11y-contracts/index.js +1 -0
- package/dist/accordion/index.d.ts +78 -0
- package/dist/accordion/index.js +264 -0
- package/dist/adapters/index.d.ts +9 -0
- package/dist/adapters/index.js +1 -0
- package/dist/alert/index.d.ts +33 -0
- package/dist/alert/index.js +54 -0
- package/dist/alert-dialog/index.d.ts +69 -0
- package/dist/alert-dialog/index.js +94 -0
- package/dist/badge/index.d.ts +48 -0
- package/dist/badge/index.js +89 -0
- package/dist/breadcrumb/index.d.ts +55 -0
- package/dist/breadcrumb/index.js +77 -0
- package/dist/button/index.d.ts +46 -0
- package/dist/button/index.js +86 -0
- package/dist/callout/index.d.ts +41 -0
- package/dist/callout/index.js +63 -0
- package/dist/card/index.d.ts +54 -0
- package/dist/card/index.js +103 -0
- package/dist/carousel/index.d.ts +98 -0
- package/dist/carousel/index.js +243 -0
- package/dist/checkbox/index.d.ts +50 -0
- package/dist/checkbox/index.js +87 -0
- package/dist/combobox/index.d.ts +114 -0
- package/dist/combobox/index.js +431 -0
- package/dist/command-palette/index.d.ts +73 -0
- package/dist/command-palette/index.js +147 -0
- package/dist/context-menu/index.d.ts +111 -0
- package/dist/context-menu/index.js +372 -0
- package/dist/copy-button/index.d.ts +62 -0
- package/dist/copy-button/index.js +183 -0
- package/dist/core/index.d.ts +20 -0
- package/dist/core/index.js +2 -0
- package/dist/core/selection.d.ts +5 -0
- package/dist/core/selection.js +39 -0
- package/dist/core/value-range.d.ts +49 -0
- package/dist/core/value-range.js +134 -0
- package/dist/date-picker/index.d.ts +210 -0
- package/dist/date-picker/index.js +895 -0
- package/dist/dialog/index.d.ts +95 -0
- package/dist/dialog/index.js +153 -0
- package/dist/disclosure/index.d.ts +52 -0
- package/dist/disclosure/index.js +159 -0
- package/dist/drawer/index.d.ts +30 -0
- package/dist/drawer/index.js +39 -0
- package/dist/feed/index.d.ts +77 -0
- package/dist/feed/index.js +260 -0
- package/dist/grid/index.d.ts +103 -0
- package/dist/grid/index.js +415 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +51 -0
- package/dist/input/index.d.ts +86 -0
- package/dist/input/index.js +156 -0
- package/dist/interactions/composite-navigation.d.ts +69 -0
- package/dist/interactions/composite-navigation.js +169 -0
- package/dist/interactions/index.d.ts +15 -0
- package/dist/interactions/index.js +4 -0
- package/dist/interactions/keyboard-intents.d.ts +16 -0
- package/dist/interactions/keyboard-intents.js +33 -0
- package/dist/interactions/overlay-focus.d.ts +40 -0
- package/dist/interactions/overlay-focus.js +93 -0
- package/dist/interactions/typeahead.d.ts +20 -0
- package/dist/interactions/typeahead.js +41 -0
- package/dist/landmarks/index.d.ts +39 -0
- package/dist/landmarks/index.js +58 -0
- package/dist/link/index.d.ts +34 -0
- package/dist/link/index.js +39 -0
- package/dist/listbox/index.d.ts +92 -0
- package/dist/listbox/index.js +337 -0
- package/dist/menu/index.d.ts +132 -0
- package/dist/menu/index.js +541 -0
- package/dist/menu-button/index.d.ts +71 -0
- package/dist/menu-button/index.js +121 -0
- package/dist/meter/index.d.ts +45 -0
- package/dist/meter/index.js +106 -0
- package/dist/number/index.d.ts +113 -0
- package/dist/number/index.js +252 -0
- package/dist/popover/index.d.ts +70 -0
- package/dist/popover/index.js +126 -0
- package/dist/progress/index.d.ts +49 -0
- package/dist/progress/index.js +79 -0
- package/dist/radio-group/index.d.ts +61 -0
- package/dist/radio-group/index.js +150 -0
- package/dist/select/index.d.ts +92 -0
- package/dist/select/index.js +239 -0
- package/dist/sidebar/index.d.ts +74 -0
- package/dist/sidebar/index.js +186 -0
- package/dist/slider/index.d.ts +61 -0
- package/dist/slider/index.js +150 -0
- package/dist/slider-multi-thumb/index.d.ts +70 -0
- package/dist/slider-multi-thumb/index.js +222 -0
- package/dist/spinbutton/index.d.ts +75 -0
- package/dist/spinbutton/index.js +214 -0
- package/dist/spinner/index.d.ts +1 -0
- package/dist/spinner/index.js +1 -0
- package/dist/spinner/spinner.d.ts +23 -0
- package/dist/spinner/spinner.js +25 -0
- package/dist/switch/index.d.ts +40 -0
- package/dist/switch/index.js +61 -0
- package/dist/table/index.d.ts +117 -0
- package/dist/table/index.js +377 -0
- package/dist/tabs/index.d.ts +63 -0
- package/dist/tabs/index.js +174 -0
- package/dist/textarea/index.d.ts +68 -0
- package/dist/textarea/index.js +137 -0
- package/dist/toast/index.d.ts +67 -0
- package/dist/toast/index.js +145 -0
- package/dist/toolbar/index.d.ts +59 -0
- package/dist/toolbar/index.js +139 -0
- package/dist/tooltip/index.d.ts +52 -0
- package/dist/tooltip/index.js +169 -0
- package/dist/treegrid/index.d.ts +101 -0
- package/dist/treegrid/index.js +463 -0
- package/dist/treeview/index.d.ts +68 -0
- package/dist/treeview/index.js +370 -0
- package/dist/window-splitter/index.d.ts +65 -0
- package/dist/window-splitter/index.js +204 -0
- package/package.json +92 -0
- package/specs/ADR-001-headless-architecture.md +461 -0
- package/specs/ADR-002-repo-release-model.md +108 -0
- package/specs/ADR-003-public-api-versioning.md +136 -0
- package/specs/ADR-004-focus-selection-policy.md +117 -0
- package/specs/IMPLEMENTATION-ROADMAP.md +237 -0
- package/specs/ISSUE-BACKLOG.md +681 -0
- package/specs/RELEASE-CANDIDATE.md +30 -0
- package/specs/components/accordion.md +130 -0
- package/specs/components/alert-dialog.md +72 -0
- package/specs/components/alert.md +65 -0
- package/specs/components/badge.md +220 -0
- package/specs/components/breadcrumb.md +74 -0
- package/specs/components/button.md +115 -0
- package/specs/components/callout.md +195 -0
- package/specs/components/card.md +280 -0
- package/specs/components/carousel.md +140 -0
- package/specs/components/checkbox.md +172 -0
- package/specs/components/combobox.md +423 -0
- package/specs/components/command-palette.md +92 -0
- package/specs/components/context-menu.md +556 -0
- package/specs/components/copy-button.md +293 -0
- package/specs/components/date-picker.md +400 -0
- package/specs/components/dialog.md +298 -0
- package/specs/components/disclosure.md +257 -0
- package/specs/components/drawer.md +353 -0
- package/specs/components/feed.md +265 -0
- package/specs/components/grid.md +186 -0
- package/specs/components/input.md +254 -0
- package/specs/components/landmarks.md +136 -0
- package/specs/components/link.md +134 -0
- package/specs/components/listbox.md +351 -0
- package/specs/components/menu-button.md +76 -0
- package/specs/components/menu.md +623 -0
- package/specs/components/meter.md +149 -0
- package/specs/components/number.md +393 -0
- package/specs/components/popover.md +252 -0
- package/specs/components/progress.md +188 -0
- package/specs/components/radio-group.md +151 -0
- package/specs/components/select.md +144 -0
- package/specs/components/sidebar.md +321 -0
- package/specs/components/slider-multi-thumb.md +78 -0
- package/specs/components/slider.md +84 -0
- package/specs/components/spinbutton.md +140 -0
- package/specs/components/spinner.md +132 -0
- package/specs/components/switch.md +175 -0
- package/specs/components/table.md +403 -0
- package/specs/components/tabs.md +265 -0
- package/specs/components/textarea.md +185 -0
- package/specs/components/toast.md +198 -0
- package/specs/components/toolbar.md +278 -0
- package/specs/components/tooltip.md +252 -0
- package/specs/components/treegrid.md +281 -0
- package/specs/components/treeview.md +91 -0
- package/specs/components/window-splitter.md +297 -0
- package/specs/ops/git-shard-sync.md +107 -0
- package/specs/ops/release-checklist.md +76 -0
- package/specs/release/GAP-TO-GREEN-ISSUES.md +88 -0
- package/specs/release/api-freeze-candidate.md +54 -0
- package/specs/release/changelog-automation.md +76 -0
- package/specs/release/changelog.generated.md +53 -0
- package/specs/release/changelog.patch.generated.md +46 -0
- package/specs/release/consumer-integration.md +53 -0
- package/specs/release/migration-notes-pre-v1.md +40 -0
- package/specs/release/mvp-changelog.md +57 -0
- package/specs/release/release-notes-template.md +61 -0
- package/specs/release/release-rehearsal.md +113 -0
- package/specs/release/semver-deprecation-dry-run.md +89 -0
- package/specs/release/shard-release-drill-report.md +50 -0
- package/specs/release/shard-release-follow-ups.md +31 -0
- package/specs/signals.md +208 -0
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
# Table Component Contract
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
`Table` provides a headless APG-aligned model for tabular data. In its default (non-interactive) mode it is a structural container navigated by screen reader reading commands. When `interactive` mode is enabled, the root role switches to `grid` and full APG keyboard cell navigation is activated. Optional row selection support (single or multi) is available in both modes.
|
|
6
|
+
|
|
7
|
+
## Component Files
|
|
8
|
+
|
|
9
|
+
- `src/table/index.ts` - model and public `createTable` API
|
|
10
|
+
- `src/table/table.test.ts` - unit behavior tests
|
|
11
|
+
|
|
12
|
+
## Public API
|
|
13
|
+
|
|
14
|
+
### `createTable(options: CreateTableOptions): TableModel`
|
|
15
|
+
|
|
16
|
+
### CreateTableOptions
|
|
17
|
+
|
|
18
|
+
| Option | Type | Default | Description |
|
|
19
|
+
| --------------------------- | ------------------------------ | ---------------- | -------------------------------------------------------------------- |
|
|
20
|
+
| `columns` | `readonly TableColumn[]` | required | Column definitions (`{ id, index? }`) |
|
|
21
|
+
| `rows` | `readonly TableRow[]` | required | Row definitions (`{ id, index? }`) |
|
|
22
|
+
| `totalColumnCount` | `number` | `columns.length` | Logical column count (for virtualization) |
|
|
23
|
+
| `totalRowCount` | `number` | `rows.length` | Logical row count (for virtualization) |
|
|
24
|
+
| `initialSortColumnId` | `string \| null` | `null` | Initial sort column |
|
|
25
|
+
| `initialSortDirection` | `TableSortDirection` | `'none'` | Initial sort direction |
|
|
26
|
+
| `ariaLabel` | `string` | -- | Static `aria-label` for the table root |
|
|
27
|
+
| `ariaLabelledBy` | `string` | -- | `aria-labelledby` reference for the table root |
|
|
28
|
+
| `idBase` | `string` | `'table'` | Prefix for generated DOM ids |
|
|
29
|
+
| `selectable` | `'single' \| 'multi' \| false` | `false` | Row selection mode |
|
|
30
|
+
| `initialSelectedRowIds` | `readonly string[]` | `[]` | Initial selected row ids (filtered for validity on create) |
|
|
31
|
+
| `interactive` | `boolean` | `false` | Enable grid navigation mode |
|
|
32
|
+
| `initialFocusedRowIndex` | `number \| null` | `null` | Initial focused row index (interactive mode only) |
|
|
33
|
+
| `initialFocusedColumnIndex` | `number \| null` | `null` | Initial focused column index (interactive mode only) |
|
|
34
|
+
| `pageSize` | `number` | `10` | Rows per page for PageUp/PageDown (interactive mode only, minimum 1) |
|
|
35
|
+
|
|
36
|
+
### State (signal-backed)
|
|
37
|
+
|
|
38
|
+
| Signal | Type | Description |
|
|
39
|
+
| -------------------- | -------------------------- | ------------------------------------------------------------------------------- |
|
|
40
|
+
| `rowCount` | `Computed<number>` | `max(totalRowCount, rows.length)` |
|
|
41
|
+
| `columnCount` | `Computed<number>` | `max(totalColumnCount, columns.length)` |
|
|
42
|
+
| `sortColumnId` | `Atom<string \| null>` | Currently sorted column id |
|
|
43
|
+
| `sortDirection` | `Atom<TableSortDirection>` | `'ascending' \| 'descending' \| 'none'` |
|
|
44
|
+
| `selectedRowIds` | `Atom<Set<string>>` | Set of selected row ids (empty when `selectable` is `false`) |
|
|
45
|
+
| `focusedRowIndex` | `Atom<number \| null>` | Currently focused row index (null when `interactive` is `false` or no focus) |
|
|
46
|
+
| `focusedColumnIndex` | `Atom<number \| null>` | Currently focused column index (null when `interactive` is `false` or no focus) |
|
|
47
|
+
|
|
48
|
+
Static config values exposed on state for adapter convenience:
|
|
49
|
+
|
|
50
|
+
| Property | Type | Description |
|
|
51
|
+
| ------------- | ------------------------------ | ----------------------------------------------- |
|
|
52
|
+
| `selectable` | `'single' \| 'multi' \| false` | Current selection mode (from config) |
|
|
53
|
+
| `interactive` | `boolean` | Whether grid navigation is active (from config) |
|
|
54
|
+
|
|
55
|
+
### Actions
|
|
56
|
+
|
|
57
|
+
#### Sorting
|
|
58
|
+
|
|
59
|
+
| Action | Signature | Description |
|
|
60
|
+
| ----------- | ----------------------------------------------------------- | ----------------------------- |
|
|
61
|
+
| `sortBy` | `(columnId: string, direction: TableSortDirection) => void` | Set sort column and direction |
|
|
62
|
+
| `clearSort` | `() => void` | Reset sort to `none` |
|
|
63
|
+
|
|
64
|
+
#### Selection
|
|
65
|
+
|
|
66
|
+
| Action | Signature | Description |
|
|
67
|
+
| -------------------- | ------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
|
68
|
+
| `selectRow` | `(rowId: string) => void` | Select a row. In single mode, clears other selections first. No-op when `selectable` is `false` or `rowId` is unknown. |
|
|
69
|
+
| `deselectRow` | `(rowId: string) => void` | Deselect a row. No-op when `selectable` is `false` or `rowId` is not selected. |
|
|
70
|
+
| `toggleRowSelection` | `(rowId: string) => void` | Toggle selection state for a row. In single mode, toggling on clears other selections. No-op when `selectable` is `false`. |
|
|
71
|
+
| `selectAllRows` | `() => void` | Select all rows. Only works in `multi` mode. No-op when `selectable` is not `multi`. |
|
|
72
|
+
| `clearSelection` | `() => void` | Clear all row selections. No-op when `selectable` is `false`. |
|
|
73
|
+
|
|
74
|
+
#### Grid Navigation (interactive mode only)
|
|
75
|
+
|
|
76
|
+
All navigation actions are no-ops when `interactive` is `false`.
|
|
77
|
+
|
|
78
|
+
| Action | Signature | Description |
|
|
79
|
+
| --------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
|
|
80
|
+
| `moveFocus` | `(direction: 'up' \| 'down' \| 'left' \| 'right') => void` | Move focused cell one step in the given direction. No change at boundary. |
|
|
81
|
+
| `moveFocusToStart` | `() => void` | Move focus to first cell (row 0, column 0). Equivalent to Ctrl+Home. |
|
|
82
|
+
| `moveFocusToEnd` | `() => void` | Move focus to last cell (last row, last column). Equivalent to Ctrl+End. |
|
|
83
|
+
| `moveFocusToRowStart` | `() => void` | Move focus to first cell in the current row. Equivalent to Home. |
|
|
84
|
+
| `moveFocusToRowEnd` | `() => void` | Move focus to last cell in the current row. Equivalent to End. |
|
|
85
|
+
| `setFocusedCell` | `(rowIndex: number, columnIndex: number) => void` | Programmatically set the focused cell. Clamped to valid bounds. |
|
|
86
|
+
| `pageUp` | `() => void` | Move focus up by `pageSize` rows (clamped). |
|
|
87
|
+
| `pageDown` | `() => void` | Move focus down by `pageSize` rows (clamped). |
|
|
88
|
+
| `handleKeyDown` | `(event: TableKeyboardEventLike) => void` | Delegates keyboard events to navigation/selection actions. Only active in interactive mode. |
|
|
89
|
+
|
|
90
|
+
### Contracts (ready-to-spread ARIA prop objects)
|
|
91
|
+
|
|
92
|
+
| Contract | Return Type | Description |
|
|
93
|
+
| ----------------------------------- | ------------------------ | ----------------------------------------------- |
|
|
94
|
+
| `getTableProps()` | `TableProps` | ARIA attributes for the table/grid root element |
|
|
95
|
+
| `getRowProps(rowId)` | `TableRowProps` | ARIA attributes for a row element |
|
|
96
|
+
| `getCellProps(rowId, colId, span?)` | `TableCellProps` | ARIA attributes for a data cell |
|
|
97
|
+
| `getColumnHeaderProps(colId)` | `TableColumnHeaderProps` | ARIA attributes for a column header |
|
|
98
|
+
| `getRowHeaderProps(rowId, colId)` | `TableRowHeaderProps` | ARIA attributes for a row header cell |
|
|
99
|
+
|
|
100
|
+
#### `TableProps`
|
|
101
|
+
|
|
102
|
+
| Prop | Type | Notes |
|
|
103
|
+
| ---------------------- | --------------------- | --------------------------------------------------------------------------------- |
|
|
104
|
+
| `id` | `string` | `"{idBase}-root"` |
|
|
105
|
+
| `role` | `'table' \| 'grid'` | `'table'` when `interactive` is false, `'grid'` when true |
|
|
106
|
+
| `aria-label` | `string \| undefined` | From options |
|
|
107
|
+
| `aria-labelledby` | `string \| undefined` | From options |
|
|
108
|
+
| `aria-rowcount` | `number` | From `rowCount` computed |
|
|
109
|
+
| `aria-colcount` | `number` | From `columnCount` computed |
|
|
110
|
+
| `aria-multiselectable` | `'true' \| undefined` | Present only when `selectable` is `'multi'` |
|
|
111
|
+
| `tabindex` | `'0' \| undefined` | `'0'` when `interactive` is true (grid root receives focus), undefined when false |
|
|
112
|
+
|
|
113
|
+
#### `TableRowProps`
|
|
114
|
+
|
|
115
|
+
| Prop | Type | Notes |
|
|
116
|
+
| --------------- | -------------------------------- | ------------------------------------------------------------------------ |
|
|
117
|
+
| `id` | `string` | `"{idBase}-row-{rowId}"` |
|
|
118
|
+
| `role` | `'row'` | Always `'row'` regardless of mode |
|
|
119
|
+
| `aria-rowindex` | `number` | 1-based; uses `row.index` if provided, else positional index + 1 |
|
|
120
|
+
| `aria-selected` | `'true' \| 'false' \| undefined` | Present only when `selectable` is not `false`. Reflects selection state. |
|
|
121
|
+
|
|
122
|
+
#### `TableCellProps`
|
|
123
|
+
|
|
124
|
+
| Prop | Type | Notes |
|
|
125
|
+
| --------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------ |
|
|
126
|
+
| `id` | `string` | `"{idBase}-cell-{rowId}-{colId}"` |
|
|
127
|
+
| `role` | `'cell' \| 'gridcell'` | `'cell'` when `interactive` is false, `'gridcell'` when true |
|
|
128
|
+
| `aria-colindex` | `number` | 1-based column index |
|
|
129
|
+
| `aria-colspan` | `number \| undefined` | From span parameter |
|
|
130
|
+
| `aria-rowspan` | `number \| undefined` | From span parameter |
|
|
131
|
+
| `tabindex` | `'0' \| '-1' \| undefined` | Present only in interactive mode. `'0'` for the focused cell, `'-1'` for all others (roving tabindex). |
|
|
132
|
+
| `data-active` | `'true' \| 'false' \| undefined` | Present only in interactive mode. Indicates currently focused cell. |
|
|
133
|
+
|
|
134
|
+
#### `TableColumnHeaderProps`
|
|
135
|
+
|
|
136
|
+
| Prop | Type | Notes |
|
|
137
|
+
| --------------- | -------------------------- | ------------------------------------------------------------------------------------------- |
|
|
138
|
+
| `id` | `string` | `"{idBase}-column-header-{colId}"` |
|
|
139
|
+
| `role` | `'columnheader'` | Always `'columnheader'` |
|
|
140
|
+
| `aria-colindex` | `number` | 1-based column index |
|
|
141
|
+
| `aria-sort` | `TableSortDirection` | Sort state for this column |
|
|
142
|
+
| `tabindex` | `'0' \| '-1' \| undefined` | Present only in interactive mode. Roving tabindex (headers participate in grid navigation). |
|
|
143
|
+
|
|
144
|
+
#### `TableRowHeaderProps`
|
|
145
|
+
|
|
146
|
+
| Prop | Type | Notes |
|
|
147
|
+
| --------------- | ------------- | --------------------------------------- |
|
|
148
|
+
| `id` | `string` | `"{idBase}-row-header-{rowId}-{colId}"` |
|
|
149
|
+
| `role` | `'rowheader'` | Always `'rowheader'` |
|
|
150
|
+
| `aria-rowindex` | `number` | 1-based row index |
|
|
151
|
+
| `aria-colindex` | `number` | 1-based column index |
|
|
152
|
+
|
|
153
|
+
## APG and A11y Contract
|
|
154
|
+
|
|
155
|
+
### Non-interactive mode (default)
|
|
156
|
+
|
|
157
|
+
- root role: `table`
|
|
158
|
+
- row role: `row`
|
|
159
|
+
- cell role: `cell`
|
|
160
|
+
- header roles: `columnheader`, `rowheader`
|
|
161
|
+
- required attributes:
|
|
162
|
+
- root: `aria-label` or `aria-labelledby`, `aria-colcount`, `aria-rowcount`
|
|
163
|
+
- row: `aria-rowindex`, `aria-selected` (when selectable)
|
|
164
|
+
- cell: `aria-colindex`, `aria-colspan`, `aria-rowspan`
|
|
165
|
+
- header: `aria-sort` (if applicable)
|
|
166
|
+
- `Table` is a static structure; users navigate it using screen reader reading commands.
|
|
167
|
+
- If a cell contains interactive elements, those elements are part of the page's tab sequence.
|
|
168
|
+
|
|
169
|
+
### Interactive mode (`interactive: true`)
|
|
170
|
+
|
|
171
|
+
- root role: `grid`
|
|
172
|
+
- row role: `row`
|
|
173
|
+
- cell role: `gridcell`
|
|
174
|
+
- header roles: `columnheader`, `rowheader`
|
|
175
|
+
- focus management: roving tabindex on cells
|
|
176
|
+
- required attributes:
|
|
177
|
+
- root: `aria-label` or `aria-labelledby`, `aria-colcount`, `aria-rowcount`, `tabindex="0"`, `aria-multiselectable` (if multi-select)
|
|
178
|
+
- row: `aria-rowindex`, `aria-selected` (when selectable)
|
|
179
|
+
- cell: `aria-colindex`, `tabindex`, `data-active`
|
|
180
|
+
|
|
181
|
+
### Selection attributes (both modes)
|
|
182
|
+
|
|
183
|
+
- When `selectable` is not `false`:
|
|
184
|
+
- `aria-selected` is present on every `row` element (`'true'` or `'false'`)
|
|
185
|
+
- `aria-multiselectable="true"` is on root when `selectable` is `'multi'`
|
|
186
|
+
|
|
187
|
+
## Keyboard Contract (interactive mode only)
|
|
188
|
+
|
|
189
|
+
| Key | Modifier | Action |
|
|
190
|
+
| -------------- | ----------- | ---------------------------------------------------- |
|
|
191
|
+
| `ArrowUp` | -- | `moveFocus('up')` |
|
|
192
|
+
| `ArrowDown` | -- | `moveFocus('down')` |
|
|
193
|
+
| `ArrowLeft` | -- | `moveFocus('left')` |
|
|
194
|
+
| `ArrowRight` | -- | `moveFocus('right')` |
|
|
195
|
+
| `Home` | -- | `moveFocusToRowStart()` |
|
|
196
|
+
| `End` | -- | `moveFocusToRowEnd()` |
|
|
197
|
+
| `Home` | Ctrl / Meta | `moveFocusToStart()` |
|
|
198
|
+
| `End` | Ctrl / Meta | `moveFocusToEnd()` |
|
|
199
|
+
| `PageUp` | -- | `pageUp()` |
|
|
200
|
+
| `PageDown` | -- | `pageDown()` |
|
|
201
|
+
| `Space` | -- | `toggleRowSelection(focusedRowId)` (when selectable) |
|
|
202
|
+
| `Ctrl/Cmd + A` | -- | `selectAllRows()` (when selectable is multi) |
|
|
203
|
+
|
|
204
|
+
## Transitions Table
|
|
205
|
+
|
|
206
|
+
### Sorting Transitions
|
|
207
|
+
|
|
208
|
+
| Trigger | Preconditions | Next State |
|
|
209
|
+
| -------------------------- | ---------------------------------------- | -------------------------------------------- |
|
|
210
|
+
| `sortBy(columnId, dir)` | `columnId` exists, `dir` is not `'none'` | `sortColumnId=columnId`, `sortDirection=dir` |
|
|
211
|
+
| `sortBy(columnId, 'none')` | any | `sortColumnId=null`, `sortDirection='none'` |
|
|
212
|
+
| `clearSort()` | any | `sortColumnId=null`, `sortDirection='none'` |
|
|
213
|
+
|
|
214
|
+
### Selection Transitions
|
|
215
|
+
|
|
216
|
+
| Trigger | Preconditions | Next State |
|
|
217
|
+
| --------------------------- | ------------------------------------------- | ------------------------------------------ |
|
|
218
|
+
| `selectRow(rowId)` | `selectable='single'`, `rowId` is known | `selectedRowIds={rowId}` (clears previous) |
|
|
219
|
+
| `selectRow(rowId)` | `selectable='multi'`, `rowId` is known | `selectedRowIds` adds `rowId` |
|
|
220
|
+
| `deselectRow(rowId)` | `selectable` is not `false`, `rowId` in set | `selectedRowIds` removes `rowId` |
|
|
221
|
+
| `toggleRowSelection(rowId)` | `selectable='single'`, `rowId` not selected | `selectedRowIds={rowId}` |
|
|
222
|
+
| `toggleRowSelection(rowId)` | `selectable='single'`, `rowId` selected | `selectedRowIds={}` |
|
|
223
|
+
| `toggleRowSelection(rowId)` | `selectable='multi'`, `rowId` not selected | `selectedRowIds` adds `rowId` |
|
|
224
|
+
| `toggleRowSelection(rowId)` | `selectable='multi'`, `rowId` selected | `selectedRowIds` removes `rowId` |
|
|
225
|
+
| `selectAllRows()` | `selectable='multi'` | `selectedRowIds` = all known row ids |
|
|
226
|
+
| `selectAllRows()` | `selectable` is not `'multi'` | no-op |
|
|
227
|
+
| `clearSelection()` | `selectable` is not `false` | `selectedRowIds={}` |
|
|
228
|
+
| `clearSelection()` | `selectable=false` | no-op |
|
|
229
|
+
|
|
230
|
+
### Grid Navigation Transitions (interactive mode only)
|
|
231
|
+
|
|
232
|
+
| Trigger | Preconditions | Next State |
|
|
233
|
+
| ----------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------ |
|
|
234
|
+
| `moveFocus('up')` | `focusedRowIndex > 0` | `focusedRowIndex -= 1` |
|
|
235
|
+
| `moveFocus('up')` | `focusedRowIndex = 0` | no change |
|
|
236
|
+
| `moveFocus('down')` | `focusedRowIndex < rowCount - 1` | `focusedRowIndex += 1` |
|
|
237
|
+
| `moveFocus('down')` | `focusedRowIndex = rowCount - 1` | no change |
|
|
238
|
+
| `moveFocus('left')` | `focusedColumnIndex > 0` | `focusedColumnIndex -= 1` |
|
|
239
|
+
| `moveFocus('left')` | `focusedColumnIndex = 0` | no change |
|
|
240
|
+
| `moveFocus('right')` | `focusedColumnIndex < columnCount - 1` | `focusedColumnIndex += 1` |
|
|
241
|
+
| `moveFocus('right')` | `focusedColumnIndex = columnCount - 1` | no change |
|
|
242
|
+
| `moveFocusToStart()` | any | `focusedRowIndex=0`, `focusedColumnIndex=0` |
|
|
243
|
+
| `moveFocusToEnd()` | any | `focusedRowIndex=rowCount-1`, `focusedColumnIndex=columnCount-1` |
|
|
244
|
+
| `moveFocusToRowStart()` | any | `focusedColumnIndex=0` |
|
|
245
|
+
| `moveFocusToRowEnd()` | any | `focusedColumnIndex=columnCount-1` |
|
|
246
|
+
| `setFocusedCell(r, c)` | any | `focusedRowIndex=clamp(r, 0, rowCount-1)`, `focusedColumnIndex=clamp(c, 0, columnCount-1)` |
|
|
247
|
+
| `pageUp()` | any | `focusedRowIndex=max(0, focusedRowIndex - pageSize)` |
|
|
248
|
+
| `pageDown()` | any | `focusedRowIndex=min(rowCount-1, focusedRowIndex + pageSize)` |
|
|
249
|
+
|
|
250
|
+
### Keyboard to Action Mapping (interactive mode)
|
|
251
|
+
|
|
252
|
+
| Key Event | Mapped Action |
|
|
253
|
+
| ------------------------------------------- | ---------------------------------------------- |
|
|
254
|
+
| `ArrowUp` | `moveFocus('up')` |
|
|
255
|
+
| `ArrowDown` | `moveFocus('down')` |
|
|
256
|
+
| `ArrowLeft` | `moveFocus('left')` |
|
|
257
|
+
| `ArrowRight` | `moveFocus('right')` |
|
|
258
|
+
| `Home` | `moveFocusToRowStart()` |
|
|
259
|
+
| `End` | `moveFocusToRowEnd()` |
|
|
260
|
+
| `Ctrl+Home` / `Meta+Home` | `moveFocusToStart()` |
|
|
261
|
+
| `Ctrl+End` / `Meta+End` | `moveFocusToEnd()` |
|
|
262
|
+
| `PageUp` | `pageUp()` |
|
|
263
|
+
| `PageDown` | `pageDown()` |
|
|
264
|
+
| `Space` (when selectable) | `toggleRowSelection(rows[focusedRowIndex].id)` |
|
|
265
|
+
| `Ctrl+A` / `Meta+A` (when selectable=multi) | `selectAllRows()` |
|
|
266
|
+
|
|
267
|
+
## Invariants
|
|
268
|
+
|
|
269
|
+
### General
|
|
270
|
+
|
|
271
|
+
- `aria-rowcount` and `aria-colcount` must reflect the total number of rows and columns in the data set, even if only a subset is rendered.
|
|
272
|
+
- `aria-rowindex` and `aria-colindex` must be 1-based and reflect the position in the total data set.
|
|
273
|
+
- `getRowProps`, `getCellProps`, `getColumnHeaderProps`, `getRowHeaderProps` throw `Error` for unknown row/column ids.
|
|
274
|
+
|
|
275
|
+
### Selection
|
|
276
|
+
|
|
277
|
+
- When `selectable` is `false`, `selectedRowIds` is always empty and selection actions are no-ops.
|
|
278
|
+
- In `single` mode, `selectedRowIds` contains at most one id.
|
|
279
|
+
- `selectRow` in `single` mode clears all other selections before adding the new one.
|
|
280
|
+
- `selectAllRows` only has effect in `multi` mode.
|
|
281
|
+
- `aria-selected` is present on rows only when `selectable` is not `false`.
|
|
282
|
+
- `aria-multiselectable` is present on root only when `selectable` is `'multi'`.
|
|
283
|
+
- `initialSelectedRowIds` are filtered on create: unknown row ids are excluded. In single mode, only the first valid id is kept.
|
|
284
|
+
|
|
285
|
+
### Grid Navigation
|
|
286
|
+
|
|
287
|
+
- When `interactive` is `false`, `focusedRowIndex` and `focusedColumnIndex` are always `null` and navigation actions are no-ops.
|
|
288
|
+
- When `interactive` is `true`, exactly one cell has `tabindex="0"` (roving tabindex); all other cells have `tabindex="-1"`.
|
|
289
|
+
- `focusedRowIndex` and `focusedColumnIndex` stay within grid bounds: `[0, rowCount-1]` and `[0, columnCount-1]` respectively.
|
|
290
|
+
- When `interactive` is `true` and initial focus indices are not provided, focus defaults to `(0, 0)`.
|
|
291
|
+
- Role switches: root is `'grid'` (not `'table'`), data cells are `'gridcell'` (not `'cell'`).
|
|
292
|
+
- `handleKeyDown` is a no-op when `interactive` is `false`.
|
|
293
|
+
|
|
294
|
+
## Adapter Expectations
|
|
295
|
+
|
|
296
|
+
UIKit adapter will:
|
|
297
|
+
|
|
298
|
+
**Signals read (reactive, drive re-renders):**
|
|
299
|
+
|
|
300
|
+
- `state.rowCount()` -- total row count
|
|
301
|
+
- `state.columnCount()` -- total column count
|
|
302
|
+
- `state.sortColumnId()` -- currently sorted column id
|
|
303
|
+
- `state.sortDirection()` -- sort direction
|
|
304
|
+
- `state.selectedRowIds()` -- set of selected row ids
|
|
305
|
+
- `state.focusedRowIndex()` -- focused row index (interactive mode)
|
|
306
|
+
- `state.focusedColumnIndex()` -- focused column index (interactive mode)
|
|
307
|
+
- `state.selectable` -- selection mode (static, for conditional rendering)
|
|
308
|
+
- `state.interactive` -- interactive mode flag (static, for conditional rendering)
|
|
309
|
+
|
|
310
|
+
**Actions called (event handlers, never mutate state directly):**
|
|
311
|
+
|
|
312
|
+
- `actions.sortBy(columnId, direction)` -- column header click
|
|
313
|
+
- `actions.clearSort()` -- reset sort
|
|
314
|
+
- `actions.selectRow(rowId)` -- row click (single select)
|
|
315
|
+
- `actions.deselectRow(rowId)` -- row deselect
|
|
316
|
+
- `actions.toggleRowSelection(rowId)` -- row click (toggle)
|
|
317
|
+
- `actions.selectAllRows()` -- select-all checkbox/shortcut
|
|
318
|
+
- `actions.clearSelection()` -- clear all selections
|
|
319
|
+
- `actions.moveFocus(direction)` -- arrow key navigation (interactive)
|
|
320
|
+
- `actions.moveFocusToStart()` / `actions.moveFocusToEnd()` -- Ctrl+Home/End (interactive)
|
|
321
|
+
- `actions.moveFocusToRowStart()` / `actions.moveFocusToRowEnd()` -- Home/End (interactive)
|
|
322
|
+
- `actions.setFocusedCell(rowIndex, columnIndex)` -- programmatic focus / cell click (interactive)
|
|
323
|
+
- `actions.pageUp()` / `actions.pageDown()` -- PageUp/PageDown (interactive)
|
|
324
|
+
- `actions.handleKeyDown(event)` -- keyboard delegation (interactive)
|
|
325
|
+
|
|
326
|
+
**Contracts spread (attribute maps applied directly to DOM elements):**
|
|
327
|
+
|
|
328
|
+
- `contracts.getTableProps()` -- spread onto table/grid root element
|
|
329
|
+
- `contracts.getRowProps(rowId)` -- spread onto each row element
|
|
330
|
+
- `contracts.getCellProps(rowId, colId, span?)` -- spread onto each data cell
|
|
331
|
+
- `contracts.getColumnHeaderProps(colId)` -- spread onto each column header
|
|
332
|
+
- `contracts.getRowHeaderProps(rowId, colId)` -- spread onto row header cells
|
|
333
|
+
|
|
334
|
+
**UIKit-only concerns (NOT in headless):**
|
|
335
|
+
|
|
336
|
+
- Display variants (striped, compact, bordered) -- CSS-only
|
|
337
|
+
- Sticky header positioning -- CSS-only
|
|
338
|
+
- Visual selection indicators (checkbox column, row highlighting)
|
|
339
|
+
- DOM focus management (calling `.focus()` on cells when `focusedRowIndex`/`focusedColumnIndex` change)
|
|
340
|
+
- `preventDefault()` on keyboard events handled by `handleKeyDown`
|
|
341
|
+
|
|
342
|
+
## Minimum Test Matrix
|
|
343
|
+
|
|
344
|
+
### Structural (existing)
|
|
345
|
+
|
|
346
|
+
- correct structural ARIA roles (`table`, `row`, `cell`)
|
|
347
|
+
- 1-based index mapping for `aria-rowindex` and `aria-colindex`
|
|
348
|
+
- `aria-sort` state transitions when `sortBy` is called
|
|
349
|
+
- support for `colspan` and `rowspan` metadata in `getCellProps`
|
|
350
|
+
- virtualization support (correct total counts vs rendered counts)
|
|
351
|
+
|
|
352
|
+
### Selection
|
|
353
|
+
|
|
354
|
+
- `selectable=false`: no `aria-selected` on rows, selection actions are no-ops
|
|
355
|
+
- `selectable='single'`: `selectRow` replaces current selection
|
|
356
|
+
- `selectable='single'`: `selectedRowIds` contains at most one id
|
|
357
|
+
- `selectable='single'`: `toggleRowSelection` toggles single row on/off
|
|
358
|
+
- `selectable='single'`: `selectAllRows` is a no-op
|
|
359
|
+
- `selectable='multi'`: `selectRow` adds to selection
|
|
360
|
+
- `selectable='multi'`: `toggleRowSelection` adds/removes individual row
|
|
361
|
+
- `selectable='multi'`: `selectAllRows` selects all known rows
|
|
362
|
+
- `selectable='multi'`: `clearSelection` empties selection set
|
|
363
|
+
- `aria-multiselectable="true"` present only when `selectable='multi'`
|
|
364
|
+
- `aria-selected` present on every row when `selectable` is not `false`
|
|
365
|
+
- `initialSelectedRowIds` filtered for validity and mode constraints
|
|
366
|
+
|
|
367
|
+
### Grid Navigation (interactive mode)
|
|
368
|
+
|
|
369
|
+
- `interactive=false`: roles are `table`/`cell`, no `tabindex` on cells, navigation actions are no-ops
|
|
370
|
+
- `interactive=true`: root role is `grid`, cell role is `gridcell`
|
|
371
|
+
- `interactive=true`: exactly one cell has `tabindex="0"` (roving tabindex)
|
|
372
|
+
- arrow key navigation moves focus within bounds
|
|
373
|
+
- boundary clamping (no wrap, no change at edges)
|
|
374
|
+
- Home/End navigate to row start/end
|
|
375
|
+
- Ctrl+Home/End navigate to grid start/end
|
|
376
|
+
- PageUp/PageDown move by `pageSize` rows (clamped)
|
|
377
|
+
- `setFocusedCell` clamps to valid bounds
|
|
378
|
+
- `handleKeyDown` delegates to correct navigation actions
|
|
379
|
+
- Space key triggers `toggleRowSelection` when selectable
|
|
380
|
+
- Ctrl+A triggers `selectAllRows` when selectable is multi
|
|
381
|
+
- default focus is `(0, 0)` when interactive and no initial focus provided
|
|
382
|
+
|
|
383
|
+
### Combined Modes
|
|
384
|
+
|
|
385
|
+
- interactive + selectable: Space key selects focused row
|
|
386
|
+
- interactive + selectable='multi': Ctrl+A selects all rows
|
|
387
|
+
- non-interactive + selectable: selection works without grid navigation
|
|
388
|
+
|
|
389
|
+
## ADR-001 Compliance
|
|
390
|
+
|
|
391
|
+
- **Runtime Policy**: Reatom v1000 only; no @statx/\* in headless core.
|
|
392
|
+
- **Layering**: core -> interactions -> a11y-contracts -> adapters; adapters remain thin mappings.
|
|
393
|
+
- **Independence**: No imports from @project/_, apps/_, or other out-of-package modules.
|
|
394
|
+
- **Verification**: Mandatory adapter integration tests and standalone package test execution.
|
|
395
|
+
|
|
396
|
+
## Out of Scope (Current)
|
|
397
|
+
|
|
398
|
+
- cell-level selection (only row selection is supported; use `Grid` for cell selection)
|
|
399
|
+
- cell editing mode (inline inputs)
|
|
400
|
+
- column/row reordering (drag and drop)
|
|
401
|
+
- column resizing
|
|
402
|
+
- complex filtering logic (should be handled in the model/service layer)
|
|
403
|
+
- `aria-activedescendant` focus strategy (only roving tabindex is supported in interactive mode)
|