@lekoala/date-picker 0.2.0 → 0.4.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 +12 -8
- package/custom-elements.json +32 -2
- package/dist/date-picker.css +163 -115
- package/dist/date-picker.js +656 -171
- package/dist/date-picker.min.css +1 -1
- package/dist/date-picker.min.js +13 -13
- package/dist/date-picker.standalone.min.js +177 -129
- package/dist/types/date-calendar.d.ts +34 -3
- package/dist/types/date-calendar.d.ts.map +1 -1
- package/dist/types/date-field.d.ts +15 -5
- package/dist/types/date-field.d.ts.map +1 -1
- package/dist/types/date-picker.d.ts +153 -14
- package/dist/types/date-picker.d.ts.map +1 -1
- package/dist/types/date-range.d.ts +102 -20
- package/dist/types/date-range.d.ts.map +1 -1
- package/dist/types/date.d.ts +10 -0
- package/dist/types/date.d.ts.map +1 -1
- package/dist/types/intl.d.ts.map +1 -1
- package/docs/ACCESSIBILITY.md +22 -10
- package/docs/API.md +72 -3
- package/docs/ARCHITECTURE.md +16 -2
- package/docs/AT-CHECKLIST.md +1 -1
- package/docs/DECISIONS.md +19 -4
- package/docs/MIGRATION-FLATPICKR.md +2 -2
- package/docs/TESTING.md +1 -1
- package/docs/USE_CASES.md +3 -0
- package/package.json +7 -7
- package/src/date-calendar.js +148 -48
- package/src/date-field.js +20 -8
- package/src/date-picker.css +163 -115
- package/src/date-picker.js +549 -110
- package/src/date-range.js +184 -30
- package/src/date.js +50 -1
- package/src/intl.js +13 -7
package/docs/ACCESSIBILITY.md
CHANGED
|
@@ -38,13 +38,15 @@ Weekday headings use visible short labels plus a full `abbr` value.
|
|
|
38
38
|
| Shift+PageDown | same/clamped day in next year |
|
|
39
39
|
| Enter / Space | activate focused date |
|
|
40
40
|
|
|
41
|
-
Moving focus never changes selection.
|
|
41
|
+
Moving focus never changes selection. Each cell that actually receives focus dispatches `datefocus`, so an application can follow keyboard navigation without reading internal DOM classes; hover never dispatches it, and assigning `focusedDate` without moving focus stays silent.
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
A range preview projected onto the band is visual only: it never contributes a range start/end to a day's accessible name, so a proposal is never announced as a committed selection.
|
|
44
|
+
|
|
45
|
+
Endpoint dragging is a pointer enhancement. Every adjustment it offers is also reachable from the keyboard: activating before or after a complete range moves the corresponding bound.
|
|
44
46
|
|
|
45
|
-
|
|
47
|
+
## Month/year controls
|
|
46
48
|
|
|
47
|
-
The
|
|
49
|
+
The month control is a native select and the year control is a native number input. They are easy to discover, keyboard-operable, and do not require nested custom popovers.
|
|
48
50
|
|
|
49
51
|
## Date picker input
|
|
50
52
|
|
|
@@ -99,19 +101,29 @@ The same resolved date state used by the grid is used for manual-entry validatio
|
|
|
99
101
|
|
|
100
102
|
Forced colors should not rely on the normal selected-day fill tokens.
|
|
101
103
|
|
|
102
|
-
When `forced-colors: active`, the selected day
|
|
104
|
+
When `forced-colors: active`, the selected day and range endpoints switch to a border-based treatment:
|
|
103
105
|
|
|
104
106
|
- `Canvas` background;
|
|
105
107
|
- `CanvasText` foreground;
|
|
106
|
-
- `Highlight` outer border;
|
|
107
|
-
-
|
|
108
|
+
- `2px Highlight` outer border;
|
|
109
|
+
- in-range days keep a `1px Highlight` underline (the range fill is lost and the browser does not know the semantics).
|
|
108
110
|
|
|
109
|
-
Today keeps
|
|
110
|
-
|
|
111
|
-
This avoids relying on white text over a `Highlight` fill, which can become ambiguous when the active high-contrast theme resolves `Highlight` near-white or when small white digits rasterize poorly. The disabled state is `GrayText` at full opacity.
|
|
111
|
+
Today keeps a thin `1px Highlight` border while selected carries the wider border, so the two stay distinct through border width. The disabled state relies on the native dimming, verified by capture. The month select hands its caret back to the browser (`appearance: auto`), since the author-drawn gradient is a non-system color.
|
|
112
112
|
|
|
113
113
|
Guard: re-run the `color-contrast` review and the forced-colors screenshot bundle whenever `--dp-*` styling or day-cell rendering changes.
|
|
114
114
|
|
|
115
|
+
## Visible focus
|
|
116
|
+
|
|
117
|
+
Fields (including native time companions), calendar header controls and day cells use an inner solid outline. `--dp-focus` defaults to `var(--dp-accent)` and `--dp-focus-width` defaults to `2px`; the inward offset follows the width. This replaces the previous accent border and soft halo on fields and header controls. The resting input appearance stays consumer-owned.
|
|
118
|
+
|
|
119
|
+
Focus is only ever drawn on a real box. The calendar trigger is a real button painted inside the date field's own box, so the field's focus ring naturally wraps the affordance too — no host-level ring, no pseudo-element. When the trigger itself is focused it shows a small inner ring, like the native `input[type=time]` indicator, instead of pretending the whole field is focused.
|
|
120
|
+
|
|
121
|
+
Day cells use an **inner** ring too: keyboard focus stays inside the 44px cell instead of reading as a second selected state, and it never bleeds onto a neighbouring range band. The ring is an `outline` with `outline-offset: -2px`, not an inset `box-shadow`, because forced colors computes `box-shadow` to `none` — an inset shadow would erase the keyboard indicator (including on disabled days, which must stay discoverable). `outline-color` is force-adjusted to a system color there, so the ring survives high contrast. On a cell already filled with the accent (selected, range endpoints) the ring inverts to `--dp-accent-fg` so it stays readable.
|
|
122
|
+
|
|
123
|
+
Each native time companion is an independent control and keeps its own ring; they are not joined, so rings never merge and no neighbour swallows another's focus. In `[range]`, each bound owns its trigger and its own ring, and focus follows the active bound rather than the original trigger.
|
|
124
|
+
|
|
125
|
+
Closing returns focus to the control that opened the popover (field or trigger) in single mode.
|
|
126
|
+
|
|
115
127
|
## Recommended manual test matrix
|
|
116
128
|
|
|
117
129
|
Beyond what the automated suite covers, the following pairings should be exercised before a broad adoption of 0.1. Results are recorded in [AT-CHECKLIST.md](AT-CHECKLIST.md); the human rows there are still open:
|
package/docs/API.md
CHANGED
|
@@ -31,7 +31,7 @@ Presentation-only range band. It never changes `value`, `selection` or `focusedD
|
|
|
31
31
|
calendar.isDateDisabled = (date) => boolean;
|
|
32
32
|
calendar.dateState = (date, sourceState) => ({
|
|
33
33
|
disabled,
|
|
34
|
-
enabled, // true =
|
|
34
|
+
enabled, // true = lifts isDateDisabled() only; never min/max or resolved disabled
|
|
35
35
|
description,
|
|
36
36
|
...applicationMetadata
|
|
37
37
|
});
|
|
@@ -62,6 +62,7 @@ calendar.nextMonth();
|
|
|
62
62
|
calendar.focusDate("2026-09-10");
|
|
63
63
|
calendar.focusGrid();
|
|
64
64
|
calendar.getDateState("2026-09-10");
|
|
65
|
+
await calendar.activateDate("2026-09-10");
|
|
65
66
|
await calendar.ensureDate("2026-09-10");
|
|
66
67
|
await calendar.refreshSource();
|
|
67
68
|
calendar.render();
|
|
@@ -87,6 +88,28 @@ calendar.addEventListener("dateactivate", (event) => {
|
|
|
87
88
|
});
|
|
88
89
|
```
|
|
89
90
|
|
|
91
|
+
`activateDate(date)` runs that same path on demand: availability check, then
|
|
92
|
+
the cancelable event, then selection. Click, keyboard and the range endpoint
|
|
93
|
+
drag all go through it, so an application that cancels `dateactivate` cannot be
|
|
94
|
+
bypassed by a different input device.
|
|
95
|
+
|
|
96
|
+
#### `datefocus`
|
|
97
|
+
|
|
98
|
+
Fired when a date cell actually receives focus, with `{ date, state }`. Arrow,
|
|
99
|
+
Home, End, PageUp and PageDown all emit one through the cell that took focus.
|
|
100
|
+
|
|
101
|
+
```js
|
|
102
|
+
calendar.addEventListener("datefocus", (event) => {
|
|
103
|
+
console.log(event.detail.date);
|
|
104
|
+
});
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
It observes navigation and nothing else: a disabled day still reports it (grid
|
|
108
|
+
navigation stays discoverable), hover never does, and assigning `focusedDate`
|
|
109
|
+
without moving DOM focus stays silent. The three verbs are distinct —
|
|
110
|
+
`datefocus` observes, `dateactivate` requests an activation, `datechange`
|
|
111
|
+
observes a selection.
|
|
112
|
+
|
|
90
113
|
#### `datechange`
|
|
91
114
|
|
|
92
115
|
Fired when `selection="single"` changes `value`.
|
|
@@ -152,6 +175,23 @@ The direct child text input is required. On enhancement its original `name` move
|
|
|
152
175
|
- `isDateDisabled`;
|
|
153
176
|
- `messages`.
|
|
154
177
|
|
|
178
|
+
### Popup positioning
|
|
179
|
+
|
|
180
|
+
- `coordinateSpace` — JS-only, `"viewport"` by default.
|
|
181
|
+
|
|
182
|
+
```text
|
|
183
|
+
"viewport"
|
|
184
|
+
panel uses position: fixed
|
|
185
|
+
general/default integration
|
|
186
|
+
|
|
187
|
+
"document"
|
|
188
|
+
panel uses position: absolute
|
|
189
|
+
explicit optimization when the application knows the picker
|
|
190
|
+
reference moves with root-page scrolling
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
`document` follows the `@lekoala/floating` consumer contract: the picker panel is a top-layer popover, so its absolute positioning resolves against the initial containing block; the application remains responsible for knowing that the `<date-picker>` reference itself moves with the root document. Changing `coordinateSpace` while open configures the next opening, never the current one. See the `@lekoala/floating` documentation for the coordinate-space doctrine.
|
|
194
|
+
|
|
155
195
|
### Read-only properties
|
|
156
196
|
|
|
157
197
|
```js
|
|
@@ -231,6 +271,8 @@ Optional native time companions (one or both). They are direct children, fixed a
|
|
|
231
271
|
|
|
232
272
|
Two editable fields share one calendar popup. The mode is fixed at connect time (`range` attribute + both marked direct child inputs); switching single/range at runtime is not supported in V1.
|
|
233
273
|
|
|
274
|
+
Each field gets its own calendar trigger, inserted right after it (`start`, then its trigger, `end`, then its trigger), so DOM order matches visual/tab order. Both triggers point at the same popup; activating the other bound's trigger while it is open switches the active bound and keeps it open.
|
|
275
|
+
|
|
234
276
|
### Strict API
|
|
235
277
|
|
|
236
278
|
- `range` — atomic get/set of `{ start, end }` (canonical `YYYY-MM-DD`). The setter validates through `normalizeRange` (ISO-only, ordered; inverted throws) and applies both bounds, the visible fields and both hidden fields **before** one `rangechange` event — no intermediate new-start/old-end state is observable.
|
|
@@ -253,8 +295,12 @@ The business range may be temporarily incomplete or inverted; `calendar.highligh
|
|
|
253
295
|
### Interaction
|
|
254
296
|
|
|
255
297
|
- When an ordered range is complete, picking before its start updates only the start; picking after its end updates only the end, regardless of the opening field. The popup closes after this update. An uneditable target bound refuses the activation.
|
|
256
|
-
- Otherwise, opening through a field targets that bound (`activeEndpoint`). Picking from `start` commits it, moves the endpoint to `end` without closing, and a second pick commits and closes.
|
|
257
|
-
-
|
|
298
|
+
- Otherwise, opening through a field targets that bound (`activeEndpoint`). Picking from `start` commits it, moves the endpoint to `end` without closing, and a second pick commits and closes.
|
|
299
|
+
- **Calendar interaction never produces an inverted range.** While a range is being created, a second pick before the anchor is sorted rather than refused: `10` then `15` gives `10 -> 15`, `10` then `5` gives `5 -> 10`, and `10` then `10` a one-day range. That transition moves **both** bounds at once and is applied in a single write — one `rangechange`, with synthetic `input`/`change` on each bound that moved, and no intermediate inverted pair ever observable. Typed text keeps the opposite contract and may be temporarily inverted (see [DECISIONS D6](DECISIONS.md)).
|
|
300
|
+
- Changing one bound of an already complete pair keeps that bound's identity: moving `end` before `start` is refused (`dateinvalid`, nothing changes) instead of rewriting the other field.
|
|
301
|
+
- **Preview.** While a range is being created, hovering a day or moving the keyboard focus projects the range the next pick would commit onto `calendar.highlightedRange`, and the pick commits exactly what was projected. The projection is visual only: no `rangechange`, no field write, no validation and no source request. It reads the month already loaded, so a day known to be unavailable promises nothing; the real availability check still runs on commit. Pointer hover never moves `focusedDate`, which stays the keyboard target. Leaving the grid, Escape and closing the popup restore the committed band.
|
|
302
|
+
- **Endpoint drag.** On a complete, ordered range whose two ends sit on different days, each endpoint can be dragged with a mouse or pen (pointer events plus capture, armed only once the pointer travels). A handle never crosses the other one — the candidate is clamped to it, equality allowed — an unavailable cell is not a drop target, and `pointercancel` restores the committed band. The drop runs the same projection and the same `ensureDate` + cancelable `dateactivate` path as a click, emits one `rangechange`, and keeps the popup open: a drag adjusts a range, it does not validate it. A press without travel stays a plain click. Touch keeps native scrolling and stays a plain tap.
|
|
303
|
+
- Each bound owns its trigger (`input[data-range-start]` then its trigger, `input[data-range-end]` then its trigger); the two triggers share **one** popup, calendar, source and state. Pressing the other bound's trigger while the popup is open switches the active bound and keeps it open instead of toggling it shut. A trigger never opens a `readonly`/`disabled` bound.
|
|
258
304
|
- Typing stays independent per field. Moving `start` past `end` keeps both values and flags the *modified* bound with `rangeOrderStart`; fixing either side revalidates both. Parse/source errors are never cleared by cross-bound revalidation.
|
|
259
305
|
- A stale availability response resolving after the active endpoint changed (or after a newer selection) never commits; a field focus that switches the endpoint invalidates in-flight activations.
|
|
260
306
|
|
|
@@ -278,6 +324,29 @@ rangePosition("2026-09-12", range); // "start" | "in" | "end" | "single" | ""
|
|
|
278
324
|
|
|
279
325
|
`normalizeRange` validates an ordered displayable range and never reorders: an inversion or an `end` without a `start` throws. `rangePosition` classifies one day within a displayable range (see [`highlightedRange`](#highlightedrange)).
|
|
280
326
|
|
|
327
|
+
## `DateRangeController`
|
|
328
|
+
|
|
329
|
+
The pure two-bound state machine behind `date-picker[range]`. It owns the
|
|
330
|
+
selection rules and knows nothing about DOM, popups or async availability.
|
|
331
|
+
|
|
332
|
+
```js
|
|
333
|
+
const model = new DateRangeController();
|
|
334
|
+
model.focus("start");
|
|
335
|
+
model.activate("2026-09-10"); // { status: "pending", endpoint: "start", … }
|
|
336
|
+
model.previewRange("2026-09-05"); // { start: "2026-09-05", end: "2026-09-10" }
|
|
337
|
+
model.activate("2026-09-05"); // changedEndpoints: ["start", "end"]
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
- `project(date, editable?)` / `projectEndpoint(date, endpoint, editable?)` describe a transition without applying it;
|
|
341
|
+
- `activate(date, editable?)` / `moveEndpoint(date, endpoint, editable?)` apply one;
|
|
342
|
+
- `previewRange(date, endpoint?, editable?)` returns the displayable band a candidate would produce, or `null` when there is nothing to promise.
|
|
343
|
+
|
|
344
|
+
A transition is `{ status, endpoint, changedEndpoints, range, activeEndpoint, close? }`.
|
|
345
|
+
`range` is the whole resulting pair, because one interaction can move both
|
|
346
|
+
bounds; `changedEndpoints` names the ones that actually move. Every rule exists
|
|
347
|
+
once, as a projection, and the previews read the same projections the commit
|
|
348
|
+
applies.
|
|
349
|
+
|
|
281
350
|
## `createDateAdapter(locale)`
|
|
282
351
|
|
|
283
352
|
Locale-aware editable numeric adapter:
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -93,7 +93,10 @@ This prevents rich decorations from breaking grid semantics or keyboard behavior
|
|
|
93
93
|
3. `min` / `max`;
|
|
94
94
|
4. `isDateDisabled()`.
|
|
95
95
|
|
|
96
|
-
`enabled: true` is the explicit exception seam:
|
|
96
|
+
`enabled: true` is the explicit exception seam: when the resolved state carries
|
|
97
|
+
`enabled === true`, it lifts `isDateDisabled()` only. It never bypasses
|
|
98
|
+
`min`/`max` or the resolved `state.disabled` (source state merged with
|
|
99
|
+
`dateState()` output, so `dateState()` can already replace a source `disabled`).
|
|
97
100
|
|
|
98
101
|
Both click activation and typed picker validation use this resolved state.
|
|
99
102
|
|
|
@@ -144,6 +147,17 @@ The picker creates a `<date-calendar selection="none">` in a native manual popov
|
|
|
144
147
|
|
|
145
148
|
Native Popover owns the top layer. `@lekoala/floating` owns coordinates and geometry updates.
|
|
146
149
|
|
|
150
|
+
### Coordinate space
|
|
151
|
+
|
|
152
|
+
`picker.coordinateSpace` (`"viewport"` default, `"document"` explicit opt-in, JS-only, no attribute) is frozen once per opening in `show()`; `panel.style.position` is coupled:
|
|
153
|
+
|
|
154
|
+
```text
|
|
155
|
+
viewport (default) → fixed
|
|
156
|
+
document → absolute
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
No layout inference is performed. Coordinate-space choice is consumer policy; `@lekoala/floating` owns only coordinate math and update tracking.
|
|
160
|
+
|
|
147
161
|
The picker does not implement:
|
|
148
162
|
|
|
149
163
|
- custom portals;
|
|
@@ -157,7 +171,7 @@ The popup remains in authored DOM, preserving inherited locale/theme/density con
|
|
|
157
171
|
|
|
158
172
|
Two product shapes cover the range cases, and both keep range rules outside the classes:
|
|
159
173
|
|
|
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.
|
|
174
|
+
`<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. The shell owns one calendar trigger per bound (each inserted after its own field, both pointing at the same popup) and switches the active endpoint without closing it. `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
175
|
|
|
162
176
|
`linkDateRange(start, end)` keeps the external relationship for two visually separate pickers:
|
|
163
177
|
|
package/docs/AT-CHECKLIST.md
CHANGED
|
@@ -44,7 +44,7 @@ Release decision (0.1.0): the human reader/OS pairings below are **deferred past
|
|
|
44
44
|
|
|
45
45
|
- [x] [auto] Typing a valid localized date updates the field validation state (no stale invalid message).
|
|
46
46
|
- [x] [split] Typing an impossible date produces an announced invalid message (native invalid state/message covered automatically; announcement still manual).
|
|
47
|
-
- [
|
|
47
|
+
- [ ] [manual] Forced-colors mode keeps selected/today/disabled distinguishable with the simplified border-based treatment (re-check after the neutral-palette + rhythm pass; spot-check with the forced-colors screenshot bundle).
|
|
48
48
|
|
|
49
49
|
## Automated pass
|
|
50
50
|
|
package/docs/DECISIONS.md
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
# Decisions
|
|
2
2
|
|
|
3
|
-
Frozen contracts for the
|
|
3
|
+
Frozen contracts for the public surface. A change to a decision below is a breaking change and needs a proposal, not an opportunistic edit.
|
|
4
4
|
|
|
5
5
|
## D1 — Calendar events are flat, verb-named
|
|
6
6
|
|
|
7
7
|
Never merged with the `value` concept (`dateactivate` is activation, not `value`).
|
|
8
8
|
|
|
9
|
-
- `date-calendar`: `dateactivate`, `datechange`, `dateinvalid`, `displaychange`, `dateloadstart`, `dateloadend`, `dateloaderror`.
|
|
10
|
-
- `date-picker`: `valuechange`, `open`, `close
|
|
9
|
+
- `date-calendar`: `dateactivate`, `datefocus`, `datechange`, `dateinvalid`, `displaychange`, `dateloadstart`, `dateloadend`, `dateloaderror`.
|
|
10
|
+
- `date-picker`: `valuechange`, `open`, `close`, plus `rangechange` in range mode.
|
|
11
|
+
|
|
12
|
+
The observation verbs stay distinct: `datefocus` observes navigation, `dateactivate` requests an activation (and is cancelable), `datechange` observes a selection. `datefocus` is named after the user event rather than after the `focusedDate` setter, so a programmatic assignment that moves no DOM focus never announces one.
|
|
11
13
|
|
|
12
14
|
No `dp:` namespace prefix on element events, even though `@lekoala/combobox` namespaces its events (`combobox:*`).
|
|
13
15
|
|
|
@@ -35,6 +37,8 @@ Context: DOM-backed styling hooks become API the moment they are documented —
|
|
|
35
37
|
|
|
36
38
|
- The picker opens its popover when the input receives focus (`open-on-focus`, default `true`; set `open-on-focus="false"` to opt out).
|
|
37
39
|
- 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.
|
|
40
|
+
- Closing returns focus to the control that opened the popover (field or trigger) — the usual popover convention. Range mode is the exception: focus follows the active bound, because a first pick moves the workflow to the other bound.
|
|
41
|
+
- The calendar trigger is overlaid inside the field's own box, like the native `input[type=time]` indicator. The field reserves its place (`padding-inline-end`), keeps its border/background, and owns the focus ring for the pair; the trigger is a real button with an inner ring of its own. A range bound owns its own trigger; the two share one popup, calendar, source and state.
|
|
38
42
|
- `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
43
|
- `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
44
|
- 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.
|
|
@@ -44,4 +48,15 @@ Context: DOM-backed styling hooks become API the moment they are documented —
|
|
|
44
48
|
- Time fields are authored native `input[type=time]` marked `[data-time-start]` / `[data-time-end]`; `name`, `value`, `required`, `min`, `max`, `step` and submission stay native. There is no hidden time input and no time field controller.
|
|
45
49
|
- `picker.value` / `picker.range` remain date-only (`YYYY-MM-DD`); the picker never constructs a datetime, a timestamp or a timezone-bearing value.
|
|
46
50
|
- The picker only coordinates from/to order (`start <= end`, equality allowed); duration rules stay application-owned.
|
|
47
|
-
- Custom time UI and slot picking stay outside the package.
|
|
51
|
+
- Custom time UI and slot picking stay outside the package.
|
|
52
|
+
|
|
53
|
+
## D6 — Calendar interaction never produces an inverted range
|
|
54
|
+
|
|
55
|
+
Pointing and typing are different contracts, deliberately.
|
|
56
|
+
|
|
57
|
+
- **Interaction** (click, keyboard, endpoint drag) always yields an ordered range. While a range is being created, the second selection is sorted against the anchor: `10` then `5` commits `5 -> 10`, and `10` then `10` a one-day range. No `dateinvalid` is fired merely because the second point is earlier.
|
|
58
|
+
- **Text entry** may be temporarily inverted. Typing `start` past `end` keeps both values and reports `rangeOrderStart` / `rangeOrderEnd` on the modified bound, because a half-typed pair is a normal editing state.
|
|
59
|
+
- An explicit change to **one bound of an already complete pair** keeps that bound's identity: moving `end` before `start` is refused rather than silently rewriting the other field.
|
|
60
|
+
- Because one interaction can move both bounds at once, a transition describes the whole resulting pair (`{ status, endpoint, changedEndpoints, range, activeEndpoint }`) and is applied in a single write: one `rangechange`, with synthetic `input` / `change` only on the bounds that actually moved.
|
|
61
|
+
- Hover, keyboard focus and endpoint drag read the **same projection** (`previewRange()`) that the commit applies, so what the band promises is what the next selection commits. A preview writes nothing: no `rangechange`, no field write, no validation, no source request.
|
|
62
|
+
- Click, keyboard and drop all end in that one range machine and in the same `ensureDate` plus cancelable `dateactivate` path, so an application veto cannot be bypassed by choosing another input device.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Flatpickr / Formidable migration map
|
|
2
2
|
|
|
3
|
-
This
|
|
3
|
+
This project was scoped against the existing `FlatpickrInput` wrapper rather than against Flatpickr's full feature list.
|
|
4
4
|
|
|
5
5
|
## Directly covered
|
|
6
6
|
|
|
@@ -78,7 +78,7 @@ calendar.isDateDisabled = (date) => isWeekend(date);
|
|
|
78
78
|
// weekend rule. Hard min/max bounds still win.
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
-
The prototype promotes this legacy behavior into a documented generic field: `enabled: true` reopens a date disabled by `isDateDisabled()
|
|
81
|
+
The prototype promotes this legacy behavior into a documented generic field: `enabled: true` reopens a date disabled by `isDateDisabled()`, but never bypasses hard `min`/`max` bounds or a resolved `state.disabled`.
|
|
82
82
|
|
|
83
83
|
## Not migrated into this component
|
|
84
84
|
|
package/docs/TESTING.md
CHANGED
|
@@ -68,6 +68,6 @@ Keep explicit tests for these regressions:
|
|
|
68
68
|
|
|
69
69
|
## CI
|
|
70
70
|
|
|
71
|
-
The included workflow uses Bun 1.4.
|
|
71
|
+
The included workflow uses Bun 1.4.3 and runs static checks/unit tests before Playwright.
|
|
72
72
|
|
|
73
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.
|
package/docs/USE_CASES.md
CHANGED
|
@@ -111,6 +111,9 @@ Acceptance:
|
|
|
111
111
|
- an inverted range (start after end) keeps both entered values and surfaces an order error on the bound that was just modified; changing either side revalidates both;
|
|
112
112
|
- form reset restores both fields in one resync;
|
|
113
113
|
- a stale async availability response arriving after the active endpoint changed never commits a value or closes the popup;
|
|
114
|
+
- calendar interaction never produces an inverted range: while a range is being created, a second selection before the first is sorted (`10` then `5` commits `5 -> 10`) in one atomic update; an explicit change to one bound of an already complete pair keeps that bound's identity instead;
|
|
115
|
+
- pointer hover and keyboard navigation project the range the next selection would commit onto the band, without changing any value, and a selection commits exactly what was projected;
|
|
116
|
+
- the endpoints of a complete range can be dragged to adjust it, through the same projection, the same availability check and the same cancelable activation as a click;
|
|
114
117
|
- only the bounds are validated; per-night availability stays application-owned.
|
|
115
118
|
|
|
116
119
|
## U10 — Date with native time companions
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lekoala/date-picker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Native-first date picker and inline calendar primitives",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
},
|
|
21
21
|
"homepage": "https://github.com/lekoala/date-picker",
|
|
22
22
|
"bugs": "https://github.com/lekoala/date-picker/issues",
|
|
23
|
-
"packageManager": "bun@1.4.
|
|
23
|
+
"packageManager": "bun@1.4.3",
|
|
24
24
|
"keywords": [
|
|
25
25
|
"date-picker",
|
|
26
26
|
"calendar",
|
|
@@ -162,13 +162,13 @@
|
|
|
162
162
|
"prepublishOnly": "bun run verify"
|
|
163
163
|
},
|
|
164
164
|
"devDependencies": {
|
|
165
|
-
"@biomejs/biome": "2.5.
|
|
166
|
-
"@lekoala/calendar": "0.
|
|
167
|
-
"@playwright/test": "1.
|
|
168
|
-
"temporal-polyfill": "^1.0.
|
|
165
|
+
"@biomejs/biome": "2.5.15",
|
|
166
|
+
"@lekoala/calendar": "0.2.0",
|
|
167
|
+
"@playwright/test": "1.64.0",
|
|
168
|
+
"temporal-polyfill": "^1.0.5",
|
|
169
169
|
"typescript": "7.0.2"
|
|
170
170
|
},
|
|
171
171
|
"dependencies": {
|
|
172
|
-
"@lekoala/floating": "^0.
|
|
172
|
+
"@lekoala/floating": "^0.2.0"
|
|
173
173
|
}
|
|
174
174
|
}
|