@lekoala/date-picker 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 +236 -0
- package/custom-elements.json +366 -0
- package/dist/date-picker.css +344 -0
- package/dist/date-picker.js +2693 -0
- package/dist/date-picker.min.css +1 -0
- package/dist/date-picker.min.js +17 -0
- package/dist/date-picker.standalone.min.js +361 -0
- package/dist/types/calendar-model.d.ts +34 -0
- package/dist/types/calendar-model.d.ts.map +1 -0
- package/dist/types/date-calendar.d.ts +228 -0
- package/dist/types/date-calendar.d.ts.map +1 -0
- package/dist/types/date-field.d.ts +94 -0
- package/dist/types/date-field.d.ts.map +1 -0
- package/dist/types/date-picker.d.ts +258 -0
- package/dist/types/date-picker.d.ts.map +1 -0
- package/dist/types/date-range.d.ts +89 -0
- package/dist/types/date-range.d.ts.map +1 -0
- package/dist/types/date.d.ts +61 -0
- package/dist/types/date.d.ts.map +1 -0
- package/dist/types/define.d.ts +2 -0
- package/dist/types/define.d.ts.map +1 -0
- package/dist/types/index.d.ts +9 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/intl.d.ts +33 -0
- package/dist/types/intl.d.ts.map +1 -0
- package/dist/types/locales/ar.d.ts +22 -0
- package/dist/types/locales/ar.d.ts.map +1 -0
- package/dist/types/locales/de.d.ts +22 -0
- package/dist/types/locales/de.d.ts.map +1 -0
- package/dist/types/locales/en.d.ts +22 -0
- package/dist/types/locales/en.d.ts.map +1 -0
- package/dist/types/locales/es.d.ts +22 -0
- package/dist/types/locales/es.d.ts.map +1 -0
- package/dist/types/locales/fr.d.ts +22 -0
- package/dist/types/locales/fr.d.ts.map +1 -0
- package/dist/types/locales/hi.d.ts +22 -0
- package/dist/types/locales/hi.d.ts.map +1 -0
- package/dist/types/locales/id.d.ts +22 -0
- package/dist/types/locales/id.d.ts.map +1 -0
- package/dist/types/locales/it.d.ts +22 -0
- package/dist/types/locales/it.d.ts.map +1 -0
- package/dist/types/locales/ja.d.ts +22 -0
- package/dist/types/locales/ja.d.ts.map +1 -0
- package/dist/types/locales/ko.d.ts +22 -0
- package/dist/types/locales/ko.d.ts.map +1 -0
- package/dist/types/locales/nl.d.ts +22 -0
- package/dist/types/locales/nl.d.ts.map +1 -0
- package/dist/types/locales/pl.d.ts +22 -0
- package/dist/types/locales/pl.d.ts.map +1 -0
- package/dist/types/locales/pt-BR.d.ts +22 -0
- package/dist/types/locales/pt-BR.d.ts.map +1 -0
- package/dist/types/locales/pt-PT.d.ts +22 -0
- package/dist/types/locales/pt-PT.d.ts.map +1 -0
- package/dist/types/locales/ru.d.ts +22 -0
- package/dist/types/locales/ru.d.ts.map +1 -0
- package/dist/types/locales/tr.d.ts +22 -0
- package/dist/types/locales/tr.d.ts.map +1 -0
- package/dist/types/locales/zh-CN.d.ts +22 -0
- package/dist/types/locales/zh-CN.d.ts.map +1 -0
- package/dist/types/messages.d.ts +43 -0
- package/dist/types/messages.d.ts.map +1 -0
- package/dist/types/source.d.ts +28 -0
- package/dist/types/source.d.ts.map +1 -0
- package/docs/ACCESSIBILITY.md +128 -0
- package/docs/API.md +289 -0
- package/docs/ARCHITECTURE.md +183 -0
- package/docs/AT-CHECKLIST.md +63 -0
- package/docs/CALENDAR-INTEGRATION.md +70 -0
- package/docs/DECISIONS.md +40 -0
- package/docs/MIGRATION-FLATPICKR.md +92 -0
- package/docs/ROADMAP.md +105 -0
- package/docs/TESTING.md +73 -0
- package/docs/USE_CASES.md +128 -0
- package/package.json +174 -0
- package/src/calendar-model.js +80 -0
- package/src/date-calendar.js +800 -0
- package/src/date-field.js +203 -0
- package/src/date-picker.css +344 -0
- package/src/date-picker.js +1140 -0
- package/src/date-range.js +172 -0
- package/src/date.js +203 -0
- package/src/define.js +9 -0
- package/src/index.js +15 -0
- package/src/intl.js +155 -0
- package/src/locales/ar.js +20 -0
- package/src/locales/de.js +20 -0
- package/src/locales/en.js +20 -0
- package/src/locales/es.js +20 -0
- package/src/locales/fr.js +20 -0
- package/src/locales/hi.js +20 -0
- package/src/locales/id.js +20 -0
- package/src/locales/it.js +20 -0
- package/src/locales/ja.js +20 -0
- package/src/locales/ko.js +20 -0
- package/src/locales/nl.js +20 -0
- package/src/locales/pl.js +20 -0
- package/src/locales/pt-BR.js +20 -0
- package/src/locales/pt-PT.js +20 -0
- package/src/locales/ru.js +20 -0
- package/src/locales/tr.js +20 -0
- package/src/locales/zh-CN.js +20 -0
- package/src/messages.js +31 -0
- package/src/source.js +50 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
The package should remain much smaller than a scheduling calendar while covering the cases where a normal date field stops being enough.
|
|
6
|
+
|
|
7
|
+
The key architectural rule is that **the calendar is the primitive and the picker is a composition**.
|
|
8
|
+
|
|
9
|
+
```text
|
|
10
|
+
civil date helpers
|
|
11
|
+
↓
|
|
12
|
+
CalendarModel
|
|
13
|
+
↓
|
|
14
|
+
<date-calendar> ── source / date state / day decoration
|
|
15
|
+
↓
|
|
16
|
+
<date-picker> ─── input / parse+format / Popover / floating
|
|
17
|
+
↓
|
|
18
|
+
linkDateRange() or application navigation
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## 1. Civil dates are the canonical model
|
|
22
|
+
|
|
23
|
+
Public values are `YYYY-MM-DD` strings. Public months are `YYYY-MM` strings.
|
|
24
|
+
|
|
25
|
+
A selected date is not an instant. The core therefore does not expose a JavaScript `Date`, epoch milliseconds, UTC conversion, timezone or `Temporal` object as the canonical API.
|
|
26
|
+
|
|
27
|
+
`Date` is used internally only as a browser primitive for safe UTC calendar arithmetic and `Intl.DateTimeFormat` presentation.
|
|
28
|
+
|
|
29
|
+
This keeps form values predictable and avoids accidental midnight/timezone conversions.
|
|
30
|
+
|
|
31
|
+
## 2. Four distinct concepts
|
|
32
|
+
|
|
33
|
+
The component must never collapse these concepts:
|
|
34
|
+
|
|
35
|
+
```text
|
|
36
|
+
display = month currently rendered
|
|
37
|
+
focus = current keyboard target
|
|
38
|
+
activate = date explicitly actioned by click / Enter / Space
|
|
39
|
+
value = selected date
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Examples:
|
|
43
|
+
|
|
44
|
+
- changing the month select changes `display`, not `value`;
|
|
45
|
+
- pressing ArrowRight changes `focus`, not `value`;
|
|
46
|
+
- pressing Enter emits `dateactivate`;
|
|
47
|
+
- in `selection="single"`, accepted activation also updates `value`;
|
|
48
|
+
- in `selection="none"`, activation is left entirely to the consumer.
|
|
49
|
+
|
|
50
|
+
This is what lets the same `<date-calendar>` power both a date picker and the mini-calendar beside `@lekoala/calendar`.
|
|
51
|
+
|
|
52
|
+
`highlightedRange` is a separate, strictly explanatory concept: it paints a band over rendered cells (`data-range-start` / `data-in-range` / `data-range-end`) without touching `display`, `focusedDate`, activation or `value`. The picker, an inline calendar or an app-owned controller can attach a range for display; the band is validated by `normalizeRange()` (ordered, never reordered) so the presentation never represents a range the model would contest. The business range (which may be temporarily inverted or in progress) stays in whichever controller owns it and is only forwarded to `highlightedRange` when it is displayable.
|
|
53
|
+
|
|
54
|
+
## 3. True month math vs presentation
|
|
55
|
+
|
|
56
|
+
`dates.getMonthWeeks()` returns the true civil weeks that cover a month: 4, 5 or 6 full rows.
|
|
57
|
+
|
|
58
|
+
It does not know whether the UI wants a stable height.
|
|
59
|
+
|
|
60
|
+
`<date-calendar fixed-weeks>` pads the rendered grid to six rows. That choice belongs to presentation, not date math.
|
|
61
|
+
|
|
62
|
+
This mirrors the existing `@lekoala/calendar` direction: surrounding UI can consume the same true month primitive and decide how to display it.
|
|
63
|
+
|
|
64
|
+
## 4. DOM ownership
|
|
65
|
+
|
|
66
|
+
`<date-calendar>` owns the grid structure, focus semantics and interactive cells. A consumer cannot replace the day `td`.
|
|
67
|
+
|
|
68
|
+
Two extension seams are intentionally narrow:
|
|
69
|
+
|
|
70
|
+
### `dateState(date, sourceState)`
|
|
71
|
+
|
|
72
|
+
Returns state/metadata for one day. The core reads only generic fields such as:
|
|
73
|
+
|
|
74
|
+
- `disabled`;
|
|
75
|
+
- `description`.
|
|
76
|
+
|
|
77
|
+
Everything else remains application metadata.
|
|
78
|
+
|
|
79
|
+
### `renderDay(date, state)`
|
|
80
|
+
|
|
81
|
+
Returns a `Node`, `DocumentFragment`, string or nothing. The result is appended inside a component-owned, non-interactive, `aria-hidden` container. That container name is an implementation detail: consumers style the nodes they return and keep their own class names.
|
|
82
|
+
|
|
83
|
+
Accessible meaning belongs in `state.description`, not in purely visual dots/badges.
|
|
84
|
+
|
|
85
|
+
This prevents rich decorations from breaking grid semantics or keyboard behavior.
|
|
86
|
+
|
|
87
|
+
## 5. Constraints have one resolution path
|
|
88
|
+
|
|
89
|
+
`calendar.getDateState(date)` composes:
|
|
90
|
+
|
|
91
|
+
1. remote/source state;
|
|
92
|
+
2. `dateState()` application state;
|
|
93
|
+
3. `min` / `max`;
|
|
94
|
+
4. `isDateDisabled()`.
|
|
95
|
+
|
|
96
|
+
`enabled: true` is the explicit exception seam: it can reopen a date disabled by generic/source rules, but never bypasses hard `min`/`max` bounds.
|
|
97
|
+
|
|
98
|
+
Both click activation and typed picker validation use this resolved state.
|
|
99
|
+
|
|
100
|
+
There should not be one "disabled" implementation for the grid and another for manual text input.
|
|
101
|
+
|
|
102
|
+
## 6. Async source contract
|
|
103
|
+
|
|
104
|
+
A source is either a function or an object with `load()`:
|
|
105
|
+
|
|
106
|
+
```js
|
|
107
|
+
load({ start, end }, { signal })
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The range is inclusive and covers the cells actually rendered for that month. With `fixed-weeks`, this can extend beyond the month itself.
|
|
111
|
+
|
|
112
|
+
Navigation cancels superseded loads using `AbortController`. Results are normalized to a map keyed by date.
|
|
113
|
+
|
|
114
|
+
A load reports three outcomes: `loaded`, `cancelled` and `failed`. A cancelled or failed load never authorizes a date: `ensureDate()` returns `false` and both cell activation and typed picker validation refuse the date until a later load confirms it. `dateloaderror` keeps observing the failure; the loader error is never swallowed silently on the validation path.
|
|
115
|
+
|
|
116
|
+
Supported response shapes:
|
|
117
|
+
|
|
118
|
+
```js
|
|
119
|
+
{ dates: { "2026-09-10": {...} } }
|
|
120
|
+
{ "2026-09-10": {...} }
|
|
121
|
+
[{ date: "2026-09-10", ...state }]
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
No server URL, query parameter names or backend schema are hard-coded into the core. `createFetchSource()` is only a convenience adapter.
|
|
125
|
+
|
|
126
|
+
## 7. Picker composition
|
|
127
|
+
|
|
128
|
+
`<date-picker>` enhances a direct child text input.
|
|
129
|
+
|
|
130
|
+
The visible input owns:
|
|
131
|
+
|
|
132
|
+
- label association;
|
|
133
|
+
- focus;
|
|
134
|
+
- typing;
|
|
135
|
+
- required state;
|
|
136
|
+
- validation UI;
|
|
137
|
+
- the localized display string.
|
|
138
|
+
|
|
139
|
+
A hidden input receives the original `name` and owns canonical form submission. This avoids Flatpickr's "pretty format means no editable input" tradeoff while preserving ordinary form submission.
|
|
140
|
+
|
|
141
|
+
The picker creates a `<date-calendar selection="none">` in a native manual popover. On `dateactivate`, it commits the canonical value, formats the visible input and closes the popup.
|
|
142
|
+
|
|
143
|
+
## 8. Popup responsibilities
|
|
144
|
+
|
|
145
|
+
Native Popover owns the top layer. `@lekoala/floating` owns coordinates and geometry updates.
|
|
146
|
+
|
|
147
|
+
The picker does not implement:
|
|
148
|
+
|
|
149
|
+
- custom portals;
|
|
150
|
+
- dropdown parent options;
|
|
151
|
+
- manual clipping calculations;
|
|
152
|
+
- z-index escalation APIs.
|
|
153
|
+
|
|
154
|
+
The popup remains in authored DOM, preserving inherited locale/theme/density context.
|
|
155
|
+
|
|
156
|
+
## 9. Range relationships
|
|
157
|
+
|
|
158
|
+
Two product shapes cover the range cases, and both keep range rules outside the classes:
|
|
159
|
+
|
|
160
|
+
`<date-picker range>` shares **one** calendar surface between two editable fields. Each input is owned by a `DateFieldController` (parsing, hidden ISO, commit/stale protection); the pair is driven by `DateRangeController` in `date-range.js`, which owns the active-endpoint transitions and the start/end state. `DatePickerElement` remains a shell: it coordinates DOM/popup/focus only. `value` is unavailable in range mode and `range` is the atomic pair API; inverted or incomplete business ranges are never forwarded to `highlightedRange`, which stays presentation-only.
|
|
161
|
+
|
|
162
|
+
`linkDateRange(start, end)` keeps the external relationship for two visually separate pickers:
|
|
163
|
+
|
|
164
|
+
```text
|
|
165
|
+
end.min = max(authorMin, start.value)
|
|
166
|
+
start.max = min(authorMax, end.value)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
It does not auto-correct values. If a new start date makes the current end invalid, the end input becomes invalid and the user decides what value replaces it.
|
|
170
|
+
|
|
171
|
+
## 10. Keep the class small
|
|
172
|
+
|
|
173
|
+
If `DateCalendarElement` or `DatePickerElement` starts absorbing pure rules, move them out.
|
|
174
|
+
|
|
175
|
+
Expected homes:
|
|
176
|
+
|
|
177
|
+
- date arithmetic → `date.js`;
|
|
178
|
+
- locale parsing/formatting → `intl.js`;
|
|
179
|
+
- state transitions → `calendar-model.js`;
|
|
180
|
+
- payload normalization/fetch adapter → `source.js`;
|
|
181
|
+
- start/end relationship → `date-range.js`.
|
|
182
|
+
|
|
183
|
+
Do not add a plugin system to solve a one-off rendering need.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Manual accessibility test pass
|
|
2
|
+
|
|
3
|
+
Automated Playwright covers interaction invariants; this checklist now distinguishes what the repo can re-check from what only a human with a real screen reader, keyboard or visual review can confirm.
|
|
4
|
+
|
|
5
|
+
Release decision (0.1.0): the human reader/OS pairings below are **deferred past 0.1** — the automated portion is the 0.1 gate, and the [Recommended manual test matrix](ACCESSIBILITY.md#recommended-manual-test-matrix) plus the Record table stay open until the passes are actually run and logged.
|
|
6
|
+
|
|
7
|
+
## Legend
|
|
8
|
+
|
|
9
|
+
- `[auto]`: can be verified in-repo with Playwright and DOM assertions.
|
|
10
|
+
- `[manual]`: requires a human AT or visual pass.
|
|
11
|
+
- `[split]`: behavior can be checked automatically here, but announcement or perception still needs a human pass. Leave these unchecked until the manual portion is done.
|
|
12
|
+
|
|
13
|
+
## Setup
|
|
14
|
+
|
|
15
|
+
- Engines: Chrome/Narrator (Windows), Firefox/NVDA, macOS/VoiceOver at least once.
|
|
16
|
+
- Contrast: run `color-contrast` review in DevTools on the demo plus a forced-colors spot check.
|
|
17
|
+
- Page under test: `demo/index.html` (uses `fr-BE` labels) and `test/fixtures/form-state.html`.
|
|
18
|
+
|
|
19
|
+
## Keyboard only
|
|
20
|
+
|
|
21
|
+
- [x] [auto] Tab reaches the input, the popup button and the month/year selects as native controls.
|
|
22
|
+
- [x] [auto] Inside an open popup, Tab enters the grid on exactly one `tabindex="0"` cell.
|
|
23
|
+
- [x] [auto] Arrow keys move by day, ArrowUp/ArrowDown by week; no date is selected while moving.
|
|
24
|
+
- [x] [auto] Home/End move to the first/last cell of the week.
|
|
25
|
+
- [x] [auto] PageUp/PageDown move month; Shift+PageUp/PageDown move year; focus follows across a month boundary.
|
|
26
|
+
- [x] [auto] Enter/Space activates and, in `selection="single"`, selects.
|
|
27
|
+
- [ ] [split] Disabled days stay focusable via arrow keys but never select; reason is announced (behavior covered automatically; announcement still manual).
|
|
28
|
+
- [x] [auto] Escape in the picker closes the popup and restores focus to the input; in a `<dialog>`, the second Escape closes the dialog.
|
|
29
|
+
- [x] [split] Month/year selects and prev/next buttons work without a pointer (control behavior covered automatically; manual keyboard/AT pass still open).
|
|
30
|
+
- [x] [manual] Links/callback-seam cases (U8): all demo cards operate without a pointing device.
|
|
31
|
+
|
|
32
|
+
## Screen reader
|
|
33
|
+
|
|
34
|
+
- [ ] [manual] The grid is announced as a table with full weekday names (`abbr` expands).
|
|
35
|
+
- [ ] [split] `aria-current="date"` marks today; `aria-selected` marks the selection (attributes present in DOM; announcement still manual).
|
|
36
|
+
- [ ] [split] Each cell's accessible name includes the full date and, when present, the `description` (ARIA label content is present in DOM; announcement still manual).
|
|
37
|
+
- [ ] [manual] Disabled cells announce "unavailable" distinctly.
|
|
38
|
+
- [ ] [split] The picker input announces its role (combobox), the popup trigger, and the format hint (attributes wired in DOM; announcement still manual).
|
|
39
|
+
- [ ] [split] The popup announces as a dialog with a label, and `aria-modal` stays absent (role/label/absence are wired in DOM; announcement still manual).
|
|
40
|
+
- [ ] [manual] The mini calendar + `calendar-view` demo announces both parts; activating a day announces the agenda navigation.
|
|
41
|
+
- [ ] [manual] Month/year navigation and source loading announcements are not clipped or repeated twice.
|
|
42
|
+
|
|
43
|
+
## Form & lifecycle
|
|
44
|
+
|
|
45
|
+
- [x] [auto] Typing a valid localized date updates the field validation state (no stale invalid message).
|
|
46
|
+
- [x] [split] Typing an impossible date produces an announced invalid message (native invalid state/message covered automatically; announcement still manual).
|
|
47
|
+
- [x] [manual] Forced-colors mode keeps selected/today/disabled distinguishable — including a selected day when `Highlight` resolves near-white (no white-on-white from `--dp-accent`; spot-checked with the forced-colors screenshot bundle).
|
|
48
|
+
|
|
49
|
+
## Automated pass
|
|
50
|
+
|
|
51
|
+
- Date: 2026-09-06.
|
|
52
|
+
- Engines: Chromium, Firefox, WebKit.
|
|
53
|
+
- Browser command run: `bun run test:browser:accessibility`.
|
|
54
|
+
- Result: 124 browser tests passed and 2 were skipped across the configured desktop projects; the forced-colors style assertion is Chromium-only.
|
|
55
|
+
- Forced-colors spot check: `bun run shot:forced-colors:important` reviewed manually after the selected-day forced-colors fix.
|
|
56
|
+
|
|
57
|
+
## Record
|
|
58
|
+
|
|
59
|
+
Run once per supported screen reader pairing, then add the date, the pair (OS + reader + browser), and any failures as issues. Still open for 0.1.0.
|
|
60
|
+
|
|
61
|
+
| Date | OS / reader / browser | Result | Regression issues |
|
|
62
|
+
| --- | --- | --- | --- |
|
|
63
|
+
| | | ✅ / ❌ | |
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Integration with @lekoala/calendar
|
|
2
|
+
|
|
3
|
+
The mini calendar is intentionally a consumer of the date-picker package, not a feature duplicated inside the scheduling engine.
|
|
4
|
+
|
|
5
|
+
The live integration lives in `demo/index.html` (card 6): a real `<calendar-view view="month">` is wired to the mini calendar. `@lekoala/calendar` (and its `temporal-polyfill` peer) are dev dependencies used by the demo only.
|
|
6
|
+
|
|
7
|
+
`@lekoala/calendar` already exposes the right navigation seam:
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
calendar.gotoDate("2026-09-10");
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
A minimal sidebar navigator therefore looks like this:
|
|
14
|
+
|
|
15
|
+
```html
|
|
16
|
+
<date-calendar id="mini"
|
|
17
|
+
selection="none"
|
|
18
|
+
fixed-weeks
|
|
19
|
+
show-week-numbers></date-calendar>
|
|
20
|
+
|
|
21
|
+
<calendar-view id="agenda"></calendar-view>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
const mini = document.querySelector("#mini");
|
|
26
|
+
const agenda = document.querySelector("#agenda");
|
|
27
|
+
|
|
28
|
+
mini.display = agenda.date.toString().slice(0, 7);
|
|
29
|
+
mini.focusedDate = agenda.date.toString();
|
|
30
|
+
|
|
31
|
+
mini.addEventListener("dateactivate", (event) => {
|
|
32
|
+
agenda.gotoDate(event.detail.date);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
agenda.addEventListener("calendar:datechange", (event) => {
|
|
36
|
+
const date = event.detail.date.toString();
|
|
37
|
+
mini.display = date.slice(0, 7);
|
|
38
|
+
mini.focusedDate = date;
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Notice what is missing: the mini calendar does not need a selected value. The agenda anchor is application/calendar state, not form selection state.
|
|
43
|
+
|
|
44
|
+
## Active week / availability decoration
|
|
45
|
+
|
|
46
|
+
Application state can decorate the mini calendar without changing its navigation semantics:
|
|
47
|
+
|
|
48
|
+
```js
|
|
49
|
+
mini.dateState = (date, sourceState) => ({
|
|
50
|
+
...sourceState,
|
|
51
|
+
description: isBookable(date)
|
|
52
|
+
? "Bookable; activate to navigate"
|
|
53
|
+
: "Closed; activate to navigate"
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
mini.renderDay = (date, state) => state.available ? "•" : "";
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
A closed day can still navigate an administrative agenda. Do not set `disabled` merely because booking is unavailable unless activation itself is forbidden for this particular calendar.
|
|
60
|
+
|
|
61
|
+
That distinction mirrors the showcase rule: navigation policy and booking policy are different concerns.
|
|
62
|
+
|
|
63
|
+
## Shared month math
|
|
64
|
+
|
|
65
|
+
Both packages expose month-week derivation. They stay **deliberately duplicated**, each owning its own contract:
|
|
66
|
+
|
|
67
|
+
- `@lekoala/calendar` owns Temporal-based math (`Temporal.PlainDate`).
|
|
68
|
+
- `@lekoala/date-picker` owns civil string math (`dates.getMonthWeeks()`, `YYYY-MM-DD`).
|
|
69
|
+
|
|
70
|
+
This keeps the two packages dependency-free from each other: a circular dependency between a picker and a scheduler would cost more than a few duplicated lines. `dates.getMonthWeeks()` keeps the scheduler's important contract (full civil weeks, configurable first day, 4–6 rows, no forced padding) so both stay interchangeable for the mini-calendar seam. A future shared `@lekoala/date-math` package is possible, but only as a separate proposal; it must not introduce a circular dependency.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Decisions
|
|
2
|
+
|
|
3
|
+
Frozen contracts for the `0.1.0` surface. A change to a decision below is a breaking change and needs a proposal, not an opportunistic edit.
|
|
4
|
+
|
|
5
|
+
## D1 — Calendar events are flat, verb-named
|
|
6
|
+
|
|
7
|
+
Never merged with the `value` concept (`dateactivate` is activation, not `value`).
|
|
8
|
+
|
|
9
|
+
- `date-calendar`: `dateactivate`, `datechange`, `dateinvalid`, `displaychange`, `dateloadstart`, `dateloadend`, `dateloaderror`.
|
|
10
|
+
- `date-picker`: `valuechange`, `open`, `close`.
|
|
11
|
+
|
|
12
|
+
No `dp:` namespace prefix on element events, even though `@lekoala/combobox` namespaces its events (`combobox:*`).
|
|
13
|
+
|
|
14
|
+
Rationale: the flat names describe a distinct concept (`dateactivate`), never collide with native input events, and are already wired through the docs, demos and tests. A namespace is not worth the churn for this package.
|
|
15
|
+
|
|
16
|
+
## D2 — `selection="none"` is the final spelling
|
|
17
|
+
|
|
18
|
+
The navigation-only calendar keeps `selection="none"`.
|
|
19
|
+
|
|
20
|
+
`selection` remains a value attribute (`single` | `none`); it is how the mini calendar speaks the navigation contract: accepted activation dispatches `dateactivate`, `value` is untouched.
|
|
21
|
+
|
|
22
|
+
## D3 — CSS: `--dp-*` is public, `.dp-*` is not
|
|
23
|
+
|
|
24
|
+
> `--dp-*` is the public styling namespace. `.dp-*` identifies component-owned DOM but is not a public styling API unless explicitly documented.
|
|
25
|
+
|
|
26
|
+
- The namespace prefix is `dp`, final. There are no `--lko-dp-*` or `.lko-dp-*` classes. A shared `--lko-*` token space is reserved for tokens actually shared between LeKoala packages, not as a per-component prefix.
|
|
27
|
+
- The `--dp-*` custom properties (background, foreground, accent, border, radius, day-size, gap, …) are the **public customization surface**. They are listed in `custom-elements.json` and are stable.
|
|
28
|
+
- Every `.dp-*` class is **internal DOM** and may change, move or disappear without a semver bump. This includes `.dp-day`, `.dp-grid`, `.dp-picker-panel` and the day-extra container.
|
|
29
|
+
- `renderDay()` is a public extension seam, but the name of the container its output is placed into is **not** promised. Consumers style their own returned nodes and assign their own class names.
|
|
30
|
+
- Demos, tests and internal code may reference `.dp-*` classes; their use there does not turn those classes into public API.
|
|
31
|
+
|
|
32
|
+
Context: DOM-backed styling hooks become API the moment they are documented — every internal rename then breaks consumers. We prefer to expose hooks (`--dp-*`, `renderDay()`, events) and keep the rendered anatomy free to evolve.
|
|
33
|
+
|
|
34
|
+
## D4 — `<date-picker>` opens on focus; `open` is read-only
|
|
35
|
+
|
|
36
|
+
- The picker opens its popover when the input receives focus (`open-on-focus`, default `true`; set `open-on-focus="false"` to opt out).
|
|
37
|
+
- Opening on focus **never steals keyboard focus**: the user can start typing immediately. The grid takes focus only on ArrowDown, on the calendar trigger, or after the user moves the pointer into the popup.
|
|
38
|
+
- `show()` / `hide()` are the control surface. `open` is a **read-only getter** reflecting the popover. There is no `open` attribute: the popover element is the single machine of truth and a second state machine would only drift.
|
|
39
|
+
- `readonly` mirrors native semantics: the field is non-editable, the value is still submitted, and the picker cannot change it — the calendar trigger is disabled. `disabled` additionally unsubmits the field. `readonly` is never used to mean "no typing, but calendar selection allowed".
|
|
40
|
+
- Clearing the editable input clears the canonical value (the component renders no internal clear button). Form resets follow the native `defaultValue` contract. This matches the Open UI datepicker `clear` concept: clearing is a text-editing action, not a component-owned affordance.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Flatpickr / Formidable migration map
|
|
2
|
+
|
|
3
|
+
This prototype was scoped against the existing `FlatpickrInput` wrapper rather than against Flatpickr's full feature list.
|
|
4
|
+
|
|
5
|
+
## Directly covered
|
|
6
|
+
|
|
7
|
+
| Existing need | New direction |
|
|
8
|
+
|---------------------------|------------------------------------------------------------|
|
|
9
|
+
| canonical `Y-m-d` value | `date-picker.value` / hidden form value `YYYY-MM-DD` |
|
|
10
|
+
| localized pretty display | `Intl` numeric adapter while staying editable |
|
|
11
|
+
| `minDate` / `maxDate` | `min` / `max` |
|
|
12
|
+
| disable callback | `isDateDisabled(date)` |
|
|
13
|
+
| dynamic enable/disable | source state + `dateState()` |
|
|
14
|
+
| `configUrl` on navigation | abortable `source.load({start,end}, {signal})` |
|
|
15
|
+
| bars/events on dates | source metadata + `renderDay()` + accessible `description` |
|
|
16
|
+
| inline calendar | `<date-calendar>` |
|
|
17
|
+
| previous/next month | calendar methods/buttons |
|
|
18
|
+
| direct month/year choice | native month/year selects |
|
|
19
|
+
| start/end relation | `linkDateRange(start, end)` |
|
|
20
|
+
| locale | `Intl` locale + small control-message dictionaries |
|
|
21
|
+
| value-changed hook | `valuechange` / native input+change events |
|
|
22
|
+
|
|
23
|
+
## The display-format change is intentional
|
|
24
|
+
|
|
25
|
+
The old wrapper has to choose between a rich localized display and manual editing because Flatpickr cannot reliably parse arbitrary long localized output.
|
|
26
|
+
|
|
27
|
+
The new default is deliberately less decorative and more useful:
|
|
28
|
+
|
|
29
|
+
```text
|
|
30
|
+
fr-BE: 06/09/2026
|
|
31
|
+
en-US: 09/06/2026
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The pattern is generated with `Intl.DateTimeFormat(...).formatToParts()`, so the same adapter can format and parse it.
|
|
35
|
+
|
|
36
|
+
Long dates such as `6 septembre 2026` remain presentation content, not the editing format of the field.
|
|
37
|
+
|
|
38
|
+
## Dynamic source payload
|
|
39
|
+
|
|
40
|
+
Legacy payload:
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"enable": ["2025-06-07"],
|
|
45
|
+
"events": [
|
|
46
|
+
"2025-06-07",
|
|
47
|
+
{ "date": "2025-06-09", "class": "bg-danger" }
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Suggested new payload:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"dates": {
|
|
57
|
+
"2025-06-07": {
|
|
58
|
+
"disabled": false,
|
|
59
|
+
"description": "Available"
|
|
60
|
+
},
|
|
61
|
+
"2025-06-09": {
|
|
62
|
+
"description": "Limited availability",
|
|
63
|
+
"status": "danger"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The backend returns meaning, not CSS class names. The consumer decides how `status` is rendered.
|
|
70
|
+
|
|
71
|
+
## Explicit enable-overrides-disable
|
|
72
|
+
|
|
73
|
+
If an application currently relies on a remote `enable` list overriding a generic disabled rule (for example: weekends disabled except one exceptional Saturday), implement that policy in one `dateState` resolver:
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
calendar.isDateDisabled = (date) => isWeekend(date);
|
|
77
|
+
// Source state { enabled: true } is an explicit exception to the generic
|
|
78
|
+
// weekend rule. Hard min/max bounds still win.
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The prototype promotes this legacy behavior into a documented generic field: `enabled: true` reopens a date disabled by `isDateDisabled()` or ordinary source state, but never bypasses hard `min`/`max` bounds.
|
|
82
|
+
|
|
83
|
+
## Not migrated into this component
|
|
84
|
+
|
|
85
|
+
The existing wrapper also exposes Flatpickr features that should not be carried into the date picker core:
|
|
86
|
+
|
|
87
|
+
- `enableTime`;
|
|
88
|
+
- `noCalendar` / time-only;
|
|
89
|
+
- confirm-date plugin;
|
|
90
|
+
- month-as-value selection plugin.
|
|
91
|
+
|
|
92
|
+
Those are separate controls/use cases. Adding them here would immediately mix civil-date selection with time and alternate value models.
|
package/docs/ROADMAP.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Roadmap
|
|
2
|
+
|
|
3
|
+
## P0 — prove the contracts
|
|
4
|
+
|
|
5
|
+
- [x] canonical civil-date helpers;
|
|
6
|
+
- [x] true month weeks;
|
|
7
|
+
- [x] `value / display / focused` split;
|
|
8
|
+
- [x] inline `<date-calendar>`;
|
|
9
|
+
- [x] navigation-only selection mode;
|
|
10
|
+
- [x] month/year controls;
|
|
11
|
+
- [x] APG-style grid keyboard interaction;
|
|
12
|
+
- [x] localized editable adapter;
|
|
13
|
+
- [x] `<date-picker>` Popover + floating composition;
|
|
14
|
+
- [x] source contract with AbortController;
|
|
15
|
+
- [x] day state + safe decoration seam;
|
|
16
|
+
- [x] start/end linking primitive;
|
|
17
|
+
- [x] unit/browser test skeleton;
|
|
18
|
+
- [x] demos and architecture docs.
|
|
19
|
+
|
|
20
|
+
## P1 — hardened before 0.1 (shipped in 0.1.0)
|
|
21
|
+
|
|
22
|
+
- [x] run TypeScript 7/Biome/Bun checks in the actual repo;
|
|
23
|
+
- [x] run Playwright on all engines;
|
|
24
|
+
- [x] manual AT checklist and record ([docs/AT-CHECKLIST.md](AT-CHECKLIST.md)); the automated portion is the 0.1 gate, the human reader/OS rows track as a post-0.1 recommendation;
|
|
25
|
+
- [x] add a real `@lekoala/calendar` mini-calendar integration demo;
|
|
26
|
+
- [x] validate Popover behavior inside `<dialog>` and nested app surfaces;
|
|
27
|
+
- [x] test form reset, disabled toggles and dynamic name/form ownership;
|
|
28
|
+
- [x] decide exact event naming — flat `dateactivate` etc. ([DECISIONS.md](DECISIONS.md), D1);
|
|
29
|
+
- [x] decide whether `selection="none"` is the final public spelling — yes (D2);
|
|
30
|
+
- [x] decide public styling tokens/class names — `--dp-*` public, `.dp-*` internal (D3);
|
|
31
|
+
- [x] decide the shared-range picker shape — `<date-picker range>` + `linkDateRange` (U9/U5);
|
|
32
|
+
- [x] decide the popup modality on touch surfaces — stays non-modal in 0.1 ([ACCESSIBILITY.md](ACCESSIBILITY.md));
|
|
33
|
+
- [x] add custom-elements manifest once names stabilize — `custom-elements.json`;
|
|
34
|
+
- [x] add package/generated-artifact checks like `@lekoala/combobox`;
|
|
35
|
+
- [x] commit `bun.lock` and freeze installs in CI.
|
|
36
|
+
|
|
37
|
+
## P2 — real-world migration
|
|
38
|
+
|
|
39
|
+
Use Formidable/Flatpickr cases as migration fixtures:
|
|
40
|
+
|
|
41
|
+
- remote enable/disable payload;
|
|
42
|
+
- date markers/events → metadata/decoration;
|
|
43
|
+
- French/Spanish locale → shipped locale message dictionaries (`ar`, `de`, `en`, `es`, `fr`, `hi`, `id`, `it`, `ja`, `ko`, `nl`, `pl`, `pt-BR`, `pt-PT`, `ru`, `tr`, `zh-CN`);
|
|
44
|
+
- initial canonical values;
|
|
45
|
+
- inline calendar;
|
|
46
|
+
- linked start/end fields;
|
|
47
|
+
- month navigation source refresh.
|
|
48
|
+
|
|
49
|
+
## Explicitly deferred
|
|
50
|
+
|
|
51
|
+
These should require a separate proposal, not opportunistic growth:
|
|
52
|
+
|
|
53
|
+
- time picker / datetime picker;
|
|
54
|
+
- single-grid visual **selection** interaction (the band itself is core presentation via `calendar.highlightedRange`);
|
|
55
|
+
- month-as-value picker;
|
|
56
|
+
- multi-date;
|
|
57
|
+
- recurrence;
|
|
58
|
+
- timezone-aware values;
|
|
59
|
+
- natural-language parsing;
|
|
60
|
+
- plugin registry;
|
|
61
|
+
- multiple visible months;
|
|
62
|
+
- swipe gestures;
|
|
63
|
+
- full calendar/scheduler features.
|
|
64
|
+
|
|
65
|
+
## Open UI nomenclature reference
|
|
66
|
+
|
|
67
|
+
Cross-check our coverage against the Open UI datepicker research: <https://open-ui.org/components/datepicker.research/> — a cross-design-system vocabulary inventory for date pickers (2023, no longer maintained). It is a term checklist, not a behavior specification; normative behavior stays with the WAI-ARIA APG grid/dialog patterns and the HTML spec. Use these terms as-is in proposals instead of re-defining them.
|
|
68
|
+
|
|
69
|
+
| Open UI concept | Status | Note |
|
|
70
|
+
| --- | --- | --- |
|
|
71
|
+
| `open` | core | `show()` / `hide()` with a read-only `open` getter (D4). |
|
|
72
|
+
| `navigation` | core | month/year selects plus prev/next buttons (P0). |
|
|
73
|
+
| `selected` | core | `aria-selected` mapped from `value`. |
|
|
74
|
+
| `disabled` | core | native disabled state on the component and its controls. |
|
|
75
|
+
| `disabled dates` | core | `min` / `max`, `isDateDisabled`, source state (P0). |
|
|
76
|
+
| `required` | core | forwarded to the visible native input. |
|
|
77
|
+
| `manual input` | core | editable native field with localized parsing (U1). |
|
|
78
|
+
| `format` | core | `intl.js`; format hint exposed via `aria-describedby`. |
|
|
79
|
+
| `error` | core | distinct `setCustomValidity` messages for invalid vs unavailable dates. |
|
|
80
|
+
| `readonly` / `disabled` | core | native semantics kept separate (D4). |
|
|
81
|
+
| `default` | core | form reset via `defaultValue` (D4). |
|
|
82
|
+
| `clear` | contract boundary | no internal clear button; clearing happens by editing the text input (D4). |
|
|
83
|
+
| `custom cells` | contract boundary | `renderDay()` only; cells stay component-owned (D3). |
|
|
84
|
+
| `weeknumber` | contract boundary | `show-week-numbers` (U3). |
|
|
85
|
+
| `custom weeks` | no | only the `first-day` attribute; no custom week rendering. |
|
|
86
|
+
| `range` | core | `<date-picker range>` two-field shared surface (U9); `linkDateRange()` stays external (U5). |
|
|
87
|
+
| `range selected` | deferred | selection interaction needs its own proposal; `highlightedRange` covers band presentation only. |
|
|
88
|
+
| `range presets` | deferred | application-owned. |
|
|
89
|
+
| `date-time picker` | deferred | time/datetime is explicitly out of scope. |
|
|
90
|
+
| `side by side` | deferred | maps to "multiple visible months". |
|
|
91
|
+
| `footer` | deferred | not part of the grid contract. |
|
|
92
|
+
| `shape`, `size`, `compact`, `subtle`, `light`, `borderless`, `no icon` | app-owned | styling via public `--dp-*` tokens (D3), not a component API. |
|
|
93
|
+
|
|
94
|
+
## Related prior art: Duet Date Picker
|
|
95
|
+
|
|
96
|
+
<https://github.com/duetds/date-picker> — a Web Components date picker by the Duet Design System (MIT, Stencil, ~10 kb). The repository was **archived in 2024**; treat it as a source of observed behaviors, not maintained API reference. It validates several of our choices and highlights points we must decide:
|
|
97
|
+
|
|
98
|
+
- `isDateDisabled` (same seam name as ours), hidden-input ISO submission, `formatLongDate`-style cell labels ("17 November 2020"), month/year announced on change, and a real `table[role="grid"]` with a single roving tab stop — all mirrored in this package.
|
|
99
|
+
- **Differs from us on the canonical model**: its `dateAdapter` and events work with `Date` objects (`valueAsDate`). That is the exact `Date`-object tradeoff our canonical `YYYY-MM-DD` strings avoid.
|
|
100
|
+
- **Differs on modality**: it ships a modal dialog with overlay and an explicit close button, while our popup intentionally stays non-modal (`aria-modal` absent). `docs/ACCESSIBILITY.md` still holds the open question of going modal on small/touch surfaces.
|
|
101
|
+
- **Tab wrap-around**: Duet cycles focus back to the first focusable element inside the dialog. Our non-modal popup lets Tab leave the picker — decided, documented in `docs/ACCESSIBILITY.md`.
|
|
102
|
+
- **Opening focus target**: Duet moves focus to the first (month) select on open; we focus the grid — a deliberate divergence (ArrowDown / trigger button enter the grid).
|
|
103
|
+
- **Deferred alignments**: Duet's touch gestures and modified mobile interface map to our explicit "swipe gestures" deferral.
|
|
104
|
+
|
|
105
|
+
Resolved without further work: popover flipping near an edge is handled by `@lekoala/floating`, so no `direction`-style prop is needed; the open-popover `Tab` contract is decide-and-document only (see above), not code.
|
package/docs/TESTING.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Testing
|
|
2
|
+
|
|
3
|
+
## Fast loop
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
bun run test:syntax
|
|
7
|
+
bun run typecheck
|
|
8
|
+
bun run test
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Pure date math, locale parsing and model transitions belong in `test/unit` and must remain DOM-free.
|
|
12
|
+
|
|
13
|
+
## Browser loop
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bun run test:browser
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Chromium covers the fast interaction loop. Before release:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
bun run test:browser:all
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
runs Chromium, Firefox and WebKit.
|
|
26
|
+
|
|
27
|
+
For the accessibility-adjacent browser subset used by the AT checklist:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
bun run test:browser:accessibility
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
This runs the focused picker/calendar/focus/form/surface specs across the configured desktop projects, including the forced-colors regression check in `calendar.spec.js`.
|
|
34
|
+
The forced-colors style assertion follows the Chromium capture pipeline, so it is intentionally skipped on Firefox and WebKit.
|
|
35
|
+
|
|
36
|
+
## What belongs in browser tests
|
|
37
|
+
|
|
38
|
+
Browser tests should cover behavior that cannot be trusted to a DOM shim:
|
|
39
|
+
|
|
40
|
+
- Popover open/close;
|
|
41
|
+
- focus movement and roving tabindex;
|
|
42
|
+
- real keyboard events;
|
|
43
|
+
- month/year selects;
|
|
44
|
+
- form submission shape;
|
|
45
|
+
- floating placement staying in the viewport;
|
|
46
|
+
- outside pointer close;
|
|
47
|
+
- source cancellation;
|
|
48
|
+
- range validation;
|
|
49
|
+
- forced-colors/zoom smoke cases.
|
|
50
|
+
|
|
51
|
+
## Core invariants
|
|
52
|
+
|
|
53
|
+
Keep explicit tests for these regressions:
|
|
54
|
+
|
|
55
|
+
1. `value`, `display` and `focusedDate` are independent.
|
|
56
|
+
2. Month/year navigation never selects.
|
|
57
|
+
3. Arrow navigation never selects.
|
|
58
|
+
4. `dateactivate` is the only user activation seam.
|
|
59
|
+
5. `selection="none"` never mutates `value`.
|
|
60
|
+
6. `getMonthWeeks()` never pads.
|
|
61
|
+
7. `fixed-weeks` renders 42 day cells.
|
|
62
|
+
8. Outside-month cells can activate.
|
|
63
|
+
9. Source navigation aborts stale loads.
|
|
64
|
+
10. Typed and clicked dates resolve the same disabled state.
|
|
65
|
+
11. Start/end linkage never silently changes sibling values.
|
|
66
|
+
12. Exactly one grid cell has `tabindex="0"`.
|
|
67
|
+
13. A failed or cancelled source load never authorizes activation or validation.
|
|
68
|
+
|
|
69
|
+
## CI
|
|
70
|
+
|
|
71
|
+
The included workflow uses Bun 1.4.2 and runs static checks/unit tests before Playwright.
|
|
72
|
+
|
|
73
|
+
`bun.lock` is committed. Keep installs frozen in CI (`bun install --frozen-lockfile`) so dependency resolution stays deterministic. Regenerate the lock with `bun install` when the manifest changes and commit it in the same change.
|