@lekoala/date-picker 0.5.1 → 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.
- package/README.md +296 -294
- package/custom-elements.json +3 -0
- package/dist/date-picker.js +256 -91
- package/dist/date-picker.min.css +1 -1
- package/dist/date-picker.min.js +11 -12
- package/dist/date-picker.standalone.min.js +12 -506
- package/dist/types/date-calendar.d.ts +5 -90
- package/dist/types/date-picker.d.ts +12 -314
- package/dist/types/date.d.ts.map +1 -1
- package/docs/API.md +444 -442
- package/docs/ARCHITECTURE.md +2 -0
- package/docs/DECISIONS.md +74 -74
- package/docs/USE_CASES.md +156 -156
- package/package.json +3 -3
- package/src/activation-intents.js +9 -0
- package/src/date-calendar.js +99 -18
- package/src/date-field.js +57 -10
- package/src/date-picker.js +187 -86
- package/src/date.js +4 -1
- package/dist/types/date-calendar.d.ts.map +0 -1
- package/dist/types/date-field.d.ts +0 -104
- package/dist/types/date-field.d.ts.map +0 -1
- package/dist/types/date-picker.d.ts.map +0 -1
- package/dist/types/time.d.ts +0 -8
- package/dist/types/time.d.ts.map +0 -1
- package/dist/types/upgrade-properties.d.ts +0 -8
- package/dist/types/upgrade-properties.d.ts.map +0 -1
package/src/date-calendar.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { activationIntents } from "./activation-intents.js";
|
|
1
2
|
import { CalendarModel } from "./calendar-model.js";
|
|
2
3
|
import {
|
|
3
4
|
getDefaultControlClasses,
|
|
@@ -162,9 +163,18 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
162
163
|
this._id = `date-calendar-${++uid}`;
|
|
163
164
|
this._model = new CalendarModel();
|
|
164
165
|
this._connected = false;
|
|
166
|
+
/** Connect/disconnect generation guarding deferred work. */
|
|
167
|
+
this._lifecycleId = 0;
|
|
165
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;
|
|
166
173
|
/** @type {DateSource | null} */
|
|
167
174
|
this._source = null;
|
|
175
|
+
/** Intent the host attached to the next activation of `date`.
|
|
176
|
+
* @type {{date: string, intent: unknown} | null} */
|
|
177
|
+
this._hostIntent = null;
|
|
168
178
|
this._sourceStates = new Map();
|
|
169
179
|
this._loadedRanges = new Set();
|
|
170
180
|
this._loadController = null;
|
|
@@ -203,6 +213,7 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
203
213
|
|
|
204
214
|
connectedCallback() {
|
|
205
215
|
if (this._connected) return;
|
|
216
|
+
this._lifecycleId++;
|
|
206
217
|
upgradeProperties(this, [
|
|
207
218
|
"locale",
|
|
208
219
|
"min",
|
|
@@ -244,6 +255,7 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
244
255
|
this._stopDefaultMessages?.();
|
|
245
256
|
this._stopDefaultMessages = null;
|
|
246
257
|
this._connected = false;
|
|
258
|
+
this._lifecycleId++;
|
|
247
259
|
this._loadController?.abort();
|
|
248
260
|
this._loadController = null;
|
|
249
261
|
this._loadingKey = "";
|
|
@@ -254,12 +266,43 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
254
266
|
this.removeEventListener("focusin", this._onFocusIn);
|
|
255
267
|
}
|
|
256
268
|
|
|
257
|
-
|
|
258
|
-
|
|
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;
|
|
259
293
|
this._syncModel();
|
|
260
294
|
this.render();
|
|
261
295
|
}
|
|
262
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
|
+
|
|
263
306
|
/** @public */
|
|
264
307
|
get locale() {
|
|
265
308
|
return resolveLocale(this.getAttribute("locale") || "");
|
|
@@ -537,7 +580,7 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
537
580
|
return { start: first[0], end: last[6] };
|
|
538
581
|
}
|
|
539
582
|
|
|
540
|
-
/** @public @param {string} date */
|
|
583
|
+
/** @public @param {string} date @returns {DateState & {disabled: boolean}} */
|
|
541
584
|
getDateState(date) {
|
|
542
585
|
const fromSource = this._sourceStates.get(date) || {};
|
|
543
586
|
const local = this._dateState?.(date, { ...fromSource }) || {};
|
|
@@ -665,7 +708,7 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
665
708
|
// Same month: no full render, just move the roving tabindex.
|
|
666
709
|
this._moveFocusDom(previousFocused, this._model.focused, moveFocus);
|
|
667
710
|
}
|
|
668
|
-
if (moveFocus)
|
|
711
|
+
if (moveFocus) this._queueFocus(() => this.focusGrid());
|
|
669
712
|
}
|
|
670
713
|
|
|
671
714
|
/** @param {string} previous @param {string} next @param {boolean} [moveFocus] */
|
|
@@ -681,6 +724,16 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
681
724
|
}
|
|
682
725
|
}
|
|
683
726
|
|
|
727
|
+
/** Leave exactly one roving tab stop, on `date`'s cell when rendered.
|
|
728
|
+
* @param {string} date */
|
|
729
|
+
_syncRovingDom(date) {
|
|
730
|
+
for (const cell of this.querySelectorAll('.dp-day[tabindex="0"]')) {
|
|
731
|
+
if (cell instanceof HTMLElement && cell.dataset.date !== date) cell.tabIndex = -1;
|
|
732
|
+
}
|
|
733
|
+
const cell = this.querySelector(`.dp-day[data-date="${CSS.escape(date)}"]`);
|
|
734
|
+
if (cell instanceof HTMLElement) cell.tabIndex = 0;
|
|
735
|
+
}
|
|
736
|
+
|
|
684
737
|
/** @param {string} previousValue @param {string} nextValue */
|
|
685
738
|
_moveSelectionDom(previousValue, nextValue) {
|
|
686
739
|
if (previousValue) {
|
|
@@ -767,21 +820,33 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
767
820
|
* @param {string} date @returns {Promise<boolean>}
|
|
768
821
|
*/
|
|
769
822
|
async activateDate(date) {
|
|
823
|
+
if (!this._connected) return false;
|
|
824
|
+
const lifecycleId = this._lifecycleId;
|
|
825
|
+
const hostIntent = this._hostIntent?.date === date ? this._hostIntent.intent : undefined;
|
|
826
|
+
this._hostIntent = null;
|
|
827
|
+
/** @param {Event} event */
|
|
828
|
+
const tagged = (event) => {
|
|
829
|
+
if (hostIntent !== undefined) activationIntents.set(event, hostIntent);
|
|
830
|
+
return event;
|
|
831
|
+
};
|
|
770
832
|
// Guarantee remote state before deciding: a date clicked before the
|
|
771
833
|
// source resolves must not be treated as available, and a cancelled or
|
|
772
834
|
// failed load must not authorize it either.
|
|
773
835
|
const confirmed = await this.ensureDate(date);
|
|
836
|
+
if (!this._connected || lifecycleId !== this._lifecycleId) return false;
|
|
774
837
|
const state = this.getDateState(date);
|
|
775
838
|
if (!confirmed || state.disabled) {
|
|
776
|
-
this.dispatchEvent(new CustomEvent("dateinvalid", { detail: { date, state }, bubbles: true }));
|
|
839
|
+
this.dispatchEvent(tagged(new CustomEvent("dateinvalid", { detail: { date, state }, bubbles: true })));
|
|
777
840
|
return false;
|
|
778
841
|
}
|
|
779
842
|
const accepted = this.dispatchEvent(
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
843
|
+
tagged(
|
|
844
|
+
new CustomEvent("dateactivate", {
|
|
845
|
+
detail: { date, state },
|
|
846
|
+
bubbles: true,
|
|
847
|
+
cancelable: true,
|
|
848
|
+
}),
|
|
849
|
+
),
|
|
785
850
|
);
|
|
786
851
|
if (!accepted) return false;
|
|
787
852
|
if (this.selection === "single") {
|
|
@@ -820,6 +885,8 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
820
885
|
this._model.setFocused(this._clamp(date));
|
|
821
886
|
this._reflect("display", this._model.display);
|
|
822
887
|
if (previousDisplay !== this.display) this._emitDisplayChange();
|
|
888
|
+
// Engines that do not focus a clicked cell still need the tab stop moved.
|
|
889
|
+
else this._syncRovingDom(this._model.focused);
|
|
823
890
|
void this.activateDate(date);
|
|
824
891
|
}
|
|
825
892
|
|
|
@@ -832,9 +899,8 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
832
899
|
}
|
|
833
900
|
if (target instanceof HTMLInputElement && target.matches(".dp-year-input")) {
|
|
834
901
|
const year = Number(target.value);
|
|
835
|
-
// 9999
|
|
836
|
-
|
|
837
|
-
if (!Number.isInteger(year) || year < 1 || year > 9998) {
|
|
902
|
+
// 9999-12 overflows into 10000, but _setDisplay() clamps it to 9999-11.
|
|
903
|
+
if (!Number.isInteger(year) || year < 1 || year > 9999) {
|
|
838
904
|
if (this._connected) this.render(false);
|
|
839
905
|
return;
|
|
840
906
|
}
|
|
@@ -855,6 +921,8 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
855
921
|
const date = target.dataset.date || "";
|
|
856
922
|
if (!isDate(date)) return;
|
|
857
923
|
this._model.focused = date;
|
|
924
|
+
// A pointer focuses the cell directly; keep exactly one tab stop on it.
|
|
925
|
+
this._syncRovingDom(date);
|
|
858
926
|
this.dispatchEvent(
|
|
859
927
|
new CustomEvent("datefocus", { detail: { date, state: this.getDateState(date) }, bubbles: true }),
|
|
860
928
|
);
|
|
@@ -915,8 +983,8 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
915
983
|
const nextKey = stepMonthKey(display, 1);
|
|
916
984
|
const prevDisabled = Boolean(minMonth && prevKey !== null && prevKey < minMonth);
|
|
917
985
|
const nextDisabled = Boolean(maxMonth && nextKey !== null && nextKey > maxMonth);
|
|
918
|
-
const minYearAttr =
|
|
919
|
-
const maxYearAttr =
|
|
986
|
+
const minYearAttr = ` min="${this.min ? Number(this.min.slice(0, 4)) : 1}"`;
|
|
987
|
+
const maxYearAttr = ` max="${this.max ? Number(this.max.slice(0, 4)) : 9999}"`;
|
|
920
988
|
const showWeeks = this.showWeekNumbers && this.firstDay === 1;
|
|
921
989
|
const weekHeader = showWeeks
|
|
922
990
|
? `<th scope="col" class="dp-week-heading" abbr="${escapeHtml(this.messages.week)}">#</th>`
|
|
@@ -981,8 +1049,21 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
981
1049
|
const yearControl = `<label class="dp-visually-hidden" for="${this._id}-year">${escapeHtml(this.messages.year)}</label>
|
|
982
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)}">`;
|
|
983
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
|
+
|
|
984
1065
|
this._rendering = true;
|
|
985
|
-
this.innerHTML = `
|
|
1066
|
+
this._renderRoot.innerHTML = `
|
|
986
1067
|
<div class="dp-calendar-shell">
|
|
987
1068
|
<div class="dp-calendar-header">
|
|
988
1069
|
${yearFirst ? yearControl : monthControl}
|
|
@@ -990,18 +1071,18 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
990
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>
|
|
991
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>
|
|
992
1073
|
</div>
|
|
993
|
-
<h2 id="${headingId}" class="dp-calendar-heading dp-visually-hidden" aria-live="polite">${escapeHtml(formatMonthYear(`${display}-15`, locale))}</h2>
|
|
994
1074
|
<table id="${gridId}" class="dp-grid" role="grid" aria-labelledby="${headingId}">
|
|
995
1075
|
<thead><tr>${weekHeader}${dayHeaders}</tr></thead>
|
|
996
1076
|
<tbody>${rows}</tbody>
|
|
997
1077
|
</table>
|
|
998
1078
|
</div>`;
|
|
1079
|
+
this._heading.textContent = formatMonthYear(`${display}-15`, locale);
|
|
999
1080
|
this._rendering = false;
|
|
1000
1081
|
this._paintedRange = { ...this._highlightedRange };
|
|
1001
1082
|
this._paintedPreview = this.hasAttribute("data-range-preview");
|
|
1002
1083
|
|
|
1003
1084
|
if (focusKey) {
|
|
1004
|
-
|
|
1085
|
+
this._queueFocus(() => {
|
|
1005
1086
|
let selector = focusKey;
|
|
1006
1087
|
// A day-cell key goes stale across display changes: the old date can
|
|
1007
1088
|
// reappear as outside-month padding in the new month (fixed-weeks or
|
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
|
|
@@ -26,6 +35,8 @@ export class DateFieldController {
|
|
|
26
35
|
this.canonical = "";
|
|
27
36
|
this._dirty = false;
|
|
28
37
|
this._commitId = 0;
|
|
38
|
+
/** A torn-down controller is terminal: no commit may start or resume. */
|
|
39
|
+
this._disposed = false;
|
|
29
40
|
this._originalName = input.getAttribute("name") || "";
|
|
30
41
|
/** @type {MutationObserver | null} */
|
|
31
42
|
this._attributeObserver = null;
|
|
@@ -38,11 +49,33 @@ export class DateFieldController {
|
|
|
38
49
|
* @type {((date: string, options?: {moveDisplay?: boolean}) => Promise<{ok: boolean, message?: string}>) | null}
|
|
39
50
|
*/
|
|
40
51
|
this.confirm = null;
|
|
52
|
+
/** Canonical value behind a `defaultValue` the enhancement generated, or null. @type {string | null} */
|
|
53
|
+
this._generatedDefault = null;
|
|
41
54
|
}
|
|
42
55
|
|
|
43
56
|
/** @param {string} locale */
|
|
44
57
|
setLocale(locale) {
|
|
58
|
+
const generated = this._generatedDefault;
|
|
59
|
+
const previous = generated ? this.adapter.format(generated) : null;
|
|
45
60
|
this.adapter = createDateAdapter(locale || "");
|
|
61
|
+
// A generated default is localized text: reformat it so a later form reset
|
|
62
|
+
// never parses the old locale's text with the new adapter. An app-changed
|
|
63
|
+
// default no longer matches and stays untouched.
|
|
64
|
+
if (generated && this.input.defaultValue === previous) {
|
|
65
|
+
this.input.defaultValue = this.adapter.format(generated);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Make the current text the reset default when the author provided none.
|
|
71
|
+
* Never redefine an author-provided `input.defaultValue`: the next reset must
|
|
72
|
+
* use the native default (which the app may change).
|
|
73
|
+
*/
|
|
74
|
+
adoptDefault() {
|
|
75
|
+
const input = this.input;
|
|
76
|
+
if (input.getAttribute("value") != null || input.defaultValue !== "") return;
|
|
77
|
+
input.defaultValue = input.value;
|
|
78
|
+
this._generatedDefault = this.canonical && input.value ? this.canonical : null;
|
|
46
79
|
}
|
|
47
80
|
|
|
48
81
|
/** @param {Record<string, string>} messages */
|
|
@@ -65,7 +98,7 @@ export class DateFieldController {
|
|
|
65
98
|
}
|
|
66
99
|
|
|
67
100
|
get isDisabled() {
|
|
68
|
-
return this.input.disabled;
|
|
101
|
+
return this.input.matches(":disabled");
|
|
69
102
|
}
|
|
70
103
|
|
|
71
104
|
get isReadOnly() {
|
|
@@ -148,34 +181,44 @@ export class DateFieldController {
|
|
|
148
181
|
* `options` is forwarded to the `confirm` gate so a revalidation can ask it
|
|
149
182
|
* not to move the calendar display (source reloads must not re-home it).
|
|
150
183
|
* @param {{moveDisplay?: boolean}} [options]
|
|
151
|
-
* @returns {Promise<{status: "ok" | "clear" | "invalid" | "stale", value?: string}>}
|
|
184
|
+
* @returns {Promise<{status: "ok" | "clear" | "invalid" | "stale", value?: string, commitId: number}>}
|
|
152
185
|
*/
|
|
153
186
|
async commit(options = {}) {
|
|
154
187
|
const input = this.input;
|
|
155
188
|
const commitId = ++this._commitId;
|
|
189
|
+
if (this._disposed) return { status: "stale", commitId };
|
|
156
190
|
const text = input.value.trim();
|
|
157
191
|
if (!text) {
|
|
158
|
-
if (commitId !== this._commitId) return { status: "stale" };
|
|
192
|
+
if (commitId !== this._commitId) return { status: "stale", commitId };
|
|
159
193
|
input.setCustomValidity("");
|
|
160
|
-
return { status: "clear", value: "" };
|
|
194
|
+
return { status: "clear", value: "", commitId };
|
|
161
195
|
}
|
|
162
196
|
const parsed = this.adapter.parse(text);
|
|
163
197
|
if (!parsed) {
|
|
164
|
-
if (commitId !== this._commitId) return { status: "stale" };
|
|
198
|
+
if (commitId !== this._commitId) return { status: "stale", commitId };
|
|
165
199
|
if (this.hidden) this.hidden.value = "";
|
|
166
200
|
input.setCustomValidity(this.messages.invalidDate);
|
|
167
|
-
return { status: "invalid" };
|
|
201
|
+
return { status: "invalid", commitId };
|
|
168
202
|
}
|
|
169
203
|
if (this.confirm) {
|
|
170
204
|
const result = await this.confirm(parsed, options);
|
|
171
|
-
|
|
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 };
|
|
172
208
|
if (!result?.ok) {
|
|
173
209
|
if (this.hidden) this.hidden.value = "";
|
|
174
210
|
input.setCustomValidity(result?.message || this.messages.unavailableDate);
|
|
175
|
-
return { status: "invalid" };
|
|
211
|
+
return { status: "invalid", commitId };
|
|
176
212
|
}
|
|
177
213
|
}
|
|
178
|
-
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;
|
|
179
222
|
}
|
|
180
223
|
|
|
181
224
|
/**
|
|
@@ -203,8 +246,12 @@ export class DateFieldController {
|
|
|
203
246
|
}
|
|
204
247
|
|
|
205
248
|
/** Remove the hidden field and attribute observer, restoring the original
|
|
206
|
-
* 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. */
|
|
207
252
|
teardown() {
|
|
253
|
+
this.dirty();
|
|
254
|
+
this._disposed = true;
|
|
208
255
|
this._attributeObserver?.disconnect();
|
|
209
256
|
this._attributeObserver = null;
|
|
210
257
|
const hiddenName = this.hidden?.getAttribute("name") || this._originalName;
|