@reliability-design/web 1.4.5 → 1.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/components/rel-alert/rel-alert.ts +1 -1
  2. package/dist/components/rel-autocomplete/rel-autocomplete.ts +33 -2
  3. package/dist/components/rel-badge/rel-badge.ts +6 -1
  4. package/dist/components/rel-calendar/rel-calendar.ts +64 -18
  5. package/dist/components/rel-carousel/rel-carousel.ts +25 -2
  6. package/dist/components/rel-chip/rel-chip.ts +31 -20
  7. package/dist/components/rel-date-picker/date-parse.test.ts +116 -0
  8. package/dist/components/rel-date-picker/date-parse.ts +178 -0
  9. package/dist/components/rel-date-picker/rel-date-picker.ts +529 -90
  10. package/dist/components/rel-drawer/rel-drawer.ts +6 -3
  11. package/dist/components/rel-menu/rel-menu.ts +6 -0
  12. package/dist/components/rel-otp/rel-otp.ts +12 -2
  13. package/dist/components/rel-pagination/rel-pagination.ts +13 -0
  14. package/dist/components/rel-popover/rel-popover.ts +33 -2
  15. package/dist/components/rel-progress/rel-progress.ts +13 -4
  16. package/dist/components/rel-radio/README.md +6 -3
  17. package/dist/components/rel-radio/rel-radio.ts +236 -60
  18. package/dist/components/rel-range-picker/rel-range-picker.ts +452 -95
  19. package/dist/components/rel-segmented-button/rel-segmented-button.ts +5 -1
  20. package/dist/components/rel-select/rel-select.ts +44 -13
  21. package/dist/components/rel-sheet/rel-sheet.ts +163 -18
  22. package/dist/components/rel-sidebar/rel-sidebar.ts +7 -1
  23. package/dist/components/rel-skeleton/rel-skeleton.ts +9 -0
  24. package/dist/components/rel-slider/rel-slider.ts +6 -3
  25. package/dist/components/rel-snackbar/rel-snackbar.ts +20 -16
  26. package/dist/components/rel-stepper/rel-stepper.ts +3 -1
  27. package/dist/components/rel-switch/rel-switch.ts +3 -1
  28. package/dist/components/rel-textfield/rel-textfield.ts +263 -75
  29. package/dist/components/rel-time-picker/rel-time-picker.ts +450 -94
  30. package/dist/components/rel-time-picker/time-parse.test.ts +67 -0
  31. package/dist/components/rel-time-picker/time-parse.ts +85 -0
  32. package/dist/components/rel-toast/rel-toast.ts +4 -2
  33. package/dist/components/rel-tooltip/README.md +13 -5
  34. package/dist/components/rel-tooltip/rel-tooltip.ts +3 -0
  35. package/dist/components/rel-upload/rel-upload.ts +25 -0
  36. package/dist/index.d.mts +241 -11
  37. package/dist/index.d.ts +241 -11
  38. package/dist/index.js +2573 -598
  39. package/dist/index.js.map +1 -1
  40. package/dist/index.mjs +2614 -640
  41. package/dist/index.mjs.map +1 -1
  42. package/package.json +2 -2
@@ -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);
@@ -204,6 +207,7 @@ export class RelSegmentedButton extends LitElement {
204
207
  <div
205
208
  part="base" class="${containerClasses}"
206
209
  role="radiogroup"
210
+ aria-label="${this.fieldAriaLabel ?? nothing}"
207
211
  @keydown="${this.onKeyDown}"
208
212
  >
209
213
  ${this.options.map((option) => this.renderSegment(option, rovingValue))}
@@ -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";
@@ -731,13 +732,15 @@ export class RelSelect extends LitElement {
731
732
  }
732
733
 
733
734
  private addGlobalListeners() {
734
- 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);
735
738
  window.addEventListener("resize", this.updatePosition);
736
739
  window.addEventListener("scroll", this.onPageScroll, true);
737
740
  }
738
741
 
739
742
  private removeGlobalListeners() {
740
- document.removeEventListener("pointerdown", this.onOutsidePointerDown);
743
+ document.removeEventListener("pointerdown", this.onOutsidePointerDown, true);
741
744
  window.removeEventListener("resize", this.updatePosition);
742
745
  window.removeEventListener("scroll", this.onPageScroll, true);
743
746
  }
@@ -882,6 +885,7 @@ export class RelSelect extends LitElement {
882
885
  idx >= 0
883
886
  ? this.selectedValues.filter((v) => v !== option.value)
884
887
  : [...this.selectedValues, option.value];
888
+ announce(`${option.label} ${idx >= 0 ? "deselected" : "selected"}`);
885
889
  if (this.state === "error") this.state = "default";
886
890
  this.emitChange();
887
891
  }
@@ -896,6 +900,7 @@ export class RelSelect extends LitElement {
896
900
  const toAdd = filtered.map((o) => o.value).filter((v) => !this.selectedValues.includes(v));
897
901
  this.selectedValues = [...this.selectedValues, ...toAdd];
898
902
  }
903
+ announce(status === "checked" ? "All options deselected" : "All options selected");
899
904
  if (this.state === "error") this.state = "default";
900
905
  this.emitChange();
901
906
  }
@@ -903,6 +908,8 @@ export class RelSelect extends LitElement {
903
908
  private removeChip(value: string) {
904
909
  if (this.state === "disabled") return;
905
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`);
906
913
  this.emitChange();
907
914
  }
908
915
 
@@ -941,8 +948,14 @@ export class RelSelect extends LitElement {
941
948
 
942
949
  private onSearchInput = (e: InputEvent) => {
943
950
  this.searchValue = (e.target as HTMLInputElement).value;
944
- this.highlightedIndex = this.filteredOptions.length > 0 ? 0 : -1;
951
+ const count = this.filteredOptions.length;
952
+ this.highlightedIndex = count > 0 ? 0 : -1;
945
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
+ }
946
959
  requestAnimationFrame(() => this.updatePosition());
947
960
  };
948
961
 
@@ -1186,6 +1199,17 @@ export class RelSelect extends LitElement {
1186
1199
 
1187
1200
  const selectedSingle = opts.find((o) => o.value === this.selected);
1188
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
+
1189
1213
  return html`
1190
1214
  <div class="${wrapperClasses}">
1191
1215
  ${labelHidden
@@ -1209,9 +1233,9 @@ export class RelSelect extends LitElement {
1209
1233
  aria-haspopup="listbox"
1210
1234
  aria-expanded="${this.open}"
1211
1235
  aria-controls="${listboxId}"
1212
- aria-activedescendant="${activeOptionId}"
1236
+ aria-activedescendant="${this.noSearch ? activeOptionId : nothing}"
1213
1237
  aria-invalid="${this.state === "error"}"
1214
- aria-label="${this.fieldAriaLabelledby ? nothing : accessibleName || this.placeholder}"
1238
+ aria-label="${this.fieldAriaLabelledby ? nothing : triggerLabel}"
1215
1239
  aria-labelledby="${this.fieldAriaLabelledby ?? nothing}"
1216
1240
  ?disabled="${isDisabled}"
1217
1241
  @click="${this.onTriggerClick}"
@@ -1271,6 +1295,11 @@ export class RelSelect extends LitElement {
1271
1295
  <input
1272
1296
  part="input" class="search-input"
1273
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}"
1274
1303
  placeholder="${this.searchPlaceholder}"
1275
1304
  .value="${this.searchValue}"
1276
1305
  @input="${this.onSearchInput}"
@@ -1285,19 +1314,20 @@ export class RelSelect extends LitElement {
1285
1314
  class="select-all-row ${this.selectAllHighlighted ? "highlighted" : ""}"
1286
1315
  role="option"
1287
1316
  aria-selected="${this.selectAllStatus === "checked"}"
1317
+ aria-label="${this.selectAllLabel}"
1288
1318
  @pointerenter="${() => {
1289
1319
  this.selectAllHighlighted = true;
1290
1320
  this.highlightedIndex = -1;
1291
1321
  }}"
1292
1322
  @pointerleave="${() => (this.selectAllHighlighted = false)}"
1323
+ @mousedown="${(e: Event) => e.preventDefault()}"
1324
+ @click="${() => this.toggleSelectAll()}"
1293
1325
  >
1294
1326
  <rel-checkbox
1327
+ inert
1328
+ aria-hidden="true"
1295
1329
  label="${this.selectAllLabel}"
1296
1330
  status="${this.selectAllStatus}"
1297
- @rel-change=${(e: Event) => {
1298
- e.stopPropagation();
1299
- this.toggleSelectAll();
1300
- }}
1301
1331
  ></rel-checkbox>
1302
1332
  </div>`
1303
1333
  : ""}
@@ -1320,20 +1350,21 @@ export class RelSelect extends LitElement {
1320
1350
  class="checkbox-option ${index === this.highlightedIndex ? "highlighted" : ""}"
1321
1351
  role="option"
1322
1352
  aria-selected="${this.selectedValues.includes(option.value)}"
1353
+ aria-label="${option.label}"
1323
1354
  @pointerenter="${() => {
1324
1355
  this.highlightedIndex = index;
1325
1356
  this.selectAllHighlighted = false;
1326
1357
  }}"
1358
+ @mousedown="${(e: Event) => e.preventDefault()}"
1359
+ @click="${() => this.toggleMultiOption(option)}"
1327
1360
  >
1328
1361
  <rel-checkbox
1362
+ inert
1363
+ aria-hidden="true"
1329
1364
  label="${option.label}"
1330
1365
  icon="${option.icon}"
1331
1366
  supportingtext="${option.supportingText}"
1332
1367
  status="${this.selectedValues.includes(option.value) ? "checked" : "unchecked"}"
1333
- @rel-change=${(e: Event) => {
1334
- e.stopPropagation();
1335
- this.toggleMultiOption(option);
1336
- }}
1337
1368
  ></rel-checkbox>
1338
1369
  </div>
1339
1370
  `
@@ -1,9 +1,11 @@
1
- import { LitElement, html, css } from "lit";
1
+ import { LitElement, html, css, nothing } from "lit";
2
2
  import { customElement, property, query } from "lit/decorators.js";
3
3
  import "@reliability-design/icons/rel-icon";
4
+ import "../rel-progress/rel-progress.js";
4
5
  import { getDeepActiveElement, getFocusableElements, trapTabKey } from "../../utils/focus.js";
5
6
 
6
7
  export type ModalSide = "top" | "bottom" | "center";
8
+ export type ModalSize = "sm" | "md" | "lg" | "xl" | "fullscreen";
7
9
 
8
10
  @customElement("rel-modal")
9
11
  export class RelModal extends LitElement {
@@ -11,9 +13,37 @@ export class RelModal extends LitElement {
11
13
  @property({ type: String }) side: ModalSide = "bottom";
12
14
  @property({ type: String }) title = "";
13
15
  @property({ type: String }) description = "";
16
+ /** Alias for `description`; rendered only when `description` is empty. */
17
+ @property({ type: String }) subtitle = "";
14
18
  @property({ type: Boolean }) dismissible = true;
15
19
  @property({ type: Boolean }) modal = true;
16
20
 
21
+ /** Panel width scale. `md` matches the Figma Modal Sheet width; `fullscreen` covers the viewport. */
22
+ @property({ type: String, reflect: true }) size: ModalSize = "md";
23
+
24
+ /* Granular dismissal switches — each is ANDed with the legacy coarse
25
+ `dismissible` switch so existing `dismissible={false}` consumers keep
26
+ their fully-locked behavior. */
27
+ @property({ type: Boolean, attribute: "close-on-backdrop-click" }) closeOnBackdropClick = true;
28
+ @property({ type: Boolean, attribute: "close-on-escape" }) closeOnEscape = true;
29
+ @property({ type: Boolean, attribute: "show-close-button" }) showCloseButton = true;
30
+
31
+ /** Move focus into the panel when opened (modal mode only). */
32
+ @property({ type: Boolean, attribute: "auto-focus" }) autoFocus = true;
33
+ /** Return focus to the opener when closed. */
34
+ @property({ type: Boolean, attribute: "restore-focus" }) restoreFocus = true;
35
+
36
+ /** When false, the body no longer scrolls internally — tall content clips at the panel's max height. */
37
+ @property({ type: Boolean }) scrollable = true;
38
+
39
+ /** Shows a spinner overlay and blocks all interaction inside the panel. */
40
+ @property({ type: Boolean, reflect: true }) loading = false;
41
+ /** Blocks user-initiated closes (X / backdrop / Escape) and footer actions. */
42
+ @property({ type: Boolean, reflect: true }) disabled = false;
43
+
44
+ /** Stacking order of the whole modal (backdrop + panel). */
45
+ @property({ type: Number, attribute: "z-index" }) zIndex = 1000;
46
+
17
47
  /**
18
48
  * Figma "Breakpoint" axis. responsive (default) collapses to a full-width
19
49
  * bottom sheet below 480px; web keeps the fixed desktop panel at every size.
@@ -86,16 +116,30 @@ export class RelModal extends LitElement {
86
116
  overflow: hidden;
87
117
  outline: none;
88
118
  font-family: var(--rel-fonts-font-family);
89
- width: clamp(360px, 49vw, 708px);
119
+ /* md is the Figma Modal Sheet Breakpoint=Web width (708px cap). */
120
+ --modal-width: clamp(360px, 49vw, 708px);
121
+ width: var(--modal-width);
90
122
  left: 50%;
91
123
  transition: transform var(--modal-transition-duration) var(--modal-transition-timing),
92
124
  opacity var(--modal-transition-duration) var(--modal-transition-timing);
93
125
  }
94
126
 
127
+ .panel[data-size="sm"] {
128
+ --modal-width: min(400px, calc(100vw - 32px));
129
+ }
130
+
131
+ .panel[data-size="lg"] {
132
+ --modal-width: min(900px, calc(100vw - 48px));
133
+ }
134
+
135
+ .panel[data-size="xl"] {
136
+ --modal-width: min(1140px, calc(100vw - 48px));
137
+ }
138
+
95
139
  .panel[data-side="bottom"] {
96
140
  bottom: 48px;
97
141
  left: 50%;
98
- width: clamp(360px, 49vw, 708px);
142
+ width: var(--modal-width);
99
143
  max-height: calc(100vh - 96px);
100
144
  border-radius: var(--rel-border-radius-sm);
101
145
  transform: translateX(-50%) translateY(calc(100% + 48px));
@@ -108,7 +152,7 @@ export class RelModal extends LitElement {
108
152
  .panel[data-side="top"] {
109
153
  top: 48px;
110
154
  left: 50%;
111
- width: clamp(360px, 49vw, 708px);
155
+ width: var(--modal-width);
112
156
  max-height: calc(100vh - 96px);
113
157
  border-radius: var(--rel-border-radius-sm);
114
158
  transform: translateX(-50%) translateY(calc(-100% - 48px));
@@ -121,7 +165,7 @@ export class RelModal extends LitElement {
121
165
  .panel[data-side="center"] {
122
166
  top: 50%;
123
167
  left: 50%;
124
- width: clamp(360px, 49vw, 708px);
168
+ width: var(--modal-width);
125
169
  max-height: calc(100vh - 96px);
126
170
  border-radius: var(--rel-border-radius-sm);
127
171
  transform: translate(-50%, -50%) scale(0.96);
@@ -159,6 +203,50 @@ export class RelModal extends LitElement {
159
203
  }
160
204
  }
161
205
 
206
+ /* size="fullscreen" — edge-to-edge at every side value; declared after the
207
+ side and responsive-collapse rules so it wins their specificity ties. */
208
+ .panel[data-size="fullscreen"],
209
+ .panel[data-size="fullscreen"][data-side="top"],
210
+ .panel[data-size="fullscreen"][data-side="center"],
211
+ .panel[data-size="fullscreen"][data-side="bottom"] {
212
+ top: 0;
213
+ bottom: 0;
214
+ left: 0;
215
+ right: 0;
216
+ width: 100%;
217
+ height: 100%;
218
+ max-height: none;
219
+ border-radius: 0;
220
+ transform: translateY(16px);
221
+ opacity: 0;
222
+ }
223
+
224
+ .modal-container.open .panel[data-size="fullscreen"],
225
+ .modal-container.open .panel[data-size="fullscreen"][data-side="top"],
226
+ .modal-container.open .panel[data-size="fullscreen"][data-side="center"],
227
+ .modal-container.open .panel[data-size="fullscreen"][data-side="bottom"] {
228
+ transform: translateY(0);
229
+ opacity: 1;
230
+ }
231
+
232
+ @media (max-width: 480px) {
233
+ :host([breakpoint="responsive"]) .panel[data-size="fullscreen"] {
234
+ top: 0;
235
+ bottom: 0;
236
+ left: 0;
237
+ right: 0;
238
+ width: 100%;
239
+ height: 100%;
240
+ max-height: none;
241
+ border-radius: 0;
242
+ transform: translateY(16px);
243
+ }
244
+
245
+ :host([breakpoint="responsive"]) .modal-container.open .panel[data-size="fullscreen"] {
246
+ transform: translateY(0);
247
+ }
248
+ }
249
+
162
250
  .header {
163
251
  display: flex;
164
252
  align-items: flex-start;
@@ -241,12 +329,51 @@ export class RelModal extends LitElement {
241
329
  outline-offset: 2px;
242
330
  }
243
331
 
332
+ .close-button:disabled {
333
+ cursor: not-allowed;
334
+ opacity: 0.5;
335
+ }
336
+
337
+ .close-button:disabled:hover {
338
+ background: transparent;
339
+ }
340
+
244
341
  .content {
245
342
  flex: 1;
246
343
  overflow-y: auto;
247
344
  padding: var(--rel-spacing-xl);
248
345
  }
249
346
 
347
+ .content.no-scroll {
348
+ overflow-y: visible;
349
+ }
350
+
351
+ :host([disabled]) .footer {
352
+ opacity: 0.5;
353
+ }
354
+
355
+ .loading-overlay {
356
+ position: absolute;
357
+ inset: 0;
358
+ z-index: 10;
359
+ display: flex;
360
+ align-items: center;
361
+ justify-content: center;
362
+ }
363
+
364
+ /* Scrim as a pseudo-element so its opacity doesn't dull the spinner. */
365
+ .loading-overlay::before {
366
+ content: "";
367
+ position: absolute;
368
+ inset: 0;
369
+ background: var(--rel-bg-surface);
370
+ opacity: 0.7;
371
+ }
372
+
373
+ .loading-overlay rel-progress {
374
+ position: relative;
375
+ }
376
+
250
377
  .footer {
251
378
  display: none;
252
379
  }
@@ -332,7 +459,7 @@ export class RelModal extends LitElement {
332
459
 
333
460
  // Move focus into the dialog — before this, a keyboard user had to Tab
334
461
  // through the entire page behind the overlay to reach the sheet.
335
- if (this.modal) {
462
+ if (this.modal && this.autoFocus) {
336
463
  this.updateComplete.then(() => {
337
464
  const panel = this.panelElement;
338
465
  if (!panel) return;
@@ -353,11 +480,13 @@ export class RelModal extends LitElement {
353
480
  private handleClose() {
354
481
  this.unlockScroll();
355
482
  document.removeEventListener("keydown", this.handleKeydown);
356
- if (this.previousFocus?.focus) {
483
+ if (this.restoreFocus && this.previousFocus?.focus) {
357
484
  this.updateComplete.then(() => {
358
485
  this.previousFocus?.focus();
359
486
  this.previousFocus = null;
360
487
  });
488
+ } else {
489
+ this.previousFocus = null;
361
490
  }
362
491
  this.dispatchEvent(
363
492
  new CustomEvent("rel-open-change", {
@@ -387,7 +516,7 @@ export class RelModal extends LitElement {
387
516
 
388
517
  private handleKeydown = (e: KeyboardEvent) => {
389
518
  if (!this.open) return;
390
- if (e.key === "Escape" && this.dismissible) {
519
+ if (e.key === "Escape" && this.dismissible && this.closeOnEscape && !this.disabled) {
391
520
  e.preventDefault();
392
521
  this.open = false;
393
522
  return;
@@ -399,23 +528,26 @@ export class RelModal extends LitElement {
399
528
  };
400
529
 
401
530
  private handleOverlayClick = (e: MouseEvent) => {
402
- if (!this.dismissible) return;
531
+ if (!this.dismissible || !this.closeOnBackdropClick || this.disabled || this.loading) return;
403
532
  if (e.target === this.overlayElement) {
404
533
  this.open = false;
405
534
  }
406
535
  };
407
536
 
408
537
  private handleCloseButtonClick() {
538
+ if (this.disabled || this.loading) return;
409
539
  this.open = false;
410
540
  }
411
541
 
412
542
  render() {
413
543
  const hasTitle = !!this.title;
414
- const hasDescription = !!this.description;
415
- const showHeader = hasTitle || hasDescription || this.dismissible;
544
+ const effectiveDescription = this.description || this.subtitle;
545
+ const hasDescription = !!effectiveDescription;
546
+ const showCloseX = this.dismissible && this.showCloseButton;
547
+ const showHeader = hasTitle || hasDescription || showCloseX;
416
548
 
417
549
  return html`
418
- <div class="modal-container ${this.open ? "open" : ""}">
550
+ <div class="modal-container ${this.open ? "open" : ""}" style="z-index: ${this.zIndex}">
419
551
  <div
420
552
  part="overlay"
421
553
  class="overlay ${!this.modal ? 'no-modal' : ''}"
@@ -425,19 +557,26 @@ export class RelModal extends LitElement {
425
557
  part="panel"
426
558
  class="panel"
427
559
  data-side="${this.side}"
560
+ data-size="${this.size}"
428
561
  role="${this.modal ? "dialog" : "region"}"
429
562
  aria-modal="${this.modal}"
563
+ aria-busy="${this.loading ? "true" : nothing}"
564
+ aria-labelledby="${hasTitle ? "modal-title" : nothing}"
565
+ aria-describedby="${hasDescription ? "modal-description" : nothing}"
566
+ aria-label="${hasTitle ? nothing : "Dialog"}"
430
567
  tabindex="-1">
431
568
 
432
- <div part="header" class="header ${!showHeader ? "empty" : ""}">
569
+ <div part="header" class="header ${!showHeader ? "empty" : ""}" ?inert=${this.loading}>
433
570
  <div class="header-text">
434
- ${hasTitle ? html`<h2 class="title">${this.title}</h2>` : ""}
435
- ${hasDescription ? html`<p class="description">${this.description}</p>` : ""}
571
+ ${hasTitle ? html`<h2 class="title" id="modal-title">${this.title}</h2>` : ""}
572
+ ${hasDescription ? html`<p class="description" id="modal-description">${effectiveDescription}</p>` : ""}
573
+ <slot name="header"></slot>
436
574
  </div>
437
- ${this.dismissible
575
+ ${showCloseX
438
576
  ? html`<button
439
577
  part="close-button"
440
578
  class="close-button"
579
+ ?disabled=${this.disabled}
441
580
  @click="${this.handleCloseButtonClick}"
442
581
  aria-label="Close">
443
582
  <rel-icon name="close" size="sm"></rel-icon>
@@ -445,13 +584,19 @@ export class RelModal extends LitElement {
445
584
  : ""}
446
585
  </div>
447
586
 
448
- <div part="content" class="content">
587
+ <div part="content" class="content ${this.scrollable ? "" : "no-scroll"}" ?inert=${this.loading}>
449
588
  <slot></slot>
450
589
  </div>
451
590
 
452
- <div part="footer" class="footer">
591
+ <div part="footer" class="footer" ?inert=${this.loading || this.disabled}>
453
592
  <slot name="footer"></slot>
454
593
  </div>
594
+
595
+ ${this.loading
596
+ ? html`<div part="loading" class="loading-overlay">
597
+ <rel-progress mode="circular-indeterminate" label="Loading"></rel-progress>
598
+ </div>`
599
+ : ""}
455
600
  </div>
456
601
  </div>
457
602
  `;
@@ -670,6 +670,7 @@ export class RelSidebar extends LitElement {
670
670
  data-side="${this.side}"
671
671
  style="--sidebar-width: ${this.getWidthValue()}"
672
672
  role="navigation"
673
+ aria-label="Sidebar"
673
674
  aria-hidden="${isOffcanvas && !this.isOpen}"
674
675
  tabindex="-1"
675
676
  >
@@ -778,11 +779,16 @@ export class RelSidebarTrigger extends LitElement {
778
779
  };
779
780
 
780
781
  render() {
782
+ const p = this.provider;
783
+ const expanded =
784
+ p && !p.isMobile && p.collapsible === "icon"
785
+ ? !this.isCollapsed
786
+ : this.isOpen;
781
787
  return html`
782
788
  <button
783
789
  @click="${this.handleClick}"
784
790
  type="button"
785
- aria-expanded="${this.isOpen}"
791
+ aria-expanded="${expanded}"
786
792
  aria-label="Toggle sidebar"
787
793
  >
788
794
  <slot>
@@ -16,6 +16,15 @@ export class RelSkeleton extends LitElement {
16
16
  protected override updated(changed: Map<string, unknown>) {
17
17
  if (changed.has("width") && this.width) this.style.width = this.width;
18
18
  if (changed.has("height") && this.height) this.style.height = this.height;
19
+ if (changed.has("loaded")) {
20
+ if (this.loaded) {
21
+ this.removeAttribute("role");
22
+ this.removeAttribute("aria-label");
23
+ } else {
24
+ this.setAttribute("role", "status");
25
+ this.setAttribute("aria-label", "Loading");
26
+ }
27
+ }
19
28
  }
20
29
 
21
30
  static styles = css`
@@ -1,4 +1,4 @@
1
- import { LitElement, html, css } from "lit";
1
+ import { LitElement, html, css, nothing } from "lit";
2
2
  import { customElement, property, state } from "lit/decorators.js";
3
3
 
4
4
  /**
@@ -34,6 +34,8 @@ export class RelSlider extends LitElement {
34
34
  /** Upper value when type="range" (attribute value-end). */
35
35
  @property({ type: Number, attribute: "value-end" }) valueEnd: number = 100;
36
36
 
37
+ @property({ type: String, attribute: "field-aria-label" }) fieldAriaLabel?: string;
38
+
37
39
  @state() private ticks: number[] = [];
38
40
 
39
41
  private _onChange: (value: number) => void = () => {};
@@ -619,7 +621,7 @@ export class RelSlider extends LitElement {
619
621
  <input
620
622
  class="slider-input"
621
623
  type="range"
622
- aria-label="Range start"
624
+ aria-label="${this.fieldAriaLabel ? `${this.fieldAriaLabel} minimum` : "Minimum value"}"
623
625
  .min="${String(this.min)}"
624
626
  .max="${String(this.max)}"
625
627
  .step="${String(this.step)}"
@@ -630,7 +632,7 @@ export class RelSlider extends LitElement {
630
632
  <input
631
633
  class="slider-input"
632
634
  type="range"
633
- aria-label="Range end"
635
+ aria-label="${this.fieldAriaLabel ? `${this.fieldAriaLabel} maximum` : "Maximum value"}"
634
636
  .min="${String(this.min)}"
635
637
  .max="${String(this.max)}"
636
638
  .step="${String(this.step)}"
@@ -653,6 +655,7 @@ export class RelSlider extends LitElement {
653
655
  <input
654
656
  class="slider-input"
655
657
  type="range"
658
+ aria-label="${this.fieldAriaLabel || nothing}"
656
659
  .min="${String(this.min)}"
657
660
  .max="${String(this.max)}"
658
661
  .step="${this.type === 'discrete' ? String(this.step) : '1'}"
@@ -1,6 +1,7 @@
1
1
  import { LitElement, html, css } from "lit";
2
2
  import { customElement, property, state } from "lit/decorators.js";
3
3
  import "@reliability-design/icons/rel-icon";
4
+ import { announce } from "../../utils/announce";
4
5
 
5
6
  export type SnackbarLines = "single" | "double";
6
7
  export type SnackbarType = "default" | "success" | "error" | "warning";
@@ -100,19 +101,19 @@ export class RelSnackbar extends LitElement {
100
101
  min-width: 0;
101
102
  }
102
103
 
103
- .snackbar-message {
104
+ .snackbar-message {
104
105
  margin-right: var(--rel-spacing-sm);
105
- flex: 1;
106
+ flex: 1;
106
107
 
107
- font-family: var(--rel-body-md-regular-font-family);
108
+ font-family: var(--rel-body-md-regular-font-family);
108
109
 
109
- font-weight: var(--rel-body-md-regular-font-weight);
110
+ font-weight: var(--rel-body-md-regular-font-weight);
110
111
 
111
- font-size: var(--rel-body-md-regular-font-size);
112
+ font-size: var(--rel-body-md-regular-font-size);
112
113
 
113
- line-height: var(--rel-body-md-regular-line-height);
114
+ line-height: var(--rel-body-md-regular-line-height);
114
115
 
115
- letter-spacing: var(--rel-body-md-regular-letter-spacing);
116
+ letter-spacing: var(--rel-body-md-regular-letter-spacing);
116
117
 
117
118
  }
118
119
 
@@ -137,17 +138,17 @@ export class RelSnackbar extends LitElement {
137
138
  .snackbar-action {
138
139
  background: none;
139
140
  border: none;
140
- color: var(--rel-text-static-light);
141
+ color: var(--rel-text-static-light);
141
142
  cursor: pointer;
142
143
  padding: 0;
143
144
  display: flex;
144
145
  align-items: center;
145
- transition: opacity 0.2s ease;
146
- font-family: var(--rel-action-md-font-family);
147
- font-weight: var(--rel-action-md-font-weight);
148
- font-size: var(--rel-action-md-font-size);
149
- line-height: var(--rel-action-md-line-height);
150
- letter-spacing: var(--rel-action-md-letter-spacing);
146
+ transition: opacity 0.2s ease;
147
+ font-family: var(--rel-action-md-font-family);
148
+ font-weight: var(--rel-action-md-font-weight);
149
+ font-size: var(--rel-action-md-font-size);
150
+ line-height: var(--rel-action-md-line-height);
151
+ letter-spacing: var(--rel-action-md-letter-spacing);
151
152
  }
152
153
 
153
154
  .snackbar-action:hover {
@@ -232,6 +233,9 @@ export class RelSnackbar extends LitElement {
232
233
  if ((changed.has("duration") || changed.has("longerAction")) && this.show) {
233
234
  this.setAutoDismiss();
234
235
  }
236
+ if ((changed.has("show") || changed.has("message")) && this.show) {
237
+ announce(this.message, this.type === "error" ? "assertive" : "polite");
238
+ }
235
239
  }
236
240
 
237
241
  disconnectedCallback() {
@@ -297,7 +301,7 @@ export class RelSnackbar extends LitElement {
297
301
  .join(" ");
298
302
 
299
303
  return html`
300
- <div class="${snackbarClasses}">
304
+ <div class="${snackbarClasses}" role="${this.type === "error" ? "alert" : "status"}">
301
305
  <div class="snackbar-body">
302
306
  ${this.type !== "default" ? this.renderIcon() : ""}
303
307
  <div part="label" class="snackbar-message">${this.message}</div>
@@ -313,7 +317,7 @@ export class RelSnackbar extends LitElement {
313
317
  : ""}
314
318
  ${this.iconClose
315
319
  ? html`
316
- <button part="close-button" class="snackbar-close" @click="${this.dismiss}">
320
+ <button part="close-button" class="snackbar-close" @click="${this.dismiss}" aria-label="Close">
317
321
  <rel-icon name="close" size="md"></rel-icon>
318
322
  </button>
319
323
  `