@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
@@ -1,8 +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";
5
+ import { getDeepActiveElement, getFocusableElements, trapTabKey } from "../../utils/focus.js";
4
6
 
5
7
  export type ModalSide = "top" | "bottom" | "center";
8
+ export type ModalSize = "sm" | "md" | "lg" | "xl" | "fullscreen";
6
9
 
7
10
  @customElement("rel-modal")
8
11
  export class RelModal extends LitElement {
@@ -10,9 +13,37 @@ export class RelModal extends LitElement {
10
13
  @property({ type: String }) side: ModalSide = "bottom";
11
14
  @property({ type: String }) title = "";
12
15
  @property({ type: String }) description = "";
16
+ /** Alias for `description`; rendered only when `description` is empty. */
17
+ @property({ type: String }) subtitle = "";
13
18
  @property({ type: Boolean }) dismissible = true;
14
19
  @property({ type: Boolean }) modal = true;
15
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
+
16
47
  /**
17
48
  * Figma "Breakpoint" axis. responsive (default) collapses to a full-width
18
49
  * bottom sheet below 480px; web keeps the fixed desktop panel at every size.
@@ -21,6 +52,7 @@ export class RelModal extends LitElement {
21
52
  breakpoint: "responsive" | "web" = "responsive";
22
53
 
23
54
  @query(".overlay") private overlayElement!: HTMLElement;
55
+ @query(".panel") private panelElement!: HTMLElement;
24
56
 
25
57
  private previousFocus: HTMLElement | null = null;
26
58
  private originalBodyOverflow = "";
@@ -84,16 +116,30 @@ export class RelModal extends LitElement {
84
116
  overflow: hidden;
85
117
  outline: none;
86
118
  font-family: var(--rel-fonts-font-family);
87
- 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);
88
122
  left: 50%;
89
123
  transition: transform var(--modal-transition-duration) var(--modal-transition-timing),
90
124
  opacity var(--modal-transition-duration) var(--modal-transition-timing);
91
125
  }
92
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
+
93
139
  .panel[data-side="bottom"] {
94
140
  bottom: 48px;
95
141
  left: 50%;
96
- width: clamp(360px, 49vw, 708px);
142
+ width: var(--modal-width);
97
143
  max-height: calc(100vh - 96px);
98
144
  border-radius: var(--rel-border-radius-sm);
99
145
  transform: translateX(-50%) translateY(calc(100% + 48px));
@@ -106,7 +152,7 @@ export class RelModal extends LitElement {
106
152
  .panel[data-side="top"] {
107
153
  top: 48px;
108
154
  left: 50%;
109
- width: clamp(360px, 49vw, 708px);
155
+ width: var(--modal-width);
110
156
  max-height: calc(100vh - 96px);
111
157
  border-radius: var(--rel-border-radius-sm);
112
158
  transform: translateX(-50%) translateY(calc(-100% - 48px));
@@ -119,7 +165,7 @@ export class RelModal extends LitElement {
119
165
  .panel[data-side="center"] {
120
166
  top: 50%;
121
167
  left: 50%;
122
- width: clamp(360px, 49vw, 708px);
168
+ width: var(--modal-width);
123
169
  max-height: calc(100vh - 96px);
124
170
  border-radius: var(--rel-border-radius-sm);
125
171
  transform: translate(-50%, -50%) scale(0.96);
@@ -157,6 +203,50 @@ export class RelModal extends LitElement {
157
203
  }
158
204
  }
159
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
+
160
250
  .header {
161
251
  display: flex;
162
252
  align-items: flex-start;
@@ -239,12 +329,51 @@ export class RelModal extends LitElement {
239
329
  outline-offset: 2px;
240
330
  }
241
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
+
242
341
  .content {
243
342
  flex: 1;
244
343
  overflow-y: auto;
245
344
  padding: var(--rel-spacing-xl);
246
345
  }
247
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
+
248
377
  .footer {
249
378
  display: none;
250
379
  }
@@ -322,9 +451,23 @@ export class RelModal extends LitElement {
322
451
  }
323
452
 
324
453
  private handleOpen() {
325
- this.previousFocus = document.activeElement as HTMLElement;
454
+ // Deep-resolved — document.activeElement retargets to shadow hosts whose
455
+ // .focus() may be a no-op, silently dropping the restore.
456
+ this.previousFocus = getDeepActiveElement();
326
457
  this.lockScroll();
327
458
  document.addEventListener("keydown", this.handleKeydown);
459
+
460
+ // Move focus into the dialog — before this, a keyboard user had to Tab
461
+ // through the entire page behind the overlay to reach the sheet.
462
+ if (this.modal && this.autoFocus) {
463
+ this.updateComplete.then(() => {
464
+ const panel = this.panelElement;
465
+ if (!panel) return;
466
+ const focusTarget = getFocusableElements(panel)[0];
467
+ if (focusTarget) focusTarget.focus();
468
+ else panel.focus();
469
+ });
470
+ }
328
471
  this.dispatchEvent(
329
472
  new CustomEvent("rel-open-change", {
330
473
  detail: true,
@@ -337,11 +480,13 @@ export class RelModal extends LitElement {
337
480
  private handleClose() {
338
481
  this.unlockScroll();
339
482
  document.removeEventListener("keydown", this.handleKeydown);
340
- if (this.previousFocus?.focus) {
483
+ if (this.restoreFocus && this.previousFocus?.focus) {
341
484
  this.updateComplete.then(() => {
342
485
  this.previousFocus?.focus();
343
486
  this.previousFocus = null;
344
487
  });
488
+ } else {
489
+ this.previousFocus = null;
345
490
  }
346
491
  this.dispatchEvent(
347
492
  new CustomEvent("rel-open-change", {
@@ -371,30 +516,38 @@ export class RelModal extends LitElement {
371
516
 
372
517
  private handleKeydown = (e: KeyboardEvent) => {
373
518
  if (!this.open) return;
374
- if (e.key === "Escape" && this.dismissible) {
519
+ if (e.key === "Escape" && this.dismissible && this.closeOnEscape && !this.disabled) {
375
520
  e.preventDefault();
376
521
  this.open = false;
522
+ return;
523
+ }
524
+ // aria-modal promised containment; actually deliver it.
525
+ if (e.key === "Tab" && this.modal && this.panelElement) {
526
+ trapTabKey(e, this.panelElement);
377
527
  }
378
528
  };
379
529
 
380
530
  private handleOverlayClick = (e: MouseEvent) => {
381
- if (!this.dismissible) return;
531
+ if (!this.dismissible || !this.closeOnBackdropClick || this.disabled || this.loading) return;
382
532
  if (e.target === this.overlayElement) {
383
533
  this.open = false;
384
534
  }
385
535
  };
386
536
 
387
537
  private handleCloseButtonClick() {
538
+ if (this.disabled || this.loading) return;
388
539
  this.open = false;
389
540
  }
390
541
 
391
542
  render() {
392
543
  const hasTitle = !!this.title;
393
- const hasDescription = !!this.description;
394
- 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;
395
548
 
396
549
  return html`
397
- <div class="modal-container ${this.open ? "open" : ""}">
550
+ <div class="modal-container ${this.open ? "open" : ""}" style="z-index: ${this.zIndex}">
398
551
  <div
399
552
  part="overlay"
400
553
  class="overlay ${!this.modal ? 'no-modal' : ''}"
@@ -404,19 +557,26 @@ export class RelModal extends LitElement {
404
557
  part="panel"
405
558
  class="panel"
406
559
  data-side="${this.side}"
560
+ data-size="${this.size}"
407
561
  role="${this.modal ? "dialog" : "region"}"
408
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"}"
409
567
  tabindex="-1">
410
568
 
411
- <div part="header" class="header ${!showHeader ? "empty" : ""}">
569
+ <div part="header" class="header ${!showHeader ? "empty" : ""}" ?inert=${this.loading}>
412
570
  <div class="header-text">
413
- ${hasTitle ? html`<h2 class="title">${this.title}</h2>` : ""}
414
- ${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>
415
574
  </div>
416
- ${this.dismissible
575
+ ${showCloseX
417
576
  ? html`<button
418
577
  part="close-button"
419
578
  class="close-button"
579
+ ?disabled=${this.disabled}
420
580
  @click="${this.handleCloseButtonClick}"
421
581
  aria-label="Close">
422
582
  <rel-icon name="close" size="sm"></rel-icon>
@@ -424,13 +584,19 @@ export class RelModal extends LitElement {
424
584
  : ""}
425
585
  </div>
426
586
 
427
- <div part="content" class="content">
587
+ <div part="content" class="content ${this.scrollable ? "" : "no-scroll"}" ?inert=${this.loading}>
428
588
  <slot></slot>
429
589
  </div>
430
590
 
431
- <div part="footer" class="footer">
591
+ <div part="footer" class="footer" ?inert=${this.loading || this.disabled}>
432
592
  <slot name="footer"></slot>
433
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
+ : ""}
434
600
  </div>
435
601
  </div>
436
602
  `;
@@ -1,6 +1,8 @@
1
1
  import { LitElement, html, css, nothing } from "lit";
2
2
  import { customElement, property, state, query } from "lit/decorators.js";
3
3
  import "@reliability-design/icons/rel-icon";
4
+ import { matchesShortcut, isEditableTarget } from "../../utils/shortcuts.js";
5
+ import { getDeepActiveElement } from "../../utils/focus.js";
4
6
 
5
7
  export type SidebarVariant = "sidebar" | "floating" | "inset";
6
8
  export type SidebarSide = "left" | "right";
@@ -11,12 +13,12 @@ export type SidebarChangeReason = "trigger" | "rail" | "overlay" | "escape" | "s
11
13
  // ============================================================================
12
14
  // rel-sidebar-provider (Root State Manager)
13
15
  // ============================================================================
14
- /**
15
- * @deprecated The rel-sidebar family has no Figma design in RelDS 2.0 and is
16
- * excluded from the parity contract. Compose sidebars from rel-drawer,
17
- * rel-menu and rel-resizable instead. It remains exported for compatibility
18
- * but will be removed in a future major version.
19
- */
16
+ /**
17
+ * @deprecated The rel-sidebar family has no Figma design in RelDS 2.0 and is
18
+ * excluded from the parity contract. Compose sidebars from rel-drawer,
19
+ * rel-menu and rel-resizable instead. It remains exported for compatibility
20
+ * but will be removed in a future major version.
21
+ */
20
22
  @customElement("rel-sidebar-provider")
21
23
  export class RelSidebarProvider extends LitElement {
22
24
  @property({ type: Boolean, reflect: true }) open?: boolean;
@@ -206,17 +208,19 @@ export class RelSidebarProvider extends LitElement {
206
208
  private handleKeyDown = (e: KeyboardEvent) => {
207
209
  if (!this.keyboardShortcut) return;
208
210
 
209
- // Ctrl/Cmd + B
210
- if ((e.ctrlKey || e.metaKey) && e.key === "b") {
211
- e.preventDefault();
211
+ // Ctrl+B (Cmd+B on Apple) via the shared matcher — stricter than the old
212
+ // hand-rolled check, which also fired on Ctrl+Shift+B.
213
+ if (!matchesShortcut(e, "mod+b")) return;
214
+ // Leave the combo alone in text-editing contexts (bold in editors).
215
+ if (isEditableTarget(e)) return;
216
+ e.preventDefault();
212
217
 
213
- if (this.isMobile || this.collapsible === "offcanvas") {
214
- // Toggle open state
215
- this.setOpen(!this.activeOpen, "shortcut");
216
- } else if (this.collapsible === "icon") {
217
- // Toggle collapsed state
218
- this.setCollapsed(!this.activeCollapsed, "shortcut");
219
- }
218
+ if (this.isMobile || this.collapsible === "offcanvas") {
219
+ // Toggle open state
220
+ this.setOpen(!this.activeOpen, "shortcut");
221
+ } else if (this.collapsible === "icon") {
222
+ // Toggle collapsed state
223
+ this.setCollapsed(!this.activeCollapsed, "shortcut");
220
224
  }
221
225
  };
222
226
 
@@ -529,7 +533,7 @@ export class RelSidebar extends LitElement {
529
533
  }
530
534
 
531
535
  private setupFocusTrap() {
532
- this.triggerElement = document.activeElement as HTMLElement;
536
+ this.triggerElement = getDeepActiveElement();
533
537
  document.addEventListener("keydown", this.handleKeydown);
534
538
 
535
539
  // Focus first focusable element or container
@@ -666,6 +670,7 @@ export class RelSidebar extends LitElement {
666
670
  data-side="${this.side}"
667
671
  style="--sidebar-width: ${this.getWidthValue()}"
668
672
  role="navigation"
673
+ aria-label="Sidebar"
669
674
  aria-hidden="${isOffcanvas && !this.isOpen}"
670
675
  tabindex="-1"
671
676
  >
@@ -774,11 +779,16 @@ export class RelSidebarTrigger extends LitElement {
774
779
  };
775
780
 
776
781
  render() {
782
+ const p = this.provider;
783
+ const expanded =
784
+ p && !p.isMobile && p.collapsible === "icon"
785
+ ? !this.isCollapsed
786
+ : this.isOpen;
777
787
  return html`
778
788
  <button
779
789
  @click="${this.handleClick}"
780
790
  type="button"
781
- aria-expanded="${this.isOpen}"
791
+ aria-expanded="${expanded}"
782
792
  aria-label="Toggle sidebar"
783
793
  >
784
794
  <slot>
@@ -888,26 +898,26 @@ export class RelSidebarGroup extends LitElement {
888
898
  export class RelSidebarGroupLabel extends LitElement {
889
899
  static styles = css`
890
900
  :host {
891
- display: block;
901
+ display: block;
892
902
  color: var(--rel-text-placeholder);
893
- text-transform: uppercase;
903
+ text-transform: uppercase;
894
904
  padding: var(--rel-spacing-xs, 8px) var(--rel-spacing-sm, 12px) var(--rel-spacing-2xs, 4px);
895
- margin-bottom: var(--rel-spacing-2xs, 4px);
896
- font-family: var(--rel-body-xs-semibold-font-family);
897
- font-weight: var(--rel-body-xs-semibold-font-weight);
898
- font-size: var(--rel-body-xs-semibold-font-size);
899
- line-height: var(--rel-body-xs-semibold-line-height);
900
- letter-spacing: var(--rel-body-xs-semibold-letter-spacing);
905
+ margin-bottom: var(--rel-spacing-2xs, 4px);
906
+ font-family: var(--rel-body-xs-semibold-font-family);
907
+ font-weight: var(--rel-body-xs-semibold-font-weight);
908
+ font-size: var(--rel-body-xs-semibold-font-size);
909
+ line-height: var(--rel-body-xs-semibold-line-height);
910
+ letter-spacing: var(--rel-body-xs-semibold-letter-spacing);
901
911
  }
902
912
 
903
913
  :host(.collapsed) {
904
- text-align: center;
905
- padding: var(--rel-spacing-xs, 8px) var(--rel-spacing-2xs, 4px) var(--rel-spacing-2xs, 4px);
906
- font-family: var(--rel-body-xs-regular-font-family);
907
- font-weight: var(--rel-body-xs-regular-font-weight);
908
- font-size: var(--rel-body-xs-regular-font-size);
909
- line-height: var(--rel-body-xs-regular-line-height);
910
- letter-spacing: var(--rel-body-xs-regular-letter-spacing);
914
+ text-align: center;
915
+ padding: var(--rel-spacing-xs, 8px) var(--rel-spacing-2xs, 4px) var(--rel-spacing-2xs, 4px);
916
+ font-family: var(--rel-body-xs-regular-font-family);
917
+ font-weight: var(--rel-body-xs-regular-font-weight);
918
+ font-size: var(--rel-body-xs-regular-font-size);
919
+ line-height: var(--rel-body-xs-regular-line-height);
920
+ letter-spacing: var(--rel-body-xs-regular-letter-spacing);
911
921
  }
912
922
  `;
913
923
 
@@ -1023,16 +1033,16 @@ export class RelSidebarMenuButton extends LitElement {
1023
1033
  background: none;
1024
1034
  border: none;
1025
1035
  border-radius: var(--rel-border-radius-sm, 8px);
1026
- cursor: pointer;
1036
+ cursor: pointer;
1027
1037
  color: var(--rel-text-default);
1028
1038
  transition: background 0.15s ease, color 0.15s ease;
1029
1039
  text-align: left;
1030
- position: relative;
1031
- font-family: var(--rel-body-md-medium-font-family);
1032
- font-weight: var(--rel-body-md-medium-font-weight);
1033
- font-size: var(--rel-body-md-medium-font-size);
1034
- line-height: var(--rel-body-md-medium-line-height);
1035
- letter-spacing: var(--rel-body-md-medium-letter-spacing);
1040
+ position: relative;
1041
+ font-family: var(--rel-body-md-medium-font-family);
1042
+ font-weight: var(--rel-body-md-medium-font-weight);
1043
+ font-size: var(--rel-body-md-medium-font-size);
1044
+ line-height: var(--rel-body-md-medium-line-height);
1045
+ letter-spacing: var(--rel-body-md-medium-letter-spacing);
1036
1046
  }
1037
1047
 
1038
1048
  button:hover:not([disabled]) {
@@ -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
  `