@rcarls/rc-menu 0.6.0 → 0.7.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.
- package/CHANGELOG.md +85 -0
- package/dist/custom-elements.json +42 -12
- package/dist/rc-menu-define.js +1 -1
- package/dist/rc-menu-zM-Q3-uP.js +182 -0
- package/dist/rc-menu-zM-Q3-uP.js.map +1 -0
- package/dist/rc-menu.js +1 -1
- package/dist/types/packages/rc-menu/src/rc-menu.d.ts +15 -9
- package/package.json +2 -2
- package/dist/rc-menu-Df7GAw1H.js +0 -353
- package/dist/rc-menu-Df7GAw1H.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,90 @@
|
|
|
1
1
|
# @rcarls/rc-menu
|
|
2
2
|
|
|
3
|
+
## 0.7.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 687a9be: Add shared popup surface tokens: `--rc-popup-background`, `--rc-popup-color`,
|
|
8
|
+
`--rc-popup-border`, `--rc-popup-radius`, `--rc-popup-shadow`, and
|
|
9
|
+
`--rc-popup-padding-block`. Menus, adaptive-menu popups, and select and
|
|
10
|
+
combobox listboxes read them before their own fallbacks. Themes set them at the
|
|
11
|
+
theme root, so popups rendered inside other shadow roots keep the theme's
|
|
12
|
+
surface. Select and combobox add `--rc-*-listbox-background` and
|
|
13
|
+
`--rc-*-listbox-color`.
|
|
14
|
+
|
|
15
|
+
Breaking: popup surfaces no longer fall back to `--rc-control-radius`. Set
|
|
16
|
+
`--rc-popup-radius` (or the per-component radius token) to change popup
|
|
17
|
+
corners. The Material, Substrate, and Win31 themes no longer set per-component
|
|
18
|
+
popup surface tokens such as `--rc-menu-radius` or
|
|
19
|
+
`--rc-adaptive-menu-popup-background`; override the `--rc-popup-*` tokens
|
|
20
|
+
instead. Material select and combobox listboxes now use the menu container
|
|
21
|
+
color, Material forced-colors popups gain a border, and Substrate listboxes use
|
|
22
|
+
the menu shadow and padding. Material retains its two-layer adaptive-menu popup
|
|
23
|
+
shadow through a component token while other popups continue using the shared
|
|
24
|
+
shadow.
|
|
25
|
+
|
|
26
|
+
- 6d126fd: Add shared item type tokens: `--rc-item-font-family`, `--rc-item-font-size`,
|
|
27
|
+
`--rc-item-font-weight`, `--rc-item-line-height`, `--rc-item-letter-spacing`,
|
|
28
|
+
and `--rc-item-icon-size`. Menu, listbox, and adaptive-menu items read them.
|
|
29
|
+
Adaptive menu adds `--rc-adaptive-menu-trigger-hover-background`,
|
|
30
|
+
`--rc-adaptive-menu-trigger-shadow`, and
|
|
31
|
+
`--rc-adaptive-menu-trigger-active-shadow`, and styles overflowed `rc-button`
|
|
32
|
+
and `rc-menu-button` rows from its item tokens. Themes declare their menu,
|
|
33
|
+
adaptive-menu, and item tokens on the theme root, so menus inside other shadow
|
|
34
|
+
roots match menus in the document. Substrate's adaptive-menu trigger now shows
|
|
35
|
+
its hover background.
|
|
36
|
+
|
|
37
|
+
Breaking: theme menu and adaptive-menu tokens are inherited from the theme root
|
|
38
|
+
instead of set on each element, so a value set on an ancestor now reaches
|
|
39
|
+
nested menus. Material sets menu, listbox, and adaptive-menu item type through
|
|
40
|
+
`--rc-item-*` instead of per-component rules; override those tokens to change
|
|
41
|
+
item type. Themes no longer style the adaptive-menu trigger through
|
|
42
|
+
`::part(overflow-trigger)` hover and pressed rules; use the new trigger tokens.
|
|
43
|
+
|
|
44
|
+
### Patch Changes
|
|
45
|
+
|
|
46
|
+
- 5c006de: Ship component CSS minified. Package builds now run the CSS in Lit `css` styles
|
|
47
|
+
and light-DOM structural styles through Lightning CSS, removing comments and
|
|
48
|
+
whitespace while keeping source maps that resolve to each template. Rendered
|
|
49
|
+
styles are unchanged.
|
|
50
|
+
|
|
51
|
+
`@rcarls/rc-common` adds `cssText`, an identity tag that marks a plain CSS
|
|
52
|
+
string for minification. `rc-chip`, `rc-disclosure`, and `rc-accordion` now
|
|
53
|
+
depend on `@rcarls/rc-common` for it.
|
|
54
|
+
|
|
55
|
+
- Updated dependencies [e3dfa59]
|
|
56
|
+
- Updated dependencies [50d5610]
|
|
57
|
+
- Updated dependencies [9ee769c]
|
|
58
|
+
- Updated dependencies [1179f24]
|
|
59
|
+
- Updated dependencies [595fe37]
|
|
60
|
+
- Updated dependencies [38cd00c]
|
|
61
|
+
- Updated dependencies [5c006de]
|
|
62
|
+
- Updated dependencies [a6553ff]
|
|
63
|
+
- Updated dependencies [2205478]
|
|
64
|
+
- Updated dependencies [4341311]
|
|
65
|
+
- Updated dependencies [899bb5e]
|
|
66
|
+
- Updated dependencies [25fcfe0]
|
|
67
|
+
- Updated dependencies [51f6403]
|
|
68
|
+
- Updated dependencies [9caca6b]
|
|
69
|
+
- Updated dependencies [b764a3c]
|
|
70
|
+
- Updated dependencies [54d9ead]
|
|
71
|
+
- Updated dependencies [b9175e9]
|
|
72
|
+
- Updated dependencies [c8b762b]
|
|
73
|
+
- Updated dependencies [55e1e7f]
|
|
74
|
+
- Updated dependencies [f1ca82d]
|
|
75
|
+
- Updated dependencies [b87f27b]
|
|
76
|
+
- Updated dependencies [32ebead]
|
|
77
|
+
- Updated dependencies [bfefedd]
|
|
78
|
+
- Updated dependencies [c130173]
|
|
79
|
+
- Updated dependencies [11680b6]
|
|
80
|
+
- Updated dependencies [223d36c]
|
|
81
|
+
- Updated dependencies [7b91a44]
|
|
82
|
+
- Updated dependencies [aa04ead]
|
|
83
|
+
- Updated dependencies [ac65b5b]
|
|
84
|
+
- Updated dependencies [afcfef8]
|
|
85
|
+
- Updated dependencies [11ef1ad]
|
|
86
|
+
- @rcarls/rc-common@0.7.0
|
|
87
|
+
|
|
3
88
|
## 0.6.0
|
|
4
89
|
|
|
5
90
|
### Patch Changes
|
|
@@ -57,32 +57,32 @@
|
|
|
57
57
|
{
|
|
58
58
|
"description": "Block (top/bottom) padding inside the panel.",
|
|
59
59
|
"name": "--rc-menu-padding-block",
|
|
60
|
-
"default": "
|
|
60
|
+
"default": "var(--rc-popup-padding-block)"
|
|
61
61
|
},
|
|
62
62
|
{
|
|
63
|
-
"description": "Background color
|
|
63
|
+
"description": "Background color.",
|
|
64
64
|
"name": "--rc-menu-background",
|
|
65
|
-
"default": "
|
|
65
|
+
"default": "var(--rc-popup-background)"
|
|
66
66
|
},
|
|
67
67
|
{
|
|
68
|
-
"description": "Border
|
|
68
|
+
"description": "Border.",
|
|
69
69
|
"name": "--rc-menu-border",
|
|
70
|
-
"default": "
|
|
70
|
+
"default": "var(--rc-popup-border)"
|
|
71
71
|
},
|
|
72
72
|
{
|
|
73
73
|
"description": "Menu panel border radius.",
|
|
74
74
|
"name": "--rc-menu-radius",
|
|
75
|
-
"default": "var(--rc-
|
|
75
|
+
"default": "var(--rc-popup-radius)"
|
|
76
76
|
},
|
|
77
77
|
{
|
|
78
|
-
"description": "Box shadow
|
|
78
|
+
"description": "Box shadow.",
|
|
79
79
|
"name": "--rc-menu-shadow",
|
|
80
|
-
"default": "
|
|
80
|
+
"default": "var(--rc-popup-shadow)"
|
|
81
81
|
},
|
|
82
82
|
{
|
|
83
|
-
"description": "Text color
|
|
83
|
+
"description": "Text color.",
|
|
84
84
|
"name": "--rc-menu-color",
|
|
85
|
-
"default": "
|
|
85
|
+
"default": "var(--rc-popup-color)"
|
|
86
86
|
},
|
|
87
87
|
{
|
|
88
88
|
"description": "Viewport-aware maximum inline size.",
|
|
@@ -192,6 +192,36 @@
|
|
|
192
192
|
"description": "Text color of `[data-group-label]` section headings.",
|
|
193
193
|
"name": "--rc-menu-group-label-color",
|
|
194
194
|
"default": "GrayText"
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
"description": "Item font family; shared with listbox and adaptive-menu items.",
|
|
198
|
+
"name": "--rc-item-font-family",
|
|
199
|
+
"default": "var(--rc-font-family)"
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
"description": "Item font size; shared with listbox and adaptive-menu items.",
|
|
203
|
+
"name": "--rc-item-font-size",
|
|
204
|
+
"default": "var(--rc-font-size)"
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
"description": "Item font weight; shared with listbox and adaptive-menu items.",
|
|
208
|
+
"name": "--rc-item-font-weight",
|
|
209
|
+
"default": "inherit"
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
"description": "Item line height; shared with listbox and adaptive-menu items.",
|
|
213
|
+
"name": "--rc-item-line-height",
|
|
214
|
+
"default": "var(--rc-line-height)"
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
"description": "Item letter spacing; shared with listbox and adaptive-menu items.",
|
|
218
|
+
"name": "--rc-item-letter-spacing",
|
|
219
|
+
"default": "inherit"
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
"description": "Size of icons marked with `data-rc-icon`, `data-rc-menu-leading`, or `data-rc-menu-icon`.",
|
|
223
|
+
"name": "--rc-item-icon-size",
|
|
224
|
+
"default": "inherit"
|
|
195
225
|
}
|
|
196
226
|
],
|
|
197
227
|
"slots": [
|
|
@@ -298,7 +328,7 @@
|
|
|
298
328
|
"type": {
|
|
299
329
|
"text": "Element | null"
|
|
300
330
|
},
|
|
301
|
-
"description": "
|
|
331
|
+
"description": "the element to make active. No-op when null or undefined"
|
|
302
332
|
}
|
|
303
333
|
],
|
|
304
334
|
"description": "Move the virtual cursor to a specific item."
|
|
@@ -317,7 +347,7 @@
|
|
|
317
347
|
"type": {
|
|
318
348
|
"text": "number"
|
|
319
349
|
},
|
|
320
|
-
"description": "
|
|
350
|
+
"description": "zero-based index into the navigable items array"
|
|
321
351
|
}
|
|
322
352
|
],
|
|
323
353
|
"description": "Move the virtual cursor to the item at `index`."
|
package/dist/rc-menu-define.js
CHANGED
|
@@ -0,0 +1,182 @@
|
|
|
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 v, isFocusable as s, cssText as b } from "@rcarls/rc-common";
|
|
4
|
+
const p = u`:host{box-sizing:border-box;min-width:var(--rc-menu-min-width,10em);padding-block:var(--rc-menu-padding-block,var(--rc-popup-padding-block,var(--rc-control-padding-block,.25em)));background:var(--rc-menu-background,var(--rc-popup-background,var(--rc-surface,Canvas)));border:var(--rc-menu-border,var(--rc-popup-border,var(--rc-border,1px solid ButtonBorder)));border-radius:var(--rc-menu-radius,var(--rc-popup-radius,var(--rc-radius-sm,0)));box-shadow:var(--rc-menu-shadow,var(--rc-popup-shadow,var(--rc-shadow,0 2px 8px color-mix(in srgb, CanvasText 15%, transparent))));color:var(--rc-menu-color,var(--rc-popup-color,var(--rc-field-text,CanvasText)));font-family:var(--rc-item-font-family,var(--rc-font-family,inherit));font-size:var(--rc-item-font-size,var(--rc-font-size,inherit));font-weight:var(--rc-item-font-weight,inherit);line-height:var(--rc-item-line-height,var(--rc-line-height,normal));letter-spacing:var(--rc-item-letter-spacing,inherit);max-inline-size:var(--rc-menu-max-inline-size,calc(100dvi - .5rem));max-block-size:var(--rc-menu-max-block-size,calc(100dvb - .5rem));overscroll-behavior:contain;scrollbar-gutter:stable;flex-direction:column;display:flex;overflow:auto}:host(:focus-visible){outline:var(--rc-focus-ring,auto);outline-offset:var(--rc-focus-ring-offset,0)}`;
|
|
5
|
+
var g = Object.defineProperty, f = (c, e, t, r) => {
|
|
6
|
+
for (var n = void 0, i = c.length - 1, o; i >= 0; i--)
|
|
7
|
+
(o = c[i]) && (n = o(e, t, n) || n);
|
|
8
|
+
return n && g(e, t, n), n;
|
|
9
|
+
};
|
|
10
|
+
const k = ["menuitem", "menuitemcheckbox", "menuitemradio"], x = b`@layer rc-base{rc-menu [role=menuitem],rc-menu [role=menuitemcheckbox],rc-menu [role=menuitemradio],rc-menu button:disabled{align-items:center;gap:var(--rc-menu-item-gap,var(--rc-item-gap,.5em));min-block-size:var(--rc-menu-item-min-block-size,0px);text-align:start;width:100%;padding:var(--rc-menu-item-padding-block,var(--rc-item-padding-block,.3em)) var(--rc-menu-item-padding-inline,var(--rc-item-padding-inline,.75em));appearance:none;box-sizing:border-box;color:inherit;font:inherit;cursor:default;user-select:none;transition:var(--rc-menu-item-transition);background:0 0;border:none;outline:none;flex-shrink:0;text-decoration:none;display:flex}rc-menu [role=menuitem]:hover,rc-menu [role=menuitemcheckbox]:hover,rc-menu [role=menuitemradio]:hover{background:var(--rc-menu-hover-bg,color-mix(in srgb, Highlight 8%, transparent));color:var(--rc-menu-hover-color,inherit)}rc-menu [role=menuitem][data-active],rc-menu [role=menuitemcheckbox][data-active],rc-menu [role=menuitemradio][data-active]{background:var(--rc-menu-active-bg,color-mix(in srgb, Highlight 8%, transparent));color:var(--rc-menu-active-color,inherit)}rc-menu [role=menuitemcheckbox]:after,rc-menu [role=menuitemradio]:after{content:"";flex:0 0 var(--rc-menu-check-size,1.25em);text-align:center;color:var(--rc-menu-check-color,inherit);margin-inline-start:auto;line-height:1}rc-menu [role=menuitemcheckbox]:has(:is([data-rc-menu-shortcut],[data-menu-shortcut])):after,rc-menu [role=menuitemradio]:has(:is([data-rc-menu-shortcut],[data-menu-shortcut])):after{margin-inline-start:0}rc-menu [role=menuitemcheckbox][aria-checked=true]:after{content:"✓"}rc-menu [role=menuitemcheckbox][aria-checked=mixed]:after{content:"−"}rc-menu [role=menuitemradio][aria-checked=true]:after{content:"•"}rc-menu [data-rc-menu-leading],rc-menu [data-rc-menu-trailing]{flex:none;justify-content:center;align-items:center;display:inline-flex}rc-menu :is([role=menuitem],[role=menuitemcheckbox],[role=menuitemradio]) :is([data-rc-icon],[data-rc-menu-leading],[data-rc-menu-icon]){font-size:var(--rc-item-icon-size,inherit);line-height:var(--rc-item-icon-size,inherit);flex:none}rc-menu [data-rc-menu-label]{flex:auto;min-inline-size:0}rc-menu [data-rc-menu-trailing],rc-menu [data-rc-menu-shortcut],rc-menu [data-menu-shortcut]{margin-inline-start:auto}rc-menu [data-rc-menu-shortcut],rc-menu [data-menu-shortcut]{font-size:var(--rc-menu-shortcut-size,.8em);color:var(--rc-menu-shortcut-color,GrayText);white-space:nowrap}rc-menu [role=separator],rc-menu hr{border:none;border-block-start:var(--rc-menu-separator-border,var(--rc-border,1px solid ButtonBorder));margin-block:var(--rc-menu-separator-margin-block,var(--rc-control-gap,.25em));margin-inline:var(--rc-menu-separator-margin-inline,0px)}rc-menu [role=group]{flex-direction:column;align-items:stretch;display:flex}rc-menu [data-group-label]{padding-block:var(--rc-menu-group-label-padding-block,.1em);padding-inline:var(--rc-menu-item-padding-inline,.75em);color:var(--rc-menu-group-label-color,GrayText);user-select:none;pointer-events:none}rc-menu button:disabled,rc-menu [aria-disabled=true]{color:var(--rc-menu-disabled-color,var(--rc-text-disabled,GrayText));opacity:var(--rc-menu-disabled-opacity,var(--rc-disabled-opacity,.55));pointer-events:none}}`;
|
|
11
|
+
var a;
|
|
12
|
+
const l = (a = class extends m {
|
|
13
|
+
constructor() {
|
|
14
|
+
super(...arguments), this.label = "", this._$menuItems = [], this._activeDescendantCtrl = new v(this, {
|
|
15
|
+
host: () => this,
|
|
16
|
+
items: () => this._$menuItems
|
|
17
|
+
}), this._onKeydown = (e) => {
|
|
18
|
+
switch (e.key) {
|
|
19
|
+
case "ArrowDown": {
|
|
20
|
+
e.preventDefault(), e.stopPropagation(), this._activeDescendantCtrl.activeItem ? this._activeDescendantCtrl.navigate(1) : this._activeDescendantCtrl.navigateToFirst();
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
case "ArrowUp": {
|
|
24
|
+
e.preventDefault(), e.stopPropagation(), this._activeDescendantCtrl.activeItem ? this._activeDescendantCtrl.navigate(-1) : this._activeDescendantCtrl.navigateToLast();
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
case "Home": {
|
|
28
|
+
e.preventDefault(), e.stopPropagation(), this._activeDescendantCtrl.navigateToFirst();
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
case "End": {
|
|
32
|
+
e.preventDefault(), e.stopPropagation(), this._activeDescendantCtrl.navigateToLast();
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
case "Enter":
|
|
36
|
+
// Falls through so both activation keys share the same handler.
|
|
37
|
+
case " ": {
|
|
38
|
+
e.preventDefault(), e.stopPropagation();
|
|
39
|
+
const t = this._activeDescendantCtrl.activeItem;
|
|
40
|
+
t && t.click();
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
case "Escape": {
|
|
44
|
+
e.preventDefault(), e.stopPropagation(), this.dispatchEvent(
|
|
45
|
+
new CustomEvent("rc-menu-close", {
|
|
46
|
+
bubbles: !0,
|
|
47
|
+
composed: !0,
|
|
48
|
+
detail: { reason: "escape" }
|
|
49
|
+
})
|
|
50
|
+
);
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}, this._onBlur = () => {
|
|
55
|
+
this._activeDescendantCtrl.clear();
|
|
56
|
+
}, this._onClick = (e) => {
|
|
57
|
+
const r = e.target.closest(
|
|
58
|
+
'[role="menuitem"],[role="menuitemcheckbox"],[role="menuitemradio"]'
|
|
59
|
+
);
|
|
60
|
+
if (!r || !this.contains(r) || r.matches(':disabled, [aria-disabled="true"]'))
|
|
61
|
+
return;
|
|
62
|
+
const n = this._applyCheckedState(r);
|
|
63
|
+
this.dispatchEvent(
|
|
64
|
+
new CustomEvent("rc-menu-activate", {
|
|
65
|
+
bubbles: !0,
|
|
66
|
+
composed: !0,
|
|
67
|
+
detail: {
|
|
68
|
+
item: r,
|
|
69
|
+
value: r.dataset.value ?? r.getAttribute("value") ?? "",
|
|
70
|
+
text: r.textContent?.trim() ?? "",
|
|
71
|
+
checked: n
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
);
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
static _ensureBaseStyles(e) {
|
|
78
|
+
if (a._$styledRoots.has(e))
|
|
79
|
+
return;
|
|
80
|
+
a._$styledRoots.add(e);
|
|
81
|
+
const t = document.createElement("style");
|
|
82
|
+
t.setAttribute("data-rc-light-dom-base", "rc-menu"), t.textContent = x, e instanceof Document ? e.head.appendChild(t) : e.appendChild(t);
|
|
83
|
+
}
|
|
84
|
+
connectedCallback() {
|
|
85
|
+
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());
|
|
86
|
+
}
|
|
87
|
+
disconnectedCallback() {
|
|
88
|
+
super.disconnectedCallback(), this.removeEventListener("keydown", this._onKeydown), this.removeEventListener("blur", this._onBlur), this.removeEventListener("click", this._onClick);
|
|
89
|
+
}
|
|
90
|
+
updated(e) {
|
|
91
|
+
e.has("label") && this._syncAriaLabel();
|
|
92
|
+
}
|
|
93
|
+
render() {
|
|
94
|
+
return d`<slot @slotchange=${this._onSlotChange}></slot>`;
|
|
95
|
+
}
|
|
96
|
+
/** All navigable menu item elements in document order. */
|
|
97
|
+
get items() {
|
|
98
|
+
return this._$menuItems;
|
|
99
|
+
}
|
|
100
|
+
/** Move the virtual cursor and DOM focus to the first navigable item. */
|
|
101
|
+
focusFirst() {
|
|
102
|
+
this.focus(), this._activeDescendantCtrl.navigateToFirst();
|
|
103
|
+
}
|
|
104
|
+
/** Move the virtual cursor and DOM focus to the last navigable item. */
|
|
105
|
+
focusLast() {
|
|
106
|
+
this.focus(), this._activeDescendantCtrl.navigateToLast();
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Move the virtual cursor to a specific item.
|
|
110
|
+
*
|
|
111
|
+
* @param $item - the element to make active. No-op when null or undefined
|
|
112
|
+
*/
|
|
113
|
+
focusItem(e) {
|
|
114
|
+
e && (this.focus(), this._activeDescendantCtrl.navigateToItem(e));
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Move the virtual cursor to the item at `index`.
|
|
118
|
+
*
|
|
119
|
+
* @param index - zero-based index into the navigable items array
|
|
120
|
+
*/
|
|
121
|
+
focusItemAt(e) {
|
|
122
|
+
this.focus(), this._activeDescendantCtrl.navigateTo(e);
|
|
123
|
+
}
|
|
124
|
+
_syncAriaLabel() {
|
|
125
|
+
this.label ? this.setAttribute("aria-label", this.label) : this.removeAttribute("aria-label");
|
|
126
|
+
}
|
|
127
|
+
_onSlotChange(e) {
|
|
128
|
+
const t = e.currentTarget;
|
|
129
|
+
this._$menuItems = this._collectMenuItems(t), this._initMenuItems();
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Collects navigable menu items from the slot, supporting `role="group"`
|
|
133
|
+
* containers and skipping `role="separator"` elements.
|
|
134
|
+
*
|
|
135
|
+
* Direct focusable children are included as-is. Elements with
|
|
136
|
+
* `role="group"` are traversed one level deep so their focusable children
|
|
137
|
+
* participate in arrow-key navigation. Separators and non-focusable
|
|
138
|
+
* container elements are excluded.
|
|
139
|
+
*/
|
|
140
|
+
_collectMenuItems(e) {
|
|
141
|
+
const t = [];
|
|
142
|
+
for (const r of e.assignedElements()) {
|
|
143
|
+
const n = r.getAttribute("role");
|
|
144
|
+
if (n !== "separator") {
|
|
145
|
+
if (n === "group") {
|
|
146
|
+
for (const i of r.children)
|
|
147
|
+
s(i) && t.push(i);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
s(r) && t.push(r);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return t;
|
|
154
|
+
}
|
|
155
|
+
_initMenuItems() {
|
|
156
|
+
this._$menuItems.forEach((e) => {
|
|
157
|
+
const t = e.getAttribute("role");
|
|
158
|
+
(!t || !k.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");
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
_applyCheckedState(e) {
|
|
162
|
+
const t = e.getAttribute("role");
|
|
163
|
+
if (t === "menuitemcheckbox") {
|
|
164
|
+
const r = e.getAttribute("aria-checked") === "true" ? "false" : "true";
|
|
165
|
+
return e.setAttribute("aria-checked", r), r;
|
|
166
|
+
}
|
|
167
|
+
if (t === "menuitemradio") {
|
|
168
|
+
const r = e.closest('[role="group"]'), n = r && this.contains(r) ? r : this;
|
|
169
|
+
for (const i of n.querySelectorAll('[role="menuitemradio"]'))
|
|
170
|
+
i.closest("rc-menu") === this && i.setAttribute("aria-checked", i === e ? "true" : "false");
|
|
171
|
+
return "true";
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}, a.styles = [p], a._$styledRoots = /* @__PURE__ */ new Set(), a);
|
|
175
|
+
f([
|
|
176
|
+
h({ type: String })
|
|
177
|
+
], l.prototype, "label");
|
|
178
|
+
let A = l;
|
|
179
|
+
export {
|
|
180
|
+
A as R
|
|
181
|
+
};
|
|
182
|
+
//# sourceMappingURL=rc-menu-zM-Q3-uP.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rc-menu-zM-Q3-uP.js","sources":["../src/rc-menu.styles.ts","../src/rc-menu.ts"],"sourcesContent":["import { css } from 'lit';\n\nexport const menuStyles = css`\n :host {\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n min-width: var(--rc-menu-min-width, 10em);\n padding-block: var(\n --rc-menu-padding-block,\n var(--rc-popup-padding-block, var(--rc-control-padding-block, 0.25em))\n );\n background: var(--rc-menu-background, var(--rc-popup-background, var(--rc-surface, Canvas)));\n border: var(--rc-menu-border, var(--rc-popup-border, var(--rc-border, 1px solid ButtonBorder)));\n border-radius: var(--rc-menu-radius, var(--rc-popup-radius, var(--rc-radius-sm, 0)));\n box-shadow: var(\n --rc-menu-shadow,\n var(\n --rc-popup-shadow,\n var(--rc-shadow, 0 2px 8px color-mix(in srgb, CanvasText 15%, transparent))\n )\n );\n color: var(--rc-menu-color, var(--rc-popup-color, var(--rc-field-text, CanvasText)));\n font-family: var(--rc-item-font-family, var(--rc-font-family, inherit));\n font-size: var(--rc-item-font-size, var(--rc-font-size, inherit));\n font-weight: var(--rc-item-font-weight, inherit);\n line-height: var(--rc-item-line-height, var(--rc-line-height, normal));\n letter-spacing: var(--rc-item-letter-spacing, inherit);\n max-inline-size: var(--rc-menu-max-inline-size, calc(100dvi - 0.5rem));\n max-block-size: var(--rc-menu-max-block-size, calc(100dvb - 0.5rem));\n overflow: auto;\n overscroll-behavior: contain;\n scrollbar-gutter: stable;\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, cssText, isFocusable } from '@rcarls/rc-common';\n\nimport menuStyles from './rc-menu.styles.js';\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 = cssText`\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 flex-shrink: 0;\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 — ::after acts as a fixed-width trailing column,\n pushed to the item's inline-end regardless of label length (same\n positioning as the [data-rc-menu-shortcut] slot below). Trailing rather\n than leading so it coexists with a leading icon a consumer supplies for\n the item itself (a radio/checkbox item's own icon and its checked state\n are different things; leading is already spoken for). Content is empty\n (unchecked) or a Unicode symbol (checked/mixed). Semantic state is\n conveyed by aria-checked; the visual symbol is decorative. */\n rc-menu [role='menuitemcheckbox']::after,\n rc-menu [role='menuitemradio']::after {\n content: '';\n flex: 0 0 var(--rc-menu-check-size, 1.25em);\n margin-inline-start: auto;\n text-align: center;\n line-height: 1;\n color: var(--rc-menu-check-color, inherit);\n }\n\n /* A shortcut owns the auto margin when both trailing affordances exist,\n keeping the shortcut and selection indicator in one end-aligned group. */\n rc-menu [role='menuitemcheckbox']:has(:is([data-rc-menu-shortcut], [data-menu-shortcut]))::after,\n rc-menu [role='menuitemradio']:has(:is([data-rc-menu-shortcut], [data-menu-shortcut]))::after {\n margin-inline-start: 0;\n }\n\n rc-menu [role='menuitemcheckbox'][aria-checked='true']::after {\n content: '✓';\n }\n\n rc-menu [role='menuitemcheckbox'][aria-checked='mixed']::after {\n content: '−';\n }\n\n rc-menu [role='menuitemradio'][aria-checked='true']::after {\n content: '•';\n }\n\n rc-menu [data-rc-menu-leading],\n rc-menu [data-rc-menu-trailing] {\n flex: none;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n }\n\n rc-menu :is([role='menuitem'], [role='menuitemcheckbox'], [role='menuitemradio'])\n :is([data-rc-icon], [data-rc-menu-leading], [data-rc-menu-icon]) {\n flex: none;\n font-size: var(--rc-item-icon-size, inherit);\n line-height: var(--rc-item-icon-size, inherit);\n }\n\n rc-menu [data-rc-menu-label] {\n flex: 1 1 auto;\n min-inline-size: 0;\n }\n\n /* Trailing label — keyboard shortcut or hint, pushed to the inline-end of the item */\n rc-menu [data-rc-menu-trailing],\n rc-menu [data-rc-menu-shortcut],\n rc-menu [data-menu-shortcut] {\n margin-inline-start: auto;\n }\n\n rc-menu [data-rc-menu-shortcut],\n rc-menu [data-menu-shortcut] {\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 /* 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 trailing indicator column via `::after`\n * (coexists with a leading icon the item supplies itself); `aria-checked` is initialized when missing and\n * 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, use `[data-rc-menu-leading]`, `[data-rc-menu-label]`, and\n * `[data-rc-menu-trailing]` to identify content columns. `[data-rc-menu-shortcut]`\n * (and the legacy `[data-menu-shortcut]`) is pushed to the inline-end edge.\n * - Nested menu triggers supply their own trailing indicator through `rc-menu-button`'s `indicator` slot.\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 * @attr label - Accessible label for the menu; synced to `aria-label` on the host.\n *\n * @cssprop [--rc-menu-min-width=10em] - Minimum width of the menu panel.\n * @cssprop [--rc-menu-padding-block=var(--rc-popup-padding-block)] - Block (top/bottom) padding inside the panel.\n * @cssprop [--rc-menu-background=var(--rc-popup-background)] - Background color.\n * @cssprop [--rc-menu-border=var(--rc-popup-border)] - Border.\n * @cssprop [--rc-menu-radius=var(--rc-popup-radius)] - Menu panel border radius.\n * @cssprop [--rc-menu-shadow=var(--rc-popup-shadow)] - Box shadow.\n * @cssprop [--rc-menu-color=var(--rc-popup-color)] - Text color.\n * @cssprop [--rc-menu-max-inline-size=calc(100dvi - 0.5rem)] - Viewport-aware maximum inline size.\n * @cssprop [--rc-menu-max-block-size=calc(100dvb - 0.5rem)] - Viewport-aware maximum block size; taller menus scroll.\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 trailing 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-rc-menu-shortcut]` labels.\n * @cssprop [--rc-menu-shortcut-color=GrayText] - Text color of `[data-rc-menu-shortcut]` labels.\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 * @cssprop [--rc-item-font-family=var(--rc-font-family)] - Item font family; shared with listbox and adaptive-menu items.\n * @cssprop [--rc-item-font-size=var(--rc-font-size)] - Item font size; shared with listbox and adaptive-menu items.\n * @cssprop [--rc-item-font-weight=inherit] - Item font weight; shared with listbox and adaptive-menu items.\n * @cssprop [--rc-item-line-height=var(--rc-line-height)] - Item line height; shared with listbox and adaptive-menu items.\n * @cssprop [--rc-item-letter-spacing=inherit] - Item letter spacing; shared with listbox and adaptive-menu items.\n * @cssprop [--rc-item-icon-size=inherit] - Size of icons marked with `data-rc-icon`, `data-rc-menu-leading`, or `data-rc-menu-icon`.\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) {\n return;\n }\n\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') {\n continue;\n }\n\n if (role === 'group') {\n for (const $child of $el.children) {\n if (isFocusable($child)) {\n $items.push($child);\n }\n }\n\n continue;\n }\n\n if (isFocusable($el)) {\n $items.push($el);\n }\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 // Falls through so both activation keys share the same handler.\n\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: $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","cssText","_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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"}
|
package/dist/rc-menu.js
CHANGED
|
@@ -43,12 +43,12 @@ export interface RCMenuActivateEvent {
|
|
|
43
43
|
* @attr label - Accessible label for the menu; synced to `aria-label` on the host.
|
|
44
44
|
*
|
|
45
45
|
* @cssprop [--rc-menu-min-width=10em] - Minimum width of the menu panel.
|
|
46
|
-
* @cssprop [--rc-menu-padding-block=
|
|
47
|
-
* @cssprop [--rc-menu-background=
|
|
48
|
-
* @cssprop [--rc-menu-border=
|
|
49
|
-
* @cssprop [--rc-menu-radius=var(--rc-
|
|
50
|
-
* @cssprop [--rc-menu-shadow=
|
|
51
|
-
* @cssprop [--rc-menu-color=
|
|
46
|
+
* @cssprop [--rc-menu-padding-block=var(--rc-popup-padding-block)] - Block (top/bottom) padding inside the panel.
|
|
47
|
+
* @cssprop [--rc-menu-background=var(--rc-popup-background)] - Background color.
|
|
48
|
+
* @cssprop [--rc-menu-border=var(--rc-popup-border)] - Border.
|
|
49
|
+
* @cssprop [--rc-menu-radius=var(--rc-popup-radius)] - Menu panel border radius.
|
|
50
|
+
* @cssprop [--rc-menu-shadow=var(--rc-popup-shadow)] - Box shadow.
|
|
51
|
+
* @cssprop [--rc-menu-color=var(--rc-popup-color)] - Text color.
|
|
52
52
|
* @cssprop [--rc-menu-max-inline-size=calc(100dvi - 0.5rem)] - Viewport-aware maximum inline size.
|
|
53
53
|
* @cssprop [--rc-menu-max-block-size=calc(100dvb - 0.5rem)] - Viewport-aware maximum block size; taller menus scroll.
|
|
54
54
|
* @cssprop [--rc-menu-item-min-block-size=0] - Minimum block size for menu item rows.
|
|
@@ -71,6 +71,12 @@ export interface RCMenuActivateEvent {
|
|
|
71
71
|
* @cssprop [--rc-menu-separator-margin-inline=0px] - Separator inline margin; set to match item padding for inset separators.
|
|
72
72
|
* @cssprop [--rc-menu-group-label-padding-block=0.1em] - Block padding on `[data-group-label]` section headings.
|
|
73
73
|
* @cssprop [--rc-menu-group-label-color=GrayText] - Text color of `[data-group-label]` section headings.
|
|
74
|
+
* @cssprop [--rc-item-font-family=var(--rc-font-family)] - Item font family; shared with listbox and adaptive-menu items.
|
|
75
|
+
* @cssprop [--rc-item-font-size=var(--rc-font-size)] - Item font size; shared with listbox and adaptive-menu items.
|
|
76
|
+
* @cssprop [--rc-item-font-weight=inherit] - Item font weight; shared with listbox and adaptive-menu items.
|
|
77
|
+
* @cssprop [--rc-item-line-height=var(--rc-line-height)] - Item line height; shared with listbox and adaptive-menu items.
|
|
78
|
+
* @cssprop [--rc-item-letter-spacing=inherit] - Item letter spacing; shared with listbox and adaptive-menu items.
|
|
79
|
+
* @cssprop [--rc-item-icon-size=inherit] - Size of icons marked with `data-rc-icon`, `data-rc-menu-leading`, or `data-rc-menu-icon`.
|
|
74
80
|
*/
|
|
75
81
|
export declare class RCMenu extends LitElement {
|
|
76
82
|
static styles: import('lit').CSSResult[];
|
|
@@ -87,7 +93,7 @@ export declare class RCMenu extends LitElement {
|
|
|
87
93
|
connectedCallback(): void;
|
|
88
94
|
disconnectedCallback(): void;
|
|
89
95
|
protected updated(changed: Map<PropertyKey, unknown>): void;
|
|
90
|
-
protected render(): import('lit').TemplateResult<1>;
|
|
96
|
+
protected render(): import('lit-html').TemplateResult<1>;
|
|
91
97
|
/** All navigable menu item elements in document order. */
|
|
92
98
|
get items(): readonly Element[];
|
|
93
99
|
/** Move the virtual cursor and DOM focus to the first navigable item. */
|
|
@@ -97,13 +103,13 @@ export declare class RCMenu extends LitElement {
|
|
|
97
103
|
/**
|
|
98
104
|
* Move the virtual cursor to a specific item.
|
|
99
105
|
*
|
|
100
|
-
* @param $item -
|
|
106
|
+
* @param $item - the element to make active. No-op when null or undefined
|
|
101
107
|
*/
|
|
102
108
|
focusItem($item?: Element | null): void;
|
|
103
109
|
/**
|
|
104
110
|
* Move the virtual cursor to the item at `index`.
|
|
105
111
|
*
|
|
106
|
-
* @param index -
|
|
112
|
+
* @param index - zero-based index into the navigable items array
|
|
107
113
|
*/
|
|
108
114
|
focusItemAt(index: number): void;
|
|
109
115
|
protected _syncAriaLabel(): void;
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.7.0",
|
|
7
7
|
"description": "Menu popup for command surfaces with keyboard navigation and typed activation events.",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"test:browser:firefox": "vitest --run --project=firefox"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@rcarls/rc-common": "0.
|
|
48
|
+
"@rcarls/rc-common": "0.7.0"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@custom-elements-manifest/analyzer": "0.11.0",
|
package/dist/rc-menu-Df7GAw1H.js
DELETED
|
@@ -1,353 +0,0 @@
|
|
|
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
|
-
box-sizing: border-box;
|
|
7
|
-
display: flex;
|
|
8
|
-
flex-direction: column;
|
|
9
|
-
min-width: var(--rc-menu-min-width, 10em);
|
|
10
|
-
padding-block: var(--rc-menu-padding-block, var(--rc-control-padding-block, 0.25em));
|
|
11
|
-
background: var(--rc-menu-background, var(--rc-surface, Canvas));
|
|
12
|
-
border: var(--rc-menu-border, var(--rc-border, 1px solid ButtonBorder));
|
|
13
|
-
border-radius: var(--rc-menu-radius, var(--rc-control-radius, 0));
|
|
14
|
-
box-shadow: var(
|
|
15
|
-
--rc-menu-shadow,
|
|
16
|
-
var(--rc-shadow, 0 2px 8px color-mix(in srgb, CanvasText 15%, transparent))
|
|
17
|
-
);
|
|
18
|
-
color: var(--rc-menu-color, var(--rc-field-text, CanvasText));
|
|
19
|
-
font-family: var(--rc-font-family, inherit);
|
|
20
|
-
font-size: var(--rc-font-size, inherit);
|
|
21
|
-
line-height: var(--rc-line-height, normal);
|
|
22
|
-
max-inline-size: var(--rc-menu-max-inline-size, calc(100dvi - 0.5rem));
|
|
23
|
-
max-block-size: var(--rc-menu-max-block-size, calc(100dvb - 0.5rem));
|
|
24
|
-
overflow: auto;
|
|
25
|
-
overscroll-behavior: contain;
|
|
26
|
-
scrollbar-gutter: stable;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
:host(:focus-visible) {
|
|
30
|
-
outline: var(--rc-focus-ring, auto);
|
|
31
|
-
outline-offset: var(--rc-focus-ring-offset, 0);
|
|
32
|
-
}
|
|
33
|
-
`;
|
|
34
|
-
var p = Object.defineProperty, g = (o, e, t, r) => {
|
|
35
|
-
for (var n = void 0, i = o.length - 1, c; i >= 0; i--)
|
|
36
|
-
(c = o[i]) && (n = c(e, t, n) || n);
|
|
37
|
-
return n && p(e, t, n), n;
|
|
38
|
-
};
|
|
39
|
-
const f = ["menuitem", "menuitemcheckbox", "menuitemradio"], k = `
|
|
40
|
-
@layer rc-base {
|
|
41
|
-
/* Item base layout */
|
|
42
|
-
rc-menu [role='menuitem'],
|
|
43
|
-
rc-menu [role='menuitemcheckbox'],
|
|
44
|
-
rc-menu [role='menuitemradio'],
|
|
45
|
-
rc-menu button:disabled {
|
|
46
|
-
display: flex;
|
|
47
|
-
flex-shrink: 0;
|
|
48
|
-
align-items: center;
|
|
49
|
-
gap: var(--rc-menu-item-gap, var(--rc-item-gap, 0.5em));
|
|
50
|
-
min-block-size: var(--rc-menu-item-min-block-size, 0px);
|
|
51
|
-
width: 100%;
|
|
52
|
-
text-align: start;
|
|
53
|
-
padding: var(--rc-menu-item-padding-block, var(--rc-item-padding-block, 0.3em))
|
|
54
|
-
var(--rc-menu-item-padding-inline, var(--rc-item-padding-inline, 0.75em));
|
|
55
|
-
appearance: none;
|
|
56
|
-
box-sizing: border-box;
|
|
57
|
-
border: none;
|
|
58
|
-
background: transparent;
|
|
59
|
-
text-decoration: none;
|
|
60
|
-
color: inherit;
|
|
61
|
-
font: inherit;
|
|
62
|
-
cursor: default;
|
|
63
|
-
user-select: none;
|
|
64
|
-
outline: none;
|
|
65
|
-
transition: var(--rc-menu-item-transition);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/* Pointer hover */
|
|
69
|
-
rc-menu [role='menuitem']:hover,
|
|
70
|
-
rc-menu [role='menuitemcheckbox']:hover,
|
|
71
|
-
rc-menu [role='menuitemradio']:hover {
|
|
72
|
-
background: var(--rc-menu-hover-bg, color-mix(in srgb, Highlight 8%, transparent));
|
|
73
|
-
color: var(--rc-menu-hover-color, inherit);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/* Virtual cursor (set by ActiveDescendantController) */
|
|
77
|
-
rc-menu [role='menuitem'][data-active],
|
|
78
|
-
rc-menu [role='menuitemcheckbox'][data-active],
|
|
79
|
-
rc-menu [role='menuitemradio'][data-active] {
|
|
80
|
-
background: var(--rc-menu-active-bg, color-mix(in srgb, Highlight 8%, transparent));
|
|
81
|
-
color: var(--rc-menu-active-color, inherit);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/* Check/radio indicator — ::after acts as a fixed-width trailing column,
|
|
85
|
-
pushed to the item's inline-end regardless of label length (same
|
|
86
|
-
positioning as the [data-rc-menu-shortcut] slot below). Trailing rather
|
|
87
|
-
than leading so it coexists with a leading icon a consumer supplies for
|
|
88
|
-
the item itself (a radio/checkbox item's own icon and its checked state
|
|
89
|
-
are different things; leading is already spoken for). Content is empty
|
|
90
|
-
(unchecked) or a Unicode symbol (checked/mixed). Semantic state is
|
|
91
|
-
conveyed by aria-checked; the visual symbol is decorative. */
|
|
92
|
-
rc-menu [role='menuitemcheckbox']::after,
|
|
93
|
-
rc-menu [role='menuitemradio']::after {
|
|
94
|
-
content: '';
|
|
95
|
-
flex: 0 0 var(--rc-menu-check-size, 1.25em);
|
|
96
|
-
margin-inline-start: auto;
|
|
97
|
-
text-align: center;
|
|
98
|
-
line-height: 1;
|
|
99
|
-
color: var(--rc-menu-check-color, inherit);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/* A shortcut owns the auto margin when both trailing affordances exist,
|
|
103
|
-
keeping the shortcut and selection indicator in one end-aligned group. */
|
|
104
|
-
rc-menu [role='menuitemcheckbox']:has(:is([data-rc-menu-shortcut], [data-menu-shortcut]))::after,
|
|
105
|
-
rc-menu [role='menuitemradio']:has(:is([data-rc-menu-shortcut], [data-menu-shortcut]))::after {
|
|
106
|
-
margin-inline-start: 0;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
rc-menu [role='menuitemcheckbox'][aria-checked='true']::after {
|
|
110
|
-
content: '✓';
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
rc-menu [role='menuitemcheckbox'][aria-checked='mixed']::after {
|
|
114
|
-
content: '−';
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
rc-menu [role='menuitemradio'][aria-checked='true']::after {
|
|
118
|
-
content: '•';
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
rc-menu [data-rc-menu-leading],
|
|
122
|
-
rc-menu [data-rc-menu-trailing] {
|
|
123
|
-
flex: none;
|
|
124
|
-
display: inline-flex;
|
|
125
|
-
align-items: center;
|
|
126
|
-
justify-content: center;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
rc-menu [data-rc-menu-label] {
|
|
130
|
-
flex: 1 1 auto;
|
|
131
|
-
min-inline-size: 0;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/* Trailing label — keyboard shortcut or hint, pushed to the inline-end of the item */
|
|
135
|
-
rc-menu [data-rc-menu-trailing],
|
|
136
|
-
rc-menu [data-rc-menu-shortcut],
|
|
137
|
-
rc-menu [data-menu-shortcut] {
|
|
138
|
-
margin-inline-start: auto;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
rc-menu [data-rc-menu-shortcut],
|
|
142
|
-
rc-menu [data-menu-shortcut] {
|
|
143
|
-
font-size: var(--rc-menu-shortcut-size, 0.8em);
|
|
144
|
-
color: var(--rc-menu-shortcut-color, GrayText);
|
|
145
|
-
white-space: nowrap;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/* Separators */
|
|
149
|
-
rc-menu [role='separator'],
|
|
150
|
-
rc-menu hr {
|
|
151
|
-
border: none;
|
|
152
|
-
border-block-start: var(--rc-menu-separator-border, var(--rc-border, 1px solid ButtonBorder));
|
|
153
|
-
margin-block: var(--rc-menu-separator-margin-block, var(--rc-control-gap, 0.25em));
|
|
154
|
-
margin-inline: var(--rc-menu-separator-margin-inline, 0px);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/* Group containers */
|
|
158
|
-
rc-menu [role='group'] {
|
|
159
|
-
display: flex;
|
|
160
|
-
flex-direction: column;
|
|
161
|
-
align-items: stretch;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/* Optional section label (non-interactive heading above a group of items) */
|
|
165
|
-
rc-menu [data-group-label] {
|
|
166
|
-
padding-block: var(--rc-menu-group-label-padding-block, 0.1em);
|
|
167
|
-
padding-inline: var(--rc-menu-item-padding-inline, 0.75em);
|
|
168
|
-
color: var(--rc-menu-group-label-color, GrayText);
|
|
169
|
-
user-select: none;
|
|
170
|
-
pointer-events: none;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/* Disabled items */
|
|
174
|
-
rc-menu button:disabled,
|
|
175
|
-
rc-menu [aria-disabled='true'] {
|
|
176
|
-
color: var(--rc-menu-disabled-color, var(--rc-text-disabled, GrayText));
|
|
177
|
-
opacity: var(--rc-menu-disabled-opacity, var(--rc-disabled-opacity, 0.55));
|
|
178
|
-
pointer-events: none;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
`;
|
|
182
|
-
var a;
|
|
183
|
-
const l = (a = class extends m {
|
|
184
|
-
constructor() {
|
|
185
|
-
super(...arguments), this.label = "", this._$menuItems = [], this._activeDescendantCtrl = new b(this, {
|
|
186
|
-
host: () => this,
|
|
187
|
-
items: () => this._$menuItems
|
|
188
|
-
}), this._onKeydown = (e) => {
|
|
189
|
-
switch (e.key) {
|
|
190
|
-
case "ArrowDown": {
|
|
191
|
-
e.preventDefault(), e.stopPropagation(), this._activeDescendantCtrl.activeItem ? this._activeDescendantCtrl.navigate(1) : this._activeDescendantCtrl.navigateToFirst();
|
|
192
|
-
break;
|
|
193
|
-
}
|
|
194
|
-
case "ArrowUp": {
|
|
195
|
-
e.preventDefault(), e.stopPropagation(), this._activeDescendantCtrl.activeItem ? this._activeDescendantCtrl.navigate(-1) : this._activeDescendantCtrl.navigateToLast();
|
|
196
|
-
break;
|
|
197
|
-
}
|
|
198
|
-
case "Home": {
|
|
199
|
-
e.preventDefault(), e.stopPropagation(), this._activeDescendantCtrl.navigateToFirst();
|
|
200
|
-
break;
|
|
201
|
-
}
|
|
202
|
-
case "End": {
|
|
203
|
-
e.preventDefault(), e.stopPropagation(), this._activeDescendantCtrl.navigateToLast();
|
|
204
|
-
break;
|
|
205
|
-
}
|
|
206
|
-
case "Enter":
|
|
207
|
-
// Falls through so both activation keys share the same handler.
|
|
208
|
-
case " ": {
|
|
209
|
-
e.preventDefault(), e.stopPropagation();
|
|
210
|
-
const t = this._activeDescendantCtrl.activeItem;
|
|
211
|
-
t && t.click();
|
|
212
|
-
break;
|
|
213
|
-
}
|
|
214
|
-
case "Escape": {
|
|
215
|
-
e.preventDefault(), e.stopPropagation(), this.dispatchEvent(
|
|
216
|
-
new CustomEvent("rc-menu-close", {
|
|
217
|
-
bubbles: !0,
|
|
218
|
-
composed: !0,
|
|
219
|
-
detail: { reason: "escape" }
|
|
220
|
-
})
|
|
221
|
-
);
|
|
222
|
-
break;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
}, this._onBlur = () => {
|
|
226
|
-
this._activeDescendantCtrl.clear();
|
|
227
|
-
}, this._onClick = (e) => {
|
|
228
|
-
const r = e.target.closest(
|
|
229
|
-
'[role="menuitem"],[role="menuitemcheckbox"],[role="menuitemradio"]'
|
|
230
|
-
);
|
|
231
|
-
if (!r || !this.contains(r) || r.matches(':disabled, [aria-disabled="true"]'))
|
|
232
|
-
return;
|
|
233
|
-
const n = this._applyCheckedState(r);
|
|
234
|
-
this.dispatchEvent(
|
|
235
|
-
new CustomEvent("rc-menu-activate", {
|
|
236
|
-
bubbles: !0,
|
|
237
|
-
composed: !0,
|
|
238
|
-
detail: {
|
|
239
|
-
item: r,
|
|
240
|
-
value: r.dataset.value ?? r.getAttribute("value") ?? "",
|
|
241
|
-
text: r.textContent?.trim() ?? "",
|
|
242
|
-
checked: n
|
|
243
|
-
}
|
|
244
|
-
})
|
|
245
|
-
);
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
|
-
static _ensureBaseStyles(e) {
|
|
249
|
-
if (a._$styledRoots.has(e))
|
|
250
|
-
return;
|
|
251
|
-
a._$styledRoots.add(e);
|
|
252
|
-
const t = document.createElement("style");
|
|
253
|
-
t.setAttribute("data-rc-light-dom-base", "rc-menu"), t.textContent = k, e instanceof Document ? e.head.appendChild(t) : e.appendChild(t);
|
|
254
|
-
}
|
|
255
|
-
connectedCallback() {
|
|
256
|
-
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());
|
|
257
|
-
}
|
|
258
|
-
disconnectedCallback() {
|
|
259
|
-
super.disconnectedCallback(), this.removeEventListener("keydown", this._onKeydown), this.removeEventListener("blur", this._onBlur), this.removeEventListener("click", this._onClick);
|
|
260
|
-
}
|
|
261
|
-
updated(e) {
|
|
262
|
-
e.has("label") && this._syncAriaLabel();
|
|
263
|
-
}
|
|
264
|
-
render() {
|
|
265
|
-
return d`<slot @slotchange=${this._onSlotChange}></slot>`;
|
|
266
|
-
}
|
|
267
|
-
/** All navigable menu item elements in document order. */
|
|
268
|
-
get items() {
|
|
269
|
-
return this._$menuItems;
|
|
270
|
-
}
|
|
271
|
-
/** Move the virtual cursor and DOM focus to the first navigable item. */
|
|
272
|
-
focusFirst() {
|
|
273
|
-
this.focus(), this._activeDescendantCtrl.navigateToFirst();
|
|
274
|
-
}
|
|
275
|
-
/** Move the virtual cursor and DOM focus to the last navigable item. */
|
|
276
|
-
focusLast() {
|
|
277
|
-
this.focus(), this._activeDescendantCtrl.navigateToLast();
|
|
278
|
-
}
|
|
279
|
-
/**
|
|
280
|
-
* Move the virtual cursor to a specific item.
|
|
281
|
-
*
|
|
282
|
-
* @param $item - The element to make active. No-op when null or undefined.
|
|
283
|
-
*/
|
|
284
|
-
focusItem(e) {
|
|
285
|
-
e && (this.focus(), this._activeDescendantCtrl.navigateToItem(e));
|
|
286
|
-
}
|
|
287
|
-
/**
|
|
288
|
-
* Move the virtual cursor to the item at `index`.
|
|
289
|
-
*
|
|
290
|
-
* @param index - Zero-based index into the navigable items array.
|
|
291
|
-
*/
|
|
292
|
-
focusItemAt(e) {
|
|
293
|
-
this.focus(), this._activeDescendantCtrl.navigateTo(e);
|
|
294
|
-
}
|
|
295
|
-
_syncAriaLabel() {
|
|
296
|
-
this.label ? this.setAttribute("aria-label", this.label) : this.removeAttribute("aria-label");
|
|
297
|
-
}
|
|
298
|
-
_onSlotChange(e) {
|
|
299
|
-
const t = e.currentTarget;
|
|
300
|
-
this._$menuItems = this._collectMenuItems(t), this._initMenuItems();
|
|
301
|
-
}
|
|
302
|
-
/**
|
|
303
|
-
* Collects navigable menu items from the slot, supporting `role="group"`
|
|
304
|
-
* containers and skipping `role="separator"` elements.
|
|
305
|
-
*
|
|
306
|
-
* Direct focusable children are included as-is. Elements with
|
|
307
|
-
* `role="group"` are traversed one level deep so their focusable children
|
|
308
|
-
* participate in arrow-key navigation. Separators and non-focusable
|
|
309
|
-
* container elements are excluded.
|
|
310
|
-
*/
|
|
311
|
-
_collectMenuItems(e) {
|
|
312
|
-
const t = [];
|
|
313
|
-
for (const r of e.assignedElements()) {
|
|
314
|
-
const n = r.getAttribute("role");
|
|
315
|
-
if (n !== "separator") {
|
|
316
|
-
if (n === "group") {
|
|
317
|
-
for (const i of r.children)
|
|
318
|
-
s(i) && t.push(i);
|
|
319
|
-
continue;
|
|
320
|
-
}
|
|
321
|
-
s(r) && t.push(r);
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
return t;
|
|
325
|
-
}
|
|
326
|
-
_initMenuItems() {
|
|
327
|
-
this._$menuItems.forEach((e) => {
|
|
328
|
-
const t = e.getAttribute("role");
|
|
329
|
-
(!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");
|
|
330
|
-
});
|
|
331
|
-
}
|
|
332
|
-
_applyCheckedState(e) {
|
|
333
|
-
const t = e.getAttribute("role");
|
|
334
|
-
if (t === "menuitemcheckbox") {
|
|
335
|
-
const r = e.getAttribute("aria-checked") === "true" ? "false" : "true";
|
|
336
|
-
return e.setAttribute("aria-checked", r), r;
|
|
337
|
-
}
|
|
338
|
-
if (t === "menuitemradio") {
|
|
339
|
-
const r = e.closest('[role="group"]'), n = r && this.contains(r) ? r : this;
|
|
340
|
-
for (const i of n.querySelectorAll('[role="menuitemradio"]'))
|
|
341
|
-
i.closest("rc-menu") === this && i.setAttribute("aria-checked", i === e ? "true" : "false");
|
|
342
|
-
return "true";
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
}, a.styles = [v], a._$styledRoots = /* @__PURE__ */ new Set(), a);
|
|
346
|
-
g([
|
|
347
|
-
h({ type: String })
|
|
348
|
-
], l.prototype, "label");
|
|
349
|
-
let C = l;
|
|
350
|
-
export {
|
|
351
|
-
C as R
|
|
352
|
-
};
|
|
353
|
-
//# sourceMappingURL=rc-menu-Df7GAw1H.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"rc-menu-Df7GAw1H.js","sources":["../src/rc-menu.styles.ts","../src/rc-menu.ts"],"sourcesContent":["import { css } from 'lit';\n\nexport const menuStyles = css`\n :host {\n box-sizing: border-box;\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 max-inline-size: var(--rc-menu-max-inline-size, calc(100dvi - 0.5rem));\n max-block-size: var(--rc-menu-max-block-size, calc(100dvb - 0.5rem));\n overflow: auto;\n overscroll-behavior: contain;\n scrollbar-gutter: stable;\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.js';\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 flex-shrink: 0;\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 — ::after acts as a fixed-width trailing column,\n pushed to the item's inline-end regardless of label length (same\n positioning as the [data-rc-menu-shortcut] slot below). Trailing rather\n than leading so it coexists with a leading icon a consumer supplies for\n the item itself (a radio/checkbox item's own icon and its checked state\n are different things; leading is already spoken for). Content is empty\n (unchecked) or a Unicode symbol (checked/mixed). Semantic state is\n conveyed by aria-checked; the visual symbol is decorative. */\n rc-menu [role='menuitemcheckbox']::after,\n rc-menu [role='menuitemradio']::after {\n content: '';\n flex: 0 0 var(--rc-menu-check-size, 1.25em);\n margin-inline-start: auto;\n text-align: center;\n line-height: 1;\n color: var(--rc-menu-check-color, inherit);\n }\n\n /* A shortcut owns the auto margin when both trailing affordances exist,\n keeping the shortcut and selection indicator in one end-aligned group. */\n rc-menu [role='menuitemcheckbox']:has(:is([data-rc-menu-shortcut], [data-menu-shortcut]))::after,\n rc-menu [role='menuitemradio']:has(:is([data-rc-menu-shortcut], [data-menu-shortcut]))::after {\n margin-inline-start: 0;\n }\n\n rc-menu [role='menuitemcheckbox'][aria-checked='true']::after {\n content: '✓';\n }\n\n rc-menu [role='menuitemcheckbox'][aria-checked='mixed']::after {\n content: '−';\n }\n\n rc-menu [role='menuitemradio'][aria-checked='true']::after {\n content: '•';\n }\n\n rc-menu [data-rc-menu-leading],\n rc-menu [data-rc-menu-trailing] {\n flex: none;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n }\n\n rc-menu [data-rc-menu-label] {\n flex: 1 1 auto;\n min-inline-size: 0;\n }\n\n /* Trailing label — keyboard shortcut or hint, pushed to the inline-end of the item */\n rc-menu [data-rc-menu-trailing],\n rc-menu [data-rc-menu-shortcut],\n rc-menu [data-menu-shortcut] {\n margin-inline-start: auto;\n }\n\n rc-menu [data-rc-menu-shortcut],\n rc-menu [data-menu-shortcut] {\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 /* 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 trailing indicator column via `::after`\n * (coexists with a leading icon the item supplies itself); `aria-checked` is initialized when missing and\n * 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, use `[data-rc-menu-leading]`, `[data-rc-menu-label]`, and\n * `[data-rc-menu-trailing]` to identify content columns. `[data-rc-menu-shortcut]`\n * (and the legacy `[data-menu-shortcut]`) is pushed to the inline-end edge.\n * - Nested menu triggers supply their own trailing indicator through `rc-menu-button`'s `indicator` slot.\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 * @attr label - Accessible label for the menu; synced to `aria-label` on the host.\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-color=CanvasText] - Text color; falls back through --rc-field-text.\n * @cssprop [--rc-menu-max-inline-size=calc(100dvi - 0.5rem)] - Viewport-aware maximum inline size.\n * @cssprop [--rc-menu-max-block-size=calc(100dvb - 0.5rem)] - Viewport-aware maximum block size; taller menus scroll.\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 trailing 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-rc-menu-shortcut]` labels.\n * @cssprop [--rc-menu-shortcut-color=GrayText] - Text color of `[data-rc-menu-shortcut]` labels.\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) {\n return;\n }\n\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') {\n continue;\n }\n\n if (role === 'group') {\n for (const $child of $el.children) {\n if (isFocusable($child)) {\n $items.push($child);\n }\n }\n\n continue;\n }\n\n if (isFocusable($el)) {\n $items.push($el);\n }\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 // Falls through so both activation keys share the same handler.\n\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: $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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"}
|