@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.
Files changed (58) hide show
  1. package/dist/components/rel-accordion/rel-accordion.ts +13 -1
  2. package/dist/components/rel-alert/rel-alert.ts +1 -1
  3. package/dist/components/rel-autocomplete/rel-autocomplete.ts +33 -2
  4. package/dist/components/rel-badge/rel-badge.ts +6 -1
  5. package/dist/components/rel-button/rel-button.ts +18 -2
  6. package/dist/components/rel-calendar/rel-calendar.ts +86 -18
  7. package/dist/components/rel-carousel/rel-carousel.ts +45 -2
  8. package/dist/components/rel-checkbox/rel-checkbox.ts +4 -1
  9. package/dist/components/rel-chip/rel-chip.ts +59 -19
  10. package/dist/components/rel-date-picker/date-parse.test.ts +116 -0
  11. package/dist/components/rel-date-picker/date-parse.ts +178 -0
  12. package/dist/components/rel-date-picker/rel-date-picker.ts +675 -93
  13. package/dist/components/rel-drawer/rel-drawer.ts +28 -61
  14. package/dist/components/rel-menu/rel-menu.ts +130 -15
  15. package/dist/components/rel-menubar/rel-menubar.ts +221 -63
  16. package/dist/components/rel-nav-menu/rel-nav-menu.ts +39 -3
  17. package/dist/components/rel-otp/rel-otp.ts +14 -4
  18. package/dist/components/rel-pagination/rel-pagination.ts +13 -0
  19. package/dist/components/rel-popover/rel-popover.ts +64 -5
  20. package/dist/components/rel-progress/rel-progress.ts +13 -4
  21. package/dist/components/rel-radio/README.md +6 -3
  22. package/dist/components/rel-radio/rel-radio.ts +236 -60
  23. package/dist/components/rel-range-picker/rel-range-picker.ts +581 -91
  24. package/dist/components/rel-resizable/rel-resizable-group.ts +20 -0
  25. package/dist/components/rel-segmented-button/rel-segmented-button.ts +55 -13
  26. package/dist/components/rel-select/rel-select.ts +203 -24
  27. package/dist/components/rel-sheet/rel-sheet.ts +184 -18
  28. package/dist/components/rel-sidebar/rel-sidebar.ts +50 -40
  29. package/dist/components/rel-skeleton/rel-skeleton.ts +9 -0
  30. package/dist/components/rel-slider/rel-slider.ts +6 -3
  31. package/dist/components/rel-snackbar/rel-snackbar.ts +20 -16
  32. package/dist/components/rel-stepper/rel-stepper.ts +47 -2
  33. package/dist/components/rel-switch/rel-switch.ts +3 -1
  34. package/dist/components/rel-tab/rel-tab.ts +32 -7
  35. package/dist/components/rel-textfield/rel-textfield.ts +263 -75
  36. package/dist/components/rel-time-picker/rel-time-picker.ts +749 -261
  37. package/dist/components/rel-time-picker/time-parse.test.ts +67 -0
  38. package/dist/components/rel-time-picker/time-parse.ts +85 -0
  39. package/dist/components/rel-toast/rel-toast.ts +4 -2
  40. package/dist/components/rel-tooltip/README.md +13 -5
  41. package/dist/components/rel-tooltip/rel-tooltip.ts +33 -0
  42. package/dist/components/rel-topbar/rel-topbar-actions.ts +1 -1
  43. package/dist/components/rel-topbar/rel-topbar-brand.ts +1 -1
  44. package/dist/components/rel-topbar/rel-topbar-dropdown-item.ts +1 -1
  45. package/dist/components/rel-topbar/rel-topbar-dropdown.ts +1 -1
  46. package/dist/components/rel-topbar/rel-topbar-item.ts +318 -319
  47. package/dist/components/rel-topbar/rel-topbar-mobile-panel.ts +1 -1
  48. package/dist/components/rel-topbar/rel-topbar-profile.ts +224 -224
  49. package/dist/components/rel-topbar/rel-topbar-utilities.ts +1 -1
  50. package/dist/components/rel-topbar/rel-topbar.ts +440 -440
  51. package/dist/components/rel-upload/rel-upload.ts +25 -0
  52. package/dist/index.d.mts +461 -23
  53. package/dist/index.d.ts +461 -23
  54. package/dist/index.js +3906 -807
  55. package/dist/index.js.map +1 -1
  56. package/dist/index.mjs +4143 -1055
  57. package/dist/index.mjs.map +1 -1
  58. 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
- private renderSegment(option: SegmentOption) {
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 part="base" class="${containerClasses}">
171
- ${this.options.map((option) => this.renderSegment(option))}
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
- document.addEventListener("pointerdown", this.onOutsidePointerDown);
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
- this.highlightedIndex = this.filteredOptions.length > 0 ? 0 : -1;
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
- this.highlightedIndex = Math.min(filtered.length - 1, this.highlightedIndex + 1);
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.highlightedIndex = Math.max(0, this.highlightedIndex - 1);
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
- if (this.highlightedIndex >= 0 && filtered[this.highlightedIndex]) {
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
- this.open && this.highlightedIndex >= 0 ? `${this.uniqueId}-option-${this.highlightedIndex}` : "";
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 : accessibleName || this.placeholder}"
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 class="select-all-row">
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
- @pointerenter="${() => (this.highlightedIndex = index)}"
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
  `