@lekoala/date-picker 0.5.1 → 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.
package/src/date-field.js CHANGED
@@ -1,6 +1,15 @@
1
1
  import { isDate } from "./date.js";
2
2
  import { createDateAdapter } from "./intl.js";
3
3
 
4
+ /**
5
+ * Whether the user may edit this input right now. `:disabled` is the effective
6
+ * state: the `disabled` property misses a disabled ancestor `<fieldset>`.
7
+ * @param {HTMLInputElement} input
8
+ */
9
+ export function isEditable(input) {
10
+ return !input.matches(":disabled") && !input.readOnly;
11
+ }
12
+
4
13
  /**
5
14
  * Owns a single editable form field inside a picker: the visible input, its
6
15
  * hidden canonical ISO field, the Intl adapter and the text commit pipeline
@@ -38,11 +47,33 @@ export class DateFieldController {
38
47
  * @type {((date: string, options?: {moveDisplay?: boolean}) => Promise<{ok: boolean, message?: string}>) | null}
39
48
  */
40
49
  this.confirm = null;
50
+ /** Canonical value behind a `defaultValue` the enhancement generated, or null. @type {string | null} */
51
+ this._generatedDefault = null;
41
52
  }
42
53
 
43
54
  /** @param {string} locale */
44
55
  setLocale(locale) {
56
+ const generated = this._generatedDefault;
57
+ const previous = generated ? this.adapter.format(generated) : null;
45
58
  this.adapter = createDateAdapter(locale || "");
59
+ // A generated default is localized text: reformat it so a later form reset
60
+ // never parses the old locale's text with the new adapter. An app-changed
61
+ // default no longer matches and stays untouched.
62
+ if (generated && this.input.defaultValue === previous) {
63
+ this.input.defaultValue = this.adapter.format(generated);
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Make the current text the reset default when the author provided none.
69
+ * Never redefine an author-provided `input.defaultValue`: the next reset must
70
+ * use the native default (which the app may change).
71
+ */
72
+ adoptDefault() {
73
+ const input = this.input;
74
+ if (input.getAttribute("value") != null || input.defaultValue !== "") return;
75
+ input.defaultValue = input.value;
76
+ this._generatedDefault = this.canonical && input.value ? this.canonical : null;
46
77
  }
47
78
 
48
79
  /** @param {Record<string, string>} messages */
@@ -65,7 +96,7 @@ export class DateFieldController {
65
96
  }
66
97
 
67
98
  get isDisabled() {
68
- return this.input.disabled;
99
+ return this.input.matches(":disabled");
69
100
  }
70
101
 
71
102
  get isReadOnly() {
@@ -1,4 +1,5 @@
1
1
  import { autoUpdate, reposition } from "@lekoala/floating";
2
+ import { activationIntents } from "./activation-intents.js";
2
3
  import {
3
4
  getDefaultControlClasses,
4
5
  normalizeControlClasses,
@@ -7,7 +8,7 @@ import {
7
8
  } from "./control-classes.js";
8
9
  import { compareDates, isDate, monthKey, todayISO } from "./date.js";
9
10
  import { DateCalendarElement } from "./date-calendar.js";
10
- import { DateFieldController } from "./date-field.js";
11
+ import { DateFieldController, isEditable } from "./date-field.js";
11
12
  import { DateRangeController, normalizeRange } from "./date-range.js";
12
13
  import { createDateAdapter, formatLongDate, resolveLocale } from "./intl.js";
13
14
  import { getDefaultMessages, setDefaultMessages, subscribeDefaultMessages } from "./messages.js";
@@ -19,6 +20,10 @@ let uid = 0;
19
20
  /** Pointer travel, in CSS pixels, that turns a press into an endpoint drag. */
20
21
  const DRAG_THRESHOLD = 4;
21
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
+
22
27
  export class DatePickerElement extends HTMLElement {
23
28
  static observedAttributes = ["value", "locale", "min", "max", "open-on-focus", "month-format"];
24
29
 
@@ -62,12 +67,10 @@ export class DatePickerElement extends HTMLElement {
62
67
  this._timeOrderOwned = new Set();
63
68
  /** @type {"" | "start" | "end"} */
64
69
  this._lastFocusEndpoint = "";
65
- this._rangeCommitId = 0;
66
- /** Grid activation intents, keyed by date: the generation at interaction
67
- * time (range mode supersession/stale protection) plus the bound a drag
68
- * drop targets, empty for a normal pick.
69
- * @type {Map<string, {id: number, endpoint: "" | "start" | "end"}>} */
70
- 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;
71
74
  /** Candidate date currently projected onto the calendar band, if any. */
72
75
  this._previewDate = "";
73
76
  /** Live endpoint drag, or null. @type {{pointerId:number, endpoint:"start"|"end", x:number, y:number, date:string, active:boolean} | null} */
@@ -95,13 +98,13 @@ export class DatePickerElement extends HTMLElement {
95
98
  this._controlClasses = null;
96
99
  /** @type {(() => void) | null} */
97
100
  this._stopDefaultMessages = null;
98
- /** @type {any} */
101
+ /** @type {import("./date-calendar.js").DateSource | null} */
99
102
  this._source = null;
100
- /** @type {any} */
103
+ /** @type {import("./date-calendar.js").DateStateResolver | null} */
101
104
  this._dateState = null;
102
- /** @type {any} */
105
+ /** @type {import("./date-calendar.js").DayRenderer | null} */
103
106
  this._renderDay = null;
104
- /** @type {any} */
107
+ /** @type {import("./date-calendar.js").DateDisabledPredicate | null} */
105
108
  this._isDateDisabled = null;
106
109
  /** Public picker coordinate-space request. @type {"viewport" | "document"} */
107
110
  this._coordinateSpace = "viewport";
@@ -135,6 +138,9 @@ export class DatePickerElement extends HTMLElement {
135
138
  this._stopDefaultMessages = subscribeDefaultMessages(() => {
136
139
  if (this._messages === null) this._refreshMessages();
137
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();
138
144
  }
139
145
  }
140
146
 
@@ -166,15 +172,11 @@ export class DatePickerElement extends HTMLElement {
166
172
  else input.setCustomValidity(this.messages.invalidDate);
167
173
 
168
174
  // Native form reset restores control defaultValues, then _restoreDefault()
169
- // recommits the canonical value from them. Never redefine an author-provided
170
- // input.defaultValue here: the next reset must use the native default (which
171
- // the app may change). Only when enhancement itself supplies the initial
172
- // text (picker value attr, no input value attr) does that text become the
173
- // 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.
174
178
  if (this._field.hidden) this._field.hidden.defaultValue = this._value;
175
- if (input.getAttribute("value") == null && input.defaultValue === "") {
176
- input.defaultValue = input.value;
177
- }
179
+ this._field.adoptDefault();
178
180
  }
179
181
 
180
182
  /**
@@ -241,19 +243,22 @@ export class DatePickerElement extends HTMLElement {
241
243
  const adapter = this._adapter();
242
244
  const initialStart = isDate(startInput.value) ? startInput.value : adapter.parse(startInput.value);
243
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;
244
249
  this._setRange(initialStart || "", initialEnd || "", { emit: false });
245
- if (startInput.value.trim() && !isDate(initialStart))
246
- startInput.setCustomValidity(this.messages.invalidDate);
247
- 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
+ }
248
258
 
249
259
  if (this._fields[0].hidden) this._fields[0].hidden.defaultValue = this._range.start;
250
260
  if (this._fields[1].hidden) this._fields[1].hidden.defaultValue = this._range.end;
251
- if (startInput.getAttribute("value") == null && startInput.defaultValue === "") {
252
- startInput.defaultValue = startInput.value;
253
- }
254
- if (endInput.getAttribute("value") == null && endInput.defaultValue === "") {
255
- endInput.defaultValue = endInput.value;
256
- }
261
+ for (const field of this._fields) field.adoptDefault();
257
262
  }
258
263
 
259
264
  disconnectedCallback() {
@@ -304,8 +309,10 @@ export class DatePickerElement extends HTMLElement {
304
309
  if (!this._connected || this._reflecting || oldValue === newValue) return;
305
310
  if (name === "value") {
306
311
  if (this._rangeMode()) return;
307
- if (!newValue || isDate(newValue))
312
+ if (!newValue || isDate(newValue)) {
313
+ this._supersedeActivations();
308
314
  this._setValue(newValue || "", { emit: false, format: true, reflect: false });
315
+ }
309
316
  return;
310
317
  }
311
318
  if (name === "locale") this._refreshLocale();
@@ -334,6 +341,7 @@ export class DatePickerElement extends HTMLElement {
334
341
  throw new TypeError("value is not available in range mode; use range");
335
342
  }
336
343
  if (value && !isDate(value)) throw new TypeError(`Invalid date-picker value: ${value}`);
344
+ this._supersedeActivations();
337
345
  this._setValue(value || "", { emit: false, format: true });
338
346
  }
339
347
 
@@ -345,6 +353,7 @@ export class DatePickerElement extends HTMLElement {
345
353
  set range(value) {
346
354
  if (!this._rangeMode()) throw new TypeError("range is only available on <date-picker range>");
347
355
  const { start, end } = normalizeRange(value);
356
+ this._supersedeActivations();
348
357
  this._setRange(start, end);
349
358
  }
350
359
 
@@ -444,7 +453,7 @@ export class DatePickerElement extends HTMLElement {
444
453
  if (this._connected) void this.validate();
445
454
  }
446
455
 
447
- /** @public @returns {any} */
456
+ /** @public @returns {import("./date-calendar.js").DateSource | null} */
448
457
  get source() {
449
458
  return this._source;
450
459
  }
@@ -455,7 +464,7 @@ export class DatePickerElement extends HTMLElement {
455
464
  if (this._connected) void this.validate();
456
465
  }
457
466
 
458
- /** @public @returns {any} */
467
+ /** @public @returns {import("./date-calendar.js").DateStateResolver | null} */
459
468
  get dateState() {
460
469
  return this._dateState;
461
470
  }
@@ -466,7 +475,7 @@ export class DatePickerElement extends HTMLElement {
466
475
  if (this._connected) void this.validate();
467
476
  }
468
477
 
469
- /** @public @returns {any} */
478
+ /** @public @returns {import("./date-calendar.js").DayRenderer | null} */
470
479
  get renderDay() {
471
480
  return this._renderDay;
472
481
  }
@@ -476,7 +485,7 @@ export class DatePickerElement extends HTMLElement {
476
485
  if (this._calendar) this._calendar.renderDay = this._renderDay;
477
486
  }
478
487
 
479
- /** @public @returns {any} */
488
+ /** @public @returns {import("./date-calendar.js").DateDisabledPredicate | null} */
480
489
  get isDateDisabled() {
481
490
  return this._isDateDisabled;
482
491
  }
@@ -511,8 +520,10 @@ export class DatePickerElement extends HTMLElement {
511
520
  // movement lives in the field controller.
512
521
  this._field?.syncInputState();
513
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.
514
525
  if (button) button.disabled = input.disabled || input.readOnly;
515
- if (this._open && (input.disabled || input.readOnly)) this.hide(false);
526
+ if (this._open && !isEditable(input)) this.hide(false);
516
527
  }
517
528
 
518
529
  _syncRangeFields() {
@@ -527,9 +538,8 @@ export class DatePickerElement extends HTMLElement {
527
538
  }
528
539
  this._syncDragAffordance();
529
540
  const active = this._range?.activeEndpoint === "end" ? 1 : 0;
530
- if (this._open && (this._fields[active]?.input.disabled || this._fields[active]?.input.readOnly)) {
531
- this.hide(false);
532
- }
541
+ const activeInput = this._fields[active]?.input;
542
+ if (this._open && activeInput && !isEditable(activeInput)) this.hide(false);
533
543
  }
534
544
 
535
545
  _setRangeAria() {
@@ -721,17 +731,27 @@ export class DatePickerElement extends HTMLElement {
721
731
  { signal },
722
732
  );
723
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
+ });
724
739
  calendar.addEventListener(
725
740
  "dateactivate",
726
741
  (event) => {
727
742
  const custom = /** @type {CustomEvent} */ (event);
728
743
  const date = custom.detail?.date;
729
744
  if (!isDate(date)) return;
745
+ const intent = /** @type {PickIntent | undefined} */ (activationIntents.get(custom));
730
746
  // The app listener on <date-picker> runs after this during bubbling;
731
747
  // defer the commit so preventDefault() is actually respected.
732
748
  queueMicrotask(() => {
733
749
  if (custom.defaultPrevented || !this._connected) return;
734
- 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;
735
755
  this._field?.dirty();
736
756
  this._setValue(date, { emit: true, format: true });
737
757
  this.hide(false);
@@ -755,7 +775,16 @@ export class DatePickerElement extends HTMLElement {
755
775
 
756
776
  for (const [index, field] of this._fields.entries()) {
757
777
  const endpoint = index === 1 ? "end" : "start";
758
- 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
+ );
759
788
  field.input.addEventListener("change", () => void this._commitFieldText(endpoint), { signal });
760
789
  field.input.addEventListener("blur", () => void this._commitFieldText(endpoint), { signal });
761
790
  field.input.addEventListener(
@@ -806,11 +835,10 @@ export class DatePickerElement extends HTMLElement {
806
835
  if (!isDate(date)) return;
807
836
  queueMicrotask(() => {
808
837
  if (custom.defaultPrevented || !this._connected || !this._range) return;
809
- const intent = this._pendingIntents.get(date);
838
+ const intent = /** @type {PickIntent | undefined} */ (activationIntents.get(custom));
810
839
  // A response is dropped when the active endpoint changed, the popup
811
840
  // closed or a newer activation superseded this one.
812
- if (intent === undefined || intent.id !== this._rangeCommitId) return;
813
- this._pendingIntents.delete(date);
841
+ if (intent === undefined || intent.id !== this._activationId) return;
814
842
  const editable = (/** @type {"start" | "end"} */ bound) => this._boundEditable(bound);
815
843
  // Click, keyboard and drop share this one machine; only the entry
816
844
  // point differs, a drag naming the handle it grabbed.
@@ -863,8 +891,7 @@ export class DatePickerElement extends HTMLElement {
863
891
  const calendar = this._calendar;
864
892
  if (!this._range || !calendar) return;
865
893
  this._range.focus(endpoint);
866
- this._rangeCommitId++;
867
- this._pendingIntents.clear();
894
+ this._activationId++;
868
895
  this._lastFocusEndpoint = endpoint;
869
896
  const target = this._boundCanonical(endpoint) || todayISO();
870
897
  calendar.display = monthKey(target);
@@ -950,8 +977,7 @@ export class DatePickerElement extends HTMLElement {
950
977
  if (switched) {
951
978
  // A late calendar activation targeting the previous endpoint must not
952
979
  // commit after the intent moved to the other bound.
953
- this._rangeCommitId++;
954
- this._pendingIntents.clear();
980
+ this._activationId++;
955
981
  }
956
982
  if (!this.openOnFocus || this._suppressFocusOpen) return;
957
983
  const related = event.relatedTarget;
@@ -959,30 +985,49 @@ export class DatePickerElement extends HTMLElement {
959
985
  if (!this._open) this.show({ moveFocus: false });
960
986
  }
961
987
 
962
- /** Record range-mode grid activation intent (click or keyboard) so a stale,
988
+ /** Record grid activation intent (click or keyboard) so a stale,
963
989
  * late-confirmed dateactivate can be recognized and dropped. Each new grid
964
990
  * activation supersedes every earlier pending one.
965
991
  * @param {Event} event */
966
992
  _captureGridIntent(event) {
967
- if (!this._rangeMode() || !this._range) return;
993
+ if (this._rangeMode() ? !this._range : !this._field) return;
968
994
  if (event.type === "keydown") {
969
995
  const key = /** @type {KeyboardEvent} */ (event).key;
970
996
  if (key !== "Enter" && key !== " ") return;
971
997
  }
998
+ // The click tail of a drag never activates: it must not supersede the drop.
999
+ if (event.type === "click" && this._suppressGridClick) return;
972
1000
  if (!(event.target instanceof Element)) return;
973
1001
  const cell = event.target.closest(".dp-day[data-date]");
974
1002
  const date = cell?.getAttribute("data-date") || "";
975
1003
  if (!isDate(date)) return;
976
- // Each activation supersedes every earlier pending one.
977
- this._pendingIntents.clear();
978
- 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++;
979
1024
  }
980
1025
 
981
1026
  /** Whether one bound can receive a user pick right now.
982
1027
  * @param {"start" | "end"} bound */
983
1028
  _boundEditable(bound) {
984
1029
  const field = this._fields?.[bound === "end" ? 1 : 0];
985
- return field != null && !field.input.disabled && !field.input.readOnly;
1030
+ return field != null && isEditable(field.input);
986
1031
  }
987
1032
 
988
1033
  /**
@@ -1180,8 +1225,7 @@ export class DatePickerElement extends HTMLElement {
1180
1225
  // guard: a late response can never move a bound after the popup closed or
1181
1226
  // after a newer interaction.
1182
1227
  const target = drag.endpoint === "start" ? transition.range.start : transition.range.end;
1183
- this._pendingIntents.clear();
1184
- this._pendingIntents.set(target, { id: ++this._rangeCommitId, endpoint: drag.endpoint });
1228
+ this._attachIntent(target, drag.endpoint);
1185
1229
  void this._calendar?.activateDate(target);
1186
1230
  }
1187
1231
 
@@ -1403,7 +1447,7 @@ export class DatePickerElement extends HTMLElement {
1403
1447
  if (!field) return false;
1404
1448
  // User initiated picks must respect disabled/readonly bounds; reset,
1405
1449
  // explicit validation and programmatic assignments keep their own path.
1406
- if (options.user === true && (field.input.disabled || field.input.readOnly)) return false;
1450
+ if (options.user === true && !isEditable(field.input)) return false;
1407
1451
  const next = value || "";
1408
1452
  // The range model may already hold the target (a calendar activation moved
1409
1453
  // it), so change detection reads the field canonical, not the model.
@@ -1509,8 +1553,8 @@ export class DatePickerElement extends HTMLElement {
1509
1553
  /** @type {["start", "end"]} */
1510
1554
  const bounds = ["start", "end"];
1511
1555
  const inputs = [startInput, endInput];
1512
- const startValue = startInput && !startInput.disabled ? startInput.value.trim() : "";
1513
- 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() : "";
1514
1558
  const inverted =
1515
1559
  Boolean(startInput && endInput && startValue && endValue) &&
1516
1560
  isTime(startValue) &&
@@ -1600,12 +1644,12 @@ export class DatePickerElement extends HTMLElement {
1600
1644
  if (!this._fields) return "";
1601
1645
  const preferred = this._lastFocusEndpoint || "start";
1602
1646
  const preferredIndex = preferred === "end" ? 1 : 0;
1603
- if (!this._fields[preferredIndex].input.disabled && !this._fields[preferredIndex].input.readOnly) {
1647
+ if (isEditable(this._fields[preferredIndex].input)) {
1604
1648
  return preferred;
1605
1649
  }
1606
1650
  const alternative = preferred === "end" ? "start" : "end";
1607
1651
  const alternativeIndex = alternative === "end" ? 1 : 0;
1608
- if (!this._fields[alternativeIndex].input.disabled && !this._fields[alternativeIndex].input.readOnly) {
1652
+ if (isEditable(this._fields[alternativeIndex].input)) {
1609
1653
  return alternative;
1610
1654
  }
1611
1655
  return "";
@@ -1621,6 +1665,8 @@ export class DatePickerElement extends HTMLElement {
1621
1665
  // Empty text flows through commit()'s "clear" outcome, not an early exit.
1622
1666
  for (const [index, field] of fields.entries()) {
1623
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);
1624
1670
  if (result.status === "ok" || result.status === "clear") {
1625
1671
  const which = index === 1 ? "end" : "start";
1626
1672
  this._setBound(which, result.value || "", { format: result.status === "ok" });
@@ -1632,11 +1678,9 @@ export class DatePickerElement extends HTMLElement {
1632
1678
  }
1633
1679
  const input = this._field?.input;
1634
1680
  if (!input) return true;
1635
- if (!input.value.trim()) {
1636
- input.setCustomValidity("");
1637
- } else {
1638
- await this._commitText(false, { force: true, moveDisplay: false });
1639
- }
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 });
1640
1684
  // Same start-then-end order as the range path, so a from/to inversion
1641
1685
  // ends reported on "end". A programmatic time change resyncs here too.
1642
1686
  this._revalidateTimeOrder("start");
@@ -1656,7 +1700,7 @@ export class DatePickerElement extends HTMLElement {
1656
1700
  this._activateRangeEndpoint(endpoint);
1657
1701
  } else {
1658
1702
  const input = this._input;
1659
- if (!input || input.disabled || input.readOnly) return;
1703
+ if (!input || !isEditable(input)) return;
1660
1704
  const target = this._value || this._adapter().parse(input.value) || todayISO();
1661
1705
  calendar.display = monthKey(target);
1662
1706
  calendar.focusedDate = target;
@@ -1704,8 +1748,7 @@ export class DatePickerElement extends HTMLElement {
1704
1748
  }
1705
1749
  this._open = false;
1706
1750
  this._setExpanded(false);
1707
- this._rangeCommitId++;
1708
- this._pendingIntents.clear();
1751
+ this._supersedeActivations();
1709
1752
  this._suppressGridClick = false;
1710
1753
  if (this._drag) {
1711
1754
  this._releaseDragCapture(this._drag.pointerId);
@@ -1734,7 +1777,7 @@ export class DatePickerElement extends HTMLElement {
1734
1777
  if (active) {
1735
1778
  const index = active === "end" ? 1 : 0;
1736
1779
  const field = this._fields?.[index];
1737
- if (field && !field.input.disabled && !field.input.readOnly) return active;
1780
+ if (field && isEditable(field.input)) return active;
1738
1781
  }
1739
1782
  return this._resolveRangeEndpoint();
1740
1783
  }