@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
|
@@ -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) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LitElement, html, css } from "lit";
|
|
1
|
+
import { LitElement, html, css, nothing } from "lit";
|
|
2
2
|
import { customElement, property } from "lit/decorators.js";
|
|
3
3
|
|
|
4
4
|
export type SegmentSize = "small" | "medium";
|
|
@@ -20,6 +20,9 @@ export class RelSegmentedButton extends LitElement {
|
|
|
20
20
|
|
|
21
21
|
@property({ type: String }) align: SegmentAlign = "left";
|
|
22
22
|
|
|
23
|
+
/** Accessible name for the radiogroup (it renders no visible group label). */
|
|
24
|
+
@property({ type: String, attribute: "field-aria-label" }) fieldAriaLabel?: string;
|
|
25
|
+
|
|
23
26
|
static styles = css`
|
|
24
27
|
:host {
|
|
25
28
|
color: var(--rel-text-default);
|
|
@@ -108,21 +111,21 @@ export class RelSegmentedButton extends LitElement {
|
|
|
108
111
|
|
|
109
112
|
/* Sizes */
|
|
110
113
|
.segmented-buttons.small .segment {
|
|
111
|
-
padding: var(--rel-spacing-3xs) var(--rel-spacing-xs);
|
|
112
|
-
font-family: var(--rel-body-xs-regular-font-family);
|
|
114
|
+
padding: var(--rel-spacing-3xs) var(--rel-spacing-xs);
|
|
115
|
+
font-family: var(--rel-body-xs-regular-font-family);
|
|
113
116
|
font-weight: var(--rel-body-xs-semibold-font-weight);
|
|
114
117
|
font-size: var(--rel-body-xs-semibold-font-size);
|
|
115
118
|
line-height: var(--rel-body-xs-semibold-line-height);
|
|
116
|
-
letter-spacing: var(--rel-body-xs-semibold-letter-spacing);
|
|
119
|
+
letter-spacing: var(--rel-body-xs-semibold-letter-spacing);
|
|
117
120
|
}
|
|
118
121
|
|
|
119
122
|
.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);
|
|
123
|
+
padding: var(--rel-spacing-2xs) var(--rel-spacing-md);
|
|
124
|
+
font-family: var(--rel-body-sm-regular-font-family);
|
|
125
|
+
font-weight: var(--rel-body-sm-regular-font-weight);
|
|
126
|
+
font-size: var(--rel-body-sm-regular-font-size);
|
|
127
|
+
line-height: var(--rel-body-sm-regular-line-height);
|
|
128
|
+
letter-spacing: var(--rel-body-sm-regular-letter-spacing);
|
|
126
129
|
}
|
|
127
130
|
`;
|
|
128
131
|
|
|
@@ -139,7 +142,33 @@ export class RelSegmentedButton extends LitElement {
|
|
|
139
142
|
}
|
|
140
143
|
}
|
|
141
144
|
|
|
142
|
-
|
|
145
|
+
/** APG radio-group navigation: arrows move focus AND select, the group is
|
|
146
|
+
* one Tab stop (roving tabindex on the selected segment). */
|
|
147
|
+
private onKeyDown = (e: KeyboardEvent) => {
|
|
148
|
+
const prevKey = e.key === "ArrowLeft" || e.key === "ArrowUp";
|
|
149
|
+
const nextKey = e.key === "ArrowRight" || e.key === "ArrowDown";
|
|
150
|
+
if (!prevKey && !nextKey && e.key !== "Home" && e.key !== "End") return;
|
|
151
|
+
|
|
152
|
+
const enabled = this.options.filter((o) => !o.disabled);
|
|
153
|
+
if (enabled.length === 0) return;
|
|
154
|
+
e.preventDefault();
|
|
155
|
+
|
|
156
|
+
const currentIdx = enabled.findIndex((o) => o.value === this.active);
|
|
157
|
+
let target: number;
|
|
158
|
+
if (e.key === "Home") target = 0;
|
|
159
|
+
else if (e.key === "End") target = enabled.length - 1;
|
|
160
|
+
else if (prevKey) target = currentIdx <= 0 ? enabled.length - 1 : currentIdx - 1;
|
|
161
|
+
else target = (currentIdx + 1) % enabled.length;
|
|
162
|
+
|
|
163
|
+
this.setActive(enabled[target]);
|
|
164
|
+
this.updateComplete.then(() => {
|
|
165
|
+
this.shadowRoot
|
|
166
|
+
?.querySelector<HTMLElement>('button[tabindex="0"]')
|
|
167
|
+
?.focus();
|
|
168
|
+
});
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
private renderSegment(option: SegmentOption, rovingValue: string | null) {
|
|
143
172
|
const isActive = this.active === option.value;
|
|
144
173
|
const classes = [
|
|
145
174
|
"segment",
|
|
@@ -152,6 +181,9 @@ export class RelSegmentedButton extends LitElement {
|
|
|
152
181
|
return html`
|
|
153
182
|
<button
|
|
154
183
|
part="option" class="${classes}"
|
|
184
|
+
role="radio"
|
|
185
|
+
aria-checked="${isActive}"
|
|
186
|
+
tabindex="${!option.disabled && option.value === rovingValue ? 0 : -1}"
|
|
155
187
|
?disabled="${option.disabled}"
|
|
156
188
|
@click="${() => this.setActive(option)}">
|
|
157
189
|
${option.label}
|
|
@@ -166,9 +198,19 @@ export class RelSegmentedButton extends LitElement {
|
|
|
166
198
|
`align-${this.align}`,
|
|
167
199
|
].join(" ");
|
|
168
200
|
|
|
201
|
+
const enabled = this.options.filter((o) => !o.disabled);
|
|
202
|
+
const rovingValue = enabled.some((o) => o.value === this.active)
|
|
203
|
+
? this.active
|
|
204
|
+
: enabled[0]?.value ?? null;
|
|
205
|
+
|
|
169
206
|
return html`
|
|
170
|
-
<div
|
|
171
|
-
${
|
|
207
|
+
<div
|
|
208
|
+
part="base" class="${containerClasses}"
|
|
209
|
+
role="radiogroup"
|
|
210
|
+
aria-label="${this.fieldAriaLabel ?? nothing}"
|
|
211
|
+
@keydown="${this.onKeyDown}"
|
|
212
|
+
>
|
|
213
|
+
${this.options.map((option) => this.renderSegment(option, rovingValue))}
|
|
172
214
|
</div>
|
|
173
215
|
`;
|
|
174
216
|
}
|
|
@@ -5,6 +5,7 @@ import "@reliability-design/icons/rel-icon";
|
|
|
5
5
|
import "../rel-chip/rel-chip.js";
|
|
6
6
|
import "../rel-checkbox/rel-checkbox.js";
|
|
7
7
|
import type { FieldLabelStyle } from "../rel-textfield/rel-textfield.js";
|
|
8
|
+
import { announce } from "../../utils/announce";
|
|
8
9
|
|
|
9
10
|
export type SelectState = "default" | "error" | "disabled";
|
|
10
11
|
export type SelectSize = "small" | "medium" | "large";
|
|
@@ -103,6 +104,11 @@ export class RelSelect extends LitElement {
|
|
|
103
104
|
|
|
104
105
|
@state() private open = false;
|
|
105
106
|
@state() private highlightedIndex = -1;
|
|
107
|
+
/** Virtual highlight on the Select-All header row (multi mode). */
|
|
108
|
+
@state() private selectAllHighlighted = false;
|
|
109
|
+
|
|
110
|
+
private typeaheadBuffer = "";
|
|
111
|
+
private typeaheadTimer: number | undefined;
|
|
106
112
|
@state() private panelStyle = "";
|
|
107
113
|
@state() private searchValue = "";
|
|
108
114
|
@state() private internalState: "default" | "focused" = "default";
|
|
@@ -571,6 +577,10 @@ export class RelSelect extends LitElement {
|
|
|
571
577
|
padding: 12px;
|
|
572
578
|
}
|
|
573
579
|
|
|
580
|
+
.select-all-row.highlighted {
|
|
581
|
+
background: var(--rel-select-option-hover, var(--rel-bg-neutral-elevated-1));
|
|
582
|
+
}
|
|
583
|
+
|
|
574
584
|
.select-empty {
|
|
575
585
|
padding: var(--rel-spacing-xs) 12px;
|
|
576
586
|
color: var(--rel-select-placeholder-color, var(--rel-text-body));
|
|
@@ -722,13 +732,15 @@ export class RelSelect extends LitElement {
|
|
|
722
732
|
}
|
|
723
733
|
|
|
724
734
|
private addGlobalListeners() {
|
|
725
|
-
|
|
735
|
+
// Capture phase so a pointerdown that another component stops in its own
|
|
736
|
+
// capture listener (e.g. a rel-popover trigger) still closes this listbox.
|
|
737
|
+
document.addEventListener("pointerdown", this.onOutsidePointerDown, true);
|
|
726
738
|
window.addEventListener("resize", this.updatePosition);
|
|
727
739
|
window.addEventListener("scroll", this.onPageScroll, true);
|
|
728
740
|
}
|
|
729
741
|
|
|
730
742
|
private removeGlobalListeners() {
|
|
731
|
-
document.removeEventListener("pointerdown", this.onOutsidePointerDown);
|
|
743
|
+
document.removeEventListener("pointerdown", this.onOutsidePointerDown, true);
|
|
732
744
|
window.removeEventListener("resize", this.updatePosition);
|
|
733
745
|
window.removeEventListener("scroll", this.onPageScroll, true);
|
|
734
746
|
}
|
|
@@ -769,6 +781,7 @@ export class RelSelect extends LitElement {
|
|
|
769
781
|
// before the user has touched anything reads as an unintended default.
|
|
770
782
|
// Highlight only kicks in once they actually hover or press an arrow key.
|
|
771
783
|
this.highlightedIndex = -1;
|
|
784
|
+
this.selectAllHighlighted = false;
|
|
772
785
|
this.addGlobalListeners();
|
|
773
786
|
requestAnimationFrame(() => {
|
|
774
787
|
this.showNativePopover();
|
|
@@ -872,6 +885,7 @@ export class RelSelect extends LitElement {
|
|
|
872
885
|
idx >= 0
|
|
873
886
|
? this.selectedValues.filter((v) => v !== option.value)
|
|
874
887
|
: [...this.selectedValues, option.value];
|
|
888
|
+
announce(`${option.label} ${idx >= 0 ? "deselected" : "selected"}`);
|
|
875
889
|
if (this.state === "error") this.state = "default";
|
|
876
890
|
this.emitChange();
|
|
877
891
|
}
|
|
@@ -886,6 +900,7 @@ export class RelSelect extends LitElement {
|
|
|
886
900
|
const toAdd = filtered.map((o) => o.value).filter((v) => !this.selectedValues.includes(v));
|
|
887
901
|
this.selectedValues = [...this.selectedValues, ...toAdd];
|
|
888
902
|
}
|
|
903
|
+
announce(status === "checked" ? "All options deselected" : "All options selected");
|
|
889
904
|
if (this.state === "error") this.state = "default";
|
|
890
905
|
this.emitChange();
|
|
891
906
|
}
|
|
@@ -893,6 +908,8 @@ export class RelSelect extends LitElement {
|
|
|
893
908
|
private removeChip(value: string) {
|
|
894
909
|
if (this.state === "disabled") return;
|
|
895
910
|
this.selectedValues = this.selectedValues.filter((v) => v !== value);
|
|
911
|
+
const opt = this.normalizedOptions.find((o) => o.value === value);
|
|
912
|
+
announce(`${opt?.label ?? value} removed`);
|
|
896
913
|
this.emitChange();
|
|
897
914
|
}
|
|
898
915
|
|
|
@@ -903,6 +920,20 @@ export class RelSelect extends LitElement {
|
|
|
903
920
|
|
|
904
921
|
private onTriggerKeyDown(e: KeyboardEvent) {
|
|
905
922
|
if (this.state === "disabled") return;
|
|
923
|
+
|
|
924
|
+
// Chips render inside the trigger <button>, so their remove buttons can
|
|
925
|
+
// never be tab stops — Backspace/Delete is the keyboard path for
|
|
926
|
+
// removing the most recent selection (standard multi-combobox behavior).
|
|
927
|
+
if (
|
|
928
|
+
this.multiple &&
|
|
929
|
+
(e.key === "Backspace" || e.key === "Delete") &&
|
|
930
|
+
this.selectedValues.length > 0
|
|
931
|
+
) {
|
|
932
|
+
e.preventDefault();
|
|
933
|
+
this.removeChip(this.selectedValues[this.selectedValues.length - 1]);
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
|
|
906
937
|
if (!this.open) {
|
|
907
938
|
if (["ArrowDown", "ArrowUp", "Enter", " "].includes(e.key)) {
|
|
908
939
|
e.preventDefault();
|
|
@@ -917,32 +948,86 @@ export class RelSelect extends LitElement {
|
|
|
917
948
|
|
|
918
949
|
private onSearchInput = (e: InputEvent) => {
|
|
919
950
|
this.searchValue = (e.target as HTMLInputElement).value;
|
|
920
|
-
|
|
951
|
+
const count = this.filteredOptions.length;
|
|
952
|
+
this.highlightedIndex = count > 0 ? 0 : -1;
|
|
953
|
+
this.selectAllHighlighted = false;
|
|
954
|
+
if (count === 0) {
|
|
955
|
+
announce(this.options.length === 0 ? "No options available" : "No matches found");
|
|
956
|
+
} else {
|
|
957
|
+
announce(`${count} option${count === 1 ? "" : "s"} available`);
|
|
958
|
+
}
|
|
921
959
|
requestAnimationFrame(() => this.updatePosition());
|
|
922
960
|
};
|
|
923
961
|
|
|
924
962
|
private onSearchKeyDown(e: KeyboardEvent) {
|
|
963
|
+
// Backspace in an empty search field removes the last chip (see
|
|
964
|
+
// onTriggerKeyDown for the rationale).
|
|
965
|
+
if (
|
|
966
|
+
this.multiple &&
|
|
967
|
+
e.key === "Backspace" &&
|
|
968
|
+
this.searchValue === "" &&
|
|
969
|
+
this.selectedValues.length > 0
|
|
970
|
+
) {
|
|
971
|
+
this.removeChip(this.selectedValues[this.selectedValues.length - 1]);
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
925
974
|
this.handleListNavigation(e);
|
|
926
975
|
}
|
|
927
976
|
|
|
928
977
|
private handleListNavigation(e: KeyboardEvent) {
|
|
929
978
|
const filtered = this.filteredOptions;
|
|
979
|
+
// The Select-All row participates in arrow navigation as a virtual row
|
|
980
|
+
// above the first option — it lives in the panel header, outside the
|
|
981
|
+
// option list, and would otherwise be keyboard-unreachable.
|
|
982
|
+
const hasSelectAll = this.multiple && filtered.length > 0;
|
|
983
|
+
|
|
930
984
|
switch (e.key) {
|
|
931
985
|
case "ArrowDown":
|
|
932
986
|
e.preventDefault();
|
|
933
|
-
|
|
987
|
+
if (this.selectAllHighlighted) {
|
|
988
|
+
this.selectAllHighlighted = false;
|
|
989
|
+
this.highlightedIndex = filtered.length > 0 ? 0 : -1;
|
|
990
|
+
} else if (this.highlightedIndex === -1 && hasSelectAll) {
|
|
991
|
+
this.selectAllHighlighted = true;
|
|
992
|
+
} else {
|
|
993
|
+
this.highlightedIndex = Math.min(filtered.length - 1, this.highlightedIndex + 1);
|
|
994
|
+
}
|
|
995
|
+
this.scrollHighlightedIntoView();
|
|
934
996
|
break;
|
|
935
997
|
case "ArrowUp":
|
|
936
998
|
e.preventDefault();
|
|
937
|
-
this.
|
|
999
|
+
if (!this.selectAllHighlighted && this.highlightedIndex <= 0 && hasSelectAll) {
|
|
1000
|
+
this.highlightedIndex = -1;
|
|
1001
|
+
this.selectAllHighlighted = true;
|
|
1002
|
+
} else {
|
|
1003
|
+
this.highlightedIndex = Math.max(0, this.highlightedIndex - 1);
|
|
1004
|
+
}
|
|
1005
|
+
this.scrollHighlightedIntoView();
|
|
1006
|
+
break;
|
|
1007
|
+
case "Home":
|
|
1008
|
+
if (filtered.length === 0) break;
|
|
1009
|
+
e.preventDefault();
|
|
1010
|
+
this.selectAllHighlighted = false;
|
|
1011
|
+
this.highlightedIndex = 0;
|
|
1012
|
+
this.scrollHighlightedIntoView();
|
|
1013
|
+
break;
|
|
1014
|
+
case "End":
|
|
1015
|
+
if (filtered.length === 0) break;
|
|
1016
|
+
e.preventDefault();
|
|
1017
|
+
this.selectAllHighlighted = false;
|
|
1018
|
+
this.highlightedIndex = filtered.length - 1;
|
|
1019
|
+
this.scrollHighlightedIntoView();
|
|
1020
|
+
break;
|
|
1021
|
+
case " ":
|
|
1022
|
+
// With a search field, Space types into it; only the no-search mode
|
|
1023
|
+
// (trigger holds focus) treats Space as toggle/select.
|
|
1024
|
+
if (!this.noSearch) return;
|
|
1025
|
+
e.preventDefault();
|
|
1026
|
+
this.activateHighlighted();
|
|
938
1027
|
break;
|
|
939
1028
|
case "Enter":
|
|
940
1029
|
e.preventDefault();
|
|
941
|
-
|
|
942
|
-
const opt = filtered[this.highlightedIndex];
|
|
943
|
-
if (this.multiple) this.toggleMultiOption(opt);
|
|
944
|
-
else this.selectSingleOption(opt);
|
|
945
|
-
}
|
|
1030
|
+
this.activateHighlighted();
|
|
946
1031
|
break;
|
|
947
1032
|
case "Escape":
|
|
948
1033
|
e.preventDefault();
|
|
@@ -951,9 +1036,67 @@ export class RelSelect extends LitElement {
|
|
|
951
1036
|
case "Tab":
|
|
952
1037
|
this.closeListbox();
|
|
953
1038
|
break;
|
|
1039
|
+
default:
|
|
1040
|
+
this.handleTypeahead(e);
|
|
1041
|
+
break;
|
|
954
1042
|
}
|
|
955
1043
|
}
|
|
956
1044
|
|
|
1045
|
+
/** APG typeahead for the no-search mode (with a search field, typing
|
|
1046
|
+
* filters instead). Accumulates printable keys for 500 ms and jumps the
|
|
1047
|
+
* highlight to the next option whose label starts with the buffer. */
|
|
1048
|
+
private handleTypeahead(e: KeyboardEvent) {
|
|
1049
|
+
if (!this.noSearch) return;
|
|
1050
|
+
if (e.key.length !== 1 || e.ctrlKey || e.metaKey || e.altKey) return;
|
|
1051
|
+
|
|
1052
|
+
e.preventDefault();
|
|
1053
|
+
const isNewQuery = this.typeaheadBuffer === "";
|
|
1054
|
+
this.typeaheadBuffer += e.key.toLowerCase();
|
|
1055
|
+
clearTimeout(this.typeaheadTimer);
|
|
1056
|
+
this.typeaheadTimer = window.setTimeout(() => {
|
|
1057
|
+
this.typeaheadBuffer = "";
|
|
1058
|
+
}, 500);
|
|
1059
|
+
|
|
1060
|
+
const filtered = this.filteredOptions;
|
|
1061
|
+
if (filtered.length === 0) return;
|
|
1062
|
+
|
|
1063
|
+
// A fresh single-letter query starts searching AFTER the highlight (so
|
|
1064
|
+
// repeated presses cycle through matches); a growing buffer re-tests the
|
|
1065
|
+
// current one first.
|
|
1066
|
+
const start = Math.max(0, this.highlightedIndex) + (isNewQuery ? 1 : 0);
|
|
1067
|
+
for (let offset = 0; offset < filtered.length; offset++) {
|
|
1068
|
+
const idx = (start + offset) % filtered.length;
|
|
1069
|
+
if (filtered[idx].label.toLowerCase().startsWith(this.typeaheadBuffer)) {
|
|
1070
|
+
this.selectAllHighlighted = false;
|
|
1071
|
+
this.highlightedIndex = idx;
|
|
1072
|
+
this.scrollHighlightedIntoView();
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
private activateHighlighted() {
|
|
1079
|
+
if (this.selectAllHighlighted) {
|
|
1080
|
+
this.toggleSelectAll();
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
const filtered = this.filteredOptions;
|
|
1084
|
+
if (this.highlightedIndex >= 0 && filtered[this.highlightedIndex]) {
|
|
1085
|
+
const opt = filtered[this.highlightedIndex];
|
|
1086
|
+
if (this.multiple) this.toggleMultiOption(opt);
|
|
1087
|
+
else this.selectSingleOption(opt);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
private scrollHighlightedIntoView() {
|
|
1092
|
+
if (this.highlightedIndex < 0) return;
|
|
1093
|
+
requestAnimationFrame(() => {
|
|
1094
|
+
this.renderRoot
|
|
1095
|
+
.querySelector<HTMLElement>(`#${this.uniqueId}-option-${this.highlightedIndex}`)
|
|
1096
|
+
?.scrollIntoView({ block: "nearest" });
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
|
|
957
1100
|
private renderChips() {
|
|
958
1101
|
const values = this.selectedValues;
|
|
959
1102
|
const visible = values.slice(0, Math.min(this.visibleChipCount, this.maxVisibleChips));
|
|
@@ -1046,11 +1189,27 @@ export class RelSelect extends LitElement {
|
|
|
1046
1189
|
.join(" ");
|
|
1047
1190
|
|
|
1048
1191
|
const listboxId = `${this.uniqueId}-listbox`;
|
|
1049
|
-
const activeOptionId =
|
|
1050
|
-
|
|
1192
|
+
const activeOptionId = !this.open
|
|
1193
|
+
? ""
|
|
1194
|
+
: this.selectAllHighlighted
|
|
1195
|
+
? `${this.uniqueId}-select-all`
|
|
1196
|
+
: this.highlightedIndex >= 0
|
|
1197
|
+
? `${this.uniqueId}-option-${this.highlightedIndex}`
|
|
1198
|
+
: "";
|
|
1051
1199
|
|
|
1052
1200
|
const selectedSingle = opts.find((o) => o.value === this.selected);
|
|
1053
1201
|
|
|
1202
|
+
const baseName = accessibleName || this.placeholder;
|
|
1203
|
+
let triggerLabel = baseName;
|
|
1204
|
+
if (this.multiple && this.selectedValues.length > 0) {
|
|
1205
|
+
const labels = this.selectedValues.map((v) => opts.find((o) => o.value === v)?.label ?? v);
|
|
1206
|
+
const shown = labels.slice(0, 3);
|
|
1207
|
+
const more = labels.length - shown.length;
|
|
1208
|
+
triggerLabel = `${baseName}, ${shown.join(", ")}${more > 0 ? `, plus ${more} more` : ""}`;
|
|
1209
|
+
} else if (!this.multiple && selectedSingle) {
|
|
1210
|
+
triggerLabel = `${baseName}, ${selectedSingle.label}`;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1054
1213
|
return html`
|
|
1055
1214
|
<div class="${wrapperClasses}">
|
|
1056
1215
|
${labelHidden
|
|
@@ -1074,9 +1233,9 @@ export class RelSelect extends LitElement {
|
|
|
1074
1233
|
aria-haspopup="listbox"
|
|
1075
1234
|
aria-expanded="${this.open}"
|
|
1076
1235
|
aria-controls="${listboxId}"
|
|
1077
|
-
aria-activedescendant="${activeOptionId}"
|
|
1236
|
+
aria-activedescendant="${this.noSearch ? activeOptionId : nothing}"
|
|
1078
1237
|
aria-invalid="${this.state === "error"}"
|
|
1079
|
-
aria-label="${this.fieldAriaLabelledby ? nothing :
|
|
1238
|
+
aria-label="${this.fieldAriaLabelledby ? nothing : triggerLabel}"
|
|
1080
1239
|
aria-labelledby="${this.fieldAriaLabelledby ?? nothing}"
|
|
1081
1240
|
?disabled="${isDisabled}"
|
|
1082
1241
|
@click="${this.onTriggerClick}"
|
|
@@ -1136,6 +1295,11 @@ export class RelSelect extends LitElement {
|
|
|
1136
1295
|
<input
|
|
1137
1296
|
part="input" class="search-input"
|
|
1138
1297
|
type="text"
|
|
1298
|
+
role="combobox"
|
|
1299
|
+
aria-expanded="${this.open}"
|
|
1300
|
+
aria-controls="${listboxId}"
|
|
1301
|
+
aria-activedescendant="${activeOptionId}"
|
|
1302
|
+
aria-label="${accessibleName || this.placeholder}"
|
|
1139
1303
|
placeholder="${this.searchPlaceholder}"
|
|
1140
1304
|
.value="${this.searchValue}"
|
|
1141
1305
|
@input="${this.onSearchInput}"
|
|
@@ -1145,14 +1309,25 @@ export class RelSelect extends LitElement {
|
|
|
1145
1309
|
</div>`
|
|
1146
1310
|
: ""}
|
|
1147
1311
|
${this.multiple && filtered.length > 0
|
|
1148
|
-
? html`<div
|
|
1312
|
+
? html`<div
|
|
1313
|
+
id="${this.uniqueId}-select-all"
|
|
1314
|
+
class="select-all-row ${this.selectAllHighlighted ? "highlighted" : ""}"
|
|
1315
|
+
role="option"
|
|
1316
|
+
aria-selected="${this.selectAllStatus === "checked"}"
|
|
1317
|
+
aria-label="${this.selectAllLabel}"
|
|
1318
|
+
@pointerenter="${() => {
|
|
1319
|
+
this.selectAllHighlighted = true;
|
|
1320
|
+
this.highlightedIndex = -1;
|
|
1321
|
+
}}"
|
|
1322
|
+
@pointerleave="${() => (this.selectAllHighlighted = false)}"
|
|
1323
|
+
@mousedown="${(e: Event) => e.preventDefault()}"
|
|
1324
|
+
@click="${() => this.toggleSelectAll()}"
|
|
1325
|
+
>
|
|
1149
1326
|
<rel-checkbox
|
|
1327
|
+
inert
|
|
1328
|
+
aria-hidden="true"
|
|
1150
1329
|
label="${this.selectAllLabel}"
|
|
1151
1330
|
status="${this.selectAllStatus}"
|
|
1152
|
-
@rel-change=${(e: Event) => {
|
|
1153
|
-
e.stopPropagation();
|
|
1154
|
-
this.toggleSelectAll();
|
|
1155
|
-
}}
|
|
1156
1331
|
></rel-checkbox>
|
|
1157
1332
|
</div>`
|
|
1158
1333
|
: ""}
|
|
@@ -1175,17 +1350,21 @@ export class RelSelect extends LitElement {
|
|
|
1175
1350
|
class="checkbox-option ${index === this.highlightedIndex ? "highlighted" : ""}"
|
|
1176
1351
|
role="option"
|
|
1177
1352
|
aria-selected="${this.selectedValues.includes(option.value)}"
|
|
1178
|
-
|
|
1353
|
+
aria-label="${option.label}"
|
|
1354
|
+
@pointerenter="${() => {
|
|
1355
|
+
this.highlightedIndex = index;
|
|
1356
|
+
this.selectAllHighlighted = false;
|
|
1357
|
+
}}"
|
|
1358
|
+
@mousedown="${(e: Event) => e.preventDefault()}"
|
|
1359
|
+
@click="${() => this.toggleMultiOption(option)}"
|
|
1179
1360
|
>
|
|
1180
1361
|
<rel-checkbox
|
|
1362
|
+
inert
|
|
1363
|
+
aria-hidden="true"
|
|
1181
1364
|
label="${option.label}"
|
|
1182
1365
|
icon="${option.icon}"
|
|
1183
1366
|
supportingtext="${option.supportingText}"
|
|
1184
1367
|
status="${this.selectedValues.includes(option.value) ? "checked" : "unchecked"}"
|
|
1185
|
-
@rel-change=${(e: Event) => {
|
|
1186
|
-
e.stopPropagation();
|
|
1187
|
-
this.toggleMultiOption(option);
|
|
1188
|
-
}}
|
|
1189
1368
|
></rel-checkbox>
|
|
1190
1369
|
</div>
|
|
1191
1370
|
`
|