@rcarls/rc-menubar 0.1.0 → 0.3.0

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.
@@ -0,0 +1,272 @@
1
+ import { css as g, LitElement as b, html as v } from "lit";
2
+ import { property as h, state as m } from "lit/decorators.js";
3
+ import "@rcarls/rc-common";
4
+ const p = g`
5
+ :host {
6
+ display: inline-block;
7
+ }
8
+
9
+ #root {
10
+ display: flex;
11
+ flex-direction: row;
12
+ align-items: stretch;
13
+ gap: var(--rc-menubar-gap, var(--rc-control-gap, 0.25em));
14
+ padding-inline: var(--rc-menubar-padding-inline, var(--rc-control-padding-inline, 0.25em));
15
+ padding-block: var(--rc-menubar-padding-block, var(--rc-control-padding-block, 0.25em));
16
+ border: var(--rc-menubar-border, var(--rc-border, 1px solid ButtonBorder));
17
+ border-radius: var(--rc-menubar-radius, var(--rc-control-radius, 0));
18
+ background: var(--rc-menubar-background, var(--rc-surface, Canvas));
19
+ color: var(--rc-menubar-color, var(--rc-field-text, CanvasText));
20
+ font-family: var(--rc-font-family, inherit);
21
+ font-size: var(--rc-font-size, inherit);
22
+ line-height: var(--rc-line-height, normal);
23
+ }
24
+
25
+ :host([orientation='vertical']) #root {
26
+ flex-direction: column;
27
+ align-items: stretch;
28
+ }
29
+
30
+ #slot-wrap {
31
+ display: flex;
32
+ flex-direction: inherit;
33
+ align-items: stretch;
34
+ gap: inherit;
35
+ min-inline-size: 0;
36
+ }
37
+
38
+ ::slotted(rc-menu-button) {
39
+ flex: 0 0 auto;
40
+ --rc-menu-button-trigger-block-size: var(--rc-menubar-item-block-size, 2.25em);
41
+ --rc-menu-button-trigger-padding-block: var(--rc-menubar-item-padding-block, 0.25em);
42
+ --rc-menu-button-trigger-padding-inline: var(--rc-menubar-item-padding-inline, 0.75em);
43
+ --rc-menu-button-trigger-gap: var(--rc-menubar-item-gap, var(--rc-item-gap, 0.5em));
44
+ --rc-menu-button-trigger-border: var(--rc-menubar-item-border, 1px solid transparent);
45
+ --rc-menu-button-trigger-radius: var(--rc-menubar-item-radius, var(--rc-control-radius, 0.125em));
46
+ --rc-menu-button-trigger-background: var(--rc-menubar-item-background, transparent);
47
+ --rc-menu-button-trigger-color: var(--rc-menubar-item-color, inherit);
48
+ --rc-menu-button-trigger-transition: var(--rc-menubar-item-transition);
49
+ --rc-menu-button-trigger-hover-border-color: var(--rc-menubar-item-hover-border-color, transparent);
50
+ --rc-menu-button-trigger-hover-background: var(
51
+ --rc-menubar-item-hover-background,
52
+ color-mix(in srgb, Highlight 8%, transparent)
53
+ );
54
+ --rc-menu-button-trigger-hover-color: var(--rc-menubar-item-hover-color, inherit);
55
+ --rc-menu-button-trigger-open-border-color: var(--rc-menubar-item-open-border-color, transparent);
56
+ --rc-menu-button-trigger-open-background: var(
57
+ --rc-menubar-item-open-background,
58
+ color-mix(in srgb, Highlight 12%, transparent)
59
+ );
60
+ --rc-menu-button-trigger-open-color: var(--rc-menubar-item-open-color, inherit);
61
+ }
62
+
63
+ ::slotted(rc-menu-button[open]) {
64
+ --rc-menu-button-trigger-background: var(
65
+ --rc-menubar-item-open-background,
66
+ color-mix(in srgb, Highlight 12%, transparent)
67
+ );
68
+ --rc-menu-button-trigger-color: var(--rc-menubar-item-open-color, inherit);
69
+ --rc-menu-button-trigger-hover-background: var(
70
+ --rc-menubar-item-open-background,
71
+ color-mix(in srgb, Highlight 12%, transparent)
72
+ );
73
+ --rc-menu-button-trigger-hover-color: var(--rc-menubar-item-open-color, inherit);
74
+ --rc-menu-button-trigger-hover-border-color: var(--rc-menubar-item-open-border-color, transparent);
75
+ }
76
+
77
+ :host([orientation='vertical']) ::slotted(rc-menu-button) {
78
+ inline-size: 100%;
79
+ }
80
+ `;
81
+ var f = Object.defineProperty, u = (l, t, e, n) => {
82
+ for (var i = void 0, r = l.length - 1, o; r >= 0; r--)
83
+ (o = l[r]) && (i = o(t, e, i) || i);
84
+ return i && f(t, e, i), i;
85
+ };
86
+ const $ = {
87
+ Up: "ArrowUp",
88
+ Down: "ArrowDown",
89
+ Left: "ArrowLeft",
90
+ Right: "ArrowRight"
91
+ }, c = class c extends b {
92
+ constructor() {
93
+ super(...arguments), this.label = "Menu", this.orientation = "horizontal", this._$menuButtons = [], this._boundHandleKeydown = this._handleKeydown.bind(this), this._boundHandleClick = this._handleClick.bind(this);
94
+ }
95
+ /** Syncs host ARIA attributes and registers keyboard and click handlers. */
96
+ connectedCallback() {
97
+ super.connectedCallback(), this._syncHostAria(), this.addEventListener("keydown", this._boundHandleKeydown), this.addEventListener("click", this._boundHandleClick);
98
+ }
99
+ /** Removes keyboard and click handlers. */
100
+ disconnectedCallback() {
101
+ super.disconnectedCallback(), this.removeEventListener("keydown", this._boundHandleKeydown), this.removeEventListener("click", this._boundHandleClick);
102
+ }
103
+ /** Syncs host ARIA and child orientation when `label` or `orientation` change. */
104
+ updated(t) {
105
+ super.updated(t), (t.has("label") || t.has("orientation")) && (this._syncHostAria(), this._syncMenuButtonOrientation());
106
+ }
107
+ /** Writes `role`, `aria-label`, and `aria-orientation` to the host element. */
108
+ _syncHostAria() {
109
+ this.setAttribute("role", "menubar"), this.setAttribute("aria-label", this.label), this.setAttribute("aria-orientation", this.orientation);
110
+ }
111
+ /** Propagates the current `orientation` value to all slotted `rc-menu-button` children. */
112
+ _syncMenuButtonOrientation() {
113
+ this.$menuButtons.forEach((t) => {
114
+ t.orientation = this.orientation;
115
+ });
116
+ }
117
+ /** Returns the `[slot="trigger"]` element inside a child `rc-menu-button`. */
118
+ _$getTrigger(t) {
119
+ return t.querySelector('[slot="trigger"]');
120
+ }
121
+ /** All slotted trigger elements in DOM order. */
122
+ get $items() {
123
+ return this._$menuButtons.map((t) => t.deref()).filter((t) => t != null).map((t) => this._$getTrigger(t)).filter((t) => t != null);
124
+ }
125
+ /** All slotted `rc-menu-button` children in DOM order. */
126
+ get $menuButtons() {
127
+ return this._$menuButtons.map((t) => t.deref()).filter((t) => t != null);
128
+ }
129
+ /** First trigger element in the menubar. */
130
+ get $firstItem() {
131
+ return this.$items.at(0);
132
+ }
133
+ /** Last trigger element in the menubar. */
134
+ get $lastItem() {
135
+ return this.$items.at(-1);
136
+ }
137
+ /** Trigger after `_$lastFocused`, wrapping around to the first. */
138
+ get $nextItem() {
139
+ const t = this._$lastFocused ? this.$items.indexOf(this._$lastFocused) : 0;
140
+ return this.$items.at((t + 1) % this.$items.length);
141
+ }
142
+ /** Trigger before `_$lastFocused`, wrapping around to the last. */
143
+ get $previousItem() {
144
+ const t = this._$lastFocused ? this.$items.indexOf(this._$lastFocused) : 0;
145
+ return this.$items.at((t - 1) % this.$items.length);
146
+ }
147
+ /** Returns the `rc-menu-button` whose slotted trigger matches `$trigger`. */
148
+ _$getMenuButtonForTrigger(t) {
149
+ return this.$menuButtons.find((e) => this._$getTrigger(e) === t);
150
+ }
151
+ /**
152
+ * Moves focus to a trigger element.
153
+ *
154
+ * @param item - trigger to focus; no-ops when `null` or `undefined`
155
+ */
156
+ focusItem(t) {
157
+ t?.focus();
158
+ }
159
+ /** Closes the currently open child menu without returning focus to its trigger. */
160
+ closeActiveMenu() {
161
+ const t = this._$activeMenuButton?.deref();
162
+ t && (t.closeMenu(!1), this._$activeMenuButton = void 0);
163
+ }
164
+ /** Updates roving-tabindex state when a trigger element receives focus. */
165
+ _handleItemFocus(t) {
166
+ const e = t.target;
167
+ e.slot === "trigger" && (this._$lastFocused = e, this.$items.forEach((n) => n.setAttribute("tabindex", "-1")), e.setAttribute("tabindex", "0"));
168
+ }
169
+ /** Refreshes the cached `rc-menu-button` list when slotted children change. */
170
+ _handleSlotChange(t) {
171
+ const n = t.currentTarget.assignedElements(), i = this._$menuButtons;
172
+ this._$menuButtons = n.filter((r) => r.tagName === "RC-MENU-BUTTON").map((r) => new WeakRef(r)), queueMicrotask(() => {
173
+ if (!this.isConnected)
174
+ return;
175
+ i.forEach((o) => {
176
+ const a = o.deref();
177
+ if (!a)
178
+ return;
179
+ a.removeAttribute("role");
180
+ const d = this._$getTrigger(a);
181
+ d?.removeAttribute("tabindex"), d?.removeAttribute("role");
182
+ }), this.$menuButtons.forEach((o) => {
183
+ o.removeAttribute("role"), o.orientation = this.orientation, this._$getTrigger(o)?.setAttribute("role", "menuitem");
184
+ });
185
+ const r = this.$items;
186
+ r.forEach((o, a) => {
187
+ o.setAttribute("tabindex", a === 0 ? "0" : "-1");
188
+ }), r.length > 0 && !this._$lastFocused && (this._$lastFocused = r[0]);
189
+ });
190
+ }
191
+ /** Tracks which child `rc-menu-button` is currently open. */
192
+ _handleMenuButtonToggle(t) {
193
+ const e = t.target;
194
+ t.detail.open ? this._$activeMenuButton = new WeakRef(e) : this._$activeMenuButton?.deref() === e && (this._$activeMenuButton = void 0);
195
+ }
196
+ /**
197
+ * Routes keyboard navigation actions to item focus and menu open/close changes.
198
+ *
199
+ * When a menu is open, moving to an adjacent item closes the current menu and
200
+ * opens the one attached to the newly focused trigger.
201
+ *
202
+ * @param action - the navigation action produced by the keydown handler
203
+ */
204
+ _handleNavigate(t) {
205
+ const n = this._$activeMenuButton?.deref()?.open ?? !1;
206
+ switch (t) {
207
+ case "next":
208
+ n && this.closeActiveMenu(), this.focusItem(this.$nextItem), n && this._openCurrentMenu();
209
+ break;
210
+ case "prev":
211
+ n && this.closeActiveMenu(), this.focusItem(this.$previousItem), n && this._openCurrentMenu();
212
+ break;
213
+ case "start":
214
+ n && this.closeActiveMenu(), this.focusItem(this.$firstItem), n && this._openCurrentMenu();
215
+ break;
216
+ case "end":
217
+ n && this.closeActiveMenu(), this.focusItem(this.$lastItem), n && this._openCurrentMenu();
218
+ break;
219
+ case "escape":
220
+ this.closeActiveMenu();
221
+ break;
222
+ }
223
+ }
224
+ /** Opens the menu attached to the last-focused trigger. */
225
+ _openCurrentMenu() {
226
+ if (this._$lastFocused) {
227
+ const t = this._$getMenuButtonForTrigger(this._$lastFocused);
228
+ t && t.openMenu();
229
+ }
230
+ }
231
+ /** Translates keyboard events to navigation actions and activates keyboard interaction mode. */
232
+ _handleKeydown(t) {
233
+ const e = $[t.key] ?? t.key, n = this.orientation === "horizontal" ? "ArrowRight" : "ArrowDown", i = this.orientation === "horizontal" ? "ArrowLeft" : "ArrowUp";
234
+ let r;
235
+ e === n ? r = "next" : e === i ? r = "prev" : e === "Home" ? r = "start" : e === "End" ? r = "end" : e === "Escape" ? r = "escape" : e === "Tab" && this.setAttribute("data-interaction-mode", "keyboard"), r && (this._handleNavigate(r), this.setAttribute("data-interaction-mode", "keyboard"), t.stopPropagation(), t.preventDefault());
236
+ }
237
+ /** Clears keyboard interaction mode on pointer click. */
238
+ _handleClick() {
239
+ this.removeAttribute("data-interaction-mode");
240
+ }
241
+ render() {
242
+ return v`
243
+ <div id="root" part="root">
244
+ <div
245
+ id="slot-wrap"
246
+ @focusin=${this._handleItemFocus}
247
+ @rc-menu-button-toggle=${this._handleMenuButtonToggle}
248
+ >
249
+ <slot @slotchange=${this._handleSlotChange}></slot>
250
+ </div>
251
+ </div>
252
+ `;
253
+ }
254
+ };
255
+ c.styles = [p];
256
+ let s = c;
257
+ u([
258
+ h({ type: String })
259
+ ], s.prototype, "label");
260
+ u([
261
+ h({ type: String, reflect: !0 })
262
+ ], s.prototype, "orientation");
263
+ u([
264
+ m()
265
+ ], s.prototype, "_$activeMenuButton");
266
+ u([
267
+ m()
268
+ ], s.prototype, "_$lastFocused");
269
+ export {
270
+ s as R
271
+ };
272
+ //# sourceMappingURL=rc-menubar-tmPcN84a.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rc-menubar-tmPcN84a.js","sources":["../src/rc-menubar.styles.ts","../src/rc-menubar.ts"],"sourcesContent":["import { css } from 'lit';\n\nexport const menubarStyles = css`\n :host {\n display: inline-block;\n }\n\n #root {\n display: flex;\n flex-direction: row;\n align-items: stretch;\n gap: var(--rc-menubar-gap, var(--rc-control-gap, 0.25em));\n padding-inline: var(--rc-menubar-padding-inline, var(--rc-control-padding-inline, 0.25em));\n padding-block: var(--rc-menubar-padding-block, var(--rc-control-padding-block, 0.25em));\n border: var(--rc-menubar-border, var(--rc-border, 1px solid ButtonBorder));\n border-radius: var(--rc-menubar-radius, var(--rc-control-radius, 0));\n background: var(--rc-menubar-background, var(--rc-surface, Canvas));\n color: var(--rc-menubar-color, var(--rc-field-text, CanvasText));\n font-family: var(--rc-font-family, inherit);\n font-size: var(--rc-font-size, inherit);\n line-height: var(--rc-line-height, normal);\n }\n\n :host([orientation='vertical']) #root {\n flex-direction: column;\n align-items: stretch;\n }\n\n #slot-wrap {\n display: flex;\n flex-direction: inherit;\n align-items: stretch;\n gap: inherit;\n min-inline-size: 0;\n }\n\n ::slotted(rc-menu-button) {\n flex: 0 0 auto;\n --rc-menu-button-trigger-block-size: var(--rc-menubar-item-block-size, 2.25em);\n --rc-menu-button-trigger-padding-block: var(--rc-menubar-item-padding-block, 0.25em);\n --rc-menu-button-trigger-padding-inline: var(--rc-menubar-item-padding-inline, 0.75em);\n --rc-menu-button-trigger-gap: var(--rc-menubar-item-gap, var(--rc-item-gap, 0.5em));\n --rc-menu-button-trigger-border: var(--rc-menubar-item-border, 1px solid transparent);\n --rc-menu-button-trigger-radius: var(--rc-menubar-item-radius, var(--rc-control-radius, 0.125em));\n --rc-menu-button-trigger-background: var(--rc-menubar-item-background, transparent);\n --rc-menu-button-trigger-color: var(--rc-menubar-item-color, inherit);\n --rc-menu-button-trigger-transition: var(--rc-menubar-item-transition);\n --rc-menu-button-trigger-hover-border-color: var(--rc-menubar-item-hover-border-color, transparent);\n --rc-menu-button-trigger-hover-background: var(\n --rc-menubar-item-hover-background,\n color-mix(in srgb, Highlight 8%, transparent)\n );\n --rc-menu-button-trigger-hover-color: var(--rc-menubar-item-hover-color, inherit);\n --rc-menu-button-trigger-open-border-color: var(--rc-menubar-item-open-border-color, transparent);\n --rc-menu-button-trigger-open-background: var(\n --rc-menubar-item-open-background,\n color-mix(in srgb, Highlight 12%, transparent)\n );\n --rc-menu-button-trigger-open-color: var(--rc-menubar-item-open-color, inherit);\n }\n\n ::slotted(rc-menu-button[open]) {\n --rc-menu-button-trigger-background: var(\n --rc-menubar-item-open-background,\n color-mix(in srgb, Highlight 12%, transparent)\n );\n --rc-menu-button-trigger-color: var(--rc-menubar-item-open-color, inherit);\n --rc-menu-button-trigger-hover-background: var(\n --rc-menubar-item-open-background,\n color-mix(in srgb, Highlight 12%, transparent)\n );\n --rc-menu-button-trigger-hover-color: var(--rc-menubar-item-open-color, inherit);\n --rc-menu-button-trigger-hover-border-color: var(--rc-menubar-item-open-border-color, transparent);\n }\n\n :host([orientation='vertical']) ::slotted(rc-menu-button) {\n inline-size: 100%;\n }\n`;\n\nexport default menubarStyles;\n","import { LitElement, html } from 'lit';\nimport { property, state } from 'lit/decorators.js';\n\nimport { type KeyboardNavigationAction } from '@rcarls/rc-common';\n\nimport type { RCMenuButton, RCMenuButtonToggleEvent } from '@rcarls/rc-menu-button';\n\nimport menubarStyles from './rc-menubar.styles';\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'rc-menubar': RCMenubar;\n }\n}\n\nconst IE_KEY_ALIASES: Record<string, string> = {\n Up: 'ArrowUp',\n Down: 'ArrowDown',\n Left: 'ArrowLeft',\n Right: 'ArrowRight',\n};\n\n/**\n * Menubar coordinator for rc-menu-button children with roving tabindex and submenu\n * handoff, following the WAI-ARIA Menubar pattern.\n *\n * Uses roving tabindex rather than aria-activedescendant so trigger buttons\n * receive real focus — required for submenu handoff and screen reader announcements.\n *\n *\n * @see {@link https://richardcarls.github.io/rc-webcomponents/components/rc-menubar rc-menubar docs}\n * @see {@link https://www.w3.org/WAI/ARIA/apg/patterns/menubar/ WAI-ARIA Menubar pattern}\n *\n * @slot default - Takes rc-menu-button elements to display in the menubar\n *\n * @csspart root - The root container element\n *\n * @cssprop [--rc-menubar-gap=var(--rc-control-gap)] - Gap between menu buttons\n * @cssprop [--rc-menubar-padding-inline=var(--rc-control-padding-inline)] - Inline-axis padding\n * @cssprop [--rc-menubar-padding-block=var(--rc-control-padding-block)] - Block-axis padding\n * @cssprop [--rc-menubar-border=var(--rc-border)] - Root container border\n * @cssprop [--rc-menubar-radius=var(--rc-control-radius)] - Root container border radius\n * @cssprop [--rc-menubar-background=Canvas] - Root container background\n * @cssprop [--rc-menubar-color=CanvasText] - Root container text color\n * @cssprop [--rc-menubar-item-block-size=2.25em] - Minimum block size for child menu-button triggers\n * @cssprop [--rc-menubar-item-padding-block=0.25em] - Child trigger block-axis padding\n * @cssprop [--rc-menubar-item-padding-inline=0.75em] - Child trigger inline-axis padding\n * @cssprop [--rc-menubar-item-gap=var(--rc-item-gap)] - Gap between flex children inside child triggers\n * @cssprop [--rc-menubar-item-border=1px solid transparent] - Child trigger border\n * @cssprop [--rc-menubar-item-radius=var(--rc-control-radius)] - Child trigger border radius\n * @cssprop [--rc-menubar-item-background=transparent] - Child trigger background\n * @cssprop [--rc-menubar-item-color=inherit] - Child trigger text color\n * @cssprop [--rc-menubar-item-transition] - CSS transition applied to child triggers\n * @cssprop [--rc-menubar-item-hover-background=color-mix(in srgb, Highlight 8%, transparent)] - Child trigger hover background\n * @cssprop [--rc-menubar-item-hover-color=inherit] - Child trigger hover text color\n * @cssprop [--rc-menubar-item-hover-border-color=transparent] - Child trigger hover border color\n * @cssprop [--rc-menubar-item-open-background=color-mix(in srgb, Highlight 12%, transparent)] - Child trigger background when its menu is open\n * @cssprop [--rc-menubar-item-open-color=inherit] - Child trigger text color when its menu is open\n * @cssprop [--rc-menubar-item-open-border-color=transparent] - Child trigger border color when its menu is open\n */\nexport class RCMenubar extends LitElement {\n static styles = [menubarStyles];\n\n /** Accessible label for the `role=\"menubar\"` element. */\n @property({ type: String })\n label = 'Menu';\n\n /** Orientation of the menubar, controls which arrow keys move between items. */\n @property({ type: String, reflect: true })\n orientation: 'horizontal' | 'vertical' = 'horizontal';\n\n /** Weak reference to the currently open child `rc-menu-button`. */\n @state()\n protected _$activeMenuButton: WeakRef<RCMenuButton> | undefined;\n\n /** The last trigger element to have received focus, used for roving-tabindex bookkeeping. */\n @state()\n protected _$lastFocused: HTMLElement | undefined;\n\n /** Weak references to all slotted `rc-menu-button` children, in DOM order. */\n protected _$menuButtons: WeakRef<RCMenuButton>[] = [];\n\n /** Bound keydown handler, held for `removeEventListener` pairing. */\n protected _boundHandleKeydown = this._handleKeydown.bind(this);\n\n /** Bound click handler, held for `removeEventListener` pairing. */\n protected _boundHandleClick = this._handleClick.bind(this);\n\n /** Syncs host ARIA attributes and registers keyboard and click handlers. */\n override connectedCallback() {\n super.connectedCallback();\n\n this._syncHostAria();\n\n this.addEventListener('keydown', this._boundHandleKeydown);\n this.addEventListener('click', this._boundHandleClick);\n }\n\n /** Removes keyboard and click handlers. */\n override disconnectedCallback() {\n super.disconnectedCallback();\n\n this.removeEventListener('keydown', this._boundHandleKeydown);\n this.removeEventListener('click', this._boundHandleClick);\n }\n\n /** Syncs host ARIA and child orientation when `label` or `orientation` change. */\n protected override updated(changedProperties: Map<PropertyKey, unknown>) {\n super.updated(changedProperties);\n\n if (changedProperties.has('label') || changedProperties.has('orientation')) {\n this._syncHostAria();\n this._syncMenuButtonOrientation();\n }\n }\n\n /** Writes `role`, `aria-label`, and `aria-orientation` to the host element. */\n protected _syncHostAria(): void {\n this.setAttribute('role', 'menubar');\n this.setAttribute('aria-label', this.label);\n this.setAttribute('aria-orientation', this.orientation);\n }\n\n /** Propagates the current `orientation` value to all slotted `rc-menu-button` children. */\n protected _syncMenuButtonOrientation(): void {\n this.$menuButtons.forEach(($menuButton) => {\n $menuButton.orientation = this.orientation;\n });\n }\n\n /** Returns the `[slot=\"trigger\"]` element inside a child `rc-menu-button`. */\n protected _$getTrigger($menuButton: RCMenuButton): HTMLElement | null {\n return $menuButton.querySelector('[slot=\"trigger\"]');\n }\n\n /** All slotted trigger elements in DOM order. */\n get $items(): HTMLElement[] {\n return this._$menuButtons\n .map((ref) => ref.deref())\n .filter((el): el is RCMenuButton => el != null)\n .map(($mb) => this._$getTrigger($mb))\n .filter(($el): $el is HTMLElement => $el != null);\n }\n\n /** All slotted `rc-menu-button` children in DOM order. */\n get $menuButtons(): RCMenuButton[] {\n return this._$menuButtons\n .map((ref) => ref.deref())\n .filter((el): el is RCMenuButton => el != null);\n }\n\n /** First trigger element in the menubar. */\n get $firstItem(): HTMLElement | undefined {\n return this.$items.at(0);\n }\n\n /** Last trigger element in the menubar. */\n get $lastItem(): HTMLElement | undefined {\n return this.$items.at(-1);\n }\n\n /** Trigger after `_$lastFocused`, wrapping around to the first. */\n get $nextItem(): HTMLElement | undefined {\n const index = this._$lastFocused ? this.$items.indexOf(this._$lastFocused) : 0;\n\n return this.$items.at((index + 1) % this.$items.length);\n }\n\n /** Trigger before `_$lastFocused`, wrapping around to the last. */\n get $previousItem(): HTMLElement | undefined {\n const index = this._$lastFocused ? this.$items.indexOf(this._$lastFocused) : 0;\n\n return this.$items.at((index - 1) % this.$items.length);\n }\n\n /** Returns the `rc-menu-button` whose slotted trigger matches `$trigger`. */\n protected _$getMenuButtonForTrigger($trigger: HTMLElement): RCMenuButton | undefined {\n return this.$menuButtons.find(($mb) => this._$getTrigger($mb) === $trigger);\n }\n\n /**\n * Moves focus to a trigger element.\n *\n * @param item - trigger to focus; no-ops when `null` or `undefined`\n */\n focusItem(item?: HTMLElement | null) {\n if (item != null) {\n item.focus();\n }\n }\n\n /** Closes the currently open child menu without returning focus to its trigger. */\n closeActiveMenu() {\n const $activeMenuButton = this._$activeMenuButton?.deref();\n\n if ($activeMenuButton) {\n $activeMenuButton.closeMenu(false);\n\n this._$activeMenuButton = undefined;\n }\n }\n\n /** Updates roving-tabindex state when a trigger element receives focus. */\n protected _handleItemFocus(e: FocusEvent) {\n const $target = e.target as HTMLElement;\n\n if ($target.slot !== 'trigger') return;\n\n this._$lastFocused = $target;\n\n this.$items.forEach(($item) => $item.setAttribute('tabindex', '-1'));\n\n $target.setAttribute('tabindex', '0');\n }\n\n /** Refreshes the cached `rc-menu-button` list when slotted children change. */\n protected _handleSlotChange(e: Event) {\n const $slot = e.currentTarget as HTMLSlotElement;\n const $elements = $slot.assignedElements();\n\n const prevButtons = this._$menuButtons;\n\n // Cache element references synchronously. Defer all DOM mutations.\n // slotchange fires synchronously inside a framework reactive update pass\n // on second+ mount (shadow DOM already exists), so the handler must be\n // instantaneous to avoid interacting with the reactive system.\n this._$menuButtons = $elements\n .filter((el): el is RCMenuButton => el.tagName === 'RC-MENU-BUTTON')\n .map((el) => new WeakRef(el));\n\n queueMicrotask(() => {\n if (!this.isConnected) {\n return;\n }\n\n prevButtons.forEach((ref) => {\n const $menuButton = ref.deref();\n if (!$menuButton) {\n return;\n }\n\n $menuButton.removeAttribute('role');\n\n const $trigger = this._$getTrigger($menuButton);\n\n $trigger?.removeAttribute('tabindex');\n $trigger?.removeAttribute('role');\n });\n\n this.$menuButtons.forEach(($menuButton) => {\n $menuButton.removeAttribute('role');\n $menuButton.orientation = this.orientation;\n\n const $trigger = this._$getTrigger($menuButton);\n\n $trigger?.setAttribute('role', 'menuitem');\n });\n\n const $items = this.$items;\n\n $items.forEach(($trigger, index) => {\n $trigger.setAttribute('tabindex', index === 0 ? '0' : '-1');\n });\n\n if ($items.length > 0 && !this._$lastFocused) {\n this._$lastFocused = $items[0];\n }\n });\n }\n\n /** Tracks which child `rc-menu-button` is currently open. */\n protected _handleMenuButtonToggle(e: CustomEvent<RCMenuButtonToggleEvent>) {\n const $menuButton = e.target as RCMenuButton;\n\n if (e.detail.open) {\n this._$activeMenuButton = new WeakRef($menuButton);\n } else {\n if (this._$activeMenuButton?.deref() === $menuButton) {\n this._$activeMenuButton = undefined;\n }\n }\n }\n\n /**\n * Routes keyboard navigation actions to item focus and menu open/close changes.\n *\n * When a menu is open, moving to an adjacent item closes the current menu and\n * opens the one attached to the newly focused trigger.\n *\n * @param action - the navigation action produced by the keydown handler\n */\n protected _handleNavigate(action: KeyboardNavigationAction) {\n const $activeMenu = this._$activeMenuButton?.deref();\n const wasMenuOpen = $activeMenu?.open ?? false;\n\n switch (action) {\n case 'next':\n if (wasMenuOpen) {\n this.closeActiveMenu();\n }\n\n this.focusItem(this.$nextItem);\n\n if (wasMenuOpen) {\n this._openCurrentMenu();\n }\n\n break;\n\n case 'prev':\n if (wasMenuOpen) {\n this.closeActiveMenu();\n }\n\n this.focusItem(this.$previousItem);\n\n if (wasMenuOpen) {\n this._openCurrentMenu();\n }\n\n break;\n\n case 'start':\n if (wasMenuOpen) {\n this.closeActiveMenu();\n }\n\n this.focusItem(this.$firstItem);\n\n if (wasMenuOpen) {\n this._openCurrentMenu();\n }\n\n break;\n\n case 'end':\n if (wasMenuOpen) {\n this.closeActiveMenu();\n }\n\n this.focusItem(this.$lastItem);\n\n if (wasMenuOpen) {\n this._openCurrentMenu();\n }\n\n break;\n\n case 'escape':\n this.closeActiveMenu();\n\n break;\n }\n }\n\n /** Opens the menu attached to the last-focused trigger. */\n protected _openCurrentMenu() {\n if (this._$lastFocused) {\n const $menuButton = this._$getMenuButtonForTrigger(this._$lastFocused);\n\n if ($menuButton) {\n $menuButton.openMenu();\n }\n }\n }\n\n /** Translates keyboard events to navigation actions and activates keyboard interaction mode. */\n protected _handleKeydown(e: KeyboardEvent): void {\n const key = IE_KEY_ALIASES[e.key] ?? e.key;\n const navNext = this.orientation === 'horizontal' ? 'ArrowRight' : 'ArrowDown';\n const navPrev = this.orientation === 'horizontal' ? 'ArrowLeft' : 'ArrowUp';\n\n let action: KeyboardNavigationAction | undefined;\n\n if (key === navNext) {\n action = 'next';\n } else if (key === navPrev) {\n action = 'prev';\n } else if (key === 'Home') {\n action = 'start';\n } else if (key === 'End') {\n action = 'end';\n } else if (key === 'Escape') {\n action = 'escape';\n } else if (key === 'Tab') {\n this.setAttribute('data-interaction-mode', 'keyboard');\n }\n\n if (!action) {\n return;\n }\n\n this._handleNavigate(action);\n this.setAttribute('data-interaction-mode', 'keyboard');\n\n e.stopPropagation();\n e.preventDefault();\n }\n\n /** Clears keyboard interaction mode on pointer click. */\n protected _handleClick(): void {\n this.removeAttribute('data-interaction-mode');\n }\n\n protected override render() {\n return html`\n <div id=\"root\" part=\"root\">\n <div\n id=\"slot-wrap\"\n @focusin=${this._handleItemFocus}\n @rc-menu-button-toggle=${this._handleMenuButtonToggle}\n >\n <slot @slotchange=${this._handleSlotChange}></slot>\n </div>\n </div>\n `;\n }\n}\n\nexport default RCMenubar;\n"],"names":["menubarStyles","css","IE_KEY_ALIASES","_RCMenubar","LitElement","changedProperties","$menuButton","ref","el","$mb","$el","index","$trigger","item","$activeMenuButton","e","$target","$item","$elements","prevButtons","$items","action","wasMenuOpen","key","navNext","navPrev","html","RCMenubar","__decorateClass","property","state"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { R as e } from "./rc-menubar-BWcf6GHO.js";
1
+ import { R as e } from "./rc-menubar-tmPcN84a.js";
2
2
  export {
3
3
  e as RCMenubar
4
4
  };
@@ -1,16 +1,26 @@
1
1
  import { LitElement } from 'lit';
2
- import { RCMenuButton } from '@rcarls/rc-menu-button';
2
+ import { KeyboardNavigationAction } from '@rcarls/rc-common';
3
+ import { RCMenuButton, RCMenuButtonToggleEvent } from '@rcarls/rc-menu-button';
3
4
  declare global {
4
5
  interface HTMLElementTagNameMap {
5
6
  'rc-menubar': RCMenubar;
6
7
  }
7
8
  }
8
9
  /**
9
- * A menubar component containing menu buttons, as defined in WAI-ARIA
10
+ * Menubar coordinator for rc-menu-button children with roving tabindex and submenu
11
+ * handoff, following the WAI-ARIA Menubar pattern.
12
+ *
13
+ * Uses roving tabindex rather than aria-activedescendant so trigger buttons
14
+ * receive real focus — required for submenu handoff and screen reader announcements.
15
+ *
16
+ *
17
+ * @see {@link https://richardcarls.github.io/rc-webcomponents/components/rc-menubar rc-menubar docs}
18
+ * @see {@link https://www.w3.org/WAI/ARIA/apg/patterns/menubar/ WAI-ARIA Menubar pattern}
10
19
  *
11
- * @see https://www.w3.org/WAI/ARIA/apg/patterns/menubar/
12
20
  * @slot default - Takes rc-menu-button elements to display in the menubar
21
+ *
13
22
  * @csspart root - The root container element
23
+ *
14
24
  * @cssprop [--rc-menubar-gap=var(--rc-control-gap)] - Gap between menu buttons
15
25
  * @cssprop [--rc-menubar-padding-inline=var(--rc-control-padding-inline)] - Inline-axis padding
16
26
  * @cssprop [--rc-menubar-padding-block=var(--rc-control-padding-block)] - Block-axis padding
@@ -18,55 +28,93 @@ declare global {
18
28
  * @cssprop [--rc-menubar-radius=var(--rc-control-radius)] - Root container border radius
19
29
  * @cssprop [--rc-menubar-background=Canvas] - Root container background
20
30
  * @cssprop [--rc-menubar-color=CanvasText] - Root container text color
31
+ * @cssprop [--rc-menubar-item-block-size=2.25em] - Minimum block size for child menu-button triggers
32
+ * @cssprop [--rc-menubar-item-padding-block=0.25em] - Child trigger block-axis padding
33
+ * @cssprop [--rc-menubar-item-padding-inline=0.75em] - Child trigger inline-axis padding
34
+ * @cssprop [--rc-menubar-item-gap=var(--rc-item-gap)] - Gap between flex children inside child triggers
35
+ * @cssprop [--rc-menubar-item-border=1px solid transparent] - Child trigger border
36
+ * @cssprop [--rc-menubar-item-radius=var(--rc-control-radius)] - Child trigger border radius
37
+ * @cssprop [--rc-menubar-item-background=transparent] - Child trigger background
38
+ * @cssprop [--rc-menubar-item-color=inherit] - Child trigger text color
39
+ * @cssprop [--rc-menubar-item-transition] - CSS transition applied to child triggers
40
+ * @cssprop [--rc-menubar-item-hover-background=color-mix(in srgb, Highlight 8%, transparent)] - Child trigger hover background
41
+ * @cssprop [--rc-menubar-item-hover-color=inherit] - Child trigger hover text color
42
+ * @cssprop [--rc-menubar-item-hover-border-color=transparent] - Child trigger hover border color
43
+ * @cssprop [--rc-menubar-item-open-background=color-mix(in srgb, Highlight 12%, transparent)] - Child trigger background when its menu is open
44
+ * @cssprop [--rc-menubar-item-open-color=inherit] - Child trigger text color when its menu is open
45
+ * @cssprop [--rc-menubar-item-open-border-color=transparent] - Child trigger border color when its menu is open
21
46
  */
22
47
  export declare class RCMenubar extends LitElement {
23
48
  static styles: import('lit').CSSResult[];
24
- /** Accessible label for this menubar */
49
+ /** Accessible label for the `role="menubar"` element. */
25
50
  label: string;
26
- /** Menubar orientation, for keyboard navigation */
51
+ /** Orientation of the menubar, controls which arrow keys move between items. */
27
52
  orientation: 'horizontal' | 'vertical';
28
- /** Reference to the currently open menu button */
29
- private _activeMenuButton;
30
- /** The last menu button trigger to have focus */
31
- private _lastFocused;
32
- /** Array of menu button elements */
33
- private _menuButtons;
34
- private _boundHandleKeydown;
35
- private _boundHandleClick;
53
+ /** Weak reference to the currently open child `rc-menu-button`. */
54
+ protected _$activeMenuButton: WeakRef<RCMenuButton> | undefined;
55
+ /** The last trigger element to have received focus, used for roving-tabindex bookkeeping. */
56
+ protected _$lastFocused: HTMLElement | undefined;
57
+ /** Weak references to all slotted `rc-menu-button` children, in DOM order. */
58
+ protected _$menuButtons: WeakRef<RCMenuButton>[];
59
+ /** Bound keydown handler, held for `removeEventListener` pairing. */
60
+ protected _boundHandleKeydown: (e: KeyboardEvent) => void;
61
+ /** Bound click handler, held for `removeEventListener` pairing. */
62
+ protected _boundHandleClick: () => void;
63
+ /** Syncs host ARIA attributes and registers keyboard and click handlers. */
36
64
  connectedCallback(): void;
65
+ /** Removes keyboard and click handlers. */
37
66
  disconnectedCallback(): void;
67
+ /** Syncs host ARIA and child orientation when `label` or `orientation` change. */
38
68
  protected updated(changedProperties: Map<PropertyKey, unknown>): void;
39
- /** Keep ARIA semantics on the custom element host for slotted children. */
40
- private _syncHostAria;
41
- private _syncMenuButtonOrientation;
42
- /** Get the trigger element from a menu button */
43
- private _getTrigger;
44
- /** Array of focusable trigger elements */
45
- get items(): HTMLElement[];
46
- /** Array of menu button elements */
47
- get menuButtons(): RCMenuButton[];
48
- /** First trigger element */
49
- get firstItem(): HTMLElement | undefined;
50
- /** Last trigger element */
51
- get lastItem(): HTMLElement | undefined;
52
- /** Next trigger element, in tab-order */
53
- get nextItem(): HTMLElement | undefined;
54
- /** Previous trigger element, in tab-order */
55
- get previousItem(): HTMLElement | undefined;
56
- /** Get the menu button that contains a trigger */
57
- private _getMenuButtonForTrigger;
58
- /** Set focus to a specific trigger element */
69
+ /** Writes `role`, `aria-label`, and `aria-orientation` to the host element. */
70
+ protected _syncHostAria(): void;
71
+ /** Propagates the current `orientation` value to all slotted `rc-menu-button` children. */
72
+ protected _syncMenuButtonOrientation(): void;
73
+ /** Returns the `[slot="trigger"]` element inside a child `rc-menu-button`. */
74
+ protected _$getTrigger($menuButton: RCMenuButton): HTMLElement | null;
75
+ /** All slotted trigger elements in DOM order. */
76
+ get $items(): HTMLElement[];
77
+ /** All slotted `rc-menu-button` children in DOM order. */
78
+ get $menuButtons(): RCMenuButton[];
79
+ /** First trigger element in the menubar. */
80
+ get $firstItem(): HTMLElement | undefined;
81
+ /** Last trigger element in the menubar. */
82
+ get $lastItem(): HTMLElement | undefined;
83
+ /** Trigger after `_$lastFocused`, wrapping around to the first. */
84
+ get $nextItem(): HTMLElement | undefined;
85
+ /** Trigger before `_$lastFocused`, wrapping around to the last. */
86
+ get $previousItem(): HTMLElement | undefined;
87
+ /** Returns the `rc-menu-button` whose slotted trigger matches `$trigger`. */
88
+ protected _$getMenuButtonForTrigger($trigger: HTMLElement): RCMenuButton | undefined;
89
+ /**
90
+ * Moves focus to a trigger element.
91
+ *
92
+ * @param item - trigger to focus; no-ops when `null` or `undefined`
93
+ */
59
94
  focusItem(item?: HTMLElement | null): void;
60
- /** Close any open menu */
95
+ /** Closes the currently open child menu without returning focus to its trigger. */
61
96
  closeActiveMenu(): void;
62
- private _handleItemFocus;
63
- private _handleSlotChange;
64
- private _handleMenuButtonToggle;
65
- private _handleNavigate;
66
- private _openCurrentMenu;
67
- private _handleKeydown;
68
- private _handleClick;
69
- private _normalizeKey;
97
+ /** Updates roving-tabindex state when a trigger element receives focus. */
98
+ protected _handleItemFocus(e: FocusEvent): void;
99
+ /** Refreshes the cached `rc-menu-button` list when slotted children change. */
100
+ protected _handleSlotChange(e: Event): void;
101
+ /** Tracks which child `rc-menu-button` is currently open. */
102
+ protected _handleMenuButtonToggle(e: CustomEvent<RCMenuButtonToggleEvent>): void;
103
+ /**
104
+ * Routes keyboard navigation actions to item focus and menu open/close changes.
105
+ *
106
+ * When a menu is open, moving to an adjacent item closes the current menu and
107
+ * opens the one attached to the newly focused trigger.
108
+ *
109
+ * @param action - the navigation action produced by the keydown handler
110
+ */
111
+ protected _handleNavigate(action: KeyboardNavigationAction): void;
112
+ /** Opens the menu attached to the last-focused trigger. */
113
+ protected _openCurrentMenu(): void;
114
+ /** Translates keyboard events to navigation actions and activates keyboard interaction mode. */
115
+ protected _handleKeydown(e: KeyboardEvent): void;
116
+ /** Clears keyboard interaction mode on pointer click. */
117
+ protected _handleClick(): void;
70
118
  protected render(): import('lit').TemplateResult<1>;
71
119
  }
72
120
  export default RCMenubar;
package/package.json CHANGED
@@ -3,14 +3,14 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.1.0",
7
- "description": "Headless WAI-ARIA menubar with roving tabindex, built with Lit",
6
+ "version": "0.3.0",
7
+ "description": "Menubar coordinator for rc-menu-button children with roving tabindex and submenu handoff.",
8
8
  "repository": {
9
9
  "type": "git",
10
10
  "url": "git+https://github.com/richardcarls/rc-webcomponents.git",
11
11
  "directory": "packages/rc-menubar"
12
12
  },
13
- "homepage": "https://github.com/richardcarls/rc-webcomponents#readme",
13
+ "homepage": "https://richardcarls.github.io/rc-webcomponents/components/rc-menubar",
14
14
  "license": "MIT",
15
15
  "type": "module",
16
16
  "files": [
@@ -37,24 +37,21 @@
37
37
  ],
38
38
  "customElements": "dist/custom-elements.json",
39
39
  "scripts": {
40
- "dev": "vite",
41
40
  "build": "tsc && vite build && cem analyze",
42
41
  "cem:analyze": "cem analyze",
43
42
  "preview": "vite preview",
44
- "test:browser": "vitest",
45
- "test:browser:chrome": "vitest --project=chromium",
46
- "test:browser:firefox": "vitest --project=firefox",
47
- "yalc:publish": "yalc publish --push"
43
+ "test:browser": "vitest --run",
44
+ "test:browser:chrome": "vitest --run --project=chromium",
45
+ "test:browser:firefox": "vitest --run --project=firefox"
48
46
  },
49
47
  "dependencies": {
50
- "@rcarls/rc-common": "workspace:^",
51
- "@rcarls/rc-menu": "workspace:^",
52
- "@rcarls/rc-menu-button": "workspace:^",
48
+ "@rcarls/rc-common": "workspace:*",
49
+ "@rcarls/rc-menu": "workspace:*",
50
+ "@rcarls/rc-menu-button": "workspace:*",
53
51
  "@types/node": "^25.6.0"
54
52
  },
55
53
  "devDependencies": {
56
54
  "@custom-elements-manifest/analyzer": "0.11.0",
57
- "@guanghechen/rollup-plugin-copy": "^6.0.9",
58
55
  "@vitest/browser-playwright": "4.1.5",
59
56
  "lit": "^3.0.0",
60
57
  "playwright": "^1.56.0",
package/dist/demo.css DELETED
@@ -1,85 +0,0 @@
1
- *, *::before, *::after {
2
- box-sizing: border-box;
3
- }
4
-
5
- :root {
6
- color-scheme: light dark;
7
- --rc-accent: Highlight; /* Chrome doesn't support AccentColor on the open web */
8
- }
9
-
10
- @supports (color: AccentColor) {
11
- :root {
12
- --rc-accent: AccentColor;
13
- }
14
- }
15
-
16
- [data-theme="light"] { color-scheme: light; }
17
- [data-theme="dark"] { color-scheme: dark; }
18
-
19
- [data-theme="solarized-light"] {
20
- color-scheme: light;
21
- --rc-surface: #fdf6e3;
22
- --rc-text: #657b83;
23
- --rc-border: 1px solid #93a1a1;
24
- --rc-shadow: 0 2px 8px rgba(0, 0, 0, .12);
25
- }
26
-
27
- [data-theme="solarized-dark"] {
28
- color-scheme: dark;
29
- --rc-surface: #002b36;
30
- --rc-text: #839496;
31
- --rc-border: 1px solid #586e75;
32
- --rc-shadow: 0 2px 8px rgba(0, 0, 0, .3);
33
- }
34
-
35
- body {
36
- font-family: system-ui, sans-serif;
37
- margin: 0;
38
- }
39
-
40
- .demo-page {
41
- max-width: 60rem;
42
- margin: 0 auto;
43
- padding: 2rem 1.5rem;
44
- }
45
-
46
- .demo-controls {
47
- display: flex;
48
- align-items: center;
49
- gap: 1rem;
50
- margin-bottom: 2rem;
51
- padding-bottom: 1rem;
52
- border-bottom: 1px solid ButtonBorder;
53
- }
54
-
55
- .demo-controls .demo-title {
56
- flex: 1;
57
- }
58
-
59
- .demo-controls h1 {
60
- margin: 0 0 0.15rem;
61
- font-size: 1.5rem;
62
- }
63
-
64
- .demo-controls a {
65
- font-size: 0.8rem;
66
- }
67
-
68
- .demo-section {
69
- margin-bottom: 2rem;
70
- }
71
-
72
- .demo-section h2 {
73
- margin: 0 0 0.5rem;
74
- }
75
-
76
- .theme-picker {
77
- padding: 0.35em 0.75em;
78
- font-family: inherit;
79
- font-size: 0.8rem;
80
- cursor: pointer;
81
- border: 1px solid ButtonBorder;
82
- border-radius: 4px;
83
- background: Canvas;
84
- color: CanvasText;
85
- }