@reliability-design/web 1.4.6 → 1.4.8

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 (148) hide show
  1. package/CHANGELOG.md +246 -49
  2. package/dist/components/rel-accordion/README.md +133 -132
  3. package/dist/components/rel-alert/README.md +145 -141
  4. package/dist/components/rel-autocomplete/README.md +172 -61
  5. package/dist/components/rel-avatar/README.md +118 -130
  6. package/dist/components/rel-badge/README.md +97 -93
  7. package/dist/components/rel-breadcrumb/README.md +96 -102
  8. package/dist/components/rel-button/README.md +190 -130
  9. package/dist/components/rel-calendar/README.md +109 -109
  10. package/dist/components/rel-card/README.md +75 -94
  11. package/dist/components/rel-cardmedia/README.md +100 -127
  12. package/dist/components/rel-carousel/README.md +186 -166
  13. package/dist/components/rel-checkbox/README.md +96 -75
  14. package/dist/components/rel-chip/README.md +138 -121
  15. package/dist/components/rel-container/README.md +107 -99
  16. package/dist/components/rel-date-picker/README.md +200 -289
  17. package/dist/components/rel-divider/README.md +88 -100
  18. package/dist/components/rel-drawer/README.md +187 -200
  19. package/dist/components/rel-kbd/README.md +141 -151
  20. package/dist/components/rel-layout/README.md +34 -0
  21. package/dist/components/rel-menu/README.md +180 -151
  22. package/dist/components/rel-menubar/README.md +218 -329
  23. package/dist/components/rel-otp/README.md +171 -440
  24. package/dist/components/rel-pagination/README.md +100 -424
  25. package/dist/components/rel-popover/README.md +203 -568
  26. package/dist/components/rel-progress/README.md +88 -110
  27. package/dist/components/rel-radio/README.md +131 -138
  28. package/dist/components/rel-range-picker/README.md +165 -0
  29. package/dist/components/rel-resizable/README.md +199 -140
  30. package/dist/components/rel-segmented-button/README.md +156 -141
  31. package/dist/components/rel-select/README.md +156 -93
  32. package/dist/components/rel-sheet/README.md +168 -224
  33. package/dist/components/rel-skeleton/README.md +104 -0
  34. package/dist/components/rel-slider/README.md +131 -135
  35. package/dist/components/rel-snackbar/README.md +168 -134
  36. package/dist/components/rel-stepper/README.md +162 -160
  37. package/dist/components/rel-switch/README.md +117 -102
  38. package/dist/components/rel-tab/README.md +173 -163
  39. package/dist/components/rel-textfield/README.md +176 -69
  40. package/dist/components/rel-time-picker/README.md +122 -157
  41. package/dist/components/rel-toast/README.md +165 -148
  42. package/dist/components/rel-tooltip/README.md +119 -111
  43. package/dist/components/rel-upload/README.md +220 -0
  44. package/dist/index.d.mts +1726 -447
  45. package/dist/index.d.ts +1726 -447
  46. package/dist/index.js +13715 -10870
  47. package/dist/index.js.map +1 -1
  48. package/dist/index.mjs +14018 -11163
  49. package/dist/index.mjs.map +1 -1
  50. package/dist/styles/focus-ring.ts +84 -0
  51. package/dist/styles/vars.css +892 -866
  52. package/dist/{vars-XNMTSRVN.css → vars-DXX6QOAG.css} +892 -866
  53. package/package.json +3 -2
  54. package/dist/components/rel-accordion/rel-accordion.ts +0 -336
  55. package/dist/components/rel-alert/rel-alert.ts +0 -305
  56. package/dist/components/rel-autocomplete/rel-autocomplete.ts +0 -720
  57. package/dist/components/rel-avatar/rel-avatar.ts +0 -142
  58. package/dist/components/rel-badge/rel-badge.ts +0 -175
  59. package/dist/components/rel-breadcrumb/rel-breadcrumb.ts +0 -106
  60. package/dist/components/rel-button/rel-button.ts +0 -560
  61. package/dist/components/rel-calendar/index.ts +0 -1
  62. package/dist/components/rel-calendar/rel-calendar.ts +0 -565
  63. package/dist/components/rel-card/rel-card.ts +0 -88
  64. package/dist/components/rel-cardmedia/rel-cardmedia.ts +0 -88
  65. package/dist/components/rel-carousel/index.ts +0 -1
  66. package/dist/components/rel-carousel/rel-carousel.ts +0 -723
  67. package/dist/components/rel-chart/README.md +0 -286
  68. package/dist/components/rel-chart/index.ts +0 -1
  69. package/dist/components/rel-chart/rel-chart.ts +0 -754
  70. package/dist/components/rel-checkbox/rel-checkbox.ts +0 -382
  71. package/dist/components/rel-chip/rel-chip.ts +0 -483
  72. package/dist/components/rel-container/rel-container.ts +0 -95
  73. package/dist/components/rel-date-picker/date-format.test.ts +0 -43
  74. package/dist/components/rel-date-picker/date-format.ts +0 -47
  75. package/dist/components/rel-date-picker/date-parse.test.ts +0 -116
  76. package/dist/components/rel-date-picker/date-parse.ts +0 -178
  77. package/dist/components/rel-date-picker/index.ts +0 -1
  78. package/dist/components/rel-date-picker/rel-date-picker.ts +0 -1676
  79. package/dist/components/rel-divider/rel-divider.ts +0 -54
  80. package/dist/components/rel-drawer/index.ts +0 -1
  81. package/dist/components/rel-drawer/rel-drawer-footer.ts +0 -27
  82. package/dist/components/rel-drawer/rel-drawer.ts +0 -607
  83. package/dist/components/rel-kbd/rel-kbd.ts +0 -188
  84. package/dist/components/rel-layout/rel-layout.ts +0 -49
  85. package/dist/components/rel-menu/rel-menu.ts +0 -541
  86. package/dist/components/rel-menubar/index.ts +0 -2
  87. package/dist/components/rel-menubar/rel-menubar-menu.css +0 -3
  88. package/dist/components/rel-menubar/rel-menubar-menu.ts +0 -17
  89. package/dist/components/rel-menubar/rel-menubar.ts +0 -777
  90. package/dist/components/rel-nav-menu/README.md +0 -289
  91. package/dist/components/rel-nav-menu/index.ts +0 -3
  92. package/dist/components/rel-nav-menu/rel-nav-menu-item.ts +0 -243
  93. package/dist/components/rel-nav-menu/rel-nav-menu-panel.ts +0 -72
  94. package/dist/components/rel-nav-menu/rel-nav-menu.ts +0 -270
  95. package/dist/components/rel-navbar/README.md +0 -126
  96. package/dist/components/rel-navbar/rel-navbar.ts +0 -105
  97. package/dist/components/rel-otp/index.ts +0 -1
  98. package/dist/components/rel-otp/rel-otp.ts +0 -527
  99. package/dist/components/rel-pagination/index.ts +0 -1
  100. package/dist/components/rel-pagination/rel-pagination.ts +0 -522
  101. package/dist/components/rel-popover/index.ts +0 -2
  102. package/dist/components/rel-popover/rel-popover-content.ts +0 -81
  103. package/dist/components/rel-popover/rel-popover.ts +0 -475
  104. package/dist/components/rel-progress/rel-progress.ts +0 -234
  105. package/dist/components/rel-radio/rel-radio.ts +0 -481
  106. package/dist/components/rel-range-picker/rel-range-picker.ts +0 -1688
  107. package/dist/components/rel-resizable/index.ts +0 -3
  108. package/dist/components/rel-resizable/rel-resizable-group.ts +0 -405
  109. package/dist/components/rel-resizable/rel-resizable-handle.ts +0 -150
  110. package/dist/components/rel-resizable/rel-resizable-panel.ts +0 -58
  111. package/dist/components/rel-segmented-button/rel-segmented-button.ts +0 -217
  112. package/dist/components/rel-select/rel-select.ts +0 -1416
  113. package/dist/components/rel-sheet/rel-sheet.ts +0 -610
  114. package/dist/components/rel-sidebar/README.md +0 -209
  115. package/dist/components/rel-sidebar/rel-sidebar.ts +0 -1191
  116. package/dist/components/rel-skeleton/rel-skeleton.ts +0 -143
  117. package/dist/components/rel-slider/rel-slider.ts +0 -673
  118. package/dist/components/rel-snackbar/rel-snackbar.ts +0 -329
  119. package/dist/components/rel-stepper/rel-stepper.ts +0 -567
  120. package/dist/components/rel-switch/rel-switch.ts +0 -199
  121. package/dist/components/rel-tab/rel-tab.ts +0 -264
  122. package/dist/components/rel-table/README.md +0 -178
  123. package/dist/components/rel-table/rel-table-body.ts +0 -22
  124. package/dist/components/rel-table/rel-table-caption.ts +0 -30
  125. package/dist/components/rel-table/rel-table-cell.ts +0 -43
  126. package/dist/components/rel-table/rel-table-footer.ts +0 -24
  127. package/dist/components/rel-table/rel-table-head.ts +0 -47
  128. package/dist/components/rel-table/rel-table-header.ts +0 -22
  129. package/dist/components/rel-table/rel-table-row.ts +0 -37
  130. package/dist/components/rel-table/rel-table.ts +0 -62
  131. package/dist/components/rel-textfield/rel-textfield.ts +0 -1054
  132. package/dist/components/rel-time-picker/rel-time-picker.ts +0 -1482
  133. package/dist/components/rel-time-picker/time-parse.test.ts +0 -67
  134. package/dist/components/rel-time-picker/time-parse.ts +0 -85
  135. package/dist/components/rel-toast/rel-toast.ts +0 -206
  136. package/dist/components/rel-tooltip/rel-tooltip.ts +0 -193
  137. package/dist/components/rel-topbar/rel-topbar-actions.ts +0 -27
  138. package/dist/components/rel-topbar/rel-topbar-brand.ts +0 -54
  139. package/dist/components/rel-topbar/rel-topbar-dropdown-item.ts +0 -69
  140. package/dist/components/rel-topbar/rel-topbar-dropdown.ts +0 -31
  141. package/dist/components/rel-topbar/rel-topbar-item.ts +0 -324
  142. package/dist/components/rel-topbar/rel-topbar-mobile-panel.ts +0 -27
  143. package/dist/components/rel-topbar/rel-topbar-profile.ts +0 -243
  144. package/dist/components/rel-topbar/rel-topbar-utilities.ts +0 -27
  145. package/dist/components/rel-topbar/rel-topbar.ts +0 -446
  146. package/dist/components/rel-upload/UPLOAD-DESIGN-CHANGES.md +0 -134
  147. package/dist/components/rel-upload/rel-upload-actions.ts +0 -163
  148. package/dist/components/rel-upload/rel-upload.ts +0 -968
@@ -1,541 +0,0 @@
1
- import { LitElement, html, nothing } from "lit";
2
- import { customElement, property, state } from "lit/decorators.js";
3
- import "@reliability-design/icons/rel-icon";
4
- import "../rel-checkbox/rel-checkbox.js";
5
- import "../rel-radio/rel-radio.js";
6
-
7
- export type MenuType = "default" | "checkbox" | "arrow" | "select-option" | "fixed" | "fit";
8
-
9
- export interface MenuItem {
10
- label: string;
11
- active?: boolean;
12
- checked?: boolean;
13
- disabled?: boolean;
14
- submenu?: MenuItem[];
15
- open?: boolean;
16
- }
17
-
18
- @customElement("rel-menu-item")
19
- export class RelMenuItem extends LitElement {
20
- @property({ type: String }) label: string = "";
21
- @property({ type: String }) value: string = "";
22
- @property({ type: Boolean }) disabled: boolean = false;
23
- @property({ type: String }) icon: string = "";
24
-
25
- createRenderRoot() {
26
- return this;
27
- }
28
-
29
- render() {
30
- return html``;
31
- }
32
- }
33
-
34
- @customElement("rel-menu")
35
- export class RelMenu extends LitElement {
36
- @property({ type: String }) type: MenuType = "default";
37
-
38
- @property({ type: Array }) items: MenuItem[] = [];
39
-
40
- @state() private focusedIndex: number = 0;
41
- /** Index inside the open submenu that virtual focus sits on; -1 = main list. */
42
- @state() private subFocusedIndex: number = -1;
43
- @state() private _items: MenuItem[] = [];
44
-
45
- /** Stable id prefix for aria-activedescendant targets. */
46
- private menuId = `rel-menu-${Math.random().toString(36).slice(2)}`;
47
-
48
- public static readonly _css = `
49
- :host {
50
- color: var(--rel-text-default);
51
- display: inline-block;
52
- }
53
-
54
- .rel-menu {
55
- position: relative;
56
- display: inline-block;
57
- background: var(--rel-bg-neutral);
58
- color: var(--rel-text-default);
59
- border-radius: var(--rel-border-radius-md);
60
- box-shadow: var(--rel-shadows-shadow-300);
61
- min-width: 220px;
62
- /* No vertical padding: items fill edge-to-edge so a first/last-item hover
63
- reaches the menu's top/bottom. NOT overflow:hidden — that clips the
64
- type=arrow flyout (position:absolute at left:100%). Instead the first
65
- and last items carry the outer corner radius so hover backgrounds clip
66
- to the rounded shape without any overflow. */
67
- padding: 0;
68
- outline: none;
69
- overflow: visible;
70
- font-family: var(--rel-body-lg-regular-font-family);
71
- font-weight: var(--rel-body-lg-regular-font-weight);
72
- font-size: var(--rel-body-lg-regular-font-size);
73
- line-height: var(--rel-body-lg-regular-line-height);
74
- letter-spacing: var(--rel-body-lg-regular-letter-spacing);
75
- }
76
-
77
- .rel-menu:focus-visible,
78
- :host(:focus-visible) .rel-menu {
79
- outline: 2px solid var(--rel-border-focus);
80
- outline-offset: 2px;
81
- }
82
-
83
- .rel-menu-item {
84
- position: relative;
85
- display: flex;
86
- align-items: center;
87
- gap: var(--rel-spacing-xs);
88
- padding: var(--rel-spacing-xs) var(--rel-spacing-sm);
89
- line-height: var(--rel-body-sm-regular-line-height);
90
- cursor: pointer;
91
- transition: background 0.15s ease, color 0.15s ease;
92
- user-select: none;
93
- }
94
-
95
- /* First/last items carry the menu's outer radius so their hover fill clips
96
- to the rounded corners (replaces the removed overflow:hidden). */
97
- .rel-menu > .rel-menu-item:first-child {
98
- border-top-left-radius: var(--rel-border-radius-md);
99
- border-top-right-radius: var(--rel-border-radius-md);
100
- }
101
-
102
- .rel-menu > .rel-menu-item:last-child {
103
- border-bottom-left-radius: var(--rel-border-radius-md);
104
- border-bottom-right-radius: var(--rel-border-radius-md);
105
- }
106
-
107
- .rel-menu-item:hover {
108
- background: var(--rel-bg-neutral-elevated-1);
109
- }
110
-
111
- .rel-menu-item:active {
112
- background: var(--rel-bg-neutral-elevated-1);
113
- }
114
-
115
- .rel-menu-item.active {
116
- background: var(--rel-bg-neutral-elevated-2);
117
- font-weight: var(--rel-body-lg-medium-font-weight);
118
- }
119
-
120
- .rel-menu-item.disabled {
121
- background-color: transparent;
122
- color: var(--rel-text-disabled);
123
- /* Keep the item pointer-interactive so cursor:not-allowed actually
124
- renders; clicks are already neutralised by the item.disabled guard in
125
- onItemClick/onItemEnter. pointer-events:none here would make the row
126
- transparent to the pointer and suppress the cursor. */
127
- cursor: not-allowed;
128
- }
129
-
130
- /* [data-focused] (presence, not ="true"): the ?data-focused boolean
131
- binding renders the attribute with an EMPTY value, so a ="true"
132
- selector never matches. */
133
- .rel-menu-item[data-focused] {
134
- outline: 2px solid var(--rel-border-focus);
135
- outline-offset: -2px;
136
- }
137
-
138
- /* Width modes (Figma Type=Fixed / Type=Fit) */
139
- .rel-menu.fixed {
140
- width: var(--rel-menu-width, 300px);
141
- min-width: 0;
142
- box-sizing: border-box;
143
- }
144
-
145
- .rel-menu.fit {
146
- min-width: 0;
147
- width: max-content;
148
- }
149
-
150
- /* Checkbox (Type=Checkbox) / radio (Type=Select Option) rows render the real
151
- rel-checkbox / rel-radio. pointer-events:none keeps the whole row the click
152
- target so onItemClick stays the single source of truth. */
153
- .menu-control {
154
- pointer-events: none;
155
- width: 100%;
156
- }
157
-
158
- .menu-label {
159
- flex: 1 1 auto;
160
- }
161
-
162
- /* Submenu affordance — same rel-icon chevron the menubar uses. */
163
- .submenu-chevron {
164
- margin-left: auto;
165
- display: inline-flex;
166
- align-items: center;
167
- opacity: 0.6;
168
- pointer-events: none;
169
- }
170
-
171
- /* Shortcut / hint text (Figma body-sm-regular, caption tone) */
172
- .menu-shortcut,
173
- ::slotted([slot="shortcut"]) {
174
- font-family: var(--rel-body-sm-regular-font-family);
175
- font-weight: var(--rel-body-sm-regular-font-weight);
176
- font-size: var(--rel-body-sm-regular-font-size);
177
- line-height: var(--rel-body-sm-regular-line-height);
178
- letter-spacing: var(--rel-body-sm-regular-letter-spacing);
179
- color: var(--rel-text-caption);
180
- margin-left: auto;
181
- padding-left: var(--rel-spacing-md);
182
- }
183
-
184
-
185
- .rel-submenu {
186
- border: 1px solid var(--rel-border-subtle);
187
- position: absolute;
188
- top: calc(-1 * var(--rel-spacing-3xs));
189
- /* flush against the trigger (no gap) so moving the pointer into the flyout
190
- never crosses a dead zone that would fire the trigger's mouseleave */
191
- left: 100%;
192
- min-width: 200px;
193
- background: var(--rel-bg-neutral);
194
- color: var(--rel-text-default);
195
- border-radius: var(--rel-border-radius-md);
196
- box-shadow: var(--rel-shadows-shadow-300);
197
- /* padding:0 + overflow:hidden — safe here (a submenu has no further nested
198
- flyout to clip) — so sub-item hovers fill fully and clip to the corners */
199
- padding: 0;
200
- overflow: hidden;
201
- visibility: hidden;
202
- pointer-events: none;
203
- z-index: 20;
204
- }
205
-
206
- .rel-submenu.visible {
207
- visibility: visible;
208
- pointer-events: auto;
209
- }
210
-
211
- .rel-submenu .rel-menu-item {
212
- padding: var(--rel-spacing-xs) var(--rel-spacing-sm);
213
- }
214
- `;
215
-
216
- createRenderRoot() {
217
- return this;
218
- }
219
-
220
- connectedCallback() {
221
- super.connectedCallback();
222
- document.addEventListener("click", this.handleDocumentClick);
223
- }
224
-
225
- disconnectedCallback() {
226
- super.disconnectedCallback();
227
- document.removeEventListener("click", this.handleDocumentClick);
228
- }
229
-
230
- updated(changedProperties: Map<string, unknown>) {
231
- if (changedProperties.has('items')) {
232
- this._items = this.items.map(item => ({ ...item }));
233
- }
234
- }
235
-
236
- /** Index of a submenu locked open by a click (survives hover-out). -1 = none. */
237
- private lockedIndex = -1;
238
-
239
- private handleDocumentClick = (e: MouseEvent) => {
240
- // composedPath (NOT e.target): document-level e.target is retargeted to the
241
- // shadow host when this menu lives inside a shadow root, so this.contains()
242
- // wrongly reads an inside-click as outside and closes an open submenu on the
243
- // click that opened it. composedPath pierces shadow boundaries.
244
- if (!e.composedPath().includes(this)) {
245
- this.lockedIndex = -1;
246
- this.subFocusedIndex = -1;
247
- this._items = this._items.map(i => ({ ...i, open: false }));
248
- }
249
- };
250
-
251
- private onItemEnter(item: MenuItem, index: number) {
252
- if (item.disabled) return;
253
- if (this.type === "arrow" && item.submenu?.length) {
254
- // Hover opens this submenu; a click-locked one also stays open.
255
- this._items = this._items.map((i, idx) => ({ ...i, open: idx === index || idx === this.lockedIndex }));
256
- }
257
- }
258
-
259
- private onItemLeave(_item: MenuItem, index: number) {
260
- if (this.type !== "arrow") return;
261
- // Hover-out closes the submenu UNLESS it was locked open by a click. Moving
262
- // into the flyout doesn't fire this (the flyout is a DOM descendant of the
263
- // item and sits flush at left:100%, so there's no dead-zone gap).
264
- if (this.lockedIndex === index) return;
265
- this._items = this._items.map((i, idx) => ({ ...i, open: idx === this.lockedIndex }));
266
- }
267
-
268
- private onItemClick(item: MenuItem, index: number, ev?: Event) {
269
- ev?.stopPropagation();
270
- if (item.disabled) return;
271
-
272
- this.focusedIndex = index;
273
-
274
- if (this.type === "checkbox") {
275
- const updated = { ...item, checked: !item.checked };
276
- this._items = this._items.map((i, idx) => idx === index ? updated : i);
277
- this.dispatchEvent(
278
- new CustomEvent("rel-check", {
279
- detail: { item: updated, checked: updated.checked },
280
- bubbles: true,
281
- composed: true,
282
- })
283
- );
284
- return;
285
- }
286
-
287
- if (this.type === "arrow" && item.submenu?.length) {
288
- // Click LOCKS the submenu open (click again to unlock/close).
289
- const lock = this.lockedIndex !== index;
290
- this.lockedIndex = lock ? index : -1;
291
- this._items = this._items.map((i, idx) => ({ ...i, open: idx === index ? lock : false }));
292
- return;
293
- }
294
-
295
- // Default: set active
296
- const updated = { ...item, active: true };
297
- this._items = this._items.map((i, idx) => idx === index ? updated : { ...i, active: false });
298
- this.dispatchEvent(
299
- new CustomEvent("rel-select", {
300
- detail: { item: updated },
301
- bubbles: true,
302
- composed: true,
303
- })
304
- );
305
- }
306
-
307
- private onSubClick(parentIndex: number, subIndex: number, ev?: Event) {
308
- ev?.stopPropagation();
309
- this.lockedIndex = -1;
310
- this._items = this._items.map((i, idx) => ({
311
- ...i,
312
- active: false,
313
- open: false,
314
- submenu: i.submenu?.map((s, sIdx) => ({
315
- ...s,
316
- active: idx === parentIndex && sIdx === subIndex,
317
- })),
318
- }));
319
- this.dispatchEvent(
320
- new CustomEvent("rel-submenu-select", {
321
- detail: {
322
- parent: this._items[parentIndex],
323
- sub: this._items[parentIndex]?.submenu?.[subIndex],
324
- },
325
- bubbles: true,
326
- composed: true,
327
- })
328
- );
329
- }
330
-
331
- /** Keyboard-open locks the submenu (no hover-out to close it) and moves
332
- * virtual focus onto its first entry. */
333
- private openSubmenuAt(index: number) {
334
- this.lockedIndex = index;
335
- this.subFocusedIndex = 0;
336
- this._items = this._items.map((i, idx) => ({ ...i, open: idx === index }));
337
- }
338
-
339
- private closeSubmenus() {
340
- this.lockedIndex = -1;
341
- this.subFocusedIndex = -1;
342
- this._items = this._items.map(i => ({ ...i, open: false }));
343
- }
344
-
345
- private onKeydown(ev: KeyboardEvent) {
346
- const last = this._items.length - 1;
347
- const focusedItem = this._items[this.focusedIndex];
348
-
349
- // ── Virtual focus is inside an open submenu ──
350
- if (
351
- this.subFocusedIndex >= 0 &&
352
- this.type === "arrow" &&
353
- focusedItem?.open &&
354
- focusedItem.submenu?.length
355
- ) {
356
- const subLast = focusedItem.submenu.length - 1;
357
- switch (ev.key) {
358
- case "ArrowDown":
359
- this.subFocusedIndex = Math.min(subLast, this.subFocusedIndex + 1);
360
- ev.preventDefault();
361
- return;
362
- case "ArrowUp":
363
- this.subFocusedIndex = Math.max(0, this.subFocusedIndex - 1);
364
- ev.preventDefault();
365
- return;
366
- case "Home":
367
- this.subFocusedIndex = 0;
368
- ev.preventDefault();
369
- return;
370
- case "End":
371
- this.subFocusedIndex = subLast;
372
- ev.preventDefault();
373
- return;
374
- case "ArrowLeft":
375
- case "Escape":
376
- // Close only the submenu; don't let Escape also dismiss an
377
- // enclosing popover in the same keystroke.
378
- this.closeSubmenus();
379
- ev.preventDefault();
380
- ev.stopPropagation();
381
- return;
382
- case "Enter":
383
- case " ":
384
- this.onSubClick(this.focusedIndex, this.subFocusedIndex);
385
- ev.preventDefault();
386
- return;
387
- default:
388
- return;
389
- }
390
- }
391
-
392
- // ── Virtual focus is in the main list ──
393
- if (ev.key === "ArrowDown") {
394
- this.focusedIndex = Math.min(last, this.focusedIndex + 1);
395
- ev.preventDefault();
396
- } else if (ev.key === "ArrowUp") {
397
- this.focusedIndex = Math.max(0, this.focusedIndex - 1);
398
- ev.preventDefault();
399
- } else if (ev.key === "Home") {
400
- this.focusedIndex = 0;
401
- ev.preventDefault();
402
- } else if (ev.key === "End") {
403
- this.focusedIndex = last;
404
- ev.preventDefault();
405
- } else if (ev.key === "ArrowRight") {
406
- const item = this._items[this.focusedIndex];
407
- if (this.type === "arrow" && item?.submenu?.length && !item.disabled) {
408
- this.openSubmenuAt(this.focusedIndex);
409
- ev.preventDefault();
410
- }
411
- } else if (ev.key === "ArrowLeft" || ev.key === "Escape") {
412
- const anyOpen = this._items.some(i => i.open);
413
- if (anyOpen) {
414
- this.closeSubmenus();
415
- ev.preventDefault();
416
- if (ev.key === "Escape") ev.stopPropagation();
417
- }
418
- } else if (ev.key === "Enter" || ev.key === " ") {
419
- const item = this._items[this.focusedIndex];
420
- if (this.type === "arrow" && item?.submenu?.length && !item.disabled) {
421
- this.openSubmenuAt(this.focusedIndex);
422
- } else {
423
- this.onItemClick(item, this.focusedIndex);
424
- }
425
- ev.preventDefault();
426
- }
427
- }
428
-
429
- private renderMenuItem(item: MenuItem, index: number) {
430
- const isFocused = this.focusedIndex === index && this.subFocusedIndex < 0;
431
- const hasSubmenu = this.type === "arrow" && !!item.submenu?.length;
432
- const role =
433
- this.type === "checkbox"
434
- ? "menuitemcheckbox"
435
- : this.type === "select-option"
436
- ? "menuitemradio"
437
- : "menuitem";
438
- const ariaChecked =
439
- this.type === "checkbox"
440
- ? String(!!item.checked)
441
- : this.type === "select-option"
442
- ? String(!!item.active)
443
- : nothing;
444
-
445
- // Checkbox / select-option rows delegate their control to the real
446
- // rel-checkbox / rel-radio components (pointer-events:none so the whole ROW
447
- // stays the click target — onItemClick is the single source of truth and
448
- // the control just reflects item state).
449
- const control =
450
- this.type === "checkbox"
451
- ? html`<rel-checkbox
452
- class="menu-control"
453
- inert
454
- aria-hidden="true"
455
- label="${item.label}"
456
- status="${item.checked ? "checked" : "unchecked"}"
457
- state="${item.disabled ? "disabled" : "default"}"
458
- ></rel-checkbox>`
459
- : this.type === "select-option"
460
- ? html`<rel-radio
461
- class="menu-control"
462
- inert
463
- aria-hidden="true"
464
- .options="${[{ value: item.label, label: item.label, disabled: !!item.disabled }]}"
465
- .selectedValue="${item.active ? item.label : null}"
466
- ></rel-radio>`
467
- : html`
468
- <span class="menu-label">${item.label}</span>
469
- ${hasSubmenu
470
- ? html`<span class="submenu-chevron"
471
- ><rel-icon name="chevron-right" size="sm"></rel-icon
472
- ></span>`
473
- : ""}
474
- `;
475
-
476
- return html`
477
- <div
478
- id="${this.menuId}-item-${index}"
479
- class="rel-menu-item ${item.active ? "active" : ""} ${item.disabled ? "disabled" : ""}"
480
- role="${role}"
481
- aria-checked="${ariaChecked}"
482
- aria-label="${this.type === "checkbox" || this.type === "select-option" ? item.label : nothing}"
483
- aria-disabled="${item.disabled ? "true" : nothing}"
484
- aria-haspopup="${hasSubmenu ? "menu" : nothing}"
485
- aria-expanded="${hasSubmenu ? String(!!item.open) : nothing}"
486
- ?data-focused="${isFocused}"
487
- @mouseenter="${() => this.onItemEnter(item, index)}"
488
- @mouseleave="${() => this.onItemLeave(item, index)}"
489
- @click="${(e: Event) => this.onItemClick(item, index, e)}">
490
- ${control}
491
- ${hasSubmenu
492
- ? html`
493
- <div
494
- part="panel" class="rel-submenu ${item.open ? "visible" : ""}"
495
- role="menu"
496
- @click="${(e: Event) => e.stopPropagation()}">
497
- ${item.submenu!.map(
498
- (sub, subIndex) => html`
499
- <div
500
- id="${this.menuId}-item-${index}-sub-${subIndex}"
501
- class="rel-menu-item ${sub.active ? "active" : ""} ${sub.disabled ? "disabled" : ""}"
502
- role="menuitem"
503
- aria-disabled="${sub.disabled ? "true" : nothing}"
504
- ?data-focused="${this.focusedIndex === index && this.subFocusedIndex === subIndex}"
505
- @click="${(e: Event) => this.onSubClick(index, subIndex, e)}">
506
- <span class="menu-label">${sub.label}</span>
507
- </div>
508
- `
509
- )}
510
- </div>
511
- `
512
- : ""}
513
- </div>
514
- `;
515
- }
516
-
517
- render() {
518
- // DOM focus stays on the container; this tells AT which item virtual
519
- // focus is on as the user arrows around (main list or open submenu).
520
- const activeDescendant =
521
- this._items.length === 0
522
- ? nothing
523
- : this.subFocusedIndex >= 0
524
- ? `${this.menuId}-item-${this.focusedIndex}-sub-${this.subFocusedIndex}`
525
- : `${this.menuId}-item-${this.focusedIndex}`;
526
-
527
- return html`
528
- <style>${RelMenu._css}</style>
529
- <div
530
- part="list" class="rel-menu ${this.type === "fixed" || this.type === "fit" ? this.type : ""}"
531
- role="menu"
532
- aria-label="Menu"
533
- tabindex="0"
534
- aria-activedescendant="${activeDescendant}"
535
- @keydown="${this.onKeydown}"
536
- @click="${(e: Event) => e.stopPropagation()}">
537
- ${this._items.map((item, index) => this.renderMenuItem(item, index))}
538
- </div>
539
- `;
540
- }
541
- }
@@ -1,2 +0,0 @@
1
- export * from "./rel-menubar";
2
- export * from "./rel-menubar-menu";
@@ -1,3 +0,0 @@
1
- rel-menubar-menu {
2
- display: none;
3
- }
@@ -1,17 +0,0 @@
1
- import { LitElement, html } from "lit";
2
- import { customElement, property } from "lit/decorators.js";
3
- import "./rel-menubar-menu.css";
4
-
5
- @customElement("rel-menubar-menu")
6
- export class RelMenubarMenu extends LitElement {
7
- @property({ type: String }) label = "";
8
-
9
- // Disable shadow DOM to allow parent to access children
10
- createRenderRoot() {
11
- return this;
12
- }
13
-
14
- render() {
15
- return html``;
16
- }
17
- }