@lekoala/date-picker 0.2.0 → 0.3.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 +6 -4
- package/custom-elements.json +12 -2
- package/dist/date-picker.css +153 -105
- package/dist/date-picker.js +166 -78
- package/dist/date-picker.min.css +1 -1
- package/dist/date-picker.min.js +13 -13
- package/dist/date-picker.standalone.min.js +167 -119
- package/dist/types/date-calendar.d.ts.map +1 -1
- package/dist/types/date-field.d.ts +3 -1
- package/dist/types/date-field.d.ts.map +1 -1
- package/dist/types/date-picker.d.ts +62 -5
- package/dist/types/date-picker.d.ts.map +1 -1
- package/docs/ACCESSIBILITY.md +17 -9
- package/docs/API.md +21 -2
- package/docs/ARCHITECTURE.md +16 -2
- package/docs/AT-CHECKLIST.md +1 -1
- package/docs/DECISIONS.md +2 -0
- package/docs/MIGRATION-FLATPICKR.md +1 -1
- package/package.json +4 -4
- package/src/date-calendar.js +3 -6
- package/src/date-field.js +12 -4
- package/src/date-picker.css +153 -105
- package/src/date-picker.js +206 -75
package/src/date-picker.js
CHANGED
|
@@ -36,7 +36,11 @@ export class DatePickerElement extends HTMLElement {
|
|
|
36
36
|
/** Map of clicked/keyboard-activated grid dates to the generation at click
|
|
37
37
|
* time (range mode supersession/stale protection). @type {Map<string, number>} */
|
|
38
38
|
this._pendingIntents = new Map();
|
|
39
|
-
|
|
39
|
+
/** Overlaid calendar triggers, one per field (single) or per bound (range). @type {{button: HTMLButtonElement, endpoint: "" | "start" | "end"}[]} */
|
|
40
|
+
this._buttons = [];
|
|
41
|
+
/** Control that opened the popover, so single mode can restore focus to it.
|
|
42
|
+
* @type {HTMLElement | null} */
|
|
43
|
+
this._returnFocus = null;
|
|
40
44
|
this._panel = null;
|
|
41
45
|
this._calendar = null;
|
|
42
46
|
this._originalDescribedBy = "";
|
|
@@ -56,6 +60,8 @@ export class DatePickerElement extends HTMLElement {
|
|
|
56
60
|
this._renderDay = null;
|
|
57
61
|
/** @type {any} */
|
|
58
62
|
this._isDateDisabled = null;
|
|
63
|
+
/** Public picker coordinate-space request. @type {"viewport" | "document"} */
|
|
64
|
+
this._coordinateSpace = "viewport";
|
|
59
65
|
}
|
|
60
66
|
|
|
61
67
|
_rangeMode() {
|
|
@@ -165,7 +171,7 @@ export class DatePickerElement extends HTMLElement {
|
|
|
165
171
|
field.confirm = (date) => this._confirmDate(date);
|
|
166
172
|
field.setupFormValue();
|
|
167
173
|
}
|
|
168
|
-
this._build(
|
|
174
|
+
this._build();
|
|
169
175
|
this._setRangeAria();
|
|
170
176
|
this._syncRangeFields();
|
|
171
177
|
this._bindRange();
|
|
@@ -204,10 +210,11 @@ export class DatePickerElement extends HTMLElement {
|
|
|
204
210
|
this._teardownField(this._field);
|
|
205
211
|
}
|
|
206
212
|
this._field = null;
|
|
207
|
-
this.
|
|
213
|
+
for (const { button } of this._buttons) button.remove();
|
|
214
|
+
this._buttons = [];
|
|
215
|
+
this._returnFocus = null;
|
|
208
216
|
this._panel?.remove();
|
|
209
217
|
this._formatHint?.remove();
|
|
210
|
-
this._button = null;
|
|
211
218
|
this._panel = null;
|
|
212
219
|
this._calendar = null;
|
|
213
220
|
this._formatHint = null;
|
|
@@ -392,6 +399,15 @@ export class DatePickerElement extends HTMLElement {
|
|
|
392
399
|
if (this._connected) void this.validate();
|
|
393
400
|
}
|
|
394
401
|
|
|
402
|
+
/** @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"} */
|
|
403
|
+
get coordinateSpace() {
|
|
404
|
+
return this._coordinateSpace;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
set coordinateSpace(value) {
|
|
408
|
+
this._coordinateSpace = value === "document" ? "document" : "viewport";
|
|
409
|
+
}
|
|
410
|
+
|
|
395
411
|
_adapter() {
|
|
396
412
|
return this._field?.adapter ?? this._fields?.[0]?.adapter ?? createDateAdapter(this.locale);
|
|
397
413
|
}
|
|
@@ -406,16 +422,22 @@ export class DatePickerElement extends HTMLElement {
|
|
|
406
422
|
// The hidden canonical field owns submission; dynamic name/form/disabled
|
|
407
423
|
// movement lives in the field controller.
|
|
408
424
|
this._field?.syncInputState();
|
|
409
|
-
|
|
425
|
+
const button = this._buttons[0]?.button;
|
|
426
|
+
if (button) button.disabled = input.disabled || input.readOnly;
|
|
410
427
|
if (this._open && (input.disabled || input.readOnly)) this.hide(false);
|
|
411
428
|
}
|
|
412
429
|
|
|
413
430
|
_syncRangeFields() {
|
|
414
431
|
if (!this._fields) return;
|
|
415
432
|
for (const field of this._fields) field.syncInputState();
|
|
433
|
+
// Each trigger follows its own bound: a disabled end never disables the
|
|
434
|
+
// start trigger.
|
|
435
|
+
for (const { button, endpoint } of this._buttons) {
|
|
436
|
+
const index = endpoint === "end" ? 1 : 0;
|
|
437
|
+
const field = this._fields[index];
|
|
438
|
+
button.disabled = Boolean(field?.input.disabled || field?.input.readOnly);
|
|
439
|
+
}
|
|
416
440
|
const active = this._range?.activeEndpoint === "end" ? 1 : 0;
|
|
417
|
-
const disabled = this._fields.every((field) => field.input.disabled || field.input.readOnly);
|
|
418
|
-
if (this._button) this._button.disabled = disabled;
|
|
419
441
|
if (this._open && (this._fields[active]?.input.disabled || this._fields[active]?.input.readOnly)) {
|
|
420
442
|
this.hide(false);
|
|
421
443
|
}
|
|
@@ -436,20 +458,52 @@ export class DatePickerElement extends HTMLElement {
|
|
|
436
458
|
}
|
|
437
459
|
}
|
|
438
460
|
|
|
439
|
-
/**
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
461
|
+
/**
|
|
462
|
+
* Create one calendar trigger. It is a real button overlaid inside the
|
|
463
|
+
* field's own box (see the CSS): the field reserves the room, the trigger
|
|
464
|
+
* stays transparent, and the field's focus ring wraps both.
|
|
465
|
+
* @param {"" | "start" | "end"} endpoint
|
|
466
|
+
* @returns {HTMLButtonElement}
|
|
467
|
+
*/
|
|
468
|
+
_createTrigger(endpoint) {
|
|
445
469
|
const button = document.createElement("button");
|
|
446
470
|
button.type = "button";
|
|
447
471
|
button.className = "dp-picker-button";
|
|
448
472
|
button.setAttribute("aria-haspopup", "dialog");
|
|
449
473
|
button.setAttribute("aria-expanded", "false");
|
|
450
|
-
button.setAttribute("aria-controls",
|
|
474
|
+
button.setAttribute("aria-controls", `${this._id}-panel`);
|
|
475
|
+
if (endpoint) button.dataset.endpoint = endpoint;
|
|
451
476
|
button.innerHTML =
|
|
452
477
|
'<span aria-hidden="true"><svg viewBox="0 0 16 16" width="1em" height="1em" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"><rect x="1.75" y="3" width="12.5" height="11" rx="1.75"/><path d="M1.75 6.75h12.5"/><path d="M5.25 1.75v2.5M10.75 1.75v2.5"/></svg></span>';
|
|
478
|
+
return button;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Build the popup once and one trigger per field. Each trigger is inserted
|
|
483
|
+
* immediately after its own field so DOM order follows visual/tab order
|
|
484
|
+
* (`date -> its trigger -> next control`). The hidden ISO field may sit
|
|
485
|
+
* between the two; it is not focusable.
|
|
486
|
+
*/
|
|
487
|
+
_build() {
|
|
488
|
+
const ranges = this._rangeMode();
|
|
489
|
+
/** @type {[HTMLInputElement, "" | "start" | "end"][]} */
|
|
490
|
+
const targets = [];
|
|
491
|
+
if (ranges) {
|
|
492
|
+
for (const [index, field] of (this._fields || []).entries()) {
|
|
493
|
+
targets.push([field.input, index === 1 ? "end" : "start"]);
|
|
494
|
+
}
|
|
495
|
+
} else if (this._input) {
|
|
496
|
+
targets.push([this._input, ""]);
|
|
497
|
+
}
|
|
498
|
+
if (!targets.length) return;
|
|
499
|
+
const panelId = `${this._id}-panel`;
|
|
500
|
+
const hintId = `${this._id}-format`;
|
|
501
|
+
|
|
502
|
+
this._buttons = targets.map(([input, endpoint]) => {
|
|
503
|
+
const button = this._createTrigger(endpoint);
|
|
504
|
+
input.insertAdjacentElement("afterend", button);
|
|
505
|
+
return { button, endpoint };
|
|
506
|
+
});
|
|
453
507
|
|
|
454
508
|
const panel = document.createElement("div");
|
|
455
509
|
panel.id = panelId;
|
|
@@ -467,23 +521,21 @@ export class DatePickerElement extends HTMLElement {
|
|
|
467
521
|
hint.id = hintId;
|
|
468
522
|
hint.className = "dp-visually-hidden";
|
|
469
523
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
panel.insertAdjacentElement("afterend", hint);
|
|
524
|
+
// Out of flow (popover / visually hidden): placement is irrelevant.
|
|
525
|
+
this.append(panel, hint);
|
|
473
526
|
|
|
474
|
-
this._button = button;
|
|
475
527
|
this._panel = panel;
|
|
476
528
|
this._calendar = calendar;
|
|
477
529
|
this._formatHint = hint;
|
|
478
530
|
|
|
479
|
-
if (!this.
|
|
480
|
-
this._originalDescribedBy =
|
|
531
|
+
if (!ranges && this._input) {
|
|
532
|
+
this._originalDescribedBy = this._input.getAttribute("aria-describedby") || "";
|
|
481
533
|
const describedBy = [this._originalDescribedBy, hintId].filter(Boolean).join(" ");
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
534
|
+
this._input.setAttribute("aria-describedby", describedBy);
|
|
535
|
+
this._input.setAttribute("role", "combobox");
|
|
536
|
+
this._input.setAttribute("aria-haspopup", "dialog");
|
|
537
|
+
this._input.setAttribute("aria-expanded", "false");
|
|
538
|
+
this._input.setAttribute("aria-controls", panelId);
|
|
487
539
|
}
|
|
488
540
|
this._refreshLocale();
|
|
489
541
|
this._refreshButtonLabel();
|
|
@@ -491,7 +543,7 @@ export class DatePickerElement extends HTMLElement {
|
|
|
491
543
|
|
|
492
544
|
_bind() {
|
|
493
545
|
const input = this._input;
|
|
494
|
-
const button = this.
|
|
546
|
+
const button = this._buttons[0]?.button;
|
|
495
547
|
const calendar = this._calendar;
|
|
496
548
|
if (!input || !button || !calendar) return;
|
|
497
549
|
const controller = new AbortController();
|
|
@@ -550,10 +602,11 @@ export class DatePickerElement extends HTMLElement {
|
|
|
550
602
|
this.hide(false);
|
|
551
603
|
return;
|
|
552
604
|
}
|
|
553
|
-
this.show();
|
|
605
|
+
this.show({ returnFocus: button });
|
|
554
606
|
},
|
|
555
607
|
{ signal },
|
|
556
608
|
);
|
|
609
|
+
button.addEventListener("keydown", (event) => this._onTriggerKeyDown("", event), { signal });
|
|
557
610
|
calendar.addEventListener(
|
|
558
611
|
"dateactivate",
|
|
559
612
|
(event) => {
|
|
@@ -568,7 +621,7 @@ export class DatePickerElement extends HTMLElement {
|
|
|
568
621
|
this._field?.dirty();
|
|
569
622
|
this._setValue(date, { emit: true, format: true });
|
|
570
623
|
this.hide(false);
|
|
571
|
-
this.
|
|
624
|
+
this._restoreFocus();
|
|
572
625
|
});
|
|
573
626
|
},
|
|
574
627
|
{ signal },
|
|
@@ -601,9 +654,8 @@ export class DatePickerElement extends HTMLElement {
|
|
|
601
654
|
}
|
|
602
655
|
|
|
603
656
|
_bindRange() {
|
|
604
|
-
const button = this._button;
|
|
605
657
|
const calendar = this._calendar;
|
|
606
|
-
if (!this._fields || !
|
|
658
|
+
if (!this._fields || !this._buttons.length || !calendar) return;
|
|
607
659
|
const controller = new AbortController();
|
|
608
660
|
this._controller = controller;
|
|
609
661
|
const { signal } = controller;
|
|
@@ -620,21 +672,16 @@ export class DatePickerElement extends HTMLElement {
|
|
|
620
672
|
);
|
|
621
673
|
field.input.addEventListener("keydown", (event) => this._onFieldKeyDown(endpoint, event), { signal });
|
|
622
674
|
}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
(
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
}
|
|
634
|
-
this.show();
|
|
635
|
-
},
|
|
636
|
-
{ signal },
|
|
637
|
-
);
|
|
675
|
+
for (const entry of this._buttons) {
|
|
676
|
+
const endpoint = entry.endpoint;
|
|
677
|
+
if (endpoint === "") continue;
|
|
678
|
+
entry.button.addEventListener("click", (event) => this._onRangeTriggerClick(endpoint, event), {
|
|
679
|
+
signal,
|
|
680
|
+
});
|
|
681
|
+
entry.button.addEventListener("keydown", (event) => this._onTriggerKeyDown(endpoint, event), {
|
|
682
|
+
signal,
|
|
683
|
+
});
|
|
684
|
+
}
|
|
638
685
|
// Record grid-activation intent synchronously (click or Enter/Space). The
|
|
639
686
|
// picker only commits a dateactivate whose generation still matches, so a
|
|
640
687
|
// response that resolves after the active endpoint changed (or after a
|
|
@@ -705,9 +752,72 @@ export class DatePickerElement extends HTMLElement {
|
|
|
705
752
|
this.addEventListener("keydown", (event) => this._onEscape(event), { signal });
|
|
706
753
|
}
|
|
707
754
|
|
|
755
|
+
/**
|
|
756
|
+
* Point the shared calendar at one range bound and invalidate any pending
|
|
757
|
+
* activation, so a late response can never commit on the wrong bound.
|
|
758
|
+
* @param {"start" | "end"} endpoint
|
|
759
|
+
*/
|
|
760
|
+
_activateRangeEndpoint(endpoint) {
|
|
761
|
+
const calendar = this._calendar;
|
|
762
|
+
if (!this._range || !calendar) return;
|
|
763
|
+
this._range.focus(endpoint);
|
|
764
|
+
this._rangeCommitId++;
|
|
765
|
+
this._pendingIntents.clear();
|
|
766
|
+
this._lastFocusEndpoint = endpoint;
|
|
767
|
+
const target = this._boundCanonical(endpoint) || todayISO();
|
|
768
|
+
calendar.display = monthKey(target);
|
|
769
|
+
calendar.focusedDate = target;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* Range has one trigger per bound. A press on the other bound's trigger is a
|
|
774
|
+
* local switch, not a toggle: it repoints the single open popup instead of
|
|
775
|
+
* closing it. Only a press on the already-active bound toggles shut.
|
|
776
|
+
* @param {"start" | "end"} endpoint
|
|
777
|
+
* @param {MouseEvent} event
|
|
778
|
+
*/
|
|
779
|
+
_onRangeTriggerClick(endpoint, event) {
|
|
780
|
+
if (!this._open) {
|
|
781
|
+
this.show({ endpoint });
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
if (event.detail === 0) {
|
|
785
|
+
// Keyboard activation of an already-open picker: switch the bound, then
|
|
786
|
+
// move into the grid. Escape keeps closing it.
|
|
787
|
+
this._activateRangeEndpoint(endpoint);
|
|
788
|
+
setTimeout(() => this._calendar?.focusGrid(), 0);
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
if (endpoint !== this._range?.activeEndpoint) {
|
|
792
|
+
this._activateRangeEndpoint(endpoint);
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
this.hide(false);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* ArrowDown on a trigger opens the popup and drops into the grid, like the
|
|
800
|
+
* field itself (Enter/Space go through the click path). Needed because focus
|
|
801
|
+
* now returns to the trigger that opened the popover.
|
|
802
|
+
* @param {"" | "start" | "end"} endpoint
|
|
803
|
+
* @param {KeyboardEvent} event
|
|
804
|
+
*/
|
|
805
|
+
_onTriggerKeyDown(endpoint, event) {
|
|
806
|
+
if (event.key !== "ArrowDown") return;
|
|
807
|
+
event.preventDefault();
|
|
808
|
+
if (this._open) {
|
|
809
|
+
if (this._rangeMode() && endpoint) this._activateRangeEndpoint(endpoint);
|
|
810
|
+
// Chromium drops script-initiated focus changes during keydown dispatch.
|
|
811
|
+
setTimeout(() => this._calendar?.focusGrid(), 0);
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
if (this._rangeMode()) this.show({ endpoint: endpoint || undefined });
|
|
815
|
+
else this.show({ returnFocus: this._buttons[0]?.button });
|
|
816
|
+
}
|
|
817
|
+
|
|
708
818
|
/** @param {"" | "start" | "end"} endpoint @param {KeyboardEvent} event */
|
|
709
819
|
_onFieldKeyDown(endpoint, event) {
|
|
710
|
-
if (event.key === "ArrowDown"
|
|
820
|
+
if (event.key === "ArrowDown") {
|
|
711
821
|
event.preventDefault();
|
|
712
822
|
if (this._rangeMode() && endpoint) {
|
|
713
823
|
this._lastFocusEndpoint = endpoint;
|
|
@@ -758,6 +868,8 @@ export class DatePickerElement extends HTMLElement {
|
|
|
758
868
|
const cell = event.target.closest(".dp-day[data-date]");
|
|
759
869
|
const date = cell?.getAttribute("data-date") || "";
|
|
760
870
|
if (!isDate(date)) return;
|
|
871
|
+
// Each activation supersedes every earlier pending one.
|
|
872
|
+
this._pendingIntents.clear();
|
|
761
873
|
this._pendingIntents.set(date, ++this._rangeCommitId);
|
|
762
874
|
}
|
|
763
875
|
|
|
@@ -818,25 +930,29 @@ export class DatePickerElement extends HTMLElement {
|
|
|
818
930
|
}
|
|
819
931
|
|
|
820
932
|
_refreshButtonLabel() {
|
|
821
|
-
|
|
933
|
+
const single = this._buttons[0]?.button;
|
|
934
|
+
if (!single) return;
|
|
822
935
|
if (this._rangeMode()) {
|
|
823
936
|
this._refreshRangeButtonLabel();
|
|
824
937
|
return;
|
|
825
938
|
}
|
|
826
939
|
const prefix = this._value ? this._messages.changeDate : this._messages.chooseDate;
|
|
827
940
|
const suffix = this._value ? `, ${formatLongDate(this._value, this.locale)}` : "";
|
|
828
|
-
|
|
941
|
+
single.setAttribute("aria-label", `${prefix}${suffix}`);
|
|
829
942
|
}
|
|
830
943
|
|
|
944
|
+
/** Each bound has its own trigger, so each needs its own accessible name: the
|
|
945
|
+
* bound label is what tells two otherwise identical triggers apart. */
|
|
831
946
|
_refreshRangeButtonLabel() {
|
|
832
|
-
if (!this.
|
|
947
|
+
if (!this._range) return;
|
|
833
948
|
const { start, end } = this._range;
|
|
834
|
-
const
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
? `, ${formatLongDate(
|
|
838
|
-
|
|
839
|
-
|
|
949
|
+
for (const { button, endpoint } of this._buttons) {
|
|
950
|
+
const bound = endpoint === "end" ? end : start;
|
|
951
|
+
const prefix = bound ? this._messages.changeDate : this._messages.chooseDate;
|
|
952
|
+
const suffix = bound ? `, ${formatLongDate(bound, this.locale)}` : "";
|
|
953
|
+
const boundLabel = endpoint === "end" ? this._messages.rangeEnd : this._messages.rangeStart;
|
|
954
|
+
button.setAttribute("aria-label", `${prefix}${suffix}, ${boundLabel}`);
|
|
955
|
+
}
|
|
840
956
|
}
|
|
841
957
|
|
|
842
958
|
/** @param {string} value */
|
|
@@ -912,7 +1028,9 @@ export class DatePickerElement extends HTMLElement {
|
|
|
912
1028
|
this._orderTags.delete(index);
|
|
913
1029
|
return false;
|
|
914
1030
|
}
|
|
915
|
-
|
|
1031
|
+
// Typed commits keep native input/change only; synthetic events are
|
|
1032
|
+
// reserved for calendar-driven changes (no native input event occurred).
|
|
1033
|
+
this._setBound(which, result.value || "", { emit: false, user: true, format: result.status === "ok" });
|
|
916
1034
|
return true;
|
|
917
1035
|
}
|
|
918
1036
|
|
|
@@ -1099,13 +1217,19 @@ export class DatePickerElement extends HTMLElement {
|
|
|
1099
1217
|
}, 0);
|
|
1100
1218
|
}
|
|
1101
1219
|
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1220
|
+
/**
|
|
1221
|
+
* Restore focus to the control that opened the popover (single mode). focus()
|
|
1222
|
+
* must not run synchronously from a keydown handler (Chromium drops focus
|
|
1223
|
+
* changes there) and must not re-open the popover under the default
|
|
1224
|
+
* open-on-focus. Deferring keeps _suppressFocusOpen active for the call.
|
|
1225
|
+
*/
|
|
1226
|
+
_restoreFocus() {
|
|
1227
|
+
const target = this._returnFocus;
|
|
1228
|
+
this._returnFocus = null;
|
|
1229
|
+
if (!(target instanceof HTMLElement) || !target.isConnected || target.matches(":disabled")) return;
|
|
1106
1230
|
this._suppressFocusOpen = true;
|
|
1107
1231
|
setTimeout(() => {
|
|
1108
|
-
|
|
1232
|
+
target.focus();
|
|
1109
1233
|
this._suppressFocusOpen = false;
|
|
1110
1234
|
}, 0);
|
|
1111
1235
|
}
|
|
@@ -1160,34 +1284,39 @@ export class DatePickerElement extends HTMLElement {
|
|
|
1160
1284
|
return input.checkValidity() && timesValid;
|
|
1161
1285
|
}
|
|
1162
1286
|
|
|
1163
|
-
/** @public @param {{moveFocus?:boolean}} [options] */
|
|
1287
|
+
/** @public @param {{moveFocus?:boolean, endpoint?:"start"|"end", returnFocus?:HTMLElement}} [options] */
|
|
1164
1288
|
show(options = {}) {
|
|
1165
1289
|
const panel = this._panel;
|
|
1166
1290
|
const calendar = this._calendar;
|
|
1167
|
-
|
|
1168
|
-
if (!panel || !calendar || !button || this._open) return;
|
|
1291
|
+
if (!panel || !calendar || !this._buttons.length || this._open) return;
|
|
1169
1292
|
if (this._rangeMode()) {
|
|
1170
|
-
const endpoint = this._resolveRangeEndpoint();
|
|
1293
|
+
const endpoint = options.endpoint || this._resolveRangeEndpoint();
|
|
1171
1294
|
if (!endpoint) return;
|
|
1172
|
-
this.
|
|
1173
|
-
this._rangeCommitId++;
|
|
1174
|
-
this._pendingIntents.clear();
|
|
1175
|
-
const target = this._boundCanonical(endpoint) || todayISO();
|
|
1176
|
-
calendar.display = monthKey(target);
|
|
1177
|
-
calendar.focusedDate = target;
|
|
1178
|
-
this._lastFocusEndpoint = endpoint;
|
|
1295
|
+
this._activateRangeEndpoint(endpoint);
|
|
1179
1296
|
} else {
|
|
1180
1297
|
const input = this._input;
|
|
1181
1298
|
if (!input || input.disabled || input.readOnly) return;
|
|
1182
1299
|
const target = this._value || this._adapter().parse(input.value) || todayISO();
|
|
1183
1300
|
calendar.display = monthKey(target);
|
|
1184
1301
|
calendar.focusedDate = target;
|
|
1302
|
+
// Popover convention: closing returns focus to the control that opened.
|
|
1303
|
+
this._returnFocus = options.returnFocus instanceof HTMLElement ? options.returnFocus : input;
|
|
1185
1304
|
}
|
|
1186
1305
|
panel.showPopover();
|
|
1187
1306
|
this._open = true;
|
|
1188
1307
|
this._setExpanded(true);
|
|
1308
|
+
// Freeze the explicitly configured coordinate space once per opening: a
|
|
1309
|
+
// change made while open takes effect on the next opening, never the
|
|
1310
|
+
// current one.
|
|
1311
|
+
const coordinateSpace = this.coordinateSpace;
|
|
1312
|
+
panel.style.position = coordinateSpace === "document" ? "absolute" : "fixed";
|
|
1189
1313
|
const position = () =>
|
|
1190
|
-
reposition(this, panel, {
|
|
1314
|
+
reposition(this, panel, {
|
|
1315
|
+
placement: "bottom-start",
|
|
1316
|
+
distance: 4,
|
|
1317
|
+
shiftPadding: 8,
|
|
1318
|
+
coordinateSpace,
|
|
1319
|
+
});
|
|
1191
1320
|
position();
|
|
1192
1321
|
this._stopTracking = autoUpdate(this, panel, position);
|
|
1193
1322
|
if (options.moveFocus !== false) queueMicrotask(() => calendar.focusGrid());
|
|
@@ -1198,7 +1327,7 @@ export class DatePickerElement extends HTMLElement {
|
|
|
1198
1327
|
_setExpanded(expanded) {
|
|
1199
1328
|
const value = expanded ? "true" : "false";
|
|
1200
1329
|
this._input?.setAttribute("aria-expanded", value);
|
|
1201
|
-
this.
|
|
1330
|
+
for (const { button } of this._buttons) button.setAttribute("aria-expanded", value);
|
|
1202
1331
|
for (const field of this._fields || []) field.input.setAttribute("aria-expanded", value);
|
|
1203
1332
|
}
|
|
1204
1333
|
|
|
@@ -1218,10 +1347,12 @@ export class DatePickerElement extends HTMLElement {
|
|
|
1218
1347
|
this._pendingIntents.clear();
|
|
1219
1348
|
if (restoreFocus) {
|
|
1220
1349
|
if (this._rangeMode()) {
|
|
1350
|
+
// Range owns a workflow: after a first pick the active bound moved, so
|
|
1351
|
+
// focus follows the bound rather than the original trigger.
|
|
1221
1352
|
const endpoint = this._restoreFocusEndpoint();
|
|
1222
1353
|
if (endpoint) this._focusField(endpoint);
|
|
1223
1354
|
} else {
|
|
1224
|
-
this.
|
|
1355
|
+
this._restoreFocus();
|
|
1225
1356
|
}
|
|
1226
1357
|
}
|
|
1227
1358
|
this.dispatchEvent(new Event("close", { bubbles: true }));
|