@rcarls/rc-menu 0.3.2 → 0.4.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/README.md +50 -88
- package/dist/custom-elements.json +14 -24
- package/dist/{rc-menu-Drg26iOK.js → rc-menu-DJ8upIAd.js} +13 -21
- package/dist/rc-menu-DJ8upIAd.js.map +1 -0
- package/dist/rc-menu-define.js +1 -1
- package/dist/rc-menu.js +1 -1
- package/dist/types/packages/rc-menu/src/rc-menu.d.ts +10 -10
- package/package.json +1 -1
- package/dist/rc-menu-Drg26iOK.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# `@rcarls/rc-menu`
|
|
2
2
|
|
|
3
|
-
Menu popup for command surfaces with keyboard navigation and typed activation
|
|
3
|
+
Menu popup for command surfaces with keyboard navigation and typed activation
|
|
4
|
+
events, following the
|
|
5
|
+
[WAI-ARIA Menu pattern](https://www.w3.org/WAI/ARIA/apg/patterns/menu/).
|
|
4
6
|
|
|
5
7
|
Docs: [https://richardcarls.github.io/rc-webcomponents/components/rc-menu](https://richardcarls.github.io/rc-webcomponents/components/rc-menu).
|
|
6
8
|
|
|
@@ -15,113 +17,73 @@ npm install @rcarls/rc-menu
|
|
|
15
17
|
## Import
|
|
16
18
|
|
|
17
19
|
```js
|
|
18
|
-
import '@rcarls/rc-menu';
|
|
19
|
-
import { RCMenu } from '@rcarls/rc-menu';
|
|
20
|
+
import '@rcarls/rc-menu/define';
|
|
21
|
+
import { RCMenu } from '@rcarls/rc-menu';
|
|
20
22
|
```
|
|
21
23
|
|
|
24
|
+
The `define` entry point registers `<rc-menu>`. The package root exports the
|
|
25
|
+
class without registering the custom element.
|
|
26
|
+
|
|
22
27
|
---
|
|
23
28
|
|
|
24
29
|
## Basic usage
|
|
25
30
|
|
|
26
|
-
Slot any focusable elements as menu items. Non-focusable elements (separators,
|
|
31
|
+
Slot any focusable elements as menu items. Non-focusable elements (separators,
|
|
32
|
+
headings) are rendered but excluded from keyboard navigation.
|
|
27
33
|
|
|
28
34
|
```html
|
|
29
35
|
<rc-menu label="File">
|
|
30
|
-
<button>New</button>
|
|
31
|
-
<button>Open…</button>
|
|
32
|
-
<hr
|
|
33
|
-
<button disabled>Save</button>
|
|
34
|
-
<button>Save As…</button>
|
|
36
|
+
<button type="button" value="new">New</button>
|
|
37
|
+
<button type="button" value="open">Open…</button>
|
|
38
|
+
<hr />
|
|
39
|
+
<button type="button" disabled>Save</button>
|
|
40
|
+
<button type="button" value="save-as">Save As…</button>
|
|
35
41
|
</rc-menu>
|
|
36
42
|
```
|
|
37
43
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
## API
|
|
43
|
-
|
|
44
|
-
### Properties / attributes
|
|
45
|
-
|
|
46
|
-
| Property | Attribute | Type | Default | Description |
|
|
47
|
-
|---|---|---|---|---|
|
|
48
|
-
| `label` | `label` | `string` | `''` | Value of the `aria-label` on the menu container. Required for accessibility when no visible label is present. |
|
|
44
|
+
For a cascading item, use an [`rc-menu-button`](../rc-menu-button/README.md)
|
|
45
|
+
and provide its visual affordance through the `indicator` slot. `rc-menu` does
|
|
46
|
+
not generate an indicator glyph, so applications and themes retain control of
|
|
47
|
+
iconography and writing-direction behavior.
|
|
49
48
|
|
|
50
|
-
|
|
49
|
+
`rc-menu` is typically used inside an
|
|
50
|
+
[`rc-menu-button`](../rc-menu-button/README.md) or
|
|
51
|
+
[`rc-menubar`](../rc-menubar/README.md), which handle positioning and trigger
|
|
52
|
+
wiring. It can also be used standalone as a context menu.
|
|
51
53
|
|
|
52
|
-
|
|
53
|
-
|---|---|---|
|
|
54
|
-
| `--rc-menu-min-width` | `10em` | Minimum width of the menu container |
|
|
55
|
-
| `--rc-menu-padding-block` | `0.25em` | Block-axis (top/bottom) padding inside the root |
|
|
56
|
-
| `--rc-menu-background` | `Canvas` | Menu background color (system color keyword) |
|
|
57
|
-
| `--rc-menu-border` | `1px solid ButtonBorder` | Menu border |
|
|
58
|
-
| `--rc-menu-shadow` | `0 2px 8px rgba(0,0,0,0.15)` | Box shadow |
|
|
54
|
+
## Interaction model
|
|
59
55
|
|
|
60
|
-
|
|
56
|
+
DOM focus remains on the `rc-menu` host. Arrow keys, Home, and End move a
|
|
57
|
+
virtual cursor exposed through `aria-activedescendant`; slotted menu items keep
|
|
58
|
+
`tabindex="-1"`. The `focusFirst()`, `focusLast()`, `focusItem()`, and
|
|
59
|
+
`focusItemAt()` methods use the same model: they focus the host and select an
|
|
60
|
+
active item without moving DOM focus into that item.
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
Direct focusable children become `role="menuitem"` entries unless they already
|
|
63
|
+
use `menuitemcheckbox` or `menuitemradio`. A labeled `role="group"` may contain
|
|
64
|
+
one level of related items. Checkbox and radio items maintain `aria-checked`,
|
|
65
|
+
while `<hr>` and `role="separator"` elements remain outside navigation.
|
|
65
66
|
|
|
66
|
-
|
|
67
|
+
## Events
|
|
67
68
|
|
|
68
|
-
|
|
69
|
-
|---|---|
|
|
70
|
-
| *(default)* | Menu items. Only focusable elements participate in keyboard navigation. `<button>`, `<a href>`, elements with `tabindex`, and `<input>`/`<select>`/`<textarea>` are recognised as focusable; all others are skipped. |
|
|
69
|
+
Both events bubble across shadow boundaries and are not cancelable.
|
|
71
70
|
|
|
72
|
-
|
|
71
|
+
| Event | Detail | When |
|
|
72
|
+
| ------------------ | --------------------------------- | ---------------------------------------------------------- |
|
|
73
|
+
| `rc-menu-activate` | `{ item, value, text, checked? }` | Enter, Space, or a pointer click activates an enabled item |
|
|
74
|
+
| `rc-menu-close` | `{ reason: 'escape' }` | Escape requests that the containing popup close |
|
|
73
75
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
| `rc-menu-activate` | Yes (composed) | No | `{ item: HTMLElement }` | User presses Enter or Space on a focused item |
|
|
77
|
-
| `rc-menu-close` | Yes (composed) | No | — | User presses Escape |
|
|
76
|
+
`value` comes from `data-value` or `value`, `text` is the item's trimmed text
|
|
77
|
+
content, and `checked` reports the resulting checkbox or radio state.
|
|
78
78
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
```ts
|
|
82
|
-
focusFirst(): void // Focus the first navigable item
|
|
83
|
-
focusLast(): void // Focus the last navigable item
|
|
84
|
-
focusItem(item: HTMLElement): void // Focus a specific item
|
|
85
|
-
focusItemAt(index: number): void // Focus item at zero-based index
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
### Read-only getters
|
|
89
|
-
|
|
90
|
-
```ts
|
|
91
|
-
items: HTMLElement[] // All currently navigable (focusable) slotted items
|
|
92
|
-
firstItem: HTMLElement | undefined
|
|
93
|
-
lastItem: HTMLElement | undefined
|
|
94
|
-
nextItem: HTMLElement | undefined // Relative to the currently focused item
|
|
95
|
-
previousItem: HTMLElement | undefined
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
---
|
|
99
|
-
|
|
100
|
-
## Keyboard behaviour
|
|
101
|
-
|
|
102
|
-
| Key | Action |
|
|
103
|
-
|---|---|
|
|
104
|
-
| `ArrowDown` | Focus next item (wraps) |
|
|
105
|
-
| `ArrowUp` | Focus previous item (wraps) |
|
|
106
|
-
| `Home` | Focus first item |
|
|
107
|
-
| `End` | Focus last item |
|
|
108
|
-
| `Enter` / `Space` | Activate focused item (fires `rc-menu-activate`) |
|
|
109
|
-
| `Escape` | Fire `rc-menu-close` |
|
|
110
|
-
|
|
111
|
-
---
|
|
112
|
-
|
|
113
|
-
## ARIA
|
|
114
|
-
|
|
115
|
-
| Attribute | Where | Value |
|
|
116
|
-
|---|---|---|
|
|
117
|
-
| `role="menu"` | Root `div` | — |
|
|
118
|
-
| `aria-label` | Root `div` | Value of `label` property |
|
|
119
|
-
| `role="menuitem"` | Each slotted focusable item | Set dynamically on slot change |
|
|
120
|
-
| `tabindex="0"` | Active item | One item is tabbable at a time (roving tabindex) |
|
|
121
|
-
| `tabindex="-1"` | All other items | Removed from tab order |
|
|
122
|
-
|
|
123
|
-
---
|
|
79
|
+
## Styling
|
|
124
80
|
|
|
125
|
-
|
|
81
|
+
Menu items remain in light DOM. `rc-menu` injects their structural base styles
|
|
82
|
+
once per containing document or shadow root under `@layer rc-base`; inherited
|
|
83
|
+
`--rc-menu-*` custom properties customize the host and rows. The component does
|
|
84
|
+
not expose CSS parts. Add keyboard hints with `[data-menu-shortcut]`, and provide
|
|
85
|
+
nested-menu indicators through `rc-menu-button`'s `indicator` slot.
|
|
126
86
|
|
|
127
|
-
|
|
87
|
+
See the [component docs](https://richardcarls.github.io/rc-webcomponents/components/rc-menu)
|
|
88
|
+
for the generated API reference, including attributes, methods, slots, events,
|
|
89
|
+
and CSS custom properties.
|
|
@@ -79,6 +79,11 @@
|
|
|
79
79
|
"name": "--rc-menu-shadow",
|
|
80
80
|
"default": "0 2px 8px color-mix(in srgb, CanvasText 15%, transparent)"
|
|
81
81
|
},
|
|
82
|
+
{
|
|
83
|
+
"description": "Text color; falls back through --rc-field-text.",
|
|
84
|
+
"name": "--rc-menu-color",
|
|
85
|
+
"default": "CanvasText"
|
|
86
|
+
},
|
|
82
87
|
{
|
|
83
88
|
"description": "Minimum block size for menu item rows.",
|
|
84
89
|
"name": "--rc-menu-item-min-block-size",
|
|
@@ -143,21 +148,6 @@
|
|
|
143
148
|
"name": "--rc-menu-shortcut-color",
|
|
144
149
|
"default": "GrayText"
|
|
145
150
|
},
|
|
146
|
-
{
|
|
147
|
-
"description": "Text content for submenu indicators.",
|
|
148
|
-
"name": "--rc-menu-submenu-indicator-content",
|
|
149
|
-
"default": "'›'"
|
|
150
|
-
},
|
|
151
|
-
{
|
|
152
|
-
"description": "Font size of submenu indicators.",
|
|
153
|
-
"name": "--rc-menu-submenu-indicator-size",
|
|
154
|
-
"default": "1em"
|
|
155
|
-
},
|
|
156
|
-
{
|
|
157
|
-
"description": "Text color of submenu indicators.",
|
|
158
|
-
"name": "--rc-menu-submenu-indicator-color",
|
|
159
|
-
"default": "currentColor"
|
|
160
|
-
},
|
|
161
151
|
{
|
|
162
152
|
"description": "Disabled menu item text color.",
|
|
163
153
|
"name": "--rc-menu-disabled-color",
|
|
@@ -196,14 +186,14 @@
|
|
|
196
186
|
],
|
|
197
187
|
"slots": [
|
|
198
188
|
{
|
|
199
|
-
"description": "Menu items, groups, and separators. Supported content model: - Focusable elements (`<button>`, `<a>`, `[tabindex]`) become navigable `role=\"menuitem\"` entries. - `role=\"menuitemcheckbox\"` / `role=\"menuitemradio\"` elements get a leading indicator column via `::before`; `aria-checked` is initialized when missing and updated on activation. - `role=\"group\"` divs group related items; label them with `aria-label` or `aria-labelledby`. Optionally add a visible `[data-group-label]` element as first child of the group for section headings. - `role=\"separator\"` / `<hr>` produce horizontal dividers between sections. - Within any item, `[data-menu-shortcut]` elements are pushed to the inline-end edge for keyboard hints. -
|
|
189
|
+
"description": "Menu items, groups, and separators. Supported content model: - Focusable elements (`<button>`, `<a>`, `[tabindex]`) become navigable `role=\"menuitem\"` entries. - `role=\"menuitemcheckbox\"` / `role=\"menuitemradio\"` elements get a leading indicator column via `::before`; `aria-checked` is initialized when missing and updated on activation. - `role=\"group\"` divs group related items; label them with `aria-label` or `aria-labelledby`. Optionally add a visible `[data-group-label]` element as first child of the group for section headings. - `role=\"separator\"` / `<hr>` produce horizontal dividers between sections. - Within any item, `[data-menu-shortcut]` elements are pushed to the inline-end edge for keyboard hints. - Nested menu triggers supply their own trailing indicator through `rc-menu-button`'s `indicator` slot.",
|
|
200
190
|
"name": "default"
|
|
201
191
|
}
|
|
202
192
|
],
|
|
203
193
|
"members": [
|
|
204
194
|
{
|
|
205
195
|
"kind": "field",
|
|
206
|
-
"name": "
|
|
196
|
+
"name": "_$styledRoots",
|
|
207
197
|
"privacy": "private",
|
|
208
198
|
"static": true,
|
|
209
199
|
"readonly": true,
|
|
@@ -221,7 +211,7 @@
|
|
|
221
211
|
},
|
|
222
212
|
"parameters": [
|
|
223
213
|
{
|
|
224
|
-
"name": "root",
|
|
214
|
+
"name": "$root",
|
|
225
215
|
"type": {
|
|
226
216
|
"text": "Document | ShadowRoot"
|
|
227
217
|
}
|
|
@@ -240,7 +230,7 @@
|
|
|
240
230
|
},
|
|
241
231
|
{
|
|
242
232
|
"kind": "field",
|
|
243
|
-
"name": "
|
|
233
|
+
"name": "_$menuItems",
|
|
244
234
|
"type": {
|
|
245
235
|
"text": "Element[]"
|
|
246
236
|
},
|
|
@@ -252,7 +242,7 @@
|
|
|
252
242
|
"name": "_activeDescendantCtrl",
|
|
253
243
|
"privacy": "protected",
|
|
254
244
|
"readonly": true,
|
|
255
|
-
"default": "new ActiveDescendantController(this, { host: () => this, items: () => this.
|
|
245
|
+
"default": "new ActiveDescendantController(this, { host: () => this, items: () => this._$menuItems, })"
|
|
256
246
|
},
|
|
257
247
|
{
|
|
258
248
|
"kind": "field",
|
|
@@ -293,7 +283,7 @@
|
|
|
293
283
|
},
|
|
294
284
|
"parameters": [
|
|
295
285
|
{
|
|
296
|
-
"name": "item",
|
|
286
|
+
"name": "$item",
|
|
297
287
|
"optional": true,
|
|
298
288
|
"type": {
|
|
299
289
|
"text": "Element | null"
|
|
@@ -361,7 +351,7 @@
|
|
|
361
351
|
},
|
|
362
352
|
"parameters": [
|
|
363
353
|
{
|
|
364
|
-
"name": "slot",
|
|
354
|
+
"name": "$slot",
|
|
365
355
|
"type": {
|
|
366
356
|
"text": "HTMLSlotElement"
|
|
367
357
|
}
|
|
@@ -390,7 +380,7 @@
|
|
|
390
380
|
},
|
|
391
381
|
"parameters": [
|
|
392
382
|
{
|
|
393
|
-
"name": "item",
|
|
383
|
+
"name": "$item",
|
|
394
384
|
"type": {
|
|
395
385
|
"text": "HTMLElement"
|
|
396
386
|
}
|
|
@@ -425,12 +415,12 @@
|
|
|
425
415
|
],
|
|
426
416
|
"attributes": [
|
|
427
417
|
{
|
|
418
|
+
"description": "Accessible label for this menu. Synced to `aria-label` on the host.\n\nPrefer `aria-labelledby` for a visible label associated via ID.",
|
|
428
419
|
"name": "label",
|
|
429
420
|
"type": {
|
|
430
421
|
"text": "string"
|
|
431
422
|
},
|
|
432
423
|
"default": "''",
|
|
433
|
-
"description": "Accessible label for this menu. Synced to `aria-label` on the host.\n\nPrefer `aria-labelledby` for a visible label associated via ID.",
|
|
434
424
|
"fieldName": "label"
|
|
435
425
|
}
|
|
436
426
|
],
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { css as u, LitElement as m, html as d } from "lit";
|
|
2
2
|
import { property as h } from "lit/decorators.js";
|
|
3
|
-
import { ActiveDescendantController as
|
|
4
|
-
const
|
|
3
|
+
import { ActiveDescendantController as v, isFocusable as s } from "@rcarls/rc-common";
|
|
4
|
+
const b = u`
|
|
5
5
|
:host {
|
|
6
6
|
display: flex;
|
|
7
7
|
flex-direction: column;
|
|
@@ -107,15 +107,6 @@ const f = ["menuitem", "menuitemcheckbox", "menuitemradio"], k = `
|
|
|
107
107
|
white-space: nowrap;
|
|
108
108
|
}
|
|
109
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
110
|
/* Separators */
|
|
120
111
|
rc-menu [role='separator'],
|
|
121
112
|
rc-menu hr {
|
|
@@ -153,9 +144,9 @@ const f = ["menuitem", "menuitemcheckbox", "menuitemradio"], k = `
|
|
|
153
144
|
var a;
|
|
154
145
|
const l = (a = class extends m {
|
|
155
146
|
constructor() {
|
|
156
|
-
super(...arguments), this.label = "", this.
|
|
147
|
+
super(...arguments), this.label = "", this._$menuItems = [], this._activeDescendantCtrl = new v(this, {
|
|
157
148
|
host: () => this,
|
|
158
|
-
items: () => this.
|
|
149
|
+
items: () => this._$menuItems
|
|
159
150
|
}), this._onKeydown = (e) => {
|
|
160
151
|
switch (e.key) {
|
|
161
152
|
case "ArrowDown": {
|
|
@@ -175,6 +166,7 @@ const l = (a = class extends m {
|
|
|
175
166
|
break;
|
|
176
167
|
}
|
|
177
168
|
case "Enter":
|
|
169
|
+
// Falls through so both activation keys share the same handler.
|
|
178
170
|
case " ": {
|
|
179
171
|
e.preventDefault(), e.stopPropagation();
|
|
180
172
|
const t = this._activeDescendantCtrl.activeItem;
|
|
@@ -216,9 +208,9 @@ const l = (a = class extends m {
|
|
|
216
208
|
};
|
|
217
209
|
}
|
|
218
210
|
static _ensureBaseStyles(e) {
|
|
219
|
-
if (a.
|
|
211
|
+
if (a._$styledRoots.has(e))
|
|
220
212
|
return;
|
|
221
|
-
a.
|
|
213
|
+
a._$styledRoots.add(e);
|
|
222
214
|
const t = document.createElement("style");
|
|
223
215
|
t.setAttribute("data-rc-light-dom-base", "rc-menu"), t.textContent = k, e instanceof Document ? e.head.appendChild(t) : e.appendChild(t);
|
|
224
216
|
}
|
|
@@ -236,7 +228,7 @@ const l = (a = class extends m {
|
|
|
236
228
|
}
|
|
237
229
|
/** All navigable menu item elements in document order. */
|
|
238
230
|
get items() {
|
|
239
|
-
return this.
|
|
231
|
+
return this._$menuItems;
|
|
240
232
|
}
|
|
241
233
|
/** Move the virtual cursor and DOM focus to the first navigable item. */
|
|
242
234
|
focusFirst() {
|
|
@@ -249,7 +241,7 @@ const l = (a = class extends m {
|
|
|
249
241
|
/**
|
|
250
242
|
* Move the virtual cursor to a specific item.
|
|
251
243
|
*
|
|
252
|
-
* @param item - The element to make active. No-op when null or undefined.
|
|
244
|
+
* @param $item - The element to make active. No-op when null or undefined.
|
|
253
245
|
*/
|
|
254
246
|
focusItem(e) {
|
|
255
247
|
e && (this.focus(), this._activeDescendantCtrl.navigateToItem(e));
|
|
@@ -267,7 +259,7 @@ const l = (a = class extends m {
|
|
|
267
259
|
}
|
|
268
260
|
_onSlotChange(e) {
|
|
269
261
|
const t = e.currentTarget;
|
|
270
|
-
this.
|
|
262
|
+
this._$menuItems = this._collectMenuItems(t), this._initMenuItems();
|
|
271
263
|
}
|
|
272
264
|
/**
|
|
273
265
|
* Collects navigable menu items from the slot, supporting `role="group"`
|
|
@@ -294,7 +286,7 @@ const l = (a = class extends m {
|
|
|
294
286
|
return t;
|
|
295
287
|
}
|
|
296
288
|
_initMenuItems() {
|
|
297
|
-
this.
|
|
289
|
+
this._$menuItems.forEach((e) => {
|
|
298
290
|
const t = e.getAttribute("role");
|
|
299
291
|
(!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
292
|
});
|
|
@@ -312,7 +304,7 @@ const l = (a = class extends m {
|
|
|
312
304
|
return "true";
|
|
313
305
|
}
|
|
314
306
|
}
|
|
315
|
-
}, a.styles = [
|
|
307
|
+
}, a.styles = [b], a._$styledRoots = /* @__PURE__ */ new Set(), a);
|
|
316
308
|
g([
|
|
317
309
|
h({ type: String })
|
|
318
310
|
], l.prototype, "label");
|
|
@@ -320,4 +312,4 @@ let C = l;
|
|
|
320
312
|
export {
|
|
321
313
|
C as R
|
|
322
314
|
};
|
|
323
|
-
//# sourceMappingURL=rc-menu-
|
|
315
|
+
//# sourceMappingURL=rc-menu-DJ8upIAd.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rc-menu-DJ8upIAd.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 /* 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 * - 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-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-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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"}
|
package/dist/rc-menu-define.js
CHANGED
package/dist/rc-menu.js
CHANGED
|
@@ -32,17 +32,20 @@ export interface RCMenuActivateEvent {
|
|
|
32
32
|
* add a visible `[data-group-label]` element as first child of the group for section headings.
|
|
33
33
|
* - `role="separator"` / `<hr>` produce horizontal dividers between sections.
|
|
34
34
|
* - Within any item, `[data-menu-shortcut]` elements are pushed to the inline-end edge for keyboard hints.
|
|
35
|
-
* -
|
|
35
|
+
* - Nested menu triggers supply their own trailing indicator through `rc-menu-button`'s `indicator` slot.
|
|
36
36
|
*
|
|
37
37
|
* @fires rc-menu-activate - Fired when a menu item is activated via keyboard (Enter/Space) or pointer click.
|
|
38
38
|
* @fires rc-menu-close - Fired when Escape is pressed.
|
|
39
39
|
*
|
|
40
|
+
* @attr label - Accessible label for the menu; synced to `aria-label` on the host.
|
|
41
|
+
*
|
|
40
42
|
* @cssprop [--rc-menu-min-width=10em] - Minimum width of the menu panel.
|
|
41
43
|
* @cssprop [--rc-menu-padding-block=0.25em] - Block (top/bottom) padding inside the panel.
|
|
42
44
|
* @cssprop [--rc-menu-background=Canvas] - Background color; falls back through --rc-surface.
|
|
43
45
|
* @cssprop [--rc-menu-border=1px solid ButtonBorder] - Border; falls back through --rc-border.
|
|
44
46
|
* @cssprop [--rc-menu-radius=var(--rc-control-radius)] - Menu panel border radius.
|
|
45
47
|
* @cssprop [--rc-menu-shadow=0 2px 8px color-mix(in srgb, CanvasText 15%, transparent)] - Box shadow; falls back through --rc-shadow.
|
|
48
|
+
* @cssprop [--rc-menu-color=CanvasText] - Text color; falls back through --rc-field-text.
|
|
46
49
|
* @cssprop [--rc-menu-item-min-block-size=0] - Minimum block size for menu item rows.
|
|
47
50
|
* @cssprop [--rc-menu-item-padding-block=var(--rc-item-padding-block)] - Menu item block-axis padding.
|
|
48
51
|
* @cssprop [--rc-menu-item-padding-inline=var(--rc-item-padding-inline)] - Menu item inline-axis padding.
|
|
@@ -56,9 +59,6 @@ export interface RCMenuActivateEvent {
|
|
|
56
59
|
* @cssprop [--rc-menu-check-color=inherit] - Color of the check/radio symbol.
|
|
57
60
|
* @cssprop [--rc-menu-shortcut-size=0.8em] - Font size of `[data-menu-shortcut]` labels.
|
|
58
61
|
* @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
62
|
* @cssprop [--rc-menu-disabled-color=GrayText] - Disabled menu item text color.
|
|
63
63
|
* @cssprop [--rc-menu-disabled-opacity=var(--rc-disabled-opacity)] - Disabled menu item opacity.
|
|
64
64
|
* @cssprop [--rc-menu-separator-border=1px solid ButtonBorder] - Separator border.
|
|
@@ -69,7 +69,7 @@ export interface RCMenuActivateEvent {
|
|
|
69
69
|
*/
|
|
70
70
|
export declare class RCMenu extends LitElement {
|
|
71
71
|
static styles: import('lit').CSSResult[];
|
|
72
|
-
private static readonly
|
|
72
|
+
private static readonly _$styledRoots;
|
|
73
73
|
private static _ensureBaseStyles;
|
|
74
74
|
/**
|
|
75
75
|
* Accessible label for this menu. Synced to `aria-label` on the host.
|
|
@@ -77,7 +77,7 @@ export declare class RCMenu extends LitElement {
|
|
|
77
77
|
* Prefer `aria-labelledby` for a visible label associated via ID.
|
|
78
78
|
*/
|
|
79
79
|
label: string;
|
|
80
|
-
protected
|
|
80
|
+
protected _$menuItems: Element[];
|
|
81
81
|
protected readonly _activeDescendantCtrl: ActiveDescendantController;
|
|
82
82
|
connectedCallback(): void;
|
|
83
83
|
disconnectedCallback(): void;
|
|
@@ -92,9 +92,9 @@ export declare class RCMenu extends LitElement {
|
|
|
92
92
|
/**
|
|
93
93
|
* Move the virtual cursor to a specific item.
|
|
94
94
|
*
|
|
95
|
-
* @param item - The element to make active. No-op when null or undefined.
|
|
95
|
+
* @param $item - The element to make active. No-op when null or undefined.
|
|
96
96
|
*/
|
|
97
|
-
focusItem(item?: Element | null): void;
|
|
97
|
+
focusItem($item?: Element | null): void;
|
|
98
98
|
/**
|
|
99
99
|
* Move the virtual cursor to the item at `index`.
|
|
100
100
|
*
|
|
@@ -112,9 +112,9 @@ export declare class RCMenu extends LitElement {
|
|
|
112
112
|
* participate in arrow-key navigation. Separators and non-focusable
|
|
113
113
|
* container elements are excluded.
|
|
114
114
|
*/
|
|
115
|
-
protected _collectMenuItems(slot: HTMLSlotElement): Element[];
|
|
115
|
+
protected _collectMenuItems($slot: HTMLSlotElement): Element[];
|
|
116
116
|
protected _initMenuItems(): void;
|
|
117
|
-
protected _applyCheckedState(item: HTMLElement): 'true' | 'false' | 'mixed' | undefined;
|
|
117
|
+
protected _applyCheckedState($item: HTMLElement): 'true' | 'false' | 'mixed' | undefined;
|
|
118
118
|
protected _onKeydown: (e: KeyboardEvent) => void;
|
|
119
119
|
protected _onBlur: () => void;
|
|
120
120
|
protected _onClick: (e: MouseEvent) => void;
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
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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"}
|