@lekoala/date-picker 0.5.0 → 0.5.2

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,21 +1,52 @@
1
1
  import { autoUpdate, reposition } from "@lekoala/floating";
2
- import { getDefaultControlClasses, resolveControlClasses, sameControlClasses } from "./control-classes.js";
2
+ import { activationIntents } from "./activation-intents.js";
3
+ import {
4
+ getDefaultControlClasses,
5
+ normalizeControlClasses,
6
+ resolveControlClasses,
7
+ setDefaultControlClasses,
8
+ } from "./control-classes.js";
3
9
  import { compareDates, isDate, monthKey, todayISO } from "./date.js";
4
10
  import { DateCalendarElement } from "./date-calendar.js";
5
- import { DateFieldController } from "./date-field.js";
11
+ import { DateFieldController, isEditable } from "./date-field.js";
6
12
  import { DateRangeController, normalizeRange } from "./date-range.js";
7
13
  import { createDateAdapter, formatLongDate, resolveLocale } from "./intl.js";
8
- import { getDefaultMessages } from "./messages.js";
14
+ import { getDefaultMessages, setDefaultMessages, subscribeDefaultMessages } from "./messages.js";
9
15
  import { compareTimes, isTime } from "./time.js";
16
+ import { upgradeProperties } from "./upgrade-properties.js";
10
17
 
11
18
  let uid = 0;
12
19
 
13
20
  /** Pointer travel, in CSS pixels, that turns a press into an endpoint drag. */
14
21
  const DRAG_THRESHOLD = 4;
15
22
 
23
+ /** A grid activation intent: the generation at interaction time plus the bound
24
+ * a drag drop targets, empty for a normal pick.
25
+ * @typedef {{id: number, endpoint: "" | "start" | "end"}} PickIntent */
26
+
16
27
  export class DatePickerElement extends HTMLElement {
17
28
  static observedAttributes = ["value", "locale", "min", "max", "open-on-focus", "month-format"];
18
29
 
30
+ /** @public @param {Partial<import("./control-classes.js").ControlClasses>} classes */
31
+ static setDefaultControlClasses(classes) {
32
+ setDefaultControlClasses(classes);
33
+ }
34
+
35
+ /** @public */
36
+ static getDefaultControlClasses() {
37
+ return getDefaultControlClasses();
38
+ }
39
+
40
+ /** @public @param {Partial<typeof import("./messages.js").DEFAULT_MESSAGES>} messages */
41
+ static setDefaultMessages(messages) {
42
+ setDefaultMessages(messages);
43
+ }
44
+
45
+ /** @public */
46
+ static getDefaultMessages() {
47
+ return getDefaultMessages();
48
+ }
49
+
19
50
  constructor() {
20
51
  super();
21
52
  this._id = `date-picker-${++uid}`;
@@ -36,12 +67,10 @@ export class DatePickerElement extends HTMLElement {
36
67
  this._timeOrderOwned = new Set();
37
68
  /** @type {"" | "start" | "end"} */
38
69
  this._lastFocusEndpoint = "";
39
- this._rangeCommitId = 0;
40
- /** Grid activation intents, keyed by date: the generation at interaction
41
- * time (range mode supersession/stale protection) plus the bound a drag
42
- * drop targets, empty for a normal pick.
43
- * @type {Map<string, {id: number, endpoint: "" | "start" | "end"}>} */
44
- this._pendingIntents = new Map();
70
+ /** Activation generation. A grid intent records it at interaction time
71
+ * and travels with its own activation (see `activationIntents`), so a late
72
+ * response is checked against the generation, never looked up by date. */
73
+ this._activationId = 0;
45
74
  /** Candidate date currently projected onto the calendar band, if any. */
46
75
  this._previewDate = "";
47
76
  /** Live endpoint drag, or null. @type {{pointerId:number, endpoint:"start"|"end", x:number, y:number, date:string, active:boolean} | null} */
@@ -63,15 +92,19 @@ export class DatePickerElement extends HTMLElement {
63
92
  this._stopTracking = null;
64
93
  this._open = false;
65
94
  this._suppressFocusOpen = false;
66
- this._messages = getDefaultMessages();
67
- this._controlClasses = getDefaultControlClasses();
68
- /** @type {any} */
95
+ /** @type {typeof import("./messages.js").DEFAULT_MESSAGES | null} */
96
+ this._messages = null;
97
+ /** @type {Partial<import("./control-classes.js").ControlClasses> | null} */
98
+ this._controlClasses = null;
99
+ /** @type {(() => void) | null} */
100
+ this._stopDefaultMessages = null;
101
+ /** @type {import("./date-calendar.js").DateSource | null} */
69
102
  this._source = null;
70
- /** @type {any} */
103
+ /** @type {import("./date-calendar.js").DateStateResolver | null} */
71
104
  this._dateState = null;
72
- /** @type {any} */
105
+ /** @type {import("./date-calendar.js").DayRenderer | null} */
73
106
  this._renderDay = null;
74
- /** @type {any} */
107
+ /** @type {import("./date-calendar.js").DateDisabledPredicate | null} */
75
108
  this._isDateDisabled = null;
76
109
  /** Public picker coordinate-space request. @type {"viewport" | "document"} */
77
110
  this._coordinateSpace = "viewport";
@@ -83,11 +116,32 @@ export class DatePickerElement extends HTMLElement {
83
116
 
84
117
  connectedCallback() {
85
118
  if (this._connected) return;
86
- if (this.hasAttribute("range")) {
87
- this._connectRange();
88
- return;
119
+ upgradeProperties(this, [
120
+ "locale",
121
+ "min",
122
+ "max",
123
+ "monthFormat",
124
+ "openOnFocus",
125
+ "coordinateSpace",
126
+ "controlClasses",
127
+ "messages",
128
+ "source",
129
+ "dateState",
130
+ "renderDay",
131
+ "isDateDisabled",
132
+ ]);
133
+ if (this.hasAttribute("range")) this._connectRange();
134
+ else this._connectSingle();
135
+ // The range setter requires the controller created by _connectRange().
136
+ if (this._connected) {
137
+ upgradeProperties(this, ["value", "range"]);
138
+ this._stopDefaultMessages = subscribeDefaultMessages(() => {
139
+ if (this._messages === null) this._refreshMessages();
140
+ });
141
+ // Applying the initial value clears custom validity; check it against
142
+ // min/max/isDateDisabled/source like any later constraint change.
143
+ void this.validate();
89
144
  }
90
- this._connectSingle();
91
145
  }
92
146
 
93
147
  _connectSingle() {
@@ -101,7 +155,7 @@ export class DatePickerElement extends HTMLElement {
101
155
  this._connected = true;
102
156
  this._input = input;
103
157
  this._discoverTimeFields();
104
- this._field = new DateFieldController(input, { messages: this._messages, locale: this.locale });
158
+ this._field = new DateFieldController(input, { messages: this.messages, locale: this.locale });
105
159
  this._field.onAttributesChanged = () => this._syncInputState();
106
160
  this._field.confirm = (date, options) => this._confirmDate(date, options);
107
161
  this._setupFormValue();
@@ -115,18 +169,14 @@ export class DatePickerElement extends HTMLElement {
115
169
  this.getAttribute("value") || (isDate(input.value) ? input.value : adapter.parse(input.value));
116
170
  if (initial && isDate(initial)) this._setValue(initial, { emit: false, format: true });
117
171
  else if (!input.value) this._setValue("", { emit: false, format: false });
118
- else input.setCustomValidity(this._messages.invalidDate);
172
+ else input.setCustomValidity(this.messages.invalidDate);
119
173
 
120
174
  // Native form reset restores control defaultValues, then _restoreDefault()
121
- // recommits the canonical value from them. Never redefine an author-provided
122
- // input.defaultValue here: the next reset must use the native default (which
123
- // the app may change). Only when enhancement itself supplies the initial
124
- // text (picker value attr, no input value attr) does that text become the
125
- // default to restore.
175
+ // recommits the canonical value from them. Only when enhancement itself
176
+ // supplies the initial text (picker value attr, no input value attr) does
177
+ // that text become the default to restore.
126
178
  if (this._field.hidden) this._field.hidden.defaultValue = this._value;
127
- if (input.getAttribute("value") == null && input.defaultValue === "") {
128
- input.defaultValue = input.value;
129
- }
179
+ this._field.adoptDefault();
130
180
  }
131
181
 
132
182
  /**
@@ -171,8 +221,8 @@ export class DatePickerElement extends HTMLElement {
171
221
  this._connected = true;
172
222
  this._input = null;
173
223
  this._fields = [
174
- new DateFieldController(startInput, { messages: this._messages, locale: this.locale }),
175
- new DateFieldController(endInput, { messages: this._messages, locale: this.locale }),
224
+ new DateFieldController(startInput, { messages: this.messages, locale: this.locale }),
225
+ new DateFieldController(endInput, { messages: this.messages, locale: this.locale }),
176
226
  ];
177
227
  this._range = new DateRangeController();
178
228
  this._rangeOriginalDescribedBy = [
@@ -193,22 +243,27 @@ export class DatePickerElement extends HTMLElement {
193
243
  const adapter = this._adapter();
194
244
  const initialStart = isDate(startInput.value) ? startInput.value : adapter.parse(startInput.value);
195
245
  const initialEnd = isDate(endInput.value) ? endInput.value : adapter.parse(endInput.value);
246
+ // Read the authored text before _setRange() reformats an unparsable one away.
247
+ const startText = startInput.value;
248
+ const endText = endInput.value;
196
249
  this._setRange(initialStart || "", initialEnd || "", { emit: false });
197
- if (startInput.value.trim() && !isDate(initialStart))
198
- startInput.setCustomValidity(this._messages.invalidDate);
199
- if (endInput.value.trim() && !isDate(initialEnd)) endInput.setCustomValidity(this._messages.invalidDate);
250
+ for (const [input, text, initial] of /** @type {const} */ ([
251
+ [startInput, startText, initialStart],
252
+ [endInput, endText, initialEnd],
253
+ ])) {
254
+ if (!text.trim() || isDate(initial)) continue;
255
+ input.value = text;
256
+ input.setCustomValidity(this.messages.invalidDate);
257
+ }
200
258
 
201
259
  if (this._fields[0].hidden) this._fields[0].hidden.defaultValue = this._range.start;
202
260
  if (this._fields[1].hidden) this._fields[1].hidden.defaultValue = this._range.end;
203
- if (startInput.getAttribute("value") == null && startInput.defaultValue === "") {
204
- startInput.defaultValue = startInput.value;
205
- }
206
- if (endInput.getAttribute("value") == null && endInput.defaultValue === "") {
207
- endInput.defaultValue = endInput.value;
208
- }
261
+ for (const field of this._fields) field.adoptDefault();
209
262
  }
210
263
 
211
264
  disconnectedCallback() {
265
+ this._stopDefaultMessages?.();
266
+ this._stopDefaultMessages = null;
212
267
  this._connected = false;
213
268
  this.hide(false);
214
269
  this._controller?.abort();
@@ -254,8 +309,10 @@ export class DatePickerElement extends HTMLElement {
254
309
  if (!this._connected || this._reflecting || oldValue === newValue) return;
255
310
  if (name === "value") {
256
311
  if (this._rangeMode()) return;
257
- if (!newValue || isDate(newValue))
312
+ if (!newValue || isDate(newValue)) {
313
+ this._supersedeActivations();
258
314
  this._setValue(newValue || "", { emit: false, format: true, reflect: false });
315
+ }
259
316
  return;
260
317
  }
261
318
  if (name === "locale") this._refreshLocale();
@@ -284,6 +341,7 @@ export class DatePickerElement extends HTMLElement {
284
341
  throw new TypeError("value is not available in range mode; use range");
285
342
  }
286
343
  if (value && !isDate(value)) throw new TypeError(`Invalid date-picker value: ${value}`);
344
+ this._supersedeActivations();
287
345
  this._setValue(value || "", { emit: false, format: true });
288
346
  }
289
347
 
@@ -295,6 +353,7 @@ export class DatePickerElement extends HTMLElement {
295
353
  set range(value) {
296
354
  if (!this._rangeMode()) throw new TypeError("range is only available on <date-picker range>");
297
355
  const { start, end } = normalizeRange(value);
356
+ this._supersedeActivations();
298
357
  this._setRange(start, end);
299
358
  }
300
359
 
@@ -356,37 +415,45 @@ export class DatePickerElement extends HTMLElement {
356
415
  }
357
416
 
358
417
  /**
359
- * Author classes for header controls; merged with global defaults.
418
+ * Author classes for header controls; omitted roles follow current global defaults.
360
419
  * @public
361
420
  * @returns {import("./control-classes.js").ControlClasses}
362
421
  */
363
422
  get controlClasses() {
364
- return { ...this._controlClasses };
423
+ return resolveControlClasses(this._controlClasses);
365
424
  }
366
425
 
367
426
  /** @param {Partial<import("./control-classes.js").ControlClasses> | null} value */
368
427
  set controlClasses(value) {
369
- const classes = resolveControlClasses(value);
370
- if (sameControlClasses(classes, this._controlClasses)) return;
371
- this._controlClasses = classes;
428
+ this._controlClasses = normalizeControlClasses(value);
372
429
  if (this._calendar) this._calendar.controlClasses = this._controlClasses;
373
430
  }
374
431
 
375
- /** @public */
432
+ /** @public @returns {typeof import("./messages.js").DEFAULT_MESSAGES} */
376
433
  get messages() {
377
- return this._messages;
434
+ return this._messages ?? getDefaultMessages();
378
435
  }
379
436
 
437
+ /** @param {Partial<typeof import("./messages.js").DEFAULT_MESSAGES> | null} value */
380
438
  set messages(value) {
381
- this._messages = { ...getDefaultMessages(), ...(value || {}) };
439
+ this._messages = value == null ? null : { ...getDefaultMessages(), ...value };
382
440
  if (this._calendar) this._calendar.messages = this._messages;
383
- for (const field of this._fields || []) field.setMessages(this._messages);
384
- this._field?.setMessages(this._messages);
441
+ this._refreshMessages();
442
+ }
443
+
444
+ _refreshMessages() {
445
+ const messages = this.messages;
446
+ for (const field of this._fields || []) field.setMessages(messages);
447
+ this._field?.setMessages(messages);
448
+ this._panel?.setAttribute("aria-label", messages.calendar);
449
+ const field = this._field ?? this._fields?.[0];
450
+ if (this._formatHint && field)
451
+ this._formatHint.textContent = `${messages.formatHint}: ${field.placeholder}`;
385
452
  this._refreshButtonLabel();
386
- void this.validate();
453
+ if (this._connected) void this.validate();
387
454
  }
388
455
 
389
- /** @public @returns {any} */
456
+ /** @public @returns {import("./date-calendar.js").DateSource | null} */
390
457
  get source() {
391
458
  return this._source;
392
459
  }
@@ -397,7 +464,7 @@ export class DatePickerElement extends HTMLElement {
397
464
  if (this._connected) void this.validate();
398
465
  }
399
466
 
400
- /** @public @returns {any} */
467
+ /** @public @returns {import("./date-calendar.js").DateStateResolver | null} */
401
468
  get dateState() {
402
469
  return this._dateState;
403
470
  }
@@ -408,7 +475,7 @@ export class DatePickerElement extends HTMLElement {
408
475
  if (this._connected) void this.validate();
409
476
  }
410
477
 
411
- /** @public @returns {any} */
478
+ /** @public @returns {import("./date-calendar.js").DayRenderer | null} */
412
479
  get renderDay() {
413
480
  return this._renderDay;
414
481
  }
@@ -418,7 +485,7 @@ export class DatePickerElement extends HTMLElement {
418
485
  if (this._calendar) this._calendar.renderDay = this._renderDay;
419
486
  }
420
487
 
421
- /** @public @returns {any} */
488
+ /** @public @returns {import("./date-calendar.js").DateDisabledPredicate | null} */
422
489
  get isDateDisabled() {
423
490
  return this._isDateDisabled;
424
491
  }
@@ -453,8 +520,10 @@ export class DatePickerElement extends HTMLElement {
453
520
  // movement lives in the field controller.
454
521
  this._field?.syncInputState();
455
522
  const button = this._buttons[0]?.button;
523
+ // Own state only: a disabled ancestor fieldset already disables the button
524
+ // natively, and copying it would stick once the fieldset is re-enabled.
456
525
  if (button) button.disabled = input.disabled || input.readOnly;
457
- if (this._open && (input.disabled || input.readOnly)) this.hide(false);
526
+ if (this._open && !isEditable(input)) this.hide(false);
458
527
  }
459
528
 
460
529
  _syncRangeFields() {
@@ -469,9 +538,8 @@ export class DatePickerElement extends HTMLElement {
469
538
  }
470
539
  this._syncDragAffordance();
471
540
  const active = this._range?.activeEndpoint === "end" ? 1 : 0;
472
- if (this._open && (this._fields[active]?.input.disabled || this._fields[active]?.input.readOnly)) {
473
- this.hide(false);
474
- }
541
+ const activeInput = this._fields[active]?.input;
542
+ if (this._open && activeInput && !isEditable(activeInput)) this.hide(false);
475
543
  }
476
544
 
477
545
  _setRangeAria() {
@@ -541,7 +609,7 @@ export class DatePickerElement extends HTMLElement {
541
609
  panel.className = "dp-picker-panel";
542
610
  panel.setAttribute("popover", "manual");
543
611
  panel.setAttribute("role", "dialog");
544
- panel.setAttribute("aria-label", this._messages.calendar);
612
+ panel.setAttribute("aria-label", this.messages.calendar);
545
613
 
546
614
  const calendar = new DateCalendarElement();
547
615
  calendar.selection = "none";
@@ -663,17 +731,27 @@ export class DatePickerElement extends HTMLElement {
663
731
  { signal },
664
732
  );
665
733
  button.addEventListener("keydown", (event) => this._onTriggerKeyDown("", event), { signal });
734
+ calendar.addEventListener("click", (event) => this._captureGridIntent(event), { capture: true, signal });
735
+ calendar.addEventListener("keydown", (event) => this._captureGridIntent(event), {
736
+ capture: true,
737
+ signal,
738
+ });
666
739
  calendar.addEventListener(
667
740
  "dateactivate",
668
741
  (event) => {
669
742
  const custom = /** @type {CustomEvent} */ (event);
670
743
  const date = custom.detail?.date;
671
744
  if (!isDate(date)) return;
745
+ const intent = /** @type {PickIntent | undefined} */ (activationIntents.get(custom));
672
746
  // The app listener on <date-picker> runs after this during bubbling;
673
747
  // defer the commit so preventDefault() is actually respected.
674
748
  queueMicrotask(() => {
675
749
  if (custom.defaultPrevented || !this._connected) return;
676
- if (this._input?.disabled || this._input?.readOnly) return;
750
+ // A grid pick that resolves after the popup closed, after a newer pick
751
+ // or after a programmatic value must not overwrite it. A programmatic
752
+ // calendar.activateDate() carries no intent and still commits.
753
+ if (intent && intent.id !== this._activationId) return;
754
+ if (!this._input || !isEditable(this._input)) return;
677
755
  this._field?.dirty();
678
756
  this._setValue(date, { emit: true, format: true });
679
757
  this.hide(false);
@@ -697,7 +775,16 @@ export class DatePickerElement extends HTMLElement {
697
775
 
698
776
  for (const [index, field] of this._fields.entries()) {
699
777
  const endpoint = index === 1 ? "end" : "start";
700
- field.input.addEventListener("input", () => field.handleInput(), { signal });
778
+ field.input.addEventListener(
779
+ "input",
780
+ () => {
781
+ // handleInput() rewrites the custom validity: any order error is gone.
782
+ field.handleInput();
783
+ this._orderTags.delete(index);
784
+ if (!field.isDirty) this._revalidateRange(endpoint);
785
+ },
786
+ { signal },
787
+ );
701
788
  field.input.addEventListener("change", () => void this._commitFieldText(endpoint), { signal });
702
789
  field.input.addEventListener("blur", () => void this._commitFieldText(endpoint), { signal });
703
790
  field.input.addEventListener(
@@ -748,11 +835,10 @@ export class DatePickerElement extends HTMLElement {
748
835
  if (!isDate(date)) return;
749
836
  queueMicrotask(() => {
750
837
  if (custom.defaultPrevented || !this._connected || !this._range) return;
751
- const intent = this._pendingIntents.get(date);
838
+ const intent = /** @type {PickIntent | undefined} */ (activationIntents.get(custom));
752
839
  // A response is dropped when the active endpoint changed, the popup
753
840
  // closed or a newer activation superseded this one.
754
- if (intent === undefined || intent.id !== this._rangeCommitId) return;
755
- this._pendingIntents.delete(date);
841
+ if (intent === undefined || intent.id !== this._activationId) return;
756
842
  const editable = (/** @type {"start" | "end"} */ bound) => this._boundEditable(bound);
757
843
  // Click, keyboard and drop share this one machine; only the entry
758
844
  // point differs, a drag naming the handle it grabbed.
@@ -805,8 +891,7 @@ export class DatePickerElement extends HTMLElement {
805
891
  const calendar = this._calendar;
806
892
  if (!this._range || !calendar) return;
807
893
  this._range.focus(endpoint);
808
- this._rangeCommitId++;
809
- this._pendingIntents.clear();
894
+ this._activationId++;
810
895
  this._lastFocusEndpoint = endpoint;
811
896
  const target = this._boundCanonical(endpoint) || todayISO();
812
897
  calendar.display = monthKey(target);
@@ -892,8 +977,7 @@ export class DatePickerElement extends HTMLElement {
892
977
  if (switched) {
893
978
  // A late calendar activation targeting the previous endpoint must not
894
979
  // commit after the intent moved to the other bound.
895
- this._rangeCommitId++;
896
- this._pendingIntents.clear();
980
+ this._activationId++;
897
981
  }
898
982
  if (!this.openOnFocus || this._suppressFocusOpen) return;
899
983
  const related = event.relatedTarget;
@@ -901,30 +985,49 @@ export class DatePickerElement extends HTMLElement {
901
985
  if (!this._open) this.show({ moveFocus: false });
902
986
  }
903
987
 
904
- /** Record range-mode grid activation intent (click or keyboard) so a stale,
988
+ /** Record grid activation intent (click or keyboard) so a stale,
905
989
  * late-confirmed dateactivate can be recognized and dropped. Each new grid
906
990
  * activation supersedes every earlier pending one.
907
991
  * @param {Event} event */
908
992
  _captureGridIntent(event) {
909
- if (!this._rangeMode() || !this._range) return;
993
+ if (this._rangeMode() ? !this._range : !this._field) return;
910
994
  if (event.type === "keydown") {
911
995
  const key = /** @type {KeyboardEvent} */ (event).key;
912
996
  if (key !== "Enter" && key !== " ") return;
913
997
  }
998
+ // The click tail of a drag never activates: it must not supersede the drop.
999
+ if (event.type === "click" && this._suppressGridClick) return;
914
1000
  if (!(event.target instanceof Element)) return;
915
1001
  const cell = event.target.closest(".dp-day[data-date]");
916
1002
  const date = cell?.getAttribute("data-date") || "";
917
1003
  if (!isDate(date)) return;
918
- // Each activation supersedes every earlier pending one.
919
- this._pendingIntents.clear();
920
- this._pendingIntents.set(date, { id: ++this._rangeCommitId, endpoint: "" });
1004
+ this._attachIntent(date, "");
1005
+ }
1006
+
1007
+ /**
1008
+ * Hand a new intent to the calendar's next activation of `date`. Taking a
1009
+ * new generation supersedes every earlier pending activation.
1010
+ * @param {string} date @param {"" | "start" | "end"} endpoint
1011
+ */
1012
+ _attachIntent(date, endpoint) {
1013
+ if (!this._calendar) return;
1014
+ this._calendar._hostIntent = { date, intent: { id: ++this._activationId, endpoint } };
1015
+ }
1016
+
1017
+ /**
1018
+ * Invalidate every pending grid activation: each one carries its own intent,
1019
+ * which now fails the generation check. A programmatic
1020
+ * `calendar.activateDate()` carries none.
1021
+ */
1022
+ _supersedeActivations() {
1023
+ this._activationId++;
921
1024
  }
922
1025
 
923
1026
  /** Whether one bound can receive a user pick right now.
924
1027
  * @param {"start" | "end"} bound */
925
1028
  _boundEditable(bound) {
926
1029
  const field = this._fields?.[bound === "end" ? 1 : 0];
927
- return field != null && !field.input.disabled && !field.input.readOnly;
1030
+ return field != null && isEditable(field.input);
928
1031
  }
929
1032
 
930
1033
  /**
@@ -1122,8 +1225,7 @@ export class DatePickerElement extends HTMLElement {
1122
1225
  // guard: a late response can never move a bound after the popup closed or
1123
1226
  // after a newer interaction.
1124
1227
  const target = drag.endpoint === "start" ? transition.range.start : transition.range.end;
1125
- this._pendingIntents.clear();
1126
- this._pendingIntents.set(target, { id: ++this._rangeCommitId, endpoint: drag.endpoint });
1228
+ this._attachIntent(target, drag.endpoint);
1127
1229
  void this._calendar?.activateDate(target);
1128
1230
  }
1129
1231
 
@@ -1184,7 +1286,7 @@ export class DatePickerElement extends HTMLElement {
1184
1286
  calendar.renderDay = this._renderDay;
1185
1287
  calendar.isDateDisabled = this._isDateDisabled;
1186
1288
  if (!this._rangeMode() && this._value) calendar.value = this._value;
1187
- if (panel) panel.setAttribute("aria-label", this._messages.calendar);
1289
+ if (panel) panel.setAttribute("aria-label", this.messages.calendar);
1188
1290
  }
1189
1291
 
1190
1292
  _refreshLocale() {
@@ -1193,7 +1295,7 @@ export class DatePickerElement extends HTMLElement {
1193
1295
  if (this._rangeMode() && this._fields) {
1194
1296
  this._fields[0].setLocale(this.locale);
1195
1297
  this._fields[1].setLocale(this.locale);
1196
- hint.textContent = `${this._messages.formatHint}: ${this._fields[0].placeholder}`;
1298
+ hint.textContent = `${this.messages.formatHint}: ${this._fields[0].placeholder}`;
1197
1299
  for (const [index, field] of this._fields.entries()) {
1198
1300
  if (!field.input.hasAttribute("placeholder") || this._generatedPlaceholder) {
1199
1301
  field.input.placeholder = field.placeholder;
@@ -1208,7 +1310,7 @@ export class DatePickerElement extends HTMLElement {
1208
1310
  const field = this._field;
1209
1311
  if (!field) return;
1210
1312
  field.setLocale(this.locale);
1211
- hint.textContent = `${this._messages.formatHint}: ${field.placeholder}`;
1313
+ hint.textContent = `${this.messages.formatHint}: ${field.placeholder}`;
1212
1314
  if (!field.input.hasAttribute("placeholder") || this._generatedPlaceholder) {
1213
1315
  field.input.placeholder = field.placeholder;
1214
1316
  this._generatedPlaceholder = true;
@@ -1224,7 +1326,7 @@ export class DatePickerElement extends HTMLElement {
1224
1326
  this._refreshRangeButtonLabel();
1225
1327
  return;
1226
1328
  }
1227
- const prefix = this._value ? this._messages.changeDate : this._messages.chooseDate;
1329
+ const prefix = this._value ? this.messages.changeDate : this.messages.chooseDate;
1228
1330
  const suffix = this._value ? `, ${formatLongDate(this._value, this.locale)}` : "";
1229
1331
  single.setAttribute("aria-label", `${prefix}${suffix}`);
1230
1332
  }
@@ -1236,9 +1338,9 @@ export class DatePickerElement extends HTMLElement {
1236
1338
  const { start, end } = this._range;
1237
1339
  for (const { button, endpoint } of this._buttons) {
1238
1340
  const bound = endpoint === "end" ? end : start;
1239
- const prefix = bound ? this._messages.changeDate : this._messages.chooseDate;
1341
+ const prefix = bound ? this.messages.changeDate : this.messages.chooseDate;
1240
1342
  const suffix = bound ? `, ${formatLongDate(bound, this.locale)}` : "";
1241
- const boundLabel = endpoint === "end" ? this._messages.rangeEnd : this._messages.rangeStart;
1343
+ const boundLabel = endpoint === "end" ? this.messages.rangeEnd : this.messages.rangeStart;
1242
1344
  button.setAttribute("aria-label", `${prefix}${suffix}, ${boundLabel}`);
1243
1345
  }
1244
1346
  }
@@ -1294,7 +1396,7 @@ export class DatePickerElement extends HTMLElement {
1294
1396
  }
1295
1397
  const confirmed = await calendar.ensureDate(date);
1296
1398
  const state = calendar.getDateState(date);
1297
- if (!confirmed || state.disabled) return { ok: false, message: this._messages.unavailableDate };
1399
+ if (!confirmed || state.disabled) return { ok: false, message: this.messages.unavailableDate };
1298
1400
  return { ok: true };
1299
1401
  }
1300
1402
 
@@ -1345,7 +1447,7 @@ export class DatePickerElement extends HTMLElement {
1345
1447
  if (!field) return false;
1346
1448
  // User initiated picks must respect disabled/readonly bounds; reset,
1347
1449
  // explicit validation and programmatic assignments keep their own path.
1348
- if (options.user === true && (field.input.disabled || field.input.readOnly)) return false;
1450
+ if (options.user === true && !isEditable(field.input)) return false;
1349
1451
  const next = value || "";
1350
1452
  // The range model may already hold the target (a calendar activation moved
1351
1453
  // it), so change detection reads the field canonical, not the model.
@@ -1425,7 +1527,7 @@ export class DatePickerElement extends HTMLElement {
1425
1527
  for (const bound of bounds) {
1426
1528
  const index = bound === "end" ? 1 : 0;
1427
1529
  const input = this._fields[index].input;
1428
- const message = bound === "end" ? this._messages.rangeOrderEnd : this._messages.rangeOrderStart;
1530
+ const message = bound === "end" ? this.messages.rangeOrderEnd : this.messages.rangeOrderStart;
1429
1531
  const shouldHaveOrder = inverted && bound === which;
1430
1532
  if (shouldHaveOrder) {
1431
1533
  input.setCustomValidity(message);
@@ -1451,8 +1553,8 @@ export class DatePickerElement extends HTMLElement {
1451
1553
  /** @type {["start", "end"]} */
1452
1554
  const bounds = ["start", "end"];
1453
1555
  const inputs = [startInput, endInput];
1454
- const startValue = startInput && !startInput.disabled ? startInput.value.trim() : "";
1455
- const endValue = endInput && !endInput.disabled ? endInput.value.trim() : "";
1556
+ const startValue = startInput && !startInput.matches(":disabled") ? startInput.value.trim() : "";
1557
+ const endValue = endInput && !endInput.matches(":disabled") ? endInput.value.trim() : "";
1456
1558
  const inverted =
1457
1559
  Boolean(startInput && endInput && startValue && endValue) &&
1458
1560
  isTime(startValue) &&
@@ -1461,7 +1563,7 @@ export class DatePickerElement extends HTMLElement {
1461
1563
  for (const [index, bound] of bounds.entries()) {
1462
1564
  const input = inputs[index];
1463
1565
  if (!input) continue;
1464
- const message = bound === "end" ? this._messages.rangeOrderEnd : this._messages.rangeOrderStart;
1566
+ const message = bound === "end" ? this.messages.rangeOrderEnd : this.messages.rangeOrderStart;
1465
1567
  const shouldHaveOrder = inverted && bound === which;
1466
1568
  if (shouldHaveOrder) {
1467
1569
  input.setCustomValidity(message);
@@ -1542,12 +1644,12 @@ export class DatePickerElement extends HTMLElement {
1542
1644
  if (!this._fields) return "";
1543
1645
  const preferred = this._lastFocusEndpoint || "start";
1544
1646
  const preferredIndex = preferred === "end" ? 1 : 0;
1545
- if (!this._fields[preferredIndex].input.disabled && !this._fields[preferredIndex].input.readOnly) {
1647
+ if (isEditable(this._fields[preferredIndex].input)) {
1546
1648
  return preferred;
1547
1649
  }
1548
1650
  const alternative = preferred === "end" ? "start" : "end";
1549
1651
  const alternativeIndex = alternative === "end" ? 1 : 0;
1550
- if (!this._fields[alternativeIndex].input.disabled && !this._fields[alternativeIndex].input.readOnly) {
1652
+ if (isEditable(this._fields[alternativeIndex].input)) {
1551
1653
  return alternative;
1552
1654
  }
1553
1655
  return "";
@@ -1563,6 +1665,8 @@ export class DatePickerElement extends HTMLElement {
1563
1665
  // Empty text flows through commit()'s "clear" outcome, not an early exit.
1564
1666
  for (const [index, field] of fields.entries()) {
1565
1667
  const result = await field.commit({ moveDisplay: false });
1668
+ // A parse/source error replaced the order error, which no longer owns it.
1669
+ if (result.status === "invalid") this._orderTags.delete(index);
1566
1670
  if (result.status === "ok" || result.status === "clear") {
1567
1671
  const which = index === 1 ? "end" : "start";
1568
1672
  this._setBound(which, result.value || "", { format: result.status === "ok" });
@@ -1574,11 +1678,9 @@ export class DatePickerElement extends HTMLElement {
1574
1678
  }
1575
1679
  const input = this._field?.input;
1576
1680
  if (!input) return true;
1577
- if (!input.value.trim()) {
1578
- input.setCustomValidity("");
1579
- } else {
1580
- await this._commitText(false, { force: true, moveDisplay: false });
1581
- }
1681
+ // Empty text flows through commit()'s "clear" outcome so the canonical
1682
+ // value and hidden ISO are cleared with it.
1683
+ await this._commitText(false, { force: true, moveDisplay: false });
1582
1684
  // Same start-then-end order as the range path, so a from/to inversion
1583
1685
  // ends reported on "end". A programmatic time change resyncs here too.
1584
1686
  this._revalidateTimeOrder("start");
@@ -1598,7 +1700,7 @@ export class DatePickerElement extends HTMLElement {
1598
1700
  this._activateRangeEndpoint(endpoint);
1599
1701
  } else {
1600
1702
  const input = this._input;
1601
- if (!input || input.disabled || input.readOnly) return;
1703
+ if (!input || !isEditable(input)) return;
1602
1704
  const target = this._value || this._adapter().parse(input.value) || todayISO();
1603
1705
  calendar.display = monthKey(target);
1604
1706
  calendar.focusedDate = target;
@@ -1646,8 +1748,7 @@ export class DatePickerElement extends HTMLElement {
1646
1748
  }
1647
1749
  this._open = false;
1648
1750
  this._setExpanded(false);
1649
- this._rangeCommitId++;
1650
- this._pendingIntents.clear();
1751
+ this._supersedeActivations();
1651
1752
  this._suppressGridClick = false;
1652
1753
  if (this._drag) {
1653
1754
  this._releaseDragCapture(this._drag.pointerId);
@@ -1676,7 +1777,7 @@ export class DatePickerElement extends HTMLElement {
1676
1777
  if (active) {
1677
1778
  const index = active === "end" ? 1 : 0;
1678
1779
  const field = this._fields?.[index];
1679
- if (field && !field.input.disabled && !field.input.readOnly) return active;
1780
+ if (field && isEditable(field.input)) return active;
1680
1781
  }
1681
1782
  return this._resolveRangeEndpoint();
1682
1783
  }