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