@reliability-design/web 1.4.3 → 1.4.5
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-autocomplete/README.md +1 -0
- package/dist/components/rel-autocomplete/rel-autocomplete.ts +56 -1
- package/dist/components/rel-button/rel-button.ts +18 -2
- package/dist/components/rel-calendar/rel-calendar.ts +23 -1
- package/dist/components/rel-carousel/rel-carousel.ts +20 -0
- package/dist/components/rel-checkbox/README.md +69 -22
- package/dist/components/rel-checkbox/rel-checkbox.ts +119 -9
- package/dist/components/rel-chip/rel-chip.ts +29 -0
- package/dist/components/rel-date-picker/rel-date-picker.ts +156 -13
- package/dist/components/rel-drawer/rel-drawer.ts +22 -58
- package/dist/components/rel-menu/rel-menu.ts +124 -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 +3 -3
- package/dist/components/rel-pagination/README.md +14 -0
- package/dist/components/rel-pagination/rel-pagination.ts +30 -14
- package/dist/components/rel-popover/rel-popover.ts +31 -3
- package/dist/components/rel-range-picker/rel-range-picker.ts +251 -3
- package/dist/components/rel-resizable/rel-resizable-group.ts +20 -0
- package/dist/components/rel-segmented-button/rel-segmented-button.ts +50 -12
- package/dist/components/rel-select/README.md +1 -0
- package/dist/components/rel-select/rel-select.ts +1385 -1170
- package/dist/components/rel-sheet/rel-sheet.ts +22 -1
- package/dist/components/rel-sidebar/rel-sidebar.ts +43 -39
- package/dist/components/rel-stepper/rel-stepper.ts +44 -1
- package/dist/components/rel-tab/rel-tab.ts +32 -7
- package/dist/components/rel-time-picker/rel-time-picker.ts +338 -206
- package/dist/components/rel-tooltip/rel-tooltip.ts +30 -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/UPLOAD-DESIGN-CHANGES.md +134 -0
- package/dist/components/rel-upload/rel-upload-actions.ts +163 -0
- package/dist/components/rel-upload/rel-upload.ts +943 -0
- package/dist/index.d.mts +491 -18
- package/dist/index.d.ts +491 -18
- package/dist/index.js +2748 -410
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2953 -627
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { LitElement, html, css } from "lit";
|
|
2
2
|
import { customElement, property, state, query } from "lit/decorators.js";
|
|
3
|
+
import { getDeepActiveElement, getFocusableElements } from "../../utils/focus.js";
|
|
3
4
|
|
|
4
5
|
type SideType = "top" | "right" | "bottom" | "left";
|
|
5
6
|
type AlignType = "start" | "center" | "end";
|
|
@@ -86,13 +87,15 @@ export class RelPopover extends LitElement {
|
|
|
86
87
|
super.connectedCallback();
|
|
87
88
|
this.internalOpen = this.isControlled() ? !!this.open : this.defaultOpen;
|
|
88
89
|
|
|
89
|
-
// ONE interaction entry point — capture phase
|
|
90
|
+
// ONE interaction entry point per input method — capture phase
|
|
90
91
|
this.addEventListener("pointerdown", this.onHostPointerDown, true);
|
|
92
|
+
this.addEventListener("keydown", this.onHostKeyDown, true);
|
|
91
93
|
}
|
|
92
94
|
|
|
93
95
|
disconnectedCallback() {
|
|
94
96
|
super.disconnectedCallback();
|
|
95
97
|
this.removeEventListener("pointerdown", this.onHostPointerDown, true);
|
|
98
|
+
this.removeEventListener("keydown", this.onHostKeyDown, true);
|
|
96
99
|
this.removeGlobalListeners();
|
|
97
100
|
this.hideNativePopover();
|
|
98
101
|
}
|
|
@@ -159,13 +162,25 @@ export class RelPopover extends LitElement {
|
|
|
159
162
|
this.setOpen(!this.activeOpen);
|
|
160
163
|
};
|
|
161
164
|
|
|
165
|
+
private onHostKeyDown = (e: KeyboardEvent) => {
|
|
166
|
+
if (this.disabled) return;
|
|
167
|
+
if (e.key !== "Enter" && e.key !== " ") return;
|
|
168
|
+
if (!this.triggerEl || !e.composedPath().includes(this.triggerEl)) return;
|
|
169
|
+
|
|
170
|
+
// Keyboard twin of onHostPointerDown. preventDefault also suppresses the
|
|
171
|
+
// native synthetic click of a <button> trigger, which would toggle twice.
|
|
172
|
+
e.preventDefault();
|
|
173
|
+
e.stopPropagation();
|
|
174
|
+
|
|
175
|
+
this.setOpen(!this.activeOpen);
|
|
176
|
+
};
|
|
177
|
+
|
|
162
178
|
/* =======================
|
|
163
179
|
* OPEN / CLOSE
|
|
164
180
|
* ======================= */
|
|
165
181
|
|
|
166
182
|
private onOpen() {
|
|
167
|
-
this.returnFocusEl =
|
|
168
|
-
(document.activeElement as HTMLElement) ?? this.triggerEl;
|
|
183
|
+
this.returnFocusEl = getDeepActiveElement() ?? this.triggerEl;
|
|
169
184
|
|
|
170
185
|
this.addGlobalListeners();
|
|
171
186
|
this.showNativePopover();
|
|
@@ -386,6 +401,19 @@ export class RelPopover extends LitElement {
|
|
|
386
401
|
this.triggerEl.setAttribute("aria-haspopup", "dialog");
|
|
387
402
|
this.triggerEl.setAttribute("aria-expanded", String(this.activeOpen));
|
|
388
403
|
this.triggerEl.setAttribute("aria-controls", this.uniqueId);
|
|
404
|
+
|
|
405
|
+
// A bare <div>/<span> trigger must be Tab-reachable. Skip anything that
|
|
406
|
+
// is (or contains) natively focusable so buttons don't get a second stop.
|
|
407
|
+
if (
|
|
408
|
+
this.triggerEl.tabIndex < 0 &&
|
|
409
|
+
!this.triggerEl.hasAttribute("tabindex") &&
|
|
410
|
+
getFocusableElements(this.triggerEl).length === 0
|
|
411
|
+
) {
|
|
412
|
+
this.triggerEl.setAttribute("tabindex", "0");
|
|
413
|
+
if (!this.triggerEl.hasAttribute("role")) {
|
|
414
|
+
this.triggerEl.setAttribute("role", "button");
|
|
415
|
+
}
|
|
416
|
+
}
|
|
389
417
|
}
|
|
390
418
|
|
|
391
419
|
/* =======================
|
|
@@ -50,8 +50,12 @@ export class RelRangePicker extends LitElement {
|
|
|
50
50
|
@state() private viewMonth: number = new Date().getMonth();
|
|
51
51
|
@state() private isSelectingRange: boolean = false;
|
|
52
52
|
@state() private hoverDate: string | null = null;
|
|
53
|
+
/** ISO date of the cell that owns the grid's single Tab stop (roving tabindex). */
|
|
54
|
+
@state() private focusedDate: string | null = null;
|
|
53
55
|
|
|
54
56
|
@query(".trigger-button") private triggerButton?: HTMLButtonElement;
|
|
57
|
+
@query(".popover") private popoverEl?: HTMLDivElement;
|
|
58
|
+
@query(".backdrop") private backdropEl?: HTMLDivElement;
|
|
55
59
|
|
|
56
60
|
static styles = css`
|
|
57
61
|
:host {
|
|
@@ -113,6 +117,17 @@ export class RelRangePicker extends LitElement {
|
|
|
113
117
|
|
|
114
118
|
/* ── Popover shell ── */
|
|
115
119
|
.popover {
|
|
120
|
+
/* Reset native [popover] UA styles first (the shell is promoted to
|
|
121
|
+
the top layer so transformed / overflow:hidden ancestors — e.g. a
|
|
122
|
+
centered rel-modal panel — can't hijack or clip it). While in the
|
|
123
|
+
top layer, JS overrides the anchored position below with inline
|
|
124
|
+
fixed coordinates; the CSS anchoring is the no-Popover-API
|
|
125
|
+
fallback. */
|
|
126
|
+
margin: 0;
|
|
127
|
+
padding: 0;
|
|
128
|
+
width: auto;
|
|
129
|
+
height: auto;
|
|
130
|
+
color: inherit;
|
|
116
131
|
position: absolute;
|
|
117
132
|
top: calc(100% + 4px);
|
|
118
133
|
left: 0;
|
|
@@ -137,6 +152,13 @@ export class RelRangePicker extends LitElement {
|
|
|
137
152
|
.backdrop {
|
|
138
153
|
position: fixed;
|
|
139
154
|
inset: 0;
|
|
155
|
+
/* Reset native [popover] UA styles — the backdrop is promoted to the
|
|
156
|
+
top layer right before the panel so the panel stacks above it. */
|
|
157
|
+
margin: 0;
|
|
158
|
+
padding: 0;
|
|
159
|
+
width: auto;
|
|
160
|
+
height: auto;
|
|
161
|
+
border: none;
|
|
140
162
|
z-index: 2147483646;
|
|
141
163
|
background: rgba(0,0,0,0.4);
|
|
142
164
|
}
|
|
@@ -332,6 +354,12 @@ export class RelRangePicker extends LitElement {
|
|
|
332
354
|
cursor: pointer;
|
|
333
355
|
}
|
|
334
356
|
|
|
357
|
+
.day-cell:focus-visible {
|
|
358
|
+
outline: 2px solid var(--rel-border-focus);
|
|
359
|
+
outline-offset: -2px;
|
|
360
|
+
z-index: 2;
|
|
361
|
+
}
|
|
362
|
+
|
|
335
363
|
.day-cell.other-month {
|
|
336
364
|
opacity: 0.35;
|
|
337
365
|
pointer-events: none;
|
|
@@ -448,17 +476,111 @@ export class RelRangePicker extends LitElement {
|
|
|
448
476
|
super.disconnectedCallback();
|
|
449
477
|
document.removeEventListener("click", this.handleClickOutside);
|
|
450
478
|
document.removeEventListener("keydown", this.handleGlobalKeydown);
|
|
479
|
+
window.removeEventListener("resize", this.onReposition);
|
|
480
|
+
window.removeEventListener("scroll", this.onReposition, true);
|
|
481
|
+
this.hideTopLayer();
|
|
451
482
|
}
|
|
452
483
|
|
|
453
|
-
updated() {
|
|
484
|
+
updated(changed: Map<string, unknown>) {
|
|
454
485
|
this.style.width = this.width;
|
|
455
486
|
this.style.minWidth = this.minWidth;
|
|
456
487
|
this.style.maxWidth = this.maxWidth;
|
|
457
488
|
this.style.height = this.height;
|
|
458
489
|
this.style.minHeight = this.minHeight;
|
|
459
490
|
this.style.maxHeight = this.maxHeight;
|
|
491
|
+
|
|
492
|
+
// `open` is a public reflected property (consumers may set it directly,
|
|
493
|
+
// not only via the trigger), so top-layer promotion syncs here rather
|
|
494
|
+
// than inside openPopover/cancelPopover.
|
|
495
|
+
if (changed.has("open")) {
|
|
496
|
+
if (this.open) {
|
|
497
|
+
this.showTopLayer();
|
|
498
|
+
window.addEventListener("resize", this.onReposition);
|
|
499
|
+
window.addEventListener("scroll", this.onReposition, true);
|
|
500
|
+
} else {
|
|
501
|
+
window.removeEventListener("resize", this.onReposition);
|
|
502
|
+
window.removeEventListener("scroll", this.onReposition, true);
|
|
503
|
+
this.hideTopLayer();
|
|
504
|
+
}
|
|
505
|
+
}
|
|
460
506
|
}
|
|
461
507
|
|
|
508
|
+
/* A transformed ancestor (e.g. rel-modal's centered panel with
|
|
509
|
+
* translate(-50%,-50%)) becomes the containing block for position:fixed
|
|
510
|
+
* descendants, and its overflow:hidden clips position:absolute ones —
|
|
511
|
+
* either way the picker shell breaks inside a modal. Promoting shell
|
|
512
|
+
* (and popup-mode backdrop) to the browser top layer sidesteps both —
|
|
513
|
+
* same fix as rel-popover / rel-autocomplete / rel-select. `manual`
|
|
514
|
+
* keeps outside-click and Escape handling under this component's own
|
|
515
|
+
* control. While in the top layer the CSS anchoring (absolute, below
|
|
516
|
+
* the trigger) is meaningless, so popover mode gets inline fixed
|
|
517
|
+
* coordinates from updatePopoverPosition(). */
|
|
518
|
+
|
|
519
|
+
private showTopLayer() {
|
|
520
|
+
// Backdrop first — later-shown popovers stack above earlier ones.
|
|
521
|
+
for (const el of [this.mode === "popup" ? this.backdropEl : undefined, this.popoverEl]) {
|
|
522
|
+
if (!el || typeof el.showPopover !== "function") continue;
|
|
523
|
+
try {
|
|
524
|
+
if (!el.matches(":popover-open")) el.showPopover();
|
|
525
|
+
} catch {
|
|
526
|
+
/* older engines / detached element — CSS fallback applies */
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
if (this.mode === "popover") {
|
|
530
|
+
requestAnimationFrame(() => this.updatePopoverPosition());
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
private hideTopLayer() {
|
|
535
|
+
for (const el of [this.popoverEl, this.backdropEl]) {
|
|
536
|
+
if (!el || typeof el.hidePopover !== "function") continue;
|
|
537
|
+
try {
|
|
538
|
+
if (el.matches(":popover-open")) el.hidePopover();
|
|
539
|
+
} catch {
|
|
540
|
+
/* already hidden or unsupported — nothing to do */
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
if (this.popoverEl) {
|
|
544
|
+
this.popoverEl.style.position = "";
|
|
545
|
+
this.popoverEl.style.top = "";
|
|
546
|
+
this.popoverEl.style.left = "";
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
private onReposition = () => {
|
|
551
|
+
if (this.open && this.mode === "popover") this.updatePopoverPosition();
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
private updatePopoverPosition = () => {
|
|
555
|
+
const panel = this.popoverEl;
|
|
556
|
+
if (!panel || !this.open || this.mode !== "popover") return;
|
|
557
|
+
|
|
558
|
+
// Host rect anchors both trigger variants (default button and icon).
|
|
559
|
+
const t = this.getBoundingClientRect();
|
|
560
|
+
const c = panel.getBoundingClientRect();
|
|
561
|
+
|
|
562
|
+
let top = t.bottom + 4;
|
|
563
|
+
if (top + c.height > window.innerHeight - 8) {
|
|
564
|
+
top = Math.max(8, t.top - c.height - 4);
|
|
565
|
+
}
|
|
566
|
+
let left = t.left;
|
|
567
|
+
if (left + c.width > window.innerWidth - 8) {
|
|
568
|
+
left = Math.max(8, window.innerWidth - c.width - 8);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Without top-layer support a transformed ancestor may be the containing
|
|
572
|
+
// block for position:fixed, shifting the shell away from the viewport
|
|
573
|
+
// coordinates computed above. Apply, re-measure, and cancel the offset.
|
|
574
|
+
panel.style.position = "fixed";
|
|
575
|
+
panel.style.top = `${top}px`;
|
|
576
|
+
panel.style.left = `${left}px`;
|
|
577
|
+
const applied = panel.getBoundingClientRect();
|
|
578
|
+
top -= applied.top - top;
|
|
579
|
+
left -= applied.left - left;
|
|
580
|
+
panel.style.top = `${top}px`;
|
|
581
|
+
panel.style.left = `${left}px`;
|
|
582
|
+
};
|
|
583
|
+
|
|
462
584
|
// ── Open / close ──
|
|
463
585
|
|
|
464
586
|
private handleClickOutside = (e: MouseEvent) => {
|
|
@@ -513,6 +635,7 @@ export class RelRangePicker extends LitElement {
|
|
|
513
635
|
this.draftActivePreset = null;
|
|
514
636
|
this.isSelectingRange = false;
|
|
515
637
|
this.hoverDate = null;
|
|
638
|
+
this.focusedDate = null;
|
|
516
639
|
|
|
517
640
|
if (this.draftFrom) {
|
|
518
641
|
const date = this.parseISODate(this.draftFrom);
|
|
@@ -868,11 +991,130 @@ export class RelRangePicker extends LitElement {
|
|
|
868
991
|
this.hoverDate = this.formatISODate(day.date);
|
|
869
992
|
}
|
|
870
993
|
|
|
994
|
+
// ── Grid keyboard navigation (APG grid pattern) ──
|
|
995
|
+
|
|
996
|
+
/**
|
|
997
|
+
* The cell that carries tabindex="0". Prefers the last roved date, then the
|
|
998
|
+
* draft start, then today, then the first selectable day — always exactly
|
|
999
|
+
* one Tab stop, even after arrowing away with nothing selected.
|
|
1000
|
+
*/
|
|
1001
|
+
private getRovingIso(days: RangeDay[]): string {
|
|
1002
|
+
const inView = days.filter((d) => d.isCurrentMonth);
|
|
1003
|
+
if (inView.length === 0) return "";
|
|
1004
|
+
|
|
1005
|
+
const candidates = [
|
|
1006
|
+
this.focusedDate,
|
|
1007
|
+
this.draftFrom,
|
|
1008
|
+
this.formatISODate(new Date()),
|
|
1009
|
+
];
|
|
1010
|
+
for (const iso of candidates) {
|
|
1011
|
+
if (iso && inView.some((d) => this.formatISODate(d.date) === iso)) {
|
|
1012
|
+
return iso;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
const firstEnabled = inView.find((d) => !d.isDisabled) ?? inView[0];
|
|
1016
|
+
return this.formatISODate(firstEnabled.date);
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
private handleGridKeydown = (e: KeyboardEvent) => {
|
|
1020
|
+
const cell = (e.target as HTMLElement).closest?.(".day-cell") as
|
|
1021
|
+
| HTMLElement
|
|
1022
|
+
| null;
|
|
1023
|
+
const iso = cell?.dataset.iso;
|
|
1024
|
+
if (!iso) return;
|
|
1025
|
+
const date = this.parseISODate(iso);
|
|
1026
|
+
if (!date) return;
|
|
1027
|
+
|
|
1028
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
1029
|
+
e.preventDefault();
|
|
1030
|
+
const isDisabled = this.isDateDisabled(date);
|
|
1031
|
+
if (!isDisabled) {
|
|
1032
|
+
this.handleDayClick({
|
|
1033
|
+
date,
|
|
1034
|
+
day: date.getDate(),
|
|
1035
|
+
isCurrentMonth: true,
|
|
1036
|
+
isToday: false,
|
|
1037
|
+
isDraftFrom: false,
|
|
1038
|
+
isDraftTo: false,
|
|
1039
|
+
isInRange: false,
|
|
1040
|
+
isDisabled,
|
|
1041
|
+
isWeekend: false,
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
const next = new Date(date);
|
|
1048
|
+
const dow = next.getDay();
|
|
1049
|
+
switch (e.key) {
|
|
1050
|
+
case "ArrowLeft":
|
|
1051
|
+
next.setDate(next.getDate() - 1);
|
|
1052
|
+
break;
|
|
1053
|
+
case "ArrowRight":
|
|
1054
|
+
next.setDate(next.getDate() + 1);
|
|
1055
|
+
break;
|
|
1056
|
+
case "ArrowUp":
|
|
1057
|
+
next.setDate(next.getDate() - 7);
|
|
1058
|
+
break;
|
|
1059
|
+
case "ArrowDown":
|
|
1060
|
+
next.setDate(next.getDate() + 7);
|
|
1061
|
+
break;
|
|
1062
|
+
case "Home":
|
|
1063
|
+
next.setDate(
|
|
1064
|
+
next.getDate() -
|
|
1065
|
+
(this.weekStart === "mon" ? (dow === 0 ? 6 : dow - 1) : dow)
|
|
1066
|
+
);
|
|
1067
|
+
break;
|
|
1068
|
+
case "End":
|
|
1069
|
+
next.setDate(
|
|
1070
|
+
next.getDate() +
|
|
1071
|
+
(this.weekStart === "mon" ? (dow === 0 ? 0 : 7 - dow) : 6 - dow)
|
|
1072
|
+
);
|
|
1073
|
+
break;
|
|
1074
|
+
case "PageUp":
|
|
1075
|
+
if (e.shiftKey) next.setFullYear(next.getFullYear() - 1);
|
|
1076
|
+
else next.setMonth(next.getMonth() - 1);
|
|
1077
|
+
break;
|
|
1078
|
+
case "PageDown":
|
|
1079
|
+
if (e.shiftKey) next.setFullYear(next.getFullYear() + 1);
|
|
1080
|
+
else next.setMonth(next.getMonth() + 1);
|
|
1081
|
+
break;
|
|
1082
|
+
default:
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
e.preventDefault();
|
|
1087
|
+
this.moveGridFocusTo(next);
|
|
1088
|
+
};
|
|
1089
|
+
|
|
1090
|
+
private async moveGridFocusTo(date: Date) {
|
|
1091
|
+
this.focusedDate = this.formatISODate(date);
|
|
1092
|
+
|
|
1093
|
+
if (
|
|
1094
|
+
date.getMonth() !== this.viewMonth ||
|
|
1095
|
+
date.getFullYear() !== this.viewYear
|
|
1096
|
+
) {
|
|
1097
|
+
this.viewMonth = date.getMonth();
|
|
1098
|
+
this.viewYear = date.getFullYear();
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
// Keyboard twin of handleDayHover: keep the range preview following focus.
|
|
1102
|
+
if (this.isSelectingRange && !this.isDateDisabled(date)) {
|
|
1103
|
+
this.hoverDate = this.focusedDate;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
await this.updateComplete;
|
|
1107
|
+
this.renderRoot
|
|
1108
|
+
.querySelector<HTMLElement>(`.day-cell[data-iso="${this.focusedDate}"]`)
|
|
1109
|
+
?.focus();
|
|
1110
|
+
}
|
|
1111
|
+
|
|
871
1112
|
// ── Render ──
|
|
872
1113
|
|
|
873
1114
|
render() {
|
|
874
1115
|
const quickSelects = this.getQuickSelects();
|
|
875
1116
|
const days = this.getCalendarDays();
|
|
1117
|
+
const rovingIso = this.getRovingIso(days);
|
|
876
1118
|
const weekdayLabels = this.getWeekdayLabels();
|
|
877
1119
|
|
|
878
1120
|
const draftFromDisplay = this.draftFrom ? this.formatDisplayDate(this.draftFrom) : "";
|
|
@@ -916,11 +1158,12 @@ export class RelRangePicker extends LitElement {
|
|
|
916
1158
|
`}
|
|
917
1159
|
|
|
918
1160
|
${isPopup && this.open
|
|
919
|
-
? html`<div part="overlay" class="backdrop" @click=${this.cancelPopover}></div>`
|
|
1161
|
+
? html`<div part="overlay" class="backdrop" popover="manual" @click=${this.cancelPopover}></div>`
|
|
920
1162
|
: nothing}
|
|
921
1163
|
|
|
922
1164
|
<div
|
|
923
1165
|
part="panel" class="popover ${this.open ? "open" : ""} ${isPopup ? "popup" : ""}"
|
|
1166
|
+
popover="manual"
|
|
924
1167
|
role="dialog"
|
|
925
1168
|
aria-label="Select date range"
|
|
926
1169
|
>
|
|
@@ -1006,8 +1249,10 @@ export class RelRangePicker extends LitElement {
|
|
|
1006
1249
|
role="grid"
|
|
1007
1250
|
aria-label="Select date range"
|
|
1008
1251
|
@mouseleave=${() => { this.hoverDate = null; }}
|
|
1252
|
+
@keydown=${this.handleGridKeydown}
|
|
1009
1253
|
>
|
|
1010
1254
|
${days.map((day) => {
|
|
1255
|
+
const iso = this.formatISODate(day.date);
|
|
1011
1256
|
const cellClasses = [
|
|
1012
1257
|
"day-cell",
|
|
1013
1258
|
day.isInRange ? "in-range" : "",
|
|
@@ -1025,8 +1270,11 @@ export class RelRangePicker extends LitElement {
|
|
|
1025
1270
|
<div
|
|
1026
1271
|
class="${cellClasses}"
|
|
1027
1272
|
role="gridcell"
|
|
1028
|
-
|
|
1273
|
+
tabindex="${day.isCurrentMonth && iso === rovingIso ? 0 : -1}"
|
|
1274
|
+
data-iso="${iso}"
|
|
1275
|
+
aria-label="${iso}"
|
|
1029
1276
|
aria-selected="${day.isDraftFrom || day.isDraftTo || day.isInRange}"
|
|
1277
|
+
aria-disabled="${day.isDisabled ? "true" : nothing}"
|
|
1030
1278
|
@click=${() => this.handleDayClick(day)}
|
|
1031
1279
|
@mouseenter=${() => this.handleDayHover(day)}
|
|
1032
1280
|
>
|
|
@@ -94,6 +94,16 @@ export class RelResizableGroup extends LitElement {
|
|
|
94
94
|
// Calculate initial sizes
|
|
95
95
|
const sizes = this.calculateInitialSizes();
|
|
96
96
|
this.applySizes(sizes);
|
|
97
|
+
|
|
98
|
+
// Initialize each handle's aria-valuenow — before this, AT had no value
|
|
99
|
+
// to announce until the first drag/keypress.
|
|
100
|
+
this.handles.forEach((handle, idx) => {
|
|
101
|
+
const left = sizes[idx];
|
|
102
|
+
const right = sizes[idx + 1];
|
|
103
|
+
if (left !== undefined && right !== undefined && left + right > 0) {
|
|
104
|
+
handle.updateAriaValue((left / (left + right)) * 100);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
97
107
|
}
|
|
98
108
|
|
|
99
109
|
private calculateInitialSizes(): number[] {
|
|
@@ -275,6 +285,16 @@ export class RelResizableGroup extends LitElement {
|
|
|
275
285
|
const delta = e.key === "ArrowUp" ? -step : step;
|
|
276
286
|
this.applyKeyboardResize(handleIndex, delta);
|
|
277
287
|
applied = true;
|
|
288
|
+
} else if (e.key === "Home" || e.key === "End") {
|
|
289
|
+
// APG window-splitter: Home collapses the leading panel to its min,
|
|
290
|
+
// End grows it to its max (both still clamped by the neighbor).
|
|
291
|
+
const leftPanel = this.panels[handleIndex];
|
|
292
|
+
if (leftPanel && handleIndex + 1 < this.panels.length) {
|
|
293
|
+
e.preventDefault();
|
|
294
|
+
const target = e.key === "Home" ? leftPanel.minSize : leftPanel.maxSize;
|
|
295
|
+
this.applyKeyboardResize(handleIndex, target - leftPanel.currentSize);
|
|
296
|
+
applied = true;
|
|
297
|
+
}
|
|
278
298
|
}
|
|
279
299
|
|
|
280
300
|
if (applied) {
|
|
@@ -108,21 +108,21 @@ export class RelSegmentedButton extends LitElement {
|
|
|
108
108
|
|
|
109
109
|
/* Sizes */
|
|
110
110
|
.segmented-buttons.small .segment {
|
|
111
|
-
padding: var(--rel-spacing-3xs) var(--rel-spacing-xs);
|
|
112
|
-
font-family: var(--rel-body-xs-regular-font-family);
|
|
111
|
+
padding: var(--rel-spacing-3xs) var(--rel-spacing-xs);
|
|
112
|
+
font-family: var(--rel-body-xs-regular-font-family);
|
|
113
113
|
font-weight: var(--rel-body-xs-semibold-font-weight);
|
|
114
114
|
font-size: var(--rel-body-xs-semibold-font-size);
|
|
115
115
|
line-height: var(--rel-body-xs-semibold-line-height);
|
|
116
|
-
letter-spacing: var(--rel-body-xs-semibold-letter-spacing);
|
|
116
|
+
letter-spacing: var(--rel-body-xs-semibold-letter-spacing);
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
.segmented-buttons.medium .segment {
|
|
120
|
-
padding: var(--rel-spacing-2xs) var(--rel-spacing-md);
|
|
121
|
-
font-family: var(--rel-body-sm-regular-font-family);
|
|
122
|
-
font-weight: var(--rel-body-sm-regular-font-weight);
|
|
123
|
-
font-size: var(--rel-body-sm-regular-font-size);
|
|
124
|
-
line-height: var(--rel-body-sm-regular-line-height);
|
|
125
|
-
letter-spacing: var(--rel-body-sm-regular-letter-spacing);
|
|
120
|
+
padding: var(--rel-spacing-2xs) var(--rel-spacing-md);
|
|
121
|
+
font-family: var(--rel-body-sm-regular-font-family);
|
|
122
|
+
font-weight: var(--rel-body-sm-regular-font-weight);
|
|
123
|
+
font-size: var(--rel-body-sm-regular-font-size);
|
|
124
|
+
line-height: var(--rel-body-sm-regular-line-height);
|
|
125
|
+
letter-spacing: var(--rel-body-sm-regular-letter-spacing);
|
|
126
126
|
}
|
|
127
127
|
`;
|
|
128
128
|
|
|
@@ -139,7 +139,33 @@ export class RelSegmentedButton extends LitElement {
|
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
|
|
142
|
+
/** APG radio-group navigation: arrows move focus AND select, the group is
|
|
143
|
+
* one Tab stop (roving tabindex on the selected segment). */
|
|
144
|
+
private onKeyDown = (e: KeyboardEvent) => {
|
|
145
|
+
const prevKey = e.key === "ArrowLeft" || e.key === "ArrowUp";
|
|
146
|
+
const nextKey = e.key === "ArrowRight" || e.key === "ArrowDown";
|
|
147
|
+
if (!prevKey && !nextKey && e.key !== "Home" && e.key !== "End") return;
|
|
148
|
+
|
|
149
|
+
const enabled = this.options.filter((o) => !o.disabled);
|
|
150
|
+
if (enabled.length === 0) return;
|
|
151
|
+
e.preventDefault();
|
|
152
|
+
|
|
153
|
+
const currentIdx = enabled.findIndex((o) => o.value === this.active);
|
|
154
|
+
let target: number;
|
|
155
|
+
if (e.key === "Home") target = 0;
|
|
156
|
+
else if (e.key === "End") target = enabled.length - 1;
|
|
157
|
+
else if (prevKey) target = currentIdx <= 0 ? enabled.length - 1 : currentIdx - 1;
|
|
158
|
+
else target = (currentIdx + 1) % enabled.length;
|
|
159
|
+
|
|
160
|
+
this.setActive(enabled[target]);
|
|
161
|
+
this.updateComplete.then(() => {
|
|
162
|
+
this.shadowRoot
|
|
163
|
+
?.querySelector<HTMLElement>('button[tabindex="0"]')
|
|
164
|
+
?.focus();
|
|
165
|
+
});
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
private renderSegment(option: SegmentOption, rovingValue: string | null) {
|
|
143
169
|
const isActive = this.active === option.value;
|
|
144
170
|
const classes = [
|
|
145
171
|
"segment",
|
|
@@ -152,6 +178,9 @@ export class RelSegmentedButton extends LitElement {
|
|
|
152
178
|
return html`
|
|
153
179
|
<button
|
|
154
180
|
part="option" class="${classes}"
|
|
181
|
+
role="radio"
|
|
182
|
+
aria-checked="${isActive}"
|
|
183
|
+
tabindex="${!option.disabled && option.value === rovingValue ? 0 : -1}"
|
|
155
184
|
?disabled="${option.disabled}"
|
|
156
185
|
@click="${() => this.setActive(option)}">
|
|
157
186
|
${option.label}
|
|
@@ -166,9 +195,18 @@ export class RelSegmentedButton extends LitElement {
|
|
|
166
195
|
`align-${this.align}`,
|
|
167
196
|
].join(" ");
|
|
168
197
|
|
|
198
|
+
const enabled = this.options.filter((o) => !o.disabled);
|
|
199
|
+
const rovingValue = enabled.some((o) => o.value === this.active)
|
|
200
|
+
? this.active
|
|
201
|
+
: enabled[0]?.value ?? null;
|
|
202
|
+
|
|
169
203
|
return html`
|
|
170
|
-
<div
|
|
171
|
-
${
|
|
204
|
+
<div
|
|
205
|
+
part="base" class="${containerClasses}"
|
|
206
|
+
role="radiogroup"
|
|
207
|
+
@keydown="${this.onKeyDown}"
|
|
208
|
+
>
|
|
209
|
+
${this.options.map((option) => this.renderSegment(option, rovingValue))}
|
|
172
210
|
</div>
|
|
173
211
|
`;
|
|
174
212
|
}
|
|
@@ -119,3 +119,4 @@ None exposed.
|
|
|
119
119
|
- Typed text that doesn't exactly match (case-insensitive) any option reverts to the last committed `selected` value on blur, Escape, or Tab
|
|
120
120
|
- The ghost-text suggestion shows the remainder of the first option whose name starts with the typed text (case-insensitive); it has no effect on filtering, which matches substrings anywhere in an option
|
|
121
121
|
- Dropdown panel is positioned via `position: fixed` and flips above the trigger if there isn't enough viewport space below
|
|
122
|
+
- The open listbox is promoted to the browser **top layer** (native Popover API, `popover="manual"`), so it renders correctly inside transformed / `overflow: hidden` containers — a centered `rel-modal`, a bottom `rel-drawer`. Engines without the Popover API fall back to `position: fixed` with a measure-and-cancel correction for transformed ancestors
|