@reliability-design/web 1.4.4 → 1.4.5

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 (39) hide show
  1. package/dist/components/rel-accordion/rel-accordion.ts +13 -1
  2. package/dist/components/rel-button/rel-button.ts +18 -2
  3. package/dist/components/rel-calendar/rel-calendar.ts +23 -1
  4. package/dist/components/rel-carousel/rel-carousel.ts +20 -0
  5. package/dist/components/rel-checkbox/rel-checkbox.ts +4 -1
  6. package/dist/components/rel-chip/rel-chip.ts +29 -0
  7. package/dist/components/rel-date-picker/rel-date-picker.ts +156 -13
  8. package/dist/components/rel-drawer/rel-drawer.ts +22 -58
  9. package/dist/components/rel-menu/rel-menu.ts +124 -15
  10. package/dist/components/rel-menubar/rel-menubar.ts +221 -63
  11. package/dist/components/rel-nav-menu/rel-nav-menu.ts +39 -3
  12. package/dist/components/rel-otp/rel-otp.ts +3 -3
  13. package/dist/components/rel-popover/rel-popover.ts +31 -3
  14. package/dist/components/rel-range-picker/rel-range-picker.ts +134 -1
  15. package/dist/components/rel-resizable/rel-resizable-group.ts +20 -0
  16. package/dist/components/rel-segmented-button/rel-segmented-button.ts +50 -12
  17. package/dist/components/rel-select/rel-select.ts +159 -11
  18. package/dist/components/rel-sheet/rel-sheet.ts +22 -1
  19. package/dist/components/rel-sidebar/rel-sidebar.ts +43 -39
  20. package/dist/components/rel-stepper/rel-stepper.ts +44 -1
  21. package/dist/components/rel-tab/rel-tab.ts +32 -7
  22. package/dist/components/rel-time-picker/rel-time-picker.ts +338 -206
  23. package/dist/components/rel-tooltip/rel-tooltip.ts +30 -0
  24. package/dist/components/rel-topbar/rel-topbar-actions.ts +1 -1
  25. package/dist/components/rel-topbar/rel-topbar-brand.ts +1 -1
  26. package/dist/components/rel-topbar/rel-topbar-dropdown-item.ts +1 -1
  27. package/dist/components/rel-topbar/rel-topbar-dropdown.ts +1 -1
  28. package/dist/components/rel-topbar/rel-topbar-item.ts +318 -319
  29. package/dist/components/rel-topbar/rel-topbar-mobile-panel.ts +1 -1
  30. package/dist/components/rel-topbar/rel-topbar-profile.ts +224 -224
  31. package/dist/components/rel-topbar/rel-topbar-utilities.ts +1 -1
  32. package/dist/components/rel-topbar/rel-topbar.ts +440 -440
  33. package/dist/index.d.mts +225 -17
  34. package/dist/index.d.ts +225 -17
  35. package/dist/index.js +1468 -344
  36. package/dist/index.js.map +1 -1
  37. package/dist/index.mjs +1721 -607
  38. package/dist/index.mjs.map +1 -1
  39. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { LitElement, html } from "lit";
1
+ import { LitElement, html, nothing } from "lit";
2
2
  import { customElement, property, state } from "lit/decorators.js";
3
3
  import "@reliability-design/icons/rel-icon";
4
4
  import "../rel-checkbox/rel-checkbox.js";
@@ -38,8 +38,13 @@ export class RelMenu extends LitElement {
38
38
  @property({ type: Array }) items: MenuItem[] = [];
39
39
 
40
40
  @state() private focusedIndex: number = 0;
41
+ /** Index inside the open submenu that virtual focus sits on; -1 = main list. */
42
+ @state() private subFocusedIndex: number = -1;
41
43
  @state() private _items: MenuItem[] = [];
42
44
 
45
+ /** Stable id prefix for aria-activedescendant targets. */
46
+ private menuId = `rel-menu-${Math.random().toString(36).slice(2)}`;
47
+
43
48
  public static readonly _css = `
44
49
  :host {
45
50
  color: var(--rel-text-default);
@@ -122,7 +127,10 @@ export class RelMenu extends LitElement {
122
127
  cursor: not-allowed;
123
128
  }
124
129
 
125
- .rel-menu-item[data-focused="true"] {
130
+ /* [data-focused] (presence, not ="true"): the ?data-focused boolean
131
+ binding renders the attribute with an EMPTY value, so a ="true"
132
+ selector never matches. */
133
+ .rel-menu-item[data-focused] {
126
134
  outline: 2px solid var(--rel-border-focus);
127
135
  outline-offset: -2px;
128
136
  }
@@ -235,6 +243,7 @@ export class RelMenu extends LitElement {
235
243
  // click that opened it. composedPath pierces shadow boundaries.
236
244
  if (!e.composedPath().includes(this)) {
237
245
  this.lockedIndex = -1;
246
+ this.subFocusedIndex = -1;
238
247
  this._items = this._items.map(i => ({ ...i, open: false }));
239
248
  }
240
249
  };
@@ -319,38 +328,119 @@ export class RelMenu extends LitElement {
319
328
  );
320
329
  }
321
330
 
331
+ /** Keyboard-open locks the submenu (no hover-out to close it) and moves
332
+ * virtual focus onto its first entry. */
333
+ private openSubmenuAt(index: number) {
334
+ this.lockedIndex = index;
335
+ this.subFocusedIndex = 0;
336
+ this._items = this._items.map((i, idx) => ({ ...i, open: idx === index }));
337
+ }
338
+
339
+ private closeSubmenus() {
340
+ this.lockedIndex = -1;
341
+ this.subFocusedIndex = -1;
342
+ this._items = this._items.map(i => ({ ...i, open: false }));
343
+ }
344
+
322
345
  private onKeydown(ev: KeyboardEvent) {
323
346
  const last = this._items.length - 1;
347
+ const focusedItem = this._items[this.focusedIndex];
348
+
349
+ // ── Virtual focus is inside an open submenu ──
350
+ if (
351
+ this.subFocusedIndex >= 0 &&
352
+ this.type === "arrow" &&
353
+ focusedItem?.open &&
354
+ focusedItem.submenu?.length
355
+ ) {
356
+ const subLast = focusedItem.submenu.length - 1;
357
+ switch (ev.key) {
358
+ case "ArrowDown":
359
+ this.subFocusedIndex = Math.min(subLast, this.subFocusedIndex + 1);
360
+ ev.preventDefault();
361
+ return;
362
+ case "ArrowUp":
363
+ this.subFocusedIndex = Math.max(0, this.subFocusedIndex - 1);
364
+ ev.preventDefault();
365
+ return;
366
+ case "Home":
367
+ this.subFocusedIndex = 0;
368
+ ev.preventDefault();
369
+ return;
370
+ case "End":
371
+ this.subFocusedIndex = subLast;
372
+ ev.preventDefault();
373
+ return;
374
+ case "ArrowLeft":
375
+ case "Escape":
376
+ // Close only the submenu; don't let Escape also dismiss an
377
+ // enclosing popover in the same keystroke.
378
+ this.closeSubmenus();
379
+ ev.preventDefault();
380
+ ev.stopPropagation();
381
+ return;
382
+ case "Enter":
383
+ case " ":
384
+ this.onSubClick(this.focusedIndex, this.subFocusedIndex);
385
+ ev.preventDefault();
386
+ return;
387
+ default:
388
+ return;
389
+ }
390
+ }
324
391
 
392
+ // ── Virtual focus is in the main list ──
325
393
  if (ev.key === "ArrowDown") {
326
394
  this.focusedIndex = Math.min(last, this.focusedIndex + 1);
327
395
  ev.preventDefault();
328
396
  } else if (ev.key === "ArrowUp") {
329
397
  this.focusedIndex = Math.max(0, this.focusedIndex - 1);
330
398
  ev.preventDefault();
399
+ } else if (ev.key === "Home") {
400
+ this.focusedIndex = 0;
401
+ ev.preventDefault();
402
+ } else if (ev.key === "End") {
403
+ this.focusedIndex = last;
404
+ ev.preventDefault();
331
405
  } else if (ev.key === "ArrowRight") {
332
406
  const item = this._items[this.focusedIndex];
333
- if (this.type === "arrow" && item?.submenu?.length) {
334
- // keyboard-open locks (no hover-out to close it)
335
- this.lockedIndex = this.focusedIndex;
336
- this._items = this._items.map((i, idx) => ({
337
- ...i,
338
- open: idx === this.focusedIndex ? true : i.open,
339
- }));
407
+ if (this.type === "arrow" && item?.submenu?.length && !item.disabled) {
408
+ this.openSubmenuAt(this.focusedIndex);
409
+ ev.preventDefault();
340
410
  }
341
411
  } else if (ev.key === "ArrowLeft" || ev.key === "Escape") {
342
- this.lockedIndex = -1;
343
- this._items = this._items.map(i => ({ ...i, open: false }));
412
+ const anyOpen = this._items.some(i => i.open);
413
+ if (anyOpen) {
414
+ this.closeSubmenus();
415
+ ev.preventDefault();
416
+ if (ev.key === "Escape") ev.stopPropagation();
417
+ }
344
418
  } else if (ev.key === "Enter" || ev.key === " ") {
345
419
  const item = this._items[this.focusedIndex];
346
- this.onItemClick(item, this.focusedIndex);
420
+ if (this.type === "arrow" && item?.submenu?.length && !item.disabled) {
421
+ this.openSubmenuAt(this.focusedIndex);
422
+ } else {
423
+ this.onItemClick(item, this.focusedIndex);
424
+ }
347
425
  ev.preventDefault();
348
426
  }
349
427
  }
350
428
 
351
429
  private renderMenuItem(item: MenuItem, index: number) {
352
- const isFocused = this.focusedIndex === index;
430
+ const isFocused = this.focusedIndex === index && this.subFocusedIndex < 0;
353
431
  const hasSubmenu = this.type === "arrow" && !!item.submenu?.length;
432
+ const role =
433
+ this.type === "checkbox"
434
+ ? "menuitemcheckbox"
435
+ : this.type === "select-option"
436
+ ? "menuitemradio"
437
+ : "menuitem";
438
+ const ariaChecked =
439
+ this.type === "checkbox"
440
+ ? String(!!item.checked)
441
+ : this.type === "select-option"
442
+ ? String(!!item.active)
443
+ : nothing;
354
444
 
355
445
  // Checkbox / select-option rows delegate their control to the real
356
446
  // rel-checkbox / rel-radio components (pointer-events:none so the whole ROW
@@ -381,8 +471,13 @@ export class RelMenu extends LitElement {
381
471
 
382
472
  return html`
383
473
  <div
474
+ id="${this.menuId}-item-${index}"
384
475
  class="rel-menu-item ${item.active ? "active" : ""} ${item.disabled ? "disabled" : ""}"
385
- role="menuitem"
476
+ role="${role}"
477
+ aria-checked="${ariaChecked}"
478
+ aria-disabled="${item.disabled ? "true" : nothing}"
479
+ aria-haspopup="${hasSubmenu ? "menu" : nothing}"
480
+ aria-expanded="${hasSubmenu ? String(!!item.open) : nothing}"
386
481
  ?data-focused="${isFocused}"
387
482
  @mouseenter="${() => this.onItemEnter(item, index)}"
388
483
  @mouseleave="${() => this.onItemLeave(item, index)}"
@@ -392,12 +487,16 @@ export class RelMenu extends LitElement {
392
487
  ? html`
393
488
  <div
394
489
  part="panel" class="rel-submenu ${item.open ? "visible" : ""}"
490
+ role="menu"
395
491
  @click="${(e: Event) => e.stopPropagation()}">
396
492
  ${item.submenu!.map(
397
493
  (sub, subIndex) => html`
398
494
  <div
399
- class="rel-menu-item ${sub.active ? "active" : ""}"
495
+ id="${this.menuId}-item-${index}-sub-${subIndex}"
496
+ class="rel-menu-item ${sub.active ? "active" : ""} ${sub.disabled ? "disabled" : ""}"
400
497
  role="menuitem"
498
+ aria-disabled="${sub.disabled ? "true" : nothing}"
499
+ ?data-focused="${this.focusedIndex === index && this.subFocusedIndex === subIndex}"
401
500
  @click="${(e: Event) => this.onSubClick(index, subIndex, e)}">
402
501
  <span class="menu-label">${sub.label}</span>
403
502
  </div>
@@ -411,12 +510,22 @@ export class RelMenu extends LitElement {
411
510
  }
412
511
 
413
512
  render() {
513
+ // DOM focus stays on the container; this tells AT which item virtual
514
+ // focus is on as the user arrows around (main list or open submenu).
515
+ const activeDescendant =
516
+ this._items.length === 0
517
+ ? nothing
518
+ : this.subFocusedIndex >= 0
519
+ ? `${this.menuId}-item-${this.focusedIndex}-sub-${this.subFocusedIndex}`
520
+ : `${this.menuId}-item-${this.focusedIndex}`;
521
+
414
522
  return html`
415
523
  <style>${RelMenu._css}</style>
416
524
  <div
417
525
  part="list" class="rel-menu ${this.type === "fixed" || this.type === "fit" ? this.type : ""}"
418
526
  role="menu"
419
527
  tabindex="0"
528
+ aria-activedescendant="${activeDescendant}"
420
529
  @keydown="${this.onKeydown}"
421
530
  @click="${(e: Event) => e.stopPropagation()}">
422
531
  ${this._items.map((item, index) => this.renderMenuItem(item, index))}