@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, state, query } from "lit/decorators.js";
3
3
  import "@reliability-design/icons/rel-icon";
4
4
  import "./rel-drawer-footer";
@@ -550,6 +550,9 @@ export class RelDrawer extends LitElement {
550
550
  style="${this.width ? `--drawer-width: ${this.width};` : ''}${this.maxHeight ? `--drawer-max-height: ${this.maxHeight};` : ''}${this.getPanelStyle()}"
551
551
  role="${this.modal ? "dialog" : "region"}"
552
552
  aria-modal="${this.modal}"
553
+ aria-labelledby="${hasTitle ? "drawer-title" : nothing}"
554
+ aria-describedby="${hasDescription ? "drawer-description" : nothing}"
555
+ aria-label="${hasTitle ? nothing : "Drawer"}"
553
556
  tabindex="-1">
554
557
  ${showHandleBar
555
558
  ? html`<div
@@ -564,10 +567,10 @@ export class RelDrawer extends LitElement {
564
567
  <div class="header-top">
565
568
  <div>
566
569
  ${hasTitle
567
- ? html`<h2 class="title">${this.title}</h2>`
570
+ ? html`<h2 class="title" id="drawer-title">${this.title}</h2>`
568
571
  : ""}
569
572
  ${hasDescription
570
- ? html`<p class="description">${this.description}</p>`
573
+ ? html`<p class="description" id="drawer-description">${this.description}</p>`
571
574
  : ""}
572
575
  </div>
573
576
  ${showCloseButton
@@ -450,6 +450,8 @@ export class RelMenu extends LitElement {
450
450
  this.type === "checkbox"
451
451
  ? html`<rel-checkbox
452
452
  class="menu-control"
453
+ inert
454
+ aria-hidden="true"
453
455
  label="${item.label}"
454
456
  status="${item.checked ? "checked" : "unchecked"}"
455
457
  state="${item.disabled ? "disabled" : "default"}"
@@ -457,6 +459,8 @@ export class RelMenu extends LitElement {
457
459
  : this.type === "select-option"
458
460
  ? html`<rel-radio
459
461
  class="menu-control"
462
+ inert
463
+ aria-hidden="true"
460
464
  .options="${[{ value: item.label, label: item.label, disabled: !!item.disabled }]}"
461
465
  .selectedValue="${item.active ? item.label : null}"
462
466
  ></rel-radio>`
@@ -475,6 +479,7 @@ export class RelMenu extends LitElement {
475
479
  class="rel-menu-item ${item.active ? "active" : ""} ${item.disabled ? "disabled" : ""}"
476
480
  role="${role}"
477
481
  aria-checked="${ariaChecked}"
482
+ aria-label="${this.type === "checkbox" || this.type === "select-option" ? item.label : nothing}"
478
483
  aria-disabled="${item.disabled ? "true" : nothing}"
479
484
  aria-haspopup="${hasSubmenu ? "menu" : nothing}"
480
485
  aria-expanded="${hasSubmenu ? String(!!item.open) : nothing}"
@@ -524,6 +529,7 @@ export class RelMenu extends LitElement {
524
529
  <div
525
530
  part="list" class="rel-menu ${this.type === "fixed" || this.type === "fit" ? this.type : ""}"
526
531
  role="menu"
532
+ aria-label="Menu"
527
533
  tabindex="0"
528
534
  aria-activedescendant="${activeDescendant}"
529
535
  @keydown="${this.onKeydown}"
@@ -1,5 +1,6 @@
1
- import { LitElement, html, css } from "lit";
1
+ import { LitElement, html, css, nothing } from "lit";
2
2
  import { customElement, property, state, query } from "lit/decorators.js";
3
+ import { announce } from "../../utils/announce";
3
4
 
4
5
  export type OtpType = "numeric" | "alphanumeric";
5
6
 
@@ -77,7 +78,7 @@ export class RelOtp extends LitElement {
77
78
  height: 48px;
78
79
  border: var(--rel-stroke-sm) solid var(--rel-border-default);
79
80
  border-radius: var(--rel-border-radius-sm);
80
- background: var(--rel-bg-neutral);
81
+ background: var(--rel-bg-neutral);
81
82
  text-align: center;
82
83
  color: var(--rel-text-default);
83
84
  outline: none;
@@ -230,6 +231,7 @@ export class RelOtp extends LitElement {
230
231
  composed: true,
231
232
  })
232
233
  );
234
+ announce("Code complete");
233
235
  }
234
236
  }
235
237
 
@@ -390,6 +392,13 @@ export class RelOtp extends LitElement {
390
392
 
391
393
  this.updateValue();
392
394
 
395
+ const filled = this.cells.filter((c) => c).length;
396
+ if (filled < this.length) {
397
+ announce(
398
+ `${filled} ${this.type === "alphanumeric" ? "characters" : "digits"} entered`
399
+ );
400
+ }
401
+
393
402
  // Focus next empty cell or last cell
394
403
  const nextEmptyIndex = this.cells.findIndex((c, i) => i >= startIndex && !c);
395
404
  if (nextEmptyIndex >= 0) {
@@ -480,6 +489,7 @@ export class RelOtp extends LitElement {
480
489
  ?readonly="${this.readonly}"
481
490
  ?required="${this.required && index === 0}"
482
491
  aria-label="${this.getAriaLabel(index)}"
492
+ aria-invalid="${this.invalid ? "true" : nothing}"
483
493
  @pointerdown="${this.handlePointerDown}"
484
494
  @input="${this.handleInput}"
485
495
  @keydown="${this.handleKeyDown}"
@@ -1,5 +1,6 @@
1
1
  import { LitElement, html, css, nothing } from "lit";
2
2
  import { customElement, property, state } from "lit/decorators.js";
3
+ import { announce } from "../../utils/announce";
3
4
 
4
5
  @customElement("rel-pagination")
5
6
  export class RelPagination extends LitElement {
@@ -390,6 +391,18 @@ export class RelPagination extends LitElement {
390
391
  const total = this.computedTotalPages;
391
392
  if (this.disabled || newPage < 1 || newPage > total || newPage === this.activePage) return;
392
393
  if (this.page === undefined) this.currentPage = newPage;
394
+ announce(`Page ${newPage} of ${total}`);
395
+ const focused = this.shadowRoot?.activeElement as HTMLButtonElement | null;
396
+ if (focused?.classList.contains("pag-nav-btn")) {
397
+ this.updateComplete.then(() => {
398
+ if (focused.isConnected && focused.disabled) {
399
+ const sibling = focused.parentElement?.querySelector<HTMLButtonElement>(
400
+ ".pag-nav-btn:not(:disabled)"
401
+ );
402
+ sibling?.focus();
403
+ }
404
+ });
405
+ }
393
406
  this.dispatchEvent(
394
407
  new CustomEvent("rel-change", {
395
408
  detail: { page: newPage },
@@ -5,6 +5,12 @@ import { getDeepActiveElement, getFocusableElements } from "../../utils/focus.js
5
5
  type SideType = "top" | "right" | "bottom" | "left";
6
6
  type AlignType = "start" | "center" | "end";
7
7
 
8
+ /** Every currently-open popover on the page (module-level, shared by all
9
+ * instances). Used so opening one popover light-dismisses any other open
10
+ * popover it is not nested inside — regardless of whether it was opened by
11
+ * pointer, keyboard, or programmatically. */
12
+ const openPopovers = new Set<RelPopover>();
13
+
8
14
  @customElement("rel-popover")
9
15
  export class RelPopover extends LitElement {
10
16
  /* =======================
@@ -182,6 +188,9 @@ export class RelPopover extends LitElement {
182
188
  private onOpen() {
183
189
  this.returnFocusEl = getDeepActiveElement() ?? this.triggerEl;
184
190
 
191
+ this.closeOtherPopovers();
192
+ openPopovers.add(this);
193
+
185
194
  this.addGlobalListeners();
186
195
  this.showNativePopover();
187
196
 
@@ -192,11 +201,29 @@ export class RelPopover extends LitElement {
192
201
  }
193
202
 
194
203
  private onClose() {
204
+ openPopovers.delete(this);
195
205
  this.removeGlobalListeners();
196
206
  this.hideNativePopover();
197
207
  this.restoreFocus();
198
208
  }
199
209
 
210
+ /**
211
+ * Light-dismiss every other open popover that isn't an ancestor of this
212
+ * one (nested popovers keep their parent open). This is what the outside-
213
+ * pointerdown listener would do for a click on our trigger — except the
214
+ * trigger handler stops that pointerdown in the capture phase, and a
215
+ * keyboard (Enter/Space) or programmatic open never produces one at all.
216
+ * Honors each popover's own `closeOnOutsideClick` setting.
217
+ */
218
+ private closeOtherPopovers() {
219
+ for (const other of openPopovers) {
220
+ if (other === this) continue;
221
+ if (!other.closeOnOutsideClick) continue;
222
+ if (other.contains(this)) continue;
223
+ other.setOpen(false);
224
+ }
225
+ }
226
+
200
227
  /* =======================
201
228
  * TOP LAYER (native Popover API)
202
229
  *
@@ -234,7 +261,11 @@ export class RelPopover extends LitElement {
234
261
 
235
262
  private addGlobalListeners() {
236
263
  if (this.closeOnOutsideClick) {
237
- document.addEventListener("pointerdown", this.onOutsidePointerDown);
264
+ // Capture phase: another popover's trigger handler (onHostPointerDown)
265
+ // calls stopPropagation() in its own capture listener, which would
266
+ // otherwise swallow the event before a bubble-phase document listener
267
+ // ever saw it — leaving this popover open alongside the new one.
268
+ document.addEventListener("pointerdown", this.onOutsidePointerDown, true);
238
269
  }
239
270
 
240
271
  if (this.closeOnEscape || this.trapFocus) {
@@ -246,7 +277,7 @@ export class RelPopover extends LitElement {
246
277
  }
247
278
 
248
279
  private removeGlobalListeners() {
249
- document.removeEventListener("pointerdown", this.onOutsidePointerDown);
280
+ document.removeEventListener("pointerdown", this.onOutsidePointerDown, true);
250
281
  document.removeEventListener("keydown", this.onKeyDown);
251
282
  window.removeEventListener("resize", this.updatePosition);
252
283
  window.removeEventListener("scroll", this.updatePosition, true);
@@ -15,6 +15,9 @@ export class RelProgress extends LitElement {
15
15
  @property({ type: Number })
16
16
  value = 0;
17
17
 
18
+ @property({ type: String })
19
+ label = "";
20
+
18
21
  private radius = 15.9155;
19
22
  private circumference = 2 * Math.PI * this.radius;
20
23
 
@@ -155,9 +158,14 @@ export class RelProgress extends LitElement {
155
158
  return Math.min(100, Math.max(0, this.value));
156
159
  }
157
160
 
161
+ private get accessibleName() {
162
+ if (this.label) return this.label;
163
+ return this.mode.endsWith("indeterminate") ? "Loading" : "Progress";
164
+ }
165
+
158
166
  private renderLinearDeterminate() {
159
167
  return html`
160
- <div part="track" class="linear-track" role="progressbar"
168
+ <div part="track" class="linear-track" role="progressbar" aria-label="${this.accessibleName}"
161
169
  aria-valuenow="${this.safeValue}" aria-valuemin="0" aria-valuemax="100">
162
170
  <div part="indicator" class="linear-bar determinate" style="width: ${this.safeValue}%"></div>
163
171
  </div>
@@ -166,7 +174,7 @@ export class RelProgress extends LitElement {
166
174
 
167
175
  private renderLinearIndeterminate() {
168
176
  return html`
169
- <div part="track" class="linear-track" role="progressbar">
177
+ <div part="track" class="linear-track" role="progressbar" aria-label="${this.accessibleName}">
170
178
  <div part="indicator" class="linear-bar indeterminate"></div>
171
179
  </div>
172
180
  `;
@@ -177,7 +185,8 @@ export class RelProgress extends LitElement {
177
185
  this.circumference - (this.safeValue / 100) * this.circumference;
178
186
 
179
187
  return html`
180
- <div class="circular">
188
+ <div class="circular" role="progressbar" aria-label="${this.accessibleName}"
189
+ aria-valuenow="${this.safeValue}" aria-valuemin="0" aria-valuemax="100">
181
190
  <svg viewBox="0 0 36 36" class="circular-svg">
182
191
  <circle class="circular-bg" cx="18" cy="18" r="${this.radius}" />
183
192
  <circle
@@ -195,7 +204,7 @@ export class RelProgress extends LitElement {
195
204
 
196
205
  private renderCircularIndeterminate() {
197
206
  return html`
198
- <div class="circular">
207
+ <div class="circular" role="progressbar" aria-label="${this.accessibleName}">
199
208
  <svg viewBox="0 0 50 50" class="spinner">
200
209
  <circle
201
210
  class="spinner-path"
@@ -61,9 +61,11 @@ A radio button group component that allows users to select a single option from
61
61
  | `options` | `RadioOption[]` | `[]` | Array of radio options (see RadioOption interface below) |
62
62
  | `name` | `string` | `"radio-group"` | Name attribute for the radio group (for form grouping) |
63
63
  | `orientation` | `"vertical" \| "horizontal"` | `"vertical"` | Layout axis: `"vertical"` stacks options in a column; `"horizontal"` lays them out in a wrapping row (reflected attribute) |
64
- | `size` | `"small" \| "medium"` | `"medium"` | Control scale: `"medium"` = 20px circle with body-lg labels; `"small"` = 16px circle with body-md labels (reflected attribute) |
64
+ | `size` | `"small" \| "medium" \| "large"` | `"medium"` | Control scale (Figma Size L/M/S): `"medium"` = 20px control with body-lg labels; `"large"` = 24px control with body-xl labels; `"small"` = 20px control with body-md labels — S and M share the control size, only typography steps down (reflected attribute) |
65
+ | `density` | `"compact" \| "comfortable"` | `"compact"` | Supporting-text layout (Figma Density): `"compact"` puts the supporting text on its own line under the row, indented past the control; `"comfortable"` stacks label + supporting text beside the control (reflected attribute) |
66
+ | `alignment` | `"leading" \| "trailing"` | `"leading"` | Control position (Figma Layout): `"trailing"` pushes the control to the far edge of the row and always renders comfortable density — the spec has no compact trailing layout (reflected attribute) |
65
67
  | `selectedValue` | `string \| null` | `null` | Currently selected option value (reflected attribute) |
66
- | `disabled` | `boolean` | `false` | Disables the entire radio group |
68
+ | `disabled` | `boolean` | `false` | Disables the entire radio group (renders at 40% opacity) |
67
69
 
68
70
  ### RadioOption Interface
69
71
  ```typescript
@@ -72,7 +74,8 @@ interface RadioOption {
72
74
  label: string; // Display label
73
75
  supportingText?: string; // Optional helper text below the label
74
76
  disabled?: boolean; // Disable individual option
75
- error?: boolean; // Show error state for individual option
77
+ icon?: string; // rel-icon name rendered before the label
78
+ avatar?: string; // 1–2 initials rendered as a circular chip before the label (ignored when icon is set)
76
79
  }
77
80
  ```
78
81