@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/README.md +296 -294
- package/custom-elements.json +3 -0
- package/dist/date-picker.js +116 -56
- package/dist/date-picker.min.css +1 -1
- package/dist/date-picker.min.js +12 -12
- package/dist/date-picker.standalone.min.js +12 -505
- package/dist/types/activation-intents.d.ts +10 -0
- package/dist/types/activation-intents.d.ts.map +1 -0
- package/dist/types/date-calendar.d.ts +13 -2
- package/dist/types/date-calendar.d.ts.map +1 -1
- package/dist/types/date-field.d.ts +14 -0
- package/dist/types/date-field.d.ts.map +1 -1
- package/dist/types/date-picker.d.ts +44 -30
- package/dist/types/date-picker.d.ts.map +1 -1
- package/docs/API.md +442 -442
- package/docs/DECISIONS.md +74 -74
- package/docs/USE_CASES.md +156 -156
- package/package.json +2 -2
- package/src/activation-intents.js +9 -0
- package/src/date-calendar.js +38 -12
- package/src/date-field.js +32 -1
- package/src/date-picker.js +111 -68
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() {
|
package/src/date-picker.js
CHANGED
|
@@ -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
|
-
|
|
66
|
-
|
|
67
|
-
*
|
|
68
|
-
|
|
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 {
|
|
101
|
+
/** @type {import("./date-calendar.js").DateSource | null} */
|
|
99
102
|
this._source = null;
|
|
100
|
-
/** @type {
|
|
103
|
+
/** @type {import("./date-calendar.js").DateStateResolver | null} */
|
|
101
104
|
this._dateState = null;
|
|
102
|
-
/** @type {
|
|
105
|
+
/** @type {import("./date-calendar.js").DayRenderer | null} */
|
|
103
106
|
this._renderDay = null;
|
|
104
|
-
/** @type {
|
|
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.
|
|
170
|
-
//
|
|
171
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
246
|
-
startInput
|
|
247
|
-
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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
|
|
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
|
-
|
|
531
|
-
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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 (
|
|
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
|
-
|
|
977
|
-
|
|
978
|
-
|
|
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 &&
|
|
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.
|
|
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
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
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
|
|
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.
|
|
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 &&
|
|
1780
|
+
if (field && isEditable(field.input)) return active;
|
|
1738
1781
|
}
|
|
1739
1782
|
return this._resolveRangeEndpoint();
|
|
1740
1783
|
}
|