@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
package/dist/index.js CHANGED
@@ -29,6 +29,7 @@ var __decorateClass = (decorators, target, key, kind) => {
29
29
  // src/index.ts
30
30
  var index_exports = {};
31
31
  __export(index_exports, {
32
+ FocusTrap: () => FocusTrap,
32
33
  RelAccordion: () => RelAccordion,
33
34
  RelAlert: () => RelAlert,
34
35
  RelAutocomplete: () => RelAutocomplete,
@@ -89,7 +90,16 @@ __export(index_exports, {
89
90
  RelTopbarProfile: () => RelTopbarProfile,
90
91
  RelTopbarUtilities: () => RelTopbarUtilities,
91
92
  RelUpload: () => RelUpload,
92
- RelUploadActions: () => RelUploadActions
93
+ RelUploadActions: () => RelUploadActions,
94
+ bindShortcut: () => bindShortcut,
95
+ formatShortcutParts: () => formatShortcutParts,
96
+ getDeepActiveElement: () => getDeepActiveElement,
97
+ getFocusableElements: () => getFocusableElements,
98
+ isApplePlatform: () => isApplePlatform,
99
+ isEditableTarget: () => isEditableTarget,
100
+ matchesShortcut: () => matchesShortcut,
101
+ parseShortcut: () => parseShortcut,
102
+ trapTabKey: () => trapTabKey
93
103
  });
94
104
  module.exports = __toCommonJS(index_exports);
95
105
  var import_vars = require("./vars-XNMTSRVN.css");
@@ -106,6 +116,8 @@ var RelAccordion = class extends import_lit.LitElement {
106
116
  this.size = "md";
107
117
  this.type = "default";
108
118
  this.appearance = "shadow";
119
+ /** Stable id prefix wiring the header to its panel. */
120
+ this.accordionId = `rel-accordion-${Math.random().toString(36).slice(2)}`;
109
121
  }
110
122
  toggle() {
111
123
  this.open = !this.open;
@@ -122,9 +134,11 @@ var RelAccordion = class extends import_lit.LitElement {
122
134
  <div part="base" class="accordion">
123
135
  <div
124
136
  part="trigger" class="header"
137
+ id="${this.accordionId}-header"
125
138
  role="button"
126
139
  tabindex="0"
127
140
  aria-expanded="${this.open}"
141
+ aria-controls="${this.accordionId}-body"
128
142
  @click=${this.toggle}
129
143
  @keydown=${(e) => {
130
144
  if (e.key === "Enter" || e.key === " ") {
@@ -147,7 +161,14 @@ var RelAccordion = class extends import_lit.LitElement {
147
161
  </slot>
148
162
  </div>
149
163
  </div>
150
- <div part="content" class="body ${this.open ? "open" : "closed"}">
164
+ <div
165
+ part="content"
166
+ class="body ${this.open ? "open" : "closed"}"
167
+ id="${this.accordionId}-body"
168
+ role="region"
169
+ aria-labelledby="${this.accordionId}-header"
170
+ ?inert=${!this.open}
171
+ >
151
172
  <slot></slot>
152
173
  </div>
153
174
  </div>
@@ -2837,9 +2858,12 @@ RelButton.styles = import_lit8.css`
2837
2858
  }
2838
2859
  }
2839
2860
 
2840
- /* Sizes */
2861
+ /* Sizes. Min-widths come from the Figma Button master (S/M/L =
2862
+ 80/100/120) so short labels like "OK" keep a proper pill shape;
2863
+ --rel-button-min-width lets a consumer override per instance. */
2841
2864
  .size-small {
2842
2865
  height: 32px;
2866
+ min-width: var(--rel-button-min-width, 80px);
2843
2867
  padding: 0 var(--rel-spacing-sm);
2844
2868
  font-family: var(--rel-action-md-font-family);
2845
2869
  font-weight: var(--rel-action-md-font-weight);
@@ -2850,6 +2874,7 @@ RelButton.styles = import_lit8.css`
2850
2874
 
2851
2875
  .size-medium {
2852
2876
  height: 40px;
2877
+ min-width: var(--rel-button-min-width, 100px);
2853
2878
  gap: var(--rel-spacing-xs);
2854
2879
  padding: 0 var(--rel-spacing-md);
2855
2880
  font-family: var(--rel-action-lg-font-family);
@@ -2861,6 +2886,7 @@ RelButton.styles = import_lit8.css`
2861
2886
 
2862
2887
  .size-large {
2863
2888
  height: 48px;
2889
+ min-width: var(--rel-button-min-width, 120px);
2864
2890
  padding: 0 var(--rel-spacing-lg);
2865
2891
  font-family: var(--rel-action-xl-font-family);
2866
2892
  font-weight: var(--rel-action-xl-font-weight);
@@ -2967,6 +2993,11 @@ RelButton.styles = import_lit8.css`
2967
2993
  color: var(--rel-action-neutral-label-icons-ghost);
2968
2994
  }
2969
2995
 
2996
+ /* Figma master: ghost S has a narrower 56px min than primary/outline. */
2997
+ .btn-text.size-small {
2998
+ min-width: var(--rel-button-min-width, 56px);
2999
+ }
3000
+
2970
3001
  .btn-text.tone-critical {
2971
3002
  color: var(--rel-action-danger-label-icons-ghost);
2972
3003
  }
@@ -2975,6 +3006,8 @@ RelButton.styles = import_lit8.css`
2975
3006
  background: transparent;
2976
3007
  color: var(--rel-action-neutral-label-icons-link);
2977
3008
  text-decoration: underline;
3009
+ /* Links are inline actions — no minimum footprint in the Figma master. */
3010
+ min-width: 0;
2978
3011
  }
2979
3012
 
2980
3013
  button.btn-link:hover:not(:disabled) {
@@ -3003,6 +3036,7 @@ RelButton.styles = import_lit8.css`
3003
3036
  /* Icon Only as an independent axis: square button of the current style */
3004
3037
  .btn-icon-only {
3005
3038
  padding: 0;
3039
+ min-width: 0;
3006
3040
  }
3007
3041
 
3008
3042
  .btn-icon-only.size-small {
@@ -3021,6 +3055,7 @@ RelButton.styles = import_lit8.css`
3021
3055
  width: 40px;
3022
3056
  height: 40px;
3023
3057
  padding: 0;
3058
+ min-width: 0;
3024
3059
  border-radius: 50%;
3025
3060
  }
3026
3061
  .btn-icon.tone-default {
@@ -3065,7 +3100,9 @@ RelButton.styles = import_lit8.css`
3065
3100
  .btn-text: ghost gets tone-accent hovers below (this generic grey overlay
3066
3101
  has higher specificity than the per-tone ghost rules and was overriding
3067
3102
  them, which is why every ghost hover looked grey). */
3068
- button:hover:not(:disabled):not(.btn-primary):not(.btn-default):not(.btn-icon):not(.btn-secondary):not(.btn-static):not(.btn-text) {
3103
+ /* Link buttons change label color only (rule above) — the overlay fill
3104
+ here would make a hovered link read as a ghost button. */
3105
+ button:hover:not(:disabled):not(.btn-primary):not(.btn-default):not(.btn-icon):not(.btn-secondary):not(.btn-static):not(.btn-text):not(.btn-link) {
3069
3106
  background: var(--rel-action-overlay-hover);
3070
3107
  }
3071
3108
 
@@ -3417,9 +3454,28 @@ var RelCalendar = class extends import_lit9.LitElement {
3417
3454
  });
3418
3455
  }
3419
3456
  }
3457
+ /**
3458
+ * Index of the single grid cell carrying tabindex="0". The old expression
3459
+ * (`isSelected || (index === 0 && !focusedDate)`) left the grid with ZERO
3460
+ * tab stops once the user arrowed around without a selection — Tab could
3461
+ * never re-enter the calendar.
3462
+ */
3463
+ getRovingIndex(days) {
3464
+ const candidates = [
3465
+ this.focusedDate ? days.findIndex(
3466
+ (d) => d.isCurrentMonth && this.isSameDay(d.date, this.focusedDate)
3467
+ ) : -1,
3468
+ days.findIndex((d) => d.isSelected),
3469
+ days.findIndex((d) => d.isCurrentMonth && d.isToday),
3470
+ days.findIndex((d) => d.isCurrentMonth && !d.isDisabled)
3471
+ ];
3472
+ const found = candidates.find((i) => i >= 0);
3473
+ return found ?? 0;
3474
+ }
3420
3475
  render() {
3421
3476
  const days = this.getCalendarDays();
3422
3477
  const weekdayLabels = this.getWeekdayLabels();
3478
+ const rovingIndex = this.getRovingIndex(days);
3423
3479
  return import_lit9.html`
3424
3480
  <div role="grid" aria-label="Calendar">
3425
3481
  <div part="header" class="header">
@@ -3472,7 +3528,7 @@ var RelCalendar = class extends import_lit9.LitElement {
3472
3528
  ?disabled=${day.isDisabled}
3473
3529
  @click=${() => this.selectDay(day)}
3474
3530
  @keydown=${(e) => this.handleKeyDown(e, day, index)}
3475
- tabindex="${day.isSelected || index === 0 && !this.focusedDate ? 0 : -1}"
3531
+ tabindex="${index === rovingIndex ? 0 : -1}"
3476
3532
  >
3477
3533
  ${day.day}
3478
3534
  </button>
@@ -3772,12 +3828,23 @@ var RelCarousel = class extends import_lit11.LitElement {
3772
3828
  // Number of clone slides prepended before the real slides
3773
3829
  this.clonesBefore = 0;
3774
3830
  this._silentJumping = false;
3831
+ this.handleFocusIn = () => {
3832
+ if (this.autoplay) this.stopAutoplay();
3833
+ };
3834
+ this.handleFocusOut = (e) => {
3835
+ if (!this.autoplay) return;
3836
+ const next = e.relatedTarget;
3837
+ if (next && (this.contains(next) || this.shadowRoot?.contains(next))) return;
3838
+ this.startAutoplay();
3839
+ };
3775
3840
  }
3776
3841
  connectedCallback() {
3777
3842
  super.connectedCallback();
3778
3843
  this.prefersReducedMotion = window.matchMedia(
3779
3844
  "(prefers-reduced-motion: reduce)"
3780
3845
  ).matches;
3846
+ this.addEventListener("focusin", this.handleFocusIn);
3847
+ this.addEventListener("focusout", this.handleFocusOut);
3781
3848
  if (this.autoplay) {
3782
3849
  this.startAutoplay();
3783
3850
  }
@@ -3809,6 +3876,8 @@ var RelCarousel = class extends import_lit11.LitElement {
3809
3876
  }
3810
3877
  disconnectedCallback() {
3811
3878
  super.disconnectedCallback();
3879
+ this.removeEventListener("focusin", this.handleFocusIn);
3880
+ this.removeEventListener("focusout", this.handleFocusOut);
3812
3881
  this._ro?.disconnect();
3813
3882
  this.stopAutoplay();
3814
3883
  if (this.animationId) {
@@ -4123,6 +4192,7 @@ var RelCarousel = class extends import_lit11.LitElement {
4123
4192
  class="dot ${i === this.value ? "active" : ""}"
4124
4193
  part="dot"
4125
4194
  aria-label="Go to slide ${i + 1}"
4195
+ aria-current="${i === this.value ? "true" : import_lit11.nothing}"
4126
4196
  @click=${() => this.goToSlide(i)}
4127
4197
  ></button>
4128
4198
  `) : import_lit11.nothing}
@@ -4570,6 +4640,9 @@ var RelCheckbox = class extends import_lit13.LitElement {
4570
4640
  <label class="row">
4571
4641
  <input
4572
4642
  type="checkbox"
4643
+ .checked=${this.status === "checked"}
4644
+ .indeterminate=${this.status === "indeterminate"}
4645
+ aria-checked="${this.status === "indeterminate" ? "mixed" : import_lit13.nothing}"
4573
4646
  @click=${this.toggle}
4574
4647
  @blur=${() => this._onTouched()}
4575
4648
  ?disabled=${this.state === "disabled"}
@@ -4908,6 +4981,109 @@ function formatDatePattern(date, pattern, locale = "en-US") {
4908
4981
  );
4909
4982
  }
4910
4983
 
4984
+ // src/utils/focus.ts
4985
+ var FOCUSABLE_SELECTOR = [
4986
+ 'a[href]:not([tabindex="-1"])',
4987
+ 'button:not([disabled]):not([tabindex="-1"])',
4988
+ 'input:not([disabled]):not([tabindex="-1"])',
4989
+ 'select:not([disabled]):not([tabindex="-1"])',
4990
+ 'textarea:not([disabled]):not([tabindex="-1"])',
4991
+ 'audio[controls]:not([tabindex="-1"])',
4992
+ 'video[controls]:not([tabindex="-1"])',
4993
+ '[contenteditable]:not([contenteditable="false"]):not([tabindex="-1"])',
4994
+ '[tabindex]:not([tabindex="-1"])'
4995
+ ].join(",");
4996
+ function isVisible(el) {
4997
+ return !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length);
4998
+ }
4999
+ function getFocusableElements(root) {
5000
+ const out = [];
5001
+ const visit = (el) => {
5002
+ if (el instanceof HTMLElement && el.matches(FOCUSABLE_SELECTOR) && isVisible(el)) {
5003
+ out.push(el);
5004
+ }
5005
+ if (el instanceof HTMLSlotElement) {
5006
+ el.assignedElements({ flatten: true }).forEach(visit);
5007
+ } else if (el.shadowRoot) {
5008
+ Array.from(el.shadowRoot.children).forEach(visit);
5009
+ } else {
5010
+ Array.from(el.children).forEach(visit);
5011
+ }
5012
+ };
5013
+ const start = root instanceof ShadowRoot ? Array.from(root.children) : root.shadowRoot ? Array.from(root.shadowRoot.children) : Array.from(root.children);
5014
+ start.forEach(visit);
5015
+ return out;
5016
+ }
5017
+ function getDeepActiveElement() {
5018
+ let active = document.activeElement;
5019
+ while (active?.shadowRoot?.activeElement) {
5020
+ active = active.shadowRoot.activeElement;
5021
+ }
5022
+ return active instanceof HTMLElement ? active : null;
5023
+ }
5024
+ function trapTabKey(e, container) {
5025
+ if (e.key !== "Tab") return;
5026
+ const focusable = getFocusableElements(container);
5027
+ if (focusable.length === 0) {
5028
+ e.preventDefault();
5029
+ return;
5030
+ }
5031
+ const first = focusable[0];
5032
+ const last = focusable[focusable.length - 1];
5033
+ const active = getDeepActiveElement();
5034
+ const inside = active ? focusable.includes(active) : false;
5035
+ if (e.shiftKey) {
5036
+ if (!inside || active === first) {
5037
+ e.preventDefault();
5038
+ last.focus();
5039
+ }
5040
+ } else {
5041
+ if (!inside || active === last) {
5042
+ e.preventDefault();
5043
+ first.focus();
5044
+ }
5045
+ }
5046
+ }
5047
+ var FocusTrap = class {
5048
+ constructor(getContainer) {
5049
+ this.getContainer = getContainer;
5050
+ this.previousFocus = null;
5051
+ this.active = false;
5052
+ this.onKeyDown = (e) => {
5053
+ const container = this.getContainer();
5054
+ if (container) trapTabKey(e, container);
5055
+ };
5056
+ }
5057
+ activate(options) {
5058
+ if (this.active) return;
5059
+ this.active = true;
5060
+ this.previousFocus = getDeepActiveElement();
5061
+ document.addEventListener("keydown", this.onKeyDown, true);
5062
+ requestAnimationFrame(() => {
5063
+ if (!this.active) return;
5064
+ const container = this.getContainer();
5065
+ if (!container) return;
5066
+ const target = options?.initialFocus ?? getFocusableElements(container)[0] ?? null;
5067
+ if (target) {
5068
+ target.focus();
5069
+ } else if (container instanceof HTMLElement) {
5070
+ container.tabIndex = -1;
5071
+ container.focus();
5072
+ }
5073
+ });
5074
+ }
5075
+ deactivate(options) {
5076
+ if (!this.active) return;
5077
+ this.active = false;
5078
+ document.removeEventListener("keydown", this.onKeyDown, true);
5079
+ const previous = this.previousFocus;
5080
+ this.previousFocus = null;
5081
+ if (options?.restoreFocus !== false && previous?.isConnected) {
5082
+ requestAnimationFrame(() => previous.focus());
5083
+ }
5084
+ }
5085
+ };
5086
+
4911
5087
  // src/components/rel-date-picker/rel-date-picker.ts
4912
5088
  var RelDatePicker = class extends import_lit14.LitElement {
4913
5089
  constructor() {
@@ -4945,9 +5121,58 @@ var RelDatePicker = class extends import_lit14.LitElement {
4945
5121
  }
4946
5122
  };
4947
5123
  this.handleEscapeKey = (e) => {
4948
- if (e.key === "Escape" && this.open) {
5124
+ if (!this.open) return;
5125
+ if (e.key === "Escape") {
4949
5126
  this.closeCalendarPopover();
5127
+ return;
5128
+ }
5129
+ if (e.key === "Tab") {
5130
+ const popover = this.renderRoot.querySelector(".popover");
5131
+ if (popover) trapTabKey(e, popover);
5132
+ }
5133
+ };
5134
+ this.handleDropdownKeydown = (e) => {
5135
+ const menu = e.currentTarget;
5136
+ const items = Array.from(
5137
+ menu.querySelectorAll(".dropdown-item")
5138
+ );
5139
+ if (items.length === 0) return;
5140
+ if (e.key === "Escape") {
5141
+ e.preventDefault();
5142
+ e.stopPropagation();
5143
+ const trigger = menu.closest(".dropdown-container")?.querySelector(".dropdown-trigger");
5144
+ this.monthDropdownOpen = false;
5145
+ this.yearDropdownOpen = false;
5146
+ trigger?.focus();
5147
+ return;
5148
+ }
5149
+ if (e.key === "Tab") {
5150
+ this.monthDropdownOpen = false;
5151
+ this.yearDropdownOpen = false;
5152
+ return;
5153
+ }
5154
+ const idx = items.indexOf(getDeepActiveElement());
5155
+ let target;
5156
+ switch (e.key) {
5157
+ case "ArrowDown":
5158
+ target = Math.min(items.length - 1, idx + 1);
5159
+ break;
5160
+ case "ArrowUp":
5161
+ target = Math.max(0, idx - 1);
5162
+ break;
5163
+ case "Home":
5164
+ target = 0;
5165
+ break;
5166
+ case "End":
5167
+ target = items.length - 1;
5168
+ break;
5169
+ default:
5170
+ return;
4950
5171
  }
5172
+ e.preventDefault();
5173
+ e.stopPropagation();
5174
+ items[target]?.focus();
5175
+ items[target]?.scrollIntoView({ block: "nearest" });
4951
5176
  };
4952
5177
  }
4953
5178
  // minDate/maxDate supersede the deprecated min/max aliases
@@ -4984,10 +5209,12 @@ var RelDatePicker = class extends import_lit14.LitElement {
4984
5209
  }
4985
5210
  if (changedProperties.has("open") && this.open) {
4986
5211
  setTimeout(() => {
4987
- const firstDay = this.calendarGrid?.querySelector(
5212
+ const target = this.calendarGrid?.querySelector(
5213
+ '.day-button[tabindex="0"]:not(:disabled)'
5214
+ ) ?? this.calendarGrid?.querySelector(
4988
5215
  ".day-button:not(:disabled)"
4989
5216
  );
4990
- firstDay?.focus();
5217
+ target?.focus();
4991
5218
  }, 100);
4992
5219
  }
4993
5220
  }
@@ -5285,27 +5512,67 @@ var RelDatePicker = class extends import_lit14.LitElement {
5285
5512
  e.stopPropagation();
5286
5513
  this.monthDropdownOpen = !this.monthDropdownOpen;
5287
5514
  this.yearDropdownOpen = false;
5515
+ if (this.monthDropdownOpen) this.focusOpenDropdownSelection();
5288
5516
  }
5289
5517
  toggleYearDropdown(e) {
5290
5518
  e.stopPropagation();
5291
5519
  this.yearDropdownOpen = !this.yearDropdownOpen;
5292
5520
  this.monthDropdownOpen = false;
5521
+ if (this.yearDropdownOpen) this.focusOpenDropdownSelection();
5522
+ }
5523
+ handleDropdownTriggerKeydown(e, kind) {
5524
+ if (e.key !== "ArrowDown") return;
5525
+ e.preventDefault();
5526
+ e.stopPropagation();
5527
+ this.monthDropdownOpen = kind === "month";
5528
+ this.yearDropdownOpen = kind === "year";
5529
+ this.focusOpenDropdownSelection();
5530
+ }
5531
+ async focusOpenDropdownSelection() {
5532
+ await this.updateComplete;
5533
+ const menu = this.renderRoot.querySelector(".dropdown-menu.open");
5534
+ if (!menu) return;
5535
+ const target = menu.querySelector(".dropdown-item.selected") ?? menu.querySelector(".dropdown-item");
5536
+ target?.focus();
5537
+ target?.scrollIntoView({ block: "nearest" });
5293
5538
  }
5294
5539
  selectMonth(month, e) {
5295
5540
  e.stopPropagation();
5541
+ const trigger = e.target.closest(".dropdown-container")?.querySelector(".dropdown-trigger");
5296
5542
  this.viewMonth = month;
5297
5543
  this.monthDropdownOpen = false;
5544
+ this.updateComplete.then(() => trigger?.focus());
5298
5545
  }
5299
5546
  selectYear(year, e) {
5300
5547
  e.stopPropagation();
5548
+ const trigger = e.target.closest(".dropdown-container")?.querySelector(".dropdown-trigger");
5301
5549
  this.viewYear = year;
5302
5550
  this.yearDropdownOpen = false;
5551
+ this.updateComplete.then(() => trigger?.focus());
5552
+ }
5553
+ /**
5554
+ * Index of the single grid cell carrying tabindex="0". The old expression
5555
+ * (`isSelected || (index === 0 && !value)`) left the grid with ZERO tab
5556
+ * stops once the user arrowed around without a selection.
5557
+ */
5558
+ getRovingIndex(days) {
5559
+ const candidates = [
5560
+ this.focusedDate ? days.findIndex(
5561
+ (d) => d.isCurrentMonth && this.isSameDay(d.date, this.focusedDate)
5562
+ ) : -1,
5563
+ days.findIndex((d) => d.isSelected),
5564
+ days.findIndex((d) => d.isCurrentMonth && d.isToday),
5565
+ days.findIndex((d) => d.isCurrentMonth && !d.isDisabled)
5566
+ ];
5567
+ const found = candidates.find((i) => i >= 0);
5568
+ return found ?? 0;
5303
5569
  }
5304
5570
  render() {
5305
5571
  const days = this.getCalendarDays();
5306
5572
  const weekdayLabels = this.getWeekdayLabels();
5307
5573
  const months = this.getMonthsList();
5308
5574
  const years = this.getYearsList();
5575
+ const rovingIndex = this.getRovingIndex(days);
5309
5576
  const isError = this.error;
5310
5577
  const belowText = isError && this.errorMessage ? this.errorMessage : this.helperText;
5311
5578
  const describedById = belowText ? "date-picker-desc" : void 0;
@@ -5392,22 +5659,31 @@ var RelDatePicker = class extends import_lit14.LitElement {
5392
5659
  class="dropdown-trigger"
5393
5660
  type="button"
5394
5661
  @click=${this.toggleMonthDropdown}
5662
+ @keydown=${(e) => this.handleDropdownTriggerKeydown(e, "month")}
5395
5663
  aria-label="Select month"
5664
+ aria-haspopup="listbox"
5396
5665
  aria-expanded=${this.monthDropdownOpen}
5397
5666
  >
5398
5667
  ${this.getMonthName()}
5399
5668
  </button>
5400
- <div class="dropdown-menu ${this.monthDropdownOpen ? "open" : ""}">
5669
+ <div
5670
+ class="dropdown-menu ${this.monthDropdownOpen ? "open" : ""}"
5671
+ role="listbox"
5672
+ aria-label="Month"
5673
+ @keydown=${this.handleDropdownKeydown}
5674
+ >
5401
5675
  ${months.map(
5402
5676
  (m) => import_lit14.html`
5403
- <div
5677
+ <button
5404
5678
  class="dropdown-item ${m.value === this.viewMonth ? "selected" : ""}"
5679
+ type="button"
5680
+ tabindex="-1"
5405
5681
  @click=${(e) => this.selectMonth(m.value, e)}
5406
5682
  role="option"
5407
5683
  aria-selected=${m.value === this.viewMonth}
5408
5684
  >
5409
5685
  ${m.label}
5410
- </div>
5686
+ </button>
5411
5687
  `
5412
5688
  )}
5413
5689
  </div>
@@ -5418,22 +5694,31 @@ var RelDatePicker = class extends import_lit14.LitElement {
5418
5694
  class="dropdown-trigger"
5419
5695
  type="button"
5420
5696
  @click=${this.toggleYearDropdown}
5697
+ @keydown=${(e) => this.handleDropdownTriggerKeydown(e, "year")}
5421
5698
  aria-label="Select year"
5699
+ aria-haspopup="listbox"
5422
5700
  aria-expanded=${this.yearDropdownOpen}
5423
5701
  >
5424
5702
  ${this.viewYear}
5425
5703
  </button>
5426
- <div class="dropdown-menu ${this.yearDropdownOpen ? "open" : ""}">
5704
+ <div
5705
+ class="dropdown-menu ${this.yearDropdownOpen ? "open" : ""}"
5706
+ role="listbox"
5707
+ aria-label="Year"
5708
+ @keydown=${this.handleDropdownKeydown}
5709
+ >
5427
5710
  ${years.map(
5428
5711
  (y) => import_lit14.html`
5429
- <div
5712
+ <button
5430
5713
  class="dropdown-item ${y === this.viewYear ? "selected" : ""}"
5714
+ type="button"
5715
+ tabindex="-1"
5431
5716
  @click=${(e) => this.selectYear(y, e)}
5432
5717
  role="option"
5433
5718
  aria-selected=${y === this.viewYear}
5434
5719
  >
5435
5720
  ${y}
5436
- </div>
5721
+ </button>
5437
5722
  `
5438
5723
  )}
5439
5724
  </div>
@@ -5476,7 +5761,7 @@ var RelDatePicker = class extends import_lit14.LitElement {
5476
5761
  ?disabled=${day.isDisabled}
5477
5762
  @click=${() => this.selectDay(day)}
5478
5763
  @keydown=${(e) => this.handleDayKeydown(e, day, index)}
5479
- tabindex="${day.isSelected || index === 0 && !this.value ? 0 : -1}"
5764
+ tabindex="${index === rovingIndex ? 0 : -1}"
5480
5765
  >
5481
5766
  ${day.day}
5482
5767
  </button>
@@ -5770,6 +6055,11 @@ RelDatePicker.styles = import_lit14.css`
5770
6055
  }
5771
6056
 
5772
6057
  .dropdown-item {
6058
+ display: block;
6059
+ width: 100%;
6060
+ border: none;
6061
+ background: transparent;
6062
+ text-align: left;
5773
6063
  padding: var(--rel-spacing-xs) var(--rel-spacing-sm);
5774
6064
  cursor: pointer;
5775
6065
  color: var(--rel-text-default);
@@ -5781,6 +6071,11 @@ RelDatePicker.styles = import_lit14.css`
5781
6071
  letter-spacing: var(--rel-body-sm-regular-letter-spacing);
5782
6072
  }
5783
6073
 
6074
+ .dropdown-item:focus-visible {
6075
+ outline: 2px solid var(--rel-border-focus);
6076
+ outline-offset: -2px;
6077
+ }
6078
+
5784
6079
  .dropdown-item:hover {
5785
6080
  background: var(--rel-bg-surface-elevated);
5786
6081
  }
@@ -6009,6 +6304,7 @@ var RelRangePicker = class extends import_lit15.LitElement {
6009
6304
  this.viewMonth = (/* @__PURE__ */ new Date()).getMonth();
6010
6305
  this.isSelectingRange = false;
6011
6306
  this.hoverDate = null;
6307
+ this.focusedDate = null;
6012
6308
  this.onReposition = () => {
6013
6309
  if (this.open && this.mode === "popover") this.updatePopoverPosition();
6014
6310
  };
@@ -6065,6 +6361,69 @@ var RelRangePicker = class extends import_lit15.LitElement {
6065
6361
  const qs = this.getQuickSelects().find((q) => q.label === label);
6066
6362
  if (qs) this.selectQuickSelect(qs);
6067
6363
  };
6364
+ this.handleGridKeydown = (e) => {
6365
+ const cell = e.target.closest?.(".day-cell");
6366
+ const iso = cell?.dataset.iso;
6367
+ if (!iso) return;
6368
+ const date = this.parseISODate(iso);
6369
+ if (!date) return;
6370
+ if (e.key === "Enter" || e.key === " ") {
6371
+ e.preventDefault();
6372
+ const isDisabled = this.isDateDisabled(date);
6373
+ if (!isDisabled) {
6374
+ this.handleDayClick({
6375
+ date,
6376
+ day: date.getDate(),
6377
+ isCurrentMonth: true,
6378
+ isToday: false,
6379
+ isDraftFrom: false,
6380
+ isDraftTo: false,
6381
+ isInRange: false,
6382
+ isDisabled,
6383
+ isWeekend: false
6384
+ });
6385
+ }
6386
+ return;
6387
+ }
6388
+ const next = new Date(date);
6389
+ const dow = next.getDay();
6390
+ switch (e.key) {
6391
+ case "ArrowLeft":
6392
+ next.setDate(next.getDate() - 1);
6393
+ break;
6394
+ case "ArrowRight":
6395
+ next.setDate(next.getDate() + 1);
6396
+ break;
6397
+ case "ArrowUp":
6398
+ next.setDate(next.getDate() - 7);
6399
+ break;
6400
+ case "ArrowDown":
6401
+ next.setDate(next.getDate() + 7);
6402
+ break;
6403
+ case "Home":
6404
+ next.setDate(
6405
+ next.getDate() - (this.weekStart === "mon" ? dow === 0 ? 6 : dow - 1 : dow)
6406
+ );
6407
+ break;
6408
+ case "End":
6409
+ next.setDate(
6410
+ next.getDate() + (this.weekStart === "mon" ? dow === 0 ? 0 : 7 - dow : 6 - dow)
6411
+ );
6412
+ break;
6413
+ case "PageUp":
6414
+ if (e.shiftKey) next.setFullYear(next.getFullYear() - 1);
6415
+ else next.setMonth(next.getMonth() - 1);
6416
+ break;
6417
+ case "PageDown":
6418
+ if (e.shiftKey) next.setFullYear(next.getFullYear() + 1);
6419
+ else next.setMonth(next.getMonth() + 1);
6420
+ break;
6421
+ default:
6422
+ return;
6423
+ }
6424
+ e.preventDefault();
6425
+ this.moveGridFocusTo(next);
6426
+ };
6068
6427
  }
6069
6428
  // ── Lifecycle ──
6070
6429
  connectedCallback() {
@@ -6149,6 +6508,7 @@ var RelRangePicker = class extends import_lit15.LitElement {
6149
6508
  this.draftActivePreset = null;
6150
6509
  this.isSelectingRange = false;
6151
6510
  this.hoverDate = null;
6511
+ this.focusedDate = null;
6152
6512
  if (this.draftFrom) {
6153
6513
  const date = this.parseISODate(this.draftFrom);
6154
6514
  if (date) {
@@ -6447,10 +6807,45 @@ var RelRangePicker = class extends import_lit15.LitElement {
6447
6807
  if (!this.isSelectingRange || day.isDisabled) return;
6448
6808
  this.hoverDate = this.formatISODate(day.date);
6449
6809
  }
6810
+ // ── Grid keyboard navigation (APG grid pattern) ──
6811
+ /**
6812
+ * The cell that carries tabindex="0". Prefers the last roved date, then the
6813
+ * draft start, then today, then the first selectable day — always exactly
6814
+ * one Tab stop, even after arrowing away with nothing selected.
6815
+ */
6816
+ getRovingIso(days) {
6817
+ const inView = days.filter((d) => d.isCurrentMonth);
6818
+ if (inView.length === 0) return "";
6819
+ const candidates = [
6820
+ this.focusedDate,
6821
+ this.draftFrom,
6822
+ this.formatISODate(/* @__PURE__ */ new Date())
6823
+ ];
6824
+ for (const iso of candidates) {
6825
+ if (iso && inView.some((d) => this.formatISODate(d.date) === iso)) {
6826
+ return iso;
6827
+ }
6828
+ }
6829
+ const firstEnabled = inView.find((d) => !d.isDisabled) ?? inView[0];
6830
+ return this.formatISODate(firstEnabled.date);
6831
+ }
6832
+ async moveGridFocusTo(date) {
6833
+ this.focusedDate = this.formatISODate(date);
6834
+ if (date.getMonth() !== this.viewMonth || date.getFullYear() !== this.viewYear) {
6835
+ this.viewMonth = date.getMonth();
6836
+ this.viewYear = date.getFullYear();
6837
+ }
6838
+ if (this.isSelectingRange && !this.isDateDisabled(date)) {
6839
+ this.hoverDate = this.focusedDate;
6840
+ }
6841
+ await this.updateComplete;
6842
+ this.renderRoot.querySelector(`.day-cell[data-iso="${this.focusedDate}"]`)?.focus();
6843
+ }
6450
6844
  // ── Render ──
6451
6845
  render() {
6452
6846
  const quickSelects = this.getQuickSelects();
6453
6847
  const days = this.getCalendarDays();
6848
+ const rovingIso = this.getRovingIso(days);
6454
6849
  const weekdayLabels = this.getWeekdayLabels();
6455
6850
  const draftFromDisplay = this.draftFrom ? this.formatDisplayDate(this.draftFrom) : "";
6456
6851
  const draftToDisplay = this.draftTo ? this.formatDisplayDate(this.draftTo) : "";
@@ -6579,8 +6974,10 @@ var RelRangePicker = class extends import_lit15.LitElement {
6579
6974
  @mouseleave=${() => {
6580
6975
  this.hoverDate = null;
6581
6976
  }}
6977
+ @keydown=${this.handleGridKeydown}
6582
6978
  >
6583
6979
  ${days.map((day) => {
6980
+ const iso = this.formatISODate(day.date);
6584
6981
  const cellClasses = [
6585
6982
  "day-cell",
6586
6983
  day.isInRange ? "in-range" : "",
@@ -6595,8 +6992,11 @@ var RelRangePicker = class extends import_lit15.LitElement {
6595
6992
  <div
6596
6993
  class="${cellClasses}"
6597
6994
  role="gridcell"
6598
- aria-label="${this.formatISODate(day.date)}"
6995
+ tabindex="${day.isCurrentMonth && iso === rovingIso ? 0 : -1}"
6996
+ data-iso="${iso}"
6997
+ aria-label="${iso}"
6599
6998
  aria-selected="${day.isDraftFrom || day.isDraftTo || day.isInRange}"
6999
+ aria-disabled="${day.isDisabled ? "true" : import_lit15.nothing}"
6600
7000
  @click=${() => this.handleDayClick(day)}
6601
7001
  @mouseenter=${() => this.handleDayHover(day)}
6602
7002
  >
@@ -6941,6 +7341,12 @@ RelRangePicker.styles = import_lit15.css`
6941
7341
  cursor: pointer;
6942
7342
  }
6943
7343
 
7344
+ .day-cell:focus-visible {
7345
+ outline: 2px solid var(--rel-border-focus);
7346
+ outline-offset: -2px;
7347
+ z-index: 2;
7348
+ }
7349
+
6944
7350
  .day-cell.other-month {
6945
7351
  opacity: 0.35;
6946
7352
  pointer-events: none;
@@ -7116,6 +7522,9 @@ __decorateClass([
7116
7522
  __decorateClass([
7117
7523
  (0, import_decorators15.state)()
7118
7524
  ], RelRangePicker.prototype, "hoverDate", 2);
7525
+ __decorateClass([
7526
+ (0, import_decorators15.state)()
7527
+ ], RelRangePicker.prototype, "focusedDate", 2);
7119
7528
  __decorateClass([
7120
7529
  (0, import_decorators15.query)(".trigger-button")
7121
7530
  ], RelRangePicker.prototype, "triggerButton", 2);
@@ -7145,6 +7554,33 @@ var RelChip = class extends import_lit16.LitElement {
7145
7554
  this.removed = false;
7146
7555
  this.draggableChip = false;
7147
7556
  this.dragging = false;
7557
+ /** The chip body is announced as a button, so it must also act on keys:
7558
+ * Enter/Space re-dispatch as a host click (consumers' @click handlers
7559
+ * fire), Delete/Backspace remove when removable, and arrow keys ask the
7560
+ * container to reorder a draggable chip (`chip-move`) — the keyboard
7561
+ * alternative to pointer drag-and-drop. */
7562
+ this.handleKeydown = (e) => {
7563
+ if (e.key === "Enter" || e.key === " ") {
7564
+ e.preventDefault();
7565
+ this.click();
7566
+ return;
7567
+ }
7568
+ if ((e.key === "Delete" || e.key === "Backspace") && this.removable) {
7569
+ e.preventDefault();
7570
+ this.handleRemove(e);
7571
+ return;
7572
+ }
7573
+ if (this.draggableChip && (e.key === "ArrowLeft" || e.key === "ArrowRight")) {
7574
+ e.preventDefault();
7575
+ this.dispatchEvent(
7576
+ new CustomEvent("chip-move", {
7577
+ detail: { direction: e.key === "ArrowLeft" ? -1 : 1 },
7578
+ bubbles: true,
7579
+ composed: true
7580
+ })
7581
+ );
7582
+ }
7583
+ };
7148
7584
  this.onDragStart = (e) => {
7149
7585
  if (!this.draggableChip) return;
7150
7586
  this.dragging = true;
@@ -7198,6 +7634,7 @@ var RelChip = class extends import_lit16.LitElement {
7198
7634
  role="button"
7199
7635
  tabindex="0"
7200
7636
  aria-label="${this.label}"
7637
+ @keydown=${this.handleKeydown}
7201
7638
  >
7202
7639
  ${showAvatar ? import_lit16.html`<rel-avatar type="text" text=${this.avatar} size="xs"></rel-avatar>` : null}
7203
7640
  ${showLeadingIcon ? import_lit16.html`<rel-icon part="icon" class="chip-icon" name="${this.leadingIcon}"></rel-icon>` : null}
@@ -7789,26 +8226,8 @@ var RelDrawer = class extends import_lit20.LitElement {
7789
8226
  this.close("escape");
7790
8227
  return;
7791
8228
  }
7792
- if (e.key === "Tab" && this.modal) {
7793
- const focusable = this.getFocusableElements();
7794
- if (focusable.length === 0) {
7795
- e.preventDefault();
7796
- return;
7797
- }
7798
- const firstFocusable = focusable[0];
7799
- const lastFocusable = focusable[focusable.length - 1];
7800
- const activeElement = document.activeElement;
7801
- if (e.shiftKey) {
7802
- if (activeElement === firstFocusable) {
7803
- e.preventDefault();
7804
- lastFocusable.focus();
7805
- }
7806
- } else {
7807
- if (activeElement === lastFocusable) {
7808
- e.preventDefault();
7809
- firstFocusable.focus();
7810
- }
7811
- }
8229
+ if (e.key === "Tab" && this.modal && this.panelElement) {
8230
+ trapTabKey(e, this.panelElement);
7812
8231
  }
7813
8232
  };
7814
8233
  // Drag-to-close handlers
@@ -7867,12 +8286,19 @@ var RelDrawer = class extends import_lit20.LitElement {
7867
8286
  }
7868
8287
  }
7869
8288
  handleOpen() {
7870
- this.previousFocus = document.activeElement;
8289
+ this.previousFocus = getDeepActiveElement();
7871
8290
  this.lockScroll();
7872
8291
  if (this.modal) {
7873
8292
  this.updateComplete.then(() => {
7874
- const focusTarget = this.querySelector("[autofocus]") ?? this.querySelector(".close-button") ?? this.querySelector(".panel");
7875
- focusTarget?.focus();
8293
+ const panel = this.panelElement;
8294
+ if (!panel) return;
8295
+ const focusTarget = getFocusableElements(panel)[0];
8296
+ if (focusTarget) {
8297
+ focusTarget.focus();
8298
+ } else {
8299
+ panel.tabIndex = -1;
8300
+ panel.focus();
8301
+ }
7876
8302
  });
7877
8303
  }
7878
8304
  document.addEventListener("keydown", this.handleKeydown);
@@ -7934,29 +8360,6 @@ var RelDrawer = class extends import_lit20.LitElement {
7934
8360
  document.body.style.overflow = this.originalBodyOverflow;
7935
8361
  document.body.style.paddingRight = this.originalBodyPaddingRight;
7936
8362
  }
7937
- getFocusableElements() {
7938
- if (!this.panelElement) return [];
7939
- const selectors = [
7940
- "a[href]",
7941
- "button:not([disabled])",
7942
- "textarea:not([disabled])",
7943
- "input:not([disabled])",
7944
- "select:not([disabled])",
7945
- '[tabindex]:not([tabindex="-1"])'
7946
- ].join(",");
7947
- const elements = Array.from(
7948
- this.panelElement.querySelectorAll(selectors)
7949
- );
7950
- const slots = this.panelElement.querySelectorAll("slot");
7951
- slots.forEach((slot) => {
7952
- const assigned = slot.assignedElements({ flatten: true });
7953
- assigned.forEach((el) => {
7954
- const focusableInSlot = el.querySelectorAll(selectors);
7955
- elements.push(...Array.from(focusableInSlot));
7956
- });
7957
- });
7958
- return elements;
7959
- }
7960
8363
  handleOverlayClick(e) {
7961
8364
  if (!this.dismissible || !this.closeOnBackdrop) return;
7962
8365
  if (e.target === this.overlayElement) {
@@ -8967,12 +9370,16 @@ var RelMenu = class extends import_lit24.LitElement {
8967
9370
  this.type = "default";
8968
9371
  this.items = [];
8969
9372
  this.focusedIndex = 0;
9373
+ this.subFocusedIndex = -1;
8970
9374
  this._items = [];
9375
+ /** Stable id prefix for aria-activedescendant targets. */
9376
+ this.menuId = `rel-menu-${Math.random().toString(36).slice(2)}`;
8971
9377
  /** Index of a submenu locked open by a click (survives hover-out). -1 = none. */
8972
9378
  this.lockedIndex = -1;
8973
9379
  this.handleDocumentClick = (e) => {
8974
9380
  if (!e.composedPath().includes(this)) {
8975
9381
  this.lockedIndex = -1;
9382
+ this.subFocusedIndex = -1;
8976
9383
  this._items = this._items.map((i) => ({ ...i, open: false }));
8977
9384
  }
8978
9385
  };
@@ -9059,35 +9466,95 @@ var RelMenu = class extends import_lit24.LitElement {
9059
9466
  })
9060
9467
  );
9061
9468
  }
9469
+ /** Keyboard-open locks the submenu (no hover-out to close it) and moves
9470
+ * virtual focus onto its first entry. */
9471
+ openSubmenuAt(index) {
9472
+ this.lockedIndex = index;
9473
+ this.subFocusedIndex = 0;
9474
+ this._items = this._items.map((i, idx) => ({ ...i, open: idx === index }));
9475
+ }
9476
+ closeSubmenus() {
9477
+ this.lockedIndex = -1;
9478
+ this.subFocusedIndex = -1;
9479
+ this._items = this._items.map((i) => ({ ...i, open: false }));
9480
+ }
9062
9481
  onKeydown(ev) {
9063
9482
  const last = this._items.length - 1;
9483
+ const focusedItem = this._items[this.focusedIndex];
9484
+ if (this.subFocusedIndex >= 0 && this.type === "arrow" && focusedItem?.open && focusedItem.submenu?.length) {
9485
+ const subLast = focusedItem.submenu.length - 1;
9486
+ switch (ev.key) {
9487
+ case "ArrowDown":
9488
+ this.subFocusedIndex = Math.min(subLast, this.subFocusedIndex + 1);
9489
+ ev.preventDefault();
9490
+ return;
9491
+ case "ArrowUp":
9492
+ this.subFocusedIndex = Math.max(0, this.subFocusedIndex - 1);
9493
+ ev.preventDefault();
9494
+ return;
9495
+ case "Home":
9496
+ this.subFocusedIndex = 0;
9497
+ ev.preventDefault();
9498
+ return;
9499
+ case "End":
9500
+ this.subFocusedIndex = subLast;
9501
+ ev.preventDefault();
9502
+ return;
9503
+ case "ArrowLeft":
9504
+ case "Escape":
9505
+ this.closeSubmenus();
9506
+ ev.preventDefault();
9507
+ ev.stopPropagation();
9508
+ return;
9509
+ case "Enter":
9510
+ case " ":
9511
+ this.onSubClick(this.focusedIndex, this.subFocusedIndex);
9512
+ ev.preventDefault();
9513
+ return;
9514
+ default:
9515
+ return;
9516
+ }
9517
+ }
9064
9518
  if (ev.key === "ArrowDown") {
9065
9519
  this.focusedIndex = Math.min(last, this.focusedIndex + 1);
9066
9520
  ev.preventDefault();
9067
9521
  } else if (ev.key === "ArrowUp") {
9068
9522
  this.focusedIndex = Math.max(0, this.focusedIndex - 1);
9069
9523
  ev.preventDefault();
9524
+ } else if (ev.key === "Home") {
9525
+ this.focusedIndex = 0;
9526
+ ev.preventDefault();
9527
+ } else if (ev.key === "End") {
9528
+ this.focusedIndex = last;
9529
+ ev.preventDefault();
9070
9530
  } else if (ev.key === "ArrowRight") {
9071
9531
  const item = this._items[this.focusedIndex];
9072
- if (this.type === "arrow" && item?.submenu?.length) {
9073
- this.lockedIndex = this.focusedIndex;
9074
- this._items = this._items.map((i, idx) => ({
9075
- ...i,
9076
- open: idx === this.focusedIndex ? true : i.open
9077
- }));
9532
+ if (this.type === "arrow" && item?.submenu?.length && !item.disabled) {
9533
+ this.openSubmenuAt(this.focusedIndex);
9534
+ ev.preventDefault();
9078
9535
  }
9079
9536
  } else if (ev.key === "ArrowLeft" || ev.key === "Escape") {
9080
- this.lockedIndex = -1;
9081
- this._items = this._items.map((i) => ({ ...i, open: false }));
9537
+ const anyOpen = this._items.some((i) => i.open);
9538
+ if (anyOpen) {
9539
+ this.closeSubmenus();
9540
+ ev.preventDefault();
9541
+ if (ev.key === "Escape") ev.stopPropagation();
9542
+ }
9082
9543
  } else if (ev.key === "Enter" || ev.key === " ") {
9083
9544
  const item = this._items[this.focusedIndex];
9084
- this.onItemClick(item, this.focusedIndex);
9545
+ if (this.type === "arrow" && item?.submenu?.length && !item.disabled) {
9546
+ this.openSubmenuAt(this.focusedIndex);
9547
+ } else {
9548
+ this.onItemClick(item, this.focusedIndex);
9549
+ }
9085
9550
  ev.preventDefault();
9086
9551
  }
9087
9552
  }
9088
9553
  renderMenuItem(item, index) {
9089
- const isFocused = this.focusedIndex === index;
9554
+ const isFocused = this.focusedIndex === index && this.subFocusedIndex < 0;
9090
9555
  const hasSubmenu = this.type === "arrow" && !!item.submenu?.length;
9556
+ const role = this.type === "checkbox" ? "menuitemcheckbox" : this.type === "select-option" ? "menuitemradio" : "menuitem";
9557
+ const ariaChecked = this.type === "checkbox" ? String(!!item.checked) : this.type === "select-option" ? String(!!item.active) : import_lit24.nothing;
9091
9558
  const control = this.type === "checkbox" ? import_lit24.html`<rel-checkbox
9092
9559
  class="menu-control"
9093
9560
  label="${item.label}"
@@ -9105,8 +9572,13 @@ var RelMenu = class extends import_lit24.LitElement {
9105
9572
  `;
9106
9573
  return import_lit24.html`
9107
9574
  <div
9575
+ id="${this.menuId}-item-${index}"
9108
9576
  class="rel-menu-item ${item.active ? "active" : ""} ${item.disabled ? "disabled" : ""}"
9109
- role="menuitem"
9577
+ role="${role}"
9578
+ aria-checked="${ariaChecked}"
9579
+ aria-disabled="${item.disabled ? "true" : import_lit24.nothing}"
9580
+ aria-haspopup="${hasSubmenu ? "menu" : import_lit24.nothing}"
9581
+ aria-expanded="${hasSubmenu ? String(!!item.open) : import_lit24.nothing}"
9110
9582
  ?data-focused="${isFocused}"
9111
9583
  @mouseenter="${() => this.onItemEnter(item, index)}"
9112
9584
  @mouseleave="${() => this.onItemLeave(item, index)}"
@@ -9115,12 +9587,16 @@ var RelMenu = class extends import_lit24.LitElement {
9115
9587
  ${hasSubmenu ? import_lit24.html`
9116
9588
  <div
9117
9589
  part="panel" class="rel-submenu ${item.open ? "visible" : ""}"
9590
+ role="menu"
9118
9591
  @click="${(e) => e.stopPropagation()}">
9119
9592
  ${item.submenu.map(
9120
9593
  (sub, subIndex) => import_lit24.html`
9121
9594
  <div
9122
- class="rel-menu-item ${sub.active ? "active" : ""}"
9595
+ id="${this.menuId}-item-${index}-sub-${subIndex}"
9596
+ class="rel-menu-item ${sub.active ? "active" : ""} ${sub.disabled ? "disabled" : ""}"
9123
9597
  role="menuitem"
9598
+ aria-disabled="${sub.disabled ? "true" : import_lit24.nothing}"
9599
+ ?data-focused="${this.focusedIndex === index && this.subFocusedIndex === subIndex}"
9124
9600
  @click="${(e) => this.onSubClick(index, subIndex, e)}">
9125
9601
  <span class="menu-label">${sub.label}</span>
9126
9602
  </div>
@@ -9132,12 +9608,14 @@ var RelMenu = class extends import_lit24.LitElement {
9132
9608
  `;
9133
9609
  }
9134
9610
  render() {
9611
+ const activeDescendant = this._items.length === 0 ? import_lit24.nothing : this.subFocusedIndex >= 0 ? `${this.menuId}-item-${this.focusedIndex}-sub-${this.subFocusedIndex}` : `${this.menuId}-item-${this.focusedIndex}`;
9135
9612
  return import_lit24.html`
9136
9613
  <style>${RelMenu._css}</style>
9137
9614
  <div
9138
9615
  part="list" class="rel-menu ${this.type === "fixed" || this.type === "fit" ? this.type : ""}"
9139
9616
  role="menu"
9140
9617
  tabindex="0"
9618
+ aria-activedescendant="${activeDescendant}"
9141
9619
  @keydown="${this.onKeydown}"
9142
9620
  @click="${(e) => e.stopPropagation()}">
9143
9621
  ${this._items.map((item, index) => this.renderMenuItem(item, index))}
@@ -9227,7 +9705,10 @@ RelMenu._css = `
9227
9705
  cursor: not-allowed;
9228
9706
  }
9229
9707
 
9230
- .rel-menu-item[data-focused="true"] {
9708
+ /* [data-focused] (presence, not ="true"): the ?data-focused boolean
9709
+ binding renders the attribute with an EMPTY value, so a ="true"
9710
+ selector never matches. */
9711
+ .rel-menu-item[data-focused] {
9231
9712
  outline: 2px solid var(--rel-border-focus);
9232
9713
  outline-offset: -2px;
9233
9714
  }
@@ -9318,6 +9799,9 @@ __decorateClass([
9318
9799
  __decorateClass([
9319
9800
  (0, import_decorators24.state)()
9320
9801
  ], RelMenu.prototype, "focusedIndex", 2);
9802
+ __decorateClass([
9803
+ (0, import_decorators24.state)()
9804
+ ], RelMenu.prototype, "subFocusedIndex", 2);
9321
9805
  __decorateClass([
9322
9806
  (0, import_decorators24.state)()
9323
9807
  ], RelMenu.prototype, "_items", 2);
@@ -9328,16 +9812,159 @@ RelMenu = __decorateClass([
9328
9812
  // src/components/rel-menubar/rel-menubar.ts
9329
9813
  var import_lit25 = require("lit");
9330
9814
  var import_decorators25 = require("lit/decorators.js");
9815
+
9816
+ // src/utils/shortcuts.ts
9817
+ function isApplePlatform() {
9818
+ if (typeof navigator === "undefined") return false;
9819
+ return /Mac|iPhone|iPad|iPod/i.test(navigator.platform || navigator.userAgent);
9820
+ }
9821
+ var KEY_ALIASES = {
9822
+ esc: "escape",
9823
+ space: " ",
9824
+ plus: "+",
9825
+ return: "enter",
9826
+ up: "arrowup",
9827
+ down: "arrowdown",
9828
+ left: "arrowleft",
9829
+ right: "arrowright"
9830
+ };
9831
+ function parseShortcut(shortcut) {
9832
+ const parsed = {
9833
+ key: "",
9834
+ ctrl: false,
9835
+ meta: false,
9836
+ alt: false,
9837
+ shift: false
9838
+ };
9839
+ const tokens = shortcut.split("+").map((t) => t.trim().toLowerCase()).filter(Boolean);
9840
+ if (shortcut.endsWith("+") && !shortcut.endsWith("++")) tokens.push("+");
9841
+ for (const token of tokens) {
9842
+ switch (token) {
9843
+ case "mod":
9844
+ if (isApplePlatform()) parsed.meta = true;
9845
+ else parsed.ctrl = true;
9846
+ break;
9847
+ case "ctrl":
9848
+ case "control":
9849
+ parsed.ctrl = true;
9850
+ break;
9851
+ case "meta":
9852
+ case "cmd":
9853
+ case "command":
9854
+ case "win":
9855
+ parsed.meta = true;
9856
+ break;
9857
+ case "alt":
9858
+ case "option":
9859
+ parsed.alt = true;
9860
+ break;
9861
+ case "shift":
9862
+ parsed.shift = true;
9863
+ break;
9864
+ default:
9865
+ parsed.key = KEY_ALIASES[token] ?? token;
9866
+ }
9867
+ }
9868
+ return parsed;
9869
+ }
9870
+ function matchesShortcut(e, shortcut) {
9871
+ const parsed = parseShortcut(shortcut);
9872
+ if (!parsed.key) return false;
9873
+ return e.key.toLowerCase() === parsed.key && e.ctrlKey === parsed.ctrl && e.metaKey === parsed.meta && e.altKey === parsed.alt && e.shiftKey === parsed.shift;
9874
+ }
9875
+ function isEditableTarget(e) {
9876
+ const target = e.composedPath()[0];
9877
+ if (!(target instanceof HTMLElement)) return false;
9878
+ if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement) {
9879
+ return true;
9880
+ }
9881
+ return target.isContentEditable;
9882
+ }
9883
+ function bindShortcut(shortcut, handler, options = {}) {
9884
+ const target = options.target ?? document;
9885
+ const listener = (event) => {
9886
+ const e = event;
9887
+ if (!matchesShortcut(e, shortcut)) return;
9888
+ if (!options.allowInEditable && isEditableTarget(e)) return;
9889
+ if (options.preventDefault !== false) e.preventDefault();
9890
+ handler(e);
9891
+ };
9892
+ target.addEventListener("keydown", listener);
9893
+ return () => target.removeEventListener("keydown", listener);
9894
+ }
9895
+ var APPLE_LABELS = {
9896
+ mod: "\u2318",
9897
+ meta: "\u2318",
9898
+ cmd: "\u2318",
9899
+ command: "\u2318",
9900
+ ctrl: "\u2303",
9901
+ control: "\u2303",
9902
+ alt: "\u2325",
9903
+ option: "\u2325",
9904
+ shift: "\u21E7"
9905
+ };
9906
+ var GENERIC_LABELS = {
9907
+ mod: "Ctrl",
9908
+ meta: "Win",
9909
+ cmd: "Win",
9910
+ command: "Win",
9911
+ ctrl: "Ctrl",
9912
+ control: "Ctrl",
9913
+ alt: "Alt",
9914
+ option: "Alt",
9915
+ shift: "Shift"
9916
+ };
9917
+ var KEY_LABELS = {
9918
+ arrowup: "\u2191",
9919
+ arrowdown: "\u2193",
9920
+ arrowleft: "\u2190",
9921
+ arrowright: "\u2192",
9922
+ escape: "Esc",
9923
+ " ": "Space"
9924
+ };
9925
+ function formatShortcutParts(shortcut) {
9926
+ const labels = isApplePlatform() ? APPLE_LABELS : GENERIC_LABELS;
9927
+ return shortcut.split("+").map((t) => t.trim()).filter(Boolean).map((token) => {
9928
+ const lower = token.toLowerCase();
9929
+ if (labels[lower]) return labels[lower];
9930
+ const key = KEY_ALIASES[lower] ?? lower;
9931
+ if (KEY_LABELS[key]) return KEY_LABELS[key];
9932
+ return key.length === 1 ? key.toUpperCase() : key[0].toUpperCase() + key.slice(1);
9933
+ });
9934
+ }
9935
+
9936
+ // src/components/rel-menubar/rel-menubar.ts
9331
9937
  var import_rel_icon10 = require("@reliability-design/icons/rel-icon");
9332
9938
  var RelMenubar = class extends import_lit25.LitElement {
9333
9939
  constructor() {
9334
9940
  super(...arguments);
9941
+ this.bindShortcuts = false;
9335
9942
  this.menus = [];
9336
9943
  this.openMenuIndex = -1;
9337
9944
  this.focusedTriggerIndex = 0;
9338
9945
  this.focusedItemIndex = -1;
9946
+ this.focusedSubItemIndex = -1;
9339
9947
  this.openSubmenuElement = null;
9340
9948
  this.hoverSwitchEnabled = false;
9949
+ /** Stable id prefix for aria-activedescendant targets (light DOM, so ids
9950
+ * must be unique per menubar instance on the page). */
9951
+ this.menubarId = `rel-menubar-${Math.random().toString(36).slice(2)}`;
9952
+ this.handleShortcutKeyDown = (e) => {
9953
+ if (!this.bindShortcuts || e.defaultPrevented) return;
9954
+ if (isEditableTarget(e)) return;
9955
+ for (const { menu } of this.menus) {
9956
+ for (const item of this.getFocusableItems(menu)) {
9957
+ const shortcut = item.getAttribute("data-shortcut");
9958
+ if (!shortcut) continue;
9959
+ if (item.disabled) continue;
9960
+ if (matchesShortcut(e, shortcut)) {
9961
+ e.preventDefault();
9962
+ this.handleItemClick(item);
9963
+ return;
9964
+ }
9965
+ }
9966
+ }
9967
+ };
9341
9968
  this.handleOutsideClick = (e) => {
9342
9969
  if (!e.composedPath().includes(this)) {
9343
9970
  this.closeAllMenus();
@@ -9351,6 +9978,7 @@ var RelMenubar = class extends import_lit25.LitElement {
9351
9978
  super.connectedCallback();
9352
9979
  this.discoverMenus();
9353
9980
  document.addEventListener("click", this.handleOutsideClick);
9981
+ document.addEventListener("keydown", this.handleShortcutKeyDown);
9354
9982
  this._onScroll = () => {
9355
9983
  this._repositionOpenMenu();
9356
9984
  this._repositionOpenSubmenu();
@@ -9360,6 +9988,7 @@ var RelMenubar = class extends import_lit25.LitElement {
9360
9988
  disconnectedCallback() {
9361
9989
  super.disconnectedCallback();
9362
9990
  document.removeEventListener("click", this.handleOutsideClick);
9991
+ document.removeEventListener("keydown", this.handleShortcutKeyDown);
9363
9992
  if (this._onScroll) {
9364
9993
  window.removeEventListener("scroll", this._onScroll, { capture: true });
9365
9994
  }
@@ -9393,12 +10022,26 @@ var RelMenubar = class extends import_lit25.LitElement {
9393
10022
  closeAllMenus() {
9394
10023
  this.openMenuIndex = -1;
9395
10024
  this.openSubmenuElement = null;
10025
+ this.focusedItemIndex = -1;
10026
+ this.focusedSubItemIndex = -1;
9396
10027
  this.hoverSwitchEnabled = false;
9397
10028
  this.requestUpdate();
9398
10029
  }
10030
+ /** Close everything and put DOM focus back on the trigger that owned the
10031
+ * open menu. (Capturing the index BEFORE closing — closeAllMenus resets
10032
+ * openMenuIndex to -1, which used to drop focus to <body>.) */
10033
+ closeAllMenusAndRefocusTrigger() {
10034
+ const idx = this.openMenuIndex;
10035
+ this.closeAllMenus();
10036
+ if (idx !== -1) {
10037
+ this.focusedTriggerIndex = idx;
10038
+ this.focusTrigger(idx);
10039
+ }
10040
+ }
9399
10041
  openMenu(index, triggerElement) {
9400
10042
  this.openMenuIndex = index;
9401
10043
  this.focusedItemIndex = -1;
10044
+ this.focusedSubItemIndex = -1;
9402
10045
  this.openSubmenuElement = null;
9403
10046
  this.requestUpdate();
9404
10047
  this.updateComplete.then(() => {
@@ -9440,27 +10083,20 @@ var RelMenubar = class extends import_lit25.LitElement {
9440
10083
  event.preventDefault();
9441
10084
  this.focusedTriggerIndex = Math.max(0, this.focusedTriggerIndex - 1);
9442
10085
  this.focusTrigger(this.focusedTriggerIndex);
9443
- } else if (key === "Enter" || key === " ") {
10086
+ } else if (key === "Enter" || key === " " || key === "ArrowDown") {
9444
10087
  event.preventDefault();
9445
10088
  const trigger = this.querySelector(
9446
10089
  `.menubar-trigger[data-index="${this.focusedTriggerIndex}"]`
9447
10090
  );
9448
10091
  if (trigger) {
9449
- this.openMenu(this.focusedTriggerIndex, trigger);
9450
- this.updateComplete.then(() => {
9451
- const focusTarget = this.querySelector(
9452
- "[autofocus], [role='menuitem']"
9453
- );
9454
- if (focusTarget) {
9455
- focusTarget.focus();
9456
- } else {
9457
- this.focusMenuItem(0);
9458
- }
9459
- });
10092
+ if (this.openMenuIndex !== this.focusedTriggerIndex) {
10093
+ this.openMenu(this.focusedTriggerIndex, trigger);
10094
+ }
10095
+ this.updateComplete.then(() => this.focusMenuItem(0));
9460
10096
  }
9461
- } else if (key === "ArrowDown" && this.openMenuIndex !== -1) {
10097
+ } else if (key === "Escape" && this.openMenuIndex !== -1) {
9462
10098
  event.preventDefault();
9463
- this.focusMenuItem(0);
10099
+ this.closeAllMenusAndRefocusTrigger();
9464
10100
  }
9465
10101
  }
9466
10102
  focusTrigger(index) {
@@ -9473,6 +10109,36 @@ var RelMenubar = class extends import_lit25.LitElement {
9473
10109
  const { key } = event;
9474
10110
  const menuContent = this.menus[this.openMenuIndex]?.menu;
9475
10111
  if (!menuContent) return;
10112
+ if (this.openSubmenuElement) {
10113
+ const subItems = this.getSubmenuItems(this.openSubmenuElement);
10114
+ if (key === "ArrowDown") {
10115
+ event.preventDefault();
10116
+ this.focusedSubItemIndex = Math.min(
10117
+ subItems.length - 1,
10118
+ this.focusedSubItemIndex + 1
10119
+ );
10120
+ } else if (key === "ArrowUp") {
10121
+ event.preventDefault();
10122
+ this.focusedSubItemIndex = Math.max(0, this.focusedSubItemIndex - 1);
10123
+ } else if (key === "Home") {
10124
+ event.preventDefault();
10125
+ this.focusedSubItemIndex = 0;
10126
+ } else if (key === "End") {
10127
+ event.preventDefault();
10128
+ this.focusedSubItemIndex = subItems.length - 1;
10129
+ } else if (key === "ArrowLeft" || key === "Escape") {
10130
+ event.preventDefault();
10131
+ event.stopPropagation();
10132
+ this.openSubmenuElement = null;
10133
+ this.focusedSubItemIndex = -1;
10134
+ } else if (key === "Enter" || key === " ") {
10135
+ event.preventDefault();
10136
+ const subItem = subItems[this.focusedSubItemIndex];
10137
+ if (subItem) this.handleSubmenuItemClick(subItem);
10138
+ }
10139
+ this.requestUpdate();
10140
+ return;
10141
+ }
9476
10142
  const items = this.getFocusableItems(menuContent);
9477
10143
  if (key === "ArrowDown") {
9478
10144
  event.preventDefault();
@@ -9482,30 +10148,56 @@ var RelMenubar = class extends import_lit25.LitElement {
9482
10148
  event.preventDefault();
9483
10149
  this.focusedItemIndex = Math.max(0, this.focusedItemIndex - 1);
9484
10150
  this.requestUpdate();
10151
+ } else if (key === "Home") {
10152
+ event.preventDefault();
10153
+ this.focusedItemIndex = 0;
10154
+ this.requestUpdate();
10155
+ } else if (key === "End") {
10156
+ event.preventDefault();
10157
+ this.focusedItemIndex = items.length - 1;
10158
+ this.requestUpdate();
9485
10159
  } else if (key === "ArrowRight") {
9486
10160
  event.preventDefault();
9487
10161
  const currentItem = items[this.focusedItemIndex];
9488
10162
  if (currentItem?.hasAttribute("data-submenu")) {
9489
- this.openSubmenu(currentItem);
10163
+ const panel = this.querySelector(
10164
+ `.menubar-panel[data-menu-index="${this.openMenuIndex}"]`
10165
+ );
10166
+ const renderedTrigger = panel?.querySelector(
10167
+ `[data-focus-index="${this.focusedItemIndex}"]`
10168
+ );
10169
+ this.focusedSubItemIndex = 0;
10170
+ this.openSubmenu(currentItem, renderedTrigger ?? void 0);
9490
10171
  }
9491
10172
  } else if (key === "ArrowLeft") {
9492
10173
  event.preventDefault();
9493
- if (this.openSubmenuElement) {
9494
- this.openSubmenuElement = null;
9495
- this.requestUpdate();
9496
- }
9497
10174
  } else if (key === "Escape") {
9498
10175
  event.preventDefault();
9499
- this.closeAllMenus();
9500
- this.focusTrigger(this.openMenuIndex);
10176
+ this.closeAllMenusAndRefocusTrigger();
9501
10177
  } else if (key === "Enter" || key === " ") {
9502
10178
  event.preventDefault();
9503
10179
  const currentItem = items[this.focusedItemIndex];
9504
10180
  if (currentItem) {
9505
- this.handleItemClick(currentItem);
10181
+ if (currentItem.hasAttribute("data-submenu")) {
10182
+ const panel = this.querySelector(
10183
+ `.menubar-panel[data-menu-index="${this.openMenuIndex}"]`
10184
+ );
10185
+ const renderedTrigger = panel?.querySelector(
10186
+ `[data-focus-index="${this.focusedItemIndex}"]`
10187
+ );
10188
+ this.focusedSubItemIndex = 0;
10189
+ this.openSubmenu(currentItem, renderedTrigger ?? void 0);
10190
+ } else {
10191
+ this.handleItemClick(currentItem);
10192
+ }
9506
10193
  }
9507
10194
  }
9508
10195
  }
10196
+ getSubmenuItems(submenuElement) {
10197
+ return Array.from(submenuElement.children).filter(
10198
+ (el) => el.hasAttribute("data-item")
10199
+ );
10200
+ }
9509
10201
  getFocusableItems(menuContent) {
9510
10202
  return Array.from(menuContent.children).filter(
9511
10203
  (el) => el.hasAttribute("data-item") || el.hasAttribute("data-checkbox") || el.hasAttribute("data-radio") || el.hasAttribute("data-submenu")
@@ -9542,27 +10234,41 @@ var RelMenubar = class extends import_lit25.LitElement {
9542
10234
  } else if (item.hasAttribute("data-submenu")) {
9543
10235
  this.openSubmenu(item);
9544
10236
  } else {
9545
- this.closeAllMenus();
10237
+ this.dispatchSelectEvent(item);
10238
+ this.closeAllMenusAndRefocusTrigger();
9546
10239
  }
9547
10240
  }
9548
- openSubmenu(submenuTrigger, renderedTrigger, submenuIndex) {
9549
- this.openSubmenuElement = submenuTrigger;
9550
- this.requestUpdate();
9551
- this.updateComplete.then(() => {
9552
- const submenuId = `submenu-${submenuIndex ?? 0}`;
9553
- const submenuPanel = this.querySelector(
9554
- `.submenu-panel[data-submenu-id="${submenuId}"]`
9555
- );
9556
- if (submenuPanel) {
9557
- const positionTarget = renderedTrigger ?? submenuTrigger;
9558
- const rect = positionTarget.getBoundingClientRect();
9559
- submenuPanel.style.top = `${rect.top}px`;
9560
- submenuPanel.style.left = `${rect.right + 4}px`;
10241
+ dispatchSelectEvent(item) {
10242
+ this.dispatchEvent(
10243
+ new CustomEvent("rel-menubar-select", {
10244
+ detail: {
10245
+ label: item.textContent?.trim() ?? "",
10246
+ value: item.getAttribute("value") ?? void 0,
10247
+ shortcut: item.getAttribute("data-shortcut") ?? void 0
10248
+ },
10249
+ bubbles: true,
10250
+ composed: true
10251
+ })
10252
+ );
10253
+ }
10254
+ openSubmenu(submenuTrigger, renderedTrigger) {
10255
+ this.openSubmenuElement = submenuTrigger;
10256
+ this.requestUpdate();
10257
+ this.updateComplete.then(() => {
10258
+ const submenuPanel = this.querySelector(".submenu-panel.open");
10259
+ if (submenuPanel) {
10260
+ const positionTarget = renderedTrigger ?? submenuPanel.previousElementSibling;
10261
+ const rect = positionTarget?.getBoundingClientRect();
10262
+ if (rect) {
10263
+ submenuPanel.style.top = `${rect.top}px`;
10264
+ submenuPanel.style.left = `${rect.right + 4}px`;
10265
+ }
9561
10266
  }
9562
10267
  });
9563
10268
  }
9564
- handleSubmenuItemClick(_item) {
9565
- this.closeAllMenus();
10269
+ handleSubmenuItemClick(item) {
10270
+ this.dispatchSelectEvent(item);
10271
+ this.closeAllMenusAndRefocusTrigger();
9566
10272
  }
9567
10273
  dispatchChangeEvent(type, labelOrName, value) {
9568
10274
  const detail = type === "checkbox" ? { type, label: labelOrName, name: "", checked: value } : { type, label: "", name: labelOrName, value };
@@ -9574,16 +10280,21 @@ var RelMenubar = class extends import_lit25.LitElement {
9574
10280
  })
9575
10281
  );
9576
10282
  }
9577
- renderMenuItem(item, index) {
9578
- const isFocused = index === this.focusedItemIndex;
10283
+ renderMenuItem(item, focusIndex, menuIndex) {
10284
+ const isFocused = focusIndex >= 0 && focusIndex === this.focusedItemIndex;
10285
+ const itemId = focusIndex >= 0 ? `${this.menubarId}-m${menuIndex}-i${focusIndex}` : import_lit25.nothing;
9579
10286
  if (item.hasAttribute("data-item")) {
9580
10287
  const button = item;
9581
10288
  const disabled = button.disabled;
9582
10289
  const shortcut = button.getAttribute("data-shortcut") || "";
9583
10290
  return import_lit25.html`
9584
10291
  <div
10292
+ id="${itemId}"
9585
10293
  class="menu-item ${isFocused ? "focused" : ""}"
10294
+ role="menuitem"
10295
+ data-focus-index="${focusIndex}"
9586
10296
  data-disabled="${disabled}"
10297
+ aria-disabled="${disabled ? "true" : import_lit25.nothing}"
9587
10298
  @click="${() => !disabled && this.handleItemClick(item)}"
9588
10299
  >
9589
10300
  <span>${button.textContent}</span>
@@ -9592,7 +10303,7 @@ var RelMenubar = class extends import_lit25.LitElement {
9592
10303
  `;
9593
10304
  }
9594
10305
  if (item.hasAttribute("data-separator")) {
9595
- return import_lit25.html`<div class="menu-separator"></div>`;
10306
+ return import_lit25.html`<div class="menu-separator" role="separator"></div>`;
9596
10307
  }
9597
10308
  if (item.hasAttribute("data-checkbox")) {
9598
10309
  const label = item;
@@ -9600,6 +10311,8 @@ var RelMenubar = class extends import_lit25.LitElement {
9600
10311
  const labelText = Array.from(label.childNodes).filter((n) => n.nodeType === Node.TEXT_NODE).map((n) => n.textContent?.trim()).filter(Boolean).join(" ") || label.getAttribute("label") || "";
9601
10312
  return import_lit25.html`
9602
10313
  <rel-checkbox
10314
+ id="${itemId}"
10315
+ data-focus-index="${focusIndex}"
9603
10316
  label=${labelText}
9604
10317
  .status=${checkbox?.checked ? "checked" : "unchecked"}
9605
10318
  @rel-change=${(e) => {
@@ -9611,17 +10324,21 @@ var RelMenubar = class extends import_lit25.LitElement {
9611
10324
  }
9612
10325
  if (item.hasAttribute("data-submenu")) {
9613
10326
  const label = item.getAttribute("label") || item.textContent?.trim() || "";
9614
- const submenuId = `submenu-${index}`;
9615
10327
  return import_lit25.html`
9616
10328
  <div
10329
+ id="${itemId}"
9617
10330
  class="menu-item menu-submenu-trigger ${isFocused ? "focused" : ""}"
9618
- @click="${(e) => this.openSubmenu(item, e.currentTarget, index)}"
9619
- @mouseenter="${(e) => this.openSubmenu(item, e.currentTarget, index)}"
10331
+ role="menuitem"
10332
+ data-focus-index="${focusIndex}"
10333
+ aria-haspopup="menu"
10334
+ aria-expanded="${this.openSubmenuElement === item}"
10335
+ @click="${(e) => this.openSubmenu(item, e.currentTarget)}"
10336
+ @mouseenter="${(e) => this.openSubmenu(item, e.currentTarget)}"
9620
10337
  >
9621
10338
  <span>${label}</span>
9622
10339
  <rel-icon name="chevron-right"></rel-icon>
9623
10340
  </div>
9624
- ${this.renderSubmenu(item, submenuId)}
10341
+ ${this.renderSubmenu(item)}
9625
10342
  `;
9626
10343
  }
9627
10344
  if (item.hasAttribute("data-radio-group")) {
@@ -9646,24 +10363,23 @@ var RelMenubar = class extends import_lit25.LitElement {
9646
10363
  }
9647
10364
  return null;
9648
10365
  }
9649
- renderSubmenu(submenuElement, submenuId) {
10366
+ renderSubmenu(submenuElement) {
9650
10367
  const isOpen = this.openSubmenuElement === submenuElement;
9651
- const submenuItems = Array.from(submenuElement.children);
10368
+ const submenuItems = this.getSubmenuItems(submenuElement);
9652
10369
  return import_lit25.html`
9653
- <div class="submenu-panel ${isOpen ? "open" : ""}" data-submenu-id="${submenuId}">
9654
- ${submenuItems.map((subItem) => {
9655
- if (subItem.hasAttribute("data-item")) {
9656
- const button = subItem;
9657
- return import_lit25.html`
9658
- <div
9659
- class="menu-item"
9660
- @click="${() => this.handleSubmenuItemClick(subItem)}"
9661
- >
9662
- <span>${button.textContent}</span>
9663
- </div>
9664
- `;
9665
- }
9666
- return null;
10370
+ <div class="submenu-panel ${isOpen ? "open" : ""}" role="menu">
10371
+ ${submenuItems.map((subItem, subIdx) => {
10372
+ const button = subItem;
10373
+ return import_lit25.html`
10374
+ <div
10375
+ id="${isOpen ? `${this.menubarId}-sub-${subIdx}` : import_lit25.nothing}"
10376
+ class="menu-item ${isOpen && subIdx === this.focusedSubItemIndex ? "focused" : ""}"
10377
+ role="menuitem"
10378
+ @click="${() => this.handleSubmenuItemClick(subItem)}"
10379
+ >
10380
+ <span>${button.textContent}</span>
10381
+ </div>
10382
+ `;
9667
10383
  })}
9668
10384
  </div>
9669
10385
  `;
@@ -9671,28 +10387,40 @@ var RelMenubar = class extends import_lit25.LitElement {
9671
10387
  renderMenuPanel(menuState, index) {
9672
10388
  const isOpen = index === this.openMenuIndex;
9673
10389
  const items = Array.from(menuState.menu.children);
10390
+ const focusables = this.getFocusableItems(menuState.menu);
10391
+ const activeDescendant = !isOpen ? import_lit25.nothing : this.focusedSubItemIndex >= 0 ? `${this.menubarId}-sub-${this.focusedSubItemIndex}` : this.focusedItemIndex >= 0 ? `${this.menubarId}-m${index}-i${this.focusedItemIndex}` : import_lit25.nothing;
9674
10392
  return import_lit25.html`
9675
10393
  <div
9676
10394
  class="menubar-panel ${isOpen ? "open" : ""}"
9677
10395
  data-menu-index="${index}"
9678
10396
  tabindex="-1"
10397
+ role="menu"
10398
+ aria-activedescendant="${activeDescendant}"
9679
10399
  @keydown="${this.handlePanelKeyDown}"
9680
10400
  >
9681
- ${items.map((item, idx) => this.renderMenuItem(item, idx))}
10401
+ ${items.map(
10402
+ (item) => this.renderMenuItem(item, focusables.indexOf(item), index)
10403
+ )}
9682
10404
  </div>
9683
10405
  `;
9684
10406
  }
9685
10407
  render() {
9686
10408
  return import_lit25.html`
9687
10409
  <style>${RelMenubar._css}</style>
9688
- <div class="menubar-container">
10410
+ <div class="menubar-container" role="menubar">
9689
10411
  ${this.menus.map(
9690
10412
  (menuState, index) => import_lit25.html`
9691
10413
  <button
9692
10414
  class="menubar-trigger ${index === this.openMenuIndex ? "active" : ""}"
9693
10415
  data-index="${index}"
10416
+ role="menuitem"
10417
+ aria-haspopup="menu"
10418
+ aria-expanded="${index === this.openMenuIndex}"
9694
10419
  @click="${(e) => this.handleTriggerClick(index, e)}"
9695
10420
  @mouseenter="${(e) => this.handleTriggerMouseEnter(index, e)}"
10421
+ @focus="${() => {
10422
+ this.focusedTriggerIndex = index;
10423
+ }}"
9696
10424
  @keydown="${this.handleTriggerKeyDown}"
9697
10425
  >
9698
10426
  ${menuState.menu.label}
@@ -9863,6 +10591,9 @@ RelMenubar._css = `
9863
10591
  cursor: pointer;
9864
10592
  }
9865
10593
  `;
10594
+ __decorateClass([
10595
+ (0, import_decorators25.property)({ type: Boolean, attribute: "bind-shortcuts" })
10596
+ ], RelMenubar.prototype, "bindShortcuts", 2);
9866
10597
  __decorateClass([
9867
10598
  (0, import_decorators25.state)()
9868
10599
  ], RelMenubar.prototype, "menus", 2);
@@ -9875,6 +10606,9 @@ __decorateClass([
9875
10606
  __decorateClass([
9876
10607
  (0, import_decorators25.state)()
9877
10608
  ], RelMenubar.prototype, "focusedItemIndex", 2);
10609
+ __decorateClass([
10610
+ (0, import_decorators25.state)()
10611
+ ], RelMenubar.prototype, "focusedSubItemIndex", 2);
9878
10612
  __decorateClass([
9879
10613
  (0, import_decorators25.state)()
9880
10614
  ], RelMenubar.prototype, "openSubmenuElement", 2);
@@ -10543,7 +11277,6 @@ RelTopbar.styles = import_lit34.css`
10543
11277
  color: var(--rel-topbar-item-color);
10544
11278
 
10545
11279
 
10546
-
10547
11280
  text-decoration: none;
10548
11281
  font-family: var(--rel-body-md-medium-font-family);
10549
11282
  font-weight: var(--rel-body-md-medium-font-weight);
@@ -10793,8 +11526,6 @@ RelTopbarItem.styles = import_lit35.css`
10793
11526
  color: var(--_item-color);
10794
11527
 
10795
11528
 
10796
-
10797
-
10798
11529
  text-decoration: none;
10799
11530
  white-space: nowrap;
10800
11531
  position: relative;
@@ -11026,9 +11757,7 @@ RelTopbarProfile.styles = import_lit36.css`
11026
11757
 
11027
11758
  .name {
11028
11759
 
11029
-
11030
11760
  color: var(--rel-text-default);
11031
-
11032
11761
  white-space: nowrap;
11033
11762
  overflow: hidden;
11034
11763
  text-overflow: ellipsis;
@@ -11047,9 +11776,7 @@ RelTopbarProfile.styles = import_lit36.css`
11047
11776
 
11048
11777
  .subtitle {
11049
11778
 
11050
-
11051
11779
  color: var(--rel-text-placeholder);
11052
-
11053
11780
  white-space: nowrap;
11054
11781
  overflow: hidden;
11055
11782
  text-overflow: ellipsis;
@@ -11074,9 +11801,7 @@ RelTopbarProfile.styles = import_lit36.css`
11074
11801
  padding: 2px var(--rel-spacing-2xs, 4px);
11075
11802
  border-radius: var(--rel-border-radius-xs, 2px);
11076
11803
 
11077
-
11078
11804
  text-transform: uppercase;
11079
-
11080
11805
  flex-shrink: 0;
11081
11806
  font-family: var(--rel-body-xs-semibold-font-family);
11082
11807
  font-weight: var(--rel-body-xs-semibold-font-weight);
@@ -11423,7 +12148,7 @@ var RelOtp = class extends import_lit37.LitElement {
11423
12148
  data-index="${index}"
11424
12149
  type="${this.mask ? "password" : "text"}"
11425
12150
  inputmode="${this.getInputMode()}"
11426
- autocomplete="new-password"
12151
+ autocomplete="one-time-code"
11427
12152
  pattern="${this.pattern || ""}"
11428
12153
  placeholder="${this.placeholder}"
11429
12154
  maxlength="1"
@@ -11486,7 +12211,6 @@ RelOtp.styles = import_lit37.css`
11486
12211
  align-items: center;
11487
12212
  justify-content: center;
11488
12213
  color: var(--rel-border-default);
11489
-
11490
12214
  user-select: none;
11491
12215
  padding: 0 var(--rel-spacing-3xs);
11492
12216
  font-family: var(--rel-action-xl-font-family);
@@ -11507,7 +12231,6 @@ RelOtp.styles = import_lit37.css`
11507
12231
  border-radius: var(--rel-border-radius-sm);
11508
12232
  background: var(--rel-bg-neutral);
11509
12233
 
11510
-
11511
12234
  text-align: center;
11512
12235
  color: var(--rel-text-default);
11513
12236
  outline: none;
@@ -12199,6 +12922,14 @@ var RelPopover = class extends import_lit39.LitElement {
12199
12922
  e.stopPropagation();
12200
12923
  this.setOpen(!this.activeOpen);
12201
12924
  };
12925
+ this.onHostKeyDown = (e) => {
12926
+ if (this.disabled) return;
12927
+ if (e.key !== "Enter" && e.key !== " ") return;
12928
+ if (!this.triggerEl || !e.composedPath().includes(this.triggerEl)) return;
12929
+ e.preventDefault();
12930
+ e.stopPropagation();
12931
+ this.setOpen(!this.activeOpen);
12932
+ };
12202
12933
  this.onOutsidePointerDown = (e) => {
12203
12934
  if (!this.activeOpen) return;
12204
12935
  const path = e.composedPath();
@@ -12272,10 +13003,12 @@ var RelPopover = class extends import_lit39.LitElement {
12272
13003
  super.connectedCallback();
12273
13004
  this.internalOpen = this.isControlled() ? !!this.open : this.defaultOpen;
12274
13005
  this.addEventListener("pointerdown", this.onHostPointerDown, true);
13006
+ this.addEventListener("keydown", this.onHostKeyDown, true);
12275
13007
  }
12276
13008
  disconnectedCallback() {
12277
13009
  super.disconnectedCallback();
12278
13010
  this.removeEventListener("pointerdown", this.onHostPointerDown, true);
13011
+ this.removeEventListener("keydown", this.onHostKeyDown, true);
12279
13012
  this.removeGlobalListeners();
12280
13013
  this.hideNativePopover();
12281
13014
  }
@@ -12316,7 +13049,7 @@ var RelPopover = class extends import_lit39.LitElement {
12316
13049
  * OPEN / CLOSE
12317
13050
  * ======================= */
12318
13051
  onOpen() {
12319
- this.returnFocusEl = document.activeElement ?? this.triggerEl;
13052
+ this.returnFocusEl = getDeepActiveElement() ?? this.triggerEl;
12320
13053
  this.addGlobalListeners();
12321
13054
  this.showNativePopover();
12322
13055
  requestAnimationFrame(() => {
@@ -12424,6 +13157,12 @@ var RelPopover = class extends import_lit39.LitElement {
12424
13157
  this.triggerEl.setAttribute("aria-haspopup", "dialog");
12425
13158
  this.triggerEl.setAttribute("aria-expanded", String(this.activeOpen));
12426
13159
  this.triggerEl.setAttribute("aria-controls", this.uniqueId);
13160
+ if (this.triggerEl.tabIndex < 0 && !this.triggerEl.hasAttribute("tabindex") && getFocusableElements(this.triggerEl).length === 0) {
13161
+ this.triggerEl.setAttribute("tabindex", "0");
13162
+ if (!this.triggerEl.hasAttribute("role")) {
13163
+ this.triggerEl.setAttribute("role", "button");
13164
+ }
13165
+ }
12427
13166
  }
12428
13167
  /* =======================
12429
13168
  * RENDER
@@ -12903,6 +13642,14 @@ var RelResizableGroup = class extends import_lit42.LitElement {
12903
13642
  const delta = e.key === "ArrowUp" ? -step : step;
12904
13643
  this.applyKeyboardResize(handleIndex, delta);
12905
13644
  applied = true;
13645
+ } else if (e.key === "Home" || e.key === "End") {
13646
+ const leftPanel = this.panels[handleIndex];
13647
+ if (leftPanel && handleIndex + 1 < this.panels.length) {
13648
+ e.preventDefault();
13649
+ const target2 = e.key === "Home" ? leftPanel.minSize : leftPanel.maxSize;
13650
+ this.applyKeyboardResize(handleIndex, target2 - leftPanel.currentSize);
13651
+ applied = true;
13652
+ }
12906
13653
  }
12907
13654
  if (applied) {
12908
13655
  this.dispatchResizeEvent("rel-resize-end");
@@ -12942,6 +13689,13 @@ var RelResizableGroup = class extends import_lit42.LitElement {
12942
13689
  });
12943
13690
  const sizes = this.calculateInitialSizes();
12944
13691
  this.applySizes(sizes);
13692
+ this.handles.forEach((handle, idx) => {
13693
+ const left = sizes[idx];
13694
+ const right = sizes[idx + 1];
13695
+ if (left !== void 0 && right !== void 0 && left + right > 0) {
13696
+ handle.updateAriaValue(left / (left + right) * 100);
13697
+ }
13698
+ });
12945
13699
  }
12946
13700
  calculateInitialSizes() {
12947
13701
  const sizes = [];
@@ -13320,6 +14074,26 @@ var RelSegmentedButton = class extends import_lit45.LitElement {
13320
14074
  this.active = "";
13321
14075
  this.size = "medium";
13322
14076
  this.align = "left";
14077
+ /** APG radio-group navigation: arrows move focus AND select, the group is
14078
+ * one Tab stop (roving tabindex on the selected segment). */
14079
+ this.onKeyDown = (e) => {
14080
+ const prevKey = e.key === "ArrowLeft" || e.key === "ArrowUp";
14081
+ const nextKey = e.key === "ArrowRight" || e.key === "ArrowDown";
14082
+ if (!prevKey && !nextKey && e.key !== "Home" && e.key !== "End") return;
14083
+ const enabled = this.options.filter((o) => !o.disabled);
14084
+ if (enabled.length === 0) return;
14085
+ e.preventDefault();
14086
+ const currentIdx = enabled.findIndex((o) => o.value === this.active);
14087
+ let target;
14088
+ if (e.key === "Home") target = 0;
14089
+ else if (e.key === "End") target = enabled.length - 1;
14090
+ else if (prevKey) target = currentIdx <= 0 ? enabled.length - 1 : currentIdx - 1;
14091
+ else target = (currentIdx + 1) % enabled.length;
14092
+ this.setActive(enabled[target]);
14093
+ this.updateComplete.then(() => {
14094
+ this.shadowRoot?.querySelector('button[tabindex="0"]')?.focus();
14095
+ });
14096
+ };
13323
14097
  }
13324
14098
  setActive(option) {
13325
14099
  if (!option.disabled) {
@@ -13333,7 +14107,7 @@ var RelSegmentedButton = class extends import_lit45.LitElement {
13333
14107
  );
13334
14108
  }
13335
14109
  }
13336
- renderSegment(option) {
14110
+ renderSegment(option, rovingValue) {
13337
14111
  const isActive = this.active === option.value;
13338
14112
  const classes = [
13339
14113
  "segment",
@@ -13343,6 +14117,9 @@ var RelSegmentedButton = class extends import_lit45.LitElement {
13343
14117
  return import_lit45.html`
13344
14118
  <button
13345
14119
  part="option" class="${classes}"
14120
+ role="radio"
14121
+ aria-checked="${isActive}"
14122
+ tabindex="${!option.disabled && option.value === rovingValue ? 0 : -1}"
13346
14123
  ?disabled="${option.disabled}"
13347
14124
  @click="${() => this.setActive(option)}">
13348
14125
  ${option.label}
@@ -13355,9 +14132,15 @@ var RelSegmentedButton = class extends import_lit45.LitElement {
13355
14132
  this.size,
13356
14133
  `align-${this.align}`
13357
14134
  ].join(" ");
14135
+ const enabled = this.options.filter((o) => !o.disabled);
14136
+ const rovingValue = enabled.some((o) => o.value === this.active) ? this.active : enabled[0]?.value ?? null;
13358
14137
  return import_lit45.html`
13359
- <div part="base" class="${containerClasses}">
13360
- ${this.options.map((option) => this.renderSegment(option))}
14138
+ <div
14139
+ part="base" class="${containerClasses}"
14140
+ role="radiogroup"
14141
+ @keydown="${this.onKeyDown}"
14142
+ >
14143
+ ${this.options.map((option) => this.renderSegment(option, rovingValue))}
13361
14144
  </div>
13362
14145
  `;
13363
14146
  }
@@ -13519,6 +14302,8 @@ var RelSelect = class extends import_lit46.LitElement {
13519
14302
  this.noSearch = false;
13520
14303
  this.open = false;
13521
14304
  this.highlightedIndex = -1;
14305
+ this.selectAllHighlighted = false;
14306
+ this.typeaheadBuffer = "";
13522
14307
  this.panelStyle = "";
13523
14308
  this.searchValue = "";
13524
14309
  this.internalState = "default";
@@ -13567,6 +14352,7 @@ var RelSelect = class extends import_lit46.LitElement {
13567
14352
  this.onSearchInput = (e) => {
13568
14353
  this.searchValue = e.target.value;
13569
14354
  this.highlightedIndex = this.filteredOptions.length > 0 ? 0 : -1;
14355
+ this.selectAllHighlighted = false;
13570
14356
  requestAnimationFrame(() => this.updatePosition());
13571
14357
  };
13572
14358
  }
@@ -13694,6 +14480,7 @@ var RelSelect = class extends import_lit46.LitElement {
13694
14480
  this.open = true;
13695
14481
  this.searchValue = "";
13696
14482
  this.highlightedIndex = -1;
14483
+ this.selectAllHighlighted = false;
13697
14484
  this.addGlobalListeners();
13698
14485
  requestAnimationFrame(() => {
13699
14486
  this.showNativePopover();
@@ -13769,6 +14556,11 @@ var RelSelect = class extends import_lit46.LitElement {
13769
14556
  }
13770
14557
  onTriggerKeyDown(e) {
13771
14558
  if (this.state === "disabled") return;
14559
+ if (this.multiple && (e.key === "Backspace" || e.key === "Delete") && this.selectedValues.length > 0) {
14560
+ e.preventDefault();
14561
+ this.removeChip(this.selectedValues[this.selectedValues.length - 1]);
14562
+ return;
14563
+ }
13772
14564
  if (!this.open) {
13773
14565
  if (["ArrowDown", "ArrowUp", "Enter", " "].includes(e.key)) {
13774
14566
  e.preventDefault();
@@ -13779,26 +14571,60 @@ var RelSelect = class extends import_lit46.LitElement {
13779
14571
  if (this.noSearch) this.handleListNavigation(e);
13780
14572
  }
13781
14573
  onSearchKeyDown(e) {
14574
+ if (this.multiple && e.key === "Backspace" && this.searchValue === "" && this.selectedValues.length > 0) {
14575
+ this.removeChip(this.selectedValues[this.selectedValues.length - 1]);
14576
+ return;
14577
+ }
13782
14578
  this.handleListNavigation(e);
13783
14579
  }
13784
14580
  handleListNavigation(e) {
13785
14581
  const filtered = this.filteredOptions;
14582
+ const hasSelectAll = this.multiple && filtered.length > 0;
13786
14583
  switch (e.key) {
13787
14584
  case "ArrowDown":
13788
14585
  e.preventDefault();
13789
- this.highlightedIndex = Math.min(filtered.length - 1, this.highlightedIndex + 1);
14586
+ if (this.selectAllHighlighted) {
14587
+ this.selectAllHighlighted = false;
14588
+ this.highlightedIndex = filtered.length > 0 ? 0 : -1;
14589
+ } else if (this.highlightedIndex === -1 && hasSelectAll) {
14590
+ this.selectAllHighlighted = true;
14591
+ } else {
14592
+ this.highlightedIndex = Math.min(filtered.length - 1, this.highlightedIndex + 1);
14593
+ }
14594
+ this.scrollHighlightedIntoView();
13790
14595
  break;
13791
14596
  case "ArrowUp":
13792
14597
  e.preventDefault();
13793
- this.highlightedIndex = Math.max(0, this.highlightedIndex - 1);
14598
+ if (!this.selectAllHighlighted && this.highlightedIndex <= 0 && hasSelectAll) {
14599
+ this.highlightedIndex = -1;
14600
+ this.selectAllHighlighted = true;
14601
+ } else {
14602
+ this.highlightedIndex = Math.max(0, this.highlightedIndex - 1);
14603
+ }
14604
+ this.scrollHighlightedIntoView();
14605
+ break;
14606
+ case "Home":
14607
+ if (filtered.length === 0) break;
14608
+ e.preventDefault();
14609
+ this.selectAllHighlighted = false;
14610
+ this.highlightedIndex = 0;
14611
+ this.scrollHighlightedIntoView();
14612
+ break;
14613
+ case "End":
14614
+ if (filtered.length === 0) break;
14615
+ e.preventDefault();
14616
+ this.selectAllHighlighted = false;
14617
+ this.highlightedIndex = filtered.length - 1;
14618
+ this.scrollHighlightedIntoView();
14619
+ break;
14620
+ case " ":
14621
+ if (!this.noSearch) return;
14622
+ e.preventDefault();
14623
+ this.activateHighlighted();
13794
14624
  break;
13795
14625
  case "Enter":
13796
14626
  e.preventDefault();
13797
- if (this.highlightedIndex >= 0 && filtered[this.highlightedIndex]) {
13798
- const opt = filtered[this.highlightedIndex];
13799
- if (this.multiple) this.toggleMultiOption(opt);
13800
- else this.selectSingleOption(opt);
13801
- }
14627
+ this.activateHighlighted();
13802
14628
  break;
13803
14629
  case "Escape":
13804
14630
  e.preventDefault();
@@ -13807,8 +14633,55 @@ var RelSelect = class extends import_lit46.LitElement {
13807
14633
  case "Tab":
13808
14634
  this.closeListbox();
13809
14635
  break;
14636
+ default:
14637
+ this.handleTypeahead(e);
14638
+ break;
14639
+ }
14640
+ }
14641
+ /** APG typeahead for the no-search mode (with a search field, typing
14642
+ * filters instead). Accumulates printable keys for 500 ms and jumps the
14643
+ * highlight to the next option whose label starts with the buffer. */
14644
+ handleTypeahead(e) {
14645
+ if (!this.noSearch) return;
14646
+ if (e.key.length !== 1 || e.ctrlKey || e.metaKey || e.altKey) return;
14647
+ e.preventDefault();
14648
+ const isNewQuery = this.typeaheadBuffer === "";
14649
+ this.typeaheadBuffer += e.key.toLowerCase();
14650
+ clearTimeout(this.typeaheadTimer);
14651
+ this.typeaheadTimer = window.setTimeout(() => {
14652
+ this.typeaheadBuffer = "";
14653
+ }, 500);
14654
+ const filtered = this.filteredOptions;
14655
+ if (filtered.length === 0) return;
14656
+ const start = Math.max(0, this.highlightedIndex) + (isNewQuery ? 1 : 0);
14657
+ for (let offset = 0; offset < filtered.length; offset++) {
14658
+ const idx = (start + offset) % filtered.length;
14659
+ if (filtered[idx].label.toLowerCase().startsWith(this.typeaheadBuffer)) {
14660
+ this.selectAllHighlighted = false;
14661
+ this.highlightedIndex = idx;
14662
+ this.scrollHighlightedIntoView();
14663
+ return;
14664
+ }
13810
14665
  }
13811
14666
  }
14667
+ activateHighlighted() {
14668
+ if (this.selectAllHighlighted) {
14669
+ this.toggleSelectAll();
14670
+ return;
14671
+ }
14672
+ const filtered = this.filteredOptions;
14673
+ if (this.highlightedIndex >= 0 && filtered[this.highlightedIndex]) {
14674
+ const opt = filtered[this.highlightedIndex];
14675
+ if (this.multiple) this.toggleMultiOption(opt);
14676
+ else this.selectSingleOption(opt);
14677
+ }
14678
+ }
14679
+ scrollHighlightedIntoView() {
14680
+ if (this.highlightedIndex < 0) return;
14681
+ requestAnimationFrame(() => {
14682
+ this.renderRoot.querySelector(`#${this.uniqueId}-option-${this.highlightedIndex}`)?.scrollIntoView({ block: "nearest" });
14683
+ });
14684
+ }
13812
14685
  renderChips() {
13813
14686
  const values = this.selectedValues;
13814
14687
  const visible = values.slice(0, Math.min(this.visibleChipCount, this.maxVisibleChips));
@@ -13886,7 +14759,7 @@ var RelSelect = class extends import_lit46.LitElement {
13886
14759
  this.open ? "open" : ""
13887
14760
  ].filter(Boolean).join(" ");
13888
14761
  const listboxId = `${this.uniqueId}-listbox`;
13889
- const activeOptionId = this.open && this.highlightedIndex >= 0 ? `${this.uniqueId}-option-${this.highlightedIndex}` : "";
14762
+ const activeOptionId = !this.open ? "" : this.selectAllHighlighted ? `${this.uniqueId}-select-all` : this.highlightedIndex >= 0 ? `${this.uniqueId}-option-${this.highlightedIndex}` : "";
13890
14763
  const selectedSingle = opts.find((o) => o.value === this.selected);
13891
14764
  return import_lit46.html`
13892
14765
  <div class="${wrapperClasses}">
@@ -13964,7 +14837,17 @@ var RelSelect = class extends import_lit46.LitElement {
13964
14837
  />
13965
14838
  </div>
13966
14839
  </div>` : ""}
13967
- ${this.multiple && filtered.length > 0 ? import_lit46.html`<div class="select-all-row">
14840
+ ${this.multiple && filtered.length > 0 ? import_lit46.html`<div
14841
+ id="${this.uniqueId}-select-all"
14842
+ class="select-all-row ${this.selectAllHighlighted ? "highlighted" : ""}"
14843
+ role="option"
14844
+ aria-selected="${this.selectAllStatus === "checked"}"
14845
+ @pointerenter="${() => {
14846
+ this.selectAllHighlighted = true;
14847
+ this.highlightedIndex = -1;
14848
+ }}"
14849
+ @pointerleave="${() => this.selectAllHighlighted = false}"
14850
+ >
13968
14851
  <rel-checkbox
13969
14852
  label="${this.selectAllLabel}"
13970
14853
  status="${this.selectAllStatus}"
@@ -13989,7 +14872,10 @@ var RelSelect = class extends import_lit46.LitElement {
13989
14872
  class="checkbox-option ${index === this.highlightedIndex ? "highlighted" : ""}"
13990
14873
  role="option"
13991
14874
  aria-selected="${this.selectedValues.includes(option.value)}"
13992
- @pointerenter="${() => this.highlightedIndex = index}"
14875
+ @pointerenter="${() => {
14876
+ this.highlightedIndex = index;
14877
+ this.selectAllHighlighted = false;
14878
+ }}"
13993
14879
  >
13994
14880
  <rel-checkbox
13995
14881
  label="${option.label}"
@@ -14488,6 +15374,10 @@ RelSelect.styles = import_lit46.css`
14488
15374
  padding: 12px;
14489
15375
  }
14490
15376
 
15377
+ .select-all-row.highlighted {
15378
+ background: var(--rel-select-option-hover, var(--rel-bg-neutral-elevated-1));
15379
+ }
15380
+
14491
15381
  .select-empty {
14492
15382
  padding: var(--rel-spacing-xs) 12px;
14493
15383
  color: var(--rel-select-placeholder-color, var(--rel-text-body));
@@ -14562,6 +15452,9 @@ __decorateClass([
14562
15452
  __decorateClass([
14563
15453
  (0, import_decorators46.state)()
14564
15454
  ], RelSelect.prototype, "highlightedIndex", 2);
15455
+ __decorateClass([
15456
+ (0, import_decorators46.state)()
15457
+ ], RelSelect.prototype, "selectAllHighlighted", 2);
14565
15458
  __decorateClass([
14566
15459
  (0, import_decorators46.state)()
14567
15460
  ], RelSelect.prototype, "panelStyle", 2);
@@ -14759,6 +15652,10 @@ var RelModal = class extends import_lit48.LitElement {
14759
15652
  if (e.key === "Escape" && this.dismissible) {
14760
15653
  e.preventDefault();
14761
15654
  this.open = false;
15655
+ return;
15656
+ }
15657
+ if (e.key === "Tab" && this.modal && this.panelElement) {
15658
+ trapTabKey(e, this.panelElement);
14762
15659
  }
14763
15660
  };
14764
15661
  this.handleOverlayClick = (e) => {
@@ -14809,9 +15706,18 @@ var RelModal = class extends import_lit48.LitElement {
14809
15706
  }
14810
15707
  }
14811
15708
  handleOpen() {
14812
- this.previousFocus = document.activeElement;
15709
+ this.previousFocus = getDeepActiveElement();
14813
15710
  this.lockScroll();
14814
15711
  document.addEventListener("keydown", this.handleKeydown);
15712
+ if (this.modal) {
15713
+ this.updateComplete.then(() => {
15714
+ const panel = this.panelElement;
15715
+ if (!panel) return;
15716
+ const focusTarget = getFocusableElements(panel)[0];
15717
+ if (focusTarget) focusTarget.focus();
15718
+ else panel.focus();
15719
+ });
15720
+ }
14815
15721
  this.dispatchEvent(
14816
15722
  new CustomEvent("rel-open-change", {
14817
15723
  detail: true,
@@ -15171,6 +16077,9 @@ __decorateClass([
15171
16077
  __decorateClass([
15172
16078
  (0, import_decorators48.query)(".overlay")
15173
16079
  ], RelModal.prototype, "overlayElement", 2);
16080
+ __decorateClass([
16081
+ (0, import_decorators48.query)(".panel")
16082
+ ], RelModal.prototype, "panelElement", 2);
15174
16083
  RelModal = __decorateClass([
15175
16084
  (0, import_decorators48.customElement)("rel-modal")
15176
16085
  ], RelModal);
@@ -16186,6 +17095,28 @@ var RelStepper = class extends import_lit51.LitElement {
16186
17095
  if (this.isDisabled(step)) classes.push("disabled");
16187
17096
  return classes.join(" ");
16188
17097
  }
17098
+ onStepKeyDown(e, step, index) {
17099
+ if (e.key === "Enter" || e.key === " ") {
17100
+ e.preventDefault();
17101
+ this.onStepClick(step, index);
17102
+ return;
17103
+ }
17104
+ const prevKey = e.key === "ArrowLeft" || e.key === "ArrowUp";
17105
+ const nextKey = e.key === "ArrowRight" || e.key === "ArrowDown";
17106
+ if (!prevKey && !nextKey && e.key !== "Home" && e.key !== "End") return;
17107
+ const steps = Array.from(
17108
+ this.renderRoot.querySelectorAll(".rel-step:not(.disabled)")
17109
+ );
17110
+ if (steps.length === 0) return;
17111
+ e.preventDefault();
17112
+ const current = steps.indexOf(e.currentTarget);
17113
+ let target;
17114
+ if (e.key === "Home") target = 0;
17115
+ else if (e.key === "End") target = steps.length - 1;
17116
+ else if (prevKey) target = (current - 1 + steps.length) % steps.length;
17117
+ else target = (current + 1) % steps.length;
17118
+ steps[target]?.focus();
17119
+ }
16189
17120
  onStepClick(step, index) {
16190
17121
  if (step.status === "disabled") return;
16191
17122
  this.dispatchEvent(
@@ -16238,7 +17169,12 @@ var RelStepper = class extends import_lit51.LitElement {
16238
17169
  (step, idx) => import_lit51.html`
16239
17170
  <div
16240
17171
  class="${this.getStepClasses(step)}"
17172
+ role="button"
17173
+ tabindex="${this.isDisabled(step) ? -1 : 0}"
17174
+ aria-disabled="${this.isDisabled(step) ? "true" : import_lit51.nothing}"
17175
+ aria-current="${this.isProgress(step) ? "step" : import_lit51.nothing}"
16241
17176
  @click="${() => this.onStepClick(step, idx)}"
17177
+ @keydown="${(e) => this.onStepKeyDown(e, step, idx)}"
16242
17178
  >
16243
17179
  ${isVertical && isTextRight ? import_lit51.html`
16244
17180
  <!-- Indicator with connector below -->
@@ -16325,6 +17261,17 @@ RelStepper.styles = import_lit51.css`
16325
17261
  .rel-step {
16326
17262
  display: flex;
16327
17263
  position: relative;
17264
+ cursor: pointer;
17265
+ }
17266
+
17267
+ .rel-step.disabled {
17268
+ cursor: not-allowed;
17269
+ }
17270
+
17271
+ .rel-step:focus-visible {
17272
+ outline: 2px solid var(--rel-border-focus);
17273
+ outline-offset: 2px;
17274
+ border-radius: var(--rel-border-radius-xs);
16328
17275
  }
16329
17276
 
16330
17277
  .rel-stepper.horizontal.text-below .rel-step {
@@ -16901,6 +17848,8 @@ var RelTab = class extends import_lit53.LitElement {
16901
17848
  this.activeIndex = 0;
16902
17849
  this.size = "md";
16903
17850
  this._tabItems = [];
17851
+ /** Stable id prefix linking tabs to the shared tabpanel. */
17852
+ this.tabsId = `rel-tab-${Math.random().toString(36).slice(2)}`;
16904
17853
  }
16905
17854
  _onSlotChange(e) {
16906
17855
  const slot = e.target;
@@ -16944,12 +17893,24 @@ var RelTab = class extends import_lit53.LitElement {
16944
17893
  next = (next + dir + tabs.length) % tabs.length;
16945
17894
  if (!tabs[next]?.disabled) break;
16946
17895
  }
16947
- this.selectTab(next);
16948
- this.updateComplete.then(() => {
16949
- const tabEls = this.shadowRoot?.querySelectorAll("[role=tab]");
16950
- tabEls?.[next]?.focus();
16951
- });
17896
+ this.selectAndFocusTab(next);
17897
+ return;
16952
17898
  }
17899
+ if (event.key === "Home" || event.key === "End") {
17900
+ event.preventDefault();
17901
+ const tabs = this._effectiveTabs;
17902
+ const order = [...tabs.keys()];
17903
+ if (event.key === "End") order.reverse();
17904
+ const next = order.find((i) => !tabs[i]?.disabled);
17905
+ if (next !== void 0) this.selectAndFocusTab(next);
17906
+ }
17907
+ }
17908
+ selectAndFocusTab(index) {
17909
+ this.selectTab(index);
17910
+ this.updateComplete.then(() => {
17911
+ const tabEls = this.shadowRoot?.querySelectorAll("[role=tab]");
17912
+ tabEls?.[index]?.focus();
17913
+ });
16953
17914
  }
16954
17915
  render() {
16955
17916
  const tabs = this._effectiveTabs;
@@ -16960,9 +17921,11 @@ var RelTab = class extends import_lit53.LitElement {
16960
17921
  (tab, i) => import_lit53.html`
16961
17922
  <div
16962
17923
  part="item" class="tab ${i === this.activeIndex ? "active" : ""} ${tab.disabled ? "disabled" : ""}"
17924
+ id="${this.tabsId}-tab-${i}"
16963
17925
  role="tab"
16964
17926
  aria-selected="${i === this.activeIndex}"
16965
17927
  aria-disabled="${tab.disabled ?? false}"
17928
+ aria-controls="${this.tabsId}-panel"
16966
17929
  tabindex="${tab.disabled ? -1 : i === this.activeIndex ? 0 : -1}"
16967
17930
  @click="${() => this.selectTab(i)}"
16968
17931
  @keydown="${(e) => this.onKeyDown(e, i)}"
@@ -16974,7 +17937,13 @@ var RelTab = class extends import_lit53.LitElement {
16974
17937
  `
16975
17938
  )}
16976
17939
  </div>
16977
- <div class="tab-content">
17940
+ <div
17941
+ class="tab-content"
17942
+ id="${this.tabsId}-panel"
17943
+ role="tabpanel"
17944
+ aria-labelledby="${this.tabsId}-tab-${this.activeIndex}"
17945
+ tabindex="0"
17946
+ >
16978
17947
  <slot></slot>
16979
17948
  </div>
16980
17949
  `;
@@ -17005,7 +17974,6 @@ RelTab.styles = import_lit53.css`
17005
17974
  padding: var(--rel-spacing-sm) var(--rel-spacing-md);
17006
17975
  cursor: pointer;
17007
17976
  color: var(--rel-text-body);
17008
-
17009
17977
  transition: color 0.2s ease;
17010
17978
  display: flex;
17011
17979
  align-items: center;
@@ -17123,7 +18091,8 @@ var RelTimePicker = class extends import_lit54.LitElement {
17123
18091
  this.required = false;
17124
18092
  this.readonly = false;
17125
18093
  this.format = "12h";
17126
- this.minuteStep = 1;
18094
+ this.seconds = false;
18095
+ this.minuteStep = 5;
17127
18096
  this.minTime = null;
17128
18097
  this.maxTime = null;
17129
18098
  this.open = false;
@@ -17135,6 +18104,8 @@ var RelTimePicker = class extends import_lit54.LitElement {
17135
18104
  this.fullWidth = false;
17136
18105
  this.draftHour = null;
17137
18106
  this.draftMinute = null;
18107
+ this.draftSecond = null;
18108
+ this.openUp = false;
17138
18109
  this.handleClickOutside = (e) => {
17139
18110
  if (!this.open) return;
17140
18111
  const path = e.composedPath();
@@ -17159,40 +18130,58 @@ var RelTimePicker = class extends import_lit54.LitElement {
17159
18130
  document.removeEventListener("keydown", this.handleEscapeKey);
17160
18131
  }
17161
18132
  updated(changedProperties) {
17162
- if (changedProperties.has("open") && this.open) {
18133
+ if (changedProperties.has("open")) {
18134
+ if (!this.open) {
18135
+ this.openUp = false;
18136
+ return;
18137
+ }
17163
18138
  this.resetDraftFromValue();
17164
18139
  setTimeout(() => {
18140
+ this.updatePanelPlacement();
17165
18141
  this.scrollColumnsToSelection();
17166
18142
  const target = this.columnsElement?.querySelector(
17167
18143
  ".option-button.selected:not(:disabled)"
17168
18144
  ) ?? this.columnsElement?.querySelector(
17169
18145
  ".option-button:not(:disabled)"
17170
18146
  );
17171
- target?.focus();
18147
+ target?.focus({ preventScroll: true });
17172
18148
  }, 0);
17173
18149
  }
17174
18150
  }
18151
+ /** Open upward when the panel doesn't fit below but does fit above. */
18152
+ updatePanelPlacement() {
18153
+ const panel = this.renderRoot.querySelector(".popover");
18154
+ if (!panel) return;
18155
+ const rect = this.getBoundingClientRect();
18156
+ const panelHeight = panel.offsetHeight;
18157
+ const spaceBelow = window.innerHeight - rect.bottom;
18158
+ this.openUp = spaceBelow < panelHeight + 8 && rect.top > panelHeight + 8;
18159
+ }
17175
18160
  // ── Parsing / formatting ──
17176
18161
  parseTime(time) {
17177
18162
  if (!time) return null;
17178
- const match = time.match(/^(\d{1,2}):(\d{2})$/);
18163
+ const match = time.match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?$/);
17179
18164
  if (!match) return null;
17180
18165
  const hour = parseInt(match[1], 10);
17181
18166
  const minute = parseInt(match[2], 10);
17182
- if (hour > 23 || minute > 59) return null;
17183
- return { hour, minute };
18167
+ const second = match[3] ? parseInt(match[3], 10) : 0;
18168
+ if (hour > 23 || minute > 59 || second > 59) return null;
18169
+ return { hour, minute, second };
17184
18170
  }
17185
- formatValue(hour, minute) {
17186
- return `${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}`;
18171
+ formatValue(hour, minute, second = 0) {
18172
+ const hh = String(hour).padStart(2, "0");
18173
+ const mm = String(minute).padStart(2, "0");
18174
+ return this.seconds ? `${hh}:${mm}:${String(second).padStart(2, "0")}` : `${hh}:${mm}`;
17187
18175
  }
17188
- formatDisplay(hour, minute) {
18176
+ formatDisplay(hour, minute, second = 0) {
17189
18177
  const mm = String(minute).padStart(2, "0");
18178
+ const ss = this.seconds ? `:${String(second).padStart(2, "0")}` : "";
17190
18179
  if (this.format === "24h") {
17191
- return `${String(hour).padStart(2, "0")}:${mm}`;
18180
+ return `${String(hour).padStart(2, "0")}:${mm}${ss}`;
17192
18181
  }
17193
18182
  const period = hour < 12 ? "AM" : "PM";
17194
18183
  const hour12 = hour % 12 === 0 ? 12 : hour % 12;
17195
- return `${hour12}:${mm} ${period}`;
18184
+ return `${hour12}:${mm}${ss} ${period}`;
17196
18185
  }
17197
18186
  toMinutes(hour, minute) {
17198
18187
  return hour * 60 + minute;
@@ -17242,10 +18231,17 @@ var RelTimePicker = class extends import_lit54.LitElement {
17242
18231
  }
17243
18232
  const period = this.draftPeriod ?? "AM";
17244
18233
  const base = period === "AM" ? 0 : 12;
17245
- return Array.from({ length: 12 }, (_, i) => ({
17246
- label: i === 0 ? "12" : String(i).padStart(2, "0"),
17247
- hour24: base + i
17248
- }));
18234
+ return Array.from({ length: 12 }, (_, i) => {
18235
+ const display = i + 1;
18236
+ return {
18237
+ label: String(display).padStart(2, "0"),
18238
+ hour24: base + display % 12
18239
+ };
18240
+ });
18241
+ }
18242
+ /** Seconds shown in the seconds column (always step 1). */
18243
+ getSecondOptions() {
18244
+ return Array.from({ length: 60 }, (_, s) => s);
17249
18245
  }
17250
18246
  get draftPeriod() {
17251
18247
  if (this.draftHour === null) return null;
@@ -17255,6 +18251,7 @@ var RelTimePicker = class extends import_lit54.LitElement {
17255
18251
  const parsed = this.parseTime(this.value);
17256
18252
  this.draftHour = parsed ? parsed.hour : null;
17257
18253
  this.draftMinute = parsed ? parsed.minute : null;
18254
+ this.draftSecond = parsed ? parsed.second : null;
17258
18255
  }
17259
18256
  /** Snap the draft minute to the nearest valid step minute for the hour. */
17260
18257
  clampDraftToRange() {
@@ -17297,7 +18294,11 @@ var RelTimePicker = class extends import_lit54.LitElement {
17297
18294
  }
17298
18295
  applySelection() {
17299
18296
  if (this.draftHour === null) return;
17300
- const newValue = this.formatValue(this.draftHour, this.draftMinute ?? 0);
18297
+ const newValue = this.formatValue(
18298
+ this.draftHour,
18299
+ this.draftMinute ?? 0,
18300
+ this.draftSecond ?? 0
18301
+ );
17301
18302
  this.value = newValue;
17302
18303
  this.dispatchEvent(
17303
18304
  new CustomEvent("rel-change", {
@@ -17313,6 +18314,7 @@ var RelTimePicker = class extends import_lit54.LitElement {
17313
18314
  const now = /* @__PURE__ */ new Date();
17314
18315
  const step = this.minuteStepSafe;
17315
18316
  this.draftHour = now.getHours();
18317
+ this.draftSecond = this.seconds ? now.getSeconds() : null;
17316
18318
  this.draftMinute = Math.round(now.getMinutes() / step) * step;
17317
18319
  if (this.draftMinute >= 60) {
17318
18320
  this.draftMinute = 0;
@@ -17335,6 +18337,7 @@ var RelTimePicker = class extends import_lit54.LitElement {
17335
18337
  this.value = null;
17336
18338
  this.draftHour = null;
17337
18339
  this.draftMinute = null;
18340
+ this.draftSecond = null;
17338
18341
  this.dispatchEvent(
17339
18342
  new CustomEvent("rel-change", {
17340
18343
  detail: { value: null },
@@ -17363,6 +18366,20 @@ var RelTimePicker = class extends import_lit54.LitElement {
17363
18366
  if (!this.isTimeInRange(this.draftHour, minute)) return;
17364
18367
  this.draftMinute = minute;
17365
18368
  }
18369
+ selectSecond(second) {
18370
+ if (this.draftHour === null) {
18371
+ for (let h = 0; h < 24; h++) {
18372
+ if (this.isHourEnabled(h)) {
18373
+ this.draftHour = h;
18374
+ break;
18375
+ }
18376
+ }
18377
+ if (this.draftHour === null) return;
18378
+ }
18379
+ if (this.draftMinute === null) this.draftMinute = 0;
18380
+ this.draftSecond = second;
18381
+ this.clampDraftToRange();
18382
+ }
17366
18383
  selectPeriod(period) {
17367
18384
  if (!this.isPeriodEnabled(period)) return;
17368
18385
  const base = period === "AM" ? 0 : 12;
@@ -17380,9 +18397,27 @@ var RelTimePicker = class extends import_lit54.LitElement {
17380
18397
  this.clampDraftToRange();
17381
18398
  this.scrollColumnsToSelection();
17382
18399
  }
18400
+ /**
18401
+ * Scrolls the column's own scroll container only. scrollIntoView is off
18402
+ * limits here: it walks ALL scrollable ancestors, so it yanked the page
18403
+ * up/down whenever a column recentred (user-reported jump bug).
18404
+ */
18405
+ scrollOptionInColumn(btn, block) {
18406
+ const list = btn.parentElement;
18407
+ if (!list) return;
18408
+ const top = btn.offsetTop;
18409
+ const height = btn.offsetHeight;
18410
+ if (block === "center") {
18411
+ list.scrollTop = top - (list.clientHeight - height) / 2;
18412
+ } else if (top < list.scrollTop) {
18413
+ list.scrollTop = top;
18414
+ } else if (top + height > list.scrollTop + list.clientHeight) {
18415
+ list.scrollTop = top + height - list.clientHeight;
18416
+ }
18417
+ }
17383
18418
  scrollColumnsToSelection() {
17384
18419
  this.updateComplete.then(() => {
17385
- this.columnsElement?.querySelectorAll(".option-button.selected").forEach((btn) => btn.scrollIntoView({ block: "center" }));
18420
+ this.columnsElement?.querySelectorAll(".option-button.selected").forEach((btn) => this.scrollOptionInColumn(btn, "center"));
17386
18421
  });
17387
18422
  }
17388
18423
  // ── Keyboard ──
@@ -17439,19 +18474,23 @@ var RelTimePicker = class extends import_lit54.LitElement {
17439
18474
  }
17440
18475
  if (target && target !== buttons[index]) {
17441
18476
  target.click();
17442
- target.focus();
17443
- target.scrollIntoView({ block: "nearest" });
18477
+ target.focus({ preventScroll: true });
18478
+ this.scrollOptionInColumn(target, "nearest");
17444
18479
  }
17445
18480
  }
17446
18481
  focusAdjacentColumn(from, direction) {
17447
- const order = this.format === "12h" ? ["hour", "minute", "period"] : ["hour", "minute"];
18482
+ const order = ["hour", "minute"];
18483
+ if (this.seconds) order.push("second");
18484
+ if (this.format === "12h") order.push("period");
17448
18485
  const nextIndex = order.indexOf(from) + direction;
17449
18486
  if (nextIndex < 0 || nextIndex >= order.length) return;
17450
18487
  const lists = this.columnsElement?.querySelectorAll(".column-list");
17451
18488
  const list = lists?.[nextIndex];
17452
18489
  const target = list?.querySelector(".option-button.selected:not(:disabled)") ?? list?.querySelector(".option-button:not(:disabled)");
17453
- target?.focus();
17454
- target?.scrollIntoView({ block: "nearest" });
18490
+ if (target) {
18491
+ target.focus({ preventScroll: true });
18492
+ this.scrollOptionInColumn(target, "nearest");
18493
+ }
17455
18494
  }
17456
18495
  // ── Render ──
17457
18496
  render() {
@@ -17459,10 +18498,10 @@ var RelTimePicker = class extends import_lit54.LitElement {
17459
18498
  const belowText = isError && this.errorMessage ? this.errorMessage : this.helperText;
17460
18499
  const describedById = belowText ? "time-picker-desc" : void 0;
17461
18500
  const parsedValue = this.parseTime(this.value);
17462
- const displayText = parsedValue ? this.formatDisplay(parsedValue.hour, parsedValue.minute) : "";
17463
- const draftDisplay = this.draftHour !== null ? this.formatDisplay(this.draftHour, this.draftMinute ?? 0) : "";
18501
+ const displayText = parsedValue ? this.formatDisplay(parsedValue.hour, parsedValue.minute, parsedValue.second) : "";
17464
18502
  const hourOptions = this.getHourOptions();
17465
18503
  const minuteOptions = this.getMinuteOptions();
18504
+ const secondOptions = this.seconds ? this.getSecondOptions() : [];
17466
18505
  const draftPeriod = this.draftPeriod;
17467
18506
  const wrapperClasses = [
17468
18507
  "time-picker-wrapper",
@@ -17527,104 +18566,106 @@ var RelTimePicker = class extends import_lit54.LitElement {
17527
18566
 
17528
18567
  <div
17529
18568
  part="panel"
17530
- class="popover ${this.open ? "open" : ""}"
18569
+ class="popover ${this.open ? "open" : ""} ${this.openUp ? "open-up" : ""}"
17531
18570
  role="dialog"
17532
18571
  aria-label="Choose time"
17533
18572
  >
17534
- <div class="time-readout">
17535
- <span class="time-readout-label">Time</span>
17536
- <span class="time-readout-value ${draftDisplay ? "" : "empty"}">
17537
- ${draftDisplay || (this.format === "24h" ? "--:--" : "--:-- --")}
17538
- </span>
17539
- </div>
17540
-
17541
18573
  <div class="columns">
17542
- <div class="column">
17543
- <div class="column-header" aria-hidden="true">Hour</div>
17544
- <div class="column-list" role="listbox" aria-label="Hours">
17545
- ${hourOptions.map(
18574
+ <div class="column-list" role="listbox" aria-label="Hours">
18575
+ ${hourOptions.map(
17546
18576
  (opt, i) => import_lit54.html`
17547
- <button
17548
- part="option"
17549
- class="option-button ${this.draftHour === opt.hour24 ? "selected" : ""}"
17550
- type="button"
17551
- role="option"
17552
- aria-selected=${this.draftHour === opt.hour24}
17553
- ?disabled=${!this.isHourEnabled(opt.hour24)}
17554
- @click=${() => this.selectHour(opt.hour24)}
17555
- @keydown=${(e) => this.handleOptionKeydown(e, "hour")}
17556
- tabindex=${this.draftHour === opt.hour24 || this.draftHour === null && i === 0 ? 0 : -1}
17557
- >
17558
- ${opt.label}
17559
- </button>
17560
- `
18577
+ <button
18578
+ part="option"
18579
+ class="option-button ${this.draftHour === opt.hour24 ? "selected" : ""}"
18580
+ type="button"
18581
+ role="option"
18582
+ aria-selected=${this.draftHour === opt.hour24}
18583
+ ?disabled=${!this.isHourEnabled(opt.hour24)}
18584
+ @click=${() => this.selectHour(opt.hour24)}
18585
+ @keydown=${(e) => this.handleOptionKeydown(e, "hour")}
18586
+ tabindex=${this.draftHour === opt.hour24 || this.draftHour === null && i === 0 ? 0 : -1}
18587
+ >
18588
+ ${opt.label}
18589
+ </button>
18590
+ `
17561
18591
  )}
17562
- </div>
17563
18592
  </div>
17564
18593
 
17565
- <div class="column">
17566
- <div class="column-header" aria-hidden="true">Min</div>
17567
- <div class="column-list" role="listbox" aria-label="Minutes">
17568
- ${minuteOptions.map(
18594
+ <div class="column-divider" aria-hidden="true"></div>
18595
+
18596
+ <div class="column-list" role="listbox" aria-label="Minutes">
18597
+ ${minuteOptions.map(
17569
18598
  (m, i) => import_lit54.html`
17570
- <button
17571
- part="option"
17572
- class="option-button ${this.draftMinute === m ? "selected" : ""}"
17573
- type="button"
17574
- role="option"
17575
- aria-selected=${this.draftMinute === m}
17576
- ?disabled=${this.draftHour !== null && !this.isTimeInRange(this.draftHour, m)}
17577
- @click=${() => this.selectMinute(m)}
17578
- @keydown=${(e) => this.handleOptionKeydown(e, "minute")}
17579
- tabindex=${this.draftMinute === m || this.draftMinute === null && i === 0 ? 0 : -1}
17580
- >
17581
- ${String(m).padStart(2, "0")}
17582
- </button>
17583
- `
18599
+ <button
18600
+ part="option"
18601
+ class="option-button ${this.draftMinute === m ? "selected" : ""}"
18602
+ type="button"
18603
+ role="option"
18604
+ aria-selected=${this.draftMinute === m}
18605
+ ?disabled=${this.draftHour !== null && !this.isTimeInRange(this.draftHour, m)}
18606
+ @click=${() => this.selectMinute(m)}
18607
+ @keydown=${(e) => this.handleOptionKeydown(e, "minute")}
18608
+ tabindex=${this.draftMinute === m || this.draftMinute === null && i === 0 ? 0 : -1}
18609
+ >
18610
+ ${String(m).padStart(2, "0")}
18611
+ </button>
18612
+ `
17584
18613
  )}
17585
- </div>
17586
18614
  </div>
17587
18615
 
18616
+ ${this.seconds ? import_lit54.html`
18617
+ <div class="column-divider" aria-hidden="true"></div>
18618
+ <div class="column-list" role="listbox" aria-label="Seconds">
18619
+ ${secondOptions.map(
18620
+ (s, i) => import_lit54.html`
18621
+ <button
18622
+ part="option"
18623
+ class="option-button ${this.draftSecond === s ? "selected" : ""}"
18624
+ type="button"
18625
+ role="option"
18626
+ aria-selected=${this.draftSecond === s}
18627
+ @click=${() => this.selectSecond(s)}
18628
+ @keydown=${(e) => this.handleOptionKeydown(e, "second")}
18629
+ tabindex=${this.draftSecond === s || this.draftSecond === null && i === 0 ? 0 : -1}
18630
+ >
18631
+ ${String(s).padStart(2, "0")}
18632
+ </button>
18633
+ `
18634
+ )}
18635
+ </div>
18636
+ ` : import_lit54.nothing}
17588
18637
  ${this.format === "12h" ? import_lit54.html`
17589
- <div class="column">
17590
- <div class="column-header" aria-hidden="true">AM/PM</div>
17591
- <div class="column-list" role="listbox" aria-label="AM or PM">
17592
- ${["AM", "PM"].map(
18638
+ <div class="column-divider" aria-hidden="true"></div>
18639
+ <div class="column-list" role="listbox" aria-label="AM or PM">
18640
+ ${["AM", "PM"].map(
17593
18641
  (p) => import_lit54.html`
17594
- <button
17595
- part="option"
17596
- class="option-button ${draftPeriod === p ? "selected" : ""}"
17597
- type="button"
17598
- role="option"
17599
- aria-selected=${draftPeriod === p}
17600
- ?disabled=${!this.isPeriodEnabled(p)}
17601
- @click=${() => this.selectPeriod(p)}
17602
- @keydown=${(e) => this.handleOptionKeydown(e, "period")}
17603
- tabindex=${draftPeriod === p || draftPeriod === null && p === "AM" ? 0 : -1}
17604
- >
17605
- ${p}
17606
- </button>
17607
- `
18642
+ <button
18643
+ part="option"
18644
+ class="option-button ${draftPeriod === p ? "selected" : ""}"
18645
+ type="button"
18646
+ role="option"
18647
+ aria-selected=${draftPeriod === p}
18648
+ ?disabled=${!this.isPeriodEnabled(p)}
18649
+ @click=${() => this.selectPeriod(p)}
18650
+ @keydown=${(e) => this.handleOptionKeydown(e, "period")}
18651
+ tabindex=${draftPeriod === p || draftPeriod === null && p === "AM" ? 0 : -1}
18652
+ >
18653
+ ${p}
18654
+ </button>
18655
+ `
17608
18656
  )}
17609
- </div>
17610
18657
  </div>
17611
18658
  ` : import_lit54.nothing}
17612
18659
  </div>
17613
18660
 
17614
18661
  <div part="footer" class="time-footer">
17615
- <rel-button
17616
- variant="secondary"
17617
- size="small"
17618
- border="corner-radius"
17619
- @click=${() => this.selectNow()}
17620
- >
18662
+ <rel-button variant="link" size="small" @click=${() => this.selectNow()}>
17621
18663
  Now
17622
18664
  </rel-button>
17623
18665
  <div class="footer-right">
17624
18666
  <rel-button
17625
- variant="secondary"
18667
+ variant="ghost"
17626
18668
  size="small"
17627
- border="corner-radius"
17628
18669
  @click=${() => this.cancelPopover()}
17629
18670
  >
17630
18671
  Cancel
@@ -17633,11 +18674,10 @@ var RelTimePicker = class extends import_lit54.LitElement {
17633
18674
  variant="primary"
17634
18675
  tone="default"
17635
18676
  size="small"
17636
- border="corner-radius"
17637
18677
  ?disabled=${this.draftHour === null}
17638
18678
  @click=${() => this.applySelection()}
17639
18679
  >
17640
- Apply
18680
+ OK
17641
18681
  </rel-button>
17642
18682
  </div>
17643
18683
  </div>
@@ -17821,16 +18861,19 @@ RelTimePicker.styles = import_lit54.css`
17821
18861
  height: 16px;
17822
18862
  }
17823
18863
 
18864
+ /* Panel (Figma "Time Picker"): fixed width per size, full-bleed content. */
17824
18865
  .popover {
17825
18866
  position: absolute;
17826
18867
  top: calc(100% + 4px);
17827
18868
  left: 0;
17828
18869
  z-index: 1000;
17829
18870
  background: var(--rel-bg-surface);
17830
- border: 1px solid var(--rel-border-default);
18871
+ border: 1px solid var(--rel-border-subtle);
17831
18872
  border-radius: var(--rel-border-radius-sm);
17832
18873
  box-shadow: var(--rel-shadows-shadow-300);
17833
- padding: var(--rel-spacing-md);
18874
+ overflow: hidden;
18875
+ min-width: 220px;
18876
+ width: 250px;
17834
18877
  display: none;
17835
18878
  }
17836
18879
 
@@ -17838,94 +18881,79 @@ RelTimePicker.styles = import_lit54.css`
17838
18881
  display: block;
17839
18882
  }
17840
18883
 
17841
- /* Draft readout (mirrors the range-picker FROM/TO block) */
17842
- .time-readout {
17843
- display: flex;
17844
- align-items: baseline;
17845
- gap: var(--rel-spacing-sm);
17846
- padding-bottom: var(--rel-spacing-sm);
17847
- margin-bottom: var(--rel-spacing-sm);
17848
- border-bottom: 1px solid var(--rel-border-subtle);
17849
- }
17850
-
17851
- .time-readout-label {
17852
- color: var(--rel-text-placeholder);
17853
- text-transform: uppercase;
17854
- font-family: var(--rel-body-xs-semibold-font-family);
17855
- font-weight: var(--rel-body-xs-semibold-font-weight);
17856
- font-size: var(--rel-body-xs-semibold-font-size);
17857
- line-height: var(--rel-body-xs-semibold-line-height);
17858
- letter-spacing: var(--rel-body-xs-semibold-letter-spacing);
17859
- }
17860
-
17861
- .time-readout-value {
17862
- color: var(--rel-text-default);
17863
- font-family: var(--rel-body-md-semibold-font-family);
17864
- font-weight: var(--rel-body-md-semibold-font-weight);
17865
- font-size: var(--rel-body-md-semibold-font-size);
17866
- line-height: var(--rel-body-md-semibold-line-height);
17867
- letter-spacing: var(--rel-body-md-semibold-letter-spacing);
17868
- }
17869
-
17870
- .time-readout-value.empty {
17871
- color: var(--rel-text-placeholder);
18884
+ /* Flip above the field when the viewport has no room below. */
18885
+ .popover.open-up {
18886
+ top: auto;
18887
+ bottom: calc(100% + 4px);
17872
18888
  }
17873
18889
 
18890
+ /* Full-bleed column row with 1px dividers (Figma "Columns"). */
17874
18891
  .columns {
17875
18892
  display: flex;
17876
- gap: var(--rel-spacing-xs);
17877
- }
17878
-
17879
- /* Columns share the panel width equally, so the two-column 24h layout
17880
- fills the same footprint the footer dictates instead of bunching left. */
17881
- .column {
17882
- display: flex;
17883
- flex-direction: column;
17884
- flex: 1;
17885
- min-width: 64px;
18893
+ align-items: stretch;
18894
+ height: 260px;
17886
18895
  }
17887
18896
 
17888
- .column-header {
17889
- text-align: center;
17890
- color: var(--rel-text-placeholder);
17891
- padding: var(--rel-spacing-xs);
17892
- text-transform: uppercase;
17893
- font-family: var(--rel-body-xs-semibold-font-family);
17894
- font-weight: var(--rel-body-xs-semibold-font-weight);
17895
- font-size: var(--rel-body-xs-semibold-font-size);
17896
- line-height: var(--rel-body-xs-semibold-line-height);
17897
- letter-spacing: var(--rel-body-xs-semibold-letter-spacing);
18897
+ .column-divider {
18898
+ flex-shrink: 0;
18899
+ width: 1px;
18900
+ background: var(--rel-border-subtle);
17898
18901
  }
17899
18902
 
18903
+ /* Each column scrolls independently. The 4px rail is always reserved
18904
+ (scrollbar-gutter) so columns stay aligned whether or not they
18905
+ overflow; the thumb only appears on overflow. */
17900
18906
  .column-list {
18907
+ flex: 1;
18908
+ min-width: 0;
17901
18909
  display: flex;
17902
18910
  flex-direction: column;
17903
- gap: var(--rel-spacing-3xs);
17904
- max-height: 224px;
18911
+ /* Positioned so option offsetTop is list-relative — the in-column
18912
+ scroll math depends on it (we never scrollIntoView: it scrolls every
18913
+ ancestor, page included). */
18914
+ position: relative;
17905
18915
  overflow-y: auto;
17906
- padding: var(--rel-spacing-3xs);
18916
+ padding: var(--rel-spacing-2xs);
17907
18917
  scrollbar-width: thin;
18918
+ scrollbar-color: var(--rel-border-default) transparent;
18919
+ scrollbar-gutter: stable;
17908
18920
  }
17909
18921
 
18922
+ .column-list::-webkit-scrollbar {
18923
+ width: 4px;
18924
+ }
18925
+
18926
+ .column-list::-webkit-scrollbar-track {
18927
+ background: transparent;
18928
+ }
18929
+
18930
+ .column-list::-webkit-scrollbar-thumb {
18931
+ background: var(--rel-border-default);
18932
+ border-radius: var(--rel-border-radius-full-rounded);
18933
+ }
18934
+
18935
+ /* Option cell (Figma ".Time Option"): selected uses the neutral
18936
+ elevated-2 fill + medium weight, NOT the brand color. */
17910
18937
  .option-button {
17911
18938
  border: none;
17912
18939
  background: transparent;
17913
- border-radius: var(--rel-border-radius-sm);
18940
+ border-radius: var(--rel-border-radius-xs);
17914
18941
  color: var(--rel-text-default);
17915
18942
  cursor: pointer;
17916
- padding: var(--rel-spacing-xs) var(--rel-spacing-sm);
17917
- text-align: center;
17918
18943
  flex-shrink: 0;
17919
- transition: all 0.15s ease;
17920
- font-family: var(--rel-body-sm-medium-font-family);
17921
- font-weight: var(--rel-body-sm-medium-font-weight);
17922
- font-size: var(--rel-body-sm-medium-font-size);
17923
- line-height: var(--rel-body-sm-medium-line-height);
17924
- letter-spacing: var(--rel-body-sm-medium-letter-spacing);
18944
+ height: 36px;
18945
+ padding: 0 var(--rel-spacing-xs);
18946
+ text-align: center;
18947
+ transition: background 0.15s ease;
18948
+ font-family: var(--rel-body-md-regular-font-family);
18949
+ font-weight: var(--rel-body-md-regular-font-weight);
18950
+ font-size: var(--rel-body-md-regular-font-size);
18951
+ line-height: var(--rel-body-md-regular-line-height);
18952
+ letter-spacing: var(--rel-body-md-regular-letter-spacing);
17925
18953
  }
17926
18954
 
17927
18955
  .option-button:hover:not(:disabled):not(.selected) {
17928
- background: var(--rel-bg-surface-elevated);
18956
+ background: var(--rel-bg-neutral-elevated-1);
17929
18957
  }
17930
18958
 
17931
18959
  .option-button:focus-visible {
@@ -17935,9 +18963,9 @@ RelTimePicker.styles = import_lit54.css`
17935
18963
  }
17936
18964
 
17937
18965
  .option-button.selected {
17938
- background: var(--rel-action-brand-fill);
17939
- color: var(--rel-action-brand-label-icons-primary);
17940
- font-weight: var(--rel-body-md-semibold-font-weight);
18966
+ background: var(--rel-bg-neutral-elevated-2);
18967
+ color: var(--rel-text-default);
18968
+ font-weight: var(--rel-body-md-medium-font-weight);
17941
18969
  }
17942
18970
 
17943
18971
  .option-button:disabled {
@@ -17950,14 +18978,15 @@ RelTimePicker.styles = import_lit54.css`
17950
18978
  background: transparent;
17951
18979
  }
17952
18980
 
17953
- /* Footer (mirrors the range-picker calendar-footer) */
18981
+ /* Footer (Figma "Footer"): 8px padding + 8px extra left so the "Now"
18982
+ link aligns with the buttons' inner padding. Buttons are always S. */
17954
18983
  .time-footer {
17955
18984
  display: flex;
17956
18985
  align-items: center;
17957
18986
  justify-content: space-between;
17958
- gap: var(--rel-spacing-sm);
17959
- padding-top: var(--rel-spacing-sm);
17960
- margin-top: var(--rel-spacing-sm);
18987
+ gap: var(--rel-spacing-xs);
18988
+ padding: var(--rel-spacing-xs);
18989
+ padding-left: calc(var(--rel-spacing-xs) * 2);
17961
18990
  border-top: 1px solid var(--rel-border-subtle);
17962
18991
  }
17963
18992
 
@@ -17967,6 +18996,56 @@ RelTimePicker.styles = import_lit54.css`
17967
18996
  gap: var(--rel-spacing-xs);
17968
18997
  }
17969
18998
 
18999
+ /* The design narrows OK below the DS small min-width (60 vs 80) so the
19000
+ compact footer fits the 220px panel. */
19001
+ .footer-right rel-button[variant="primary"] {
19002
+ --rel-button-min-width: 60px;
19003
+ }
19004
+
19005
+ /* Panel size variants (Figma S/M/L): width 220/250/280, option height
19006
+ 32/36/40, body-sm/md/lg type. Medium is the base above. */
19007
+ .time-picker-wrapper.small .popover {
19008
+ width: 220px;
19009
+ }
19010
+
19011
+ .time-picker-wrapper.small .columns {
19012
+ height: 232px;
19013
+ }
19014
+
19015
+ .time-picker-wrapper.small .option-button {
19016
+ height: 32px;
19017
+ font-family: var(--rel-body-sm-regular-font-family);
19018
+ font-weight: var(--rel-body-sm-regular-font-weight);
19019
+ font-size: var(--rel-body-sm-regular-font-size);
19020
+ line-height: var(--rel-body-sm-regular-line-height);
19021
+ letter-spacing: var(--rel-body-sm-regular-letter-spacing);
19022
+ }
19023
+
19024
+ .time-picker-wrapper.small .option-button.selected {
19025
+ font-weight: var(--rel-body-sm-medium-font-weight);
19026
+ }
19027
+
19028
+ .time-picker-wrapper.large .popover {
19029
+ width: 280px;
19030
+ }
19031
+
19032
+ .time-picker-wrapper.large .columns {
19033
+ height: 288px;
19034
+ }
19035
+
19036
+ .time-picker-wrapper.large .option-button {
19037
+ height: 40px;
19038
+ font-family: var(--rel-body-lg-regular-font-family);
19039
+ font-weight: var(--rel-body-lg-regular-font-weight);
19040
+ font-size: var(--rel-body-lg-regular-font-size);
19041
+ line-height: var(--rel-body-lg-regular-line-height);
19042
+ letter-spacing: var(--rel-body-lg-regular-letter-spacing);
19043
+ }
19044
+
19045
+ .time-picker-wrapper.large .option-button.selected {
19046
+ font-weight: var(--rel-body-lg-medium-font-weight);
19047
+ }
19048
+
17970
19049
  @media (prefers-reduced-motion: reduce) {
17971
19050
  .trigger-button,
17972
19051
  .icon-button,
@@ -17996,6 +19075,9 @@ __decorateClass([
17996
19075
  __decorateClass([
17997
19076
  (0, import_decorators54.property)({ type: String })
17998
19077
  ], RelTimePicker.prototype, "format", 2);
19078
+ __decorateClass([
19079
+ (0, import_decorators54.property)({ type: Boolean, reflect: true })
19080
+ ], RelTimePicker.prototype, "seconds", 2);
17999
19081
  __decorateClass([
18000
19082
  (0, import_decorators54.property)({ type: Number, attribute: "minute-step" })
18001
19083
  ], RelTimePicker.prototype, "minuteStep", 2);
@@ -18041,6 +19123,12 @@ __decorateClass([
18041
19123
  __decorateClass([
18042
19124
  (0, import_decorators54.state)()
18043
19125
  ], RelTimePicker.prototype, "draftMinute", 2);
19126
+ __decorateClass([
19127
+ (0, import_decorators54.state)()
19128
+ ], RelTimePicker.prototype, "draftSecond", 2);
19129
+ __decorateClass([
19130
+ (0, import_decorators54.state)()
19131
+ ], RelTimePicker.prototype, "openUp", 2);
18044
19132
  __decorateClass([
18045
19133
  (0, import_decorators54.query)(".trigger-button")
18046
19134
  ], RelTimePicker.prototype, "triggerButton", 2);
@@ -18279,6 +19367,26 @@ var RelTooltip = class extends import_lit56.LitElement {
18279
19367
  this.type = "single-line";
18280
19368
  // Stable id so aria-describedby can reference the bubble across renders.
18281
19369
  this.uniqueId = Math.random().toString(36).slice(2);
19370
+ /** APG: Escape dismisses the tooltip. Not swallowed — an enclosing dialog
19371
+ * may legitimately close on the same keystroke. */
19372
+ this.onKeydown = (e) => {
19373
+ if (e.key === "Escape") this.toggleAttribute("data-dismissed", true);
19374
+ };
19375
+ this.clearDismissed = () => {
19376
+ this.removeAttribute("data-dismissed");
19377
+ };
19378
+ }
19379
+ connectedCallback() {
19380
+ super.connectedCallback();
19381
+ this.addEventListener("keydown", this.onKeydown);
19382
+ this.addEventListener("mouseleave", this.clearDismissed);
19383
+ this.addEventListener("focusout", this.clearDismissed);
19384
+ }
19385
+ disconnectedCallback() {
19386
+ super.disconnectedCallback();
19387
+ this.removeEventListener("keydown", this.onKeydown);
19388
+ this.removeEventListener("mouseleave", this.clearDismissed);
19389
+ this.removeEventListener("focusout", this.clearDismissed);
18282
19390
  }
18283
19391
  firstUpdated() {
18284
19392
  this.linkTrigger();
@@ -18345,6 +19453,12 @@ RelTooltip.styles = import_lit56.css`
18345
19453
  opacity: 1;
18346
19454
  }
18347
19455
 
19456
+ /* Escape-dismissed (APG): stays hidden until hover/focus leaves and
19457
+ returns. Declared after the hover/focus rules so source order wins. */
19458
+ :host([data-dismissed]) .tooltip-bubble {
19459
+ opacity: 0;
19460
+ }
19461
+
18348
19462
  /* Positioning */
18349
19463
  .tooltip-bubble.above {
18350
19464
  bottom: calc(100% + 6px);
@@ -19341,6 +20455,7 @@ RelUploadActions = __decorateClass([
19341
20455
  __reExport(index_exports, require("@reliability-design/icons"), module.exports);
19342
20456
  // Annotate the CommonJS export names for ESM import in node:
19343
20457
  0 && (module.exports = {
20458
+ FocusTrap,
19344
20459
  RelAccordion,
19345
20460
  RelAlert,
19346
20461
  RelAutocomplete,
@@ -19402,6 +20517,15 @@ __reExport(index_exports, require("@reliability-design/icons"), module.exports);
19402
20517
  RelTopbarUtilities,
19403
20518
  RelUpload,
19404
20519
  RelUploadActions,
20520
+ bindShortcut,
20521
+ formatShortcutParts,
20522
+ getDeepActiveElement,
20523
+ getFocusableElements,
20524
+ isApplePlatform,
20525
+ isEditableTarget,
20526
+ matchesShortcut,
20527
+ parseShortcut,
20528
+ trapTabKey,
19405
20529
  ...require("@reliability-design/icons")
19406
20530
  });
19407
20531
  //# sourceMappingURL=index.js.map