@lekoala/date-picker 0.2.0 → 0.4.0
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 +12 -8
- package/custom-elements.json +32 -2
- package/dist/date-picker.css +163 -115
- package/dist/date-picker.js +656 -171
- package/dist/date-picker.min.css +1 -1
- package/dist/date-picker.min.js +13 -13
- package/dist/date-picker.standalone.min.js +177 -129
- package/dist/types/date-calendar.d.ts +34 -3
- package/dist/types/date-calendar.d.ts.map +1 -1
- package/dist/types/date-field.d.ts +15 -5
- package/dist/types/date-field.d.ts.map +1 -1
- package/dist/types/date-picker.d.ts +153 -14
- package/dist/types/date-picker.d.ts.map +1 -1
- package/dist/types/date-range.d.ts +102 -20
- package/dist/types/date-range.d.ts.map +1 -1
- package/dist/types/date.d.ts +10 -0
- package/dist/types/date.d.ts.map +1 -1
- package/dist/types/intl.d.ts.map +1 -1
- package/docs/ACCESSIBILITY.md +22 -10
- package/docs/API.md +72 -3
- package/docs/ARCHITECTURE.md +16 -2
- package/docs/AT-CHECKLIST.md +1 -1
- package/docs/DECISIONS.md +19 -4
- package/docs/MIGRATION-FLATPICKR.md +2 -2
- package/docs/TESTING.md +1 -1
- package/docs/USE_CASES.md +3 -0
- package/package.json +7 -7
- package/src/date-calendar.js +148 -48
- package/src/date-field.js +20 -8
- package/src/date-picker.css +163 -115
- package/src/date-picker.js +549 -110
- package/src/date-range.js +184 -30
- package/src/date.js +50 -1
- package/src/intl.js +13 -7
package/src/date-calendar.js
CHANGED
|
@@ -2,12 +2,13 @@ import { CalendarModel } from "./calendar-model.js";
|
|
|
2
2
|
import {
|
|
3
3
|
addDays,
|
|
4
4
|
clampDate,
|
|
5
|
+
clampDisplayMonth,
|
|
5
6
|
compareDates,
|
|
7
|
+
endOfMonth,
|
|
6
8
|
getMonthWeeks,
|
|
7
9
|
isDate,
|
|
8
10
|
isoWeekNumber,
|
|
9
11
|
monthKey,
|
|
10
|
-
shiftMonth,
|
|
11
12
|
todayISO,
|
|
12
13
|
} from "./date.js";
|
|
13
14
|
import { normalizeRange, rangePosition } from "./date-range.js";
|
|
@@ -46,20 +47,43 @@ function yearOf(month) {
|
|
|
46
47
|
return Number(month.slice(0, 4));
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Step a `YYYY-MM` key by ±1, or `null` when the step leaves the canonical
|
|
52
|
+
* 0001-01..9999-12 window. Pure arithmetic, so an out-of-range intermediate
|
|
53
|
+
* date is never constructed and the calendar edges cannot throw.
|
|
54
|
+
* @param {string} month
|
|
55
|
+
* @param {number} amount
|
|
56
|
+
* @returns {string | null}
|
|
57
|
+
*/
|
|
58
|
+
function stepMonthKey(month, amount) {
|
|
59
|
+
const [year, index] = month.split("-").map(Number);
|
|
60
|
+
const absolute = year * 12 + (index - 1) + amount;
|
|
61
|
+
const stepYear = Math.floor(absolute / 12);
|
|
62
|
+
const stepMonth = (((absolute % 12) + 12) % 12) + 1;
|
|
63
|
+
if (stepYear < 1 || stepYear > 9999) return null;
|
|
64
|
+
return `${String(stepYear).padStart(4, "0")}-${String(stepMonth).padStart(2, "0")}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Band attributes for one day position. Markup and in-place band updates read
|
|
69
|
+
* the same map, so the two paths cannot disagree about a cell's state.
|
|
70
|
+
* @param {"" | "start" | "in" | "end" | "single"} position
|
|
71
|
+
* @returns {Record<string, boolean>}
|
|
72
|
+
*/
|
|
73
|
+
function rangeAttributeMap(position) {
|
|
74
|
+
return {
|
|
75
|
+
"data-range-start": position === "start" || position === "single",
|
|
76
|
+
"data-range-end": position === "end" || position === "single",
|
|
77
|
+
"data-in-range": position === "in",
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
49
81
|
/** @param {"" | "start" | "in" | "end" | "single"} position */
|
|
50
82
|
function rangeAttributes(position) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
return ' data-range-start="true"';
|
|
56
|
-
case "end":
|
|
57
|
-
return ' data-range-end="true"';
|
|
58
|
-
case "in":
|
|
59
|
-
return ' data-in-range="true"';
|
|
60
|
-
default:
|
|
61
|
-
return "";
|
|
62
|
-
}
|
|
83
|
+
return Object.entries(rangeAttributeMap(position))
|
|
84
|
+
.filter(([, on]) => on)
|
|
85
|
+
.map(([name]) => ` ${name}="true"`)
|
|
86
|
+
.join("");
|
|
63
87
|
}
|
|
64
88
|
|
|
65
89
|
/** @param {"" | "start" | "in" | "end" | "single"} position */
|
|
@@ -125,6 +149,12 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
125
149
|
this._isDateDisabled = null;
|
|
126
150
|
/** @type {DateRange} */
|
|
127
151
|
this._highlightedRange = { start: "", end: "" };
|
|
152
|
+
// Bounds and preview flag the cells were last painted with. The preview
|
|
153
|
+
// flag changes the accessible names (a proposal drops the committed
|
|
154
|
+
// range-start/range-end suffix), so a repaint is due when either changes.
|
|
155
|
+
/** @type {DateRange} */
|
|
156
|
+
this._paintedRange = { start: "", end: "" };
|
|
157
|
+
this._paintedPreview = false;
|
|
128
158
|
this._messages = getDefaultMessages();
|
|
129
159
|
this._onClick = this._onClick.bind(this);
|
|
130
160
|
this._onChange = this._onChange.bind(this);
|
|
@@ -262,9 +292,17 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
262
292
|
|
|
263
293
|
set highlightedRange(value) {
|
|
264
294
|
const next = normalizeRange(value);
|
|
265
|
-
|
|
295
|
+
const preview = this.hasAttribute("data-range-preview");
|
|
296
|
+
// Repaint when the preview mode changed even if the bounds are identical:
|
|
297
|
+
// entering/leaving a preview rewrites every cell's accessible name.
|
|
298
|
+
if (
|
|
299
|
+
next.start === this._paintedRange.start &&
|
|
300
|
+
next.end === this._paintedRange.end &&
|
|
301
|
+
preview === this._paintedPreview
|
|
302
|
+
)
|
|
303
|
+
return;
|
|
266
304
|
this._highlightedRange = next;
|
|
267
|
-
if (this._connected) this.
|
|
305
|
+
if (this._connected) this._updateRangeDom();
|
|
268
306
|
}
|
|
269
307
|
|
|
270
308
|
/** @public */
|
|
@@ -343,7 +381,7 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
343
381
|
const attrDisplay = this.getAttribute("display") || "";
|
|
344
382
|
if (attrDisplay) {
|
|
345
383
|
try {
|
|
346
|
-
this._model.setDisplay(attrDisplay);
|
|
384
|
+
this._model.setDisplay(clampDisplayMonth(attrDisplay, firstDay));
|
|
347
385
|
} catch {
|
|
348
386
|
// Invalid author markup falls back to the current model display.
|
|
349
387
|
}
|
|
@@ -368,11 +406,21 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
368
406
|
|
|
369
407
|
/** @param {string} value */
|
|
370
408
|
_clamp(value) {
|
|
371
|
-
|
|
409
|
+
const bounded = clampDate(value, this.min, this.max);
|
|
410
|
+
const month = monthKey(bounded);
|
|
411
|
+
const safeMonth = clampDisplayMonth(month, this.firstDay);
|
|
412
|
+
if (safeMonth === month) return bounded;
|
|
413
|
+
// The renderable window excludes this month (a calendar edge): keep the
|
|
414
|
+
// day-of-month where the target month has it, else fall back to its last
|
|
415
|
+
// day, so the focused date always exists in the rendered grid.
|
|
416
|
+
const day = Number(bounded.slice(8, 10)) || 1;
|
|
417
|
+
const candidate = addDays(`${safeMonth}-01`, day - 1);
|
|
418
|
+
const last = endOfMonth(safeMonth);
|
|
419
|
+
return compareDates(candidate, last) > 0 ? last : candidate;
|
|
372
420
|
}
|
|
373
421
|
|
|
374
422
|
_weeks(display = this.display) {
|
|
375
|
-
const weeks = getMonthWeeks(display, { firstDay: this.firstDay });
|
|
423
|
+
const weeks = getMonthWeeks(clampDisplayMonth(display, this.firstDay), { firstDay: this.firstDay });
|
|
376
424
|
if (!this.fixedWeeks || weeks.length >= 6) return weeks;
|
|
377
425
|
let cursor = weeks[weeks.length - 1]?.[6] || "";
|
|
378
426
|
while (weeks.length < 6 && cursor) {
|
|
@@ -386,7 +434,7 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
386
434
|
|
|
387
435
|
/** @param {HTMLElement} element @returns {string} */
|
|
388
436
|
_focusTargetKey(element) {
|
|
389
|
-
if (element.matches(".dp-month-select, .dp-year-
|
|
437
|
+
if (element.matches(".dp-month-select, .dp-year-input") && element.id) {
|
|
390
438
|
return `#${CSS.escape(element.id)}`;
|
|
391
439
|
}
|
|
392
440
|
if (element.matches(".dp-nav[data-calendar-action]")) {
|
|
@@ -416,7 +464,8 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
416
464
|
const outsideBounds =
|
|
417
465
|
(this.min && compareDates(date, this.min) < 0) || (this.max && compareDates(date, this.max) > 0);
|
|
418
466
|
const explicitlyEnabled = state.enabled === true;
|
|
419
|
-
// min/max > state.disabled >
|
|
467
|
+
// min/max > resolved state.disabled > isDateDisabled(), unless resolved
|
|
468
|
+
// state.enabled === true lifts only isDateDisabled().
|
|
420
469
|
const disabledByRule = explicitlyEnabled ? false : Boolean(this._isDateDisabled?.(date));
|
|
421
470
|
const disabled = Boolean(outsideBounds) || Boolean(state.disabled) || disabledByRule;
|
|
422
471
|
return { ...state, disabled };
|
|
@@ -490,7 +539,7 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
490
539
|
/** @param {string} display @param {{emit?:boolean}} [options] */
|
|
491
540
|
_setDisplay(display, options = {}) {
|
|
492
541
|
const previous = this._model.display;
|
|
493
|
-
this._model.setDisplay(display);
|
|
542
|
+
this._model.setDisplay(clampDisplayMonth(display, this.firstDay));
|
|
494
543
|
this._model.focused = this._clamp(this._model.focused);
|
|
495
544
|
this._model.display = monthKey(this._model.focused);
|
|
496
545
|
this._reflect("display", this._model.display);
|
|
@@ -511,12 +560,12 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
511
560
|
|
|
512
561
|
/** @public */
|
|
513
562
|
previousMonth() {
|
|
514
|
-
return this._setDisplay(
|
|
563
|
+
return this._setDisplay(stepMonthKey(this.display, -1) ?? this.display);
|
|
515
564
|
}
|
|
516
565
|
|
|
517
566
|
/** @public */
|
|
518
567
|
nextMonth() {
|
|
519
|
-
return this._setDisplay(
|
|
568
|
+
return this._setDisplay(stepMonthKey(this.display, 1) ?? this.display);
|
|
520
569
|
}
|
|
521
570
|
|
|
522
571
|
/** @public @param {string} date @param {{moveFocus?:boolean}} [options] */
|
|
@@ -571,14 +620,62 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
571
620
|
return false;
|
|
572
621
|
}
|
|
573
622
|
|
|
623
|
+
/**
|
|
624
|
+
* Accessible name of one day cell. The band contribution is dropped while a
|
|
625
|
+
* preview is projected onto `highlightedRange`: a proposal must not be
|
|
626
|
+
* announced as a committed range start/end.
|
|
627
|
+
* @param {string} date @param {DateState} state
|
|
628
|
+
* @param {"" | "start" | "in" | "end" | "single"} position
|
|
629
|
+
*/
|
|
630
|
+
_dayLabel(date, state, position) {
|
|
631
|
+
const rangeLabel = this.hasAttribute("data-range-preview") ? "" : rangeLabelKey(position);
|
|
632
|
+
const description = typeof state.description === "string" ? state.description : "";
|
|
633
|
+
return [
|
|
634
|
+
formatLongDate(date, this.locale),
|
|
635
|
+
description,
|
|
636
|
+
rangeLabel ? this._messages[rangeLabel] : "",
|
|
637
|
+
state.disabled ? this._messages.unavailable : "",
|
|
638
|
+
]
|
|
639
|
+
.filter(Boolean)
|
|
640
|
+
.join(". ");
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Repaint the range band over the cells already rendered. The band is
|
|
645
|
+
* presentation on top of the same grid, so it must never rebuild it: a full
|
|
646
|
+
* render would drop `renderDay()` nodes and fight the roving focus on every
|
|
647
|
+
* hover of a live range preview.
|
|
648
|
+
*/
|
|
649
|
+
_updateRangeDom() {
|
|
650
|
+
this._paintedRange = { ...this._highlightedRange };
|
|
651
|
+
this._paintedPreview = this.hasAttribute("data-range-preview");
|
|
652
|
+
for (const cell of this.querySelectorAll(".dp-day[data-date]")) {
|
|
653
|
+
if (!(cell instanceof HTMLTableCellElement)) continue;
|
|
654
|
+
const date = cell.dataset.date || "";
|
|
655
|
+
const position = rangePosition(date, this._highlightedRange);
|
|
656
|
+
for (const [name, on] of Object.entries(rangeAttributeMap(position))) {
|
|
657
|
+
if (on) cell.setAttribute(name, "true");
|
|
658
|
+
else cell.removeAttribute(name);
|
|
659
|
+
}
|
|
660
|
+
cell.setAttribute("aria-label", this._dayLabel(date, this.getDateState(date), position));
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
574
664
|
/** @public */
|
|
575
665
|
focusGrid() {
|
|
576
666
|
const target = this.querySelector(`.dp-day[data-date="${CSS.escape(this.focusedDate)}"]`);
|
|
577
667
|
if (target instanceof HTMLElement) target.focus();
|
|
578
668
|
}
|
|
579
669
|
|
|
580
|
-
/**
|
|
581
|
-
|
|
670
|
+
/**
|
|
671
|
+
* Run one explicit activation: availability check, then the cancelable
|
|
672
|
+
* `dateactivate` seam, then selection. Click, keyboard and any other input
|
|
673
|
+
* device share this single path, so an application that cancels
|
|
674
|
+
* `dateactivate` cannot be bypassed by a new interaction.
|
|
675
|
+
* @public
|
|
676
|
+
* @param {string} date @returns {Promise<boolean>}
|
|
677
|
+
*/
|
|
678
|
+
async activateDate(date) {
|
|
582
679
|
// Guarantee remote state before deciding: a date clicked before the
|
|
583
680
|
// source resolves must not be treated as available, and a cancelled or
|
|
584
681
|
// failed load must not authorize it either.
|
|
@@ -632,7 +729,7 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
632
729
|
this._model.setFocused(this._clamp(date));
|
|
633
730
|
this._reflect("display", this._model.display);
|
|
634
731
|
if (previousDisplay !== this.display) this._emitDisplayChange();
|
|
635
|
-
void this.
|
|
732
|
+
void this.activateDate(date);
|
|
636
733
|
}
|
|
637
734
|
|
|
638
735
|
/** @param {Event} event */
|
|
@@ -642,13 +739,11 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
642
739
|
this._setDisplay(`${this.display.slice(0, 4)}-${target.value}`);
|
|
643
740
|
return;
|
|
644
741
|
}
|
|
645
|
-
if (target instanceof HTMLSelectElement && target.matches(".dp-year-select")) {
|
|
646
|
-
this._setDisplay(`${target.value}-${this.display.slice(5, 7)}`);
|
|
647
|
-
return;
|
|
648
|
-
}
|
|
649
742
|
if (target instanceof HTMLInputElement && target.matches(".dp-year-input")) {
|
|
650
743
|
const year = Number(target.value);
|
|
651
|
-
|
|
744
|
+
// 9999 is a valid civil year but its December grid overflows into 10000,
|
|
745
|
+
// so the editable range stops one year short of the unrenderable month.
|
|
746
|
+
if (!Number.isInteger(year) || year < 1 || year > 9998) {
|
|
652
747
|
if (this._connected) this.render(false);
|
|
653
748
|
return;
|
|
654
749
|
}
|
|
@@ -656,12 +751,22 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
656
751
|
}
|
|
657
752
|
}
|
|
658
753
|
|
|
659
|
-
/**
|
|
754
|
+
/**
|
|
755
|
+
* Real DOM focus is the single source of truth for `datefocus`: the model is
|
|
756
|
+
* synced here and the event is announced from here, so the temporary focus
|
|
757
|
+
* arithmetic in `_onKeyDown` cannot emit phantom events, and setting
|
|
758
|
+
* `focusedDate` without moving focus stays silent.
|
|
759
|
+
* @param {FocusEvent} event
|
|
760
|
+
*/
|
|
660
761
|
_onFocusIn(event) {
|
|
661
762
|
const target = event.target;
|
|
662
763
|
if (!(target instanceof HTMLTableCellElement) || !target.matches(".dp-day[data-date]")) return;
|
|
663
764
|
const date = target.dataset.date || "";
|
|
664
|
-
if (isDate(date))
|
|
765
|
+
if (!isDate(date)) return;
|
|
766
|
+
this._model.focused = date;
|
|
767
|
+
this.dispatchEvent(
|
|
768
|
+
new CustomEvent("datefocus", { detail: { date, state: this.getDateState(date) }, bubbles: true }),
|
|
769
|
+
);
|
|
665
770
|
}
|
|
666
771
|
|
|
667
772
|
/** @param {KeyboardEvent} event */
|
|
@@ -671,7 +776,7 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
671
776
|
const date = target.dataset.date || this.focusedDate;
|
|
672
777
|
if (event.key === "Enter" || event.key === " ") {
|
|
673
778
|
event.preventDefault();
|
|
674
|
-
void this.
|
|
779
|
+
void this.activateDate(date);
|
|
675
780
|
return;
|
|
676
781
|
}
|
|
677
782
|
// Movement primitives live in CalendarModel; the DOM only syncs render/focus.
|
|
@@ -715,8 +820,10 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
715
820
|
const gridId = `${this._id}-grid`;
|
|
716
821
|
const minMonth = this.min ? monthKey(this.min) : "";
|
|
717
822
|
const maxMonth = this.max ? monthKey(this.max) : "";
|
|
718
|
-
const
|
|
719
|
-
const
|
|
823
|
+
const prevKey = stepMonthKey(display, -1);
|
|
824
|
+
const nextKey = stepMonthKey(display, 1);
|
|
825
|
+
const prevDisabled = Boolean(minMonth && prevKey !== null && prevKey < minMonth);
|
|
826
|
+
const nextDisabled = Boolean(maxMonth && nextKey !== null && nextKey > maxMonth);
|
|
720
827
|
const minYearAttr = this.min ? ` min="${Number(this.min.slice(0, 4))}"` : "";
|
|
721
828
|
const maxYearAttr = this.max ? ` max="${Number(this.max.slice(0, 4))}"` : "";
|
|
722
829
|
const showWeeks = this.showWeekNumbers && this.firstDay === 1;
|
|
@@ -753,16 +860,7 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
753
860
|
const focused = this.focusedDate === date;
|
|
754
861
|
const isToday = date === today;
|
|
755
862
|
const position = rangePosition(date, this._highlightedRange);
|
|
756
|
-
const
|
|
757
|
-
const description = typeof state.description === "string" ? state.description : "";
|
|
758
|
-
const label = [
|
|
759
|
-
formatLongDate(date, locale),
|
|
760
|
-
description,
|
|
761
|
-
rangeLabel ? this._messages[rangeLabel] : "",
|
|
762
|
-
state.disabled ? this._messages.unavailable : "",
|
|
763
|
-
]
|
|
764
|
-
.filter(Boolean)
|
|
765
|
-
.join(". ");
|
|
863
|
+
const label = this._dayLabel(date, state, position);
|
|
766
864
|
return `<td class="dp-day" data-date="${date}"${outside ? ' data-outside-month="true"' : ""}${isToday ? ' data-today="true" aria-current="date"' : ""}${selected ? ' data-selected="true" aria-selected="true"' : ""}${rangeAttributes(position)}${state.disabled ? ' data-disabled="true" aria-disabled="true"' : ""} tabindex="${focused ? "0" : "-1"}" aria-label="${escapeHtml(label)}"><span class="dp-day-number" aria-hidden="true">${Number(date.slice(8, 10))}</span><span class="dp-day-extra" aria-hidden="true"></span></td>`;
|
|
767
865
|
})
|
|
768
866
|
.join("");
|
|
@@ -792,8 +890,8 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
792
890
|
<div class="dp-calendar-header">
|
|
793
891
|
${yearFirst ? yearControl : monthControl}
|
|
794
892
|
${yearFirst ? monthControl : yearControl}
|
|
795
|
-
<button type="button" class="dp-nav dp-prev" 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="
|
|
796
|
-
<button type="button" class="dp-nav dp-next" 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="
|
|
893
|
+
<button type="button" class="dp-nav dp-prev" 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>
|
|
894
|
+
<button type="button" class="dp-nav dp-next" 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>
|
|
797
895
|
</div>
|
|
798
896
|
<h2 id="${headingId}" class="dp-calendar-heading dp-visually-hidden" aria-live="polite">${escapeHtml(formatMonthYear(`${display}-15`, locale))}</h2>
|
|
799
897
|
<table id="${gridId}" class="dp-grid" role="grid" aria-labelledby="${headingId}">
|
|
@@ -802,6 +900,8 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
802
900
|
</table>
|
|
803
901
|
</div>`;
|
|
804
902
|
this._rendering = false;
|
|
903
|
+
this._paintedRange = { ...this._highlightedRange };
|
|
904
|
+
this._paintedPreview = this.hasAttribute("data-range-preview");
|
|
805
905
|
|
|
806
906
|
if (focusKey) {
|
|
807
907
|
queueMicrotask(() => {
|
package/src/date-field.js
CHANGED
|
@@ -33,8 +33,9 @@ export class DateFieldController {
|
|
|
33
33
|
this.onAttributesChanged = null;
|
|
34
34
|
/**
|
|
35
35
|
* Async availability gate for a parsed date. Returning `{ ok: false }`
|
|
36
|
-
* marks the target unavailable. Swappable at runtime.
|
|
37
|
-
*
|
|
36
|
+
* marks the target unavailable. Swappable at runtime. The commit options
|
|
37
|
+
* are forwarded so the gate can honor `moveDisplay`.
|
|
38
|
+
* @type {((date: string, options?: {moveDisplay?: boolean}) => Promise<{ok: boolean, message?: string}>) | null}
|
|
38
39
|
*/
|
|
39
40
|
this.confirm = null;
|
|
40
41
|
}
|
|
@@ -117,18 +118,26 @@ export class DateFieldController {
|
|
|
117
118
|
}
|
|
118
119
|
|
|
119
120
|
/** Live text input: the raw text no longer matches the canonical value, so
|
|
120
|
-
* the hidden ISO must not submit it.
|
|
121
|
+
* the hidden ISO must not submit it. A text that formats back to the current
|
|
122
|
+
* canonical value (e.g. re-applied by a calendar pick) keeps the ISO value
|
|
123
|
+
* valid; invalidating in-flight commits first still drops stale async work. */
|
|
121
124
|
handleInput() {
|
|
122
125
|
const input = this.input;
|
|
123
|
-
|
|
126
|
+
const text = input.value.trim();
|
|
127
|
+
const canonical = this.canonical ? this.adapter.format(this.canonical) : "";
|
|
124
128
|
this.dirty();
|
|
129
|
+
if (text === canonical) {
|
|
130
|
+
this._dirty = false;
|
|
131
|
+
if (this.hidden) this.hidden.value = this.canonical;
|
|
132
|
+
input.setCustomValidity("");
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
this._dirty = true;
|
|
125
136
|
if (this.hidden) this.hidden.value = "";
|
|
126
|
-
const text = input.value.trim();
|
|
127
137
|
if (!text) {
|
|
128
138
|
input.setCustomValidity("");
|
|
129
139
|
return;
|
|
130
140
|
}
|
|
131
|
-
const canonical = this.canonical ? this.adapter.format(this.canonical) : "";
|
|
132
141
|
input.setCustomValidity(text === canonical ? "" : this.messages.invalidDate);
|
|
133
142
|
}
|
|
134
143
|
|
|
@@ -136,9 +145,12 @@ export class DateFieldController {
|
|
|
136
145
|
* Commit the visible text. Returns the parsed canonical date on success, the
|
|
137
146
|
* empty string on a clear, or a falsy marker for invalid/stale commits. The
|
|
138
147
|
* caller applies the canonical value so reflection and events stay central.
|
|
148
|
+
* `options` is forwarded to the `confirm` gate so a revalidation can ask it
|
|
149
|
+
* not to move the calendar display (source reloads must not re-home it).
|
|
150
|
+
* @param {{moveDisplay?: boolean}} [options]
|
|
139
151
|
* @returns {Promise<{status: "ok" | "clear" | "invalid" | "stale", value?: string}>}
|
|
140
152
|
*/
|
|
141
|
-
async commit() {
|
|
153
|
+
async commit(options = {}) {
|
|
142
154
|
const input = this.input;
|
|
143
155
|
const commitId = ++this._commitId;
|
|
144
156
|
const text = input.value.trim();
|
|
@@ -155,7 +167,7 @@ export class DateFieldController {
|
|
|
155
167
|
return { status: "invalid" };
|
|
156
168
|
}
|
|
157
169
|
if (this.confirm) {
|
|
158
|
-
const result = await this.confirm(parsed);
|
|
170
|
+
const result = await this.confirm(parsed, options);
|
|
159
171
|
if (commitId !== this._commitId) return { status: "stale" };
|
|
160
172
|
if (!result?.ok) {
|
|
161
173
|
if (this.hidden) this.hidden.value = "";
|