@reliability-design/web 1.4.5 → 1.4.6
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/components/rel-alert/rel-alert.ts +1 -1
- package/dist/components/rel-autocomplete/rel-autocomplete.ts +33 -2
- package/dist/components/rel-badge/rel-badge.ts +6 -1
- package/dist/components/rel-calendar/rel-calendar.ts +64 -18
- package/dist/components/rel-carousel/rel-carousel.ts +25 -2
- package/dist/components/rel-chip/rel-chip.ts +31 -20
- package/dist/components/rel-date-picker/date-parse.test.ts +116 -0
- package/dist/components/rel-date-picker/date-parse.ts +178 -0
- package/dist/components/rel-date-picker/rel-date-picker.ts +529 -90
- package/dist/components/rel-drawer/rel-drawer.ts +6 -3
- package/dist/components/rel-menu/rel-menu.ts +6 -0
- package/dist/components/rel-otp/rel-otp.ts +12 -2
- package/dist/components/rel-pagination/rel-pagination.ts +13 -0
- package/dist/components/rel-popover/rel-popover.ts +33 -2
- package/dist/components/rel-progress/rel-progress.ts +13 -4
- package/dist/components/rel-radio/README.md +6 -3
- package/dist/components/rel-radio/rel-radio.ts +236 -60
- package/dist/components/rel-range-picker/rel-range-picker.ts +452 -95
- package/dist/components/rel-segmented-button/rel-segmented-button.ts +5 -1
- package/dist/components/rel-select/rel-select.ts +44 -13
- package/dist/components/rel-sheet/rel-sheet.ts +163 -18
- package/dist/components/rel-sidebar/rel-sidebar.ts +7 -1
- package/dist/components/rel-skeleton/rel-skeleton.ts +9 -0
- package/dist/components/rel-slider/rel-slider.ts +6 -3
- package/dist/components/rel-snackbar/rel-snackbar.ts +20 -16
- package/dist/components/rel-stepper/rel-stepper.ts +3 -1
- package/dist/components/rel-switch/rel-switch.ts +3 -1
- package/dist/components/rel-textfield/rel-textfield.ts +263 -75
- package/dist/components/rel-time-picker/rel-time-picker.ts +450 -94
- package/dist/components/rel-time-picker/time-parse.test.ts +67 -0
- package/dist/components/rel-time-picker/time-parse.ts +85 -0
- package/dist/components/rel-toast/rel-toast.ts +4 -2
- package/dist/components/rel-tooltip/README.md +13 -5
- package/dist/components/rel-tooltip/rel-tooltip.ts +3 -0
- package/dist/components/rel-upload/rel-upload.ts +25 -0
- package/dist/index.d.mts +241 -11
- package/dist/index.d.ts +241 -11
- package/dist/index.js +2573 -598
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2614 -640
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { LitElement, html, css, nothing } from "lit";
|
|
2
2
|
import { customElement, property, state, query } from "lit/decorators.js";
|
|
3
|
+
import { live } from "lit/directives/live.js";
|
|
3
4
|
import "@reliability-design/icons/rel-icon";
|
|
4
5
|
import "../rel-button/rel-button";
|
|
6
|
+
import { announce } from "../../utils/announce";
|
|
7
|
+
import { isEditableTarget } from "../../utils/shortcuts";
|
|
8
|
+
import { parseDateInput } from "../rel-date-picker/date-parse";
|
|
5
9
|
|
|
6
10
|
interface RangeDay {
|
|
7
11
|
date: Date;
|
|
@@ -42,6 +46,7 @@ export class RelRangePicker extends LitElement {
|
|
|
42
46
|
@property({ type: String, attribute: "max-height" }) maxHeight: string = "";
|
|
43
47
|
@property({ type: String }) placeholder: string = "Select date range";
|
|
44
48
|
@property({ type: String, attribute: "trigger-variant" }) triggerVariant: "default" | "icon" = "default";
|
|
49
|
+
@property({ type: Boolean }) clearable = false;
|
|
45
50
|
|
|
46
51
|
@state() private draftFrom: string = "";
|
|
47
52
|
@state() private draftTo: string = "";
|
|
@@ -52,8 +57,12 @@ export class RelRangePicker extends LitElement {
|
|
|
52
57
|
@state() private hoverDate: string | null = null;
|
|
53
58
|
/** ISO date of the cell that owns the grid's single Tab stop (roving tabindex). */
|
|
54
59
|
@state() private focusedDate: string | null = null;
|
|
60
|
+
/** Uncommitted typed text; null → the input shows the committed range. */
|
|
61
|
+
@state() private inputText: string | null = null;
|
|
62
|
+
/** Parse/range error message for the typed text (cleared on next input). */
|
|
63
|
+
@state() private inputError: string | null = null;
|
|
55
64
|
|
|
56
|
-
@query(".trigger-
|
|
65
|
+
@query(".trigger-input") private triggerInput?: HTMLInputElement;
|
|
57
66
|
@query(".popover") private popoverEl?: HTMLDivElement;
|
|
58
67
|
@query(".backdrop") private backdropEl?: HTMLDivElement;
|
|
59
68
|
|
|
@@ -97,22 +106,81 @@ export class RelRangePicker extends LitElement {
|
|
|
97
106
|
letter-spacing: var(--rel-body-md-regular-letter-spacing);
|
|
98
107
|
}
|
|
99
108
|
|
|
100
|
-
|
|
109
|
+
:host(:not([disabled])) .trigger-button:hover {
|
|
101
110
|
border-color: var(--rel-border-accent);
|
|
102
111
|
}
|
|
103
112
|
|
|
104
|
-
.trigger-button:focus-
|
|
113
|
+
.trigger-button:focus-within {
|
|
105
114
|
outline: 2px solid var(--rel-border-focus);
|
|
106
115
|
outline-offset: 2px;
|
|
107
116
|
}
|
|
108
117
|
|
|
109
|
-
.trigger-
|
|
118
|
+
.trigger-button.error {
|
|
119
|
+
border-color: var(--rel-border-danger);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.trigger-button.error:focus-within {
|
|
123
|
+
outline-color: var(--rel-border-danger);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* The editable field itself — the wrapper carries the box styles. */
|
|
127
|
+
.trigger-input {
|
|
110
128
|
flex: 1;
|
|
129
|
+
min-width: 0;
|
|
130
|
+
border: none;
|
|
131
|
+
background: transparent;
|
|
132
|
+
padding: 0;
|
|
133
|
+
margin: 0;
|
|
134
|
+
outline: none;
|
|
111
135
|
color: var(--rel-text-default);
|
|
136
|
+
font: inherit;
|
|
137
|
+
letter-spacing: inherit;
|
|
138
|
+
cursor: inherit;
|
|
112
139
|
}
|
|
113
140
|
|
|
114
|
-
.trigger-
|
|
141
|
+
.trigger-input::placeholder {
|
|
142
|
+
color: var(--rel-text-placeholder);
|
|
143
|
+
opacity: 1;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/* Typed-entry error line (the component has no helper-text API). */
|
|
147
|
+
.range-picker-support.error {
|
|
148
|
+
margin-top: var(--rel-spacing-2xs);
|
|
149
|
+
color: var(--rel-text-danger);
|
|
150
|
+
font-family: var(--rel-body-xs-regular-font-family);
|
|
151
|
+
font-weight: var(--rel-body-xs-regular-font-weight);
|
|
152
|
+
font-size: var(--rel-body-xs-regular-font-size);
|
|
153
|
+
line-height: var(--rel-body-xs-regular-line-height);
|
|
154
|
+
letter-spacing: var(--rel-body-xs-regular-letter-spacing);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.trigger-icons {
|
|
158
|
+
display: flex;
|
|
159
|
+
align-items: center;
|
|
160
|
+
gap: var(--rel-spacing-2xs);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.icon-button {
|
|
164
|
+
background: none;
|
|
165
|
+
border: none;
|
|
166
|
+
padding: var(--rel-spacing-3xs);
|
|
167
|
+
cursor: pointer;
|
|
115
168
|
color: var(--rel-text-placeholder);
|
|
169
|
+
display: flex;
|
|
170
|
+
align-items: center;
|
|
171
|
+
justify-content: center;
|
|
172
|
+
border-radius: var(--rel-border-radius-xs);
|
|
173
|
+
transition: all 0.15s ease;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.icon-button:hover {
|
|
177
|
+
background: var(--rel-bg-surface-elevated);
|
|
178
|
+
color: var(--rel-text-default);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.icon-button svg {
|
|
182
|
+
width: 16px;
|
|
183
|
+
height: 16px;
|
|
116
184
|
}
|
|
117
185
|
|
|
118
186
|
/* ── Popover shell ── */
|
|
@@ -136,7 +204,13 @@ export class RelRangePicker extends LitElement {
|
|
|
136
204
|
border-radius: var(--rel-border-radius-sm, 8px);
|
|
137
205
|
background: var(--rel-bg-neutral);
|
|
138
206
|
box-shadow: var(--rel-shadows-shadow-200);
|
|
139
|
-
|
|
207
|
+
/* Never wider or taller than the viewport (small screens) — the
|
|
208
|
+
two-calendar layout scrolls inside the shell instead of bleeding
|
|
209
|
+
off-screen. border-box so the caps include the 1px borders. */
|
|
210
|
+
box-sizing: border-box;
|
|
211
|
+
max-width: calc(100vw - 16px);
|
|
212
|
+
max-height: calc(100vh - 16px);
|
|
213
|
+
overflow: auto;
|
|
140
214
|
display: none;
|
|
141
215
|
}
|
|
142
216
|
|
|
@@ -227,6 +301,28 @@ export class RelRangePicker extends LitElement {
|
|
|
227
301
|
grid-column: 1 / -1;
|
|
228
302
|
}
|
|
229
303
|
|
|
304
|
+
/* Single-key hint chip on each preset (T, M, W, …). */
|
|
305
|
+
.preset-key {
|
|
306
|
+
flex-shrink: 0;
|
|
307
|
+
margin-left: var(--rel-spacing-2xs);
|
|
308
|
+
border: 1px solid var(--rel-border-default);
|
|
309
|
+
border-bottom-width: 2px;
|
|
310
|
+
border-radius: var(--rel-border-radius-xs);
|
|
311
|
+
background: var(--rel-bg-surface);
|
|
312
|
+
padding: 0 var(--rel-spacing-3xs);
|
|
313
|
+
min-width: 18px;
|
|
314
|
+
text-align: center;
|
|
315
|
+
font-family: var(--rel-body-xs-regular-font-family);
|
|
316
|
+
font-weight: var(--rel-body-xs-regular-font-weight);
|
|
317
|
+
font-size: var(--rel-body-xs-regular-font-size);
|
|
318
|
+
line-height: var(--rel-body-xs-regular-line-height);
|
|
319
|
+
color: var(--rel-text-label);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.preset-item.active .preset-key {
|
|
323
|
+
display: none;
|
|
324
|
+
}
|
|
325
|
+
|
|
230
326
|
/* ── FROM/TO inputs ── */
|
|
231
327
|
.date-inputs {
|
|
232
328
|
display: flex;
|
|
@@ -340,11 +436,23 @@ export class RelRangePicker extends LitElement {
|
|
|
340
436
|
}
|
|
341
437
|
|
|
342
438
|
/* ── Calendar grid ── */
|
|
439
|
+
.calendar-table {
|
|
440
|
+
display: flex;
|
|
441
|
+
flex-direction: column;
|
|
442
|
+
gap: 6px;
|
|
443
|
+
}
|
|
444
|
+
|
|
343
445
|
.calendar-grid {
|
|
344
446
|
display: grid;
|
|
345
447
|
grid-template-columns: repeat(7, 1fr);
|
|
346
448
|
}
|
|
347
449
|
|
|
450
|
+
.calendar-week {
|
|
451
|
+
grid-column: 1 / -1;
|
|
452
|
+
display: grid;
|
|
453
|
+
grid-template-columns: repeat(7, 1fr);
|
|
454
|
+
}
|
|
455
|
+
|
|
348
456
|
.day-cell {
|
|
349
457
|
height: 36px;
|
|
350
458
|
position: relative;
|
|
@@ -458,7 +566,7 @@ export class RelRangePicker extends LitElement {
|
|
|
458
566
|
}
|
|
459
567
|
|
|
460
568
|
@media (prefers-reduced-motion: reduce) {
|
|
461
|
-
.preset-item, .nav-btn, .day-inner {
|
|
569
|
+
.preset-item, .nav-btn, .day-inner, .icon-button {
|
|
462
570
|
transition: none;
|
|
463
571
|
}
|
|
464
572
|
}
|
|
@@ -565,8 +673,9 @@ export class RelRangePicker extends LitElement {
|
|
|
565
673
|
}
|
|
566
674
|
let left = t.left;
|
|
567
675
|
if (left + c.width > window.innerWidth - 8) {
|
|
568
|
-
left =
|
|
676
|
+
left = window.innerWidth - c.width - 8;
|
|
569
677
|
}
|
|
678
|
+
left = Math.max(8, left);
|
|
570
679
|
|
|
571
680
|
// Without top-layer support a transformed ancestor may be the containing
|
|
572
681
|
// block for position:fixed, shifting the shell away from the viewport
|
|
@@ -596,11 +705,15 @@ export class RelRangePicker extends LitElement {
|
|
|
596
705
|
|
|
597
706
|
if (e.key === "Escape") {
|
|
598
707
|
this.cancelPopover();
|
|
599
|
-
this.
|
|
708
|
+
this.triggerInput?.focus();
|
|
600
709
|
return;
|
|
601
710
|
}
|
|
602
711
|
|
|
603
|
-
|
|
712
|
+
// composedPath-aware: a document-level e.target retargets to the host,
|
|
713
|
+
// so a plain tagName check misses the typed-entry input in the shadow
|
|
714
|
+
// root — single-key preset shortcuts must not fire while typing.
|
|
715
|
+
if (isEditableTarget(e)) return;
|
|
716
|
+
if (e.ctrlKey || e.metaKey || e.altKey) return;
|
|
604
717
|
|
|
605
718
|
const key = e.key.toUpperCase();
|
|
606
719
|
const keyMap: Record<string, string> = {
|
|
@@ -677,27 +790,30 @@ export class RelRangePicker extends LitElement {
|
|
|
677
790
|
}
|
|
678
791
|
|
|
679
792
|
private applySelection() {
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
793
|
+
// An empty draft is committable when a committed range exists — this is
|
|
794
|
+
// how Reset propagates to the trigger (Reset → Apply clears the input).
|
|
795
|
+
if (!this.draftFrom && !this.draftTo && !this.from && !this.to) return;
|
|
796
|
+
this.from = this.draftFrom;
|
|
797
|
+
this.to = this.draftTo;
|
|
798
|
+
this.inputText = null;
|
|
799
|
+
this.inputError = null;
|
|
800
|
+
this.dispatchEvent(
|
|
801
|
+
new CustomEvent("rel-change", {
|
|
802
|
+
detail: { from: this.from, to: this.to, label: this.displayLabel },
|
|
803
|
+
bubbles: true,
|
|
804
|
+
composed: true,
|
|
805
|
+
})
|
|
806
|
+
);
|
|
807
|
+
this.open = false;
|
|
808
|
+
this.isSelectingRange = false;
|
|
809
|
+
this.hoverDate = null;
|
|
810
|
+
this.dispatchEvent(
|
|
811
|
+
new CustomEvent("rel-open-change", {
|
|
812
|
+
detail: { open: false },
|
|
813
|
+
bubbles: true,
|
|
814
|
+
composed: true,
|
|
815
|
+
})
|
|
816
|
+
);
|
|
701
817
|
}
|
|
702
818
|
|
|
703
819
|
private resetDraft() {
|
|
@@ -710,6 +826,162 @@ export class RelRangePicker extends LitElement {
|
|
|
710
826
|
}
|
|
711
827
|
}
|
|
712
828
|
|
|
829
|
+
private clearRange(e: Event) {
|
|
830
|
+
e.stopPropagation();
|
|
831
|
+
this.from = "";
|
|
832
|
+
this.to = "";
|
|
833
|
+
this.draftFrom = "";
|
|
834
|
+
this.draftTo = "";
|
|
835
|
+
this.draftActivePreset = null;
|
|
836
|
+
this.isSelectingRange = false;
|
|
837
|
+
this.hoverDate = null;
|
|
838
|
+
this.inputText = null;
|
|
839
|
+
this.inputError = null;
|
|
840
|
+
this.dispatchEvent(
|
|
841
|
+
new CustomEvent("rel-change", {
|
|
842
|
+
detail: { from: "", to: "", label: "" },
|
|
843
|
+
bubbles: true,
|
|
844
|
+
composed: true,
|
|
845
|
+
})
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// ── Typed keyboard entry ──
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* Parse "date → date" text into an ISO from/to pair, or null. Each side
|
|
853
|
+
* goes through rel-date-picker's parseDateInput, so ISO / numeric /
|
|
854
|
+
* month-name dates and the t / t±N shortcuts all work per side.
|
|
855
|
+
* Separators: "→", "..", spaced "-", or the word "to" ("t - t+7",
|
|
856
|
+
* "21/7 to 28/7"). A single date yields a one-day range. Reversed input
|
|
857
|
+
* is normalized (from ≤ to). dayFirst is hard-coded false because the
|
|
858
|
+
* displayed format is always MM/DD/YYYY.
|
|
859
|
+
*/
|
|
860
|
+
private parseRangeText(text: string): { from: string; to: string } | null {
|
|
861
|
+
const parts = text
|
|
862
|
+
.split(/\s+to\s+|\s*(?:→|\.\.)\s*|\s+-\s+/i)
|
|
863
|
+
.map((p) => p.trim())
|
|
864
|
+
.filter(Boolean);
|
|
865
|
+
if (parts.length < 1 || parts.length > 2) return null;
|
|
866
|
+
|
|
867
|
+
const opts = { referenceDate: new Date(), dayFirst: false, locale: this.locale };
|
|
868
|
+
const fromDate = parseDateInput(parts[0], opts);
|
|
869
|
+
if (!fromDate) return null;
|
|
870
|
+
const toDate = parts.length === 2 ? parseDateInput(parts[1], opts) : fromDate;
|
|
871
|
+
if (!toDate) return null;
|
|
872
|
+
|
|
873
|
+
const [a, b] =
|
|
874
|
+
fromDate.getTime() <= toDate.getTime() ? [fromDate, toDate] : [toDate, fromDate];
|
|
875
|
+
return { from: this.formatISODate(a), to: this.formatISODate(b) };
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* Parse and commit the typed text (Enter / blur). Invalid or out-of-range
|
|
880
|
+
* text keeps the field in an error state with the text preserved for
|
|
881
|
+
* correction; the committed range is untouched (Escape reverts the text).
|
|
882
|
+
*/
|
|
883
|
+
private commitTypedValue() {
|
|
884
|
+
if (this.inputText === null) return; // nothing typed since last commit
|
|
885
|
+
const text = this.inputText.trim();
|
|
886
|
+
|
|
887
|
+
if (!text) {
|
|
888
|
+
// Field cleared by keyboard — behaves like the clear button.
|
|
889
|
+
this.inputText = null;
|
|
890
|
+
this.inputError = null;
|
|
891
|
+
if (this.from || this.to) {
|
|
892
|
+
this.from = "";
|
|
893
|
+
this.to = "";
|
|
894
|
+
this.draftFrom = "";
|
|
895
|
+
this.draftTo = "";
|
|
896
|
+
this.draftActivePreset = null;
|
|
897
|
+
this.dispatchEvent(
|
|
898
|
+
new CustomEvent("rel-change", {
|
|
899
|
+
detail: { from: "", to: "", label: "" },
|
|
900
|
+
bubbles: true,
|
|
901
|
+
composed: true,
|
|
902
|
+
})
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
if (this.open) this.cancelPopover();
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
const range = this.parseRangeText(text);
|
|
910
|
+
if (!range) {
|
|
911
|
+
this.inputError = "Invalid date range";
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
const fromDate = this.parseISODate(range.from)!;
|
|
915
|
+
const toDate = this.parseISODate(range.to)!;
|
|
916
|
+
if (this.isDateDisabled(fromDate) || this.isDateDisabled(toDate)) {
|
|
917
|
+
this.inputError = "Date not available";
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
this.from = range.from;
|
|
922
|
+
this.to = range.to;
|
|
923
|
+
this.inputText = null;
|
|
924
|
+
this.inputError = null;
|
|
925
|
+
this.dispatchEvent(
|
|
926
|
+
new CustomEvent("rel-change", {
|
|
927
|
+
detail: { from: this.from, to: this.to, label: this.displayLabel },
|
|
928
|
+
bubbles: true,
|
|
929
|
+
composed: true,
|
|
930
|
+
})
|
|
931
|
+
);
|
|
932
|
+
if (this.open) this.cancelPopover();
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
private handleInputInput(e: Event) {
|
|
936
|
+
this.inputText = (e.target as HTMLInputElement).value;
|
|
937
|
+
if (this.inputError) this.inputError = null;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
private handleInputKeydown(e: KeyboardEvent) {
|
|
941
|
+
if (e.key === "Enter") {
|
|
942
|
+
e.preventDefault();
|
|
943
|
+
if (this.inputText === null) {
|
|
944
|
+
// Nothing typed — Enter opens the panel (previous trigger behavior).
|
|
945
|
+
if (!this.open) this.openPopover();
|
|
946
|
+
} else {
|
|
947
|
+
this.commitTypedValue();
|
|
948
|
+
}
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
if (e.key === "ArrowDown") {
|
|
952
|
+
e.preventDefault();
|
|
953
|
+
if (!this.open) this.openPopover();
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
if (e.key === "Escape" && this.inputText !== null) {
|
|
957
|
+
// Revert the typed text; the document listener closes the panel.
|
|
958
|
+
this.inputText = null;
|
|
959
|
+
this.inputError = null;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/** Clicking anywhere on the field opens the panel (never closes). */
|
|
964
|
+
private handleFieldClick() {
|
|
965
|
+
if (this.open || this.disabled) return;
|
|
966
|
+
this.openPopover();
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
/** The calendar icon toggles the panel (previous trigger behavior). */
|
|
970
|
+
private handleIconClick(e: Event) {
|
|
971
|
+
e.stopPropagation();
|
|
972
|
+
this.toggleOpen();
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/** Commit typed text when focus leaves the component entirely. */
|
|
976
|
+
private handleInputBlur = (e: FocusEvent) => {
|
|
977
|
+
this.dispatchEvent(new CustomEvent("rel-blur", { bubbles: true, composed: true }));
|
|
978
|
+
const related = e.relatedTarget as Node | null;
|
|
979
|
+
if (related && (this.renderRoot.contains(related) || this.contains(related))) {
|
|
980
|
+
return; // focus moved into the panel/icons — their own handlers commit
|
|
981
|
+
}
|
|
982
|
+
this.commitTypedValue();
|
|
983
|
+
};
|
|
984
|
+
|
|
713
985
|
private getTriggerLabel(): string {
|
|
714
986
|
const from = this.from ? this.formatDisplayDate(this.from) : null;
|
|
715
987
|
const to = this.to ? this.formatDisplayDate(this.to) : null;
|
|
@@ -832,6 +1104,7 @@ export class RelRangePicker extends LitElement {
|
|
|
832
1104
|
} else {
|
|
833
1105
|
this.viewMonth--;
|
|
834
1106
|
}
|
|
1107
|
+
announce(`${this.getMonthName()} ${this.viewYear}`);
|
|
835
1108
|
}
|
|
836
1109
|
|
|
837
1110
|
private nextMonth() {
|
|
@@ -841,6 +1114,7 @@ export class RelRangePicker extends LitElement {
|
|
|
841
1114
|
} else {
|
|
842
1115
|
this.viewMonth++;
|
|
843
1116
|
}
|
|
1117
|
+
announce(`${this.getMonthName()} ${this.viewYear}`);
|
|
844
1118
|
}
|
|
845
1119
|
|
|
846
1120
|
private getMonthName(): string {
|
|
@@ -849,6 +1123,23 @@ export class RelRangePicker extends LitElement {
|
|
|
849
1123
|
);
|
|
850
1124
|
}
|
|
851
1125
|
|
|
1126
|
+
private dayLabelFormatter: Intl.DateTimeFormat | null = null;
|
|
1127
|
+
|
|
1128
|
+
private formatDayLabel(day: RangeDay): string {
|
|
1129
|
+
if (!this.dayLabelFormatter) {
|
|
1130
|
+
this.dayLabelFormatter = new Intl.DateTimeFormat("en-US", {
|
|
1131
|
+
weekday: "long",
|
|
1132
|
+
month: "long",
|
|
1133
|
+
day: "numeric",
|
|
1134
|
+
year: "numeric",
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
let label = this.dayLabelFormatter.format(day.date);
|
|
1138
|
+
if (day.isDraftFrom) label += ", range start";
|
|
1139
|
+
if (day.isDraftTo) label += ", range end";
|
|
1140
|
+
return label;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
852
1143
|
private getWeekdayLabels(): string[] {
|
|
853
1144
|
const formatter = new Intl.DateTimeFormat("en-US", { weekday: "short" });
|
|
854
1145
|
const base = new Date(2024, 0, 7); // Sunday Jan 7 2024
|
|
@@ -965,19 +1256,23 @@ export class RelRangePicker extends LitElement {
|
|
|
965
1256
|
this.draftActivePreset = null;
|
|
966
1257
|
this.isSelectingRange = true;
|
|
967
1258
|
this.hoverDate = null;
|
|
1259
|
+
announce(`From date ${this.formatDisplayDate(iso)}`);
|
|
968
1260
|
} else {
|
|
969
1261
|
const fromDate = this.parseISODate(this.draftFrom);
|
|
970
1262
|
if (!fromDate) {
|
|
971
1263
|
this.draftFrom = iso;
|
|
972
1264
|
this.draftTo = "";
|
|
1265
|
+
announce(`From date ${this.formatDisplayDate(iso)}`);
|
|
973
1266
|
return;
|
|
974
1267
|
}
|
|
975
1268
|
|
|
976
1269
|
if (day.date.getTime() >= fromDate.getTime()) {
|
|
977
1270
|
this.draftTo = iso;
|
|
1271
|
+
announce(`To date ${this.formatDisplayDate(iso)}`);
|
|
978
1272
|
} else {
|
|
979
1273
|
this.draftTo = this.draftFrom;
|
|
980
1274
|
this.draftFrom = iso;
|
|
1275
|
+
announce(`From date ${this.formatDisplayDate(iso)}`);
|
|
981
1276
|
}
|
|
982
1277
|
|
|
983
1278
|
this.isSelectingRange = false;
|
|
@@ -1116,6 +1411,10 @@ export class RelRangePicker extends LitElement {
|
|
|
1116
1411
|
const days = this.getCalendarDays();
|
|
1117
1412
|
const rovingIso = this.getRovingIso(days);
|
|
1118
1413
|
const weekdayLabels = this.getWeekdayLabels();
|
|
1414
|
+
const weeks: RangeDay[][] = [];
|
|
1415
|
+
for (let i = 0; i < days.length; i += 7) {
|
|
1416
|
+
weeks.push(days.slice(i, i + 7));
|
|
1417
|
+
}
|
|
1119
1418
|
|
|
1120
1419
|
const draftFromDisplay = this.draftFrom ? this.formatDisplayDate(this.draftFrom) : "";
|
|
1121
1420
|
const draftToDisplay = this.draftTo ? this.formatDisplayDate(this.draftTo) : "";
|
|
@@ -1139,22 +1438,71 @@ export class RelRangePicker extends LitElement {
|
|
|
1139
1438
|
</rel-button>
|
|
1140
1439
|
`
|
|
1141
1440
|
: html`
|
|
1142
|
-
<
|
|
1143
|
-
part="trigger"
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
aria-haspopup="dialog"
|
|
1147
|
-
aria-expanded=${this.open}
|
|
1148
|
-
aria-label=${triggerLabel || this.placeholder}
|
|
1149
|
-
@click=${this.toggleOpen}
|
|
1441
|
+
<div
|
|
1442
|
+
part="trigger"
|
|
1443
|
+
class="trigger-button ${this.inputError ? "error" : ""}"
|
|
1444
|
+
@click=${this.handleFieldClick}
|
|
1150
1445
|
>
|
|
1151
|
-
<
|
|
1152
|
-
|
|
1446
|
+
<input
|
|
1447
|
+
part="value"
|
|
1448
|
+
class="trigger-input"
|
|
1449
|
+
type="text"
|
|
1450
|
+
.value=${live(this.inputText ?? triggerLabel)}
|
|
1451
|
+
placeholder=${this.placeholder}
|
|
1452
|
+
?disabled=${this.disabled}
|
|
1453
|
+
autocomplete="off"
|
|
1454
|
+
spellcheck="false"
|
|
1455
|
+
enterkeyhint="done"
|
|
1456
|
+
@input=${this.handleInputInput}
|
|
1457
|
+
@keydown=${this.handleInputKeydown}
|
|
1458
|
+
@blur=${this.handleInputBlur}
|
|
1459
|
+
role="combobox"
|
|
1460
|
+
aria-label=${this.placeholder}
|
|
1461
|
+
aria-haspopup="dialog"
|
|
1462
|
+
aria-expanded=${this.open}
|
|
1463
|
+
aria-controls="range-picker-panel"
|
|
1464
|
+
aria-invalid=${this.inputError !== null}
|
|
1465
|
+
aria-describedby=${this.inputError ? "range-picker-desc" : nothing}
|
|
1466
|
+
/>
|
|
1467
|
+
<span class="trigger-icons">
|
|
1468
|
+
${this.clearable && (this.from || this.to)
|
|
1469
|
+
? html`
|
|
1470
|
+
<button
|
|
1471
|
+
class="icon-button"
|
|
1472
|
+
type="button"
|
|
1473
|
+
aria-label="Clear date range"
|
|
1474
|
+
@click=${this.clearRange}
|
|
1475
|
+
>
|
|
1476
|
+
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
|
|
1477
|
+
<path
|
|
1478
|
+
d="M4 4l8 8M12 4l-8 8"
|
|
1479
|
+
stroke="currentColor"
|
|
1480
|
+
stroke-width="2"
|
|
1481
|
+
stroke-linecap="round"
|
|
1482
|
+
/>
|
|
1483
|
+
</svg>
|
|
1484
|
+
</button>
|
|
1485
|
+
`
|
|
1486
|
+
: nothing}
|
|
1487
|
+
<button
|
|
1488
|
+
class="icon-button"
|
|
1489
|
+
type="button"
|
|
1490
|
+
aria-label="Open date range panel"
|
|
1491
|
+
aria-expanded=${this.open}
|
|
1492
|
+
?disabled=${this.disabled}
|
|
1493
|
+
@click=${this.handleIconClick}
|
|
1494
|
+
>
|
|
1495
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
|
|
1496
|
+
<path d="M4 2h1V1h2v1h2V1h2v1h1a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2zm8 3H4v7h8V5z"/>
|
|
1497
|
+
</svg>
|
|
1498
|
+
</button>
|
|
1153
1499
|
</span>
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1500
|
+
</div>
|
|
1501
|
+
${this.inputError
|
|
1502
|
+
? html`<div class="range-picker-support error" id="range-picker-desc">
|
|
1503
|
+
${this.inputError}
|
|
1504
|
+
</div>`
|
|
1505
|
+
: nothing}
|
|
1158
1506
|
`}
|
|
1159
1507
|
|
|
1160
1508
|
${isPopup && this.open
|
|
@@ -1163,6 +1511,7 @@ export class RelRangePicker extends LitElement {
|
|
|
1163
1511
|
|
|
1164
1512
|
<div
|
|
1165
1513
|
part="panel" class="popover ${this.open ? "open" : ""} ${isPopup ? "popup" : ""}"
|
|
1514
|
+
id="range-picker-panel"
|
|
1166
1515
|
popover="manual"
|
|
1167
1516
|
role="dialog"
|
|
1168
1517
|
aria-label="Select date range"
|
|
@@ -1171,18 +1520,20 @@ export class RelRangePicker extends LitElement {
|
|
|
1171
1520
|
|
|
1172
1521
|
<!-- ── Left: Quick select ── -->
|
|
1173
1522
|
<div part="list" class="presets">
|
|
1174
|
-
<div class="presets-grid" role="
|
|
1523
|
+
<div class="presets-grid" role="group" aria-label="Quick date selections">
|
|
1175
1524
|
${quickSelects.map(
|
|
1176
1525
|
(qs) => html`
|
|
1177
1526
|
<button
|
|
1178
1527
|
part="option" class="preset-item ${qs.fullWidth ? "full-width" : ""} ${this.draftActivePreset === qs.label ? "active" : ""}"
|
|
1179
1528
|
type="button"
|
|
1180
|
-
role="listitem"
|
|
1181
1529
|
?disabled=${this.disabled}
|
|
1182
1530
|
aria-pressed=${this.draftActivePreset === qs.label}
|
|
1183
1531
|
@click=${() => this.selectQuickSelect(qs)}
|
|
1184
1532
|
>
|
|
1185
1533
|
<span>${qs.label}</span>
|
|
1534
|
+
${this.draftActivePreset !== qs.label
|
|
1535
|
+
? html`<kbd class="preset-key" aria-hidden="true" title="Press ${qs.key} while the panel is open">${qs.key}</kbd>`
|
|
1536
|
+
: nothing}
|
|
1186
1537
|
${this.draftActivePreset === qs.label
|
|
1187
1538
|
? html`<svg width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
|
1188
1539
|
<circle cx="12" cy="12" r="12" fill="var(--rel-bg-brand)"/>
|
|
@@ -1234,54 +1585,60 @@ export class RelRangePicker extends LitElement {
|
|
|
1234
1585
|
</button>
|
|
1235
1586
|
</div>
|
|
1236
1587
|
|
|
1237
|
-
<div class="
|
|
1238
|
-
|
|
1239
|
-
(
|
|
1240
|
-
|
|
1241
|
-
${label}
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1588
|
+
<div class="calendar-table" role="grid" aria-label="Select date range">
|
|
1589
|
+
<div class="week-header" role="row">
|
|
1590
|
+
${weekdayLabels.map(
|
|
1591
|
+
(label) => html`
|
|
1592
|
+
<div class="week-day" role="columnheader" aria-label="${label}">
|
|
1593
|
+
${label}
|
|
1594
|
+
</div>
|
|
1595
|
+
`
|
|
1596
|
+
)}
|
|
1597
|
+
</div>
|
|
1246
1598
|
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1599
|
+
<div
|
|
1600
|
+
part="list" class="calendar-grid"
|
|
1601
|
+
@mouseleave=${() => { this.hoverDate = null; }}
|
|
1602
|
+
@keydown=${this.handleGridKeydown}
|
|
1603
|
+
>
|
|
1604
|
+
${weeks.map(
|
|
1605
|
+
(week) => html`
|
|
1606
|
+
<div class="calendar-week" role="row">
|
|
1607
|
+
${week.map((day) => {
|
|
1608
|
+
const iso = this.formatISODate(day.date);
|
|
1609
|
+
const cellClasses = [
|
|
1610
|
+
"day-cell",
|
|
1611
|
+
day.isInRange ? "in-range" : "",
|
|
1612
|
+
day.isDraftFrom ? "range-start" : "",
|
|
1613
|
+
day.isDraftTo ? "range-end" : "",
|
|
1614
|
+
day.isToday ? "today" : "",
|
|
1615
|
+
day.isDisabled ? "disabled" : "",
|
|
1616
|
+
!day.isCurrentMonth ? "other-month" : "",
|
|
1617
|
+
day.isWeekend ? "weekend" : "",
|
|
1618
|
+
]
|
|
1619
|
+
.filter(Boolean)
|
|
1620
|
+
.join(" ");
|
|
1621
|
+
|
|
1622
|
+
return html`
|
|
1623
|
+
<div
|
|
1624
|
+
class="${cellClasses}"
|
|
1625
|
+
role="gridcell"
|
|
1626
|
+
tabindex="${day.isCurrentMonth && iso === rovingIso ? 0 : -1}"
|
|
1627
|
+
data-iso="${iso}"
|
|
1628
|
+
aria-label="${this.formatDayLabel(day)}"
|
|
1629
|
+
aria-selected="${day.isDraftFrom || day.isDraftTo || day.isInRange}"
|
|
1630
|
+
aria-disabled="${day.isDisabled ? "true" : nothing}"
|
|
1631
|
+
@click=${() => this.handleDayClick(day)}
|
|
1632
|
+
@mouseenter=${() => this.handleDayHover(day)}
|
|
1633
|
+
>
|
|
1634
|
+
<span class="day-inner">${day.day}</span>
|
|
1635
|
+
</div>
|
|
1636
|
+
`;
|
|
1637
|
+
})}
|
|
1638
|
+
</div>
|
|
1639
|
+
`
|
|
1640
|
+
)}
|
|
1641
|
+
</div>
|
|
1285
1642
|
</div>
|
|
1286
1643
|
|
|
1287
1644
|
<div part="footer" class="calendar-footer">
|
|
@@ -1308,7 +1665,7 @@ export class RelRangePicker extends LitElement {
|
|
|
1308
1665
|
tone="default"
|
|
1309
1666
|
size="small"
|
|
1310
1667
|
border="corner-radius"
|
|
1311
|
-
?disabled=${isDraftEmpty}
|
|
1668
|
+
?disabled=${isDraftEmpty && !this.from && !this.to}
|
|
1312
1669
|
@click=${() => this.applySelection()}
|
|
1313
1670
|
>
|
|
1314
1671
|
Apply
|