@rcarls/rc-menu 0.2.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,323 @@
1
+ import { css as u, LitElement as m, html as d } from "lit";
2
+ import { property as h } from "lit/decorators.js";
3
+ import { ActiveDescendantController as b, isFocusable as s } from "@rcarls/rc-common";
4
+ const v = u`
5
+ :host {
6
+ display: flex;
7
+ flex-direction: column;
8
+ min-width: var(--rc-menu-min-width, 10em);
9
+ padding-block: var(--rc-menu-padding-block, var(--rc-control-padding-block, 0.25em));
10
+ background: var(--rc-menu-background, var(--rc-surface, Canvas));
11
+ border: var(--rc-menu-border, var(--rc-border, 1px solid ButtonBorder));
12
+ border-radius: var(--rc-menu-radius, var(--rc-control-radius, 0));
13
+ box-shadow: var(
14
+ --rc-menu-shadow,
15
+ var(--rc-shadow, 0 2px 8px color-mix(in srgb, CanvasText 15%, transparent))
16
+ );
17
+ color: var(--rc-menu-color, var(--rc-field-text, CanvasText));
18
+ font-family: var(--rc-font-family, inherit);
19
+ font-size: var(--rc-font-size, inherit);
20
+ line-height: var(--rc-line-height, normal);
21
+ overflow: hidden;
22
+ }
23
+
24
+ :host(:focus-visible) {
25
+ outline: var(--rc-focus-ring, auto);
26
+ outline-offset: var(--rc-focus-ring-offset, 0);
27
+ }
28
+ `;
29
+ var p = Object.defineProperty, g = (o, e, t, r) => {
30
+ for (var n = void 0, i = o.length - 1, c; i >= 0; i--)
31
+ (c = o[i]) && (n = c(e, t, n) || n);
32
+ return n && p(e, t, n), n;
33
+ };
34
+ const f = ["menuitem", "menuitemcheckbox", "menuitemradio"], k = `
35
+ @layer rc-base {
36
+ /* Item base layout */
37
+ rc-menu [role='menuitem'],
38
+ rc-menu [role='menuitemcheckbox'],
39
+ rc-menu [role='menuitemradio'],
40
+ rc-menu button:disabled {
41
+ display: flex;
42
+ align-items: center;
43
+ gap: var(--rc-menu-item-gap, var(--rc-item-gap, 0.5em));
44
+ min-block-size: var(--rc-menu-item-min-block-size, 0px);
45
+ width: 100%;
46
+ text-align: start;
47
+ padding: var(--rc-menu-item-padding-block, var(--rc-item-padding-block, 0.3em))
48
+ var(--rc-menu-item-padding-inline, var(--rc-item-padding-inline, 0.75em));
49
+ appearance: none;
50
+ box-sizing: border-box;
51
+ border: none;
52
+ background: transparent;
53
+ text-decoration: none;
54
+ color: inherit;
55
+ font: inherit;
56
+ cursor: default;
57
+ user-select: none;
58
+ outline: none;
59
+ transition: var(--rc-menu-item-transition);
60
+ }
61
+
62
+ /* Pointer hover */
63
+ rc-menu [role='menuitem']:hover,
64
+ rc-menu [role='menuitemcheckbox']:hover,
65
+ rc-menu [role='menuitemradio']:hover {
66
+ background: var(--rc-menu-hover-bg, color-mix(in srgb, Highlight 8%, transparent));
67
+ color: var(--rc-menu-hover-color, inherit);
68
+ }
69
+
70
+ /* Virtual cursor (set by ActiveDescendantController) */
71
+ rc-menu [role='menuitem'][data-active],
72
+ rc-menu [role='menuitemcheckbox'][data-active],
73
+ rc-menu [role='menuitemradio'][data-active] {
74
+ background: var(--rc-menu-active-bg, color-mix(in srgb, Highlight 8%, transparent));
75
+ color: var(--rc-menu-active-color, inherit);
76
+ }
77
+
78
+ /* Check/radio indicator — ::before acts as a fixed-width leading column.
79
+ Content is empty (unchecked) or a Unicode symbol (checked/mixed).
80
+ Semantic state is conveyed by aria-checked; the visual symbol is decorative. */
81
+ rc-menu [role='menuitemcheckbox']::before,
82
+ rc-menu [role='menuitemradio']::before {
83
+ content: '';
84
+ flex: 0 0 var(--rc-menu-check-size, 1.25em);
85
+ text-align: center;
86
+ line-height: 1;
87
+ color: var(--rc-menu-check-color, inherit);
88
+ }
89
+
90
+ rc-menu [role='menuitemcheckbox'][aria-checked='true']::before {
91
+ content: '✓';
92
+ }
93
+
94
+ rc-menu [role='menuitemcheckbox'][aria-checked='mixed']::before {
95
+ content: '−';
96
+ }
97
+
98
+ rc-menu [role='menuitemradio'][aria-checked='true']::before {
99
+ content: '•';
100
+ }
101
+
102
+ /* Trailing label — keyboard shortcut or hint, pushed to the inline-end of the item */
103
+ rc-menu [data-menu-shortcut] {
104
+ margin-inline-start: auto;
105
+ font-size: var(--rc-menu-shortcut-size, 0.8em);
106
+ color: var(--rc-menu-shortcut-color, GrayText);
107
+ white-space: nowrap;
108
+ }
109
+
110
+ /* Submenu indicator for items that open a child menu. */
111
+ rc-menu [aria-haspopup='menu']::after,
112
+ rc-menu [aria-haspopup='true']::after {
113
+ content: var(--rc-menu-submenu-indicator-content, '›');
114
+ margin-inline-start: auto;
115
+ font-size: var(--rc-menu-submenu-indicator-size, 1em);
116
+ color: var(--rc-menu-submenu-indicator-color, currentColor);
117
+ }
118
+
119
+ /* Separators */
120
+ rc-menu [role='separator'],
121
+ rc-menu hr {
122
+ border: none;
123
+ border-block-start: var(--rc-menu-separator-border, var(--rc-border, 1px solid ButtonBorder));
124
+ margin-block: var(--rc-menu-separator-margin-block, var(--rc-control-gap, 0.25em));
125
+ margin-inline: var(--rc-menu-separator-margin-inline, 0px);
126
+ }
127
+
128
+ /* Group containers */
129
+ rc-menu [role='group'] {
130
+ display: flex;
131
+ flex-direction: column;
132
+ align-items: stretch;
133
+ }
134
+
135
+ /* Optional section label (non-interactive heading above a group of items) */
136
+ rc-menu [data-group-label] {
137
+ padding-block: var(--rc-menu-group-label-padding-block, 0.1em);
138
+ padding-inline: var(--rc-menu-item-padding-inline, 0.75em);
139
+ color: var(--rc-menu-group-label-color, GrayText);
140
+ user-select: none;
141
+ pointer-events: none;
142
+ }
143
+
144
+ /* Disabled items */
145
+ rc-menu button:disabled,
146
+ rc-menu [aria-disabled='true'] {
147
+ color: var(--rc-menu-disabled-color, var(--rc-text-disabled, GrayText));
148
+ opacity: var(--rc-menu-disabled-opacity, var(--rc-disabled-opacity, 0.55));
149
+ pointer-events: none;
150
+ }
151
+ }
152
+ `;
153
+ var a;
154
+ const l = (a = class extends m {
155
+ constructor() {
156
+ super(...arguments), this.label = "", this._menuItems = [], this._activeDescendantCtrl = new b(this, {
157
+ host: () => this,
158
+ items: () => this._menuItems
159
+ }), this._onKeydown = (e) => {
160
+ switch (e.key) {
161
+ case "ArrowDown": {
162
+ e.preventDefault(), e.stopPropagation(), this._activeDescendantCtrl.activeItem ? this._activeDescendantCtrl.navigate(1) : this._activeDescendantCtrl.navigateToFirst();
163
+ break;
164
+ }
165
+ case "ArrowUp": {
166
+ e.preventDefault(), e.stopPropagation(), this._activeDescendantCtrl.activeItem ? this._activeDescendantCtrl.navigate(-1) : this._activeDescendantCtrl.navigateToLast();
167
+ break;
168
+ }
169
+ case "Home": {
170
+ e.preventDefault(), e.stopPropagation(), this._activeDescendantCtrl.navigateToFirst();
171
+ break;
172
+ }
173
+ case "End": {
174
+ e.preventDefault(), e.stopPropagation(), this._activeDescendantCtrl.navigateToLast();
175
+ break;
176
+ }
177
+ case "Enter":
178
+ case " ": {
179
+ e.preventDefault(), e.stopPropagation();
180
+ const t = this._activeDescendantCtrl.activeItem;
181
+ t && t.click();
182
+ break;
183
+ }
184
+ case "Escape": {
185
+ e.preventDefault(), e.stopPropagation(), this.dispatchEvent(
186
+ new CustomEvent("rc-menu-close", {
187
+ bubbles: !0,
188
+ composed: !0,
189
+ detail: { reason: "escape" }
190
+ })
191
+ );
192
+ break;
193
+ }
194
+ }
195
+ }, this._onBlur = () => {
196
+ this._activeDescendantCtrl.clear();
197
+ }, this._onClick = (e) => {
198
+ const r = e.target.closest(
199
+ '[role="menuitem"],[role="menuitemcheckbox"],[role="menuitemradio"]'
200
+ );
201
+ if (!r || !this.contains(r) || r.matches(':disabled, [aria-disabled="true"]'))
202
+ return;
203
+ const n = this._applyCheckedState(r);
204
+ this.dispatchEvent(
205
+ new CustomEvent("rc-menu-activate", {
206
+ bubbles: !0,
207
+ composed: !0,
208
+ detail: {
209
+ item: r,
210
+ value: r.dataset.value ?? r.getAttribute("value") ?? "",
211
+ text: r.textContent?.trim() ?? "",
212
+ checked: n
213
+ }
214
+ })
215
+ );
216
+ };
217
+ }
218
+ static _ensureBaseStyles(e) {
219
+ if (a._styledRoots.has(e))
220
+ return;
221
+ a._styledRoots.add(e);
222
+ const t = document.createElement("style");
223
+ t.setAttribute("data-rc-light-dom-base", "rc-menu"), t.textContent = k, e instanceof Document ? e.head.appendChild(t) : e.appendChild(t);
224
+ }
225
+ connectedCallback() {
226
+ super.connectedCallback(), this.hasAttribute("role") || this.setAttribute("role", "menu"), this.setAttribute("tabindex", "0"), this._syncAriaLabel(), this.addEventListener("keydown", this._onKeydown), this.addEventListener("blur", this._onBlur), this.addEventListener("click", this._onClick), a._ensureBaseStyles(this.getRootNode());
227
+ }
228
+ disconnectedCallback() {
229
+ super.disconnectedCallback(), this.removeEventListener("keydown", this._onKeydown), this.removeEventListener("blur", this._onBlur), this.removeEventListener("click", this._onClick);
230
+ }
231
+ updated(e) {
232
+ e.has("label") && this._syncAriaLabel();
233
+ }
234
+ render() {
235
+ return d`<slot @slotchange=${this._onSlotChange}></slot>`;
236
+ }
237
+ /** All navigable menu item elements in document order. */
238
+ get items() {
239
+ return this._menuItems;
240
+ }
241
+ /** Move the virtual cursor and DOM focus to the first navigable item. */
242
+ focusFirst() {
243
+ this.focus(), this._activeDescendantCtrl.navigateToFirst();
244
+ }
245
+ /** Move the virtual cursor and DOM focus to the last navigable item. */
246
+ focusLast() {
247
+ this.focus(), this._activeDescendantCtrl.navigateToLast();
248
+ }
249
+ /**
250
+ * Move the virtual cursor to a specific item.
251
+ *
252
+ * @param item - The element to make active. No-op when null or undefined.
253
+ */
254
+ focusItem(e) {
255
+ e && (this.focus(), this._activeDescendantCtrl.navigateToItem(e));
256
+ }
257
+ /**
258
+ * Move the virtual cursor to the item at `index`.
259
+ *
260
+ * @param index - Zero-based index into the navigable items array.
261
+ */
262
+ focusItemAt(e) {
263
+ this.focus(), this._activeDescendantCtrl.navigateTo(e);
264
+ }
265
+ _syncAriaLabel() {
266
+ this.label ? this.setAttribute("aria-label", this.label) : this.removeAttribute("aria-label");
267
+ }
268
+ _onSlotChange(e) {
269
+ const t = e.currentTarget;
270
+ this._menuItems = this._collectMenuItems(t), this._initMenuItems();
271
+ }
272
+ /**
273
+ * Collects navigable menu items from the slot, supporting `role="group"`
274
+ * containers and skipping `role="separator"` elements.
275
+ *
276
+ * Direct focusable children are included as-is. Elements with
277
+ * `role="group"` are traversed one level deep so their focusable children
278
+ * participate in arrow-key navigation. Separators and non-focusable
279
+ * container elements are excluded.
280
+ */
281
+ _collectMenuItems(e) {
282
+ const t = [];
283
+ for (const r of e.assignedElements()) {
284
+ const n = r.getAttribute("role");
285
+ if (n !== "separator") {
286
+ if (n === "group") {
287
+ for (const i of r.children)
288
+ s(i) && t.push(i);
289
+ continue;
290
+ }
291
+ s(r) && t.push(r);
292
+ }
293
+ }
294
+ return t;
295
+ }
296
+ _initMenuItems() {
297
+ this._menuItems.forEach((e) => {
298
+ const t = e.getAttribute("role");
299
+ (!t || !f.includes(t)) && e.setAttribute("role", "menuitem"), e.setAttribute("tabindex", "-1"), (e.getAttribute("role") === "menuitemcheckbox" || e.getAttribute("role") === "menuitemradio") && !e.hasAttribute("aria-checked") && e.setAttribute("aria-checked", "false");
300
+ });
301
+ }
302
+ _applyCheckedState(e) {
303
+ const t = e.getAttribute("role");
304
+ if (t === "menuitemcheckbox") {
305
+ const r = e.getAttribute("aria-checked") === "true" ? "false" : "true";
306
+ return e.setAttribute("aria-checked", r), r;
307
+ }
308
+ if (t === "menuitemradio") {
309
+ const r = e.closest('[role="group"]'), n = r && this.contains(r) ? r : this;
310
+ for (const i of n.querySelectorAll('[role="menuitemradio"]'))
311
+ i.closest("rc-menu") === this && i.setAttribute("aria-checked", i === e ? "true" : "false");
312
+ return "true";
313
+ }
314
+ }
315
+ }, a.styles = [v], a._styledRoots = /* @__PURE__ */ new Set(), a);
316
+ g([
317
+ h({ type: String })
318
+ ], l.prototype, "label");
319
+ let C = l;
320
+ export {
321
+ C as R
322
+ };
323
+ //# sourceMappingURL=rc-menu-Drg26iOK.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rc-menu-Drg26iOK.js","sources":["../src/rc-menu.styles.ts","../src/rc-menu.ts"],"sourcesContent":["import { css } from 'lit';\n\nexport const menuStyles = css`\n :host {\n display: flex;\n flex-direction: column;\n min-width: var(--rc-menu-min-width, 10em);\n padding-block: var(--rc-menu-padding-block, var(--rc-control-padding-block, 0.25em));\n background: var(--rc-menu-background, var(--rc-surface, Canvas));\n border: var(--rc-menu-border, var(--rc-border, 1px solid ButtonBorder));\n border-radius: var(--rc-menu-radius, var(--rc-control-radius, 0));\n box-shadow: var(\n --rc-menu-shadow,\n var(--rc-shadow, 0 2px 8px color-mix(in srgb, CanvasText 15%, transparent))\n );\n color: var(--rc-menu-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 overflow: hidden;\n }\n\n :host(:focus-visible) {\n outline: var(--rc-focus-ring, auto);\n outline-offset: var(--rc-focus-ring-offset, 0);\n }\n`;\n\nexport default menuStyles;\n","import { LitElement, html } from 'lit';\nimport { property } from 'lit/decorators.js';\n\nimport { ActiveDescendantController, isFocusable } from '@rcarls/rc-common';\n\nimport menuStyles from './rc-menu.styles';\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'rc-menu': RCMenu;\n }\n}\n\n/** Detail payload for the `rc-menu-activate` event. */\nexport interface RCMenuActivateEvent {\n /** The activated menu item element. */\n item: HTMLElement;\n\n /** The item's `data-value` or `value` attribute, or an empty string. */\n value: string;\n\n /** Trimmed text content of the activated item. */\n text: string;\n\n /** Current `aria-checked` state for checkbox/radio menu items. */\n checked?: 'true' | 'false' | 'mixed';\n}\n\n/** WAI-ARIA roles that are valid direct children of `role=\"menu\"`. */\nconst MENU_ITEM_ROLES = ['menuitem', 'menuitemcheckbox', 'menuitemradio'] as const;\n\n/** Structural CSS for light-DOM menu items, injected into the containing document or shadow root. */\nconst LIGHT_DOM_CSS = `\n@layer rc-base {\n /* Item base layout */\n rc-menu [role='menuitem'],\n rc-menu [role='menuitemcheckbox'],\n rc-menu [role='menuitemradio'],\n rc-menu button:disabled {\n display: flex;\n align-items: center;\n gap: var(--rc-menu-item-gap, var(--rc-item-gap, 0.5em));\n min-block-size: var(--rc-menu-item-min-block-size, 0px);\n width: 100%;\n text-align: start;\n padding: var(--rc-menu-item-padding-block, var(--rc-item-padding-block, 0.3em))\n var(--rc-menu-item-padding-inline, var(--rc-item-padding-inline, 0.75em));\n appearance: none;\n box-sizing: border-box;\n border: none;\n background: transparent;\n text-decoration: none;\n color: inherit;\n font: inherit;\n cursor: default;\n user-select: none;\n outline: none;\n transition: var(--rc-menu-item-transition);\n }\n\n /* Pointer hover */\n rc-menu [role='menuitem']:hover,\n rc-menu [role='menuitemcheckbox']:hover,\n rc-menu [role='menuitemradio']:hover {\n background: var(--rc-menu-hover-bg, color-mix(in srgb, Highlight 8%, transparent));\n color: var(--rc-menu-hover-color, inherit);\n }\n\n /* Virtual cursor (set by ActiveDescendantController) */\n rc-menu [role='menuitem'][data-active],\n rc-menu [role='menuitemcheckbox'][data-active],\n rc-menu [role='menuitemradio'][data-active] {\n background: var(--rc-menu-active-bg, color-mix(in srgb, Highlight 8%, transparent));\n color: var(--rc-menu-active-color, inherit);\n }\n\n /* Check/radio indicator — ::before acts as a fixed-width leading column.\n Content is empty (unchecked) or a Unicode symbol (checked/mixed).\n Semantic state is conveyed by aria-checked; the visual symbol is decorative. */\n rc-menu [role='menuitemcheckbox']::before,\n rc-menu [role='menuitemradio']::before {\n content: '';\n flex: 0 0 var(--rc-menu-check-size, 1.25em);\n text-align: center;\n line-height: 1;\n color: var(--rc-menu-check-color, inherit);\n }\n\n rc-menu [role='menuitemcheckbox'][aria-checked='true']::before {\n content: '✓';\n }\n\n rc-menu [role='menuitemcheckbox'][aria-checked='mixed']::before {\n content: '−';\n }\n\n rc-menu [role='menuitemradio'][aria-checked='true']::before {\n content: '•';\n }\n\n /* Trailing label — keyboard shortcut or hint, pushed to the inline-end of the item */\n rc-menu [data-menu-shortcut] {\n margin-inline-start: auto;\n font-size: var(--rc-menu-shortcut-size, 0.8em);\n color: var(--rc-menu-shortcut-color, GrayText);\n white-space: nowrap;\n }\n\n /* Submenu indicator for items that open a child menu. */\n rc-menu [aria-haspopup='menu']::after,\n rc-menu [aria-haspopup='true']::after {\n content: var(--rc-menu-submenu-indicator-content, '›');\n margin-inline-start: auto;\n font-size: var(--rc-menu-submenu-indicator-size, 1em);\n color: var(--rc-menu-submenu-indicator-color, currentColor);\n }\n\n /* Separators */\n rc-menu [role='separator'],\n rc-menu hr {\n border: none;\n border-block-start: var(--rc-menu-separator-border, var(--rc-border, 1px solid ButtonBorder));\n margin-block: var(--rc-menu-separator-margin-block, var(--rc-control-gap, 0.25em));\n margin-inline: var(--rc-menu-separator-margin-inline, 0px);\n }\n\n /* Group containers */\n rc-menu [role='group'] {\n display: flex;\n flex-direction: column;\n align-items: stretch;\n }\n\n /* Optional section label (non-interactive heading above a group of items) */\n rc-menu [data-group-label] {\n padding-block: var(--rc-menu-group-label-padding-block, 0.1em);\n padding-inline: var(--rc-menu-item-padding-inline, 0.75em);\n color: var(--rc-menu-group-label-color, GrayText);\n user-select: none;\n pointer-events: none;\n }\n\n /* Disabled items */\n rc-menu button:disabled,\n rc-menu [aria-disabled='true'] {\n color: var(--rc-menu-disabled-color, var(--rc-text-disabled, GrayText));\n opacity: var(--rc-menu-disabled-opacity, var(--rc-disabled-opacity, 0.55));\n pointer-events: none;\n }\n}\n`;\n\n/**\n * Menu popup for command surfaces with keyboard navigation and typed activation events,\n * following the WAI-ARIA Menu pattern.\n *\n *\n * @see {@link https://richardcarls.github.io/rc-webcomponents/components/rc-menu rc-menu docs}\n * @see {@link https://www.w3.org/WAI/ARIA/apg/patterns/menu/ WAI-ARIA Menu pattern}\n *\n * @slot default - Menu items, groups, and separators. Supported content model:\n * - Focusable elements (`<button>`, `<a>`, `[tabindex]`) become navigable `role=\"menuitem\"` entries.\n * - `role=\"menuitemcheckbox\"` / `role=\"menuitemradio\"` elements get a leading indicator column via `::before`;\n * `aria-checked` is initialized when missing and updated on activation.\n * - `role=\"group\"` divs group related items; label them with `aria-label` or `aria-labelledby`. Optionally\n * add a visible `[data-group-label]` element as first child of the group for section headings.\n * - `role=\"separator\"` / `<hr>` produce horizontal dividers between sections.\n * - Within any item, `[data-menu-shortcut]` elements are pushed to the inline-end edge for keyboard hints.\n * - Items with `aria-haspopup=\"menu\"` or `aria-haspopup=\"true\"` get a trailing submenu indicator.\n *\n * @fires rc-menu-activate - Fired when a menu item is activated via keyboard (Enter/Space) or pointer click.\n * @fires rc-menu-close - Fired when Escape is pressed.\n *\n * @cssprop [--rc-menu-min-width=10em] - Minimum width of the menu panel.\n * @cssprop [--rc-menu-padding-block=0.25em] - Block (top/bottom) padding inside the panel.\n * @cssprop [--rc-menu-background=Canvas] - Background color; falls back through --rc-surface.\n * @cssprop [--rc-menu-border=1px solid ButtonBorder] - Border; falls back through --rc-border.\n * @cssprop [--rc-menu-radius=var(--rc-control-radius)] - Menu panel border radius.\n * @cssprop [--rc-menu-shadow=0 2px 8px color-mix(in srgb, CanvasText 15%, transparent)] - Box shadow; falls back through --rc-shadow.\n * @cssprop [--rc-menu-item-min-block-size=0] - Minimum block size for menu item rows.\n * @cssprop [--rc-menu-item-padding-block=var(--rc-item-padding-block)] - Menu item block-axis padding.\n * @cssprop [--rc-menu-item-padding-inline=var(--rc-item-padding-inline)] - Menu item inline-axis padding.\n * @cssprop [--rc-menu-item-gap=var(--rc-item-gap)] - Gap between flex children inside each item.\n * @cssprop [--rc-menu-item-transition] - CSS transition applied to each item row.\n * @cssprop [--rc-menu-hover-bg=color-mix(in srgb, Highlight 8%, transparent)] - Pointer-hover background.\n * @cssprop [--rc-menu-hover-color=inherit] - Pointer-hover text color.\n * @cssprop [--rc-menu-active-bg=color-mix(in srgb, Highlight 8%, transparent)] - Virtual-cursor active background.\n * @cssprop [--rc-menu-active-color=inherit] - Virtual-cursor active text color.\n * @cssprop [--rc-menu-check-size=1.25em] - Width of the leading indicator column for checkbox/radio items.\n * @cssprop [--rc-menu-check-color=inherit] - Color of the check/radio symbol.\n * @cssprop [--rc-menu-shortcut-size=0.8em] - Font size of `[data-menu-shortcut]` labels.\n * @cssprop [--rc-menu-shortcut-color=GrayText] - Text color of `[data-menu-shortcut]` labels.\n * @cssprop [--rc-menu-submenu-indicator-content='›'] - Text content for submenu indicators.\n * @cssprop [--rc-menu-submenu-indicator-size=1em] - Font size of submenu indicators.\n * @cssprop [--rc-menu-submenu-indicator-color=currentColor] - Text color of submenu indicators.\n * @cssprop [--rc-menu-disabled-color=GrayText] - Disabled menu item text color.\n * @cssprop [--rc-menu-disabled-opacity=var(--rc-disabled-opacity)] - Disabled menu item opacity.\n * @cssprop [--rc-menu-separator-border=1px solid ButtonBorder] - Separator border.\n * @cssprop [--rc-menu-separator-margin-block=var(--rc-control-gap)] - Separator block-axis margin.\n * @cssprop [--rc-menu-separator-margin-inline=0px] - Separator inline margin; set to match item padding for inset separators.\n * @cssprop [--rc-menu-group-label-padding-block=0.1em] - Block padding on `[data-group-label]` section headings.\n * @cssprop [--rc-menu-group-label-color=GrayText] - Text color of `[data-group-label]` section headings.\n */\nexport class RCMenu extends LitElement {\n static styles = [menuStyles];\n\n private static readonly _styledRoots = new Set<Document | ShadowRoot>();\n\n private static _ensureBaseStyles(root: Document | ShadowRoot): void {\n if (RCMenu._styledRoots.has(root)) {\n return;\n }\n\n RCMenu._styledRoots.add(root);\n\n const style = document.createElement('style');\n\n style.setAttribute('data-rc-light-dom-base', 'rc-menu');\n style.textContent = LIGHT_DOM_CSS;\n\n if (root instanceof Document) {\n root.head.appendChild(style);\n } else {\n root.appendChild(style);\n }\n }\n\n /**\n * Accessible label for this menu. Synced to `aria-label` on the host.\n *\n * Prefer `aria-labelledby` for a visible label associated via ID.\n */\n @property({ type: String })\n label = '';\n\n protected _menuItems: Element[] = [];\n\n protected readonly _activeDescendantCtrl = new ActiveDescendantController(this, {\n host: () => this,\n items: () => this._menuItems,\n });\n\n override connectedCallback(): void {\n super.connectedCallback();\n\n if (!this.hasAttribute('role')) {\n this.setAttribute('role', 'menu');\n }\n\n this.setAttribute('tabindex', '0');\n this._syncAriaLabel();\n\n this.addEventListener('keydown', this._onKeydown);\n this.addEventListener('blur', this._onBlur);\n this.addEventListener('click', this._onClick);\n\n RCMenu._ensureBaseStyles(this.getRootNode() as Document | ShadowRoot);\n }\n\n override disconnectedCallback(): void {\n super.disconnectedCallback();\n\n this.removeEventListener('keydown', this._onKeydown);\n this.removeEventListener('blur', this._onBlur);\n this.removeEventListener('click', this._onClick);\n }\n\n protected override updated(changed: Map<PropertyKey, unknown>): void {\n if (changed.has('label')) {\n this._syncAriaLabel();\n }\n }\n\n protected override render() {\n return html`<slot @slotchange=${this._onSlotChange}></slot>`;\n }\n\n /** All navigable menu item elements in document order. */\n get items(): readonly Element[] {\n return this._menuItems;\n }\n\n /** Move the virtual cursor and DOM focus to the first navigable item. */\n focusFirst(): void {\n this.focus();\n this._activeDescendantCtrl.navigateToFirst();\n }\n\n /** Move the virtual cursor and DOM focus to the last navigable item. */\n focusLast(): void {\n this.focus();\n this._activeDescendantCtrl.navigateToLast();\n }\n\n /**\n * Move the virtual cursor to a specific item.\n *\n * @param item - The element to make active. No-op when null or undefined.\n */\n focusItem(item?: Element | null): void {\n if (!item) return;\n this.focus();\n this._activeDescendantCtrl.navigateToItem(item);\n }\n\n /**\n * Move the virtual cursor to the item at `index`.\n *\n * @param index - Zero-based index into the navigable items array.\n */\n focusItemAt(index: number): void {\n this.focus();\n this._activeDescendantCtrl.navigateTo(index);\n }\n\n protected _syncAriaLabel(): void {\n if (this.label) {\n this.setAttribute('aria-label', this.label);\n } else {\n this.removeAttribute('aria-label');\n }\n }\n\n protected _onSlotChange(e: Event): void {\n const slot = e.currentTarget as HTMLSlotElement;\n\n this._menuItems = this._collectMenuItems(slot);\n this._initMenuItems();\n }\n\n /**\n * Collects navigable menu items from the slot, supporting `role=\"group\"`\n * containers and skipping `role=\"separator\"` elements.\n *\n * Direct focusable children are included as-is. Elements with\n * `role=\"group\"` are traversed one level deep so their focusable children\n * participate in arrow-key navigation. Separators and non-focusable\n * container elements are excluded.\n */\n protected _collectMenuItems(slot: HTMLSlotElement): Element[] {\n const items: Element[] = [];\n\n for (const el of slot.assignedElements()) {\n const role = el.getAttribute('role');\n\n if (role === 'separator') continue;\n\n if (role === 'group') {\n for (const child of el.children) {\n if (isFocusable(child)) items.push(child);\n }\n continue;\n }\n\n if (isFocusable(el)) items.push(el);\n }\n\n return items;\n }\n\n protected _initMenuItems(): void {\n this._menuItems.forEach((el) => {\n const existingRole = el.getAttribute('role');\n\n // Preserve explicit roles; only assign the default when none is present.\n if (\n !existingRole ||\n !MENU_ITEM_ROLES.includes(existingRole as (typeof MENU_ITEM_ROLES)[number])\n ) {\n el.setAttribute('role', 'menuitem');\n }\n\n el.setAttribute('tabindex', '-1');\n\n if (\n (el.getAttribute('role') === 'menuitemcheckbox' ||\n el.getAttribute('role') === 'menuitemradio') &&\n !el.hasAttribute('aria-checked')\n ) {\n el.setAttribute('aria-checked', 'false');\n }\n });\n }\n\n protected _applyCheckedState(item: HTMLElement): 'true' | 'false' | 'mixed' | undefined {\n const role = item.getAttribute('role');\n\n if (role === 'menuitemcheckbox') {\n const checked = item.getAttribute('aria-checked') === 'true' ? 'false' : 'true';\n\n item.setAttribute('aria-checked', checked);\n\n return checked;\n }\n\n if (role === 'menuitemradio') {\n const group = item.closest('[role=\"group\"]');\n const scope = group && this.contains(group) ? group : this;\n\n for (const radio of scope.querySelectorAll<HTMLElement>('[role=\"menuitemradio\"]')) {\n if (radio.closest('rc-menu') === this) {\n radio.setAttribute('aria-checked', radio === item ? 'true' : 'false');\n }\n }\n\n return 'true';\n }\n\n return undefined;\n }\n\n protected _onKeydown = (e: KeyboardEvent): void => {\n switch (e.key) {\n case 'ArrowDown': {\n e.preventDefault();\n e.stopPropagation();\n\n if (this._activeDescendantCtrl.activeItem) {\n this._activeDescendantCtrl.navigate(1);\n } else {\n this._activeDescendantCtrl.navigateToFirst();\n }\n\n break;\n }\n\n case 'ArrowUp': {\n e.preventDefault();\n e.stopPropagation();\n\n if (this._activeDescendantCtrl.activeItem) {\n this._activeDescendantCtrl.navigate(-1);\n } else {\n this._activeDescendantCtrl.navigateToLast();\n }\n\n break;\n }\n\n case 'Home': {\n e.preventDefault();\n e.stopPropagation();\n\n this._activeDescendantCtrl.navigateToFirst();\n\n break;\n }\n\n case 'End': {\n e.preventDefault();\n e.stopPropagation();\n\n this._activeDescendantCtrl.navigateToLast();\n\n break;\n }\n\n case 'Enter':\n case ' ': {\n e.preventDefault();\n e.stopPropagation();\n\n const active = this._activeDescendantCtrl.activeItem;\n\n if (active) {\n // _onClick dispatches rc-menu-activate when the synthetic click bubbles up.\n (active as HTMLElement).click();\n }\n\n break;\n }\n\n case 'Escape': {\n e.preventDefault();\n e.stopPropagation();\n\n this.dispatchEvent(\n new CustomEvent('rc-menu-close', {\n bubbles: true,\n composed: true,\n detail: { reason: 'escape' },\n }),\n );\n\n break;\n }\n }\n };\n\n protected _onBlur = (): void => {\n this._activeDescendantCtrl.clear();\n };\n\n protected _onClick = (e: MouseEvent): void => {\n const target = e.target as Element;\n const item = target.closest<HTMLElement>(\n '[role=\"menuitem\"],[role=\"menuitemcheckbox\"],[role=\"menuitemradio\"]',\n );\n\n if (!item || !this.contains(item)) {\n return;\n }\n\n if (item.matches(':disabled, [aria-disabled=\"true\"]')) {\n return;\n }\n\n const checked = this._applyCheckedState(item);\n\n this.dispatchEvent(\n new CustomEvent<RCMenuActivateEvent>('rc-menu-activate', {\n bubbles: true,\n composed: true,\n detail: {\n item,\n value: item.dataset.value ?? item.getAttribute('value') ?? '',\n text: item.textContent?.trim() ?? '',\n checked,\n },\n }),\n );\n };\n}\n\nexport default RCMenu;\n"],"names":["menuStyles","css","MENU_ITEM_ROLES","LIGHT_DOM_CSS","_RCMenu","_a","LitElement","ActiveDescendantController","active","item","checked","root","style","changed","html","index","slot","items","el","role","child","isFocusable","existingRole","group","scope","radio","__decorateClass","property","RCMenu"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { R as e } from "./rc-menu-Bd1Ytmx9.js";
1
+ import { R as e } from "./rc-menu-Drg26iOK.js";
2
2
  customElements.get("rc-menu") || customElements.define("rc-menu", e);
3
3
  export {
4
4
  e as RCMenu
package/dist/rc-menu.js CHANGED
@@ -1,4 +1,4 @@
1
- import { R as r } from "./rc-menu-Bd1Ytmx9.js";
1
+ import { R as r } from "./rc-menu-Drg26iOK.js";
2
2
  export {
3
3
  r as RCMenu
4
4
  };
@@ -0,0 +1,122 @@
1
+ import { LitElement } from 'lit';
2
+ import { ActiveDescendantController } from '@rcarls/rc-common';
3
+ declare global {
4
+ interface HTMLElementTagNameMap {
5
+ 'rc-menu': RCMenu;
6
+ }
7
+ }
8
+ /** Detail payload for the `rc-menu-activate` event. */
9
+ export interface RCMenuActivateEvent {
10
+ /** The activated menu item element. */
11
+ item: HTMLElement;
12
+ /** The item's `data-value` or `value` attribute, or an empty string. */
13
+ value: string;
14
+ /** Trimmed text content of the activated item. */
15
+ text: string;
16
+ /** Current `aria-checked` state for checkbox/radio menu items. */
17
+ checked?: 'true' | 'false' | 'mixed';
18
+ }
19
+ /**
20
+ * Menu popup for command surfaces with keyboard navigation and typed activation events,
21
+ * following the WAI-ARIA Menu pattern.
22
+ *
23
+ *
24
+ * @see {@link https://richardcarls.github.io/rc-webcomponents/components/rc-menu rc-menu docs}
25
+ * @see {@link https://www.w3.org/WAI/ARIA/apg/patterns/menu/ WAI-ARIA Menu pattern}
26
+ *
27
+ * @slot default - Menu items, groups, and separators. Supported content model:
28
+ * - Focusable elements (`<button>`, `<a>`, `[tabindex]`) become navigable `role="menuitem"` entries.
29
+ * - `role="menuitemcheckbox"` / `role="menuitemradio"` elements get a leading indicator column via `::before`;
30
+ * `aria-checked` is initialized when missing and updated on activation.
31
+ * - `role="group"` divs group related items; label them with `aria-label` or `aria-labelledby`. Optionally
32
+ * add a visible `[data-group-label]` element as first child of the group for section headings.
33
+ * - `role="separator"` / `<hr>` produce horizontal dividers between sections.
34
+ * - Within any item, `[data-menu-shortcut]` elements are pushed to the inline-end edge for keyboard hints.
35
+ * - Items with `aria-haspopup="menu"` or `aria-haspopup="true"` get a trailing submenu indicator.
36
+ *
37
+ * @fires rc-menu-activate - Fired when a menu item is activated via keyboard (Enter/Space) or pointer click.
38
+ * @fires rc-menu-close - Fired when Escape is pressed.
39
+ *
40
+ * @cssprop [--rc-menu-min-width=10em] - Minimum width of the menu panel.
41
+ * @cssprop [--rc-menu-padding-block=0.25em] - Block (top/bottom) padding inside the panel.
42
+ * @cssprop [--rc-menu-background=Canvas] - Background color; falls back through --rc-surface.
43
+ * @cssprop [--rc-menu-border=1px solid ButtonBorder] - Border; falls back through --rc-border.
44
+ * @cssprop [--rc-menu-radius=var(--rc-control-radius)] - Menu panel border radius.
45
+ * @cssprop [--rc-menu-shadow=0 2px 8px color-mix(in srgb, CanvasText 15%, transparent)] - Box shadow; falls back through --rc-shadow.
46
+ * @cssprop [--rc-menu-item-min-block-size=0] - Minimum block size for menu item rows.
47
+ * @cssprop [--rc-menu-item-padding-block=var(--rc-item-padding-block)] - Menu item block-axis padding.
48
+ * @cssprop [--rc-menu-item-padding-inline=var(--rc-item-padding-inline)] - Menu item inline-axis padding.
49
+ * @cssprop [--rc-menu-item-gap=var(--rc-item-gap)] - Gap between flex children inside each item.
50
+ * @cssprop [--rc-menu-item-transition] - CSS transition applied to each item row.
51
+ * @cssprop [--rc-menu-hover-bg=color-mix(in srgb, Highlight 8%, transparent)] - Pointer-hover background.
52
+ * @cssprop [--rc-menu-hover-color=inherit] - Pointer-hover text color.
53
+ * @cssprop [--rc-menu-active-bg=color-mix(in srgb, Highlight 8%, transparent)] - Virtual-cursor active background.
54
+ * @cssprop [--rc-menu-active-color=inherit] - Virtual-cursor active text color.
55
+ * @cssprop [--rc-menu-check-size=1.25em] - Width of the leading indicator column for checkbox/radio items.
56
+ * @cssprop [--rc-menu-check-color=inherit] - Color of the check/radio symbol.
57
+ * @cssprop [--rc-menu-shortcut-size=0.8em] - Font size of `[data-menu-shortcut]` labels.
58
+ * @cssprop [--rc-menu-shortcut-color=GrayText] - Text color of `[data-menu-shortcut]` labels.
59
+ * @cssprop [--rc-menu-submenu-indicator-content='›'] - Text content for submenu indicators.
60
+ * @cssprop [--rc-menu-submenu-indicator-size=1em] - Font size of submenu indicators.
61
+ * @cssprop [--rc-menu-submenu-indicator-color=currentColor] - Text color of submenu indicators.
62
+ * @cssprop [--rc-menu-disabled-color=GrayText] - Disabled menu item text color.
63
+ * @cssprop [--rc-menu-disabled-opacity=var(--rc-disabled-opacity)] - Disabled menu item opacity.
64
+ * @cssprop [--rc-menu-separator-border=1px solid ButtonBorder] - Separator border.
65
+ * @cssprop [--rc-menu-separator-margin-block=var(--rc-control-gap)] - Separator block-axis margin.
66
+ * @cssprop [--rc-menu-separator-margin-inline=0px] - Separator inline margin; set to match item padding for inset separators.
67
+ * @cssprop [--rc-menu-group-label-padding-block=0.1em] - Block padding on `[data-group-label]` section headings.
68
+ * @cssprop [--rc-menu-group-label-color=GrayText] - Text color of `[data-group-label]` section headings.
69
+ */
70
+ export declare class RCMenu extends LitElement {
71
+ static styles: import('lit').CSSResult[];
72
+ private static readonly _styledRoots;
73
+ private static _ensureBaseStyles;
74
+ /**
75
+ * Accessible label for this menu. Synced to `aria-label` on the host.
76
+ *
77
+ * Prefer `aria-labelledby` for a visible label associated via ID.
78
+ */
79
+ label: string;
80
+ protected _menuItems: Element[];
81
+ protected readonly _activeDescendantCtrl: ActiveDescendantController;
82
+ connectedCallback(): void;
83
+ disconnectedCallback(): void;
84
+ protected updated(changed: Map<PropertyKey, unknown>): void;
85
+ protected render(): import('lit').TemplateResult<1>;
86
+ /** All navigable menu item elements in document order. */
87
+ get items(): readonly Element[];
88
+ /** Move the virtual cursor and DOM focus to the first navigable item. */
89
+ focusFirst(): void;
90
+ /** Move the virtual cursor and DOM focus to the last navigable item. */
91
+ focusLast(): void;
92
+ /**
93
+ * Move the virtual cursor to a specific item.
94
+ *
95
+ * @param item - The element to make active. No-op when null or undefined.
96
+ */
97
+ focusItem(item?: Element | null): void;
98
+ /**
99
+ * Move the virtual cursor to the item at `index`.
100
+ *
101
+ * @param index - Zero-based index into the navigable items array.
102
+ */
103
+ focusItemAt(index: number): void;
104
+ protected _syncAriaLabel(): void;
105
+ protected _onSlotChange(e: Event): void;
106
+ /**
107
+ * Collects navigable menu items from the slot, supporting `role="group"`
108
+ * containers and skipping `role="separator"` elements.
109
+ *
110
+ * Direct focusable children are included as-is. Elements with
111
+ * `role="group"` are traversed one level deep so their focusable children
112
+ * participate in arrow-key navigation. Separators and non-focusable
113
+ * container elements are excluded.
114
+ */
115
+ protected _collectMenuItems(slot: HTMLSlotElement): Element[];
116
+ protected _initMenuItems(): void;
117
+ protected _applyCheckedState(item: HTMLElement): 'true' | 'false' | 'mixed' | undefined;
118
+ protected _onKeydown: (e: KeyboardEvent) => void;
119
+ protected _onBlur: () => void;
120
+ protected _onClick: (e: MouseEvent) => void;
121
+ }
122
+ export default RCMenu;
package/package.json CHANGED
@@ -3,14 +3,14 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.2.0",
7
- "description": "Headless WAI-ARIA menu popup web component built with Lit",
6
+ "version": "0.3.0",
7
+ "description": "Menu popup for command surfaces with keyboard navigation and typed activation events.",
8
8
  "repository": {
9
9
  "type": "git",
10
10
  "url": "git+https://github.com/richardcarls/rc-webcomponents.git",
11
11
  "directory": "packages/rc-menu"
12
12
  },
13
- "homepage": "https://github.com/richardcarls/rc-webcomponents#readme",
13
+ "homepage": "https://richardcarls.github.io/rc-webcomponents/components/rc-menu",
14
14
  "license": "MIT",
15
15
  "type": "module",
16
16
  "files": [
@@ -20,38 +20,35 @@
20
20
  "exports": {
21
21
  ".": {
22
22
  "import": {
23
- "types": "./dist/types/index.d.ts",
23
+ "types": "./dist/types/packages/rc-menu/src/index.d.ts",
24
24
  "default": "./dist/rc-menu.js"
25
25
  }
26
26
  },
27
27
  "./define": {
28
28
  "import": {
29
- "types": "./dist/types/define.d.ts",
29
+ "types": "./dist/types/packages/rc-menu/src/define.d.ts",
30
30
  "default": "./dist/rc-menu-define.js"
31
31
  }
32
32
  }
33
33
  },
34
- "types": "./dist/types/index.d.ts",
34
+ "types": "./dist/types/packages/rc-menu/src/index.d.ts",
35
35
  "sideEffects": [
36
36
  "./dist/rc-menu-define.js"
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:^"
48
+ "@rcarls/rc-common": "workspace:*"
51
49
  },
52
50
  "devDependencies": {
53
51
  "@custom-elements-manifest/analyzer": "0.11.0",
54
- "@guanghechen/rollup-plugin-copy": "^6.0.9",
55
52
  "@vitest/browser-playwright": "4.1.5",
56
53
  "lit": "^3.0.0",
57
54
  "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
- }