@lekoala/date-picker 0.4.0 → 0.5.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/README.md +281 -265
- package/custom-elements.json +10 -0
- package/dist/date-picker.css +20 -15
- package/dist/date-picker.js +199 -115
- package/dist/date-picker.min.css +1 -1
- package/dist/date-picker.min.js +13 -13
- package/dist/date-picker.standalone.min.js +34 -29
- package/dist/types/control-classes.d.ts +30 -0
- package/dist/types/control-classes.d.ts.map +1 -0
- package/dist/types/date-calendar.d.ts +15 -0
- package/dist/types/date-calendar.d.ts.map +1 -1
- package/dist/types/date-picker.d.ts +15 -0
- package/dist/types/date-picker.d.ts.map +1 -1
- package/dist/types/date.d.ts +4 -2
- package/dist/types/date.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/intl.d.ts.map +1 -1
- package/dist/types/source.d.ts +1 -0
- package/dist/types/source.d.ts.map +1 -1
- package/docs/API.md +430 -408
- package/docs/DECISIONS.md +68 -62
- package/docs/USE_CASES.md +156 -144
- package/package.json +3 -3
- package/src/control-classes.js +50 -0
- package/src/date-calendar.js +47 -16
- package/src/date-field.js +1 -1
- package/src/date-picker.css +20 -15
- package/src/date-picker.js +47 -46
- package/src/date.js +8 -3
- package/src/index.js +1 -0
- package/src/intl.js +39 -10
- package/src/source.js +14 -4
package/README.md
CHANGED
|
@@ -1,265 +1,281 @@
|
|
|
1
|
-
# @lekoala/date-picker
|
|
2
|
-
|
|
3
|
-
Small, native-first date picker and inline calendar primitives.
|
|
4
|
-
|
|
5
|
-
> The interaction contract, event names, public styling tokens and the component API are frozen — see [docs/DECISIONS.md](docs/DECISIONS.md). Internal DOM and class names are `.dp-*` implementation details and stay private.
|
|
6
|
-
|
|
7
|
-
This project fills the gap between a normal editable date field and a full scheduling calendar. A plain date input remains the right answer for simple CMS/editing forms. This component becomes useful when a date has context: availability, disabled days, annotations, remote state, start/end relationships, or a mini calendar that navigates another view.
|
|
8
|
-
|
|
9
|
-
## The two primitives
|
|
10
|
-
|
|
11
|
-
### `<date-calendar>`
|
|
12
|
-
|
|
13
|
-
An inline calendar with real month/year navigation, keyboard support and three distinct pieces of state:
|
|
14
|
-
|
|
15
|
-
- `display` — the month being rendered (`YYYY-MM`);
|
|
16
|
-
- `focusedDate` — the roving keyboard target (`YYYY-MM-DD`);
|
|
17
|
-
- `value` — the selected date (`YYYY-MM-DD`, optional).
|
|
18
|
-
|
|
19
|
-
`display` and `focusedDate` are coupled during navigation/display: changing the displayed month re-clamps the focused day into that month, because the roving-tabbable grid cell must stay visible. `value` and activation stay independent of both.
|
|
20
|
-
|
|
21
|
-
User activation is exposed separately as `dateactivate`. This is what makes the same calendar useful as a selector **and** as a mini navigator.
|
|
22
|
-
|
|
23
|
-
```html
|
|
24
|
-
<date-calendar value="2026-09-10" fixed-weeks></date-calendar>
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
For a navigator that should not own a selection:
|
|
28
|
-
|
|
29
|
-
```html
|
|
30
|
-
<date-calendar id="mini" selection="none" fixed-weeks show-week-numbers></date-calendar>
|
|
31
|
-
<script type="module">
|
|
32
|
-
const mini = document.querySelector("#mini");
|
|
33
|
-
mini.addEventListener("dateactivate", (event) => {
|
|
34
|
-
agenda.gotoDate(event.detail.date);
|
|
35
|
-
});
|
|
36
|
-
</script>
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
### `<date-picker>`
|
|
40
|
-
|
|
41
|
-
A composition around a real editable text input. The user sees and edits a short locale-aware numeric date while form submission stays canonical ISO.
|
|
42
|
-
|
|
43
|
-
**Use `<date-picker>` when choosing a date benefits from calendar context.** For a simple editable date field, a normal input with formatting and validation is the right tool instead.
|
|
44
|
-
|
|
45
|
-
```html
|
|
46
|
-
<date-picker value="2026-09-10" locale="fr-BE">
|
|
47
|
-
<input name="appointment_date" required>
|
|
48
|
-
</date-picker>
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
With JavaScript enabled the input displays `10/09/2026`; the hidden submitted value remains `2026-09-10`. The visible input keeps the label, focus, required state and validation UI.
|
|
52
|
-
|
|
53
|
-
By default the calendar opens when the field receives focus — without stealing the keyboard focus, so typing works immediately. `open-on-focus="false"` makes the calendar opt-in (trigger button or `ArrowDown`). Under `readonly` the value stays editable-by-calendar-off but is still submitted; under `disabled` the field is also unsubmitted.
|
|
54
|
-
|
|
55
|
-
## Start/end ranges
|
|
56
|
-
|
|
57
|
-
Two product shapes cover ranges.
|
|
58
|
-
|
|
59
|
-
**One shared calendar surface** — two editable fields wired to a single picker popup:
|
|
60
|
-
|
|
61
|
-
```html
|
|
62
|
-
<date-picker range>
|
|
63
|
-
<input data-range-start name="arrival" aria-label="Arrival">
|
|
64
|
-
<input data-range-end name="departure" aria-label="Departure">
|
|
65
|
-
</date-picker>
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
Opening from a field targets that bound; picking the first date moves the active endpoint to the other bound without closing, and the second pick completes and closes. `picker.range` is the atomic `{ start, end }` API (single `rangechange` event).
|
|
69
|
-
|
|
70
|
-
**Two independent pickers** — a small external link for visually separate fields:
|
|
71
|
-
|
|
72
|
-
```js
|
|
73
|
-
import { linkDateRange } from "@lekoala/date-picker";
|
|
74
|
-
|
|
75
|
-
const cleanup = linkDateRange(startPicker, endPicker);
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
The relationship applies `end >= start` and `start <= end` as effective bounds. Neither picker knows about its sibling.
|
|
79
|
-
|
|
80
|
-
## Date + time
|
|
81
|
-
|
|
82
|
-
A date pairs with one or two native time companions. The picker owns the civil date; the time inputs own their civil time values:
|
|
83
|
-
|
|
84
|
-
```html
|
|
85
|
-
<date-picker>
|
|
86
|
-
<input name="appointment[date]">
|
|
87
|
-
<input type="time" data-time-start name="appointment[time]">
|
|
88
|
-
</date-picker>
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
```text
|
|
92
|
-
appointment[date] = 2026-09-10
|
|
93
|
-
appointment[time] = 09:30
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
The application/server combines these civil values and applies the relevant timezone when a datetime/instant is needed. The picker never constructs a combined datetime value.
|
|
97
|
-
|
|
98
|
-
## Availability and per-day metadata
|
|
99
|
-
|
|
100
|
-
The calendar accepts a source function or `{ load() }` object with the same abortable shape used elsewhere in LeKoala components:
|
|
101
|
-
|
|
102
|
-
```js
|
|
103
|
-
calendar.source = async ({ start, end }, { signal }) => {
|
|
104
|
-
const response = await fetch(`/availability?start=${start}&end=${end}`, { signal });
|
|
105
|
-
return response.json();
|
|
106
|
-
};
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
Accepted payloads are deliberately simple:
|
|
110
|
-
|
|
111
|
-
```js
|
|
112
|
-
{
|
|
113
|
-
dates: {
|
|
114
|
-
"2026-09-10": {
|
|
115
|
-
enabled: true,
|
|
116
|
-
description: "Available morning and afternoon",
|
|
117
|
-
morning: true,
|
|
118
|
-
afternoon: true
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
Business metadata is not interpreted by the core. Consumers can add visual content without replacing the grid cell or its accessibility semantics:
|
|
125
|
-
|
|
126
|
-
```js
|
|
127
|
-
calendar.renderDay = (_date, state) => {
|
|
128
|
-
if (!state.morning && !state.afternoon) return "";
|
|
129
|
-
return `${state.morning ? "●" : "○"}${state.afternoon ? "●" : "○"}`;
|
|
130
|
-
};
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
## Date math
|
|
134
|
-
|
|
135
|
-
The public value model is intentionally boring: canonical `YYYY-MM-DD` strings. There is no public `Date`, timestamp, timezone or `Temporal` dependency.
|
|
136
|
-
|
|
137
|
-
Pure civil-date helpers are exported under `dates`:
|
|
138
|
-
|
|
139
|
-
```js
|
|
140
|
-
import { dates } from "@lekoala/date-picker";
|
|
141
|
-
|
|
142
|
-
const weeks = dates.getMonthWeeks("2026-09-03", { firstDay: 1 });
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
`getMonthWeeks()` returns the true 4–6 full civil weeks covering the month. It never pads to six rows. `fixed-weeks` is a rendering choice made by `<date-calendar>`.
|
|
146
|
-
|
|
147
|
-
## Accessibility baseline
|
|
148
|
-
|
|
149
|
-
The calendar follows the WAI-ARIA date-picker/grid interaction model:
|
|
150
|
-
|
|
151
|
-
- semantic `table[role="grid"]`;
|
|
152
|
-
- one roving `tabindex="0"` grid cell;
|
|
153
|
-
- arrow keys move by day/week;
|
|
154
|
-
- Home/End move to week boundaries;
|
|
155
|
-
- PageUp/PageDown move by month;
|
|
156
|
-
- Shift+PageUp/PageDown move by year;
|
|
157
|
-
- Enter/Space activate a date;
|
|
158
|
-
- keyboard movement never implicitly selects;
|
|
159
|
-
- `aria-selected` identifies the selected date;
|
|
160
|
-
- `aria-current="date"` identifies today;
|
|
161
|
-
- abbreviated weekday headers keep full `abbr` names.
|
|
162
|
-
|
|
163
|
-
The picker popup uses native Popover for the top layer and `@lekoala/floating` for geometry. It has `role="dialog"`, but it is intentionally **not** marked `aria-modal` because the current implementation does not inert the rest of the document.
|
|
164
|
-
|
|
165
|
-
The calendar trigger is a real button painted inside the field's own box (like the native `input[type=time]` indicator): the field reserves its place with `padding-inline-end`, keeps its own border and background, and its focus ring wraps the trigger too. The trigger draws no chrome of its own and shows a small inner ring when focused.
|
|
166
|
-
|
|
167
|
-
Fields, header controls and days share an inner outline that survives forced colors. Customize its color with `--dp-focus` (default `var(--dp-accent)`) and thickness with `--dp-focus-width` (default `2px`) on `date-picker` and `date-calendar`. The trigger keeps a `-5px` inset around its icon; selected days and range endpoints use `--dp-accent-fg` for contrast. Fields and header controls now show this solid outline instead of the previous soft halo.
|
|
168
|
-
|
|
169
|
-
When the popup opens on the field's focus, grid navigation is reachable via `ArrowDown` (or the trigger button) so the focus-restore/Escape flow stays predictable. Closing returns focus to the control that opened the popover: the field when it opened by focus, the trigger when it was activated. In range mode focus follows the active bound instead, because a first pick moves the workflow to the other bound.
|
|
170
|
-
|
|
171
|
-
See [docs/ACCESSIBILITY.md](docs/ACCESSIBILITY.md).
|
|
172
|
-
|
|
173
|
-
## Install / develop
|
|
174
|
-
|
|
175
|
-
```bash
|
|
176
|
-
bun install
|
|
177
|
-
bun run dev
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
If a previous `bun run dev` is still holding port `4859`, clear it with `bun run dev:kill` (or restart in one go with `bun run dev:restart`).
|
|
181
|
-
|
|
182
|
-
Tooling is aligned with the current LeKoala component repos:
|
|
183
|
-
|
|
184
|
-
- Bun 1.4.3
|
|
185
|
-
- TypeScript 7.0.2 (`checkJs` + declaration emit)
|
|
186
|
-
- Biome 2.5.15
|
|
187
|
-
- Playwright 1.64.0
|
|
188
|
-
- `@lekoala/floating ^0.2.0`
|
|
189
|
-
|
|
190
|
-
Useful commands:
|
|
191
|
-
|
|
192
|
-
```bash
|
|
193
|
-
bun run test
|
|
194
|
-
bun run typecheck
|
|
195
|
-
bun run lint
|
|
196
|
-
bun run build
|
|
197
|
-
bun run test:browser
|
|
198
|
-
bun run test:browser:all
|
|
199
|
-
bun run check:all
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
## Build outputs
|
|
203
|
-
|
|
204
|
-
`bun run build` produces:
|
|
205
|
-
|
|
206
|
-
```text
|
|
207
|
-
dist/date-picker.js
|
|
208
|
-
dist/date-picker.min.js
|
|
209
|
-
dist/date-picker.css
|
|
210
|
-
dist/date-picker.min.css
|
|
211
|
-
dist/date-picker.standalone.min.js
|
|
212
|
-
dist/types/**
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
The default ESM exports point at `src/`, matching the native-first development style of `@lekoala/combobox`. The classic build is file:// friendly; the standalone build additionally injects the component CSS.
|
|
216
|
-
|
|
217
|
-
## Scope
|
|
218
|
-
|
|
219
|
-
Included:
|
|
220
|
-
|
|
221
|
-
- inline single-date selection;
|
|
222
|
-
- mini-calendar/navigation-only use;
|
|
223
|
-
- month and year selection;
|
|
224
|
-
- previous/next navigation;
|
|
225
|
-
- editable localized input + canonical submitted value;
|
|
226
|
-
- min/max and custom disabled rules;
|
|
227
|
-
- presentation-only range band (`highlightedRange`);
|
|
228
|
-
- shared-calendar two-field range picker (`<date-picker range>`);
|
|
229
|
-
- linked start/end pickers (`linkDateRange`);
|
|
230
|
-
- native time companions (one or two `input[type=time]` beside the date);
|
|
231
|
-
- `Intl` month/year control order + `month-format="short"`;
|
|
232
|
-
- RTL through the inherited `dir` (no `isRTL` option);
|
|
233
|
-
- readable keyboard focus on disabled/selected days;
|
|
234
|
-
- compact-consumer CSS tokens (`--dp-header-control-size`, `--dp-day-radius`, …);
|
|
235
|
-
- async per-day state with cancellation;
|
|
236
|
-
- custom day decorations that cannot replace the accessible cell;
|
|
237
|
-
- fixed six-row presentation as an opt-in;
|
|
238
|
-
- ISO week numbers;
|
|
239
|
-
- keyboard and focus contracts.
|
|
240
|
-
|
|
241
|
-
Deliberately out of scope:
|
|
242
|
-
|
|
243
|
-
- custom time-picker UI and combined datetime values (only native `input[type=time]` companions are supported);
|
|
244
|
-
- timezones;
|
|
245
|
-
- in-place single-calendar range *selection interaction* (use the two-field picker);
|
|
246
|
-
- multi-date selection;
|
|
247
|
-
- recurrence;
|
|
248
|
-
- natural-language parsing;
|
|
249
|
-
- plugin registry;
|
|
250
|
-
- virtualized months;
|
|
251
|
-
- scheduling/time slots.
|
|
252
|
-
|
|
253
|
-
## Docs
|
|
254
|
-
|
|
255
|
-
- [Changelog](CHANGELOG.md)
|
|
256
|
-
- [Architecture](docs/ARCHITECTURE.md)
|
|
257
|
-
- [Decisions](docs/DECISIONS.md)
|
|
258
|
-
- [API](docs/API.md)
|
|
259
|
-
- [Accessibility checklist](docs/AT-CHECKLIST.md)
|
|
260
|
-
- [Use cases](docs/USE_CASES.md)
|
|
261
|
-
- [Accessibility](docs/ACCESSIBILITY.md)
|
|
262
|
-
- [Testing](docs/TESTING.md)
|
|
263
|
-
- [Roadmap](docs/ROADMAP.md)
|
|
264
|
-
- [Calendar integration](docs/CALENDAR-INTEGRATION.md)
|
|
265
|
-
- [Flatpickr/Formidable migration](docs/MIGRATION-FLATPICKR.md)
|
|
1
|
+
# @lekoala/date-picker
|
|
2
|
+
|
|
3
|
+
Small, native-first date picker and inline calendar primitives.
|
|
4
|
+
|
|
5
|
+
> The interaction contract, event names, public styling tokens and the component API are frozen — see [docs/DECISIONS.md](docs/DECISIONS.md). Internal DOM and class names are `.dp-*` implementation details and stay private.
|
|
6
|
+
|
|
7
|
+
This project fills the gap between a normal editable date field and a full scheduling calendar. A plain date input remains the right answer for simple CMS/editing forms. This component becomes useful when a date has context: availability, disabled days, annotations, remote state, start/end relationships, or a mini calendar that navigates another view.
|
|
8
|
+
|
|
9
|
+
## The two primitives
|
|
10
|
+
|
|
11
|
+
### `<date-calendar>`
|
|
12
|
+
|
|
13
|
+
An inline calendar with real month/year navigation, keyboard support and three distinct pieces of state:
|
|
14
|
+
|
|
15
|
+
- `display` — the month being rendered (`YYYY-MM`);
|
|
16
|
+
- `focusedDate` — the roving keyboard target (`YYYY-MM-DD`);
|
|
17
|
+
- `value` — the selected date (`YYYY-MM-DD`, optional).
|
|
18
|
+
|
|
19
|
+
`display` and `focusedDate` are coupled during navigation/display: changing the displayed month re-clamps the focused day into that month, because the roving-tabbable grid cell must stay visible. `value` and activation stay independent of both.
|
|
20
|
+
|
|
21
|
+
User activation is exposed separately as `dateactivate`. This is what makes the same calendar useful as a selector **and** as a mini navigator.
|
|
22
|
+
|
|
23
|
+
```html
|
|
24
|
+
<date-calendar value="2026-09-10" fixed-weeks></date-calendar>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
For a navigator that should not own a selection:
|
|
28
|
+
|
|
29
|
+
```html
|
|
30
|
+
<date-calendar id="mini" selection="none" fixed-weeks show-week-numbers></date-calendar>
|
|
31
|
+
<script type="module">
|
|
32
|
+
const mini = document.querySelector("#mini");
|
|
33
|
+
mini.addEventListener("dateactivate", (event) => {
|
|
34
|
+
agenda.gotoDate(event.detail.date);
|
|
35
|
+
});
|
|
36
|
+
</script>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### `<date-picker>`
|
|
40
|
+
|
|
41
|
+
A composition around a real editable text input. The user sees and edits a short locale-aware numeric date while form submission stays canonical ISO.
|
|
42
|
+
|
|
43
|
+
**Use `<date-picker>` when choosing a date benefits from calendar context.** For a simple editable date field, a normal input with formatting and validation is the right tool instead.
|
|
44
|
+
|
|
45
|
+
```html
|
|
46
|
+
<date-picker value="2026-09-10" locale="fr-BE">
|
|
47
|
+
<input name="appointment_date" required>
|
|
48
|
+
</date-picker>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
With JavaScript enabled the input displays `10/09/2026`; the hidden submitted value remains `2026-09-10`. The visible input keeps the label, focus, required state and validation UI.
|
|
52
|
+
|
|
53
|
+
By default the calendar opens when the field receives focus — without stealing the keyboard focus, so typing works immediately. `open-on-focus="false"` makes the calendar opt-in (trigger button or `ArrowDown`). Under `readonly` the value stays editable-by-calendar-off but is still submitted; under `disabled` the field is also unsubmitted.
|
|
54
|
+
|
|
55
|
+
## Start/end ranges
|
|
56
|
+
|
|
57
|
+
Two product shapes cover ranges.
|
|
58
|
+
|
|
59
|
+
**One shared calendar surface** — two editable fields wired to a single picker popup:
|
|
60
|
+
|
|
61
|
+
```html
|
|
62
|
+
<date-picker range>
|
|
63
|
+
<input data-range-start name="arrival" aria-label="Arrival">
|
|
64
|
+
<input data-range-end name="departure" aria-label="Departure">
|
|
65
|
+
</date-picker>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Opening from a field targets that bound; picking the first date moves the active endpoint to the other bound without closing, and the second pick completes and closes. `picker.range` is the atomic `{ start, end }` API (single `rangechange` event).
|
|
69
|
+
|
|
70
|
+
**Two independent pickers** — a small external link for visually separate fields:
|
|
71
|
+
|
|
72
|
+
```js
|
|
73
|
+
import { linkDateRange } from "@lekoala/date-picker";
|
|
74
|
+
|
|
75
|
+
const cleanup = linkDateRange(startPicker, endPicker);
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The relationship applies `end >= start` and `start <= end` as effective bounds. Neither picker knows about its sibling.
|
|
79
|
+
|
|
80
|
+
## Date + time
|
|
81
|
+
|
|
82
|
+
A date pairs with one or two native time companions. The picker owns the civil date; the time inputs own their civil time values:
|
|
83
|
+
|
|
84
|
+
```html
|
|
85
|
+
<date-picker>
|
|
86
|
+
<input name="appointment[date]">
|
|
87
|
+
<input type="time" data-time-start name="appointment[time]">
|
|
88
|
+
</date-picker>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
```text
|
|
92
|
+
appointment[date] = 2026-09-10
|
|
93
|
+
appointment[time] = 09:30
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The application/server combines these civil values and applies the relevant timezone when a datetime/instant is needed. The picker never constructs a combined datetime value.
|
|
97
|
+
|
|
98
|
+
## Availability and per-day metadata
|
|
99
|
+
|
|
100
|
+
The calendar accepts a source function or `{ load() }` object with the same abortable shape used elsewhere in LeKoala components:
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
calendar.source = async ({ start, end }, { signal }) => {
|
|
104
|
+
const response = await fetch(`/availability?start=${start}&end=${end}`, { signal });
|
|
105
|
+
return response.json();
|
|
106
|
+
};
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Accepted payloads are deliberately simple:
|
|
110
|
+
|
|
111
|
+
```js
|
|
112
|
+
{
|
|
113
|
+
dates: {
|
|
114
|
+
"2026-09-10": {
|
|
115
|
+
enabled: true,
|
|
116
|
+
description: "Available morning and afternoon",
|
|
117
|
+
morning: true,
|
|
118
|
+
afternoon: true
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Business metadata is not interpreted by the core. Consumers can add visual content without replacing the grid cell or its accessibility semantics:
|
|
125
|
+
|
|
126
|
+
```js
|
|
127
|
+
calendar.renderDay = (_date, state) => {
|
|
128
|
+
if (!state.morning && !state.afternoon) return "";
|
|
129
|
+
return `${state.morning ? "●" : "○"}${state.afternoon ? "●" : "○"}`;
|
|
130
|
+
};
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Date math
|
|
134
|
+
|
|
135
|
+
The public value model is intentionally boring: canonical `YYYY-MM-DD` strings. There is no public `Date`, timestamp, timezone or `Temporal` dependency.
|
|
136
|
+
|
|
137
|
+
Pure civil-date helpers are exported under `dates`:
|
|
138
|
+
|
|
139
|
+
```js
|
|
140
|
+
import { dates } from "@lekoala/date-picker";
|
|
141
|
+
|
|
142
|
+
const weeks = dates.getMonthWeeks("2026-09-03", { firstDay: 1 });
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`getMonthWeeks()` returns the true 4–6 full civil weeks covering the month. It never pads to six rows. `fixed-weeks` is a rendering choice made by `<date-calendar>`.
|
|
146
|
+
|
|
147
|
+
## Accessibility baseline
|
|
148
|
+
|
|
149
|
+
The calendar follows the WAI-ARIA date-picker/grid interaction model:
|
|
150
|
+
|
|
151
|
+
- semantic `table[role="grid"]`;
|
|
152
|
+
- one roving `tabindex="0"` grid cell;
|
|
153
|
+
- arrow keys move by day/week;
|
|
154
|
+
- Home/End move to week boundaries;
|
|
155
|
+
- PageUp/PageDown move by month;
|
|
156
|
+
- Shift+PageUp/PageDown move by year;
|
|
157
|
+
- Enter/Space activate a date;
|
|
158
|
+
- keyboard movement never implicitly selects;
|
|
159
|
+
- `aria-selected` identifies the selected date;
|
|
160
|
+
- `aria-current="date"` identifies today;
|
|
161
|
+
- abbreviated weekday headers keep full `abbr` names.
|
|
162
|
+
|
|
163
|
+
The picker popup uses native Popover for the top layer and `@lekoala/floating` for geometry. It has `role="dialog"`, but it is intentionally **not** marked `aria-modal` because the current implementation does not inert the rest of the document.
|
|
164
|
+
|
|
165
|
+
The calendar trigger is a real button painted inside the field's own box (like the native `input[type=time]` indicator): the field reserves its place with `padding-inline-end`, keeps its own border and background, and its focus ring wraps the trigger too. The trigger draws no chrome of its own and shows a small inner ring when focused.
|
|
166
|
+
|
|
167
|
+
Fields, header controls and days share an inner outline that survives forced colors. Customize its color with `--dp-focus` (default `var(--dp-accent)`) and thickness with `--dp-focus-width` (default `2px`) on `date-picker` and `date-calendar`. The trigger keeps a `-5px` inset around its icon; selected days and range endpoints use `--dp-accent-fg` for contrast. Fields and header controls now show this solid outline instead of the previous soft halo.
|
|
168
|
+
|
|
169
|
+
When the popup opens on the field's focus, grid navigation is reachable via `ArrowDown` (or the trigger button) so the focus-restore/Escape flow stays predictable. Closing returns focus to the control that opened the popover: the field when it opened by focus, the trigger when it was activated. In range mode focus follows the active bound instead, because a first pick moves the workflow to the other bound.
|
|
170
|
+
|
|
171
|
+
See [docs/ACCESSIBILITY.md](docs/ACCESSIBILITY.md).
|
|
172
|
+
|
|
173
|
+
## Install / develop
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
bun install
|
|
177
|
+
bun run dev
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
If a previous `bun run dev` is still holding port `4859`, clear it with `bun run dev:kill` (or restart in one go with `bun run dev:restart`).
|
|
181
|
+
|
|
182
|
+
Tooling is aligned with the current LeKoala component repos:
|
|
183
|
+
|
|
184
|
+
- Bun 1.4.3
|
|
185
|
+
- TypeScript 7.0.2 (`checkJs` + declaration emit)
|
|
186
|
+
- Biome 2.5.15
|
|
187
|
+
- Playwright 1.64.0
|
|
188
|
+
- `@lekoala/floating ^0.2.0`
|
|
189
|
+
|
|
190
|
+
Useful commands:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
bun run test
|
|
194
|
+
bun run typecheck
|
|
195
|
+
bun run lint
|
|
196
|
+
bun run build
|
|
197
|
+
bun run test:browser
|
|
198
|
+
bun run test:browser:all
|
|
199
|
+
bun run check:all
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Build outputs
|
|
203
|
+
|
|
204
|
+
`bun run build` produces:
|
|
205
|
+
|
|
206
|
+
```text
|
|
207
|
+
dist/date-picker.js
|
|
208
|
+
dist/date-picker.min.js
|
|
209
|
+
dist/date-picker.css
|
|
210
|
+
dist/date-picker.min.css
|
|
211
|
+
dist/date-picker.standalone.min.js
|
|
212
|
+
dist/types/**
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
The default ESM exports point at `src/`, matching the native-first development style of `@lekoala/combobox`. The classic build is file:// friendly; the standalone build additionally injects the component CSS.
|
|
216
|
+
|
|
217
|
+
## Scope
|
|
218
|
+
|
|
219
|
+
Included:
|
|
220
|
+
|
|
221
|
+
- inline single-date selection;
|
|
222
|
+
- mini-calendar/navigation-only use;
|
|
223
|
+
- month and year selection;
|
|
224
|
+
- previous/next navigation;
|
|
225
|
+
- editable localized input + canonical submitted value;
|
|
226
|
+
- min/max and custom disabled rules;
|
|
227
|
+
- presentation-only range band (`highlightedRange`);
|
|
228
|
+
- shared-calendar two-field range picker (`<date-picker range>`);
|
|
229
|
+
- linked start/end pickers (`linkDateRange`);
|
|
230
|
+
- native time companions (one or two `input[type=time]` beside the date);
|
|
231
|
+
- `Intl` month/year control order + `month-format="short"`;
|
|
232
|
+
- RTL through the inherited `dir` (no `isRTL` option);
|
|
233
|
+
- readable keyboard focus on disabled/selected days;
|
|
234
|
+
- compact-consumer CSS tokens (`--dp-header-control-size`, `--dp-day-radius`, …);
|
|
235
|
+
- async per-day state with cancellation;
|
|
236
|
+
- custom day decorations that cannot replace the accessible cell;
|
|
237
|
+
- fixed six-row presentation as an opt-in;
|
|
238
|
+
- ISO week numbers;
|
|
239
|
+
- keyboard and focus contracts.
|
|
240
|
+
|
|
241
|
+
Deliberately out of scope:
|
|
242
|
+
|
|
243
|
+
- custom time-picker UI and combined datetime values (only native `input[type=time]` companions are supported);
|
|
244
|
+
- timezones;
|
|
245
|
+
- in-place single-calendar range *selection interaction* (use the two-field picker);
|
|
246
|
+
- multi-date selection;
|
|
247
|
+
- recurrence;
|
|
248
|
+
- natural-language parsing;
|
|
249
|
+
- plugin registry;
|
|
250
|
+
- virtualized months;
|
|
251
|
+
- scheduling/time slots.
|
|
252
|
+
|
|
253
|
+
## Docs
|
|
254
|
+
|
|
255
|
+
- [Changelog](CHANGELOG.md)
|
|
256
|
+
- [Architecture](docs/ARCHITECTURE.md)
|
|
257
|
+
- [Decisions](docs/DECISIONS.md)
|
|
258
|
+
- [API](docs/API.md)
|
|
259
|
+
- [Accessibility checklist](docs/AT-CHECKLIST.md)
|
|
260
|
+
- [Use cases](docs/USE_CASES.md)
|
|
261
|
+
- [Accessibility](docs/ACCESSIBILITY.md)
|
|
262
|
+
- [Testing](docs/TESTING.md)
|
|
263
|
+
- [Roadmap](docs/ROADMAP.md)
|
|
264
|
+
- [Calendar integration](docs/CALENDAR-INTEGRATION.md)
|
|
265
|
+
- [Flatpickr/Formidable migration](docs/MIGRATION-FLATPICKR.md)
|
|
266
|
+
|
|
267
|
+
### Host control classes
|
|
268
|
+
|
|
269
|
+
Set application-wide header styles before creating calendars or pickers:
|
|
270
|
+
|
|
271
|
+
```js
|
|
272
|
+
import { setDefaultControlClasses } from "@lekoala/date-picker";
|
|
273
|
+
|
|
274
|
+
setDefaultControlClasses({
|
|
275
|
+
month: "form-select form-select-sm",
|
|
276
|
+
year: "form-control form-control-sm",
|
|
277
|
+
nav: "btn btn-outline-secondary btn-sm",
|
|
278
|
+
});
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Each role with author classes uses the host design system's skin. Override per element through `controlClasses`; roles without classes retain the default skin. Day cells and the field trigger are unchanged.
|
package/custom-elements.json
CHANGED
|
@@ -91,6 +91,11 @@
|
|
|
91
91
|
"kind": "getter",
|
|
92
92
|
"name": "showWeekNumbers"
|
|
93
93
|
},
|
|
94
|
+
{
|
|
95
|
+
"kind": "getter",
|
|
96
|
+
"name": "controlClasses",
|
|
97
|
+
"description": "Author classes for header controls; merged with global defaults."
|
|
98
|
+
},
|
|
94
99
|
{
|
|
95
100
|
"kind": "getter",
|
|
96
101
|
"name": "messages"
|
|
@@ -313,6 +318,11 @@
|
|
|
313
318
|
"kind": "getter",
|
|
314
319
|
"name": "open"
|
|
315
320
|
},
|
|
321
|
+
{
|
|
322
|
+
"kind": "getter",
|
|
323
|
+
"name": "controlClasses",
|
|
324
|
+
"description": "Author classes for header controls; merged with global defaults."
|
|
325
|
+
},
|
|
316
326
|
{
|
|
317
327
|
"kind": "getter",
|
|
318
328
|
"name": "messages"
|