@lekoala/date-picker 0.5.2 → 0.5.3

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.
@@ -1,83 +1,5 @@
1
1
  import { DateCalendarElement } from "./date-calendar.js";
2
- import { DateFieldController } from "./date-field.js";
3
- import { DateRangeController } from "./date-range.js";
4
- export type PickIntent = {
5
- id: number;
6
- endpoint: "" | "start" | "end";
7
- };
8
- /** A grid activation intent: the generation at interaction time plus the bound
9
- * a drag drop targets, empty for a normal pick.
10
- * @typedef {{id: number, endpoint: "" | "start" | "end"}} PickIntent */
11
2
  export declare class DatePickerElement extends HTMLElement {
12
- _id: string;
13
- _connected: boolean;
14
- _reflecting: boolean;
15
- _value: string;
16
- _input: HTMLInputElement | null;
17
- /** @type {DateFieldController | null} */
18
- _field: DateFieldController | null;
19
- /** @type {DateFieldController[] | null} */
20
- _fields: DateFieldController[] | null;
21
- /** @type {DateRangeController | null} */
22
- _range: DateRangeController | null;
23
- _orderTags: Set<any>;
24
- /** Native time companions, [start, end]. @type {[HTMLInputElement | null, HTMLInputElement | null]} */
25
- _timeFields: [HTMLInputElement | null, HTMLInputElement | null];
26
- /** Time inputs currently holding our order error. @type {Set<HTMLInputElement>} */
27
- _timeOrderOwned: Set<HTMLInputElement>;
28
- /** @type {"" | "start" | "end"} */
29
- _lastFocusEndpoint: "" | "start" | "end";
30
- /** Activation generation. A grid intent records it at interaction time
31
- * and travels with its own activation (see `activationIntents`), so a late
32
- * response is checked against the generation, never looked up by date. */
33
- _activationId: number;
34
- /** Candidate date currently projected onto the calendar band, if any. */
35
- _previewDate: string;
36
- /** Live endpoint drag, or null. @type {{pointerId:number, endpoint:"start"|"end", x:number, y:number, date:string, active:boolean} | null} */
37
- _drag: {
38
- pointerId: number;
39
- endpoint: "start" | "end";
40
- x: number;
41
- y: number;
42
- date: string;
43
- active: boolean;
44
- } | null;
45
- /** A real drag already committed; swallow the click that follows it. */
46
- _suppressGridClick: boolean;
47
- /** Overlaid calendar triggers, one per field (single) or per bound (range). @type {{button: HTMLButtonElement, endpoint: "" | "start" | "end"}[]} */
48
- _buttons: {
49
- button: HTMLButtonElement;
50
- endpoint: "" | "start" | "end";
51
- }[];
52
- /** Control that opened the popover, so single mode can restore focus to it.
53
- * @type {HTMLElement | null} */
54
- _returnFocus: HTMLElement | null;
55
- _panel: HTMLDivElement | null;
56
- _calendar: DateCalendarElement | null;
57
- _originalDescribedBy: string;
58
- _rangeOriginalDescribedBy: string[];
59
- _formatHint: HTMLSpanElement | null;
60
- _generatedPlaceholder: boolean;
61
- _controller: AbortController | null;
62
- _stopTracking: (() => void) | null;
63
- _open: boolean;
64
- _suppressFocusOpen: boolean;
65
- /** @type {typeof import("./messages.js").DEFAULT_MESSAGES | null} */
66
- _messages: typeof import("./messages.js").DEFAULT_MESSAGES | null;
67
- /** @type {Partial<import("./control-classes.js").ControlClasses> | null} */
68
- _controlClasses: Partial<import("./control-classes.js").ControlClasses> | null;
69
- /** @type {(() => void) | null} */
70
- _stopDefaultMessages: (() => void) | null;
71
- /** @type {import("./date-calendar.js").DateSource | null} */
72
- _source: import("./date-calendar.js").DateSource | null;
73
- /** @type {import("./date-calendar.js").DateStateResolver | null} */
74
- _dateState: import("./date-calendar.js").DateStateResolver | null;
75
- /** @type {import("./date-calendar.js").DayRenderer | null} */
76
- _renderDay: import("./date-calendar.js").DayRenderer | null;
77
- /** @type {import("./date-calendar.js").DateDisabledPredicate | null} */
78
- _isDateDisabled: import("./date-calendar.js").DateDisabledPredicate | null;
79
- /** Public picker coordinate-space request. @type {"viewport" | "document"} */
80
- _coordinateSpace: "viewport" | "document";
81
3
  static observedAttributes: string[];
82
4
  /** @public @param {Partial<import("./control-classes.js").ControlClasses>} classes */
83
5
  static setDefaultControlClasses(classes: Partial<import("./control-classes.js").ControlClasses>): void;
@@ -111,19 +33,8 @@ export declare class DatePickerElement extends HTMLElement {
111
33
  rangeOrderEnd: string;
112
34
  };
113
35
  constructor();
114
- _rangeMode(): boolean;
115
36
  connectedCallback(): void;
116
- _connectSingle(): void;
117
- /**
118
- * Find optional native time companions. They stay fully native (no hidden
119
- * input, no field controller); the picker only coordinates from/to order.
120
- * A bad composition never breaks the date picker: warn and leave times alone.
121
- */
122
- _discoverTimeFields(): void;
123
- _connectRange(): void;
124
37
  disconnectedCallback(): void;
125
- /** @param {DateFieldController | null} field */
126
- _teardownField(field: DateFieldController | null): void;
127
38
  /** @param {string} name @param {string|null} oldValue @param {string|null} newValue */
128
39
  attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
129
40
  /** @public */
@@ -171,7 +82,6 @@ export declare class DatePickerElement extends HTMLElement {
171
82
  get messages(): typeof import("./messages.js").DEFAULT_MESSAGES;
172
83
  /** @param {Partial<typeof import("./messages.js").DEFAULT_MESSAGES> | null} value */
173
84
  set messages(value: Partial<typeof import("./messages.js").DEFAULT_MESSAGES> | null);
174
- _refreshMessages(): void;
175
85
  /** @public @returns {import("./date-calendar.js").DateSource | null} */
176
86
  get source(): import("./date-calendar.js").DateSource | null;
177
87
  set source(value: import("./date-calendar.js").DateSource | null);
@@ -187,224 +97,6 @@ export declare class DatePickerElement extends HTMLElement {
187
97
  /** @public JS-only coordinate-space policy: "viewport" by default, "document" as an explicit opt-in; a change made while open applies to the next opening only. @returns {"viewport" | "document"} */
188
98
  get coordinateSpace(): "viewport" | "document";
189
99
  set coordinateSpace(value: "viewport" | "document");
190
- _adapter(): {
191
- locale: string;
192
- placeholder: string;
193
- format(value: string): string;
194
- parse(text: string): string;
195
- };
196
- _setupFormValue(): void;
197
- _syncInputState(): void;
198
- _syncRangeFields(): void;
199
- _setRangeAria(): void;
200
- /**
201
- * Create one calendar trigger. It is a real button overlaid inside the
202
- * field's own box (see the CSS): the field reserves the room, the trigger
203
- * stays transparent, and the field's focus ring wraps both.
204
- * @param {"" | "start" | "end"} endpoint
205
- * @returns {HTMLButtonElement}
206
- */
207
- _createTrigger(endpoint: "" | "start" | "end"): HTMLButtonElement;
208
- /**
209
- * Build the popup once and one trigger per field. Each trigger is inserted
210
- * immediately after its own field so DOM order follows visual/tab order
211
- * (`date -> its trigger -> next control`). The hidden ISO field may sit
212
- * between the two; it is not focusable.
213
- */
214
- _build(): void;
215
- /** @param {AbortSignal} signal @param {HTMLInputElement | null | undefined} input @param {() => void} restore */
216
- _bindPopup(signal: AbortSignal, input: HTMLInputElement | null | undefined, restore: () => void): void;
217
- _bind(): void;
218
- _bindRange(): void;
219
- /**
220
- * Point the shared calendar at one range bound and invalidate any pending
221
- * activation, so a late response can never commit on the wrong bound.
222
- * @param {"start" | "end"} endpoint
223
- */
224
- _activateRangeEndpoint(endpoint: "start" | "end"): void;
225
- /**
226
- * Range has one trigger per bound. A press on the other bound's trigger is a
227
- * local switch, not a toggle: it repoints the single open popup instead of
228
- * closing it. Only a press on the already-active bound toggles shut.
229
- * @param {"start" | "end"} endpoint
230
- * @param {MouseEvent} event
231
- */
232
- _onRangeTriggerClick(endpoint: "start" | "end", event: MouseEvent): void;
233
- /**
234
- * ArrowDown on a trigger opens the popup and drops into the grid, like the
235
- * field itself (Enter/Space go through the click path). Needed because focus
236
- * now returns to the trigger that opened the popover.
237
- * @param {"" | "start" | "end"} endpoint
238
- * @param {KeyboardEvent} event
239
- */
240
- _onTriggerKeyDown(endpoint: "" | "start" | "end", event: KeyboardEvent): void;
241
- /** @param {"" | "start" | "end"} endpoint @param {KeyboardEvent} event */
242
- _onFieldKeyDown(endpoint: "" | "start" | "end", event: KeyboardEvent): void;
243
- /** @param {"" | "start" | "end"} endpoint @param {FocusEvent} event */
244
- _onFieldFocus(endpoint: "" | "start" | "end", event: FocusEvent): void;
245
- /** Record grid activation intent (click or keyboard) so a stale,
246
- * late-confirmed dateactivate can be recognized and dropped. Each new grid
247
- * activation supersedes every earlier pending one.
248
- * @param {Event} event */
249
- _captureGridIntent(event: Event): void;
250
- /**
251
- * Hand a new intent to the calendar's next activation of `date`. Taking a
252
- * new generation supersedes every earlier pending activation.
253
- * @param {string} date @param {"" | "start" | "end"} endpoint
254
- */
255
- _attachIntent(date: string, endpoint: "" | "start" | "end"): void;
256
- /**
257
- * Invalidate every pending grid activation: each one carries its own intent,
258
- * which now fails the generation check. A programmatic
259
- * `calendar.activateDate()` carries none.
260
- */
261
- _supersedeActivations(): void;
262
- /** Whether one bound can receive a user pick right now.
263
- * @param {"start" | "end"} bound */
264
- _boundEditable(bound: "start" | "end"): boolean;
265
- /**
266
- * Single write path for a calendar-driven range change. Both fields are
267
- * synchronized before anything is announced, because one transition can move
268
- * both bounds at once (`10` then `5` commits `start 10 -> 5` together with
269
- * `end "" -> 10`); writing bound by bound would expose an intermediate
270
- * inverted pair. Synthetic `input`/`change` stay on the bounds that moved.
271
- * @param {import("./date-range.js").RangeTransition} transition
272
- */
273
- _applyRangeTransition(transition: import("./date-range.js").RangeTransition): void;
274
- /**
275
- * Project a candidate date onto the calendar band. Purely visual: no
276
- * `rangechange`, no field write, no validation and no source request — the
277
- * projection reads the month state already loaded, and the real availability
278
- * check still runs on commit.
279
- * @param {string} date @param {"" | "start" | "end"} [endpoint]
280
- */
281
- _setPreview(date: string, endpoint?: "" | "start" | "end"): void;
282
- /** Drop the projection and put the committed range back on the band. */
283
- _clearPreview(): void;
284
- /** @param {Event} event */
285
- _onGridDateFocus(event: Event): void;
286
- /**
287
- * Hover candidate. `focusedDate` stays the keyboard target only: pointer
288
- * hover never moves it, it just feeds the same projection.
289
- * @param {Event} event
290
- */
291
- _onGridPointerOver(event: Event): void;
292
- _onGridPointerLeave(): void;
293
- /** @param {EventTarget | null} target @returns {string} */
294
- _cellDate(target: EventTarget | null): string;
295
- /** @param {number} x @param {number} y @returns {string} */
296
- _dateAtPoint(x: number, y: number): string;
297
- /**
298
- * Arm an endpoint drag. Only a complete, ordered range offers two distinct
299
- * handles; a one-day range carries both markers on the same cell, and
300
- * guessing which one the user meant would be worse than not dragging.
301
- * @param {Event} event
302
- */
303
- _onGridPointerDown(event: Event): void;
304
- /** @param {Event} event */
305
- _onGridPointerMove(event: Event): void;
306
- /** @param {Event} event */
307
- _onGridPointerUp(event: Event): void;
308
- /** @param {Event} event */
309
- _onGridPointerCancel(event: Event): void;
310
- /** @param {number} pointerId */
311
- _releaseDragCapture(pointerId: number): void;
312
- /** Expose whether the band currently offers draggable handles, and whether
313
- * one is being dragged, so the cursor can say so. */
314
- _syncDragAffordance(): void;
315
- /** @param {KeyboardEvent} event */
316
- _onEscape(event: KeyboardEvent): void;
317
- _syncCalendarOptions(): void;
318
- _refreshLocale(): void;
319
- _refreshButtonLabel(): void;
320
- /** Each bound has its own trigger, so each needs its own accessible name: the
321
- * bound label is what tells two otherwise identical triggers apart. */
322
- _refreshRangeButtonLabel(): void;
323
- /** @param {string} value */
324
- _reflectValue(value: string): void;
325
- /** @param {string} value @param {{emit?:boolean,format?:boolean,reflect?:boolean,moveDisplay?:boolean}} [options] */
326
- _setValue(value: string, options?: {
327
- emit?: boolean;
328
- format?: boolean;
329
- reflect?: boolean;
330
- moveDisplay?: boolean;
331
- }): void;
332
- /**
333
- * Calendar-backed availability gate injected into the field controllers.
334
- * A cancelled or failed source load confirms nothing: do not fill the
335
- * submitted ISO field nor treat the date as available.
336
- * @param {string} date @param {{moveDisplay?: boolean}} [options]
337
- * @returns {Promise<{ok: boolean, message?: string}>}
338
- */
339
- _confirmDate(date: string, options?: {
340
- moveDisplay?: boolean;
341
- }): Promise<{
342
- ok: boolean;
343
- message?: string;
344
- }>;
345
- /** @param {boolean} emit @param {{force?: boolean, moveDisplay?: boolean}} [options] */
346
- _commitText(emit: boolean, options?: {
347
- force?: boolean;
348
- moveDisplay?: boolean;
349
- }): Promise<boolean>;
350
- /** @param {"start" | "end"} which */
351
- _commitFieldText(which: "start" | "end"): Promise<boolean>;
352
- /** @param {"start" | "end"} which @param {string} value
353
- * @param {{emit?:boolean, format?:boolean, user?:boolean}} [options] @returns {boolean} */
354
- _setBound(which: "start" | "end", value: string, options?: {
355
- emit?: boolean;
356
- format?: boolean;
357
- user?: boolean;
358
- }): boolean;
359
- /** Atomic pair update: canonical + visible + hidden before one event.
360
- * @param {string} start @param {string} end @param {{emit?:boolean, format?:boolean}} [options] */
361
- _setRange(start: string, end: string, options?: {
362
- emit?: boolean;
363
- format?: boolean;
364
- }): void;
365
- /** @param {"start" | "end"} which */
366
- _boundCanonical(which: "start" | "end"): string;
367
- _emitRangeChange(): void;
368
- /** Displayable projection of the business range. An inverted or start-less
369
- * pair shows no misleading band. @returns {{start:string, end:string}} */
370
- _displayRange(): {
371
- start: string;
372
- end: string;
373
- };
374
- /** Put the committed range back on the calendar band, dropping any
375
- * projection currently shown over it. */
376
- _syncHighlight(): void;
377
- /**
378
- * Cross-bound validity: the order error is attributed to the bound that was
379
- * just modified; the other bound only loses a stale order error, never its
380
- * independent parse/source validity.
381
- * @param {"start" | "end"} which
382
- */
383
- _revalidateRange(which: "start" | "end"): void;
384
- /**
385
- * Single-mode from/to order on the shared date: `start <= end`.
386
- * The order error is attributed to the bound that was just modified; the
387
- * other bound only loses a stale order error, never its native validity.
388
- * Missing, empty or disabled times fall back to no order constraint: times
389
- * are never implicitly required. Equality stays valid (duration rules are
390
- * application-owned).
391
- * @param {"start" | "end"} which
392
- */
393
- _revalidateTimeOrder(which: "start" | "end"): void;
394
- _clearTimeOrderValidity(): void;
395
- _restoreDefault(): void;
396
- _restoreRangeDefault(): void;
397
- /** @param {"start" | "end"} which */
398
- _focusField(which: "start" | "end"): void;
399
- /**
400
- * Restore focus to the control that opened the popover (single mode). focus()
401
- * must not run synchronously from a keydown handler (Chromium drops focus
402
- * changes there) and must not re-open the popover under the default
403
- * open-on-focus. Deferring keeps _suppressFocusOpen active for the call.
404
- */
405
- _restoreFocus(): void;
406
- /** @returns {"" | "start" | "end"} */
407
- _resolveRangeEndpoint(): "" | "start" | "end";
408
100
  /** @public */
409
101
  validate(): Promise<boolean>;
410
102
  /** @public @param {{moveFocus?:boolean, endpoint?:"start"|"end", returnFocus?:HTMLElement}} [options] */
@@ -413,14 +105,6 @@ export declare class DatePickerElement extends HTMLElement {
413
105
  endpoint?: "start" | "end";
414
106
  returnFocus?: HTMLElement;
415
107
  }): void;
416
- /** @param {boolean} expanded */
417
- _setExpanded(expanded: boolean): void;
418
108
  /** @public @param {boolean} [restoreFocus] */
419
109
  hide(restoreFocus?: boolean): void;
420
- /** The bound to restore focus to after closing a range popup: the active
421
- * endpoint first (it moved after the first selection), then the next
422
- * focusable bound.
423
- * @returns {"" | "start" | "end"} */
424
- _restoreFocusEndpoint(): "" | "start" | "end";
425
110
  }
426
- //# sourceMappingURL=date-picker.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"date.d.ts","sourceRoot":"","sources":["../../src/date.js"],"names":[],"mappings":"AAGA,2BAA2B;AAC3B,wBAAgB,UAAU,CAAC,IAAI,EADnB,MACmB,WAE9B;AAED,iDAAiD;AACjD,wBAAgB,WAAW,CAAC,IAAI,EADpB,MACoB,EAAE,KAAK,EADN,MACM,qBAGtC;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAHpB,MAGoB,GAFlB;IAAC,IAAI,EAAC,MAAM,CAAC;IAAC,KAAK,EAAC,MAAM,CAAC;IAAC,GAAG,EAAC,MAAM,CAAA;CAAC,GAAG,IAAI,CAW1D;AAED,4BAA4B;AAC5B,wBAAgB,MAAM,CAAC,KAAK,EADhB,MACgB,WAE3B;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAHnB,MAGmB,GAFjB,MAAM,CAYlB;AAED,qEAAqE;AACrE,wBAAgB,SAAS,CAAC,IAAI,EADlB,MACkB,EAAE,KAAK,EADJ,MACI,EAAE,GAAG,EADa,MACb,UAIzC;AAeD,4BAA4B;AAC5B,wBAAgB,UAAU,CAAC,KAAK,EADpB,MACoB,QAI/B;AAED,0CAA0C;AAC1C,wBAAgB,YAAY,CAAC,CAAC,EADlB,MACkB,EAAE,CAAC,EADH,MACG,cAGhC;AAED,mDAAmD;AACnD,wBAAgB,OAAO,CAAC,KAAK,EADjB,MACiB,EAAE,MAAM,EADH,MACG,UAOpC;AAED,mDAAmD;AACnD,wBAAgB,SAAS,CAAC,KAAK,EADnB,MACmB,EAAE,MAAM,EADL,MACK,UAUtC;AAED,mDAAmD;AACnD,wBAAgB,QAAQ,CAAC,KAAK,EADlB,MACkB,EAAE,MAAM,EADJ,MACI,UAOrC;AAED,mDAAmD;AACnD,wBAAgB,UAAU,CAAC,KAAK,EADpB,MACoB,EAAE,MAAM,EADN,MACM,UAGvC;AAED,4BAA4B;AAC5B,wBAAgB,YAAY,CAAC,KAAK,EADtB,MACsB,UAEjC;AAED,4BAA4B;AAC5B,wBAAgB,UAAU,CAAC,KAAK,EADpB,MACoB,UAI/B;AAED,qDAAqD;AACrD,wBAAgB,SAAS,CAAC,KAAK,EADM,MACN,UAI9B;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAF/B,MAE+B,UAKzC;AAED,qDAAqD;AACrD,wBAAgB,WAAW,CAAC,KAAK,EADrB,MACqB,EAAE,QAAQ,GADT,MACa,UAG9C;AAED,qDAAqD;AACrD,wBAAgB,SAAS,CAAC,KAAK,EADnB,MACmB,EAAE,QAAQ,GADP,MACW,UAE5C;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAHxB,MAGwB,EAAE,OAAO,AAFzC,CAA4D,EAApD;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,OAAO,CAAA;CAEF,cAsBhD;AAaD;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAJ5B,MAI4B,EAAE,QAAQ,AAH9C,CACA,EADQ,MAG0C,GAFxC,MAAM,CAyBlB;AAED,wEAAwE;AACxE,wBAAgB,SAAS,CAAC,KAAK,EADnB,MACmB,EAAE,GAAG,AADV,CAAsB,EAAd,MACO,EAAE,GAAG,AADE,CAAqB,EAAb,MACL,UAKlD;AAED,2EAA2E;AAC3E,wBAAgB,QAAQ,WAGvB;AAED,kDAAkD;AAClD,wBAAgB,aAAa,CAAC,KAAK,EADD,MACC,UAUlC"}
1
+ {"version":3,"file":"date.d.ts","sourceRoot":"","sources":["../../src/date.js"],"names":[],"mappings":"AAGA,2BAA2B;AAC3B,wBAAgB,UAAU,CAAC,IAAI,EADnB,MACmB,WAE9B;AAED,iDAAiD;AACjD,wBAAgB,WAAW,CAAC,IAAI,EADpB,MACoB,EAAE,KAAK,EADN,MACM,qBAGtC;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAHpB,MAGoB,GAFlB;IAAC,IAAI,EAAC,MAAM,CAAC;IAAC,KAAK,EAAC,MAAM,CAAC;IAAC,GAAG,EAAC,MAAM,CAAA;CAAC,GAAG,IAAI,CAW1D;AAED,4BAA4B;AAC5B,wBAAgB,MAAM,CAAC,KAAK,EADhB,MACgB,WAE3B;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAHnB,MAGmB,GAFjB,MAAM,CAelB;AAED,qEAAqE;AACrE,wBAAgB,SAAS,CAAC,IAAI,EADlB,MACkB,EAAE,KAAK,EADJ,MACI,EAAE,GAAG,EADa,MACb,UAIzC;AAeD,4BAA4B;AAC5B,wBAAgB,UAAU,CAAC,KAAK,EADpB,MACoB,QAI/B;AAED,0CAA0C;AAC1C,wBAAgB,YAAY,CAAC,CAAC,EADlB,MACkB,EAAE,CAAC,EADH,MACG,cAGhC;AAED,mDAAmD;AACnD,wBAAgB,OAAO,CAAC,KAAK,EADjB,MACiB,EAAE,MAAM,EADH,MACG,UAOpC;AAED,mDAAmD;AACnD,wBAAgB,SAAS,CAAC,KAAK,EADnB,MACmB,EAAE,MAAM,EADL,MACK,UAUtC;AAED,mDAAmD;AACnD,wBAAgB,QAAQ,CAAC,KAAK,EADlB,MACkB,EAAE,MAAM,EADJ,MACI,UAOrC;AAED,mDAAmD;AACnD,wBAAgB,UAAU,CAAC,KAAK,EADpB,MACoB,EAAE,MAAM,EADN,MACM,UAGvC;AAED,4BAA4B;AAC5B,wBAAgB,YAAY,CAAC,KAAK,EADtB,MACsB,UAEjC;AAED,4BAA4B;AAC5B,wBAAgB,UAAU,CAAC,KAAK,EADpB,MACoB,UAI/B;AAED,qDAAqD;AACrD,wBAAgB,SAAS,CAAC,KAAK,EADM,MACN,UAI9B;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAF/B,MAE+B,UAKzC;AAED,qDAAqD;AACrD,wBAAgB,WAAW,CAAC,KAAK,EADrB,MACqB,EAAE,QAAQ,GADT,MACa,UAG9C;AAED,qDAAqD;AACrD,wBAAgB,SAAS,CAAC,KAAK,EADnB,MACmB,EAAE,QAAQ,GADP,MACW,UAE5C;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAHxB,MAGwB,EAAE,OAAO,AAFzC,CAA4D,EAApD;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,OAAO,CAAA;CAEF,cAsBhD;AAaD;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAJ5B,MAI4B,EAAE,QAAQ,AAH9C,CACA,EADQ,MAG0C,GAFxC,MAAM,CAyBlB;AAED,wEAAwE;AACxE,wBAAgB,SAAS,CAAC,KAAK,EADnB,MACmB,EAAE,GAAG,AADV,CAAsB,EAAd,MACO,EAAE,GAAG,AADE,CAAqB,EAAb,MACL,UAKlD;AAED,2EAA2E;AAC3E,wBAAgB,QAAQ,WAGvB;AAED,kDAAkD;AAClD,wBAAgB,aAAa,CAAC,KAAK,EADD,MACC,UAUlC"}
package/docs/API.md CHANGED
@@ -149,7 +149,7 @@ Markup:
149
149
  </date-picker>
150
150
  ```
151
151
 
152
- The direct child text input is required. On enhancement its original `name` moves to an internal hidden input that submits the canonical ISO value.
152
+ The direct child text input is required. On enhancement its original `name` moves to an internal hidden input that submits the canonical ISO value. Dynamic `name`/`form`/`disabled` changes are synchronized by a `MutationObserver`; code that reads `FormData` in the same task as such a mutation must wait for the next microtask. Removing `name` from the visible enhanced input does not unregister the canonical field because that input no longer owns the submitted name; disable the field to omit it from submission.
153
153
 
154
154
  ### Attributes / properties
155
155
 
@@ -211,6 +211,8 @@ picker.hide();
211
211
  await picker.validate();
212
212
  ```
213
213
 
214
+ When disconnected, `validate()` cannot run source availability checks and returns the current native validity of the authored date/time inputs.
215
+
214
216
  ### Events
215
217
 
216
218
  - `valuechange` — canonical value changed;
@@ -126,6 +126,8 @@ Supported response shapes:
126
126
 
127
127
  No server URL, query parameter names or backend schema are hard-coded into the core. `createFetchSource()` is only a convenience adapter.
128
128
 
129
+ A commit belongs to the field controller and connection that started it. Disconnect tears controllers down: pending validation is invalidated, and neither the controller nor the picker may apply a result produced before the rebuild. A reconnected picker re-derives its state from the live DOM instead of resuming pending work.
130
+
129
131
  ## 7. Picker composition
130
132
 
131
133
  `<date-picker>` enhances a direct child text input.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lekoala/date-picker",
3
- "version": "0.5.2",
3
+ "version": "0.5.3",
4
4
  "description": "Native-first date picker and inline calendar primitives",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -141,7 +141,7 @@
141
141
  "lint": "biome check .",
142
142
  "format": "biome format --write .",
143
143
  "typecheck": "tsc -p jsconfig.json",
144
- "types": "tsc -p tsconfig.types.json",
144
+ "types": "tsc -p tsconfig.types.json && bun scripts/trim-types.js",
145
145
  "test": "bun test test/unit",
146
146
  "test:syntax": "bun scripts/check-syntax.js",
147
147
  "test:types": "tsc -p test/types/tsconfig.json",
@@ -163,7 +163,13 @@ export class DateCalendarElement extends HTMLElement {
163
163
  this._id = `date-calendar-${++uid}`;
164
164
  this._model = new CalendarModel();
165
165
  this._connected = false;
166
+ /** Connect/disconnect generation guarding deferred work. */
167
+ this._lifecycleId = 0;
166
168
  this._rendering = false;
169
+ /** Persistent live heading; render() updates its text after it is attached. @type {HTMLHeadingElement | null} */
170
+ this._heading = null;
171
+ /** Replaceable visual subtree kept separate from the persistent live heading. @type {HTMLDivElement | null} */
172
+ this._renderRoot = null;
167
173
  /** @type {DateSource | null} */
168
174
  this._source = null;
169
175
  /** Intent the host attached to the next activation of `date`.
@@ -207,6 +213,7 @@ export class DateCalendarElement extends HTMLElement {
207
213
 
208
214
  connectedCallback() {
209
215
  if (this._connected) return;
216
+ this._lifecycleId++;
210
217
  upgradeProperties(this, [
211
218
  "locale",
212
219
  "min",
@@ -248,6 +255,7 @@ export class DateCalendarElement extends HTMLElement {
248
255
  this._stopDefaultMessages?.();
249
256
  this._stopDefaultMessages = null;
250
257
  this._connected = false;
258
+ this._lifecycleId++;
251
259
  this._loadController?.abort();
252
260
  this._loadController = null;
253
261
  this._loadingKey = "";
@@ -258,12 +266,43 @@ export class DateCalendarElement extends HTMLElement {
258
266
  this.removeEventListener("focusin", this._onFocusIn);
259
267
  }
260
268
 
261
- attributeChangedCallback() {
262
- if (!this._connected || this._rendering) return;
269
+ /** @param {string} name @param {string|null} _oldValue @param {string|null} newValue */
270
+ attributeChangedCallback(name, _oldValue, newValue) {
271
+ if (this._rendering) return;
272
+ if (name === "value" && newValue && !isDate(newValue)) {
273
+ this._reflect("value", this._model.value);
274
+ return;
275
+ }
276
+ if ((name === "min" || name === "max") && newValue && !isDate(newValue)) {
277
+ this._reflect(name, "");
278
+ if (this._connected) {
279
+ this._syncModel();
280
+ this.render();
281
+ }
282
+ return;
283
+ }
284
+ if (name === "display" && newValue) {
285
+ try {
286
+ if (monthKey(newValue) !== newValue) throw new TypeError("Non-canonical month");
287
+ } catch {
288
+ this._reflect("display", this._model.display);
289
+ return;
290
+ }
291
+ }
292
+ if (!this._connected) return;
263
293
  this._syncModel();
264
294
  this.render();
265
295
  }
266
296
 
297
+ /** Run a focus microtask only in the lifecycle that scheduled it. @param {() => void} callback */
298
+ _queueFocus(callback) {
299
+ const lifecycleId = this._lifecycleId;
300
+ queueMicrotask(() => {
301
+ if (!this._connected || lifecycleId !== this._lifecycleId) return;
302
+ callback();
303
+ });
304
+ }
305
+
267
306
  /** @public */
268
307
  get locale() {
269
308
  return resolveLocale(this.getAttribute("locale") || "");
@@ -669,7 +708,7 @@ export class DateCalendarElement extends HTMLElement {
669
708
  // Same month: no full render, just move the roving tabindex.
670
709
  this._moveFocusDom(previousFocused, this._model.focused, moveFocus);
671
710
  }
672
- if (moveFocus) queueMicrotask(() => this.focusGrid());
711
+ if (moveFocus) this._queueFocus(() => this.focusGrid());
673
712
  }
674
713
 
675
714
  /** @param {string} previous @param {string} next @param {boolean} [moveFocus] */
@@ -781,6 +820,8 @@ export class DateCalendarElement extends HTMLElement {
781
820
  * @param {string} date @returns {Promise<boolean>}
782
821
  */
783
822
  async activateDate(date) {
823
+ if (!this._connected) return false;
824
+ const lifecycleId = this._lifecycleId;
784
825
  const hostIntent = this._hostIntent?.date === date ? this._hostIntent.intent : undefined;
785
826
  this._hostIntent = null;
786
827
  /** @param {Event} event */
@@ -792,6 +833,7 @@ export class DateCalendarElement extends HTMLElement {
792
833
  // source resolves must not be treated as available, and a cancelled or
793
834
  // failed load must not authorize it either.
794
835
  const confirmed = await this.ensureDate(date);
836
+ if (!this._connected || lifecycleId !== this._lifecycleId) return false;
795
837
  const state = this.getDateState(date);
796
838
  if (!confirmed || state.disabled) {
797
839
  this.dispatchEvent(tagged(new CustomEvent("dateinvalid", { detail: { date, state }, bubbles: true })));
@@ -1007,8 +1049,21 @@ export class DateCalendarElement extends HTMLElement {
1007
1049
  const yearControl = `<label class="dp-visually-hidden" for="${this._id}-year">${escapeHtml(this.messages.year)}</label>
1008
1050
  <input id="${this._id}-year" class="dp-year-input ${yearClass}" type="number" inputmode="numeric" value="${displayYear}"${minYearAttr}${maxYearAttr} aria-label="${escapeHtml(this.messages.year)}">`;
1009
1051
 
1052
+ if (!this._heading?.isConnected || !this._renderRoot?.isConnected) {
1053
+ this.replaceChildren();
1054
+ const heading = document.createElement("h2");
1055
+ heading.id = headingId;
1056
+ heading.className = "dp-calendar-heading dp-visually-hidden";
1057
+ heading.setAttribute("aria-live", "polite");
1058
+ const renderRoot = document.createElement("div");
1059
+ renderRoot.className = "dp-calendar-render";
1060
+ this.append(heading, renderRoot);
1061
+ this._heading = heading;
1062
+ this._renderRoot = renderRoot;
1063
+ }
1064
+
1010
1065
  this._rendering = true;
1011
- this.innerHTML = `
1066
+ this._renderRoot.innerHTML = `
1012
1067
  <div class="dp-calendar-shell">
1013
1068
  <div class="dp-calendar-header">
1014
1069
  ${yearFirst ? yearControl : monthControl}
@@ -1016,18 +1071,18 @@ export class DateCalendarElement extends HTMLElement {
1016
1071
  <button type="button" class="dp-nav dp-prev ${navClass}" data-calendar-action="previous" aria-label="${escapeHtml(this.messages.previousMonth)}"${prevDisabled ? " disabled" : ""}><svg aria-hidden="true" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="m4 10 4-4 4 4"/></svg></button>
1017
1072
  <button type="button" class="dp-nav dp-next ${navClass}" data-calendar-action="next" aria-label="${escapeHtml(this.messages.nextMonth)}"${nextDisabled ? " disabled" : ""}><svg aria-hidden="true" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="m4 6 4 4 4-4"/></svg></button>
1018
1073
  </div>
1019
- <h2 id="${headingId}" class="dp-calendar-heading dp-visually-hidden" aria-live="polite">${escapeHtml(formatMonthYear(`${display}-15`, locale))}</h2>
1020
1074
  <table id="${gridId}" class="dp-grid" role="grid" aria-labelledby="${headingId}">
1021
1075
  <thead><tr>${weekHeader}${dayHeaders}</tr></thead>
1022
1076
  <tbody>${rows}</tbody>
1023
1077
  </table>
1024
1078
  </div>`;
1079
+ this._heading.textContent = formatMonthYear(`${display}-15`, locale);
1025
1080
  this._rendering = false;
1026
1081
  this._paintedRange = { ...this._highlightedRange };
1027
1082
  this._paintedPreview = this.hasAttribute("data-range-preview");
1028
1083
 
1029
1084
  if (focusKey) {
1030
- queueMicrotask(() => {
1085
+ this._queueFocus(() => {
1031
1086
  let selector = focusKey;
1032
1087
  // A day-cell key goes stale across display changes: the old date can
1033
1088
  // reappear as outside-month padding in the new month (fixed-weeks or
package/src/date-field.js CHANGED
@@ -35,6 +35,8 @@ export class DateFieldController {
35
35
  this.canonical = "";
36
36
  this._dirty = false;
37
37
  this._commitId = 0;
38
+ /** A torn-down controller is terminal: no commit may start or resume. */
39
+ this._disposed = false;
38
40
  this._originalName = input.getAttribute("name") || "";
39
41
  /** @type {MutationObserver | null} */
40
42
  this._attributeObserver = null;
@@ -179,34 +181,44 @@ export class DateFieldController {
179
181
  * `options` is forwarded to the `confirm` gate so a revalidation can ask it
180
182
  * not to move the calendar display (source reloads must not re-home it).
181
183
  * @param {{moveDisplay?: boolean}} [options]
182
- * @returns {Promise<{status: "ok" | "clear" | "invalid" | "stale", value?: string}>}
184
+ * @returns {Promise<{status: "ok" | "clear" | "invalid" | "stale", value?: string, commitId: number}>}
183
185
  */
184
186
  async commit(options = {}) {
185
187
  const input = this.input;
186
188
  const commitId = ++this._commitId;
189
+ if (this._disposed) return { status: "stale", commitId };
187
190
  const text = input.value.trim();
188
191
  if (!text) {
189
- if (commitId !== this._commitId) return { status: "stale" };
192
+ if (commitId !== this._commitId) return { status: "stale", commitId };
190
193
  input.setCustomValidity("");
191
- return { status: "clear", value: "" };
194
+ return { status: "clear", value: "", commitId };
192
195
  }
193
196
  const parsed = this.adapter.parse(text);
194
197
  if (!parsed) {
195
- if (commitId !== this._commitId) return { status: "stale" };
198
+ if (commitId !== this._commitId) return { status: "stale", commitId };
196
199
  if (this.hidden) this.hidden.value = "";
197
200
  input.setCustomValidity(this.messages.invalidDate);
198
- return { status: "invalid" };
201
+ return { status: "invalid", commitId };
199
202
  }
200
203
  if (this.confirm) {
201
204
  const result = await this.confirm(parsed, options);
202
- if (commitId !== this._commitId) return { status: "stale" };
205
+ // A disconnect tears this controller down while the confirm is pending;
206
+ // its result must not touch validity or the hidden field any more.
207
+ if (this._disposed || commitId !== this._commitId) return { status: "stale", commitId };
203
208
  if (!result?.ok) {
204
209
  if (this.hidden) this.hidden.value = "";
205
210
  input.setCustomValidity(result?.message || this.messages.unavailableDate);
206
- return { status: "invalid" };
211
+ return { status: "invalid", commitId };
207
212
  }
208
213
  }
209
- return { status: "ok", value: parsed };
214
+ return { status: "ok", value: parsed, commitId };
215
+ }
216
+
217
+ /** A coordinator must re-check this after awaiting commit(), including for
218
+ * synchronous clear/invalid results whose Promise resumes in a microtask.
219
+ * @param {number} commitId */
220
+ isCommitCurrent(commitId) {
221
+ return commitId === this._commitId;
210
222
  }
211
223
 
212
224
  /**
@@ -234,8 +246,12 @@ export class DateFieldController {
234
246
  }
235
247
 
236
248
  /** Remove the hidden field and attribute observer, restoring the original
237
- * authored `name` on the visible input. */
249
+ * authored `name` on the visible input. A disconnect is a teardown, not a
250
+ * pause: in-flight commits are invalidated and later ones are refused, so a
251
+ * reconnected picker rebuilds from the live DOM instead of resuming them. */
238
252
  teardown() {
253
+ this.dirty();
254
+ this._disposed = true;
239
255
  this._attributeObserver?.disconnect();
240
256
  this._attributeObserver = null;
241
257
  const hiddenName = this.hidden?.getAttribute("name") || this._originalName;