@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,400 @@
|
|
|
1
|
+
# Date Picker Component Contract
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
`DatePicker` is a headless APG-aligned contract for a date+time input control with an editable text input and a popup calendar dialog. It combines combobox-style trigger behavior with dialog-based calendar and time selection. The component supports dual-mode commit: user edits to date/time are staged as draft state in the calendar/input and only pushed to committed value through explicit commit actions.
|
|
6
|
+
|
|
7
|
+
## Component Files
|
|
8
|
+
|
|
9
|
+
- `src/date-picker/index.ts` - model and public `createDatePicker` API
|
|
10
|
+
- `src/date-picker/date-picker.test.ts` - unit behavior tests
|
|
11
|
+
|
|
12
|
+
## Public API
|
|
13
|
+
|
|
14
|
+
### `createDatePicker(options)`
|
|
15
|
+
|
|
16
|
+
#### CreateDatePickerOptions
|
|
17
|
+
|
|
18
|
+
| Option | Type | Default | Description |
|
|
19
|
+
| ---------------- | --------------------------------------------------- | ---------------------------------------------- | --------------------------------------------------------------------------- | ------------------------------------ |
|
|
20
|
+
| `idBase` | `string` | `'date-picker'` | Prefix for generated IDs |
|
|
21
|
+
| `value` | `string \| null` | `null` | Initial committed value in `YYYY-MM-DDTHH:mm` format (24-hour) |
|
|
22
|
+
| `required` | `boolean` | `false` | Required marker for input validation |
|
|
23
|
+
| `disabled` | `boolean` | `false` | Component disabled state |
|
|
24
|
+
| `readonly` | `boolean` | `false` | Prevents user-driven changes |
|
|
25
|
+
| `placeholder` | `string` | `'Select date and time'` | Placeholder for the input |
|
|
26
|
+
| `locale` | `string` | `'en-US'` | Locale for label/date formatting |
|
|
27
|
+
| `timeZone` | `'local' \| 'utc'` | `'local'` | Formatting/parsing basis for value interpretation |
|
|
28
|
+
| `min` | `string` | `null` | Inclusive minimum `YYYY-MM-DDTHH:mm` value |
|
|
29
|
+
| `max` | `string` | `null` | Inclusive maximum `YYYY-MM-DDTHH:mm` value |
|
|
30
|
+
| `minuteStep` | `number` | `1` | Minute granularity for draft time updates |
|
|
31
|
+
| `hourCycle` | `12 \| 24` | `24` | Hour input rendering format |
|
|
32
|
+
| `closeOnEscape` | `boolean` | `true` | Whether Escape closes dialog |
|
|
33
|
+
| `ariaLabel` | `string` | `'Select date and time'` | Dialog accessible label |
|
|
34
|
+
| `parseDateTime` | `(value: string, locale: string) => ParsedDateTime | null` | default parser for `YYYY-MM-DD`, `YYYY-MM-DDTHH:mm`, and `YYYY-MM-DD HH:mm` | Hook to customize editable parsing |
|
|
35
|
+
| `formatDateTime` | `(value: ParsedDateTime, locale: string) => string` | default formatter returning `YYYY-MM-DDTHH:mm` | Hook to control displayed input text |
|
|
36
|
+
| `onInput` | `(value: string) => void` | `undefined` | Called after user typing changes input text |
|
|
37
|
+
| `onCommit` | `(value: string | null) => void` | `undefined` | Called after committed value changes |
|
|
38
|
+
| `onClear` | `() => void` | `undefined` | Called after `clear()` succeeds |
|
|
39
|
+
|
|
40
|
+
#### Types
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
interface ParsedDateTime {
|
|
44
|
+
date: string // YYYY-MM-DD
|
|
45
|
+
time: string // HH:mm
|
|
46
|
+
full: string // YYYY-MM-DDTHH:mm
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface CalendarDay {
|
|
50
|
+
date: string // YYYY-MM-DD
|
|
51
|
+
month: 'prev' | 'current' | 'next'
|
|
52
|
+
inRange: boolean
|
|
53
|
+
isToday: boolean
|
|
54
|
+
disabled: boolean
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## State Signals (signal-backed)
|
|
59
|
+
|
|
60
|
+
| Signal | Type | Derived | Description |
|
|
61
|
+
| ------------------------- | ------------------------ | ------- | -------------------------------------------------------------------------- |
|
|
62
|
+
| `inputValue()` | `string` | No | Current editable text shown in the input |
|
|
63
|
+
| `isOpen()` | `boolean` | No | Calendar dialog open state |
|
|
64
|
+
| `focusedDate()` | `string \| null` | No | Focused day in the visible grid (YYYY-MM-DD) |
|
|
65
|
+
| `committedDate()` | `string \| null` | No | Committed date part |
|
|
66
|
+
| `committedTime()` | `string \| null` | No | Committed time part in `HH:mm` |
|
|
67
|
+
| `draftDate()` | `string \| null` | No | Working date value while dialog is open |
|
|
68
|
+
| `draftTime()` | `string \| null` | No | Working time value while dialog is open |
|
|
69
|
+
| `displayedYear()` | `number` | No | Calendar year currently rendered |
|
|
70
|
+
| `displayedMonth()` | `number` | No | Calendar month currently rendered (1-12) |
|
|
71
|
+
| `isInputFocused()` | `boolean` | No | Tracks whether combobox input is focused |
|
|
72
|
+
| `isCalendarFocused()` | `boolean` | No | Tracks whether calendar/time zone is focused |
|
|
73
|
+
| `disabled()` | `boolean` | No | Reflects disabled mode |
|
|
74
|
+
| `readonly()` | `boolean` | No | Reflects readonly mode |
|
|
75
|
+
| `required()` | `boolean` | No | Reflects required mode |
|
|
76
|
+
| `placeholder()` | `string` | No | Current placeholder |
|
|
77
|
+
| `locale()` | `string` | No | Active locale |
|
|
78
|
+
| `timeZone()` | `'local' \| 'utc'` | No | Active timezone mode |
|
|
79
|
+
| `min()` | `string \| null` | No | Lower bound |
|
|
80
|
+
| `max()` | `string \| null` | No | Upper bound |
|
|
81
|
+
| `minuteStep()` | `number` | No | Minute step |
|
|
82
|
+
| `hourCycle()` | `12 \| 24` | No | Hour cycle |
|
|
83
|
+
| `isDualCommit()` | `boolean` | Yes | Always `true` for this component spec |
|
|
84
|
+
| `hasCommittedSelection()` | `boolean` | Yes | `committedDate() !== null && committedTime() !== null` |
|
|
85
|
+
| `hasDraftSelection()` | `boolean` | Yes | `draftDate() !== null && draftTime() !== null` |
|
|
86
|
+
| `committedValue()` | `string \| null` | Yes | `hasCommittedSelection() ? `${committedDate()}T${committedTime()}` : null` |
|
|
87
|
+
| `draftValue()` | `string \| null` | Yes | `hasDraftSelection() ? `${draftDate()}T${draftTime()}` : null` |
|
|
88
|
+
| `canCommitInput()` | `boolean` | Yes | `parsedValue() !== null` |
|
|
89
|
+
| `parsedValue()` | `ParsedDateTime \| null` | Yes | result of `parseDateTime(inputValue(), locale())` |
|
|
90
|
+
| `inputInvalid()` | `boolean` | Yes | `inputValue().length > 0 && !canCommitInput()` |
|
|
91
|
+
| `visibleDays()` | `readonly CalendarDay[]` | Yes | full 6x7 visible matrix for current `displayedMonth`/`displayedYear` |
|
|
92
|
+
| `today()` | `string` | Yes | Local current date in `YYYY-MM-DD` |
|
|
93
|
+
| `selectedCellId()` | `string \| null` | Yes | `isOpen() ? draftDate() : committedDate()` |
|
|
94
|
+
|
|
95
|
+
## Actions
|
|
96
|
+
|
|
97
|
+
- `open()` — opens dialog, initializes `draftDate/draftTime` from committed state and syncs focused date to first selectable cell
|
|
98
|
+
- `close()` — closes dialog, clears calendar focus and restores input focus logic
|
|
99
|
+
- `toggle()` — delegates to `open`/`close`
|
|
100
|
+
- `setInputValue(value: string)` — updates input text only (user typing); updates `inputInvalid` through derived parse result
|
|
101
|
+
- `commitInput()` — commits parsed `inputValue` into `committedDate/committedTime` when valid and in range
|
|
102
|
+
- `clear()` — clears input and both committed/draft values
|
|
103
|
+
- `setDisabled(value: boolean)` — updates disabled state (clears open popover)
|
|
104
|
+
- `setReadonly(value: boolean)` — updates readonly state
|
|
105
|
+
- `setRequired(value: boolean)` — updates required state
|
|
106
|
+
- `setPlaceholder(value: string)` — updates placeholder
|
|
107
|
+
- `setLocale(value: string)` — updates locale and re-renders formatted values
|
|
108
|
+
- `setTimeZone(value: 'local' | 'utc')` — updates timezone mode (affects `today()` and `jumpToNow()` sources)
|
|
109
|
+
- `setMin(value: string | null)` / `setMax(value: string | null)` — updates boundaries
|
|
110
|
+
- `setMinuteStep(value: number)` — updates minute grid/validation granularity
|
|
111
|
+
- `setHourCycle(value: 12 | 24)` — updates time parsing/formatting behavior
|
|
112
|
+
- `setDisplayedMonth(year: number, month: number)` — updates visible calendar month/year
|
|
113
|
+
- `moveMonth(offset: -1 | 1)` — prev/next month navigation
|
|
114
|
+
- `moveYear(offset: -1 | 1)` — prev/next year navigation
|
|
115
|
+
- `setFocusedDate(date: string | null)` — sets focused calendar day when valid
|
|
116
|
+
- `moveFocusPreviousDay()` / `moveFocusNextDay()` — keyboard date focus navigation
|
|
117
|
+
- `moveFocusPreviousWeek()` / `moveFocusNextWeek()` — keyboard date focus navigation
|
|
118
|
+
- `selectDraftDate(date: string)` — update draft date only (no commit in dual mode)
|
|
119
|
+
- `setDraftTime(time: string)` — set draft time only (format `HH:mm`); value is normalized by snapping to `minuteStep`
|
|
120
|
+
- `jumpToNow()` — sets draft to current local date/time and brings it into view (no commit)
|
|
121
|
+
- `commitDraft()` — explicit dual-mode commit from dialog (sets committed state and closes)
|
|
122
|
+
- `cancelDraft()` — discards draft and restores draft from committed state
|
|
123
|
+
- `handleInputKeyDown(event)` — keyboard routing on input target
|
|
124
|
+
- `handleDialogKeyDown(event)` — keyboard routing on dialog wrapper
|
|
125
|
+
- `handleCalendarKeyDown(event)` — keyboard routing on grid (day navigation/selection)
|
|
126
|
+
- `handleTimeKeyDown(event)` — keyboard routing for hour/minute controls
|
|
127
|
+
- `handleOutsidePointer()` — dialog dismissal hook from outside pointer events
|
|
128
|
+
|
|
129
|
+
## Contracts
|
|
130
|
+
|
|
131
|
+
### Contract Methods
|
|
132
|
+
|
|
133
|
+
- `getInputProps()` → `DatePickerInputProps`
|
|
134
|
+
- `getDialogProps()` → `DatePickerDialogProps`
|
|
135
|
+
- `getCalendarGridProps()` → `DatePickerCalendarGridProps`
|
|
136
|
+
- `getCalendarDayProps(date: string)` → `DatePickerCalendarDayProps`
|
|
137
|
+
- `getMonthNavButtonProps(direction: 'prev' | 'next')` → `DatePickerMonthNavButtonProps`
|
|
138
|
+
- `getYearNavButtonProps(direction: 'prev' | 'next')` → `DatePickerYearNavButtonProps`
|
|
139
|
+
- `getHourInputProps()` → `DatePickerTimeSegmentProps`
|
|
140
|
+
- `getMinuteInputProps()` → `DatePickerTimeSegmentProps`
|
|
141
|
+
- `getApplyButtonProps()` → `DatePickerButtonProps`
|
|
142
|
+
- `getCancelButtonProps()` → `DatePickerButtonProps`
|
|
143
|
+
- `getClearButtonProps()` → `DatePickerButtonProps`
|
|
144
|
+
- `getVisibleDays()` → `readonly CalendarDay[]`
|
|
145
|
+
|
|
146
|
+
### Contract Return Types
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
interface DatePickerInputProps {
|
|
150
|
+
id: string
|
|
151
|
+
role: 'combobox'
|
|
152
|
+
tabindex: '0'
|
|
153
|
+
autocomplete: 'off'
|
|
154
|
+
disabled: boolean
|
|
155
|
+
readonly?: true
|
|
156
|
+
required?: true
|
|
157
|
+
value: string
|
|
158
|
+
placeholder: string
|
|
159
|
+
'aria-haspopup': 'dialog'
|
|
160
|
+
'aria-expanded': 'true' | 'false'
|
|
161
|
+
'aria-controls': string
|
|
162
|
+
'aria-activedescendant'?: string
|
|
163
|
+
'aria-invalid'?: 'true'
|
|
164
|
+
'aria-label'?: string
|
|
165
|
+
onInput: (value: string) => void
|
|
166
|
+
onKeyDown: (event: KeyboardEvent) => void
|
|
167
|
+
onFocus: () => void
|
|
168
|
+
onBlur: () => void
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
interface DatePickerDialogProps {
|
|
172
|
+
id: string
|
|
173
|
+
role: 'dialog'
|
|
174
|
+
tabindex: '-1'
|
|
175
|
+
hidden: boolean
|
|
176
|
+
'aria-modal': 'true'
|
|
177
|
+
'aria-label': string
|
|
178
|
+
onKeyDown: (event: KeyboardEvent) => void
|
|
179
|
+
onPointerDownOutside: () => void
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
interface DatePickerCalendarGridProps {
|
|
183
|
+
id: string
|
|
184
|
+
role: 'grid'
|
|
185
|
+
tabindex: '-1'
|
|
186
|
+
'aria-label': string
|
|
187
|
+
onKeyDown: (event: KeyboardEvent) => void
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
interface DatePickerCalendarDayProps {
|
|
191
|
+
id: string
|
|
192
|
+
role: 'gridcell'
|
|
193
|
+
tabindex: '0' | '-1'
|
|
194
|
+
'aria-selected': 'true' | 'false'
|
|
195
|
+
'aria-disabled'?: 'true'
|
|
196
|
+
'aria-current'?: 'date'
|
|
197
|
+
'data-date': string
|
|
198
|
+
onClick: () => void
|
|
199
|
+
onMouseEnter: () => void
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
interface DatePickerMonthNavButtonProps {
|
|
203
|
+
id: string
|
|
204
|
+
role: 'button'
|
|
205
|
+
tabindex: '0'
|
|
206
|
+
'aria-label': 'Previous month' | 'Next month'
|
|
207
|
+
onClick: () => void
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
interface DatePickerYearNavButtonProps {
|
|
211
|
+
id: string
|
|
212
|
+
role: 'button'
|
|
213
|
+
tabindex: '0'
|
|
214
|
+
'aria-label': 'Previous year' | 'Next year'
|
|
215
|
+
onClick: () => void
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
interface DatePickerTimeSegmentProps {
|
|
219
|
+
id: string
|
|
220
|
+
type: 'text'
|
|
221
|
+
inputmode: 'numeric'
|
|
222
|
+
'aria-label': string
|
|
223
|
+
value: string
|
|
224
|
+
minlength: '2'
|
|
225
|
+
maxlength: '2'
|
|
226
|
+
disabled: boolean
|
|
227
|
+
readonly: boolean
|
|
228
|
+
onInput: (value: string) => void
|
|
229
|
+
onKeyDown: (event: KeyboardEvent) => void
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
interface DatePickerButtonProps {
|
|
233
|
+
id: string
|
|
234
|
+
role: 'button'
|
|
235
|
+
tabindex: '0'
|
|
236
|
+
'aria-label': string
|
|
237
|
+
disabled: boolean
|
|
238
|
+
onClick: () => void
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
> Calendar day ids are generated deterministically as `${idBase}-day-${date}`.
|
|
243
|
+
|
|
244
|
+
### APG and A11y Contract
|
|
245
|
+
|
|
246
|
+
- Input follows combobox conventions with popup trigger:
|
|
247
|
+
- `role='combobox'`
|
|
248
|
+
- `aria-haspopup='dialog'`
|
|
249
|
+
- `aria-expanded`, `aria-controls`, `aria-activedescendant`
|
|
250
|
+
- Dialog:
|
|
251
|
+
- `role='dialog'`, `aria-modal='true'`, `hidden` synced to `!isOpen()`
|
|
252
|
+
- Note: the contract always provides `'aria-modal': 'true'`; `hidden` is the primary open/close signal.
|
|
253
|
+
- Calendar structure:
|
|
254
|
+
- calendar uses `role='grid'`
|
|
255
|
+
- day cells use `role='gridcell'`
|
|
256
|
+
- one logical `aria-current='date'` on the current day when visible
|
|
257
|
+
- Required states conveyed via attributes:
|
|
258
|
+
- `aria-invalid='true'` on input when parse fails or value out of range
|
|
259
|
+
- `aria-required='true'` is set when required and no committed value is selected
|
|
260
|
+
|
|
261
|
+
## Behavior Contract
|
|
262
|
+
|
|
263
|
+
### Dual Commit Model
|
|
264
|
+
|
|
265
|
+
- Draft and committed state are separate by design.
|
|
266
|
+
- Day/time selection in dialog updates draft only.
|
|
267
|
+
- Input typing updates `inputValue` only.
|
|
268
|
+
- `commitInput()` or `commitDraft()` are the **only** ways that mutate committed state in non-disabled/ non-readonly mode.
|
|
269
|
+
- `close()` never commits draft in dual commit mode.
|
|
270
|
+
|
|
271
|
+
### Editable Input + Calendar
|
|
272
|
+
|
|
273
|
+
- Input is editable at all times (unless `readonly` / `disabled`).
|
|
274
|
+
- Typing updates `inputValue` immediately and emits `onInput` callback.
|
|
275
|
+
- Valid parsed input can be committed via `commitInput()` (typically on Enter).
|
|
276
|
+
- Invalid text does not change committed state and keeps `inputInvalid=true`.
|
|
277
|
+
- Enter in dialog footer applies draft through `commitDraft()`.
|
|
278
|
+
|
|
279
|
+
### Calendar Interaction
|
|
280
|
+
|
|
281
|
+
- Opening dialog sets `displayedMonth/displayedYear` to committed value month if present, otherwise current month.
|
|
282
|
+
- Focused day defaults to committed day if exists, otherwise today if selectable, otherwise first selectable day in visible month.
|
|
283
|
+
- Out-of-range and disabled dates are skipped by navigation and cannot be selected.
|
|
284
|
+
- `selectDraftDate` never mutates committed state in dual mode.
|
|
285
|
+
- `commitDraft` applies draft date/time and closes if successful.
|
|
286
|
+
- `cancelDraft` discards draft and reverts to committed value.
|
|
287
|
+
|
|
288
|
+
### Keyboard
|
|
289
|
+
|
|
290
|
+
#### Closed input state
|
|
291
|
+
|
|
292
|
+
- `ArrowDown` / `ArrowUp` / `Space` opens dialog and positions focus in calendar.
|
|
293
|
+
- `Enter` calls `commitInput()` (no-op if input is invalid / out of range).
|
|
294
|
+
- `Escape` has no effect when already closed.
|
|
295
|
+
|
|
296
|
+
#### Open dialog state
|
|
297
|
+
|
|
298
|
+
- `Escape` closes without commit when `closeOnEscape=true`.
|
|
299
|
+
- `Tab` cycles between calendar grid, hour input, minute input, and Apply/Cancel controls.
|
|
300
|
+
- Arrow navigation in grid follows APG calendar keyboard model:
|
|
301
|
+
- Left/Right: previous/next day
|
|
302
|
+
- Up/Down: previous/next week
|
|
303
|
+
- PageUp/PageDown: previous/next month
|
|
304
|
+
- Shift+PageUp/PageDown: previous/next year
|
|
305
|
+
- `Home`/`End`: first/last day in current week
|
|
306
|
+
- `Enter`/`Space` on focused day updates draft and sets `selectedCellId` but does not commit
|
|
307
|
+
- `Ctrl+Enter` or Enter on Apply button calls `commitDraft()`
|
|
308
|
+
|
|
309
|
+
## Transition Model
|
|
310
|
+
|
|
311
|
+
### Core State Transitions
|
|
312
|
+
|
|
313
|
+
| Event / action | Guards | Next state |
|
|
314
|
+
| ------------------------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------- |
|
|
315
|
+
| `open()` | `!isOpen()` and `!disabled()` | `isOpen=true`, `draftDate=committedDate`, `draftTime=committedTime`, `focusedDate` initialized |
|
|
316
|
+
| `close()` | any | `isOpen=false`, `draftDate=committedDate`, `draftTime=committedTime` |
|
|
317
|
+
| `clear()` | `!disabled() && !readonly()` | all value signals to `null`, `inputValue=''`, `isOpen` unchanged |
|
|
318
|
+
| `setInputValue(v)` | any | `inputValue=v` only |
|
|
319
|
+
| `commitInput()` | `!disabled() && !readonly() && canCommitInput()` and within bounds | `committedDate=parsed.date`, `committedTime=parsed.time`, `inputValue=formatDateTime(parsed)`, `isOpen=false` |
|
|
320
|
+
| `commitInput()` | input invalid or out of range | no-op on committed state, `inputInvalid=true` |
|
|
321
|
+
| `setDisplayedMonth(year,month)` | valid month/year | `displayedMonth/year` updated |
|
|
322
|
+
| `moveMonth(-1/1)` | any | `displayedMonth/year` shifted |
|
|
323
|
+
| `moveYear(-1/1)` | any | `displayedYear` shifted |
|
|
324
|
+
| `setFocusedDate(d)` | valid, non-disabled date in current month view | `focusedDate=d` |
|
|
325
|
+
| `selectDraftDate(d)` | date visible/selectable | `draftDate=d` |
|
|
326
|
+
| `setDraftTime(t)` | valid time format | `draftTime=normalizedToMinuteStep(t)` |
|
|
327
|
+
| `commitDraft()` | `isOpen()` and `hasDraftSelection()` and both draft values within bounds | `committedDate=draftDate`, `committedTime=draftTime`, `inputValue=formatDateTime(draft)`, `isOpen=false` |
|
|
328
|
+
| `commitDraft()` | draft invalid | no-op |
|
|
329
|
+
| `cancelDraft()` | `isOpen()` | `draftDate=committedDate`, `draftTime=committedTime`, `inputInvalid=false`, `isOpen=true` |
|
|
330
|
+
| `jumpToNow()` | `!disabled() && !readonly()` | `draftDate=today`, `draftTime=currentTimeAlignedToStep`, calendar month set to draft month |
|
|
331
|
+
|
|
332
|
+
### Keyboard Transitions
|
|
333
|
+
|
|
334
|
+
| Key context | Action triggered |
|
|
335
|
+
| ------------------------ | ----------------------------------------------------------------------- |
|
|
336
|
+
| Closed, input focused | `ArrowDown`/`ArrowUp`/`Space` -> `open()`; `Enter` -> `commitInput()` |
|
|
337
|
+
| Open, calendar focus | `ArrowRight`/`ArrowLeft` -> `moveFocusNextDay` / `moveFocusPreviousDay` |
|
|
338
|
+
| Open, calendar focus | `ArrowDown`/`ArrowUp` -> `moveFocusNextWeek` / `moveFocusPreviousWeek` |
|
|
339
|
+
| Open, calendar focus | `PageDown`/`PageUp` -> `moveMonth(1)` / `moveMonth(-1)` |
|
|
340
|
+
| Open, calendar focus | `Shift+PageDown`/`Shift+PageUp` -> `moveYear(1)` / `moveYear(-1)` |
|
|
341
|
+
| Open, focused day | `Enter`/`Space` -> `selectDraftDate(focusedDate)` |
|
|
342
|
+
| Open, draft form | `Esc` -> `close()` |
|
|
343
|
+
| Open, focused time input | `Enter` -> `commitDraft()` |
|
|
344
|
+
|
|
345
|
+
## Invariants
|
|
346
|
+
|
|
347
|
+
1. `isDualCommit()` is always `true` and never writable.
|
|
348
|
+
2. `committedValue` is `null` iff either `committedDate()` or `committedTime()` is `null`.
|
|
349
|
+
3. `draftDate()` and `draftTime()` are always restored from committed values when dialog closes without commit.
|
|
350
|
+
4. `visibleDays()` always contains day entries for exactly 6 calendar rows (42 cells).
|
|
351
|
+
5. For every entry in `visibleDays()`, `inRange === (min <= date && date <= max)` when min/max are provided; when min/max are absent, `inRange === true`.
|
|
352
|
+
6. Disabled/readonly modes are no-op for commit, commitInput, draft mutations, and keyboard-triggered selection.
|
|
353
|
+
7. Day selection cannot land on dates where `inRange === false`.
|
|
354
|
+
8. `inputInvalid` is `true` exactly when `inputValue` is non-empty and `parsedValue` is `null` or out of bounds.
|
|
355
|
+
9. `getInputProps().'aria-expanded'` equals `isOpen() ? 'true' : 'false'`.
|
|
356
|
+
10. `getDialogProps().hidden` is `!isOpen()`.
|
|
357
|
+
11. `onInput` callback is only invoked from `setInputValue`, never from programmatic `set` actions.
|
|
358
|
+
12. `onCommit` callback is invoked only from successful `commitInput` and `commitDraft`.
|
|
359
|
+
13. `commitDraft()` never commits invalid draft values.
|
|
360
|
+
|
|
361
|
+
## Adapter Expectations
|
|
362
|
+
|
|
363
|
+
UIKit (`cv-date-picker`) binds to the model as follows:
|
|
364
|
+
|
|
365
|
+
- Signals read:
|
|
366
|
+
- `state.inputValue()` / `state.isOpen()` / `state.isInputFocused()` / `state.isCalendarFocused()`
|
|
367
|
+
- `state.disabled()` / `state.readonly()` / `state.required()` / `state.placeholder()`
|
|
368
|
+
- `state.visibleDays()` / `state.focusedDate()` / `state.displayedMonth()` / `state.displayedYear()`
|
|
369
|
+
- `state.hasCommittedSelection()` / `state.committedValue()` / `state.canCommitInput()` / `state.inputInvalid()`
|
|
370
|
+
- `state.min()` / `state.max()` / `state.hourCycle()` / `state.minuteStep()`
|
|
371
|
+
|
|
372
|
+
- Actions called:
|
|
373
|
+
- `open()` / `close()` / `toggle()`
|
|
374
|
+
- `setInputValue(value)` / `commitInput()` / `clear()`
|
|
375
|
+
- `moveMonth()` / `moveYear()` / `setDisplayedMonth()` / `setFocusedDate()`
|
|
376
|
+
- `moveFocusNextDay()` / `moveFocusPreviousDay()` / `moveFocusNextWeek()` / `moveFocusPreviousWeek()`
|
|
377
|
+
- `selectDraftDate()` / `setDraftTime()` / `commitDraft()` / `cancelDraft()` / `jumpToNow()`
|
|
378
|
+
- `handleInputKeyDown()` / `handleDialogKeyDown()` / `handleCalendarKeyDown()` / `handleTimeKeyDown()` / `handleOutsidePointer()`
|
|
379
|
+
|
|
380
|
+
- Contracts spread:
|
|
381
|
+
- `contracts.getInputProps()` on the trigger input
|
|
382
|
+
- `contracts.getDialogProps()` on the popup shell
|
|
383
|
+
- `contracts.getCalendarGridProps()` on calendar body
|
|
384
|
+
- `contracts.getCalendarDayProps(date)` for each visible day
|
|
385
|
+
- `contracts.getMonthNavButtonProps()` and `contracts.getYearNavButtonProps()`
|
|
386
|
+
- `contracts.getHourInputProps()` / `contracts.getMinuteInputProps()`
|
|
387
|
+
- `contracts.getApplyButtonProps()` / `contracts.getCancelButtonProps()` / `contracts.getClearButtonProps()`
|
|
388
|
+
|
|
389
|
+
## Minimum Test Matrix
|
|
390
|
+
|
|
391
|
+
- open/close behavior, focus initialization, and outside-dismiss behavior
|
|
392
|
+
- editable input typing and validation flow
|
|
393
|
+
- dual commit path: dialog selection updates draft only
|
|
394
|
+
- apply/cancel path: commitDraft and cancelDraft semantics
|
|
395
|
+
- keyboard calendar navigation and focused-cell updates
|
|
396
|
+
- month/year navigation boundaries and disabled date skipping
|
|
397
|
+
- min/max boundary enforcement in input and draft selection
|
|
398
|
+
- minute-step rounding/validation behavior
|
|
399
|
+
- ARIA contract surfaces from all spread props
|
|
400
|
+
- disabled/readonly behavior blocking all mutating actions
|