@rcarls/rc-menu 0.0.0-next-20260921045401
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 +103 -0
- package/README.md +89 -0
- package/dist/custom-elements.json +465 -0
- package/dist/rc-menu-Df7GAw1H.js +353 -0
- package/dist/rc-menu-Df7GAw1H.js.map +1 -0
- package/dist/rc-menu-define.js +6 -0
- package/dist/rc-menu-define.js.map +1 -0
- package/dist/rc-menu.js +5 -0
- package/dist/rc-menu.js.map +1 -0
- package/dist/types/packages/rc-menu/src/define.d.ts +1 -0
- package/dist/types/packages/rc-menu/src/index.d.ts +1 -0
- package/dist/types/packages/rc-menu/src/rc-menu.d.ts +127 -0
- package/dist/types/packages/rc-menu/src/rc-menu.styles.d.ts +2 -0
- package/package.json +65 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# @rcarls/rc-menu
|
|
2
|
+
|
|
3
|
+
## 0.0.0-next-20260921045401
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [50d5610]
|
|
8
|
+
- Updated dependencies [4341311]
|
|
9
|
+
- @rcarls/rc-common@0.0.0-next-20260921045401
|
|
10
|
+
|
|
11
|
+
## 0.6.0
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- 7382c71: Add a priority-aware action toolbar that moves original controls into an
|
|
16
|
+
overflow menu. Which actions are promoted is a purely declarative computation
|
|
17
|
+
from `max-shown` and each action's `data-priority`/authored `slot`, with no
|
|
18
|
+
runtime measurement of the toolbar's or its children's rendered size: a
|
|
19
|
+
consumer whose promoted count needs to react to available space owns that
|
|
20
|
+
decision and writes the resulting `max-shown` value down. Include controlled
|
|
21
|
+
and uncontrolled open state, keyboard navigation, light dismissal, and a
|
|
22
|
+
non-Popover fallback.
|
|
23
|
+
|
|
24
|
+
Fix `--rc-adaptive-menu-touch-target-overlap-inline-start`/`-end`: the host's
|
|
25
|
+
own `max-inline-size: 100%` self-referenced a shrink-to-fit ancestor's
|
|
26
|
+
already-margin-reduced auto size (an app bar's own trailing group, for
|
|
27
|
+
example), quietly clamping the host back down and canceling the overlap out
|
|
28
|
+
from under it. The cap now grows by the overlap amount so it only ever bounds
|
|
29
|
+
the host's real content.
|
|
30
|
+
|
|
31
|
+
- 30eb232: Add icon-only activation-area sizing and Material icon-button variants. Align menu
|
|
32
|
+
selection and submenu affordances, and keep anchored popups within the viewport
|
|
33
|
+
across native and fallback positioning.
|
|
34
|
+
- Updated dependencies [6683eb9]
|
|
35
|
+
- Updated dependencies [ee7ba6c]
|
|
36
|
+
- Updated dependencies [30eb232]
|
|
37
|
+
- @rcarls/rc-common@0.6.0
|
|
38
|
+
|
|
39
|
+
## 0.5.0
|
|
40
|
+
|
|
41
|
+
### Patch Changes
|
|
42
|
+
|
|
43
|
+
- Updated dependencies [4ae2ef0]
|
|
44
|
+
- Updated dependencies [689340c]
|
|
45
|
+
- @rcarls/rc-common@0.5.0
|
|
46
|
+
|
|
47
|
+
## 0.4.2
|
|
48
|
+
|
|
49
|
+
### Patch Changes
|
|
50
|
+
|
|
51
|
+
- @rcarls/rc-common@0.4.2
|
|
52
|
+
|
|
53
|
+
## 0.4.1
|
|
54
|
+
|
|
55
|
+
### Patch Changes
|
|
56
|
+
|
|
57
|
+
- @rcarls/rc-common@0.4.1
|
|
58
|
+
|
|
59
|
+
## 0.4.0
|
|
60
|
+
|
|
61
|
+
### Minor Changes
|
|
62
|
+
|
|
63
|
+
- 3e89095: Replace rc-menu's generated submenu glyph with an author-supplied `indicator`
|
|
64
|
+
slot on rc-menu-button. Keep the decorative indicator inside the trigger bounds,
|
|
65
|
+
reserve label space for it, and expose theme-neutral size, color, and inset
|
|
66
|
+
tokens for Material, Substrate, and application themes.
|
|
67
|
+
|
|
68
|
+
### Patch Changes
|
|
69
|
+
|
|
70
|
+
- Updated dependencies [e57277f]
|
|
71
|
+
- Updated dependencies [ccca8e2]
|
|
72
|
+
- Updated dependencies [037b1b3]
|
|
73
|
+
- @rcarls/rc-common@0.4.0
|
|
74
|
+
|
|
75
|
+
## 0.3.2
|
|
76
|
+
|
|
77
|
+
### Patch Changes
|
|
78
|
+
|
|
79
|
+
- Updated dependencies [88b4086]
|
|
80
|
+
- @rcarls/rc-common@0.3.2
|
|
81
|
+
|
|
82
|
+
## 0.3.1
|
|
83
|
+
|
|
84
|
+
### Patch Changes
|
|
85
|
+
|
|
86
|
+
- @rcarls/rc-common@0.3.1
|
|
87
|
+
|
|
88
|
+
## 0.3.0
|
|
89
|
+
|
|
90
|
+
### Changed
|
|
91
|
+
|
|
92
|
+
- Normalize menu row styling around the light-DOM `rc-base` style contract.
|
|
93
|
+
- Update package metadata, README intro, and docs links.
|
|
94
|
+
|
|
95
|
+
### Dependencies
|
|
96
|
+
|
|
97
|
+
- Sync internal dependencies to 0.3.0.
|
|
98
|
+
|
|
99
|
+
## 0.2.0
|
|
100
|
+
|
|
101
|
+
### Patch Changes
|
|
102
|
+
|
|
103
|
+
- @rcarls/rc-common@0.2.0
|
package/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# `@rcarls/rc-menu`
|
|
2
|
+
|
|
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/).
|
|
6
|
+
|
|
7
|
+
Docs: [https://richardcarls.github.io/rc-webcomponents/components/rc-menu](https://richardcarls.github.io/rc-webcomponents/components/rc-menu).
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @rcarls/rc-menu
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Import
|
|
18
|
+
|
|
19
|
+
```js
|
|
20
|
+
import '@rcarls/rc-menu/define';
|
|
21
|
+
import { RCMenu } from '@rcarls/rc-menu';
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The `define` entry point registers `<rc-menu>`. The package root exports the
|
|
25
|
+
class without registering the custom element.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Basic usage
|
|
30
|
+
|
|
31
|
+
Slot any focusable elements as menu items. Non-focusable elements (separators,
|
|
32
|
+
headings) are rendered but excluded from keyboard navigation.
|
|
33
|
+
|
|
34
|
+
```html
|
|
35
|
+
<rc-menu label="File">
|
|
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>
|
|
41
|
+
</rc-menu>
|
|
42
|
+
```
|
|
43
|
+
|
|
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.
|
|
48
|
+
|
|
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.
|
|
53
|
+
|
|
54
|
+
## Interaction model
|
|
55
|
+
|
|
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
|
+
|
|
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.
|
|
66
|
+
|
|
67
|
+
## Events
|
|
68
|
+
|
|
69
|
+
Both events bubble across shadow boundaries and are not cancelable.
|
|
70
|
+
|
|
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 |
|
|
75
|
+
|
|
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
|
+
|
|
79
|
+
## Styling
|
|
80
|
+
|
|
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.
|
|
86
|
+
|
|
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.
|
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": "1.0.0",
|
|
3
|
+
"readme": "",
|
|
4
|
+
"modules": [
|
|
5
|
+
{
|
|
6
|
+
"kind": "javascript-module",
|
|
7
|
+
"path": "src/define.ts",
|
|
8
|
+
"declarations": [],
|
|
9
|
+
"exports": [
|
|
10
|
+
{
|
|
11
|
+
"kind": "custom-element-definition",
|
|
12
|
+
"name": "rc-menu",
|
|
13
|
+
"declaration": {
|
|
14
|
+
"name": "RCMenu",
|
|
15
|
+
"module": "/src/index.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"kind": "js",
|
|
20
|
+
"name": "*",
|
|
21
|
+
"declaration": {
|
|
22
|
+
"name": "*",
|
|
23
|
+
"module": "src/index.js"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"kind": "javascript-module",
|
|
30
|
+
"path": "src/index.ts",
|
|
31
|
+
"declarations": [],
|
|
32
|
+
"exports": [
|
|
33
|
+
{
|
|
34
|
+
"kind": "js",
|
|
35
|
+
"name": "*",
|
|
36
|
+
"declaration": {
|
|
37
|
+
"name": "*",
|
|
38
|
+
"module": "src/rc-menu.js"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"kind": "javascript-module",
|
|
45
|
+
"path": "src/rc-menu.ts",
|
|
46
|
+
"declarations": [
|
|
47
|
+
{
|
|
48
|
+
"kind": "class",
|
|
49
|
+
"description": "Menu popup for command surfaces with keyboard navigation and typed activation events,\nfollowing the WAI-ARIA Menu pattern.",
|
|
50
|
+
"name": "RCMenu",
|
|
51
|
+
"cssProperties": [
|
|
52
|
+
{
|
|
53
|
+
"description": "Minimum width of the menu panel.",
|
|
54
|
+
"name": "--rc-menu-min-width",
|
|
55
|
+
"default": "10em"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"description": "Block (top/bottom) padding inside the panel.",
|
|
59
|
+
"name": "--rc-menu-padding-block",
|
|
60
|
+
"default": "0.25em"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"description": "Background color; falls back through --rc-surface.",
|
|
64
|
+
"name": "--rc-menu-background",
|
|
65
|
+
"default": "Canvas"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"description": "Border; falls back through --rc-border.",
|
|
69
|
+
"name": "--rc-menu-border",
|
|
70
|
+
"default": "1px solid ButtonBorder"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"description": "Menu panel border radius.",
|
|
74
|
+
"name": "--rc-menu-radius",
|
|
75
|
+
"default": "var(--rc-control-radius)"
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"description": "Box shadow; falls back through --rc-shadow.",
|
|
79
|
+
"name": "--rc-menu-shadow",
|
|
80
|
+
"default": "0 2px 8px color-mix(in srgb, CanvasText 15%, transparent)"
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"description": "Text color; falls back through --rc-field-text.",
|
|
84
|
+
"name": "--rc-menu-color",
|
|
85
|
+
"default": "CanvasText"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"description": "Viewport-aware maximum inline size.",
|
|
89
|
+
"name": "--rc-menu-max-inline-size",
|
|
90
|
+
"default": "calc(100dvi - 0.5rem)"
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"description": "Viewport-aware maximum block size; taller menus scroll.",
|
|
94
|
+
"name": "--rc-menu-max-block-size",
|
|
95
|
+
"default": "calc(100dvb - 0.5rem)"
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"description": "Minimum block size for menu item rows.",
|
|
99
|
+
"name": "--rc-menu-item-min-block-size",
|
|
100
|
+
"default": "0"
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"description": "Menu item block-axis padding.",
|
|
104
|
+
"name": "--rc-menu-item-padding-block",
|
|
105
|
+
"default": "var(--rc-item-padding-block)"
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"description": "Menu item inline-axis padding.",
|
|
109
|
+
"name": "--rc-menu-item-padding-inline",
|
|
110
|
+
"default": "var(--rc-item-padding-inline)"
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"description": "Gap between flex children inside each item.",
|
|
114
|
+
"name": "--rc-menu-item-gap",
|
|
115
|
+
"default": "var(--rc-item-gap)"
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"description": "CSS transition applied to each item row.",
|
|
119
|
+
"name": "--rc-menu-item-transition"
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
"description": "Pointer-hover background.",
|
|
123
|
+
"name": "--rc-menu-hover-bg",
|
|
124
|
+
"default": "color-mix(in srgb, Highlight 8%, transparent)"
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
"description": "Pointer-hover text color.",
|
|
128
|
+
"name": "--rc-menu-hover-color",
|
|
129
|
+
"default": "inherit"
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"description": "Virtual-cursor active background.",
|
|
133
|
+
"name": "--rc-menu-active-bg",
|
|
134
|
+
"default": "color-mix(in srgb, Highlight 8%, transparent)"
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
"description": "Virtual-cursor active text color.",
|
|
138
|
+
"name": "--rc-menu-active-color",
|
|
139
|
+
"default": "inherit"
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"description": "Width of the trailing indicator column for checkbox/radio items.",
|
|
143
|
+
"name": "--rc-menu-check-size",
|
|
144
|
+
"default": "1.25em"
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
"description": "Color of the check/radio symbol.",
|
|
148
|
+
"name": "--rc-menu-check-color",
|
|
149
|
+
"default": "inherit"
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
"description": "Font size of `[data-rc-menu-shortcut]` labels.",
|
|
153
|
+
"name": "--rc-menu-shortcut-size",
|
|
154
|
+
"default": "0.8em"
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"description": "Text color of `[data-rc-menu-shortcut]` labels.",
|
|
158
|
+
"name": "--rc-menu-shortcut-color",
|
|
159
|
+
"default": "GrayText"
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
"description": "Disabled menu item text color.",
|
|
163
|
+
"name": "--rc-menu-disabled-color",
|
|
164
|
+
"default": "GrayText"
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
"description": "Disabled menu item opacity.",
|
|
168
|
+
"name": "--rc-menu-disabled-opacity",
|
|
169
|
+
"default": "var(--rc-disabled-opacity)"
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
"description": "Separator border.",
|
|
173
|
+
"name": "--rc-menu-separator-border",
|
|
174
|
+
"default": "1px solid ButtonBorder"
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
"description": "Separator block-axis margin.",
|
|
178
|
+
"name": "--rc-menu-separator-margin-block",
|
|
179
|
+
"default": "var(--rc-control-gap)"
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
"description": "Separator inline margin; set to match item padding for inset separators.",
|
|
183
|
+
"name": "--rc-menu-separator-margin-inline",
|
|
184
|
+
"default": "0px"
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
"description": "Block padding on `[data-group-label]` section headings.",
|
|
188
|
+
"name": "--rc-menu-group-label-padding-block",
|
|
189
|
+
"default": "0.1em"
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
"description": "Text color of `[data-group-label]` section headings.",
|
|
193
|
+
"name": "--rc-menu-group-label-color",
|
|
194
|
+
"default": "GrayText"
|
|
195
|
+
}
|
|
196
|
+
],
|
|
197
|
+
"slots": [
|
|
198
|
+
{
|
|
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 trailing indicator column via `::after` (coexists with a leading icon the item supplies itself); `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, use `[data-rc-menu-leading]`, `[data-rc-menu-label]`, and `[data-rc-menu-trailing]` to identify content columns. `[data-rc-menu-shortcut]` (and the legacy `[data-menu-shortcut]`) is pushed to the inline-end edge. - Nested menu triggers supply their own trailing indicator through `rc-menu-button`'s `indicator` slot.",
|
|
200
|
+
"name": "default"
|
|
201
|
+
}
|
|
202
|
+
],
|
|
203
|
+
"members": [
|
|
204
|
+
{
|
|
205
|
+
"kind": "field",
|
|
206
|
+
"name": "_$styledRoots",
|
|
207
|
+
"privacy": "private",
|
|
208
|
+
"static": true,
|
|
209
|
+
"readonly": true,
|
|
210
|
+
"default": "new Set<Document | ShadowRoot>()"
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
"kind": "method",
|
|
214
|
+
"name": "_ensureBaseStyles",
|
|
215
|
+
"privacy": "private",
|
|
216
|
+
"static": true,
|
|
217
|
+
"return": {
|
|
218
|
+
"type": {
|
|
219
|
+
"text": "void"
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
"parameters": [
|
|
223
|
+
{
|
|
224
|
+
"name": "$root",
|
|
225
|
+
"type": {
|
|
226
|
+
"text": "Document | ShadowRoot"
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
]
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
"kind": "field",
|
|
233
|
+
"name": "label",
|
|
234
|
+
"type": {
|
|
235
|
+
"text": "string"
|
|
236
|
+
},
|
|
237
|
+
"default": "''",
|
|
238
|
+
"description": "Accessible label for this menu. Synced to `aria-label` on the host.\n\nPrefer `aria-labelledby` for a visible label associated via ID.",
|
|
239
|
+
"attribute": "label"
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
"kind": "field",
|
|
243
|
+
"name": "_$menuItems",
|
|
244
|
+
"type": {
|
|
245
|
+
"text": "Element[]"
|
|
246
|
+
},
|
|
247
|
+
"privacy": "protected",
|
|
248
|
+
"default": "[]"
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
"kind": "field",
|
|
252
|
+
"name": "_activeDescendantCtrl",
|
|
253
|
+
"privacy": "protected",
|
|
254
|
+
"readonly": true,
|
|
255
|
+
"default": "new ActiveDescendantController(this, { host: () => this, items: () => this._$menuItems, })"
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
"kind": "field",
|
|
259
|
+
"name": "items",
|
|
260
|
+
"type": {
|
|
261
|
+
"text": "readonly Element[]"
|
|
262
|
+
},
|
|
263
|
+
"description": "All navigable menu item elements in document order.",
|
|
264
|
+
"readonly": true
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
"kind": "method",
|
|
268
|
+
"name": "focusFirst",
|
|
269
|
+
"return": {
|
|
270
|
+
"type": {
|
|
271
|
+
"text": "void"
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
"description": "Move the virtual cursor and DOM focus to the first navigable item."
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
"kind": "method",
|
|
278
|
+
"name": "focusLast",
|
|
279
|
+
"return": {
|
|
280
|
+
"type": {
|
|
281
|
+
"text": "void"
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
"description": "Move the virtual cursor and DOM focus to the last navigable item."
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
"kind": "method",
|
|
288
|
+
"name": "focusItem",
|
|
289
|
+
"return": {
|
|
290
|
+
"type": {
|
|
291
|
+
"text": "void"
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
"parameters": [
|
|
295
|
+
{
|
|
296
|
+
"name": "$item",
|
|
297
|
+
"optional": true,
|
|
298
|
+
"type": {
|
|
299
|
+
"text": "Element | null"
|
|
300
|
+
},
|
|
301
|
+
"description": "The element to make active. No-op when null or undefined."
|
|
302
|
+
}
|
|
303
|
+
],
|
|
304
|
+
"description": "Move the virtual cursor to a specific item."
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
"kind": "method",
|
|
308
|
+
"name": "focusItemAt",
|
|
309
|
+
"return": {
|
|
310
|
+
"type": {
|
|
311
|
+
"text": "void"
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
"parameters": [
|
|
315
|
+
{
|
|
316
|
+
"name": "index",
|
|
317
|
+
"type": {
|
|
318
|
+
"text": "number"
|
|
319
|
+
},
|
|
320
|
+
"description": "Zero-based index into the navigable items array."
|
|
321
|
+
}
|
|
322
|
+
],
|
|
323
|
+
"description": "Move the virtual cursor to the item at `index`."
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
"kind": "method",
|
|
327
|
+
"name": "_syncAriaLabel",
|
|
328
|
+
"privacy": "protected",
|
|
329
|
+
"return": {
|
|
330
|
+
"type": {
|
|
331
|
+
"text": "void"
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
"kind": "method",
|
|
337
|
+
"name": "_onSlotChange",
|
|
338
|
+
"privacy": "protected",
|
|
339
|
+
"return": {
|
|
340
|
+
"type": {
|
|
341
|
+
"text": "void"
|
|
342
|
+
}
|
|
343
|
+
},
|
|
344
|
+
"parameters": [
|
|
345
|
+
{
|
|
346
|
+
"name": "e",
|
|
347
|
+
"type": {
|
|
348
|
+
"text": "Event"
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
]
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
"kind": "method",
|
|
355
|
+
"name": "_collectMenuItems",
|
|
356
|
+
"privacy": "protected",
|
|
357
|
+
"return": {
|
|
358
|
+
"type": {
|
|
359
|
+
"text": "Element[]"
|
|
360
|
+
}
|
|
361
|
+
},
|
|
362
|
+
"parameters": [
|
|
363
|
+
{
|
|
364
|
+
"name": "$slot",
|
|
365
|
+
"type": {
|
|
366
|
+
"text": "HTMLSlotElement"
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
],
|
|
370
|
+
"description": "Collects navigable menu items from the slot, supporting `role=\"group\"`\ncontainers and skipping `role=\"separator\"` elements.\n\nDirect focusable children are included as-is. Elements with\n`role=\"group\"` are traversed one level deep so their focusable children\nparticipate in arrow-key navigation. Separators and non-focusable\ncontainer elements are excluded."
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
"kind": "method",
|
|
374
|
+
"name": "_initMenuItems",
|
|
375
|
+
"privacy": "protected",
|
|
376
|
+
"return": {
|
|
377
|
+
"type": {
|
|
378
|
+
"text": "void"
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
"kind": "method",
|
|
384
|
+
"name": "_applyCheckedState",
|
|
385
|
+
"privacy": "protected",
|
|
386
|
+
"return": {
|
|
387
|
+
"type": {
|
|
388
|
+
"text": "'true' | 'false' | 'mixed' | undefined"
|
|
389
|
+
}
|
|
390
|
+
},
|
|
391
|
+
"parameters": [
|
|
392
|
+
{
|
|
393
|
+
"name": "$item",
|
|
394
|
+
"type": {
|
|
395
|
+
"text": "HTMLElement"
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
]
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
"kind": "field",
|
|
402
|
+
"name": "_onKeydown",
|
|
403
|
+
"privacy": "protected"
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
"kind": "field",
|
|
407
|
+
"name": "_onBlur",
|
|
408
|
+
"privacy": "protected"
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
"kind": "field",
|
|
412
|
+
"name": "_onClick",
|
|
413
|
+
"privacy": "protected"
|
|
414
|
+
}
|
|
415
|
+
],
|
|
416
|
+
"events": [
|
|
417
|
+
{
|
|
418
|
+
"description": "Fired when a menu item is activated via keyboard (Enter/Space) or pointer click.",
|
|
419
|
+
"name": "rc-menu-activate"
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
"description": "Fired when Escape is pressed.",
|
|
423
|
+
"name": "rc-menu-close"
|
|
424
|
+
}
|
|
425
|
+
],
|
|
426
|
+
"attributes": [
|
|
427
|
+
{
|
|
428
|
+
"description": "Accessible label for this menu. Synced to `aria-label` on the host.\n\nPrefer `aria-labelledby` for a visible label associated via ID.",
|
|
429
|
+
"name": "label",
|
|
430
|
+
"type": {
|
|
431
|
+
"text": "string"
|
|
432
|
+
},
|
|
433
|
+
"default": "''",
|
|
434
|
+
"fieldName": "label"
|
|
435
|
+
}
|
|
436
|
+
],
|
|
437
|
+
"superclass": {
|
|
438
|
+
"name": "LitElement",
|
|
439
|
+
"package": "lit"
|
|
440
|
+
},
|
|
441
|
+
"tagName": "rc-menu",
|
|
442
|
+
"customElement": true
|
|
443
|
+
}
|
|
444
|
+
],
|
|
445
|
+
"exports": [
|
|
446
|
+
{
|
|
447
|
+
"kind": "js",
|
|
448
|
+
"name": "RCMenu",
|
|
449
|
+
"declaration": {
|
|
450
|
+
"name": "RCMenu",
|
|
451
|
+
"module": "src/rc-menu.ts"
|
|
452
|
+
}
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
"kind": "js",
|
|
456
|
+
"name": "default",
|
|
457
|
+
"declaration": {
|
|
458
|
+
"name": "RCMenu",
|
|
459
|
+
"module": "src/rc-menu.ts"
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
]
|
|
463
|
+
}
|
|
464
|
+
]
|
|
465
|
+
}
|
|
@@ -0,0 +1,353 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
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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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rc-menu-define.js","sources":["../src/define.ts"],"sourcesContent":["import { RCMenu } from './index.js';\n\ncustomElements.get('rc-menu') || customElements.define('rc-menu', RCMenu);\n\nexport * from './index.js';\n"],"names":["RCMenu"],"mappings":";AAEA,eAAe,IAAI,SAAS,KAAK,eAAe,OAAO,WAAWA,CAAM;"}
|
package/dist/rc-menu.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rc-menu.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './index.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './rc-menu.js';
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { LitElement } from 'lit';
|
|
2
|
+
import { ActiveDescendantController } from '@rcarls/rc-common';
|
|
3
|
+
declare global {
|
|
4
|
+
interface HTMLElementTagNameMap {
|
|
5
|
+
'rc-menu': RCMenu;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
/** Detail payload for the `rc-menu-activate` event. */
|
|
9
|
+
export interface RCMenuActivateEvent {
|
|
10
|
+
/** The activated menu item element. */
|
|
11
|
+
item: HTMLElement;
|
|
12
|
+
/** The item's `data-value` or `value` attribute, or an empty string. */
|
|
13
|
+
value: string;
|
|
14
|
+
/** Trimmed text content of the activated item. */
|
|
15
|
+
text: string;
|
|
16
|
+
/** Current `aria-checked` state for checkbox/radio menu items. */
|
|
17
|
+
checked?: 'true' | 'false' | 'mixed';
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Menu popup for command surfaces with keyboard navigation and typed activation events,
|
|
21
|
+
* following the WAI-ARIA Menu pattern.
|
|
22
|
+
*
|
|
23
|
+
*
|
|
24
|
+
* @see {@link https://richardcarls.github.io/rc-webcomponents/components/rc-menu rc-menu docs}
|
|
25
|
+
* @see {@link https://www.w3.org/WAI/ARIA/apg/patterns/menu/ WAI-ARIA Menu pattern}
|
|
26
|
+
*
|
|
27
|
+
* @slot default - Menu items, groups, and separators. Supported content model:
|
|
28
|
+
* - Focusable elements (`<button>`, `<a>`, `[tabindex]`) become navigable `role="menuitem"` entries.
|
|
29
|
+
* - `role="menuitemcheckbox"` / `role="menuitemradio"` elements get a trailing indicator column via `::after`
|
|
30
|
+
* (coexists with a leading icon the item supplies itself); `aria-checked` is initialized when missing and
|
|
31
|
+
* updated on activation.
|
|
32
|
+
* - `role="group"` divs group related items; label them with `aria-label` or `aria-labelledby`. Optionally
|
|
33
|
+
* add a visible `[data-group-label]` element as first child of the group for section headings.
|
|
34
|
+
* - `role="separator"` / `<hr>` produce horizontal dividers between sections.
|
|
35
|
+
* - Within any item, use `[data-rc-menu-leading]`, `[data-rc-menu-label]`, and
|
|
36
|
+
* `[data-rc-menu-trailing]` to identify content columns. `[data-rc-menu-shortcut]`
|
|
37
|
+
* (and the legacy `[data-menu-shortcut]`) is pushed to the inline-end edge.
|
|
38
|
+
* - Nested menu triggers supply their own trailing indicator through `rc-menu-button`'s `indicator` slot.
|
|
39
|
+
*
|
|
40
|
+
* @fires rc-menu-activate - Fired when a menu item is activated via keyboard (Enter/Space) or pointer click.
|
|
41
|
+
* @fires rc-menu-close - Fired when Escape is pressed.
|
|
42
|
+
*
|
|
43
|
+
* @attr label - Accessible label for the menu; synced to `aria-label` on the host.
|
|
44
|
+
*
|
|
45
|
+
* @cssprop [--rc-menu-min-width=10em] - Minimum width of the menu panel.
|
|
46
|
+
* @cssprop [--rc-menu-padding-block=0.25em] - Block (top/bottom) padding inside the panel.
|
|
47
|
+
* @cssprop [--rc-menu-background=Canvas] - Background color; falls back through --rc-surface.
|
|
48
|
+
* @cssprop [--rc-menu-border=1px solid ButtonBorder] - Border; falls back through --rc-border.
|
|
49
|
+
* @cssprop [--rc-menu-radius=var(--rc-control-radius)] - Menu panel border radius.
|
|
50
|
+
* @cssprop [--rc-menu-shadow=0 2px 8px color-mix(in srgb, CanvasText 15%, transparent)] - Box shadow; falls back through --rc-shadow.
|
|
51
|
+
* @cssprop [--rc-menu-color=CanvasText] - Text color; falls back through --rc-field-text.
|
|
52
|
+
* @cssprop [--rc-menu-max-inline-size=calc(100dvi - 0.5rem)] - Viewport-aware maximum inline size.
|
|
53
|
+
* @cssprop [--rc-menu-max-block-size=calc(100dvb - 0.5rem)] - Viewport-aware maximum block size; taller menus scroll.
|
|
54
|
+
* @cssprop [--rc-menu-item-min-block-size=0] - Minimum block size for menu item rows.
|
|
55
|
+
* @cssprop [--rc-menu-item-padding-block=var(--rc-item-padding-block)] - Menu item block-axis padding.
|
|
56
|
+
* @cssprop [--rc-menu-item-padding-inline=var(--rc-item-padding-inline)] - Menu item inline-axis padding.
|
|
57
|
+
* @cssprop [--rc-menu-item-gap=var(--rc-item-gap)] - Gap between flex children inside each item.
|
|
58
|
+
* @cssprop [--rc-menu-item-transition] - CSS transition applied to each item row.
|
|
59
|
+
* @cssprop [--rc-menu-hover-bg=color-mix(in srgb, Highlight 8%, transparent)] - Pointer-hover background.
|
|
60
|
+
* @cssprop [--rc-menu-hover-color=inherit] - Pointer-hover text color.
|
|
61
|
+
* @cssprop [--rc-menu-active-bg=color-mix(in srgb, Highlight 8%, transparent)] - Virtual-cursor active background.
|
|
62
|
+
* @cssprop [--rc-menu-active-color=inherit] - Virtual-cursor active text color.
|
|
63
|
+
* @cssprop [--rc-menu-check-size=1.25em] - Width of the trailing indicator column for checkbox/radio items.
|
|
64
|
+
* @cssprop [--rc-menu-check-color=inherit] - Color of the check/radio symbol.
|
|
65
|
+
* @cssprop [--rc-menu-shortcut-size=0.8em] - Font size of `[data-rc-menu-shortcut]` labels.
|
|
66
|
+
* @cssprop [--rc-menu-shortcut-color=GrayText] - Text color of `[data-rc-menu-shortcut]` labels.
|
|
67
|
+
* @cssprop [--rc-menu-disabled-color=GrayText] - Disabled menu item text color.
|
|
68
|
+
* @cssprop [--rc-menu-disabled-opacity=var(--rc-disabled-opacity)] - Disabled menu item opacity.
|
|
69
|
+
* @cssprop [--rc-menu-separator-border=1px solid ButtonBorder] - Separator border.
|
|
70
|
+
* @cssprop [--rc-menu-separator-margin-block=var(--rc-control-gap)] - Separator block-axis margin.
|
|
71
|
+
* @cssprop [--rc-menu-separator-margin-inline=0px] - Separator inline margin; set to match item padding for inset separators.
|
|
72
|
+
* @cssprop [--rc-menu-group-label-padding-block=0.1em] - Block padding on `[data-group-label]` section headings.
|
|
73
|
+
* @cssprop [--rc-menu-group-label-color=GrayText] - Text color of `[data-group-label]` section headings.
|
|
74
|
+
*/
|
|
75
|
+
export declare class RCMenu extends LitElement {
|
|
76
|
+
static styles: import('lit').CSSResult[];
|
|
77
|
+
private static readonly _$styledRoots;
|
|
78
|
+
private static _ensureBaseStyles;
|
|
79
|
+
/**
|
|
80
|
+
* Accessible label for this menu. Synced to `aria-label` on the host.
|
|
81
|
+
*
|
|
82
|
+
* Prefer `aria-labelledby` for a visible label associated via ID.
|
|
83
|
+
*/
|
|
84
|
+
label: string;
|
|
85
|
+
protected _$menuItems: Element[];
|
|
86
|
+
protected readonly _activeDescendantCtrl: ActiveDescendantController;
|
|
87
|
+
connectedCallback(): void;
|
|
88
|
+
disconnectedCallback(): void;
|
|
89
|
+
protected updated(changed: Map<PropertyKey, unknown>): void;
|
|
90
|
+
protected render(): import('lit').TemplateResult<1>;
|
|
91
|
+
/** All navigable menu item elements in document order. */
|
|
92
|
+
get items(): readonly Element[];
|
|
93
|
+
/** Move the virtual cursor and DOM focus to the first navigable item. */
|
|
94
|
+
focusFirst(): void;
|
|
95
|
+
/** Move the virtual cursor and DOM focus to the last navigable item. */
|
|
96
|
+
focusLast(): void;
|
|
97
|
+
/**
|
|
98
|
+
* Move the virtual cursor to a specific item.
|
|
99
|
+
*
|
|
100
|
+
* @param $item - The element to make active. No-op when null or undefined.
|
|
101
|
+
*/
|
|
102
|
+
focusItem($item?: Element | null): void;
|
|
103
|
+
/**
|
|
104
|
+
* Move the virtual cursor to the item at `index`.
|
|
105
|
+
*
|
|
106
|
+
* @param index - Zero-based index into the navigable items array.
|
|
107
|
+
*/
|
|
108
|
+
focusItemAt(index: number): void;
|
|
109
|
+
protected _syncAriaLabel(): void;
|
|
110
|
+
protected _onSlotChange(e: Event): void;
|
|
111
|
+
/**
|
|
112
|
+
* Collects navigable menu items from the slot, supporting `role="group"`
|
|
113
|
+
* containers and skipping `role="separator"` elements.
|
|
114
|
+
*
|
|
115
|
+
* Direct focusable children are included as-is. Elements with
|
|
116
|
+
* `role="group"` are traversed one level deep so their focusable children
|
|
117
|
+
* participate in arrow-key navigation. Separators and non-focusable
|
|
118
|
+
* container elements are excluded.
|
|
119
|
+
*/
|
|
120
|
+
protected _collectMenuItems($slot: HTMLSlotElement): Element[];
|
|
121
|
+
protected _initMenuItems(): void;
|
|
122
|
+
protected _applyCheckedState($item: HTMLElement): 'true' | 'false' | 'mixed' | undefined;
|
|
123
|
+
protected _onKeydown: (e: KeyboardEvent) => void;
|
|
124
|
+
protected _onBlur: () => void;
|
|
125
|
+
protected _onClick: (e: MouseEvent) => void;
|
|
126
|
+
}
|
|
127
|
+
export default RCMenu;
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rcarls/rc-menu",
|
|
3
|
+
"publishConfig": {
|
|
4
|
+
"access": "public"
|
|
5
|
+
},
|
|
6
|
+
"version": "0.0.0-next-20260921045401",
|
|
7
|
+
"description": "Menu popup for command surfaces with keyboard navigation and typed activation events.",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/richardcarls/rc-webcomponents.git",
|
|
11
|
+
"directory": "packages/rc-menu"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://richardcarls.github.io/rc-webcomponents/components/rc-menu",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"type": "module",
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"module": "./dist/rc-menu.js",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"import": {
|
|
23
|
+
"types": "./dist/types/packages/rc-menu/src/index.d.ts",
|
|
24
|
+
"default": "./dist/rc-menu.js"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"./define": {
|
|
28
|
+
"import": {
|
|
29
|
+
"types": "./dist/types/packages/rc-menu/src/define.d.ts",
|
|
30
|
+
"default": "./dist/rc-menu-define.js"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"types": "./dist/types/packages/rc-menu/src/index.d.ts",
|
|
35
|
+
"sideEffects": [
|
|
36
|
+
"./dist/rc-menu-define.js"
|
|
37
|
+
],
|
|
38
|
+
"customElements": "dist/custom-elements.json",
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsc && vite build && cem analyze",
|
|
41
|
+
"cem:analyze": "cem analyze",
|
|
42
|
+
"preview": "vite preview",
|
|
43
|
+
"test:browser": "vitest --run",
|
|
44
|
+
"test:browser:chrome": "vitest --run --project=chromium",
|
|
45
|
+
"test:browser:firefox": "vitest --run --project=firefox"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@rcarls/rc-common": "0.0.0-next-20260921045401"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@custom-elements-manifest/analyzer": "0.11.0",
|
|
52
|
+
"@vitest/browser-playwright": "4.1.5",
|
|
53
|
+
"lit": "^3.0.0",
|
|
54
|
+
"playwright": "^1.56.0",
|
|
55
|
+
"rollup": "^4.60.2",
|
|
56
|
+
"typescript": "~5.9.3",
|
|
57
|
+
"vite": "^7.1.7",
|
|
58
|
+
"vite-plugin-dts": "^4.5.4",
|
|
59
|
+
"vitest": "^4.0.6",
|
|
60
|
+
"vitest-browser-lit": "^1.0.1"
|
|
61
|
+
},
|
|
62
|
+
"peerDependencies": {
|
|
63
|
+
"lit": "^3.0.0"
|
|
64
|
+
}
|
|
65
|
+
}
|