@reliability-design/web 1.4.6 → 1.4.7
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.
- package/CHANGELOG.md +211 -49
- package/dist/components/rel-accordion/README.md +132 -132
- package/dist/components/rel-alert/README.md +145 -141
- package/dist/components/rel-autocomplete/README.md +156 -61
- package/dist/components/rel-avatar/README.md +116 -130
- package/dist/components/rel-badge/README.md +98 -93
- package/dist/components/rel-breadcrumb/README.md +96 -102
- package/dist/components/rel-button/README.md +160 -130
- package/dist/components/rel-calendar/README.md +109 -109
- package/dist/components/rel-card/README.md +75 -94
- package/dist/components/rel-cardmedia/README.md +100 -127
- package/dist/components/rel-carousel/README.md +184 -166
- package/dist/components/rel-checkbox/README.md +93 -75
- package/dist/components/rel-chip/README.md +132 -121
- package/dist/components/rel-container/README.md +107 -99
- package/dist/components/rel-date-picker/README.md +200 -289
- package/dist/components/rel-divider/README.md +78 -100
- package/dist/components/rel-drawer/README.md +187 -200
- package/dist/components/rel-kbd/README.md +141 -151
- package/dist/components/rel-menu/README.md +178 -151
- package/dist/components/rel-menubar/README.md +218 -329
- package/dist/components/rel-otp/README.md +170 -440
- package/dist/components/rel-pagination/README.md +100 -424
- package/dist/components/rel-popover/README.md +203 -568
- package/dist/components/rel-progress/README.md +88 -110
- package/dist/components/rel-radio/README.md +131 -138
- package/dist/components/rel-range-picker/README.md +162 -0
- package/dist/components/rel-resizable/README.md +199 -140
- package/dist/components/rel-segmented-button/README.md +155 -141
- package/dist/components/rel-select/README.md +154 -93
- package/dist/components/rel-sheet/README.md +168 -224
- package/dist/components/rel-skeleton/README.md +103 -0
- package/dist/components/rel-slider/README.md +130 -135
- package/dist/components/rel-snackbar/README.md +162 -134
- package/dist/components/rel-stepper/README.md +158 -160
- package/dist/components/rel-switch/README.md +108 -102
- package/dist/components/rel-tab/README.md +171 -163
- package/dist/components/rel-textfield/README.md +159 -69
- package/dist/components/rel-time-picker/README.md +122 -157
- package/dist/components/rel-toast/README.md +157 -148
- package/dist/components/rel-tooltip/README.md +108 -111
- package/dist/components/rel-upload/README.md +220 -0
- package/dist/index.d.mts +1147 -340
- package/dist/index.d.ts +1147 -340
- package/dist/index.js +3451 -3163
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3416 -3118
- package/dist/index.mjs.map +1 -1
- package/dist/styles/vars.css +877 -866
- package/dist/{vars-XNMTSRVN.css → vars-JHTUGNRG.css} +877 -866
- package/package.json +3 -2
- package/dist/components/rel-accordion/rel-accordion.ts +0 -336
- package/dist/components/rel-alert/rel-alert.ts +0 -305
- package/dist/components/rel-autocomplete/rel-autocomplete.ts +0 -720
- package/dist/components/rel-avatar/rel-avatar.ts +0 -142
- package/dist/components/rel-badge/rel-badge.ts +0 -175
- package/dist/components/rel-breadcrumb/rel-breadcrumb.ts +0 -106
- package/dist/components/rel-button/rel-button.ts +0 -560
- package/dist/components/rel-calendar/index.ts +0 -1
- package/dist/components/rel-calendar/rel-calendar.ts +0 -565
- package/dist/components/rel-card/rel-card.ts +0 -88
- package/dist/components/rel-cardmedia/rel-cardmedia.ts +0 -88
- package/dist/components/rel-carousel/index.ts +0 -1
- package/dist/components/rel-carousel/rel-carousel.ts +0 -723
- package/dist/components/rel-chart/README.md +0 -286
- package/dist/components/rel-chart/index.ts +0 -1
- package/dist/components/rel-chart/rel-chart.ts +0 -754
- package/dist/components/rel-checkbox/rel-checkbox.ts +0 -382
- package/dist/components/rel-chip/rel-chip.ts +0 -483
- package/dist/components/rel-container/rel-container.ts +0 -95
- package/dist/components/rel-date-picker/date-format.test.ts +0 -43
- package/dist/components/rel-date-picker/date-format.ts +0 -47
- package/dist/components/rel-date-picker/date-parse.test.ts +0 -116
- package/dist/components/rel-date-picker/date-parse.ts +0 -178
- package/dist/components/rel-date-picker/index.ts +0 -1
- package/dist/components/rel-date-picker/rel-date-picker.ts +0 -1676
- package/dist/components/rel-divider/rel-divider.ts +0 -54
- package/dist/components/rel-drawer/index.ts +0 -1
- package/dist/components/rel-drawer/rel-drawer-footer.ts +0 -27
- package/dist/components/rel-drawer/rel-drawer.ts +0 -607
- package/dist/components/rel-kbd/rel-kbd.ts +0 -188
- package/dist/components/rel-layout/rel-layout.ts +0 -49
- package/dist/components/rel-menu/rel-menu.ts +0 -541
- package/dist/components/rel-menubar/index.ts +0 -2
- package/dist/components/rel-menubar/rel-menubar-menu.css +0 -3
- package/dist/components/rel-menubar/rel-menubar-menu.ts +0 -17
- package/dist/components/rel-menubar/rel-menubar.ts +0 -777
- package/dist/components/rel-nav-menu/README.md +0 -289
- package/dist/components/rel-nav-menu/index.ts +0 -3
- package/dist/components/rel-nav-menu/rel-nav-menu-item.ts +0 -243
- package/dist/components/rel-nav-menu/rel-nav-menu-panel.ts +0 -72
- package/dist/components/rel-nav-menu/rel-nav-menu.ts +0 -270
- package/dist/components/rel-navbar/README.md +0 -126
- package/dist/components/rel-navbar/rel-navbar.ts +0 -105
- package/dist/components/rel-otp/index.ts +0 -1
- package/dist/components/rel-otp/rel-otp.ts +0 -527
- package/dist/components/rel-pagination/index.ts +0 -1
- package/dist/components/rel-pagination/rel-pagination.ts +0 -522
- package/dist/components/rel-popover/index.ts +0 -2
- package/dist/components/rel-popover/rel-popover-content.ts +0 -81
- package/dist/components/rel-popover/rel-popover.ts +0 -475
- package/dist/components/rel-progress/rel-progress.ts +0 -234
- package/dist/components/rel-radio/rel-radio.ts +0 -481
- package/dist/components/rel-range-picker/rel-range-picker.ts +0 -1688
- package/dist/components/rel-resizable/index.ts +0 -3
- package/dist/components/rel-resizable/rel-resizable-group.ts +0 -405
- package/dist/components/rel-resizable/rel-resizable-handle.ts +0 -150
- package/dist/components/rel-resizable/rel-resizable-panel.ts +0 -58
- package/dist/components/rel-segmented-button/rel-segmented-button.ts +0 -217
- package/dist/components/rel-select/rel-select.ts +0 -1416
- package/dist/components/rel-sheet/rel-sheet.ts +0 -610
- package/dist/components/rel-sidebar/README.md +0 -209
- package/dist/components/rel-sidebar/rel-sidebar.ts +0 -1191
- package/dist/components/rel-skeleton/rel-skeleton.ts +0 -143
- package/dist/components/rel-slider/rel-slider.ts +0 -673
- package/dist/components/rel-snackbar/rel-snackbar.ts +0 -329
- package/dist/components/rel-stepper/rel-stepper.ts +0 -567
- package/dist/components/rel-switch/rel-switch.ts +0 -199
- package/dist/components/rel-tab/rel-tab.ts +0 -264
- package/dist/components/rel-table/README.md +0 -178
- package/dist/components/rel-table/rel-table-body.ts +0 -22
- package/dist/components/rel-table/rel-table-caption.ts +0 -30
- package/dist/components/rel-table/rel-table-cell.ts +0 -43
- package/dist/components/rel-table/rel-table-footer.ts +0 -24
- package/dist/components/rel-table/rel-table-head.ts +0 -47
- package/dist/components/rel-table/rel-table-header.ts +0 -22
- package/dist/components/rel-table/rel-table-row.ts +0 -37
- package/dist/components/rel-table/rel-table.ts +0 -62
- package/dist/components/rel-textfield/rel-textfield.ts +0 -1054
- package/dist/components/rel-time-picker/rel-time-picker.ts +0 -1482
- package/dist/components/rel-time-picker/time-parse.test.ts +0 -67
- package/dist/components/rel-time-picker/time-parse.ts +0 -85
- package/dist/components/rel-toast/rel-toast.ts +0 -206
- package/dist/components/rel-tooltip/rel-tooltip.ts +0 -193
- package/dist/components/rel-topbar/rel-topbar-actions.ts +0 -27
- package/dist/components/rel-topbar/rel-topbar-brand.ts +0 -54
- package/dist/components/rel-topbar/rel-topbar-dropdown-item.ts +0 -69
- package/dist/components/rel-topbar/rel-topbar-dropdown.ts +0 -31
- package/dist/components/rel-topbar/rel-topbar-item.ts +0 -324
- package/dist/components/rel-topbar/rel-topbar-mobile-panel.ts +0 -27
- package/dist/components/rel-topbar/rel-topbar-profile.ts +0 -243
- package/dist/components/rel-topbar/rel-topbar-utilities.ts +0 -27
- package/dist/components/rel-topbar/rel-topbar.ts +0 -446
- package/dist/components/rel-upload/UPLOAD-DESIGN-CHANGES.md +0 -134
- package/dist/components/rel-upload/rel-upload-actions.ts +0 -163
- package/dist/components/rel-upload/rel-upload.ts +0 -968
|
@@ -1,777 +0,0 @@
|
|
|
1
|
-
import { LitElement, html, nothing } from "lit";
|
|
2
|
-
import { customElement, property, state } from "lit/decorators.js";
|
|
3
|
-
import { matchesShortcut, isEditableTarget } from "../../utils/shortcuts.js";
|
|
4
|
-
import { RelMenubarMenu } from "./rel-menubar-menu";
|
|
5
|
-
import "@reliability-design/icons/rel-icon";
|
|
6
|
-
import "../rel-checkbox/rel-checkbox.js";
|
|
7
|
-
import "../rel-radio/rel-radio.js";
|
|
8
|
-
|
|
9
|
-
interface MenuState {
|
|
10
|
-
menu: RelMenubarMenu;
|
|
11
|
-
open: boolean;
|
|
12
|
-
panelElement?: HTMLElement;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
@customElement("rel-menubar")
|
|
16
|
-
export class RelMenubar extends LitElement {
|
|
17
|
-
/**
|
|
18
|
-
* Opt-in: actually BIND the keys declared via data-shortcut (e.g.
|
|
19
|
-
* data-shortcut="Ctrl+Z"), not just display them. "Ctrl" matches Cmd on
|
|
20
|
-
* Apple platforms when written as "Mod". Keystrokes originating in
|
|
21
|
-
* inputs/textareas/contenteditable are ignored.
|
|
22
|
-
*/
|
|
23
|
-
@property({ type: Boolean, attribute: "bind-shortcuts" }) bindShortcuts = false;
|
|
24
|
-
|
|
25
|
-
@state() private menus: MenuState[] = [];
|
|
26
|
-
@state() private openMenuIndex: number = -1;
|
|
27
|
-
@state() private focusedTriggerIndex: number = 0;
|
|
28
|
-
@state() private focusedItemIndex: number = -1;
|
|
29
|
-
/** Virtual-focus index inside the open submenu; -1 = focus is in the panel. */
|
|
30
|
-
@state() private focusedSubItemIndex: number = -1;
|
|
31
|
-
@state() private openSubmenuElement: HTMLElement | null = null;
|
|
32
|
-
|
|
33
|
-
private hoverSwitchEnabled = false;
|
|
34
|
-
private _onScroll?: () => void;
|
|
35
|
-
|
|
36
|
-
/** Stable id prefix for aria-activedescendant targets (light DOM, so ids
|
|
37
|
-
* must be unique per menubar instance on the page). */
|
|
38
|
-
private menubarId = `rel-menubar-${Math.random().toString(36).slice(2)}`;
|
|
39
|
-
|
|
40
|
-
public static readonly _css = `
|
|
41
|
-
/* This component renders into LIGHT DOM (createRenderRoot returns this), so
|
|
42
|
-
:host / <slot> don't work here — the raw <rel-menubar-menu> definitions
|
|
43
|
-
are data holders that must be hidden with a plain selector. Relying on the
|
|
44
|
-
global rel-menubar-menu.css is not enough: it can't cross a consumer's
|
|
45
|
-
shadow boundary AND it's a no-op in the tsup-built package (css:"copy"). */
|
|
46
|
-
rel-menubar {
|
|
47
|
-
color: var(--rel-text-default);
|
|
48
|
-
display: block;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
rel-menubar-menu {
|
|
52
|
-
display: none;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
.menubar-container {
|
|
56
|
-
display: flex;
|
|
57
|
-
align-items: stretch;
|
|
58
|
-
background: var(--rel-bg-surface-elevated);
|
|
59
|
-
border: var(--rel-stroke-sm) solid var(--rel-border-default);
|
|
60
|
-
border-radius: var(--rel-border-radius-sm);
|
|
61
|
-
overflow: hidden;
|
|
62
|
-
padding: 0;
|
|
63
|
-
gap: var(--rel-spacing-3xs);
|
|
64
|
-
height: 40px;
|
|
65
|
-
width: fit-content;
|
|
66
|
-
font-family: var(--rel-body-sm-regular-font-family);
|
|
67
|
-
font-weight: var(--rel-body-sm-regular-font-weight);
|
|
68
|
-
font-size: var(--rel-body-sm-regular-font-size);
|
|
69
|
-
line-height: var(--rel-body-sm-regular-line-height);
|
|
70
|
-
letter-spacing: var(--rel-body-sm-regular-letter-spacing);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
.menubar-trigger {
|
|
74
|
-
display: flex;
|
|
75
|
-
align-items: center;
|
|
76
|
-
align-self: stretch;
|
|
77
|
-
height: 100%;
|
|
78
|
-
padding: 0 var(--rel-spacing-sm);
|
|
79
|
-
background: transparent;
|
|
80
|
-
border: none;
|
|
81
|
-
cursor: pointer;
|
|
82
|
-
font-family: inherit;
|
|
83
|
-
font-size: inherit;
|
|
84
|
-
color: var(--rel-text-label);
|
|
85
|
-
transition: background 0.15s ease;
|
|
86
|
-
user-select: none;
|
|
87
|
-
outline: none;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
.menubar-trigger:hover,
|
|
91
|
-
.menubar-trigger.active {
|
|
92
|
-
background: var(--rel-bg-neutral-elevated-2);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
.menubar-trigger:focus-visible {
|
|
96
|
-
outline: 2px solid var(--rel-border-focus);
|
|
97
|
-
outline-offset: 2px;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
.menubar-panel {
|
|
101
|
-
position: fixed;
|
|
102
|
-
min-width: 220px;
|
|
103
|
-
background: var(--rel-bg-surface);
|
|
104
|
-
border: var(--rel-stroke-sm) solid var(--rel-border-default);
|
|
105
|
-
border-radius: var(--rel-border-radius-sm);
|
|
106
|
-
box-shadow: var(--rel-shadows-shadow-300);
|
|
107
|
-
padding: 0;
|
|
108
|
-
overflow: hidden;
|
|
109
|
-
z-index: 1000;
|
|
110
|
-
display: none;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
.menubar-panel.open {
|
|
114
|
-
display: block;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
.menu-item {
|
|
118
|
-
position: relative;
|
|
119
|
-
display: flex;
|
|
120
|
-
align-items: center;
|
|
121
|
-
justify-content: space-between;
|
|
122
|
-
gap: var(--rel-spacing-sm);
|
|
123
|
-
padding: var(--rel-spacing-xs) var(--rel-spacing-md);
|
|
124
|
-
cursor: pointer;
|
|
125
|
-
transition: background 0.15s ease;
|
|
126
|
-
user-select: none;
|
|
127
|
-
color: var(--rel-text-label);
|
|
128
|
-
line-height: var(--rel-body-sm-regular-line-height);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
.menu-item:hover,
|
|
132
|
-
.menu-item.focused {
|
|
133
|
-
background: var(--rel-bg-neutral-elevated-2);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
.menu-item[data-disabled="true"] {
|
|
137
|
-
opacity: 0.5;
|
|
138
|
-
cursor: not-allowed;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
.menu-separator {
|
|
142
|
-
height: var(--rel-stroke-sm);
|
|
143
|
-
background: var(--rel-border-default);
|
|
144
|
-
margin: 0;
|
|
145
|
-
padding: 0;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
rel-icon {
|
|
149
|
-
display: inline-flex;
|
|
150
|
-
pointer-events: none;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
.menu-submenu-trigger {
|
|
154
|
-
position: relative;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
.menu-submenu-trigger rel-icon {
|
|
158
|
-
opacity: 0.6;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
.submenu-panel {
|
|
162
|
-
position: fixed;
|
|
163
|
-
min-width: 200px;
|
|
164
|
-
background: var(--rel-bg-surface);
|
|
165
|
-
border: var(--rel-stroke-sm) solid var(--rel-border-default);
|
|
166
|
-
border-radius: var(--rel-border-radius-sm);
|
|
167
|
-
box-shadow: var(--rel-shadows-shadow-300);
|
|
168
|
-
padding: 0;
|
|
169
|
-
overflow: hidden;
|
|
170
|
-
z-index: 1001;
|
|
171
|
-
display: none;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
.submenu-panel.open {
|
|
175
|
-
display: block;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
.menu-shortcut {
|
|
179
|
-
opacity: 0.6;
|
|
180
|
-
margin-left: auto;
|
|
181
|
-
display: flex;
|
|
182
|
-
align-items: center;
|
|
183
|
-
gap: var(--rel-spacing-3xs);
|
|
184
|
-
font-family: var(--rel-body-xs-regular-font-family);
|
|
185
|
-
font-weight: var(--rel-body-xs-regular-font-weight);
|
|
186
|
-
font-size: var(--rel-body-xs-regular-font-size);
|
|
187
|
-
line-height: var(--rel-body-xs-regular-line-height);
|
|
188
|
-
letter-spacing: var(--rel-body-xs-regular-letter-spacing);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
.menubar-panel rel-checkbox {
|
|
192
|
-
display: block;
|
|
193
|
-
width: 100%;
|
|
194
|
-
padding: var(--rel-spacing-xs, 8px) var(--rel-spacing-md, 16px);
|
|
195
|
-
box-sizing: border-box;
|
|
196
|
-
cursor: pointer;
|
|
197
|
-
}
|
|
198
|
-
`;
|
|
199
|
-
|
|
200
|
-
createRenderRoot() {
|
|
201
|
-
return this;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
connectedCallback() {
|
|
205
|
-
super.connectedCallback();
|
|
206
|
-
this.discoverMenus();
|
|
207
|
-
document.addEventListener("click", this.handleOutsideClick);
|
|
208
|
-
document.addEventListener("keydown", this.handleShortcutKeyDown);
|
|
209
|
-
this._onScroll = () => {
|
|
210
|
-
this._repositionOpenMenu();
|
|
211
|
-
this._repositionOpenSubmenu();
|
|
212
|
-
};
|
|
213
|
-
window.addEventListener('scroll', this._onScroll, { passive: true, capture: true });
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
disconnectedCallback() {
|
|
217
|
-
super.disconnectedCallback();
|
|
218
|
-
document.removeEventListener("click", this.handleOutsideClick);
|
|
219
|
-
document.removeEventListener("keydown", this.handleShortcutKeyDown);
|
|
220
|
-
if (this._onScroll) {
|
|
221
|
-
window.removeEventListener('scroll', this._onScroll, { capture: true });
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
private _repositionOpenSubmenu() {
|
|
226
|
-
if (!this.openSubmenuElement) return;
|
|
227
|
-
const submenuPanel = this.querySelector('.submenu-panel.open') as HTMLElement;
|
|
228
|
-
if (!submenuPanel) return;
|
|
229
|
-
const trigger = this.querySelector('.menu-submenu-trigger.focused, .menu-submenu-trigger:hover') as HTMLElement
|
|
230
|
-
?? submenuPanel.previousElementSibling as HTMLElement;
|
|
231
|
-
if (!trigger) return;
|
|
232
|
-
const rect = trigger.getBoundingClientRect();
|
|
233
|
-
submenuPanel.style.top = `${rect.top}px`;
|
|
234
|
-
submenuPanel.style.left = `${rect.right + 4}px`;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
private _repositionOpenMenu() {
|
|
238
|
-
if (this.openMenuIndex === -1) return;
|
|
239
|
-
const triggers = this.querySelectorAll('.menubar-trigger');
|
|
240
|
-
const trigger = triggers[this.openMenuIndex] as HTMLElement;
|
|
241
|
-
const panel = this.querySelector(`.menubar-panel[data-menu-index="${this.openMenuIndex}"]`) as HTMLElement;
|
|
242
|
-
if (!trigger || !panel) return;
|
|
243
|
-
const rect = trigger.getBoundingClientRect();
|
|
244
|
-
panel.style.top = `${rect.bottom + 4}px`;
|
|
245
|
-
panel.style.left = `${rect.left}px`;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
private discoverMenus() {
|
|
249
|
-
const menuElements = Array.from(
|
|
250
|
-
this.querySelectorAll("rel-menubar-menu")
|
|
251
|
-
) as RelMenubarMenu[];
|
|
252
|
-
this.menus = menuElements.map((menu) => ({ menu, open: false }));
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
private handleShortcutKeyDown = (e: KeyboardEvent) => {
|
|
256
|
-
if (!this.bindShortcuts || e.defaultPrevented) return;
|
|
257
|
-
if (isEditableTarget(e)) return;
|
|
258
|
-
|
|
259
|
-
for (const { menu } of this.menus) {
|
|
260
|
-
for (const item of this.getFocusableItems(menu)) {
|
|
261
|
-
const shortcut = item.getAttribute("data-shortcut");
|
|
262
|
-
if (!shortcut) continue;
|
|
263
|
-
if ((item as HTMLButtonElement).disabled) continue;
|
|
264
|
-
if (matchesShortcut(e, shortcut)) {
|
|
265
|
-
e.preventDefault();
|
|
266
|
-
this.handleItemClick(item as HTMLElement);
|
|
267
|
-
return;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
};
|
|
272
|
-
|
|
273
|
-
private handleOutsideClick = (e: MouseEvent) => {
|
|
274
|
-
// composedPath (NOT e.target): a document-level listener sees e.target
|
|
275
|
-
// retargeted to the shadow host when the menubar lives inside a shadow root
|
|
276
|
-
// (e.g. any consumer component), so this.contains(e.target) is false for a
|
|
277
|
-
// click that is actually INSIDE the menubar — closing the menu on the very
|
|
278
|
-
// click that opened it. composedPath pierces shadow boundaries.
|
|
279
|
-
if (!e.composedPath().includes(this)) {
|
|
280
|
-
this.closeAllMenus();
|
|
281
|
-
}
|
|
282
|
-
};
|
|
283
|
-
|
|
284
|
-
private closeAllMenus() {
|
|
285
|
-
this.openMenuIndex = -1;
|
|
286
|
-
this.openSubmenuElement = null;
|
|
287
|
-
this.focusedItemIndex = -1;
|
|
288
|
-
this.focusedSubItemIndex = -1;
|
|
289
|
-
this.hoverSwitchEnabled = false;
|
|
290
|
-
this.requestUpdate();
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/** Close everything and put DOM focus back on the trigger that owned the
|
|
294
|
-
* open menu. (Capturing the index BEFORE closing — closeAllMenus resets
|
|
295
|
-
* openMenuIndex to -1, which used to drop focus to <body>.) */
|
|
296
|
-
private closeAllMenusAndRefocusTrigger() {
|
|
297
|
-
const idx = this.openMenuIndex;
|
|
298
|
-
this.closeAllMenus();
|
|
299
|
-
if (idx !== -1) {
|
|
300
|
-
this.focusedTriggerIndex = idx;
|
|
301
|
-
this.focusTrigger(idx);
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
private openMenu(index: number, triggerElement: HTMLElement) {
|
|
306
|
-
this.openMenuIndex = index;
|
|
307
|
-
this.focusedItemIndex = -1;
|
|
308
|
-
this.focusedSubItemIndex = -1;
|
|
309
|
-
this.openSubmenuElement = null;
|
|
310
|
-
this.requestUpdate();
|
|
311
|
-
|
|
312
|
-
// Position the panel below the trigger
|
|
313
|
-
this.updateComplete.then(() => {
|
|
314
|
-
const panel = this.querySelector(
|
|
315
|
-
`.menubar-panel[data-menu-index="${index}"]`
|
|
316
|
-
) as HTMLElement;
|
|
317
|
-
if (panel && triggerElement) {
|
|
318
|
-
const rect = triggerElement.getBoundingClientRect();
|
|
319
|
-
panel.style.top = `${rect.bottom + 4}px`;
|
|
320
|
-
panel.style.left = `${rect.left}px`;
|
|
321
|
-
}
|
|
322
|
-
});
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
private handleTriggerClick(index: number, event: Event) {
|
|
326
|
-
const target = event.currentTarget as HTMLElement;
|
|
327
|
-
if (this.openMenuIndex === index) {
|
|
328
|
-
this.closeAllMenus();
|
|
329
|
-
} else {
|
|
330
|
-
this.openMenu(index, target);
|
|
331
|
-
this.hoverSwitchEnabled = true;
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
private handleTriggerMouseEnter(index: number, event: Event) {
|
|
336
|
-
if (this.openMenuIndex !== -1 && this.hoverSwitchEnabled) {
|
|
337
|
-
const target = event.currentTarget as HTMLElement;
|
|
338
|
-
this.openMenu(index, target);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
private handleTriggerKeyDown(event: KeyboardEvent) {
|
|
343
|
-
const { key } = event;
|
|
344
|
-
|
|
345
|
-
if (key === "ArrowRight") {
|
|
346
|
-
event.preventDefault();
|
|
347
|
-
this.focusedTriggerIndex = Math.min(
|
|
348
|
-
this.menus.length - 1,
|
|
349
|
-
this.focusedTriggerIndex + 1
|
|
350
|
-
);
|
|
351
|
-
this.focusTrigger(this.focusedTriggerIndex);
|
|
352
|
-
} else if (key === "ArrowLeft") {
|
|
353
|
-
event.preventDefault();
|
|
354
|
-
this.focusedTriggerIndex = Math.max(0, this.focusedTriggerIndex - 1);
|
|
355
|
-
this.focusTrigger(this.focusedTriggerIndex);
|
|
356
|
-
} else if (key === "Enter" || key === " " || key === "ArrowDown") {
|
|
357
|
-
event.preventDefault();
|
|
358
|
-
const trigger = this.querySelector(
|
|
359
|
-
`.menubar-trigger[data-index="${this.focusedTriggerIndex}"]`
|
|
360
|
-
) as HTMLElement;
|
|
361
|
-
if (trigger) {
|
|
362
|
-
if (this.openMenuIndex !== this.focusedTriggerIndex) {
|
|
363
|
-
this.openMenu(this.focusedTriggerIndex, trigger);
|
|
364
|
-
}
|
|
365
|
-
this.updateComplete.then(() => this.focusMenuItem(0));
|
|
366
|
-
}
|
|
367
|
-
} else if (key === "Escape" && this.openMenuIndex !== -1) {
|
|
368
|
-
event.preventDefault();
|
|
369
|
-
this.closeAllMenusAndRefocusTrigger();
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
private focusTrigger(index: number) {
|
|
374
|
-
const trigger = this.querySelector(
|
|
375
|
-
`.menubar-trigger[data-index="${index}"]`
|
|
376
|
-
) as HTMLElement;
|
|
377
|
-
trigger?.focus();
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
private handlePanelKeyDown(event: KeyboardEvent) {
|
|
381
|
-
const { key } = event;
|
|
382
|
-
const menuContent = this.menus[this.openMenuIndex]?.menu;
|
|
383
|
-
if (!menuContent) return;
|
|
384
|
-
|
|
385
|
-
// ── Virtual focus is inside an open submenu ──
|
|
386
|
-
if (this.openSubmenuElement) {
|
|
387
|
-
const subItems = this.getSubmenuItems(this.openSubmenuElement);
|
|
388
|
-
if (key === "ArrowDown") {
|
|
389
|
-
event.preventDefault();
|
|
390
|
-
this.focusedSubItemIndex = Math.min(
|
|
391
|
-
subItems.length - 1,
|
|
392
|
-
this.focusedSubItemIndex + 1
|
|
393
|
-
);
|
|
394
|
-
} else if (key === "ArrowUp") {
|
|
395
|
-
event.preventDefault();
|
|
396
|
-
this.focusedSubItemIndex = Math.max(0, this.focusedSubItemIndex - 1);
|
|
397
|
-
} else if (key === "Home") {
|
|
398
|
-
event.preventDefault();
|
|
399
|
-
this.focusedSubItemIndex = 0;
|
|
400
|
-
} else if (key === "End") {
|
|
401
|
-
event.preventDefault();
|
|
402
|
-
this.focusedSubItemIndex = subItems.length - 1;
|
|
403
|
-
} else if (key === "ArrowLeft" || key === "Escape") {
|
|
404
|
-
// Close only the submenu; keep the parent panel open and focused.
|
|
405
|
-
event.preventDefault();
|
|
406
|
-
event.stopPropagation();
|
|
407
|
-
this.openSubmenuElement = null;
|
|
408
|
-
this.focusedSubItemIndex = -1;
|
|
409
|
-
} else if (key === "Enter" || key === " ") {
|
|
410
|
-
event.preventDefault();
|
|
411
|
-
const subItem = subItems[this.focusedSubItemIndex];
|
|
412
|
-
if (subItem) this.handleSubmenuItemClick(subItem as HTMLElement);
|
|
413
|
-
}
|
|
414
|
-
this.requestUpdate();
|
|
415
|
-
return;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// ── Virtual focus is in the panel's main list ──
|
|
419
|
-
const items = this.getFocusableItems(menuContent);
|
|
420
|
-
|
|
421
|
-
if (key === "ArrowDown") {
|
|
422
|
-
event.preventDefault();
|
|
423
|
-
this.focusedItemIndex = Math.min(items.length - 1, this.focusedItemIndex + 1);
|
|
424
|
-
this.requestUpdate();
|
|
425
|
-
} else if (key === "ArrowUp") {
|
|
426
|
-
event.preventDefault();
|
|
427
|
-
this.focusedItemIndex = Math.max(0, this.focusedItemIndex - 1);
|
|
428
|
-
this.requestUpdate();
|
|
429
|
-
} else if (key === "Home") {
|
|
430
|
-
event.preventDefault();
|
|
431
|
-
this.focusedItemIndex = 0;
|
|
432
|
-
this.requestUpdate();
|
|
433
|
-
} else if (key === "End") {
|
|
434
|
-
event.preventDefault();
|
|
435
|
-
this.focusedItemIndex = items.length - 1;
|
|
436
|
-
this.requestUpdate();
|
|
437
|
-
} else if (key === "ArrowRight") {
|
|
438
|
-
event.preventDefault();
|
|
439
|
-
const currentItem = items[this.focusedItemIndex];
|
|
440
|
-
if (currentItem?.hasAttribute("data-submenu")) {
|
|
441
|
-
// Position against the RENDERED row, not the display:none source
|
|
442
|
-
// element inside <rel-menubar-menu> (whose rect is 0×0).
|
|
443
|
-
const panel = this.querySelector<HTMLElement>(
|
|
444
|
-
`.menubar-panel[data-menu-index="${this.openMenuIndex}"]`
|
|
445
|
-
);
|
|
446
|
-
const renderedTrigger = panel?.querySelector<HTMLElement>(
|
|
447
|
-
`[data-focus-index="${this.focusedItemIndex}"]`
|
|
448
|
-
);
|
|
449
|
-
this.focusedSubItemIndex = 0;
|
|
450
|
-
this.openSubmenu(currentItem as HTMLElement, renderedTrigger ?? undefined);
|
|
451
|
-
}
|
|
452
|
-
} else if (key === "ArrowLeft") {
|
|
453
|
-
event.preventDefault();
|
|
454
|
-
} else if (key === "Escape") {
|
|
455
|
-
event.preventDefault();
|
|
456
|
-
this.closeAllMenusAndRefocusTrigger();
|
|
457
|
-
} else if (key === "Enter" || key === " ") {
|
|
458
|
-
event.preventDefault();
|
|
459
|
-
const currentItem = items[this.focusedItemIndex];
|
|
460
|
-
if (currentItem) {
|
|
461
|
-
if (currentItem.hasAttribute("data-submenu")) {
|
|
462
|
-
const panel = this.querySelector<HTMLElement>(
|
|
463
|
-
`.menubar-panel[data-menu-index="${this.openMenuIndex}"]`
|
|
464
|
-
);
|
|
465
|
-
const renderedTrigger = panel?.querySelector<HTMLElement>(
|
|
466
|
-
`[data-focus-index="${this.focusedItemIndex}"]`
|
|
467
|
-
);
|
|
468
|
-
this.focusedSubItemIndex = 0;
|
|
469
|
-
this.openSubmenu(currentItem as HTMLElement, renderedTrigger ?? undefined);
|
|
470
|
-
} else {
|
|
471
|
-
this.handleItemClick(currentItem as HTMLElement);
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
private getSubmenuItems(submenuElement: HTMLElement): Element[] {
|
|
478
|
-
return Array.from(submenuElement.children).filter((el) =>
|
|
479
|
-
el.hasAttribute("data-item")
|
|
480
|
-
);
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
private getFocusableItems(menuContent: RelMenubarMenu): Element[] {
|
|
484
|
-
return Array.from(menuContent.children).filter(
|
|
485
|
-
(el) =>
|
|
486
|
-
el.hasAttribute("data-item") ||
|
|
487
|
-
el.hasAttribute("data-checkbox") ||
|
|
488
|
-
el.hasAttribute("data-radio") ||
|
|
489
|
-
el.hasAttribute("data-submenu")
|
|
490
|
-
);
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
private focusMenuItem(index: number) {
|
|
494
|
-
this.focusedItemIndex = index;
|
|
495
|
-
const panel = this.querySelector(
|
|
496
|
-
`.menubar-panel[data-menu-index="${this.openMenuIndex}"]`
|
|
497
|
-
) as HTMLElement;
|
|
498
|
-
panel?.focus();
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
private handleItemClick(item: HTMLElement) {
|
|
502
|
-
if (item.hasAttribute("data-checkbox")) {
|
|
503
|
-
const checkbox = item.querySelector('input[type="checkbox"]') as HTMLInputElement;
|
|
504
|
-
const labelText = Array.from(item.childNodes)
|
|
505
|
-
.filter(n => n.nodeType === Node.TEXT_NODE)
|
|
506
|
-
.map(n => n.textContent?.trim())
|
|
507
|
-
.filter(Boolean)
|
|
508
|
-
.join(' ') || item.getAttribute('label') || '';
|
|
509
|
-
if (checkbox) {
|
|
510
|
-
checkbox.checked = !checkbox.checked;
|
|
511
|
-
this.dispatchChangeEvent("checkbox", labelText, checkbox.checked);
|
|
512
|
-
this.requestUpdate();
|
|
513
|
-
}
|
|
514
|
-
} else if (item.hasAttribute("data-radio")) {
|
|
515
|
-
const radioGroup = item.closest("[data-radio-group]") as HTMLElement;
|
|
516
|
-
const value = item.getAttribute("data-radio") || item.getAttribute("value") || "";
|
|
517
|
-
const name = radioGroup?.getAttribute("name") || "";
|
|
518
|
-
|
|
519
|
-
// Update visual state
|
|
520
|
-
const allRadios = radioGroup?.querySelectorAll("[data-radio]");
|
|
521
|
-
allRadios?.forEach((radio) => radio.removeAttribute("data-checked"));
|
|
522
|
-
item.setAttribute("data-checked", "true");
|
|
523
|
-
|
|
524
|
-
// Update internal value
|
|
525
|
-
if (radioGroup) {
|
|
526
|
-
radioGroup.setAttribute("value", value);
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
this.dispatchChangeEvent("radio", name, value);
|
|
530
|
-
this.requestUpdate();
|
|
531
|
-
} else if (item.hasAttribute("data-submenu")) {
|
|
532
|
-
this.openSubmenu(item);
|
|
533
|
-
} else {
|
|
534
|
-
// Regular item: notify (clicks, Enter, and bound shortcuts all land
|
|
535
|
-
// here — without this event none of them were observable) and close.
|
|
536
|
-
this.dispatchSelectEvent(item);
|
|
537
|
-
this.closeAllMenusAndRefocusTrigger();
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
private dispatchSelectEvent(item: HTMLElement) {
|
|
542
|
-
this.dispatchEvent(
|
|
543
|
-
new CustomEvent("rel-menubar-select", {
|
|
544
|
-
detail: {
|
|
545
|
-
label: item.textContent?.trim() ?? "",
|
|
546
|
-
value: item.getAttribute("value") ?? undefined,
|
|
547
|
-
shortcut: item.getAttribute("data-shortcut") ?? undefined,
|
|
548
|
-
},
|
|
549
|
-
bubbles: true,
|
|
550
|
-
composed: true,
|
|
551
|
-
})
|
|
552
|
-
);
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
private openSubmenu(submenuTrigger: HTMLElement, renderedTrigger?: HTMLElement) {
|
|
556
|
-
this.openSubmenuElement = submenuTrigger;
|
|
557
|
-
this.requestUpdate();
|
|
558
|
-
|
|
559
|
-
this.updateComplete.then(() => {
|
|
560
|
-
// Only one submenu is ever open; its rendered trigger row is its
|
|
561
|
-
// previous sibling in the panel template.
|
|
562
|
-
const submenuPanel = this.querySelector(".submenu-panel.open") as HTMLElement;
|
|
563
|
-
if (submenuPanel) {
|
|
564
|
-
const positionTarget =
|
|
565
|
-
renderedTrigger ?? (submenuPanel.previousElementSibling as HTMLElement);
|
|
566
|
-
const rect = positionTarget?.getBoundingClientRect();
|
|
567
|
-
if (rect) {
|
|
568
|
-
submenuPanel.style.top = `${rect.top}px`;
|
|
569
|
-
submenuPanel.style.left = `${rect.right + 4}px`;
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
});
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
private handleSubmenuItemClick(item: HTMLElement) {
|
|
576
|
-
// Regular item - notify, close, and hand focus back to the trigger
|
|
577
|
-
this.dispatchSelectEvent(item);
|
|
578
|
-
this.closeAllMenusAndRefocusTrigger();
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
private dispatchChangeEvent(type: "checkbox" | "radio", labelOrName: string, value: string | boolean) {
|
|
582
|
-
const detail =
|
|
583
|
-
type === "checkbox"
|
|
584
|
-
? { type, label: labelOrName, name: "", checked: value as boolean }
|
|
585
|
-
: { type, label: "", name: labelOrName, value: value as string };
|
|
586
|
-
this.dispatchEvent(
|
|
587
|
-
new CustomEvent("rel-menubar-change", {
|
|
588
|
-
detail,
|
|
589
|
-
bubbles: true,
|
|
590
|
-
composed: true,
|
|
591
|
-
})
|
|
592
|
-
);
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
private renderMenuItem(item: Element, focusIndex: number, menuIndex: number): any {
|
|
596
|
-
// focusIndex is the index within getFocusableItems() — the same list the
|
|
597
|
-
// keyboard handler walks. Comparing raw child indexes against
|
|
598
|
-
// focusedItemIndex highlighted the wrong row once separators existed.
|
|
599
|
-
const isFocused = focusIndex >= 0 && focusIndex === this.focusedItemIndex;
|
|
600
|
-
const itemId =
|
|
601
|
-
focusIndex >= 0 ? `${this.menubarId}-m${menuIndex}-i${focusIndex}` : nothing;
|
|
602
|
-
|
|
603
|
-
if (item.hasAttribute("data-item")) {
|
|
604
|
-
const button = item as HTMLButtonElement;
|
|
605
|
-
const disabled = button.disabled;
|
|
606
|
-
const shortcut = button.getAttribute("data-shortcut") || "";
|
|
607
|
-
|
|
608
|
-
return html`
|
|
609
|
-
<div
|
|
610
|
-
id="${itemId}"
|
|
611
|
-
class="menu-item ${isFocused ? "focused" : ""}"
|
|
612
|
-
role="menuitem"
|
|
613
|
-
data-focus-index="${focusIndex}"
|
|
614
|
-
data-disabled="${disabled}"
|
|
615
|
-
aria-disabled="${disabled ? "true" : nothing}"
|
|
616
|
-
@click="${() => !disabled && this.handleItemClick(item as HTMLElement)}"
|
|
617
|
-
>
|
|
618
|
-
<span>${button.textContent}</span>
|
|
619
|
-
${shortcut ? html`<span class="menu-shortcut">${shortcut}</span>` : ""}
|
|
620
|
-
</div>
|
|
621
|
-
`;
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
if (item.hasAttribute("data-separator")) {
|
|
625
|
-
return html`<div class="menu-separator" role="separator"></div>`;
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
if (item.hasAttribute("data-checkbox")) {
|
|
629
|
-
const label = item as HTMLLabelElement;
|
|
630
|
-
const checkbox = label.querySelector('input[type="checkbox"]') as HTMLInputElement;
|
|
631
|
-
const labelText = Array.from(label.childNodes)
|
|
632
|
-
.filter(n => n.nodeType === Node.TEXT_NODE)
|
|
633
|
-
.map(n => n.textContent?.trim())
|
|
634
|
-
.filter(Boolean)
|
|
635
|
-
.join(' ') || label.getAttribute('label') || '';
|
|
636
|
-
|
|
637
|
-
return html`
|
|
638
|
-
<rel-checkbox
|
|
639
|
-
id="${itemId}"
|
|
640
|
-
data-focus-index="${focusIndex}"
|
|
641
|
-
label=${labelText}
|
|
642
|
-
.status=${checkbox?.checked ? "checked" : "unchecked"}
|
|
643
|
-
@rel-change=${(e: CustomEvent) => {
|
|
644
|
-
if (checkbox) checkbox.checked = e.detail.checked;
|
|
645
|
-
this.dispatchChangeEvent("checkbox", labelText, e.detail.checked);
|
|
646
|
-
}}
|
|
647
|
-
></rel-checkbox>
|
|
648
|
-
`;
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
if (item.hasAttribute("data-submenu")) {
|
|
652
|
-
const label = item.getAttribute("label") || item.textContent?.trim() || "";
|
|
653
|
-
|
|
654
|
-
return html`
|
|
655
|
-
<div
|
|
656
|
-
id="${itemId}"
|
|
657
|
-
class="menu-item menu-submenu-trigger ${isFocused ? "focused" : ""}"
|
|
658
|
-
role="menuitem"
|
|
659
|
-
data-focus-index="${focusIndex}"
|
|
660
|
-
aria-haspopup="menu"
|
|
661
|
-
aria-expanded="${this.openSubmenuElement === item}"
|
|
662
|
-
@click="${(e: Event) => this.openSubmenu(item as HTMLElement, e.currentTarget as HTMLElement)}"
|
|
663
|
-
@mouseenter="${(e: Event) => this.openSubmenu(item as HTMLElement, e.currentTarget as HTMLElement)}"
|
|
664
|
-
>
|
|
665
|
-
<span>${label}</span>
|
|
666
|
-
<rel-icon name="chevron-right"></rel-icon>
|
|
667
|
-
</div>
|
|
668
|
-
${this.renderSubmenu(item as HTMLElement)}
|
|
669
|
-
`;
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
if (item.hasAttribute("data-radio-group")) {
|
|
673
|
-
const radioButtons = Array.from(item.querySelectorAll("[data-radio]"));
|
|
674
|
-
const currentValue = item.getAttribute("value") || "";
|
|
675
|
-
const groupName = item.getAttribute("name") || "";
|
|
676
|
-
|
|
677
|
-
const options = radioButtons.map(radio => ({
|
|
678
|
-
label: radio.textContent?.trim() || "",
|
|
679
|
-
value: radio.getAttribute("data-radio") || radio.getAttribute("value") || "",
|
|
680
|
-
}));
|
|
681
|
-
|
|
682
|
-
return html`
|
|
683
|
-
<rel-radio
|
|
684
|
-
.options=${options}
|
|
685
|
-
.value=${currentValue}
|
|
686
|
-
@rel-change=${(e: CustomEvent) => {
|
|
687
|
-
const newValue = e.detail.value;
|
|
688
|
-
item.setAttribute("value", newValue);
|
|
689
|
-
this.dispatchChangeEvent("radio", groupName, newValue);
|
|
690
|
-
}}
|
|
691
|
-
></rel-radio>
|
|
692
|
-
`;
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
return null;
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
private renderSubmenu(submenuElement: HTMLElement) {
|
|
699
|
-
const isOpen = this.openSubmenuElement === submenuElement;
|
|
700
|
-
const submenuItems = this.getSubmenuItems(submenuElement);
|
|
701
|
-
|
|
702
|
-
return html`
|
|
703
|
-
<div class="submenu-panel ${isOpen ? "open" : ""}" role="menu">
|
|
704
|
-
${submenuItems.map((subItem, subIdx) => {
|
|
705
|
-
const button = subItem as HTMLButtonElement;
|
|
706
|
-
return html`
|
|
707
|
-
<div
|
|
708
|
-
id="${isOpen ? `${this.menubarId}-sub-${subIdx}` : nothing}"
|
|
709
|
-
class="menu-item ${isOpen && subIdx === this.focusedSubItemIndex ? "focused" : ""}"
|
|
710
|
-
role="menuitem"
|
|
711
|
-
@click="${() => this.handleSubmenuItemClick(subItem as HTMLElement)}"
|
|
712
|
-
>
|
|
713
|
-
<span>${button.textContent}</span>
|
|
714
|
-
</div>
|
|
715
|
-
`;
|
|
716
|
-
})}
|
|
717
|
-
</div>
|
|
718
|
-
`;
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
private renderMenuPanel(menuState: MenuState, index: number) {
|
|
722
|
-
const isOpen = index === this.openMenuIndex;
|
|
723
|
-
const items = Array.from(menuState.menu.children);
|
|
724
|
-
const focusables = this.getFocusableItems(menuState.menu);
|
|
725
|
-
|
|
726
|
-
// DOM focus stays on the panel; this tells AT which row virtual focus is
|
|
727
|
-
// on as the user arrows around (panel list or open submenu).
|
|
728
|
-
const activeDescendant = !isOpen
|
|
729
|
-
? nothing
|
|
730
|
-
: this.focusedSubItemIndex >= 0
|
|
731
|
-
? `${this.menubarId}-sub-${this.focusedSubItemIndex}`
|
|
732
|
-
: this.focusedItemIndex >= 0
|
|
733
|
-
? `${this.menubarId}-m${index}-i${this.focusedItemIndex}`
|
|
734
|
-
: nothing;
|
|
735
|
-
|
|
736
|
-
return html`
|
|
737
|
-
<div
|
|
738
|
-
class="menubar-panel ${isOpen ? "open" : ""}"
|
|
739
|
-
data-menu-index="${index}"
|
|
740
|
-
tabindex="-1"
|
|
741
|
-
role="menu"
|
|
742
|
-
aria-activedescendant="${activeDescendant}"
|
|
743
|
-
@keydown="${this.handlePanelKeyDown}"
|
|
744
|
-
>
|
|
745
|
-
${items.map((item) =>
|
|
746
|
-
this.renderMenuItem(item, focusables.indexOf(item), index)
|
|
747
|
-
)}
|
|
748
|
-
</div>
|
|
749
|
-
`;
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
render() {
|
|
753
|
-
return html`
|
|
754
|
-
<style>${RelMenubar._css}</style>
|
|
755
|
-
<div class="menubar-container" role="menubar">
|
|
756
|
-
${this.menus.map(
|
|
757
|
-
(menuState, index) => html`
|
|
758
|
-
<button
|
|
759
|
-
class="menubar-trigger ${index === this.openMenuIndex ? "active" : ""}"
|
|
760
|
-
data-index="${index}"
|
|
761
|
-
role="menuitem"
|
|
762
|
-
aria-haspopup="menu"
|
|
763
|
-
aria-expanded="${index === this.openMenuIndex}"
|
|
764
|
-
@click="${(e: Event) => this.handleTriggerClick(index, e)}"
|
|
765
|
-
@mouseenter="${(e: Event) => this.handleTriggerMouseEnter(index, e)}"
|
|
766
|
-
@focus="${() => { this.focusedTriggerIndex = index; }}"
|
|
767
|
-
@keydown="${this.handleTriggerKeyDown}"
|
|
768
|
-
>
|
|
769
|
-
${menuState.menu.label}
|
|
770
|
-
</button>
|
|
771
|
-
`
|
|
772
|
-
)}
|
|
773
|
-
</div>
|
|
774
|
-
${this.menus.map((menuState, index) => this.renderMenuPanel(menuState, index))}
|
|
775
|
-
`;
|
|
776
|
-
}
|
|
777
|
-
}
|