@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,140 @@
|
|
|
1
|
+
# Carousel Component Contract
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
`Carousel` provides a headless APG-aligned model for a slideshow or image rotator, enabling users to navigate through a set of items (slides) sequentially.
|
|
6
|
+
|
|
7
|
+
## Component Files
|
|
8
|
+
|
|
9
|
+
- `src/carousel/index.ts` - model and public `createCarousel` API
|
|
10
|
+
- `src/carousel/carousel.test.ts` - unit behavior tests
|
|
11
|
+
|
|
12
|
+
## Public API
|
|
13
|
+
|
|
14
|
+
- `createCarousel(options)`
|
|
15
|
+
- `state` (signal-backed):
|
|
16
|
+
- `activeSlideIndex()`
|
|
17
|
+
- `isPaused()`
|
|
18
|
+
- `slideCount()`
|
|
19
|
+
- `visibleSlideIndices()` - array of indices currently in view
|
|
20
|
+
- `actions`:
|
|
21
|
+
- navigation: `moveNext`, `movePrev`, `moveTo(index)`
|
|
22
|
+
- playback: `play`, `pause`, `togglePlay`
|
|
23
|
+
- keyboard: `handleKeyDown`
|
|
24
|
+
- `contracts`:
|
|
25
|
+
- `getRootProps()`
|
|
26
|
+
- `getSlideGroupProps()`
|
|
27
|
+
- `getSlideProps(index)`
|
|
28
|
+
- `getNextButtonProps()`
|
|
29
|
+
- `getPrevButtonProps()`
|
|
30
|
+
- `getPlayPauseButtonProps()`
|
|
31
|
+
- `getIndicatorProps(index)`
|
|
32
|
+
|
|
33
|
+
## APG and A11y Contract
|
|
34
|
+
|
|
35
|
+
- root role: `region` (with `aria-roledescription="carousel"`)
|
|
36
|
+
- slide group role: `group` (with `aria-roledescription="slide"`)
|
|
37
|
+
- required attributes:
|
|
38
|
+
- root: `aria-label` or `aria-labelledby`
|
|
39
|
+
- slide: `aria-label` or `aria-labelledby`, `aria-hidden` (if not visible)
|
|
40
|
+
- buttons: `aria-controls`, `aria-label`
|
|
41
|
+
- play/pause button: toggle `aria-label` between "Stop slide rotation" / "Start slide rotation" (NO `aria-pressed` per W3C APG guidance)
|
|
42
|
+
- focus management:
|
|
43
|
+
- slides are not focusable by default unless they contain interactive elements
|
|
44
|
+
- navigation controls (buttons, indicators) are in the tab sequence
|
|
45
|
+
|
|
46
|
+
## Behavior Contract
|
|
47
|
+
|
|
48
|
+
- `Carousel` supports automatic rotation (autoplay) with a configurable interval.
|
|
49
|
+
- rotation MUST pause on focus (when any element inside the carousel is focused) or on mouse hover.
|
|
50
|
+
- rotation MUST stop permanently if the user explicitly pauses it.
|
|
51
|
+
- `aria-live` is used to announce slide changes:
|
|
52
|
+
- `off` when autoplaying
|
|
53
|
+
- `polite` when the user manually navigates
|
|
54
|
+
|
|
55
|
+
## Invariants
|
|
56
|
+
|
|
57
|
+
- `activeSlideIndex` must be within `[0, slideCount - 1]`.
|
|
58
|
+
- `aria-roledescription` is used to provide a more descriptive role than `region` or `group`.
|
|
59
|
+
- only visible slides should be accessible to assistive technologies (`aria-hidden="false"`).
|
|
60
|
+
|
|
61
|
+
## Transitions Table
|
|
62
|
+
|
|
63
|
+
| Event / Action | Current State | Next State / Effect |
|
|
64
|
+
| --------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------ |
|
|
65
|
+
| `moveNext()` | any | `activeSlideIndex` = `(current + 1) % slideCount`; `aria-live` = `polite`; reset autoplay timer |
|
|
66
|
+
| `movePrev()` | any | `activeSlideIndex` = `(current - 1 + slideCount) % slideCount`; `aria-live` = `polite`; reset autoplay timer |
|
|
67
|
+
| `moveTo(index)` | any | `activeSlideIndex` = clamped index; `aria-live` = `polite`; reset autoplay timer |
|
|
68
|
+
| `play()` | `userPaused = true` | `userPaused` = `false`; autoplay resumes |
|
|
69
|
+
| `pause()` | `userPaused = false` | `userPaused` = `true`; autoplay timer cleared |
|
|
70
|
+
| `togglePlay()` | `userPaused = true` | calls `play()` |
|
|
71
|
+
| `togglePlay()` | `userPaused = false` | calls `pause()` |
|
|
72
|
+
| `handleFocusIn()` | any | `isFocusWithin` = `true`; autoplay timer cleared |
|
|
73
|
+
| `handleFocusOut()` | any | `isFocusWithin` = `false`; autoplay resumes if eligible |
|
|
74
|
+
| `handlePointerEnter()` | any | `isPointerInside` = `true`; autoplay timer cleared |
|
|
75
|
+
| `handlePointerLeave()` | any | `isPointerInside` = `false`; autoplay resumes if eligible |
|
|
76
|
+
| `handleKeyDown(ArrowRight)` | any | calls `moveNext()` |
|
|
77
|
+
| `handleKeyDown(ArrowLeft)` | any | calls `movePrev()` |
|
|
78
|
+
| `handleKeyDown(Home)` | any | calls `moveTo(0)` |
|
|
79
|
+
| `handleKeyDown(End)` | any | calls `moveTo(slideCount - 1)` |
|
|
80
|
+
| autoplay timer fires | autoplay running | `activeSlideIndex` advances by 1; `aria-live` = `off`; timer restarted |
|
|
81
|
+
|
|
82
|
+
Autoplay is "running" when: `autoplay` option enabled AND `userPaused = false` AND `isPointerInside = false` AND `isFocusWithin = false` AND `slideCount > 1`.
|
|
83
|
+
|
|
84
|
+
## Adapter Expectations
|
|
85
|
+
|
|
86
|
+
UIKit adapters MUST bind to the headless model as follows:
|
|
87
|
+
|
|
88
|
+
**Signals read (reactive, drive re-renders):**
|
|
89
|
+
|
|
90
|
+
- `state.activeSlideIndex()` — current active slide index
|
|
91
|
+
- `state.isPaused()` — computed pause state (combines user-paused, focus, pointer)
|
|
92
|
+
- `state.slideCount()` — number of slides
|
|
93
|
+
- `state.visibleSlideIndices()` — array of currently visible slide indices
|
|
94
|
+
|
|
95
|
+
**Actions called (event handlers, never mutate state directly):**
|
|
96
|
+
|
|
97
|
+
- `actions.moveNext()` / `actions.movePrev()` — on next/prev button click
|
|
98
|
+
- `actions.moveTo(index)` — on indicator click
|
|
99
|
+
- `actions.togglePlay()` — on play/pause button click
|
|
100
|
+
- `actions.handleKeyDown(event)` — on keydown within carousel
|
|
101
|
+
- `actions.handleFocusIn()` / `actions.handleFocusOut()` — on focusin/focusout on root
|
|
102
|
+
- `actions.handlePointerEnter()` / `actions.handlePointerLeave()` — on pointerenter/pointerleave on root
|
|
103
|
+
|
|
104
|
+
**Contracts spread (attribute maps applied directly to DOM elements):**
|
|
105
|
+
|
|
106
|
+
- `contracts.getRootProps()` — spread onto the carousel root element
|
|
107
|
+
- `contracts.getSlideGroupProps()` — spread onto the slide container
|
|
108
|
+
- `contracts.getSlideProps(index)` — spread onto each slide element
|
|
109
|
+
- `contracts.getNextButtonProps()` — spread onto the next button
|
|
110
|
+
- `contracts.getPrevButtonProps()` — spread onto the previous button
|
|
111
|
+
- `contracts.getPlayPauseButtonProps()` — spread onto the play/pause toggle button (returns `aria-label` only, no `aria-pressed`)
|
|
112
|
+
- `contracts.getIndicatorProps(index)` — spread onto each indicator button
|
|
113
|
+
|
|
114
|
+
**UIKit-only concerns (NOT in headless):**
|
|
115
|
+
|
|
116
|
+
- Touch/swipe gesture handling
|
|
117
|
+
- CSS transition animations
|
|
118
|
+
- Responsive layout logic
|
|
119
|
+
|
|
120
|
+
## Minimum Test Matrix
|
|
121
|
+
|
|
122
|
+
- manual navigation via `moveNext` / `movePrev`
|
|
123
|
+
- autoplay lifecycle (start, pause on focus, resume on blur)
|
|
124
|
+
- `aria-live` state transitions based on interaction source
|
|
125
|
+
- correct `aria-hidden` mapping for visible vs hidden slides
|
|
126
|
+
- indicator synchronization with `activeSlideIndex`
|
|
127
|
+
|
|
128
|
+
## ADR-001 Compliance
|
|
129
|
+
|
|
130
|
+
- **Runtime Policy**: Reatom v1000 only; no @statx/\* in headless core.
|
|
131
|
+
- **Layering**: core -> interactions -> a11y-contracts -> adapters; adapters remain thin mappings.
|
|
132
|
+
- **Independence**: No imports from @project/_, apps/_, or other out-of-package modules.
|
|
133
|
+
- **Verification**: Mandatory adapter integration tests and standalone package test execution.
|
|
134
|
+
|
|
135
|
+
## Out of Scope (Current)
|
|
136
|
+
|
|
137
|
+
- touch swipe gestures (should be handled in the adapter)
|
|
138
|
+
- complex transition animations (CSS/JS animations are out of scope for headless)
|
|
139
|
+
- vertical carousels
|
|
140
|
+
- variable-width slides
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# Checkbox Component Contract
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
`Checkbox` provides a headless APG-aligned model for a two-state or three-state (indeterminate) toggle control.
|
|
6
|
+
|
|
7
|
+
It handles checked state transitions, indeterminate state management, and standard keyboard interactions.
|
|
8
|
+
|
|
9
|
+
## Terminology (Normative)
|
|
10
|
+
|
|
11
|
+
Canonical conceptual states:
|
|
12
|
+
|
|
13
|
+
| Conceptual state | Component state (canonical) | ARIA mapping |
|
|
14
|
+
| ---------------- | -------------------------------------- | ---------------------- |
|
|
15
|
+
| `unchecked` | `checked=false`, `indeterminate=false` | `aria-checked="false"` |
|
|
16
|
+
| `checked` | `checked=true`, `indeterminate=false` | `aria-checked="true"` |
|
|
17
|
+
| `indeterminate` | `checked=false`, `indeterminate=true` | `aria-checked="mixed"` |
|
|
18
|
+
|
|
19
|
+
Notes:
|
|
20
|
+
|
|
21
|
+
- `indeterminate` is the canonical component third-state term.
|
|
22
|
+
- `mixed` is an ARIA token only (used exclusively in `aria-checked="mixed"`).
|
|
23
|
+
|
|
24
|
+
## Component Files
|
|
25
|
+
|
|
26
|
+
- `src/checkbox/index.ts` - model and public `createCheckbox` API
|
|
27
|
+
- `src/checkbox/checkbox.test.ts` - unit behavior tests
|
|
28
|
+
|
|
29
|
+
## Public API
|
|
30
|
+
|
|
31
|
+
- `createCheckbox(options)`
|
|
32
|
+
- `state` (signal-backed):
|
|
33
|
+
- `checked()`: `boolean`
|
|
34
|
+
- `indeterminate()`: `boolean`
|
|
35
|
+
- `isDisabled()`: `boolean`
|
|
36
|
+
- `isReadOnly()`: `boolean`
|
|
37
|
+
- `actions`:
|
|
38
|
+
- `toggle()`: toggles between checked and unchecked (see indeterminate transition)
|
|
39
|
+
- `setChecked(value: boolean)`: explicitly sets `checked` (normalizes indeterminate)
|
|
40
|
+
- `setIndeterminate(value: boolean)`: explicitly sets `indeterminate` (normalizes checked)
|
|
41
|
+
- `handleKeyDown(event)`
|
|
42
|
+
- `contracts`:
|
|
43
|
+
- `getCheckboxProps()`
|
|
44
|
+
|
|
45
|
+
## Form-Associated Primitives (Normative)
|
|
46
|
+
|
|
47
|
+
The headless core defines form-associated primitives and expected semantics. Adapters/wrappers MUST map these primitives to the host platform's form APIs (for example, a native `input[type="checkbox"]`).
|
|
48
|
+
|
|
49
|
+
### Primitive surface
|
|
50
|
+
|
|
51
|
+
- `name`: `string | undefined`
|
|
52
|
+
- Form field name used during submission. If `name` is empty/undefined, the control does not contribute a value.
|
|
53
|
+
- `value`: `string | undefined`
|
|
54
|
+
- Value submitted for `name` when the checkbox is submitted.
|
|
55
|
+
- If not provided, the default value is `"on"` (matching HTML checkbox defaults).
|
|
56
|
+
- `required`: `boolean | undefined`
|
|
57
|
+
- Constraint that requires the checkbox to be checked.
|
|
58
|
+
- `form`: `string | undefined`
|
|
59
|
+
- Associates the control to a specific form owner (maps to the HTML `form` attribute). If omitted, the nearest containing form is used.
|
|
60
|
+
- `defaultChecked`: `boolean | undefined`
|
|
61
|
+
- Initial `checked` state used for uncontrolled usage. Applied once during initialization.
|
|
62
|
+
- `autofocus`: `boolean | undefined`
|
|
63
|
+
- If true, the rendered control SHOULD receive focus on initial mount/connection.
|
|
64
|
+
|
|
65
|
+
### Submission semantics
|
|
66
|
+
|
|
67
|
+
- The control contributes a name/value pair only when `checked=true`.
|
|
68
|
+
- `indeterminate` behaves as unchecked for submission: if `indeterminate=true` then the control MUST NOT submit a value.
|
|
69
|
+
- When submitted, the pair is `name=value` (or `name="on"` when `value` is not provided).
|
|
70
|
+
|
|
71
|
+
### Required / validity semantics
|
|
72
|
+
|
|
73
|
+
- If `required=true`, the constraint is satisfied only when `checked=true`.
|
|
74
|
+
- `indeterminate` does not satisfy `required` (treated as unchecked for validity).
|
|
75
|
+
|
|
76
|
+
## APG and A11y Contract
|
|
77
|
+
|
|
78
|
+
- role: `checkbox`
|
|
79
|
+
- `aria-checked`: `"true" | "false" | "mixed"`
|
|
80
|
+
- `aria-disabled`: boolean (if disabled)
|
|
81
|
+
- `aria-readonly`: boolean (if readonly)
|
|
82
|
+
- `tabindex`: `0` (or `-1` if disabled)
|
|
83
|
+
- linkage: supports `aria-labelledby` and `aria-describedby` via options
|
|
84
|
+
|
|
85
|
+
## Behavior Contract
|
|
86
|
+
|
|
87
|
+
- `Space` key toggles the checked state.
|
|
88
|
+
- `Click` interaction toggles the checked state.
|
|
89
|
+
- If `indeterminate` is enabled, the component can be initialized in an indeterminate state.
|
|
90
|
+
- User toggle transition: `indeterminate` -> `checked` (standard APG recommendation).
|
|
91
|
+
- Disabled or Read-only checkboxes do not respond to toggle actions.
|
|
92
|
+
|
|
93
|
+
## Cross-Spec Consistency (Normative)
|
|
94
|
+
|
|
95
|
+
This document defines the canonical checkbox state model and invariants.
|
|
96
|
+
|
|
97
|
+
- UIKit components (for example `cv-checkbox`) and any adapters/wrappers MUST preserve the same conceptual states, invariants, and user-driven transitions defined here.
|
|
98
|
+
- If a UIKit surface intentionally diverges, the divergence MUST be explicitly documented in both specs to prevent drift.
|
|
99
|
+
|
|
100
|
+
## Invariants
|
|
101
|
+
|
|
102
|
+
- Canonical conceptual states are exactly: `unchecked`, `checked`, `indeterminate`.
|
|
103
|
+
- If represented as booleans, `indeterminate=true` implies `checked=false`.
|
|
104
|
+
- Normalization rules:
|
|
105
|
+
- When setting state, if `indeterminate=true`, then force `checked=false`.
|
|
106
|
+
- If indeterminate behavior is not enabled/configured, then any attempt to set `indeterminate=true` MUST normalize to `indeterminate=false`.
|
|
107
|
+
- A disabled checkbox cannot be toggled via `actions.toggle()`.
|
|
108
|
+
- A read-only checkbox cannot be toggled via `actions.toggle()`.
|
|
109
|
+
|
|
110
|
+
## Minimum Test Matrix
|
|
111
|
+
|
|
112
|
+
- toggle behavior (false -> true -> false)
|
|
113
|
+
- indeterminate state initialization and transition to checked on toggle
|
|
114
|
+
- disabled state prevents state changes
|
|
115
|
+
- read-only state prevents state changes
|
|
116
|
+
- keyboard `Space` interaction
|
|
117
|
+
- correct `aria-checked` mapping for all states (including `aria-checked="mixed"` for indeterminate)
|
|
118
|
+
|
|
119
|
+
## Migration Notes (Non-normative)
|
|
120
|
+
|
|
121
|
+
This section documents known terminology/state-shape changes and the breaking-change communication policy.
|
|
122
|
+
|
|
123
|
+
### Terminology change: `mixed` -> `indeterminate`
|
|
124
|
+
|
|
125
|
+
- `indeterminate` is the canonical third-state term.
|
|
126
|
+
- `mixed` remains an ARIA token only (used exclusively in `aria-checked="mixed"`).
|
|
127
|
+
|
|
128
|
+
### State shape change: tri-state -> two booleans
|
|
129
|
+
|
|
130
|
+
The current runtime implementation historically used an internal tri-state value for `checked` and related options.
|
|
131
|
+
|
|
132
|
+
- Old (legacy/internal): `checked: boolean | 'mixed'` (+ optional `mixed: boolean`) and `allowMixed`.
|
|
133
|
+
- New (canonical/public): `checked: boolean`, `indeterminate: boolean`, and `allowIndeterminate`.
|
|
134
|
+
|
|
135
|
+
Mappings:
|
|
136
|
+
|
|
137
|
+
- `checked === 'mixed'` -> `indeterminate=true` and `checked=false`.
|
|
138
|
+
- `allowMixed` -> `allowIndeterminate`.
|
|
139
|
+
|
|
140
|
+
### Breaking-change communication policy
|
|
141
|
+
|
|
142
|
+
When this contract changes in a breaking way, the change MUST be documented in this section as:
|
|
143
|
+
|
|
144
|
+
- terminology changes (old term -> new term)
|
|
145
|
+
- state/payload shape changes (old shape -> new shape)
|
|
146
|
+
- a short, explicit statement that the change is breaking and requires consumer migration
|
|
147
|
+
|
|
148
|
+
### Parity matrix (Headless vs UIKit)
|
|
149
|
+
|
|
150
|
+
This matrix is intentionally short and exists to prevent drift between `packages/headless/specs/components/checkbox.md` and `packages/uikit/specs/components/checkbox.md`.
|
|
151
|
+
|
|
152
|
+
| Surface | Headless | UIKit |
|
|
153
|
+
| ---------------------------- | ------------------------------------------ | ---------------------------------------- |
|
|
154
|
+
| Canonical third-state term | `indeterminate` | `indeterminate` attribute + event detail |
|
|
155
|
+
| ARIA token for third state | `aria-checked="mixed"` only | `aria-checked="mixed"` only |
|
|
156
|
+
| State representation | `checked:boolean`, `indeterminate:boolean` | `checked`/`indeterminate` attributes |
|
|
157
|
+
| User toggle transition | `indeterminate` -> `checked` | `indeterminate` -> `checked` |
|
|
158
|
+
| Disabled/read-only semantics | cannot toggle | cannot toggle |
|
|
159
|
+
| Payload on user interaction | N/A (actions/state API) | `{ checked, indeterminate, value? }` |
|
|
160
|
+
| Form primitives | specified (see above) | not specified on `cv-checkbox` surface |
|
|
161
|
+
|
|
162
|
+
## ADR-001 Compliance
|
|
163
|
+
|
|
164
|
+
- **Runtime Policy**: Reatom v1000 only; no @statx/\* in headless core.
|
|
165
|
+
- **Layering**: core -> interactions -> a11y-contracts -> adapters; adapters remain thin mappings.
|
|
166
|
+
- **Independence**: No imports from @project/_, apps/_, or other out-of-package modules.
|
|
167
|
+
- **Verification**: Mandatory adapter integration tests and standalone package test execution.
|
|
168
|
+
|
|
169
|
+
## Out of Scope (Current)
|
|
170
|
+
|
|
171
|
+
- native form submission integration (handled by adapters/wrappers)
|
|
172
|
+
- grouping logic (see `CheckboxGroup` or `Fieldset` specs)
|