@reliability-design/web 1.4.4 → 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-accordion/rel-accordion.ts +13 -1
- 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-button/rel-button.ts +18 -2
- package/dist/components/rel-calendar/rel-calendar.ts +86 -18
- package/dist/components/rel-carousel/rel-carousel.ts +45 -2
- package/dist/components/rel-checkbox/rel-checkbox.ts +4 -1
- package/dist/components/rel-chip/rel-chip.ts +59 -19
- 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 +675 -93
- package/dist/components/rel-drawer/rel-drawer.ts +28 -61
- package/dist/components/rel-menu/rel-menu.ts +130 -15
- package/dist/components/rel-menubar/rel-menubar.ts +221 -63
- package/dist/components/rel-nav-menu/rel-nav-menu.ts +39 -3
- package/dist/components/rel-otp/rel-otp.ts +14 -4
- package/dist/components/rel-pagination/rel-pagination.ts +13 -0
- package/dist/components/rel-popover/rel-popover.ts +64 -5
- 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 +581 -91
- package/dist/components/rel-resizable/rel-resizable-group.ts +20 -0
- package/dist/components/rel-segmented-button/rel-segmented-button.ts +55 -13
- package/dist/components/rel-select/rel-select.ts +203 -24
- package/dist/components/rel-sheet/rel-sheet.ts +184 -18
- package/dist/components/rel-sidebar/rel-sidebar.ts +50 -40
- 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 +47 -2
- package/dist/components/rel-switch/rel-switch.ts +3 -1
- package/dist/components/rel-tab/rel-tab.ts +32 -7
- package/dist/components/rel-textfield/rel-textfield.ts +263 -75
- package/dist/components/rel-time-picker/rel-time-picker.ts +749 -261
- 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 +33 -0
- package/dist/components/rel-topbar/rel-topbar-actions.ts +1 -1
- package/dist/components/rel-topbar/rel-topbar-brand.ts +1 -1
- package/dist/components/rel-topbar/rel-topbar-dropdown-item.ts +1 -1
- package/dist/components/rel-topbar/rel-topbar-dropdown.ts +1 -1
- package/dist/components/rel-topbar/rel-topbar-item.ts +318 -319
- package/dist/components/rel-topbar/rel-topbar-mobile-panel.ts +1 -1
- package/dist/components/rel-topbar/rel-topbar-profile.ts +224 -224
- package/dist/components/rel-topbar/rel-topbar-utilities.ts +1 -1
- package/dist/components/rel-topbar/rel-topbar.ts +440 -440
- package/dist/components/rel-upload/rel-upload.ts +25 -0
- package/dist/index.d.mts +461 -23
- package/dist/index.d.ts +461 -23
- package/dist/index.js +3906 -807
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +4143 -1055
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
|
@@ -1,6 +1,10 @@
|
|
|
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 { formatDatePattern } from "./date-format";
|
|
5
|
+
import { parseDateInput, isDayFirstPattern, isDayFirstLocale } from "./date-parse";
|
|
6
|
+
import { getDeepActiveElement, trapTabKey } from "../../utils/focus.js";
|
|
7
|
+
import { announce } from "../../utils/announce";
|
|
4
8
|
|
|
5
9
|
interface CalendarDay {
|
|
6
10
|
date: Date;
|
|
@@ -74,8 +78,20 @@ export class RelDatePicker extends LitElement {
|
|
|
74
78
|
@state() private focusedDate: Date | null = null;
|
|
75
79
|
@state() private yearDropdownOpen = false;
|
|
76
80
|
@state() private monthDropdownOpen = false;
|
|
81
|
+
/** Uncommitted typed text; null → the input shows the committed value. */
|
|
82
|
+
@state() private inputText: string | null = null;
|
|
83
|
+
/** Parse/range error message for the typed text (cleared on next input). */
|
|
84
|
+
@state() private inputError: string | null = null;
|
|
77
85
|
|
|
78
|
-
|
|
86
|
+
/**
|
|
87
|
+
* Whether the next popover open should move focus into the calendar grid.
|
|
88
|
+
* Icon-click / ArrowDown / external `open` do (previous behavior); opening
|
|
89
|
+
* by clicking into the text input must NOT, or typing would be impossible.
|
|
90
|
+
*/
|
|
91
|
+
private focusPanelOnOpen = true;
|
|
92
|
+
|
|
93
|
+
@query(".trigger-input") private triggerInput?: HTMLInputElement;
|
|
94
|
+
@query(".trigger-button") private triggerButton?: HTMLElement;
|
|
79
95
|
@query(".popover") private popoverElement?: HTMLElement;
|
|
80
96
|
@query(".calendar-grid") private calendarGrid?: HTMLElement;
|
|
81
97
|
|
|
@@ -152,9 +168,14 @@ export class RelDatePicker extends LitElement {
|
|
|
152
168
|
color: var(--rel-text-danger);
|
|
153
169
|
}
|
|
154
170
|
|
|
171
|
+
/* Persistent live region — out of flow while empty so it adds no gap. */
|
|
172
|
+
.date-picker-support.empty {
|
|
173
|
+
position: absolute;
|
|
174
|
+
}
|
|
175
|
+
|
|
155
176
|
.trigger-button {
|
|
156
177
|
width: 100%;
|
|
157
|
-
min-width: 200px;
|
|
178
|
+
min-width: min(200px, 100%);
|
|
158
179
|
display: flex;
|
|
159
180
|
align-items: center;
|
|
160
181
|
justify-content: space-between;
|
|
@@ -174,28 +195,48 @@ export class RelDatePicker extends LitElement {
|
|
|
174
195
|
letter-spacing: var(--rel-body-md-regular-letter-spacing);
|
|
175
196
|
}
|
|
176
197
|
|
|
177
|
-
|
|
198
|
+
:host(:not([disabled])) .trigger-button:hover {
|
|
178
199
|
border-color: var(--rel-border-strong);
|
|
179
200
|
}
|
|
180
201
|
|
|
181
|
-
.trigger-button:focus-
|
|
202
|
+
.trigger-button:focus-within {
|
|
182
203
|
outline: 2px solid var(--rel-border-focus);
|
|
183
204
|
outline-offset: 2px;
|
|
184
205
|
border-color: var(--rel-border-focus);
|
|
185
206
|
}
|
|
186
207
|
|
|
187
|
-
.trigger-button
|
|
208
|
+
:host([disabled]) .trigger-button {
|
|
188
209
|
cursor: not-allowed;
|
|
189
210
|
background: var(--rel-bg-surface-elevated);
|
|
190
211
|
}
|
|
191
212
|
|
|
213
|
+
/* The editable field itself — the wrapper carries the box styles. */
|
|
214
|
+
.trigger-input {
|
|
215
|
+
flex: 1;
|
|
216
|
+
min-width: 0;
|
|
217
|
+
border: none;
|
|
218
|
+
background: transparent;
|
|
219
|
+
padding: 0;
|
|
220
|
+
margin: 0;
|
|
221
|
+
outline: none;
|
|
222
|
+
color: var(--rel-text-default);
|
|
223
|
+
font: inherit;
|
|
224
|
+
letter-spacing: inherit;
|
|
225
|
+
cursor: inherit;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.trigger-input::placeholder {
|
|
229
|
+
color: var(--rel-text-placeholder);
|
|
230
|
+
opacity: 1;
|
|
231
|
+
}
|
|
232
|
+
|
|
192
233
|
/* Error state */
|
|
193
234
|
.trigger-button.error,
|
|
194
|
-
.trigger-button.error:hover
|
|
235
|
+
:host(:not([disabled])) .trigger-button.error:hover {
|
|
195
236
|
border-color: var(--rel-border-danger);
|
|
196
237
|
}
|
|
197
238
|
|
|
198
|
-
.trigger-button.error:focus-
|
|
239
|
+
.trigger-button.error:focus-within {
|
|
199
240
|
outline-color: var(--rel-border-danger);
|
|
200
241
|
border-color: var(--rel-border-danger);
|
|
201
242
|
}
|
|
@@ -219,15 +260,6 @@ export class RelDatePicker extends LitElement {
|
|
|
219
260
|
letter-spacing: var(--rel-body-lg-regular-letter-spacing);
|
|
220
261
|
}
|
|
221
262
|
|
|
222
|
-
.trigger-content {
|
|
223
|
-
flex: 1;
|
|
224
|
-
color: var(--rel-text-default);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
.trigger-content.placeholder {
|
|
228
|
-
color: var(--rel-text-placeholder);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
263
|
.trigger-icons {
|
|
232
264
|
display: flex;
|
|
233
265
|
align-items: center;
|
|
@@ -258,16 +290,34 @@ export class RelDatePicker extends LitElement {
|
|
|
258
290
|
}
|
|
259
291
|
|
|
260
292
|
.popover {
|
|
293
|
+
/* Reset native [popover] UA styles first (the panel is promoted to
|
|
294
|
+
the top layer so a drawer/sheet/modal ancestor — transformed,
|
|
295
|
+
overflow:hidden, or an equal-z-index sibling panel — can't clip it
|
|
296
|
+
or paint over it). While in the top layer, JS overrides the
|
|
297
|
+
anchored position below with inline fixed coordinates; the CSS
|
|
298
|
+
anchoring is the no-Popover-API fallback. */
|
|
299
|
+
margin: 0;
|
|
300
|
+
width: auto;
|
|
301
|
+
height: auto;
|
|
302
|
+
inset: auto;
|
|
303
|
+
color: inherit;
|
|
261
304
|
position: absolute;
|
|
262
305
|
top: calc(100% + 4px);
|
|
263
306
|
left: 0;
|
|
264
|
-
z-index:
|
|
307
|
+
z-index: 2147483647;
|
|
265
308
|
background: var(--rel-bg-surface);
|
|
266
309
|
border: 1px solid var(--rel-border-default);
|
|
267
310
|
border-radius: var(--rel-border-radius-sm);
|
|
268
311
|
box-shadow: var(--rel-shadows-shadow-300);
|
|
269
312
|
padding: var(--rel-spacing-md);
|
|
270
|
-
|
|
313
|
+
/* Never wider or taller than the viewport (small screens). border-box
|
|
314
|
+
so the caps include border+padding; 314px = the previous 280px
|
|
315
|
+
content-box min-width, keeping desktop rendering unchanged. */
|
|
316
|
+
box-sizing: border-box;
|
|
317
|
+
min-width: min(314px, calc(100vw - 16px));
|
|
318
|
+
max-width: calc(100vw - 16px);
|
|
319
|
+
max-height: min(600px, calc(100vh - 16px));
|
|
320
|
+
overflow-y: auto;
|
|
271
321
|
display: none;
|
|
272
322
|
}
|
|
273
323
|
|
|
@@ -275,6 +325,56 @@ export class RelDatePicker extends LitElement {
|
|
|
275
325
|
display: block;
|
|
276
326
|
}
|
|
277
327
|
|
|
328
|
+
/* Typed-shortcut hints (T → Today, …); each chip applies its value. */
|
|
329
|
+
.shortcut-row {
|
|
330
|
+
display: flex;
|
|
331
|
+
flex-wrap: wrap;
|
|
332
|
+
gap: var(--rel-spacing-2xs);
|
|
333
|
+
padding-bottom: var(--rel-spacing-xs);
|
|
334
|
+
margin-bottom: var(--rel-spacing-xs);
|
|
335
|
+
border-bottom: 1px solid var(--rel-border-subtle);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
.shortcut-chip {
|
|
339
|
+
display: inline-flex;
|
|
340
|
+
align-items: center;
|
|
341
|
+
gap: var(--rel-spacing-2xs);
|
|
342
|
+
border: none;
|
|
343
|
+
background: transparent;
|
|
344
|
+
border-radius: var(--rel-border-radius-xs);
|
|
345
|
+
padding: var(--rel-spacing-3xs) var(--rel-spacing-2xs);
|
|
346
|
+
cursor: pointer;
|
|
347
|
+
color: var(--rel-text-default);
|
|
348
|
+
transition: background 0.15s ease;
|
|
349
|
+
font-family: var(--rel-body-xs-regular-font-family);
|
|
350
|
+
font-weight: var(--rel-body-xs-regular-font-weight);
|
|
351
|
+
font-size: var(--rel-body-xs-regular-font-size);
|
|
352
|
+
line-height: var(--rel-body-xs-regular-line-height);
|
|
353
|
+
letter-spacing: var(--rel-body-xs-regular-letter-spacing);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
.shortcut-chip:hover {
|
|
357
|
+
background: var(--rel-bg-neutral-elevated-1);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.shortcut-chip:focus-visible {
|
|
361
|
+
outline: 2px solid var(--rel-border-focus);
|
|
362
|
+
outline-offset: -2px;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
.shortcut-chip kbd {
|
|
366
|
+
border: 1px solid var(--rel-border-default);
|
|
367
|
+
border-bottom-width: 2px;
|
|
368
|
+
border-radius: var(--rel-border-radius-xs);
|
|
369
|
+
background: var(--rel-bg-surface-elevated);
|
|
370
|
+
padding: 0 var(--rel-spacing-3xs);
|
|
371
|
+
min-width: 18px;
|
|
372
|
+
text-align: center;
|
|
373
|
+
font-family: inherit;
|
|
374
|
+
font-size: inherit;
|
|
375
|
+
color: var(--rel-text-label);
|
|
376
|
+
}
|
|
377
|
+
|
|
278
378
|
.calendar-header {
|
|
279
379
|
display: flex;
|
|
280
380
|
align-items: center;
|
|
@@ -357,6 +457,11 @@ export class RelDatePicker extends LitElement {
|
|
|
357
457
|
}
|
|
358
458
|
|
|
359
459
|
.dropdown-item {
|
|
460
|
+
display: block;
|
|
461
|
+
width: 100%;
|
|
462
|
+
border: none;
|
|
463
|
+
background: transparent;
|
|
464
|
+
text-align: left;
|
|
360
465
|
padding: var(--rel-spacing-xs) var(--rel-spacing-sm);
|
|
361
466
|
cursor: pointer;
|
|
362
467
|
color: var(--rel-text-default);
|
|
@@ -368,6 +473,11 @@ export class RelDatePicker extends LitElement {
|
|
|
368
473
|
letter-spacing: var(--rel-body-sm-regular-letter-spacing);
|
|
369
474
|
}
|
|
370
475
|
|
|
476
|
+
.dropdown-item:focus-visible {
|
|
477
|
+
outline: 2px solid var(--rel-border-focus);
|
|
478
|
+
outline-offset: -2px;
|
|
479
|
+
}
|
|
480
|
+
|
|
371
481
|
.dropdown-item:hover {
|
|
372
482
|
background: var(--rel-bg-surface-elevated);
|
|
373
483
|
}
|
|
@@ -403,6 +513,13 @@ export class RelDatePicker extends LitElement {
|
|
|
403
513
|
gap: var(--rel-spacing-3xs);
|
|
404
514
|
}
|
|
405
515
|
|
|
516
|
+
.calendar-week {
|
|
517
|
+
grid-column: 1 / -1;
|
|
518
|
+
display: grid;
|
|
519
|
+
grid-template-columns: repeat(7, 1fr);
|
|
520
|
+
gap: var(--rel-spacing-3xs);
|
|
521
|
+
}
|
|
522
|
+
|
|
406
523
|
.day-button {
|
|
407
524
|
aspect-ratio: 1;
|
|
408
525
|
border: none;
|
|
@@ -485,6 +602,9 @@ export class RelDatePicker extends LitElement {
|
|
|
485
602
|
super.disconnectedCallback();
|
|
486
603
|
document.removeEventListener("click", this.handleClickOutside);
|
|
487
604
|
document.removeEventListener("keydown", this.handleEscapeKey);
|
|
605
|
+
window.removeEventListener("resize", this.onReposition);
|
|
606
|
+
window.removeEventListener("scroll", this.onReposition, true);
|
|
607
|
+
this.hideTopLayer();
|
|
488
608
|
}
|
|
489
609
|
|
|
490
610
|
private handleClickOutside = (e: MouseEvent) => {
|
|
@@ -492,13 +612,21 @@ export class RelDatePicker extends LitElement {
|
|
|
492
612
|
|
|
493
613
|
const path = e.composedPath();
|
|
494
614
|
if (!path.includes(this)) {
|
|
495
|
-
|
|
615
|
+
// Don't refocus the field — the user clicked somewhere else on purpose.
|
|
616
|
+
this.closeCalendarPopover(false);
|
|
496
617
|
}
|
|
497
618
|
};
|
|
498
619
|
|
|
499
620
|
private handleEscapeKey = (e: KeyboardEvent) => {
|
|
500
|
-
if (
|
|
621
|
+
if (!this.open) return;
|
|
622
|
+
if (e.key === "Escape") {
|
|
501
623
|
this.closeCalendarPopover();
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
// The popover announces itself as a dialog — keep Tab inside it.
|
|
627
|
+
if (e.key === "Tab") {
|
|
628
|
+
const popover = this.renderRoot.querySelector<HTMLElement>(".popover");
|
|
629
|
+
if (popover) trapTabKey(e, popover);
|
|
502
630
|
}
|
|
503
631
|
};
|
|
504
632
|
|
|
@@ -511,15 +639,116 @@ export class RelDatePicker extends LitElement {
|
|
|
511
639
|
}
|
|
512
640
|
}
|
|
513
641
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
);
|
|
520
|
-
|
|
521
|
-
|
|
642
|
+
// `open` is a public reflected property (consumers may set it directly,
|
|
643
|
+
// not only via the trigger), so top-layer promotion syncs here rather
|
|
644
|
+
// than inside toggle/close.
|
|
645
|
+
if (changedProperties.has("open")) {
|
|
646
|
+
if (!this.open) {
|
|
647
|
+
window.removeEventListener("resize", this.onReposition);
|
|
648
|
+
window.removeEventListener("scroll", this.onReposition, true);
|
|
649
|
+
this.hideTopLayer();
|
|
650
|
+
this.focusPanelOnOpen = true;
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
this.showTopLayer();
|
|
654
|
+
window.addEventListener("resize", this.onReposition);
|
|
655
|
+
window.addEventListener("scroll", this.onReposition, true);
|
|
656
|
+
// Focus the roving cell (selected day, else today, else first
|
|
657
|
+
// selectable) when opening — not blindly the first of the month —
|
|
658
|
+
// unless the open came from clicking into the text input (typing must
|
|
659
|
+
// keep working).
|
|
660
|
+
const focusPanel = this.focusPanelOnOpen;
|
|
661
|
+
this.focusPanelOnOpen = true;
|
|
662
|
+
if (focusPanel) {
|
|
663
|
+
setTimeout(() => this.focusPanelSelection(), 100);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/* A drawer/sheet/modal ancestor breaks the anchored panel two ways: a
|
|
669
|
+
* transform makes it the containing block for position:fixed descendants
|
|
670
|
+
* (and overflow:hidden clips absolute ones), and an equal-z-index panel
|
|
671
|
+
* later in the DOM paints over the calendar. Promoting the panel to the
|
|
672
|
+
* browser top layer sidesteps both — same fix as rel-popover /
|
|
673
|
+
* rel-autocomplete / rel-select / rel-range-picker. `manual` keeps
|
|
674
|
+
* outside-click and Escape handling under this component's own control.
|
|
675
|
+
* While in the top layer the CSS anchoring is meaningless, so the panel
|
|
676
|
+
* gets inline fixed coordinates from updatePopoverPosition(). */
|
|
677
|
+
|
|
678
|
+
private showTopLayer() {
|
|
679
|
+
const el = this.popoverElement;
|
|
680
|
+
if (el && typeof el.showPopover === "function") {
|
|
681
|
+
try {
|
|
682
|
+
if (!el.matches(":popover-open")) el.showPopover();
|
|
683
|
+
} catch {
|
|
684
|
+
/* older engines / detached element — CSS fallback applies */
|
|
685
|
+
}
|
|
522
686
|
}
|
|
687
|
+
requestAnimationFrame(() => this.updatePopoverPosition());
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
private hideTopLayer() {
|
|
691
|
+
const el = this.popoverElement;
|
|
692
|
+
if (!el) return;
|
|
693
|
+
if (typeof el.hidePopover === "function") {
|
|
694
|
+
try {
|
|
695
|
+
if (el.matches(":popover-open")) el.hidePopover();
|
|
696
|
+
} catch {
|
|
697
|
+
/* already hidden or unsupported — nothing to do */
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
el.style.position = "";
|
|
701
|
+
el.style.top = "";
|
|
702
|
+
el.style.left = "";
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
private onReposition = () => {
|
|
706
|
+
if (this.open) this.updatePopoverPosition();
|
|
707
|
+
};
|
|
708
|
+
|
|
709
|
+
private updatePopoverPosition = () => {
|
|
710
|
+
const panel = this.popoverElement;
|
|
711
|
+
const anchor = this.triggerButton;
|
|
712
|
+
if (!panel || !anchor || !this.open) return;
|
|
713
|
+
|
|
714
|
+
const t = anchor.getBoundingClientRect();
|
|
715
|
+
const c = panel.getBoundingClientRect();
|
|
716
|
+
|
|
717
|
+
// Below the trigger; flip above when there's no room, clamp to the
|
|
718
|
+
// viewport with an 8px margin on every edge.
|
|
719
|
+
let top = t.bottom + 4;
|
|
720
|
+
if (top + c.height > window.innerHeight - 8) {
|
|
721
|
+
top = Math.max(8, t.top - c.height - 4);
|
|
722
|
+
}
|
|
723
|
+
let left = t.left;
|
|
724
|
+
if (left + c.width > window.innerWidth - 8) {
|
|
725
|
+
left = window.innerWidth - c.width - 8;
|
|
726
|
+
}
|
|
727
|
+
left = Math.max(8, left);
|
|
728
|
+
|
|
729
|
+
// Without top-layer support a transformed ancestor may be the containing
|
|
730
|
+
// block for position:fixed, shifting the panel away from the viewport
|
|
731
|
+
// coordinates computed above. Apply, re-measure, and cancel the offset.
|
|
732
|
+
panel.style.position = "fixed";
|
|
733
|
+
panel.style.top = `${top}px`;
|
|
734
|
+
panel.style.left = `${left}px`;
|
|
735
|
+
const applied = panel.getBoundingClientRect();
|
|
736
|
+
top -= applied.top - top;
|
|
737
|
+
left -= applied.left - left;
|
|
738
|
+
panel.style.top = `${top}px`;
|
|
739
|
+
panel.style.left = `${left}px`;
|
|
740
|
+
};
|
|
741
|
+
|
|
742
|
+
/** Focus the roving day cell (selected day, else today, else first). */
|
|
743
|
+
private focusPanelSelection() {
|
|
744
|
+
const target =
|
|
745
|
+
this.calendarGrid?.querySelector<HTMLButtonElement>(
|
|
746
|
+
'.day-button[tabindex="0"]:not(:disabled)'
|
|
747
|
+
) ??
|
|
748
|
+
this.calendarGrid?.querySelector<HTMLButtonElement>(
|
|
749
|
+
".day-button:not(:disabled)"
|
|
750
|
+
);
|
|
751
|
+
target?.focus();
|
|
523
752
|
}
|
|
524
753
|
|
|
525
754
|
private parseISODate(iso: string): Date | null {
|
|
@@ -665,6 +894,26 @@ export class RelDatePicker extends LitElement {
|
|
|
665
894
|
return formatter.format(new Date(this.viewYear, this.viewMonth, 1));
|
|
666
895
|
}
|
|
667
896
|
|
|
897
|
+
private dayLabelFormatter: Intl.DateTimeFormat | null = null;
|
|
898
|
+
private dayLabelLocale = "";
|
|
899
|
+
|
|
900
|
+
private formatDayLabel(date: Date): string {
|
|
901
|
+
if (!this.dayLabelFormatter || this.dayLabelLocale !== this.locale) {
|
|
902
|
+
this.dayLabelLocale = this.locale;
|
|
903
|
+
this.dayLabelFormatter = new Intl.DateTimeFormat(this.locale, {
|
|
904
|
+
weekday: "long",
|
|
905
|
+
month: "long",
|
|
906
|
+
day: "numeric",
|
|
907
|
+
year: "numeric",
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
return this.dayLabelFormatter.format(date);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
private announceMonth() {
|
|
914
|
+
announce(`${this.getMonthName()} ${this.viewYear}`);
|
|
915
|
+
}
|
|
916
|
+
|
|
668
917
|
private getMonthsList(): { value: number; label: string }[] {
|
|
669
918
|
const months = [];
|
|
670
919
|
for (let i = 0; i < 12; i++) {
|
|
@@ -709,11 +958,11 @@ export class RelDatePicker extends LitElement {
|
|
|
709
958
|
);
|
|
710
959
|
}
|
|
711
960
|
|
|
712
|
-
private closeCalendarPopover() {
|
|
961
|
+
private closeCalendarPopover(refocus = true) {
|
|
713
962
|
this.open = false;
|
|
714
963
|
this.monthDropdownOpen = false;
|
|
715
964
|
this.yearDropdownOpen = false;
|
|
716
|
-
this.
|
|
965
|
+
if (refocus) this.triggerInput?.focus();
|
|
717
966
|
this.dispatchEvent(
|
|
718
967
|
new CustomEvent("rel-open-change", {
|
|
719
968
|
detail: { open: false },
|
|
@@ -730,6 +979,7 @@ export class RelDatePicker extends LitElement {
|
|
|
730
979
|
} else {
|
|
731
980
|
this.viewMonth--;
|
|
732
981
|
}
|
|
982
|
+
this.announceMonth();
|
|
733
983
|
}
|
|
734
984
|
|
|
735
985
|
private nextMonth() {
|
|
@@ -739,33 +989,40 @@ export class RelDatePicker extends LitElement {
|
|
|
739
989
|
} else {
|
|
740
990
|
this.viewMonth++;
|
|
741
991
|
}
|
|
992
|
+
this.announceMonth();
|
|
742
993
|
}
|
|
743
994
|
|
|
744
995
|
private selectDay(day: CalendarDay) {
|
|
745
996
|
if (day.isDisabled || this.disabled || this.readonly) return;
|
|
997
|
+
this.commitDate(day.date);
|
|
998
|
+
if (this.closeOnSelect) {
|
|
999
|
+
this.closeCalendarPopover();
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
746
1002
|
|
|
747
|
-
|
|
1003
|
+
/** Commit a date: set value, reset typed text/error, fire rel-change. */
|
|
1004
|
+
private commitDate(date: Date) {
|
|
1005
|
+
const newValue = this.formatISODate(date);
|
|
748
1006
|
this.value = newValue;
|
|
749
|
-
|
|
1007
|
+
this.inputText = null;
|
|
1008
|
+
this.inputError = null;
|
|
750
1009
|
this.dispatchEvent(
|
|
751
1010
|
new CustomEvent("rel-change", {
|
|
752
1011
|
detail: {
|
|
753
1012
|
value: newValue,
|
|
754
|
-
date: new Date(
|
|
1013
|
+
date: new Date(date),
|
|
755
1014
|
},
|
|
756
1015
|
bubbles: true,
|
|
757
1016
|
composed: true,
|
|
758
1017
|
})
|
|
759
1018
|
);
|
|
760
|
-
|
|
761
|
-
if (this.closeOnSelect) {
|
|
762
|
-
this.closeCalendarPopover();
|
|
763
|
-
}
|
|
764
1019
|
}
|
|
765
1020
|
|
|
766
1021
|
private clearValue(e: Event) {
|
|
767
1022
|
e.stopPropagation();
|
|
768
1023
|
this.value = null;
|
|
1024
|
+
this.inputText = null;
|
|
1025
|
+
this.inputError = null;
|
|
769
1026
|
|
|
770
1027
|
this.dispatchEvent(
|
|
771
1028
|
new CustomEvent("rel-change", {
|
|
@@ -779,15 +1036,148 @@ export class RelDatePicker extends LitElement {
|
|
|
779
1036
|
);
|
|
780
1037
|
}
|
|
781
1038
|
|
|
782
|
-
|
|
783
|
-
|
|
1039
|
+
// ── Typed keyboard entry ──
|
|
1040
|
+
|
|
1041
|
+
/** Ambiguous "a/b" day/month order: display format wins, then locale. */
|
|
1042
|
+
private get dayFirst(): boolean {
|
|
1043
|
+
if (this.displayFormat) {
|
|
1044
|
+
const fromPattern = isDayFirstPattern(this.displayFormat);
|
|
1045
|
+
if (fromPattern !== null) return fromPattern;
|
|
1046
|
+
}
|
|
1047
|
+
return isDayFirstLocale(this.locale);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
/**
|
|
1051
|
+
* Parse and commit the typed text (Enter / blur). Invalid or out-of-range
|
|
1052
|
+
* text keeps the field in an error state with the text preserved for
|
|
1053
|
+
* correction; the committed value is untouched (Escape reverts the text).
|
|
1054
|
+
*/
|
|
1055
|
+
private commitTypedValue() {
|
|
1056
|
+
if (this.inputText === null) return; // nothing typed since last commit
|
|
1057
|
+
const text = this.inputText.trim();
|
|
1058
|
+
|
|
1059
|
+
if (!text) {
|
|
1060
|
+
// Field cleared by keyboard — behaves like the clear button.
|
|
1061
|
+
this.inputText = null;
|
|
1062
|
+
this.inputError = null;
|
|
1063
|
+
if (this.value !== null) {
|
|
1064
|
+
this.value = null;
|
|
1065
|
+
this.dispatchEvent(
|
|
1066
|
+
new CustomEvent("rel-change", {
|
|
1067
|
+
detail: { value: null, date: null },
|
|
1068
|
+
bubbles: true,
|
|
1069
|
+
composed: true,
|
|
1070
|
+
})
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1073
|
+
if (this.open) this.closeCalendarPopover();
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
const parsed = parseDateInput(text, {
|
|
1078
|
+
referenceDate: new Date(),
|
|
1079
|
+
dayFirst: this.dayFirst,
|
|
1080
|
+
locale: this.locale,
|
|
1081
|
+
});
|
|
1082
|
+
if (!parsed) {
|
|
1083
|
+
this.inputError = "Invalid date";
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
// Midnight-anchored like the calendar grid, so min/max compares match.
|
|
1087
|
+
const date = new Date(parsed.getFullYear(), parsed.getMonth(), parsed.getDate());
|
|
1088
|
+
if (this.isDateDisabledInternal(date)) {
|
|
1089
|
+
this.inputError = "Date not available";
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
this.commitDate(date);
|
|
1094
|
+
if (this.open) this.closeCalendarPopover();
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
/** Today ± offset days, committed directly (T shortcuts / popover chips). */
|
|
1098
|
+
private applyDayOffsetShortcut(offsetDays: number) {
|
|
1099
|
+
const now = new Date();
|
|
1100
|
+
const date = new Date(now.getFullYear(), now.getMonth(), now.getDate() + offsetDays);
|
|
1101
|
+
if (this.isDateDisabledInternal(date)) {
|
|
1102
|
+
this.inputError = "Date not available";
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
this.commitDate(date);
|
|
1106
|
+
if (this.open) this.closeCalendarPopover();
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
private handleInputInput(e: Event) {
|
|
1110
|
+
this.inputText = (e.target as HTMLInputElement).value;
|
|
1111
|
+
if (this.inputError) this.inputError = null;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
private handleInputKeydown(e: KeyboardEvent) {
|
|
1115
|
+
if (e.key === "Enter") {
|
|
1116
|
+
e.preventDefault();
|
|
1117
|
+
if (this.inputText === null) {
|
|
1118
|
+
// Nothing typed — Enter opens the popover (previous trigger behavior).
|
|
1119
|
+
if (!this.open) this.toggleCalendarPopover();
|
|
1120
|
+
} else {
|
|
1121
|
+
this.commitTypedValue();
|
|
1122
|
+
}
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
if (e.key === "ArrowDown") {
|
|
784
1126
|
e.preventDefault();
|
|
785
1127
|
if (!this.open) {
|
|
786
1128
|
this.toggleCalendarPopover();
|
|
1129
|
+
} else {
|
|
1130
|
+
this.focusPanelSelection();
|
|
787
1131
|
}
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
if (e.key === "Escape" && this.inputText !== null) {
|
|
1135
|
+
// Revert the typed text; the document listener closes the popover.
|
|
1136
|
+
this.inputText = null;
|
|
1137
|
+
this.inputError = null;
|
|
788
1138
|
}
|
|
789
1139
|
}
|
|
790
1140
|
|
|
1141
|
+
/** Clicking anywhere on the field opens the popover, keeping focus typed-in. */
|
|
1142
|
+
private handleFieldClick() {
|
|
1143
|
+
if (this.open || this.disabled || this.readonly) return;
|
|
1144
|
+
this.focusPanelOnOpen = false;
|
|
1145
|
+
this.toggleCalendarPopover();
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
/** The calendar icon toggles the popover with the previous focus behavior. */
|
|
1149
|
+
private handleIconClick(e: Event) {
|
|
1150
|
+
e.stopPropagation();
|
|
1151
|
+
if (this.open) {
|
|
1152
|
+
this.closeCalendarPopover();
|
|
1153
|
+
} else {
|
|
1154
|
+
this.toggleCalendarPopover();
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
/** Commit typed text when focus leaves the component entirely. */
|
|
1159
|
+
private handleInputBlur = (e: FocusEvent) => {
|
|
1160
|
+
this.dispatchEvent(new CustomEvent("rel-blur", { bubbles: true, composed: true }));
|
|
1161
|
+
const related = e.relatedTarget as Node | null;
|
|
1162
|
+
if (related && (this.renderRoot.contains(related) || this.contains(related))) {
|
|
1163
|
+
return; // focus moved into the panel/icons — their own handlers commit
|
|
1164
|
+
}
|
|
1165
|
+
this.commitTypedValue();
|
|
1166
|
+
};
|
|
1167
|
+
|
|
1168
|
+
/** T inside the panel (grid/header focus) applies Today directly. */
|
|
1169
|
+
private handlePanelKeydown = (e: KeyboardEvent) => {
|
|
1170
|
+
if (
|
|
1171
|
+
(e.key === "t" || e.key === "T") &&
|
|
1172
|
+
!e.ctrlKey &&
|
|
1173
|
+
!e.metaKey &&
|
|
1174
|
+
!e.altKey
|
|
1175
|
+
) {
|
|
1176
|
+
e.preventDefault();
|
|
1177
|
+
this.applyDayOffsetShortcut(0);
|
|
1178
|
+
}
|
|
1179
|
+
};
|
|
1180
|
+
|
|
791
1181
|
private handleDayKeydown(e: KeyboardEvent, day: CalendarDay, index: number) {
|
|
792
1182
|
const days = this.getCalendarDays();
|
|
793
1183
|
let targetIndex = index;
|
|
@@ -821,6 +1211,7 @@ export class RelDatePicker extends LitElement {
|
|
|
821
1211
|
e.preventDefault();
|
|
822
1212
|
if (e.shiftKey) {
|
|
823
1213
|
this.viewYear--;
|
|
1214
|
+
this.announceMonth();
|
|
824
1215
|
} else {
|
|
825
1216
|
this.previousMonth();
|
|
826
1217
|
}
|
|
@@ -829,6 +1220,7 @@ export class RelDatePicker extends LitElement {
|
|
|
829
1220
|
e.preventDefault();
|
|
830
1221
|
if (e.shiftKey) {
|
|
831
1222
|
this.viewYear++;
|
|
1223
|
+
this.announceMonth();
|
|
832
1224
|
} else {
|
|
833
1225
|
this.nextMonth();
|
|
834
1226
|
}
|
|
@@ -865,24 +1257,127 @@ export class RelDatePicker extends LitElement {
|
|
|
865
1257
|
e.stopPropagation();
|
|
866
1258
|
this.monthDropdownOpen = !this.monthDropdownOpen;
|
|
867
1259
|
this.yearDropdownOpen = false;
|
|
1260
|
+
if (this.monthDropdownOpen) this.focusOpenDropdownSelection();
|
|
868
1261
|
}
|
|
869
1262
|
|
|
870
1263
|
private toggleYearDropdown(e: Event) {
|
|
871
1264
|
e.stopPropagation();
|
|
872
1265
|
this.yearDropdownOpen = !this.yearDropdownOpen;
|
|
873
1266
|
this.monthDropdownOpen = false;
|
|
1267
|
+
if (this.yearDropdownOpen) this.focusOpenDropdownSelection();
|
|
874
1268
|
}
|
|
875
1269
|
|
|
1270
|
+
private handleDropdownTriggerKeydown(e: KeyboardEvent, kind: "month" | "year") {
|
|
1271
|
+
if (e.key !== "ArrowDown") return;
|
|
1272
|
+
e.preventDefault();
|
|
1273
|
+
e.stopPropagation();
|
|
1274
|
+
this.monthDropdownOpen = kind === "month";
|
|
1275
|
+
this.yearDropdownOpen = kind === "year";
|
|
1276
|
+
this.focusOpenDropdownSelection();
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
private async focusOpenDropdownSelection() {
|
|
1280
|
+
await this.updateComplete;
|
|
1281
|
+
const menu = this.renderRoot.querySelector<HTMLElement>(".dropdown-menu.open");
|
|
1282
|
+
if (!menu) return;
|
|
1283
|
+
const target =
|
|
1284
|
+
menu.querySelector<HTMLElement>(".dropdown-item.selected") ??
|
|
1285
|
+
menu.querySelector<HTMLElement>(".dropdown-item");
|
|
1286
|
+
target?.focus();
|
|
1287
|
+
target?.scrollIntoView({ block: "nearest" });
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
private handleDropdownKeydown = (e: KeyboardEvent) => {
|
|
1291
|
+
const menu = e.currentTarget as HTMLElement;
|
|
1292
|
+
const items = Array.from(
|
|
1293
|
+
menu.querySelectorAll<HTMLElement>(".dropdown-item")
|
|
1294
|
+
);
|
|
1295
|
+
if (items.length === 0) return;
|
|
1296
|
+
|
|
1297
|
+
if (e.key === "Escape") {
|
|
1298
|
+
// Close only the dropdown, not the whole calendar popover behind it.
|
|
1299
|
+
e.preventDefault();
|
|
1300
|
+
e.stopPropagation();
|
|
1301
|
+
const trigger = menu
|
|
1302
|
+
.closest(".dropdown-container")
|
|
1303
|
+
?.querySelector<HTMLElement>(".dropdown-trigger");
|
|
1304
|
+
this.monthDropdownOpen = false;
|
|
1305
|
+
this.yearDropdownOpen = false;
|
|
1306
|
+
trigger?.focus();
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
if (e.key === "Tab") {
|
|
1311
|
+
this.monthDropdownOpen = false;
|
|
1312
|
+
this.yearDropdownOpen = false;
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
const idx = items.indexOf(getDeepActiveElement() as HTMLElement);
|
|
1317
|
+
let target: number;
|
|
1318
|
+
switch (e.key) {
|
|
1319
|
+
case "ArrowDown":
|
|
1320
|
+
target = Math.min(items.length - 1, idx + 1);
|
|
1321
|
+
break;
|
|
1322
|
+
case "ArrowUp":
|
|
1323
|
+
target = Math.max(0, idx - 1);
|
|
1324
|
+
break;
|
|
1325
|
+
case "Home":
|
|
1326
|
+
target = 0;
|
|
1327
|
+
break;
|
|
1328
|
+
case "End":
|
|
1329
|
+
target = items.length - 1;
|
|
1330
|
+
break;
|
|
1331
|
+
default:
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
e.preventDefault();
|
|
1335
|
+
e.stopPropagation();
|
|
1336
|
+
items[target]?.focus();
|
|
1337
|
+
items[target]?.scrollIntoView({ block: "nearest" });
|
|
1338
|
+
};
|
|
1339
|
+
|
|
876
1340
|
private selectMonth(month: number, e: Event) {
|
|
877
1341
|
e.stopPropagation();
|
|
1342
|
+
const trigger = (e.target as HTMLElement)
|
|
1343
|
+
.closest(".dropdown-container")
|
|
1344
|
+
?.querySelector<HTMLElement>(".dropdown-trigger");
|
|
878
1345
|
this.viewMonth = month;
|
|
879
1346
|
this.monthDropdownOpen = false;
|
|
1347
|
+
this.announceMonth();
|
|
1348
|
+
// The focused item just went display:none — keep keyboard users anchored.
|
|
1349
|
+
this.updateComplete.then(() => trigger?.focus());
|
|
880
1350
|
}
|
|
881
1351
|
|
|
882
1352
|
private selectYear(year: number, e: Event) {
|
|
883
1353
|
e.stopPropagation();
|
|
1354
|
+
const trigger = (e.target as HTMLElement)
|
|
1355
|
+
.closest(".dropdown-container")
|
|
1356
|
+
?.querySelector<HTMLElement>(".dropdown-trigger");
|
|
884
1357
|
this.viewYear = year;
|
|
885
1358
|
this.yearDropdownOpen = false;
|
|
1359
|
+
this.announceMonth();
|
|
1360
|
+
this.updateComplete.then(() => trigger?.focus());
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
/**
|
|
1364
|
+
* Index of the single grid cell carrying tabindex="0". The old expression
|
|
1365
|
+
* (`isSelected || (index === 0 && !value)`) left the grid with ZERO tab
|
|
1366
|
+
* stops once the user arrowed around without a selection.
|
|
1367
|
+
*/
|
|
1368
|
+
private getRovingIndex(days: CalendarDay[]): number {
|
|
1369
|
+
const candidates = [
|
|
1370
|
+
this.focusedDate
|
|
1371
|
+
? days.findIndex(
|
|
1372
|
+
(d) => d.isCurrentMonth && this.isSameDay(d.date, this.focusedDate!)
|
|
1373
|
+
)
|
|
1374
|
+
: -1,
|
|
1375
|
+
days.findIndex((d) => d.isSelected),
|
|
1376
|
+
days.findIndex((d) => d.isCurrentMonth && d.isToday),
|
|
1377
|
+
days.findIndex((d) => d.isCurrentMonth && !d.isDisabled),
|
|
1378
|
+
];
|
|
1379
|
+
const found = candidates.find((i) => i >= 0);
|
|
1380
|
+
return found ?? 0;
|
|
886
1381
|
}
|
|
887
1382
|
|
|
888
1383
|
render() {
|
|
@@ -890,10 +1385,18 @@ export class RelDatePicker extends LitElement {
|
|
|
890
1385
|
const weekdayLabels = this.getWeekdayLabels();
|
|
891
1386
|
const months = this.getMonthsList();
|
|
892
1387
|
const years = this.getYearsList();
|
|
1388
|
+
const rovingIndex = this.getRovingIndex(days);
|
|
1389
|
+
const weeks: CalendarDay[][] = [];
|
|
1390
|
+
for (let i = 0; i < days.length; i += 7) {
|
|
1391
|
+
weeks.push(days.slice(i, i + 7));
|
|
1392
|
+
}
|
|
893
1393
|
|
|
894
|
-
const isError = this.error;
|
|
895
|
-
const belowText = isError
|
|
1394
|
+
const isError = this.error || this.inputError !== null;
|
|
1395
|
+
const belowText = isError
|
|
1396
|
+
? this.errorMessage ?? this.inputError ?? this.helperText
|
|
1397
|
+
: this.helperText;
|
|
896
1398
|
const describedById = belowText ? "date-picker-desc" : undefined;
|
|
1399
|
+
const displayText = this.value ? this.formatDisplayDate(this.value) : "";
|
|
897
1400
|
const wrapperClasses = [
|
|
898
1401
|
"date-picker-wrapper",
|
|
899
1402
|
this.size,
|
|
@@ -917,24 +1420,35 @@ export class RelDatePicker extends LitElement {
|
|
|
917
1420
|
? html`<input type="hidden" name=${this.name} .value=${this.value ?? ""} />`
|
|
918
1421
|
: nothing}
|
|
919
1422
|
|
|
920
|
-
<
|
|
1423
|
+
<div
|
|
1424
|
+
part="trigger"
|
|
921
1425
|
class="trigger-button ${isError ? "error" : ""}"
|
|
922
|
-
|
|
923
|
-
?disabled=${this.disabled}
|
|
924
|
-
@click=${this.toggleCalendarPopover}
|
|
925
|
-
@keydown=${this.handleTriggerKeydown}
|
|
926
|
-
aria-label=${this.label || "Select date"}
|
|
927
|
-
aria-haspopup="dialog"
|
|
928
|
-
aria-expanded=${this.open}
|
|
929
|
-
aria-required=${this.required}
|
|
930
|
-
aria-invalid=${isError}
|
|
931
|
-
aria-readonly=${this.readonly}
|
|
932
|
-
aria-describedby=${describedById ?? nothing}
|
|
933
|
-
aria-labelledby=${this.label ? "date-picker-label" : nothing}
|
|
1426
|
+
@click=${this.handleFieldClick}
|
|
934
1427
|
>
|
|
935
|
-
<
|
|
936
|
-
|
|
937
|
-
|
|
1428
|
+
<input
|
|
1429
|
+
part="value"
|
|
1430
|
+
class="trigger-input"
|
|
1431
|
+
type="text"
|
|
1432
|
+
.value=${live(this.inputText ?? displayText)}
|
|
1433
|
+
placeholder=${this.placeholder}
|
|
1434
|
+
?disabled=${this.disabled}
|
|
1435
|
+
?readonly=${this.readonly}
|
|
1436
|
+
autocomplete="off"
|
|
1437
|
+
spellcheck="false"
|
|
1438
|
+
enterkeyhint="done"
|
|
1439
|
+
@input=${this.handleInputInput}
|
|
1440
|
+
@keydown=${this.handleInputKeydown}
|
|
1441
|
+
@blur=${this.handleInputBlur}
|
|
1442
|
+
role="combobox"
|
|
1443
|
+
aria-label=${this.label || "Select date"}
|
|
1444
|
+
aria-haspopup="dialog"
|
|
1445
|
+
aria-expanded=${this.open}
|
|
1446
|
+
aria-controls="date-picker-panel"
|
|
1447
|
+
aria-required=${this.required}
|
|
1448
|
+
aria-invalid=${isError}
|
|
1449
|
+
aria-describedby=${describedById ?? nothing}
|
|
1450
|
+
aria-labelledby=${this.label ? "date-picker-label" : nothing}
|
|
1451
|
+
/>
|
|
938
1452
|
<span class="trigger-icons">
|
|
939
1453
|
${this.clearable && this.value
|
|
940
1454
|
? html`
|
|
@@ -955,15 +1469,57 @@ export class RelDatePicker extends LitElement {
|
|
|
955
1469
|
</button>
|
|
956
1470
|
`
|
|
957
1471
|
: nothing}
|
|
958
|
-
<
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
1472
|
+
<button
|
|
1473
|
+
class="icon-button"
|
|
1474
|
+
type="button"
|
|
1475
|
+
aria-label="Open calendar"
|
|
1476
|
+
aria-expanded=${this.open}
|
|
1477
|
+
?disabled=${this.disabled}
|
|
1478
|
+
@click=${this.handleIconClick}
|
|
1479
|
+
>
|
|
1480
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
|
1481
|
+
<path
|
|
1482
|
+
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"
|
|
1483
|
+
/>
|
|
1484
|
+
</svg>
|
|
1485
|
+
</button>
|
|
963
1486
|
</span>
|
|
964
|
-
</
|
|
1487
|
+
</div>
|
|
965
1488
|
|
|
966
|
-
<div
|
|
1489
|
+
<div
|
|
1490
|
+
id="date-picker-panel"
|
|
1491
|
+
class="popover ${this.open ? "open" : ""}"
|
|
1492
|
+
popover="manual"
|
|
1493
|
+
role="dialog"
|
|
1494
|
+
aria-label="Choose date"
|
|
1495
|
+
@keydown=${this.handlePanelKeydown}
|
|
1496
|
+
>
|
|
1497
|
+
<div part="shortcuts" class="shortcut-row">
|
|
1498
|
+
<button
|
|
1499
|
+
class="shortcut-chip"
|
|
1500
|
+
type="button"
|
|
1501
|
+
title="Type T in the field for today"
|
|
1502
|
+
@click=${() => this.applyDayOffsetShortcut(0)}
|
|
1503
|
+
>
|
|
1504
|
+
<kbd aria-hidden="true">T</kbd><span>Today</span>
|
|
1505
|
+
</button>
|
|
1506
|
+
<button
|
|
1507
|
+
class="shortcut-chip"
|
|
1508
|
+
type="button"
|
|
1509
|
+
title="Type T+1 in the field for tomorrow"
|
|
1510
|
+
@click=${() => this.applyDayOffsetShortcut(1)}
|
|
1511
|
+
>
|
|
1512
|
+
<kbd aria-hidden="true">T+1</kbd><span>Tomorrow</span>
|
|
1513
|
+
</button>
|
|
1514
|
+
<button
|
|
1515
|
+
class="shortcut-chip"
|
|
1516
|
+
type="button"
|
|
1517
|
+
title="Type T-1 in the field for yesterday"
|
|
1518
|
+
@click=${() => this.applyDayOffsetShortcut(-1)}
|
|
1519
|
+
>
|
|
1520
|
+
<kbd aria-hidden="true">T-1</kbd><span>Yesterday</span>
|
|
1521
|
+
</button>
|
|
1522
|
+
</div>
|
|
967
1523
|
<div class="calendar-header">
|
|
968
1524
|
<button
|
|
969
1525
|
class="nav-button"
|
|
@@ -988,22 +1544,31 @@ export class RelDatePicker extends LitElement {
|
|
|
988
1544
|
class="dropdown-trigger"
|
|
989
1545
|
type="button"
|
|
990
1546
|
@click=${this.toggleMonthDropdown}
|
|
1547
|
+
@keydown=${(e: KeyboardEvent) => this.handleDropdownTriggerKeydown(e, "month")}
|
|
991
1548
|
aria-label="Select month"
|
|
1549
|
+
aria-haspopup="listbox"
|
|
992
1550
|
aria-expanded=${this.monthDropdownOpen}
|
|
993
1551
|
>
|
|
994
1552
|
${this.getMonthName()}
|
|
995
1553
|
</button>
|
|
996
|
-
<div
|
|
1554
|
+
<div
|
|
1555
|
+
class="dropdown-menu ${this.monthDropdownOpen ? "open" : ""}"
|
|
1556
|
+
role="listbox"
|
|
1557
|
+
aria-label="Month"
|
|
1558
|
+
@keydown=${this.handleDropdownKeydown}
|
|
1559
|
+
>
|
|
997
1560
|
${months.map(
|
|
998
1561
|
(m) => html`
|
|
999
|
-
<
|
|
1562
|
+
<button
|
|
1000
1563
|
class="dropdown-item ${m.value === this.viewMonth ? "selected" : ""}"
|
|
1564
|
+
type="button"
|
|
1565
|
+
tabindex="-1"
|
|
1001
1566
|
@click=${(e: Event) => this.selectMonth(m.value, e)}
|
|
1002
1567
|
role="option"
|
|
1003
1568
|
aria-selected=${m.value === this.viewMonth}
|
|
1004
1569
|
>
|
|
1005
1570
|
${m.label}
|
|
1006
|
-
</
|
|
1571
|
+
</button>
|
|
1007
1572
|
`
|
|
1008
1573
|
)}
|
|
1009
1574
|
</div>
|
|
@@ -1014,22 +1579,31 @@ export class RelDatePicker extends LitElement {
|
|
|
1014
1579
|
class="dropdown-trigger"
|
|
1015
1580
|
type="button"
|
|
1016
1581
|
@click=${this.toggleYearDropdown}
|
|
1582
|
+
@keydown=${(e: KeyboardEvent) => this.handleDropdownTriggerKeydown(e, "year")}
|
|
1017
1583
|
aria-label="Select year"
|
|
1584
|
+
aria-haspopup="listbox"
|
|
1018
1585
|
aria-expanded=${this.yearDropdownOpen}
|
|
1019
1586
|
>
|
|
1020
1587
|
${this.viewYear}
|
|
1021
1588
|
</button>
|
|
1022
|
-
<div
|
|
1589
|
+
<div
|
|
1590
|
+
class="dropdown-menu ${this.yearDropdownOpen ? "open" : ""}"
|
|
1591
|
+
role="listbox"
|
|
1592
|
+
aria-label="Year"
|
|
1593
|
+
@keydown=${this.handleDropdownKeydown}
|
|
1594
|
+
>
|
|
1023
1595
|
${years.map(
|
|
1024
1596
|
(y) => html`
|
|
1025
|
-
<
|
|
1597
|
+
<button
|
|
1026
1598
|
class="dropdown-item ${y === this.viewYear ? "selected" : ""}"
|
|
1599
|
+
type="button"
|
|
1600
|
+
tabindex="-1"
|
|
1027
1601
|
@click=${(e: Event) => this.selectYear(y, e)}
|
|
1028
1602
|
role="option"
|
|
1029
1603
|
aria-selected=${y === this.viewYear}
|
|
1030
1604
|
>
|
|
1031
1605
|
${y}
|
|
1032
|
-
</
|
|
1606
|
+
</button>
|
|
1033
1607
|
`
|
|
1034
1608
|
)}
|
|
1035
1609
|
</div>
|
|
@@ -1060,34 +1634,42 @@ export class RelDatePicker extends LitElement {
|
|
|
1060
1634
|
)}
|
|
1061
1635
|
</div>
|
|
1062
1636
|
|
|
1063
|
-
<div class="calendar-grid" role="grid">
|
|
1064
|
-
${
|
|
1065
|
-
(
|
|
1066
|
-
<
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1637
|
+
<div class="calendar-grid" role="grid" aria-label="${this.getMonthName()} ${this.viewYear}">
|
|
1638
|
+
${weeks.map(
|
|
1639
|
+
(week, weekIndex) => html`
|
|
1640
|
+
<div class="calendar-week" role="row">
|
|
1641
|
+
${week.map((day, dayIndex) => {
|
|
1642
|
+
const index = weekIndex * 7 + dayIndex;
|
|
1643
|
+
return html`
|
|
1644
|
+
<button
|
|
1645
|
+
class="day-button ${day.isSelected ? "selected" : ""} ${day.isToday
|
|
1646
|
+
? "today"
|
|
1647
|
+
: ""} ${!day.isCurrentMonth ? "other-month" : ""}"
|
|
1648
|
+
role="gridcell"
|
|
1649
|
+
type="button"
|
|
1650
|
+
aria-label="${this.formatDayLabel(day.date)}"
|
|
1651
|
+
aria-selected="${day.isSelected}"
|
|
1652
|
+
aria-current=${day.isToday ? "date" : nothing}
|
|
1653
|
+
?disabled=${day.isDisabled}
|
|
1654
|
+
@click=${() => this.selectDay(day)}
|
|
1655
|
+
@keydown=${(e: KeyboardEvent) => this.handleDayKeydown(e, day, index)}
|
|
1656
|
+
tabindex="${index === rovingIndex ? 0 : -1}"
|
|
1657
|
+
>
|
|
1658
|
+
${day.day}
|
|
1659
|
+
</button>
|
|
1660
|
+
`;
|
|
1661
|
+
})}
|
|
1662
|
+
</div>
|
|
1081
1663
|
`
|
|
1082
1664
|
)}
|
|
1083
1665
|
</div>
|
|
1084
1666
|
</div>
|
|
1085
1667
|
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1668
|
+
<div
|
|
1669
|
+
class="date-picker-support ${isError ? "error" : ""} ${belowText ? "" : "empty"}"
|
|
1670
|
+
id="date-picker-desc"
|
|
1671
|
+
aria-live="polite"
|
|
1672
|
+
>${belowText ?? nothing}</div>
|
|
1091
1673
|
</div>
|
|
1092
1674
|
`;
|
|
1093
1675
|
}
|