@lekoala/date-picker 0.3.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 +8 -6
- package/custom-elements.json +20 -0
- package/dist/date-picker.css +32 -32
- package/dist/date-picker.js +493 -96
- package/dist/date-picker.min.css +1 -1
- package/dist/date-picker.min.js +13 -13
- package/dist/date-picker.standalone.min.js +46 -46
- 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 +12 -4
- package/dist/types/date-field.d.ts.map +1 -1
- package/dist/types/date-picker.d.ts +91 -9
- 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 +6 -2
- package/docs/API.md +51 -1
- package/docs/DECISIONS.md +17 -4
- package/docs/MIGRATION-FLATPICKR.md +1 -1
- package/docs/TESTING.md +1 -1
- package/docs/USE_CASES.md +3 -0
- package/package.json +6 -6
- package/src/date-calendar.js +145 -42
- package/src/date-field.js +8 -4
- package/src/date-picker.css +32 -32
- package/src/date-picker.js +345 -37
- 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) {
|
|
@@ -491,7 +539,7 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
491
539
|
/** @param {string} display @param {{emit?:boolean}} [options] */
|
|
492
540
|
_setDisplay(display, options = {}) {
|
|
493
541
|
const previous = this._model.display;
|
|
494
|
-
this._model.setDisplay(display);
|
|
542
|
+
this._model.setDisplay(clampDisplayMonth(display, this.firstDay));
|
|
495
543
|
this._model.focused = this._clamp(this._model.focused);
|
|
496
544
|
this._model.display = monthKey(this._model.focused);
|
|
497
545
|
this._reflect("display", this._model.display);
|
|
@@ -512,12 +560,12 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
512
560
|
|
|
513
561
|
/** @public */
|
|
514
562
|
previousMonth() {
|
|
515
|
-
return this._setDisplay(
|
|
563
|
+
return this._setDisplay(stepMonthKey(this.display, -1) ?? this.display);
|
|
516
564
|
}
|
|
517
565
|
|
|
518
566
|
/** @public */
|
|
519
567
|
nextMonth() {
|
|
520
|
-
return this._setDisplay(
|
|
568
|
+
return this._setDisplay(stepMonthKey(this.display, 1) ?? this.display);
|
|
521
569
|
}
|
|
522
570
|
|
|
523
571
|
/** @public @param {string} date @param {{moveFocus?:boolean}} [options] */
|
|
@@ -572,14 +620,62 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
572
620
|
return false;
|
|
573
621
|
}
|
|
574
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
|
+
|
|
575
664
|
/** @public */
|
|
576
665
|
focusGrid() {
|
|
577
666
|
const target = this.querySelector(`.dp-day[data-date="${CSS.escape(this.focusedDate)}"]`);
|
|
578
667
|
if (target instanceof HTMLElement) target.focus();
|
|
579
668
|
}
|
|
580
669
|
|
|
581
|
-
/**
|
|
582
|
-
|
|
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) {
|
|
583
679
|
// Guarantee remote state before deciding: a date clicked before the
|
|
584
680
|
// source resolves must not be treated as available, and a cancelled or
|
|
585
681
|
// failed load must not authorize it either.
|
|
@@ -633,7 +729,7 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
633
729
|
this._model.setFocused(this._clamp(date));
|
|
634
730
|
this._reflect("display", this._model.display);
|
|
635
731
|
if (previousDisplay !== this.display) this._emitDisplayChange();
|
|
636
|
-
void this.
|
|
732
|
+
void this.activateDate(date);
|
|
637
733
|
}
|
|
638
734
|
|
|
639
735
|
/** @param {Event} event */
|
|
@@ -645,7 +741,9 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
645
741
|
}
|
|
646
742
|
if (target instanceof HTMLInputElement && target.matches(".dp-year-input")) {
|
|
647
743
|
const year = Number(target.value);
|
|
648
|
-
|
|
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) {
|
|
649
747
|
if (this._connected) this.render(false);
|
|
650
748
|
return;
|
|
651
749
|
}
|
|
@@ -653,12 +751,22 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
653
751
|
}
|
|
654
752
|
}
|
|
655
753
|
|
|
656
|
-
/**
|
|
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
|
+
*/
|
|
657
761
|
_onFocusIn(event) {
|
|
658
762
|
const target = event.target;
|
|
659
763
|
if (!(target instanceof HTMLTableCellElement) || !target.matches(".dp-day[data-date]")) return;
|
|
660
764
|
const date = target.dataset.date || "";
|
|
661
|
-
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
|
+
);
|
|
662
770
|
}
|
|
663
771
|
|
|
664
772
|
/** @param {KeyboardEvent} event */
|
|
@@ -668,7 +776,7 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
668
776
|
const date = target.dataset.date || this.focusedDate;
|
|
669
777
|
if (event.key === "Enter" || event.key === " ") {
|
|
670
778
|
event.preventDefault();
|
|
671
|
-
void this.
|
|
779
|
+
void this.activateDate(date);
|
|
672
780
|
return;
|
|
673
781
|
}
|
|
674
782
|
// Movement primitives live in CalendarModel; the DOM only syncs render/focus.
|
|
@@ -712,8 +820,10 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
712
820
|
const gridId = `${this._id}-grid`;
|
|
713
821
|
const minMonth = this.min ? monthKey(this.min) : "";
|
|
714
822
|
const maxMonth = this.max ? monthKey(this.max) : "";
|
|
715
|
-
const
|
|
716
|
-
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);
|
|
717
827
|
const minYearAttr = this.min ? ` min="${Number(this.min.slice(0, 4))}"` : "";
|
|
718
828
|
const maxYearAttr = this.max ? ` max="${Number(this.max.slice(0, 4))}"` : "";
|
|
719
829
|
const showWeeks = this.showWeekNumbers && this.firstDay === 1;
|
|
@@ -750,16 +860,7 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
750
860
|
const focused = this.focusedDate === date;
|
|
751
861
|
const isToday = date === today;
|
|
752
862
|
const position = rangePosition(date, this._highlightedRange);
|
|
753
|
-
const
|
|
754
|
-
const description = typeof state.description === "string" ? state.description : "";
|
|
755
|
-
const label = [
|
|
756
|
-
formatLongDate(date, locale),
|
|
757
|
-
description,
|
|
758
|
-
rangeLabel ? this._messages[rangeLabel] : "",
|
|
759
|
-
state.disabled ? this._messages.unavailable : "",
|
|
760
|
-
]
|
|
761
|
-
.filter(Boolean)
|
|
762
|
-
.join(". ");
|
|
863
|
+
const label = this._dayLabel(date, state, position);
|
|
763
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>`;
|
|
764
865
|
})
|
|
765
866
|
.join("");
|
|
@@ -789,8 +890,8 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
789
890
|
<div class="dp-calendar-header">
|
|
790
891
|
${yearFirst ? yearControl : monthControl}
|
|
791
892
|
${yearFirst ? monthControl : yearControl}
|
|
792
|
-
<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="
|
|
793
|
-
<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>
|
|
794
895
|
</div>
|
|
795
896
|
<h2 id="${headingId}" class="dp-calendar-heading dp-visually-hidden" aria-live="polite">${escapeHtml(formatMonthYear(`${display}-15`, locale))}</h2>
|
|
796
897
|
<table id="${gridId}" class="dp-grid" role="grid" aria-labelledby="${headingId}">
|
|
@@ -799,6 +900,8 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
799
900
|
</table>
|
|
800
901
|
</div>`;
|
|
801
902
|
this._rendering = false;
|
|
903
|
+
this._paintedRange = { ...this._highlightedRange };
|
|
904
|
+
this._paintedPreview = this.hasAttribute("data-range-preview");
|
|
802
905
|
|
|
803
906
|
if (focusKey) {
|
|
804
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
|
}
|
|
@@ -144,9 +145,12 @@ export class DateFieldController {
|
|
|
144
145
|
* Commit the visible text. Returns the parsed canonical date on success, the
|
|
145
146
|
* empty string on a clear, or a falsy marker for invalid/stale commits. The
|
|
146
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]
|
|
147
151
|
* @returns {Promise<{status: "ok" | "clear" | "invalid" | "stale", value?: string}>}
|
|
148
152
|
*/
|
|
149
|
-
async commit() {
|
|
153
|
+
async commit(options = {}) {
|
|
150
154
|
const input = this.input;
|
|
151
155
|
const commitId = ++this._commitId;
|
|
152
156
|
const text = input.value.trim();
|
|
@@ -163,7 +167,7 @@ export class DateFieldController {
|
|
|
163
167
|
return { status: "invalid" };
|
|
164
168
|
}
|
|
165
169
|
if (this.confirm) {
|
|
166
|
-
const result = await this.confirm(parsed);
|
|
170
|
+
const result = await this.confirm(parsed, options);
|
|
167
171
|
if (commitId !== this._commitId) return { status: "stale" };
|
|
168
172
|
if (!result?.ok) {
|
|
169
173
|
if (this.hidden) this.hidden.value = "";
|
package/src/date-picker.css
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
--dp-hover: color-mix(in srgb, var(--dp-fg) 5%, var(--dp-bg));
|
|
7
7
|
--dp-accent: #2563eb;
|
|
8
8
|
--dp-accent-fg: #fff;
|
|
9
|
+
--dp-focus: var(--dp-accent);
|
|
10
|
+
--dp-focus-width: 2px;
|
|
9
11
|
--dp-radius: 0.5rem;
|
|
10
12
|
--dp-day-size: 2.75rem;
|
|
11
13
|
--dp-gap: 0.375rem;
|
|
@@ -137,14 +139,6 @@ html[dir="rtl"] .dp-month-select {
|
|
|
137
139
|
pointer-events: none;
|
|
138
140
|
}
|
|
139
141
|
|
|
140
|
-
/* Inherited RTL mirrors the header layout via the grid; mirror the nav glyphs
|
|
141
|
-
so previous/next keep their visual sense. Actions stay chronological: no
|
|
142
|
-
button swap, no keyboard remapping. */
|
|
143
|
-
[dir="rtl"] .dp-nav svg,
|
|
144
|
-
html[dir="rtl"] .dp-nav svg {
|
|
145
|
-
transform: scaleX(-1);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
142
|
.dp-calendar-header :where(button, select, input):disabled,
|
|
149
143
|
.dp-picker-button:disabled {
|
|
150
144
|
cursor: not-allowed;
|
|
@@ -155,12 +149,13 @@ html[dir="rtl"] .dp-nav svg {
|
|
|
155
149
|
background: var(--dp-hover);
|
|
156
150
|
}
|
|
157
151
|
|
|
158
|
-
/*
|
|
159
|
-
|
|
160
|
-
.dp-calendar-header :where(button, select, input):focus-visible
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
152
|
+
/* Inner outlines survive forced colors and keep focus inside each control.
|
|
153
|
+
The picker trigger uses a tighter inset around its icon below. */
|
|
154
|
+
.dp-calendar-header :where(button, select, input):focus-visible,
|
|
155
|
+
date-picker > input:not([type="hidden"]):focus-visible,
|
|
156
|
+
.dp-day:focus-visible {
|
|
157
|
+
outline: var(--dp-focus-width) solid var(--dp-focus);
|
|
158
|
+
outline-offset: calc(var(--dp-focus-width) * -1);
|
|
164
159
|
}
|
|
165
160
|
|
|
166
161
|
.dp-day:hover:not([data-disabled="true"]):not([data-selected="true"]):not([data-range-start="true"]):not(
|
|
@@ -205,15 +200,6 @@ html[dir="rtl"] .dp-nav svg {
|
|
|
205
200
|
font-size: 0.875rem;
|
|
206
201
|
}
|
|
207
202
|
|
|
208
|
-
/* Inner ring: keyboard focus stays inside the cell instead of reading as a
|
|
209
|
-
second selected state or bleeding onto a neighbouring range band. It is an
|
|
210
|
-
`outline` (not an inset box-shadow) because forced colors computes
|
|
211
|
-
box-shadow to `none`, which would erase the keyboard indicator entirely. */
|
|
212
|
-
.dp-day:focus-visible {
|
|
213
|
-
outline: 2px solid var(--dp-accent);
|
|
214
|
-
outline-offset: -2px;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
203
|
/* On an accent-filled cell the ring inverts so it stays readable. */
|
|
218
204
|
.dp-day:is([data-selected="true"], [data-range-start="true"], [data-range-end="true"]):focus-visible {
|
|
219
205
|
outline-color: var(--dp-accent-fg);
|
|
@@ -275,6 +261,28 @@ html[dir="rtl"] .dp-nav svg {
|
|
|
275
261
|
border-end-end-radius: var(--dp-day-radius);
|
|
276
262
|
}
|
|
277
263
|
|
|
264
|
+
/* Projected band: the range the next commit would produce. It reads as one
|
|
265
|
+
step short of the committed band, so a hovered, keyboard or dragged preview
|
|
266
|
+
is never mistaken for the selection itself. */
|
|
267
|
+
[data-range-preview] .dp-day:is([data-range-start="true"], [data-range-end="true"]) {
|
|
268
|
+
background: color-mix(in srgb, var(--dp-accent) 58%, var(--dp-bg));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
[data-range-preview] .dp-day[data-in-range="true"] {
|
|
272
|
+
background: color-mix(in srgb, var(--dp-accent) 11%, var(--dp-bg));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/* Endpoint handles are draggable only while the committed range is complete
|
|
276
|
+
and its two ends sit on different days. */
|
|
277
|
+
[data-range-drag] .dp-day:is([data-range-start="true"], [data-range-end="true"]) {
|
|
278
|
+
cursor: grab;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
[data-range-drag="active"],
|
|
282
|
+
[data-range-drag="active"] .dp-day {
|
|
283
|
+
cursor: grabbing;
|
|
284
|
+
}
|
|
285
|
+
|
|
278
286
|
.dp-day-number {
|
|
279
287
|
display: block;
|
|
280
288
|
font-variant-numeric: tabular-nums;
|
|
@@ -320,14 +328,6 @@ date-picker {
|
|
|
320
328
|
max-inline-size: 100%;
|
|
321
329
|
}
|
|
322
330
|
|
|
323
|
-
/* Visible focus is normalized to --dp-accent so the component holds together
|
|
324
|
-
without a consumer stylesheet; the field's own look stays consumer-owned. */
|
|
325
|
-
date-picker > input:not([type="hidden"]):focus-visible {
|
|
326
|
-
border-color: var(--dp-accent);
|
|
327
|
-
outline: none;
|
|
328
|
-
box-shadow: 0 0 0 3px color-mix(in srgb, var(--dp-accent) 20%, transparent);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
331
|
/* Every authored control is a grid item. */
|
|
332
332
|
date-picker > input:not([type="hidden"]) {
|
|
333
333
|
min-inline-size: 0;
|
|
@@ -368,7 +368,7 @@ date-picker > input:not([type="hidden"]):not([type="time"]) {
|
|
|
368
368
|
field already shows its own ring through :focus-visible). Same radius as
|
|
369
369
|
everywhere else: no bespoke shape. */
|
|
370
370
|
.dp-picker-button:focus-visible {
|
|
371
|
-
outline:
|
|
371
|
+
outline: var(--dp-focus-width) solid var(--dp-focus);
|
|
372
372
|
outline-offset: -5px;
|
|
373
373
|
border-radius: var(--dp-radius);
|
|
374
374
|
}
|