@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,281 @@
|
|
|
1
|
+
# Treegrid Component Contract
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
`Treegrid` provides a headless APG-aligned model for hierarchical tabular data, combining the multi-column structure of a `Grid` with the expansion/collapse behavior of a `Treeview`.
|
|
6
|
+
|
|
7
|
+
## Component Files
|
|
8
|
+
|
|
9
|
+
- `src/treegrid/index.ts` - model and public `createTreegrid` API
|
|
10
|
+
- `src/treegrid/treegrid.test.ts` - unit behavior tests
|
|
11
|
+
|
|
12
|
+
## Exported Types
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
type TreegridSelectionMode = 'single' | 'multiple'
|
|
16
|
+
type TreegridCellRole = 'gridcell' | 'rowheader' | 'columnheader'
|
|
17
|
+
|
|
18
|
+
interface TreegridRow {
|
|
19
|
+
id: string
|
|
20
|
+
index?: number
|
|
21
|
+
disabled?: boolean
|
|
22
|
+
children?: readonly TreegridRow[]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface TreegridColumn {
|
|
26
|
+
id: string
|
|
27
|
+
index?: number
|
|
28
|
+
disabled?: boolean
|
|
29
|
+
cellRole?: TreegridCellRole
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface TreegridCellId {
|
|
33
|
+
rowId: string
|
|
34
|
+
colId: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface TreegridKeyboardEventLike {
|
|
38
|
+
key: string
|
|
39
|
+
shiftKey?: boolean
|
|
40
|
+
ctrlKey?: boolean
|
|
41
|
+
metaKey?: boolean
|
|
42
|
+
altKey?: boolean
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface TreegridState {
|
|
46
|
+
activeCellId: Atom<TreegridCellId | null>
|
|
47
|
+
expandedRowIds: Atom<Set<string>>
|
|
48
|
+
selectedRowIds: Atom<Set<string>>
|
|
49
|
+
rowCount: Computed<number>
|
|
50
|
+
columnCount: Computed<number>
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface TreegridActions {
|
|
54
|
+
moveUp(): void
|
|
55
|
+
moveDown(): void
|
|
56
|
+
moveLeft(): void
|
|
57
|
+
moveRight(): void
|
|
58
|
+
moveRowStart(): void
|
|
59
|
+
moveRowEnd(): void
|
|
60
|
+
expandRow(rowId: string): void
|
|
61
|
+
collapseRow(rowId: string): void
|
|
62
|
+
toggleRowExpanded(rowId: string): void
|
|
63
|
+
selectRow(rowId: string): void
|
|
64
|
+
toggleRowSelection(rowId: string): void
|
|
65
|
+
handleKeyDown(event: TreegridKeyboardEventLike): void
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface TreegridContracts {
|
|
69
|
+
getTreegridProps(): TreegridProps
|
|
70
|
+
getRowProps(rowId: string): TreegridRowProps
|
|
71
|
+
getCellProps(rowId: string, colId: string): TreegridCellProps
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface TreegridModel {
|
|
75
|
+
readonly state: TreegridState
|
|
76
|
+
readonly actions: TreegridActions
|
|
77
|
+
readonly contracts: TreegridContracts
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Options (`CreateTreegridOptions`)
|
|
82
|
+
|
|
83
|
+
| Option | Type | Default | Description |
|
|
84
|
+
| ----------------------- | --------------------------- | ------------ | ---------------------------------------------------------- |
|
|
85
|
+
| `rows` | `readonly TreegridRow[]` | required | Hierarchical row definitions (children nested recursively) |
|
|
86
|
+
| `columns` | `readonly TreegridColumn[]` | required | Column definitions with optional role and disabled state |
|
|
87
|
+
| `disabledCells` | `readonly TreegridCellId[]` | `[]` | Individual cells disabled beyond row/column level |
|
|
88
|
+
| `idBase` | `string` | `'treegrid'` | Prefix for all generated DOM ids and atom names |
|
|
89
|
+
| `ariaLabel` | `string` | `undefined` | `aria-label` for the root treegrid element |
|
|
90
|
+
| `ariaLabelledBy` | `string` | `undefined` | `aria-labelledby` for the root treegrid element |
|
|
91
|
+
| `selectionMode` | `TreegridSelectionMode` | `'single'` | Single or multi-row selection |
|
|
92
|
+
| `initialExpandedRowIds` | `readonly string[]` | `[]` | Row ids that begin expanded; invalid/leaf ids are filtered |
|
|
93
|
+
| `initialActiveCellId` | `TreegridCellId \| null` | `null` | Initially active cell; normalized to first enabled cell |
|
|
94
|
+
| `initialSelectedRowIds` | `readonly string[]` | `[]` | Initially selected rows; trimmed to 1 for single mode |
|
|
95
|
+
|
|
96
|
+
## Public API
|
|
97
|
+
|
|
98
|
+
### `createTreegrid(options: CreateTreegridOptions): TreegridModel`
|
|
99
|
+
|
|
100
|
+
Returns a `TreegridModel` with three namespaces:
|
|
101
|
+
|
|
102
|
+
### `state` (signal-backed)
|
|
103
|
+
|
|
104
|
+
| Signal | Type | Description |
|
|
105
|
+
| ---------------- | ------------------------------ | ------------------------------------------------- |
|
|
106
|
+
| `activeCellId` | `Atom<TreegridCellId \| null>` | Currently focused cell; `null` when grid is empty |
|
|
107
|
+
| `expandedRowIds` | `Atom<Set<string>>` | Set of expanded branch row identifiers |
|
|
108
|
+
| `selectedRowIds` | `Atom<Set<string>>` | Set of selected row identifiers |
|
|
109
|
+
| `rowCount` | `Computed<number>` | Total number of rows (all, not just visible) |
|
|
110
|
+
| `columnCount` | `Computed<number>` | Total number of columns |
|
|
111
|
+
|
|
112
|
+
### `actions`
|
|
113
|
+
|
|
114
|
+
| Action | Description |
|
|
115
|
+
| ------------------------------------------------- | ----------------------------------------------------------------------- |
|
|
116
|
+
| `moveUp()` | Move active cell to the same column in the previous visible enabled row |
|
|
117
|
+
| `moveDown()` | Move active cell to the same column in the next visible enabled row |
|
|
118
|
+
| `moveLeft()` | APG ArrowLeft behavior (see Keyboard Contract) |
|
|
119
|
+
| `moveRight()` | APG ArrowRight behavior (see Keyboard Contract) |
|
|
120
|
+
| `moveRowStart()` | Move active cell to the first enabled cell in the current row |
|
|
121
|
+
| `moveRowEnd()` | Move active cell to the last enabled cell in the current row |
|
|
122
|
+
| `expandRow(rowId: string)` | Expand a branch row; no-op on leaf rows or already-expanded rows |
|
|
123
|
+
| `collapseRow(rowId: string)` | Collapse a branch row; migrates focus if active cell is a descendant |
|
|
124
|
+
| `toggleRowExpanded(rowId: string)` | Toggle expanded state of a branch row |
|
|
125
|
+
| `selectRow(rowId: string)` | Replace selection with the given row (single and multiple modes) |
|
|
126
|
+
| `toggleRowSelection(rowId: string)` | In multiple mode: add/remove from selection; in single mode: set |
|
|
127
|
+
| `handleKeyDown(event: TreegridKeyboardEventLike)` | Dispatch keyboard events to the appropriate action |
|
|
128
|
+
|
|
129
|
+
Note: `moveGridStart` (Ctrl+Home) and `moveGridEnd` (Ctrl+End) are internal actions invoked via `handleKeyDown` and are not exposed on the `actions` interface.
|
|
130
|
+
|
|
131
|
+
### `contracts`
|
|
132
|
+
|
|
133
|
+
| Contract | Description |
|
|
134
|
+
| --------------------------------------------------------------- | ----------------------------------------------------- |
|
|
135
|
+
| `getTreegridProps(): TreegridProps` | Props to spread on the root `[role=treegrid]` element |
|
|
136
|
+
| `getRowProps(rowId: string): TreegridRowProps` | Props to spread on each `[role=row]` element |
|
|
137
|
+
| `getCellProps(rowId: string, colId: string): TreegridCellProps` | Props to spread on each cell element |
|
|
138
|
+
|
|
139
|
+
## Contract Prop Shapes
|
|
140
|
+
|
|
141
|
+
### `TreegridProps` (root element)
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
interface TreegridProps {
|
|
145
|
+
id: string // `${idBase}-root`
|
|
146
|
+
role: 'treegrid'
|
|
147
|
+
tabindex: '-1'
|
|
148
|
+
'aria-label'?: string
|
|
149
|
+
'aria-labelledby'?: string
|
|
150
|
+
'aria-multiselectable': 'true' | 'false'
|
|
151
|
+
'aria-rowcount': number // total row count (all rows, not just visible)
|
|
152
|
+
'aria-colcount': number // total column count
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### `TreegridRowProps` (row element)
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
interface TreegridRowProps {
|
|
160
|
+
id: string // `${idBase}-row-${rowId}`
|
|
161
|
+
role: 'row'
|
|
162
|
+
'aria-level': number // starts at 1 for root rows
|
|
163
|
+
'aria-posinset': number // 1-based position within sibling set
|
|
164
|
+
'aria-setsize': number // total siblings count
|
|
165
|
+
'aria-rowindex': number // row.index if provided, else 1-based declaration order
|
|
166
|
+
'aria-expanded'?: 'true' | 'false' // only present on branch rows
|
|
167
|
+
'aria-selected': 'true' | 'false'
|
|
168
|
+
'aria-disabled'?: 'true' // only present when row is disabled
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### `TreegridCellProps` (cell element)
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
interface TreegridCellProps {
|
|
176
|
+
id: string // `${idBase}-cell-${rowId}-${colId}`
|
|
177
|
+
role: TreegridCellRole // from column.cellRole, defaults to 'gridcell'
|
|
178
|
+
tabindex: '0' | '-1' // '0' only for the active non-disabled cell
|
|
179
|
+
'aria-colindex': number // column.index if provided, else 1-based declaration order
|
|
180
|
+
'aria-selected': 'true' | 'false'
|
|
181
|
+
'aria-disabled'?: 'true' // only present when cell is disabled
|
|
182
|
+
'data-active': 'true' | 'false'
|
|
183
|
+
onFocus: () => void // calls setActiveCell to sync focus with state
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## APG and A11y Contract
|
|
188
|
+
|
|
189
|
+
- root role: `treegrid`
|
|
190
|
+
- row role: `row`
|
|
191
|
+
- cell role: `gridcell`, `rowheader`, or `columnheader` (per column definition)
|
|
192
|
+
- required attributes:
|
|
193
|
+
- root: `aria-label` or `aria-labelledby`, `aria-multiselectable`, `aria-rowcount`, `aria-colcount`
|
|
194
|
+
- row: `aria-level`, `aria-posinset`, `aria-setsize`, `aria-rowindex`, `aria-expanded` (for branch rows), `aria-selected`
|
|
195
|
+
- cell: `aria-selected`, `aria-colindex`, `tabindex`, `data-active`
|
|
196
|
+
|
|
197
|
+
## Keyboard Contract
|
|
198
|
+
|
|
199
|
+
- `ArrowUp` / `ArrowDown`: move focus to the cell in the previous/next visible enabled row (same column)
|
|
200
|
+
- `ArrowLeft`:
|
|
201
|
+
- if the focused row is a branch row and is expanded: collapse it
|
|
202
|
+
- if the focused row has a parent: move focus to the same column of the parent row (fallback to first enabled cell in parent if that column is disabled)
|
|
203
|
+
- otherwise (root leaf row): move focus to the previous enabled cell in the same row
|
|
204
|
+
- `ArrowRight`:
|
|
205
|
+
- if the focused row is a branch row and is collapsed: expand it (focus stays)
|
|
206
|
+
- if the focused row is a branch row and is expanded: move focus to the same column of the first child row (fallback to first enabled cell in child)
|
|
207
|
+
- otherwise (leaf row or no children): move focus to the next enabled cell in the same row
|
|
208
|
+
- `Home`: move focus to the first enabled cell in the current row
|
|
209
|
+
- `End`: move focus to the last enabled cell in the current row
|
|
210
|
+
- `Ctrl+Home` / `Meta+Home`: move focus to the first enabled cell in the first visible enabled row
|
|
211
|
+
- `Ctrl+End` / `Meta+End`: move focus to the last enabled cell in the last visible enabled row
|
|
212
|
+
|
|
213
|
+
## Invariants
|
|
214
|
+
|
|
215
|
+
- `activeCellId` must always point to a visible, enabled cell (not inside a collapsed branch, not disabled)
|
|
216
|
+
- collapsing a branch row that contains the `activeCellId` (or any descendant) migrates focus to the collapsed row (same column if not disabled, else first enabled cell in that row)
|
|
217
|
+
- `aria-level` starts at 1 for root rows
|
|
218
|
+
- `initialSelectedRowIds` is clamped to the first entry in `single` mode
|
|
219
|
+
- `expandRow` and `collapseRow` are no-ops on leaf rows
|
|
220
|
+
- disabled rows are excluded from all navigation traversal and from selection
|
|
221
|
+
|
|
222
|
+
## Minimum Test Matrix
|
|
223
|
+
|
|
224
|
+
- 2D navigation across hierarchical rows (`ArrowUp`, `ArrowDown`)
|
|
225
|
+
- `ArrowRight` expand then move to first child in same column
|
|
226
|
+
- `ArrowLeft` parent transition and collapse behavior
|
|
227
|
+
- focus migration when a parent row is collapsed while a descendant is active
|
|
228
|
+
- structural ARIA metadata (`aria-level`, `aria-posinset`, `aria-setsize`, `aria-rowindex`, `aria-expanded`, `aria-rowcount`, `aria-colcount`) correctness
|
|
229
|
+
- skipping disabled rows and disabled cells during navigation
|
|
230
|
+
- `Home` / `End` within row; `Ctrl+Home` / `Ctrl+End` across grid
|
|
231
|
+
|
|
232
|
+
## Adapter Expectations
|
|
233
|
+
|
|
234
|
+
UIKit bindings (e.g., `cv-treegrid` web component) MUST interact with the headless model as follows:
|
|
235
|
+
|
|
236
|
+
### Signals UIKit reads
|
|
237
|
+
|
|
238
|
+
| Signal | When to read |
|
|
239
|
+
| ------------------------ | ------------------------------------------------------------------------------------------------------ |
|
|
240
|
+
| `state.activeCellId()` | To derive which cell has `tabindex="0"` and `data-active="true"` (already embedded in `getCellProps`) |
|
|
241
|
+
| `state.expandedRowIds()` | To compute which child rows are visible / to drive `aria-expanded` (already embedded in `getRowProps`) |
|
|
242
|
+
| `state.selectedRowIds()` | To drive selection styling beyond ARIA (already embedded in `getRowProps` / `getCellProps`) |
|
|
243
|
+
| `state.rowCount()` | If UIKit renders a virtual list and needs the total count |
|
|
244
|
+
| `state.columnCount()` | If UIKit renders a virtual list and needs the total count |
|
|
245
|
+
|
|
246
|
+
### Actions UIKit calls
|
|
247
|
+
|
|
248
|
+
| Action | Trigger |
|
|
249
|
+
| ----------------------------------- | --------------------------------------------------------- |
|
|
250
|
+
| `actions.handleKeyDown(event)` | `keydown` event on any focusable cell or the root element |
|
|
251
|
+
| `actions.toggleRowExpanded(rowId)` | Click on expand/collapse toggle affordance |
|
|
252
|
+
| `actions.toggleRowSelection(rowId)` | Pointer click on a row (accumulate for multi-select) |
|
|
253
|
+
| `actions.selectRow(rowId)` | Programmatic selection replacement (replaces current set) |
|
|
254
|
+
| `actions.expandRow(rowId)` | Programmatic expand |
|
|
255
|
+
| `actions.collapseRow(rowId)` | Programmatic collapse |
|
|
256
|
+
|
|
257
|
+
### Contracts UIKit spreads
|
|
258
|
+
|
|
259
|
+
| Contract | Spread target |
|
|
260
|
+
| -------------------------------------- | ---------------------------------------------------------------- |
|
|
261
|
+
| `contracts.getTreegridProps()` | Root `<div role="treegrid">` or `<table>` wrapper |
|
|
262
|
+
| `contracts.getRowProps(rowId)` | Each `<tr role="row">` or `<div role="row">` |
|
|
263
|
+
| `contracts.getCellProps(rowId, colId)` | Each `<td role="gridcell/rowheader">` or `<div role="gridcell">` |
|
|
264
|
+
|
|
265
|
+
Note: `getCellProps` includes an `onFocus` handler that must be wired to the cell's `focus` event so that mouse-driven focus is reflected back into headless state.
|
|
266
|
+
|
|
267
|
+
## ADR-001 Compliance
|
|
268
|
+
|
|
269
|
+
- **Runtime Policy**: Reatom v1000 only; no @statx/\* in headless core.
|
|
270
|
+
- **Layering**: core -> interactions -> a11y-contracts -> adapters; adapters remain thin mappings.
|
|
271
|
+
- **Independence**: No imports from @project/_, apps/_, or other out-of-package modules.
|
|
272
|
+
- **Verification**: Mandatory adapter integration tests and standalone package test execution.
|
|
273
|
+
|
|
274
|
+
## Out of Scope (Current)
|
|
275
|
+
|
|
276
|
+
- async loading of child rows
|
|
277
|
+
- column-specific sorting
|
|
278
|
+
- drag and drop reordering
|
|
279
|
+
- multiple cell selection (only row selection is prioritized)
|
|
280
|
+
- level-sync from parent `cv-treegrid` (UIKit concern, not headless)
|
|
281
|
+
- multi-select click accumulation vs programmatic replace distinction (UIKit concern; headless exposes both `toggleRowSelection` and `selectRow`)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Treeview Component Contract
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
`Treeview` provides a headless APG-aligned model for hierarchical navigation,
|
|
6
|
+
expansion state, focus management, and selection behavior.
|
|
7
|
+
|
|
8
|
+
## Component Files
|
|
9
|
+
|
|
10
|
+
- `src/treeview/index.ts` - model and public `createTreeview` API
|
|
11
|
+
- `src/treeview/treeview.test.ts` - unit behavior tests
|
|
12
|
+
|
|
13
|
+
## Public API
|
|
14
|
+
|
|
15
|
+
- `createTreeview(options)`
|
|
16
|
+
- `state` (signal-backed): `activeId()`, `selectedIds()`, `expandedIds()`
|
|
17
|
+
- `actions`:
|
|
18
|
+
- focus: `setActive(id)` — programmatically set focus to a specific enabled visible node (or null to clear)
|
|
19
|
+
- navigation: `moveNext`, `movePrev`, `moveFirst`, `moveLast` — advance focus through the visible enabled node list; in single-select mode each call also moves selection to the newly focused node
|
|
20
|
+
- expansion: `expand`, `collapse`, `toggleExpanded`, `expandActive`, `collapseActive`
|
|
21
|
+
- selection: `select`, `toggleSelected`, `clearSelected`
|
|
22
|
+
- keyboard: `handleKeyDown` — dispatches navigation, expansion, and selection intents from keyboard events; ArrowUp/Down/Home/End trigger navigation (and selection-follows-focus in single-select mode); ArrowRight/Left trigger expansion/collapse or parent navigation; Space triggers `toggleSelected`; Enter triggers `select`; Ctrl/Cmd+A triggers select-all in multiple mode
|
|
23
|
+
- `contracts`:
|
|
24
|
+
- `getTreeProps()`
|
|
25
|
+
- `getItemProps(id)`
|
|
26
|
+
- `getVisibleNodeIds()`
|
|
27
|
+
|
|
28
|
+
## APG and A11y Contract
|
|
29
|
+
|
|
30
|
+
- tree role: `tree`
|
|
31
|
+
- node role: `treeitem`
|
|
32
|
+
- node metadata:
|
|
33
|
+
- `aria-level`
|
|
34
|
+
- `aria-posinset`
|
|
35
|
+
- `aria-setsize`
|
|
36
|
+
- branch metadata:
|
|
37
|
+
- `aria-expanded`
|
|
38
|
+
- selection metadata:
|
|
39
|
+
- `aria-selected`
|
|
40
|
+
- disabled metadata:
|
|
41
|
+
- `aria-disabled`
|
|
42
|
+
|
|
43
|
+
## Keyboard Contract
|
|
44
|
+
|
|
45
|
+
- `ArrowUp` / `ArrowDown`: visible-node traversal; in single-select mode also moves selection to the newly focused enabled node
|
|
46
|
+
- `Home` / `End`: first/last visible node; in single-select mode also moves selection to the newly focused enabled node
|
|
47
|
+
- `ArrowRight`:
|
|
48
|
+
- expands collapsed branch
|
|
49
|
+
- moves to first child when already expanded
|
|
50
|
+
- `ArrowLeft`:
|
|
51
|
+
- collapses expanded branch
|
|
52
|
+
- moves to parent when already collapsed
|
|
53
|
+
- `Enter`: select active node (both modes)
|
|
54
|
+
- `Space`: toggle selection on active node (both modes; in multiple mode, focus and selection are independent)
|
|
55
|
+
- `Ctrl/Cmd + A` in multiple mode: select all enabled nodes
|
|
56
|
+
|
|
57
|
+
## Selection-Follows-Focus (Single-Select Mode)
|
|
58
|
+
|
|
59
|
+
In single-select mode (`selectionMode: 'single'`), selection follows focus for navigation actions. When `moveNext`, `movePrev`, `moveFirst`, or `moveLast` moves focus to a new enabled visible node, `selectedIds` is simultaneously updated to contain only that node's id. This is the APG-recommended roving-tabindex pattern for single-select trees.
|
|
60
|
+
|
|
61
|
+
In multiple-select mode (`selectionMode: 'multiple'`), focus and selection remain independent. Navigation actions only update `activeId`; selection changes require explicit `Space`, `Enter`, or `Ctrl/Cmd+A` interactions.
|
|
62
|
+
|
|
63
|
+
## Selection and Collapse Invariants
|
|
64
|
+
|
|
65
|
+
- `activeId` is always `null` or an enabled visible node id
|
|
66
|
+
- in single-select mode, after any navigation action (`moveNext`, `movePrev`, `moveFirst`, `moveLast`) that moves focus to a new node, `selectedIds` equals `[newActiveId]`
|
|
67
|
+
- collapsing a branch while focus is inside descendants moves focus to the collapsed parent
|
|
68
|
+
- selected descendants remain selected after collapse unless explicitly changed
|
|
69
|
+
- disabled nodes cannot become selected through actions
|
|
70
|
+
|
|
71
|
+
## Minimum Test Matrix
|
|
72
|
+
|
|
73
|
+
- deterministic visible traversal from expansion state
|
|
74
|
+
- Arrow traversal with expand/collapse transitions
|
|
75
|
+
- structural ARIA metadata coverage
|
|
76
|
+
- multi-select behavior including select-all shortcut
|
|
77
|
+
- focus and selection invariants under collapse
|
|
78
|
+
|
|
79
|
+
## ADR-001 Compliance
|
|
80
|
+
|
|
81
|
+
- **Runtime Policy**: Reatom v1000 only; no @statx/\* in headless core.
|
|
82
|
+
- **Layering**: core -> interactions -> a11y-contracts -> adapters; adapters remain thin mappings.
|
|
83
|
+
- **Independence**: No imports from @project/_, apps/_, or other out-of-package modules.
|
|
84
|
+
- **Verification**: Mandatory adapter integration tests and standalone package test execution.
|
|
85
|
+
|
|
86
|
+
## Out of Scope (Current)
|
|
87
|
+
|
|
88
|
+
- async/lazy node loading
|
|
89
|
+
- drag-and-drop reordering
|
|
90
|
+
- checkbox/radio treeitem variants
|
|
91
|
+
- typeahead across visible nodes
|