@rcarls/rc-menu-button 0.1.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 +130 -0
- package/dist/custom-elements.json +485 -0
- package/dist/demo.css +85 -0
- package/dist/demo.js +40 -0
- package/dist/rc-menu-button-CmzW7P4h.js +270 -0
- package/dist/rc-menu-button-CmzW7P4h.js.map +1 -0
- package/dist/rc-menu-button-define.js +7 -0
- package/dist/rc-menu-button-define.js.map +1 -0
- package/dist/rc-menu-button.js +5 -0
- package/dist/rc-menu-button.js.map +1 -0
- package/dist/types/packages/rc-menu-button/src/define.d.ts +1 -0
- package/dist/types/packages/rc-menu-button/src/index.d.ts +1 -0
- package/dist/types/packages/rc-menu-button/src/rc-menu-button.d.ts +123 -0
- package/dist/types/packages/rc-menu-button/src/rc-menu-button.styles.d.ts +2 -0
- package/dist/types/packages/rc-menu-button/src/rc-menu-button.test.d.ts +0 -0
- package/package.json +70 -0
package/README.md
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# `@rcarls/rc-menu-button`
|
|
2
|
+
|
|
3
|
+
A WAI-ARIA compliant menu button component implementing the [Menu Button pattern](https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/). Wraps a trigger element and an `rc-menu` popup, wiring ARIA attributes, light-dismiss, and keyboard open/close automatically. Built with [Lit 3](https://lit.dev).
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @rcarls/rc-menu-button
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Import
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
import '@rcarls/rc-menu-button'; // side-effect: registers <rc-menu-button>
|
|
17
|
+
import { RCMenuButton } from '@rcarls/rc-menu-button'; // named class export
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`rc-menu-button` depends on `rc-menu`. Ensure both are installed.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Basic usage
|
|
25
|
+
|
|
26
|
+
Place a trigger element in the `trigger` slot and an `rc-menu` in the default slot.
|
|
27
|
+
|
|
28
|
+
```html
|
|
29
|
+
<rc-menu-button>
|
|
30
|
+
<button slot="trigger">File</button>
|
|
31
|
+
|
|
32
|
+
<rc-menu label="File">
|
|
33
|
+
<button>New</button>
|
|
34
|
+
<button>Open…</button>
|
|
35
|
+
<button>Save</button>
|
|
36
|
+
</rc-menu>
|
|
37
|
+
</rc-menu-button>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The component automatically sets `aria-haspopup="menu"` and `aria-expanded` on the trigger, and closes the menu when the user clicks outside or selects an item.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## API
|
|
45
|
+
|
|
46
|
+
### Properties / attributes
|
|
47
|
+
|
|
48
|
+
| Property | Attribute | Type | Default | Description |
|
|
49
|
+
|---|---|---|---|---|
|
|
50
|
+
| `open` | `open` | `boolean` | `false` | Whether the menu popup is visible. Reflects to attribute. |
|
|
51
|
+
| `orientation` | `orientation` | `'horizontal' \| 'vertical' \| undefined` | `undefined` | Arrow-key axis for opening the menu. When unset, inherited from a parent `rc-menubar` or `[role="menubar"]` element. Falls back to `'horizontal'`. |
|
|
52
|
+
|
|
53
|
+
### CSS custom properties
|
|
54
|
+
|
|
55
|
+
| Property | Default | Description |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| `--rc-menu-button-popup-z-index` | `1000` | `z-index` of the popup container |
|
|
58
|
+
|
|
59
|
+
### CSS parts
|
|
60
|
+
|
|
61
|
+
| Part | Element | Description |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| `root` | Outer `div` | Root wrapper |
|
|
64
|
+
| `popup` | Popup `div` | Container that shows/hides around the slotted `rc-menu` |
|
|
65
|
+
|
|
66
|
+
### Slots
|
|
67
|
+
|
|
68
|
+
| Slot | Description |
|
|
69
|
+
|---|---|
|
|
70
|
+
| `trigger` | The button (or other interactive element) that toggles the menu. Receives `aria-haspopup` and `aria-expanded` automatically. |
|
|
71
|
+
| *(default)* | The `rc-menu` to show as the popup. |
|
|
72
|
+
|
|
73
|
+
### Events
|
|
74
|
+
|
|
75
|
+
| Event | Bubbles | Cancelable | Detail | When |
|
|
76
|
+
|---|---|---|---|---|
|
|
77
|
+
| `rc-menu-button-toggle` | Yes (composed) | No | `{ open: boolean }` | Menu opens or closes |
|
|
78
|
+
|
|
79
|
+
### Public methods
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
openMenu(focusTarget?: 'first' | 'last'): void
|
|
83
|
+
// Open the menu. Focuses the first or last item after render.
|
|
84
|
+
|
|
85
|
+
closeMenu(returnFocus?: boolean): void
|
|
86
|
+
// Close the menu. If returnFocus is true (default), restores focus to the trigger.
|
|
87
|
+
|
|
88
|
+
toggleMenu(): void
|
|
89
|
+
// Toggle open/closed.
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Keyboard behaviour
|
|
95
|
+
|
|
96
|
+
Keyboard handling depends on the resolved `orientation` (horizontal by default, or inherited from a parent menubar).
|
|
97
|
+
|
|
98
|
+
| Key | Condition | Action |
|
|
99
|
+
|---|---|---|
|
|
100
|
+
| `ArrowDown` | Horizontal orientation | Open menu, focus first item |
|
|
101
|
+
| `ArrowUp` | Horizontal orientation | Open menu, focus last item |
|
|
102
|
+
| `ArrowRight` | Vertical orientation | Open menu, focus first item |
|
|
103
|
+
| `ArrowLeft` | Vertical orientation | Open menu, focus last item |
|
|
104
|
+
| `Enter` / `Space` | Menu closed | Open menu |
|
|
105
|
+
| `Enter` / `Space` | Menu open | Activate focused item, close menu |
|
|
106
|
+
| `Escape` | Menu open | Close menu, return focus to trigger |
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## ARIA
|
|
111
|
+
|
|
112
|
+
| Attribute | Where | Value |
|
|
113
|
+
|---|---|---|
|
|
114
|
+
| `aria-haspopup="menu"` | Trigger element | Set automatically on slot change |
|
|
115
|
+
| `aria-expanded` | Trigger element | `"true"` / `"false"`, kept in sync with `open` |
|
|
116
|
+
| `open` | Host element | Reflected boolean attribute when menu is open |
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Notes
|
|
121
|
+
|
|
122
|
+
**Popup positioning** currently uses `position: absolute; top: 100%; left: 0` relative to the host. A future release will migrate to `AnchorController` (CSS Anchor Positioning) to support directional flipping and better viewport-aware placement.
|
|
123
|
+
|
|
124
|
+
**Light dismiss** is active at all times. A capture-phase `click` listener on `document` closes the menu whenever the click falls outside the component boundary (checked via `composedPath()`).
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Browser support
|
|
129
|
+
|
|
130
|
+
All modern browsers. Requires Web Components support (Chrome 67+, Firefox 63+, Safari 12.1+).
|
|
@@ -0,0 +1,485 @@
|
|
|
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-button",
|
|
13
|
+
"declaration": {
|
|
14
|
+
"name": "RCMenuButton",
|
|
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-button"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"kind": "javascript-module",
|
|
45
|
+
"path": "src/rc-menu-button.ts",
|
|
46
|
+
"declarations": [
|
|
47
|
+
{
|
|
48
|
+
"kind": "class",
|
|
49
|
+
"description": "A menu button component that opens/closes a menu popup",
|
|
50
|
+
"name": "RCMenuButton",
|
|
51
|
+
"cssProperties": [
|
|
52
|
+
{
|
|
53
|
+
"description": "Minimum block size of the trigger",
|
|
54
|
+
"name": "--rc-menu-button-trigger-block-size",
|
|
55
|
+
"default": "var(--rc-control-block-size)"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"description": "Trigger block-axis padding",
|
|
59
|
+
"name": "--rc-menu-button-trigger-padding-block",
|
|
60
|
+
"default": "var(--rc-control-padding-block)"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"description": "Trigger inline-axis padding",
|
|
64
|
+
"name": "--rc-menu-button-trigger-padding-inline",
|
|
65
|
+
"default": "var(--rc-control-padding-inline)"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"description": "Trigger border",
|
|
69
|
+
"name": "--rc-menu-button-trigger-border",
|
|
70
|
+
"default": "var(--rc-border)"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"description": "Trigger border radius",
|
|
74
|
+
"name": "--rc-menu-button-trigger-radius",
|
|
75
|
+
"default": "var(--rc-control-radius)"
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"description": "Trigger background",
|
|
79
|
+
"name": "--rc-menu-button-trigger-background",
|
|
80
|
+
"default": "var(--rc-button-bg)"
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"description": "Trigger text color",
|
|
84
|
+
"name": "--rc-menu-button-trigger-color",
|
|
85
|
+
"default": "var(--rc-button-text)"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"description": "Z-index of the popup overlay",
|
|
89
|
+
"name": "--rc-menu-button-popup-z-index",
|
|
90
|
+
"default": "1000"
|
|
91
|
+
}
|
|
92
|
+
],
|
|
93
|
+
"cssParts": [
|
|
94
|
+
{
|
|
95
|
+
"description": "The root container element",
|
|
96
|
+
"name": "root"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"description": "The popup container element",
|
|
100
|
+
"name": "popup"
|
|
101
|
+
}
|
|
102
|
+
],
|
|
103
|
+
"slots": [
|
|
104
|
+
{
|
|
105
|
+
"description": "The button element that triggers the menu",
|
|
106
|
+
"name": "trigger"
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"description": "The rc-menu element to display as popup",
|
|
110
|
+
"name": "default"
|
|
111
|
+
}
|
|
112
|
+
],
|
|
113
|
+
"members": [
|
|
114
|
+
{
|
|
115
|
+
"kind": "field",
|
|
116
|
+
"name": "shadowRootOptions",
|
|
117
|
+
"type": {
|
|
118
|
+
"text": "ShadowRootInit"
|
|
119
|
+
},
|
|
120
|
+
"static": true,
|
|
121
|
+
"default": "{ ...LitElement.shadowRootOptions, delegatesFocus: true, }",
|
|
122
|
+
"description": "Enables delegatesFocus so the browser routes focus() on the host element\nto the slotted trigger button. This lets the host participate in a\nroving-tabindex group (e.g. rc-toolbar) as a single tab stop while the\ntrigger remains the visible focus target."
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"kind": "field",
|
|
126
|
+
"name": "_defaultOpen",
|
|
127
|
+
"type": {
|
|
128
|
+
"text": "boolean"
|
|
129
|
+
},
|
|
130
|
+
"privacy": "private",
|
|
131
|
+
"default": "false"
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"kind": "field",
|
|
135
|
+
"name": "_open",
|
|
136
|
+
"type": {
|
|
137
|
+
"text": "boolean"
|
|
138
|
+
},
|
|
139
|
+
"privacy": "private",
|
|
140
|
+
"default": "false"
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"kind": "field",
|
|
144
|
+
"name": "open",
|
|
145
|
+
"type": {
|
|
146
|
+
"text": "boolean"
|
|
147
|
+
},
|
|
148
|
+
"description": "Whether the menu is currently open. Host writes update silently.",
|
|
149
|
+
"attribute": "open",
|
|
150
|
+
"reflects": true
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"kind": "field",
|
|
154
|
+
"name": "defaultOpen",
|
|
155
|
+
"type": {
|
|
156
|
+
"text": "boolean"
|
|
157
|
+
},
|
|
158
|
+
"description": "Initial uncontrolled open state.",
|
|
159
|
+
"attribute": "default-open"
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
"kind": "field",
|
|
163
|
+
"name": "orientation",
|
|
164
|
+
"type": {
|
|
165
|
+
"text": "'horizontal' | 'vertical' | undefined"
|
|
166
|
+
},
|
|
167
|
+
"description": "Orientation of this menu button, affects which arrow keys open/close the menu.\nIf not set, inherits from a parent rc-menubar or element with role=\"menubar\".",
|
|
168
|
+
"attribute": "orientation",
|
|
169
|
+
"reflects": true
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
"kind": "field",
|
|
173
|
+
"name": "placement",
|
|
174
|
+
"type": {
|
|
175
|
+
"text": "AnchorPlacement"
|
|
176
|
+
},
|
|
177
|
+
"default": "'bottom-start'",
|
|
178
|
+
"description": "Preferred placement of the popup relative to the trigger button.",
|
|
179
|
+
"attribute": "placement",
|
|
180
|
+
"reflects": true
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
"kind": "field",
|
|
184
|
+
"name": "_effectivePlacement",
|
|
185
|
+
"type": {
|
|
186
|
+
"text": "AnchorPlacement"
|
|
187
|
+
},
|
|
188
|
+
"privacy": "private",
|
|
189
|
+
"readonly": true
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
"kind": "field",
|
|
193
|
+
"name": "_resolvedOrientation",
|
|
194
|
+
"type": {
|
|
195
|
+
"text": "'horizontal' | 'vertical'"
|
|
196
|
+
},
|
|
197
|
+
"privacy": "private",
|
|
198
|
+
"description": "Resolved orientation, considering inheritance from parent menubar.\nFalls back to 'horizontal' if no orientation is set or inherited.",
|
|
199
|
+
"readonly": true
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
"kind": "field",
|
|
203
|
+
"name": "_$root",
|
|
204
|
+
"type": {
|
|
205
|
+
"text": "HTMLElement"
|
|
206
|
+
},
|
|
207
|
+
"privacy": "private"
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
"kind": "field",
|
|
211
|
+
"name": "_$popup",
|
|
212
|
+
"type": {
|
|
213
|
+
"text": "HTMLElement"
|
|
214
|
+
},
|
|
215
|
+
"privacy": "private"
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
"kind": "field",
|
|
219
|
+
"name": "_trigger",
|
|
220
|
+
"type": {
|
|
221
|
+
"text": "WeakRef<HTMLElement> | undefined"
|
|
222
|
+
},
|
|
223
|
+
"privacy": "private",
|
|
224
|
+
"description": "Reference to the trigger button element"
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
"kind": "field",
|
|
228
|
+
"name": "_menu",
|
|
229
|
+
"type": {
|
|
230
|
+
"text": "WeakRef<RCMenu> | undefined"
|
|
231
|
+
},
|
|
232
|
+
"privacy": "private",
|
|
233
|
+
"description": "Reference to the menu element"
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
"kind": "field",
|
|
237
|
+
"name": "_anchorCtrl",
|
|
238
|
+
"privacy": "private",
|
|
239
|
+
"default": "new AnchorController(this, { anchor: () => this._$root ?? null, floating: () => this._$popup ?? null, shadowHost: () => this, placement: 'bottom-start', offset: 2, })"
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
"kind": "field",
|
|
243
|
+
"name": "_boundHandleDocumentClick",
|
|
244
|
+
"privacy": "private",
|
|
245
|
+
"description": "Bound handler for document click (light dismiss)"
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
"kind": "field",
|
|
249
|
+
"name": "_tabObserver",
|
|
250
|
+
"type": {
|
|
251
|
+
"text": "MutationObserver | undefined"
|
|
252
|
+
},
|
|
253
|
+
"privacy": "private",
|
|
254
|
+
"description": "Watches the host's own `tabindex` attribute so the trigger button's\ntabindex can be kept in sync. When a parent (e.g. rc-toolbar) places the\nhost under roving-tabindex management it sets/removes tabindex on the\nhost; we mirror that to the trigger so it doesn't become an independent\ntab stop. When the host has no tabindex the trigger keeps its natural\ntabindex=0 for standalone usage."
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
"kind": "method",
|
|
258
|
+
"name": "openMenu",
|
|
259
|
+
"parameters": [
|
|
260
|
+
{
|
|
261
|
+
"name": "focusTarget",
|
|
262
|
+
"default": "'first'",
|
|
263
|
+
"type": {
|
|
264
|
+
"text": "'first' | 'last'"
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
],
|
|
268
|
+
"description": "Opens the menu and focuses the first or last item"
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
"kind": "method",
|
|
272
|
+
"name": "closeMenu",
|
|
273
|
+
"parameters": [
|
|
274
|
+
{
|
|
275
|
+
"name": "returnFocus",
|
|
276
|
+
"default": "true"
|
|
277
|
+
}
|
|
278
|
+
],
|
|
279
|
+
"description": "Closes the menu and optionally returns focus to trigger"
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
"kind": "method",
|
|
283
|
+
"name": "toggleMenu",
|
|
284
|
+
"description": "Toggles the menu open/closed"
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
"kind": "method",
|
|
288
|
+
"name": "_dispatchToggle",
|
|
289
|
+
"privacy": "private"
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
"kind": "method",
|
|
293
|
+
"name": "_handleTriggerSlotChange",
|
|
294
|
+
"privacy": "private",
|
|
295
|
+
"parameters": [
|
|
296
|
+
{
|
|
297
|
+
"name": "e",
|
|
298
|
+
"type": {
|
|
299
|
+
"text": "Event"
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
]
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
"kind": "method",
|
|
306
|
+
"name": "_syncTriggerTabindex",
|
|
307
|
+
"privacy": "private",
|
|
308
|
+
"description": "Mirrors the host's `tabindex` attribute to the trigger button.\n\nWhen a roving-tabindex parent (e.g. rc-toolbar) marks this host inactive\nwith `tabindex=\"-1\"`, the trigger must also be suppressed so it does not\nbecome an independent tab stop. When the host is the active item\n(`tabindex=\"0\"`) or standalone (no tabindex), the trigger retains its\nnatural focusability so that Chrome's delegatesFocus can route sequential\nTab navigation through the host to the trigger."
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
"kind": "method",
|
|
312
|
+
"name": "focus",
|
|
313
|
+
"parameters": [
|
|
314
|
+
{
|
|
315
|
+
"name": "options",
|
|
316
|
+
"optional": true,
|
|
317
|
+
"type": {
|
|
318
|
+
"text": "FocusOptions"
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
],
|
|
322
|
+
"description": "Overrides the default focus() so that programmatic focus calls (e.g. from\na roving-tabindex parent navigating back to this element via arrow keys)\nalways reach the trigger.\n\nChrome's delegatesFocus will not delegate to a tabindex=\"-1\" element, so\nwhen the toolbar marks this host inactive it suppresses the trigger — and\nthe next focusItem() call silently fails. Lifting the suppression here and\ndirectly calling trigger.focus() bypasses that restriction."
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
"kind": "method",
|
|
326
|
+
"name": "_handleMenuSlotChange",
|
|
327
|
+
"privacy": "private",
|
|
328
|
+
"parameters": [
|
|
329
|
+
{
|
|
330
|
+
"name": "e",
|
|
331
|
+
"type": {
|
|
332
|
+
"text": "Event"
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
]
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
"kind": "field",
|
|
339
|
+
"name": "_onNavigate",
|
|
340
|
+
"privacy": "private"
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
"kind": "method",
|
|
344
|
+
"name": "_handleTriggerClick",
|
|
345
|
+
"privacy": "private",
|
|
346
|
+
"parameters": [
|
|
347
|
+
{
|
|
348
|
+
"name": "e",
|
|
349
|
+
"type": {
|
|
350
|
+
"text": "MouseEvent"
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
]
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
"kind": "method",
|
|
357
|
+
"name": "_handleMenuClose",
|
|
358
|
+
"privacy": "private"
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
"kind": "method",
|
|
362
|
+
"name": "_handleMenuActivate",
|
|
363
|
+
"privacy": "private"
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
"kind": "method",
|
|
367
|
+
"name": "_handleDocumentClick",
|
|
368
|
+
"privacy": "private",
|
|
369
|
+
"parameters": [
|
|
370
|
+
{
|
|
371
|
+
"name": "e",
|
|
372
|
+
"type": {
|
|
373
|
+
"text": "MouseEvent"
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
]
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
"kind": "method",
|
|
380
|
+
"name": "_setOpen",
|
|
381
|
+
"privacy": "private",
|
|
382
|
+
"return": {
|
|
383
|
+
"type": {
|
|
384
|
+
"text": "void"
|
|
385
|
+
}
|
|
386
|
+
},
|
|
387
|
+
"parameters": [
|
|
388
|
+
{
|
|
389
|
+
"name": "open",
|
|
390
|
+
"type": {
|
|
391
|
+
"text": "boolean"
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
"name": "dispatch",
|
|
396
|
+
"type": {
|
|
397
|
+
"text": "boolean"
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
]
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
"kind": "method",
|
|
404
|
+
"name": "_syncTriggerAria",
|
|
405
|
+
"privacy": "private",
|
|
406
|
+
"return": {
|
|
407
|
+
"type": {
|
|
408
|
+
"text": "void"
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
],
|
|
413
|
+
"events": [
|
|
414
|
+
{
|
|
415
|
+
"name": "rc-menu-button-toggle",
|
|
416
|
+
"type": {
|
|
417
|
+
"text": "CustomEvent"
|
|
418
|
+
},
|
|
419
|
+
"description": "Fired when the menu opens or closes"
|
|
420
|
+
}
|
|
421
|
+
],
|
|
422
|
+
"attributes": [
|
|
423
|
+
{
|
|
424
|
+
"description": "Preferred placement of the popup relative to the trigger button.",
|
|
425
|
+
"name": "placement",
|
|
426
|
+
"type": {
|
|
427
|
+
"text": "AnchorPlacement"
|
|
428
|
+
},
|
|
429
|
+
"default": "'bottom-start'",
|
|
430
|
+
"fieldName": "placement"
|
|
431
|
+
},
|
|
432
|
+
{
|
|
433
|
+
"name": "open",
|
|
434
|
+
"type": {
|
|
435
|
+
"text": "boolean"
|
|
436
|
+
},
|
|
437
|
+
"description": "Whether the menu is currently open. Host writes update silently.",
|
|
438
|
+
"fieldName": "open"
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
"name": "default-open",
|
|
442
|
+
"type": {
|
|
443
|
+
"text": "boolean"
|
|
444
|
+
},
|
|
445
|
+
"description": "Initial uncontrolled open state.",
|
|
446
|
+
"fieldName": "defaultOpen"
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
"name": "orientation",
|
|
450
|
+
"type": {
|
|
451
|
+
"text": "'horizontal' | 'vertical' | undefined"
|
|
452
|
+
},
|
|
453
|
+
"description": "Orientation of this menu button, affects which arrow keys open/close the menu.\nIf not set, inherits from a parent rc-menubar or element with role=\"menubar\".",
|
|
454
|
+
"fieldName": "orientation"
|
|
455
|
+
}
|
|
456
|
+
],
|
|
457
|
+
"superclass": {
|
|
458
|
+
"name": "LitElement",
|
|
459
|
+
"package": "lit"
|
|
460
|
+
},
|
|
461
|
+
"tagName": "rc-menu-button",
|
|
462
|
+
"customElement": true
|
|
463
|
+
}
|
|
464
|
+
],
|
|
465
|
+
"exports": [
|
|
466
|
+
{
|
|
467
|
+
"kind": "js",
|
|
468
|
+
"name": "RCMenuButton",
|
|
469
|
+
"declaration": {
|
|
470
|
+
"name": "RCMenuButton",
|
|
471
|
+
"module": "src/rc-menu-button.ts"
|
|
472
|
+
}
|
|
473
|
+
},
|
|
474
|
+
{
|
|
475
|
+
"kind": "js",
|
|
476
|
+
"name": "default",
|
|
477
|
+
"declaration": {
|
|
478
|
+
"name": "RCMenuButton",
|
|
479
|
+
"module": "src/rc-menu-button.ts"
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
]
|
|
483
|
+
}
|
|
484
|
+
]
|
|
485
|
+
}
|
package/dist/demo.css
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
*, *::before, *::after {
|
|
2
|
+
box-sizing: border-box;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
:root {
|
|
6
|
+
color-scheme: light dark;
|
|
7
|
+
--rc-accent: Highlight; /* Chrome doesn't support AccentColor on the open web */
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
@supports (color: AccentColor) {
|
|
11
|
+
:root {
|
|
12
|
+
--rc-accent: AccentColor;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
[data-theme="light"] { color-scheme: light; }
|
|
17
|
+
[data-theme="dark"] { color-scheme: dark; }
|
|
18
|
+
|
|
19
|
+
[data-theme="solarized-light"] {
|
|
20
|
+
color-scheme: light;
|
|
21
|
+
--rc-surface: #fdf6e3;
|
|
22
|
+
--rc-text: #657b83;
|
|
23
|
+
--rc-border: 1px solid #93a1a1;
|
|
24
|
+
--rc-shadow: 0 2px 8px rgba(0, 0, 0, .12);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
[data-theme="solarized-dark"] {
|
|
28
|
+
color-scheme: dark;
|
|
29
|
+
--rc-surface: #002b36;
|
|
30
|
+
--rc-text: #839496;
|
|
31
|
+
--rc-border: 1px solid #586e75;
|
|
32
|
+
--rc-shadow: 0 2px 8px rgba(0, 0, 0, .3);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
body {
|
|
36
|
+
font-family: system-ui, sans-serif;
|
|
37
|
+
margin: 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.demo-page {
|
|
41
|
+
max-width: 60rem;
|
|
42
|
+
margin: 0 auto;
|
|
43
|
+
padding: 2rem 1.5rem;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.demo-controls {
|
|
47
|
+
display: flex;
|
|
48
|
+
align-items: center;
|
|
49
|
+
gap: 1rem;
|
|
50
|
+
margin-bottom: 2rem;
|
|
51
|
+
padding-bottom: 1rem;
|
|
52
|
+
border-bottom: 1px solid ButtonBorder;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.demo-controls .demo-title {
|
|
56
|
+
flex: 1;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.demo-controls h1 {
|
|
60
|
+
margin: 0 0 0.15rem;
|
|
61
|
+
font-size: 1.5rem;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.demo-controls a {
|
|
65
|
+
font-size: 0.8rem;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.demo-section {
|
|
69
|
+
margin-bottom: 2rem;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.demo-section h2 {
|
|
73
|
+
margin: 0 0 0.5rem;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.theme-picker {
|
|
77
|
+
padding: 0.35em 0.75em;
|
|
78
|
+
font-family: inherit;
|
|
79
|
+
font-size: 0.8rem;
|
|
80
|
+
cursor: pointer;
|
|
81
|
+
border: 1px solid ButtonBorder;
|
|
82
|
+
border-radius: 4px;
|
|
83
|
+
background: Canvas;
|
|
84
|
+
color: CanvasText;
|
|
85
|
+
}
|
package/dist/demo.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const THEMES = ['', 'light', 'dark', 'solarized-light', 'solarized-dark'];
|
|
2
|
+
const LABELS = ['Auto', 'Light', 'Dark', 'Solarized ☀', 'Solarized ☾'];
|
|
3
|
+
|
|
4
|
+
const stored = localStorage.getItem('rc-demo-theme') ?? '';
|
|
5
|
+
applyTheme(stored);
|
|
6
|
+
|
|
7
|
+
function applyTheme(theme) {
|
|
8
|
+
if (theme) {
|
|
9
|
+
document.documentElement.dataset.theme = theme;
|
|
10
|
+
} else {
|
|
11
|
+
delete document.documentElement.dataset.theme;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function currentIndex() {
|
|
16
|
+
const current = document.documentElement.dataset.theme ?? '';
|
|
17
|
+
const idx = THEMES.indexOf(current);
|
|
18
|
+
return idx === -1 ? 0 : idx;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function updateButtons() {
|
|
22
|
+
const label = LABELS[currentIndex()];
|
|
23
|
+
document.querySelectorAll('.theme-picker').forEach((btn) => {
|
|
24
|
+
btn.textContent = label;
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
window.cycleTheme = function () {
|
|
29
|
+
const next = THEMES[(currentIndex() + 1) % THEMES.length];
|
|
30
|
+
applyTheme(next);
|
|
31
|
+
localStorage.setItem('rc-demo-theme', next);
|
|
32
|
+
updateButtons();
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
36
|
+
updateButtons();
|
|
37
|
+
document.querySelectorAll('.theme-picker').forEach((btn) => {
|
|
38
|
+
btn.addEventListener('click', window.cycleTheme);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { css as g, LitElement as p, html as b } from "lit";
|
|
2
|
+
import { property as l, query as h, state as d } from "lit/decorators.js";
|
|
3
|
+
import { AnchorController as f, keyNavigation as m } from "@rcarls/rc-common";
|
|
4
|
+
const _ = g`
|
|
5
|
+
:host {
|
|
6
|
+
display: inline-block;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
:host([orientation='vertical']) {
|
|
10
|
+
display: block;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
#root {
|
|
14
|
+
display: inline-block;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
:host([orientation='vertical']) #root {
|
|
18
|
+
display: block;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
#trigger-wrap {
|
|
22
|
+
display: contents;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
slot[name='trigger']::slotted(button),
|
|
26
|
+
slot[name='trigger']::slotted([role='button']) {
|
|
27
|
+
min-block-size: var(--rc-menu-button-trigger-block-size, var(--rc-control-block-size, 2.25em));
|
|
28
|
+
padding: var(--rc-menu-button-trigger-padding-block, var(--rc-control-padding-block, 0.25em))
|
|
29
|
+
var(--rc-menu-button-trigger-padding-inline, var(--rc-control-padding-inline, 0.5em));
|
|
30
|
+
border: var(--rc-menu-button-trigger-border, var(--rc-border, 1px solid ButtonBorder));
|
|
31
|
+
border-radius: var(--rc-menu-button-trigger-radius, var(--rc-control-radius, 0.125em));
|
|
32
|
+
background: var(--rc-menu-button-trigger-background, var(--rc-button-bg, ButtonFace));
|
|
33
|
+
color: var(--rc-menu-button-trigger-color, var(--rc-button-text, ButtonText));
|
|
34
|
+
font: inherit;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
:host([orientation='vertical']) slot[name='trigger']::slotted(button),
|
|
38
|
+
:host([orientation='vertical']) slot[name='trigger']::slotted([role='button']) {
|
|
39
|
+
inline-size: 100%;
|
|
40
|
+
text-align: start;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
#popup {
|
|
44
|
+
z-index: var(--rc-menu-button-popup-z-index, 1000);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
#popup[hidden] {
|
|
48
|
+
display: none;
|
|
49
|
+
}
|
|
50
|
+
`;
|
|
51
|
+
var v = Object.defineProperty, O = Object.getOwnPropertyDescriptor, o = (u, t, e, r) => {
|
|
52
|
+
for (var n = r > 1 ? void 0 : r ? O(t, e) : t, s = u.length - 1, c; s >= 0; s--)
|
|
53
|
+
(c = u[s]) && (n = (r ? c(t, e, n) : c(n)) || n);
|
|
54
|
+
return r && n && v(t, e, n), n;
|
|
55
|
+
};
|
|
56
|
+
const a = class a extends p {
|
|
57
|
+
constructor() {
|
|
58
|
+
super(...arguments), this._defaultOpen = !1, this._open = !1, this.placement = "bottom-start", this._anchorCtrl = new f(this, {
|
|
59
|
+
anchor: () => this._$root ?? null,
|
|
60
|
+
floating: () => this._$popup ?? null,
|
|
61
|
+
shadowHost: () => this,
|
|
62
|
+
placement: "bottom-start",
|
|
63
|
+
offset: 2
|
|
64
|
+
}), this._boundHandleDocumentClick = this._handleDocumentClick.bind(this), this._onNavigate = (t) => {
|
|
65
|
+
switch (t) {
|
|
66
|
+
case "open-to-first":
|
|
67
|
+
case "activate":
|
|
68
|
+
this.openMenu();
|
|
69
|
+
break;
|
|
70
|
+
case "open-to-last":
|
|
71
|
+
this.openMenu("last");
|
|
72
|
+
break;
|
|
73
|
+
case "escape":
|
|
74
|
+
this.closeMenu();
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
get open() {
|
|
80
|
+
return this._open;
|
|
81
|
+
}
|
|
82
|
+
/** Whether the menu is currently open. Host writes update silently. */
|
|
83
|
+
set open(t) {
|
|
84
|
+
const e = this._open;
|
|
85
|
+
t !== void 0 && (this._setOpen(t, !1), this.requestUpdate("open", e));
|
|
86
|
+
}
|
|
87
|
+
get defaultOpen() {
|
|
88
|
+
return this._defaultOpen;
|
|
89
|
+
}
|
|
90
|
+
/** Initial uncontrolled open state. */
|
|
91
|
+
set defaultOpen(t) {
|
|
92
|
+
const e = this._defaultOpen;
|
|
93
|
+
this._defaultOpen = t, t && !this._open && this._setOpen(!0, !1), this.requestUpdate("defaultOpen", e);
|
|
94
|
+
}
|
|
95
|
+
get _effectivePlacement() {
|
|
96
|
+
return this.placement !== "bottom-start" ? this.placement : this._resolvedOrientation === "vertical" ? "right-start" : this.placement;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Resolved orientation, considering inheritance from parent menubar.
|
|
100
|
+
* Falls back to 'horizontal' if no orientation is set or inherited.
|
|
101
|
+
*/
|
|
102
|
+
get _resolvedOrientation() {
|
|
103
|
+
if (this.orientation) return this.orientation;
|
|
104
|
+
const t = this.closest('rc-menubar, [role="menubar"]');
|
|
105
|
+
if (t) {
|
|
106
|
+
const e = t.getAttribute("orientation") ?? t.getAttribute("aria-orientation");
|
|
107
|
+
if (e === "vertical") return "vertical";
|
|
108
|
+
if (e === "horizontal") return "horizontal";
|
|
109
|
+
}
|
|
110
|
+
return "horizontal";
|
|
111
|
+
}
|
|
112
|
+
connectedCallback() {
|
|
113
|
+
super.connectedCallback(), this._tabObserver = new MutationObserver(() => this._syncTriggerTabindex()), this._tabObserver.observe(this, { attributes: !0, attributeFilter: ["tabindex"] }), document.addEventListener("click", this._boundHandleDocumentClick, !0), this.defaultOpen && this._setOpen(!0, !1);
|
|
114
|
+
}
|
|
115
|
+
disconnectedCallback() {
|
|
116
|
+
this._tabObserver?.disconnect(), this._tabObserver = void 0, super.disconnectedCallback(), document.removeEventListener("click", this._boundHandleDocumentClick, !0);
|
|
117
|
+
}
|
|
118
|
+
/** Opens the menu and focuses the first or last item */
|
|
119
|
+
openMenu(t = "first") {
|
|
120
|
+
this.open || (this._anchorCtrl.setOptions({ placement: this._effectivePlacement }), this._setOpen(!0, !0), this._anchorCtrl.update(), this.updateComplete.then(() => {
|
|
121
|
+
const e = this._menu?.deref();
|
|
122
|
+
t === "last" ? e?.focusLast() : e?.focusFirst();
|
|
123
|
+
}));
|
|
124
|
+
}
|
|
125
|
+
/** Closes the menu and optionally returns focus to trigger */
|
|
126
|
+
closeMenu(t = !0) {
|
|
127
|
+
this.open && (this._setOpen(!1, !0), t && this._trigger?.deref()?.focus());
|
|
128
|
+
}
|
|
129
|
+
/** Toggles the menu open/closed */
|
|
130
|
+
toggleMenu() {
|
|
131
|
+
this.open ? this.closeMenu() : this.openMenu();
|
|
132
|
+
}
|
|
133
|
+
_dispatchToggle() {
|
|
134
|
+
this.dispatchEvent(
|
|
135
|
+
new CustomEvent("rc-menu-button-toggle", {
|
|
136
|
+
bubbles: !0,
|
|
137
|
+
composed: !0,
|
|
138
|
+
detail: { open: this.open }
|
|
139
|
+
})
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
_handleTriggerSlotChange(t) {
|
|
143
|
+
const r = t.currentTarget.assignedElements()[0];
|
|
144
|
+
r && (this._trigger = new WeakRef(r), this._syncTriggerAria(), this._syncTriggerTabindex());
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Mirrors the host's `tabindex` attribute to the trigger button.
|
|
148
|
+
*
|
|
149
|
+
* When a roving-tabindex parent (e.g. rc-toolbar) marks this host inactive
|
|
150
|
+
* with `tabindex="-1"`, the trigger must also be suppressed so it does not
|
|
151
|
+
* become an independent tab stop. When the host is the active item
|
|
152
|
+
* (`tabindex="0"`) or standalone (no tabindex), the trigger retains its
|
|
153
|
+
* natural focusability so that Chrome's delegatesFocus can route sequential
|
|
154
|
+
* Tab navigation through the host to the trigger.
|
|
155
|
+
*/
|
|
156
|
+
_syncTriggerTabindex() {
|
|
157
|
+
const t = this._trigger?.deref();
|
|
158
|
+
t && (this.getAttribute("tabindex") === "-1" ? t.setAttribute("tabindex", "-1") : t.removeAttribute("tabindex"));
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Overrides the default focus() so that programmatic focus calls (e.g. from
|
|
162
|
+
* a roving-tabindex parent navigating back to this element via arrow keys)
|
|
163
|
+
* always reach the trigger.
|
|
164
|
+
*
|
|
165
|
+
* Chrome's delegatesFocus will not delegate to a tabindex="-1" element, so
|
|
166
|
+
* when the toolbar marks this host inactive it suppresses the trigger — and
|
|
167
|
+
* the next focusItem() call silently fails. Lifting the suppression here and
|
|
168
|
+
* directly calling trigger.focus() bypasses that restriction.
|
|
169
|
+
*/
|
|
170
|
+
focus(t) {
|
|
171
|
+
const e = this._trigger?.deref();
|
|
172
|
+
if (!e) {
|
|
173
|
+
super.focus(t);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
e.removeAttribute("tabindex"), e.focus(t);
|
|
177
|
+
}
|
|
178
|
+
_handleMenuSlotChange(t) {
|
|
179
|
+
const n = t.currentTarget.assignedElements().find((s) => s.tagName === "RC-MENU");
|
|
180
|
+
n && (this._menu = new WeakRef(n));
|
|
181
|
+
}
|
|
182
|
+
_handleTriggerClick(t) {
|
|
183
|
+
t.preventDefault(), this.toggleMenu();
|
|
184
|
+
}
|
|
185
|
+
_handleMenuClose() {
|
|
186
|
+
this.closeMenu();
|
|
187
|
+
}
|
|
188
|
+
_handleMenuActivate() {
|
|
189
|
+
this.closeMenu();
|
|
190
|
+
}
|
|
191
|
+
_handleDocumentClick(t) {
|
|
192
|
+
!this.open || t.composedPath().includes(this) || this.closeMenu(!1);
|
|
193
|
+
}
|
|
194
|
+
updated(t) {
|
|
195
|
+
super.updated(t), t.has("open") && this._syncTriggerAria(), (t.has("placement") || t.has("orientation")) && this._anchorCtrl.setOptions({ placement: this._effectivePlacement });
|
|
196
|
+
}
|
|
197
|
+
_setOpen(t, e) {
|
|
198
|
+
if (this._open === t) return;
|
|
199
|
+
const r = this._open;
|
|
200
|
+
this._open = t, this.requestUpdate("open", r), this._syncTriggerAria(), e && this._dispatchToggle();
|
|
201
|
+
}
|
|
202
|
+
_syncTriggerAria() {
|
|
203
|
+
const t = this._trigger?.deref();
|
|
204
|
+
t?.isConnected && (t.setAttribute("aria-haspopup", "menu"), t.setAttribute("aria-expanded", String(this.open)));
|
|
205
|
+
}
|
|
206
|
+
render() {
|
|
207
|
+
return b`
|
|
208
|
+
<div id="root" part="root">
|
|
209
|
+
<div
|
|
210
|
+
id="trigger-wrap"
|
|
211
|
+
${m(this._onNavigate, {
|
|
212
|
+
navigationAxis: this._resolvedOrientation,
|
|
213
|
+
handleNavAxis: !1,
|
|
214
|
+
handleOpenAxis: !0,
|
|
215
|
+
handleActivate: !0,
|
|
216
|
+
handleEscape: this.open
|
|
217
|
+
})}
|
|
218
|
+
@click=${this._handleTriggerClick}
|
|
219
|
+
>
|
|
220
|
+
<slot
|
|
221
|
+
name="trigger"
|
|
222
|
+
@slotchange=${this._handleTriggerSlotChange}
|
|
223
|
+
></slot>
|
|
224
|
+
</div>
|
|
225
|
+
<div
|
|
226
|
+
id="popup"
|
|
227
|
+
part="popup"
|
|
228
|
+
?hidden=${!this.open}
|
|
229
|
+
@rc-menu-close=${this._handleMenuClose}
|
|
230
|
+
@rc-menu-activate=${this._handleMenuActivate}
|
|
231
|
+
>
|
|
232
|
+
<slot @slotchange=${this._handleMenuSlotChange}></slot>
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
`;
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
a.styles = [_], a.shadowRootOptions = {
|
|
239
|
+
...p.shadowRootOptions,
|
|
240
|
+
delegatesFocus: !0
|
|
241
|
+
};
|
|
242
|
+
let i = a;
|
|
243
|
+
o([
|
|
244
|
+
l({ type: Boolean, reflect: !0 })
|
|
245
|
+
], i.prototype, "open", 1);
|
|
246
|
+
o([
|
|
247
|
+
l({ type: Boolean, attribute: "default-open" })
|
|
248
|
+
], i.prototype, "defaultOpen", 1);
|
|
249
|
+
o([
|
|
250
|
+
l({ type: String, reflect: !0 })
|
|
251
|
+
], i.prototype, "orientation", 2);
|
|
252
|
+
o([
|
|
253
|
+
l({ reflect: !0 })
|
|
254
|
+
], i.prototype, "placement", 2);
|
|
255
|
+
o([
|
|
256
|
+
h("#root")
|
|
257
|
+
], i.prototype, "_$root", 2);
|
|
258
|
+
o([
|
|
259
|
+
h("#popup")
|
|
260
|
+
], i.prototype, "_$popup", 2);
|
|
261
|
+
o([
|
|
262
|
+
d()
|
|
263
|
+
], i.prototype, "_trigger", 2);
|
|
264
|
+
o([
|
|
265
|
+
d()
|
|
266
|
+
], i.prototype, "_menu", 2);
|
|
267
|
+
export {
|
|
268
|
+
i as R
|
|
269
|
+
};
|
|
270
|
+
//# sourceMappingURL=rc-menu-button-CmzW7P4h.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rc-menu-button-CmzW7P4h.js","sources":["../src/rc-menu-button.styles.ts","../src/rc-menu-button.ts"],"sourcesContent":["import { css } from 'lit';\n\nexport const menuButtonStyles = css`\n :host {\n display: inline-block;\n }\n\n :host([orientation='vertical']) {\n display: block;\n }\n\n #root {\n display: inline-block;\n }\n\n :host([orientation='vertical']) #root {\n display: block;\n }\n\n #trigger-wrap {\n display: contents;\n }\n\n slot[name='trigger']::slotted(button),\n slot[name='trigger']::slotted([role='button']) {\n min-block-size: var(--rc-menu-button-trigger-block-size, var(--rc-control-block-size, 2.25em));\n padding: var(--rc-menu-button-trigger-padding-block, var(--rc-control-padding-block, 0.25em))\n var(--rc-menu-button-trigger-padding-inline, var(--rc-control-padding-inline, 0.5em));\n border: var(--rc-menu-button-trigger-border, var(--rc-border, 1px solid ButtonBorder));\n border-radius: var(--rc-menu-button-trigger-radius, var(--rc-control-radius, 0.125em));\n background: var(--rc-menu-button-trigger-background, var(--rc-button-bg, ButtonFace));\n color: var(--rc-menu-button-trigger-color, var(--rc-button-text, ButtonText));\n font: inherit;\n }\n\n :host([orientation='vertical']) slot[name='trigger']::slotted(button),\n :host([orientation='vertical']) slot[name='trigger']::slotted([role='button']) {\n inline-size: 100%;\n text-align: start;\n }\n\n #popup {\n z-index: var(--rc-menu-button-popup-z-index, 1000);\n }\n\n #popup[hidden] {\n display: none;\n }\n`;\n\nexport default menuButtonStyles;\n","import { LitElement, html } from 'lit';\nimport { property, query, state } from 'lit/decorators.js';\n\nimport {\n AnchorController,\n keyNavigation,\n type AnchorPlacement,\n type KeyboardNavigationAction,\n} from '@rcarls/rc-common';\nimport type { RCMenu } from '@rcarls/rc-menu';\n\nimport menuButtonStyles from './rc-menu-button.styles';\n\nexport interface RCMenuButtonToggleEvent {\n open: boolean;\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'rc-menu-button': RCMenuButton;\n }\n}\n\n/**\n * A menu button component that opens/closes a menu popup\n *\n * @see https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/\n * @slot trigger - The button element that triggers the menu\n * @slot default - The rc-menu element to display as popup\n * @fires rc-menu-button-toggle - Fired when the menu opens or closes\n * @cssprop [--rc-menu-button-trigger-block-size=var(--rc-control-block-size)] - Minimum block size of the trigger\n * @cssprop [--rc-menu-button-trigger-padding-block=var(--rc-control-padding-block)] - Trigger block-axis padding\n * @cssprop [--rc-menu-button-trigger-padding-inline=var(--rc-control-padding-inline)] - Trigger inline-axis padding\n * @cssprop [--rc-menu-button-trigger-border=var(--rc-border)] - Trigger border\n * @cssprop [--rc-menu-button-trigger-radius=var(--rc-control-radius)] - Trigger border radius\n * @cssprop [--rc-menu-button-trigger-background=var(--rc-button-bg)] - Trigger background\n * @cssprop [--rc-menu-button-trigger-color=var(--rc-button-text)] - Trigger text color\n * @cssprop [--rc-menu-button-popup-z-index=1000] - Z-index of the popup overlay\n * @csspart root - The root container element\n * @csspart popup - The popup container element\n * @attr placement - Preferred placement of the popup relative to the trigger.\n */\nexport class RCMenuButton extends LitElement {\n static styles = [menuButtonStyles];\n\n /**\n * Enables delegatesFocus so the browser routes focus() on the host element\n * to the slotted trigger button. This lets the host participate in a\n * roving-tabindex group (e.g. rc-toolbar) as a single tab stop while the\n * trigger remains the visible focus target.\n */\n static override shadowRootOptions: ShadowRootInit = {\n ...LitElement.shadowRootOptions,\n delegatesFocus: true,\n };\n\n private _defaultOpen = false;\n private _open = false;\n\n /** Whether the menu is currently open. Host writes update silently. */\n @property({ type: Boolean, reflect: true })\n get open(): boolean {\n return this._open;\n }\n\n /** Whether the menu is currently open. Host writes update silently. */\n set open(value: boolean | undefined) {\n const oldValue = this._open;\n\n if (value === undefined) return;\n\n this._setOpen(value, false);\n this.requestUpdate('open', oldValue);\n }\n\n /** Initial uncontrolled open state. */\n @property({ type: Boolean, attribute: 'default-open' })\n get defaultOpen(): boolean {\n return this._defaultOpen;\n }\n\n /** Initial uncontrolled open state. */\n set defaultOpen(value: boolean) {\n const oldValue = this._defaultOpen;\n\n this._defaultOpen = value;\n\n if (value && !this._open) {\n this._setOpen(true, false);\n }\n\n this.requestUpdate('defaultOpen', oldValue);\n }\n\n /**\n * Orientation of this menu button, affects which arrow keys open/close the menu.\n * If not set, inherits from a parent rc-menubar or element with role=\"menubar\".\n */\n @property({ type: String, reflect: true })\n orientation: 'horizontal' | 'vertical' | undefined;\n\n /** Preferred placement of the popup relative to the trigger button. */\n @property({ reflect: true })\n placement: AnchorPlacement = 'bottom-start';\n\n private get _effectivePlacement(): AnchorPlacement {\n if (this.placement !== 'bottom-start') return this.placement;\n return this._resolvedOrientation === 'vertical' ? 'right-start' : this.placement;\n }\n\n /**\n * Resolved orientation, considering inheritance from parent menubar.\n * Falls back to 'horizontal' if no orientation is set or inherited.\n */\n private get _resolvedOrientation(): 'horizontal' | 'vertical' {\n if (this.orientation) return this.orientation;\n\n // Inherit from parent rc-menubar or element with role=\"menubar\"\n const menubar = this.closest('rc-menubar, [role=\"menubar\"]');\n if (menubar) {\n const orientation =\n menubar.getAttribute('orientation') ??\n menubar.getAttribute('aria-orientation');\n if (orientation === 'vertical') return 'vertical';\n if (orientation === 'horizontal') return 'horizontal';\n }\n\n return 'horizontal';\n }\n\n @query('#root') private _$root!: HTMLElement;\n @query('#popup') private _$popup!: HTMLElement;\n\n /** Reference to the trigger button element */\n @state()\n private _trigger: WeakRef<HTMLElement> | undefined;\n\n /** Reference to the menu element */\n @state()\n private _menu: WeakRef<RCMenu> | undefined;\n\n private _anchorCtrl = new AnchorController(this, {\n anchor: () => this._$root ?? null,\n floating: () => this._$popup ?? null,\n shadowHost: () => this,\n placement: 'bottom-start',\n offset: 2,\n });\n\n /** Bound handler for document click (light dismiss) */\n private _boundHandleDocumentClick = this._handleDocumentClick.bind(this);\n\n /**\n * Watches the host's own `tabindex` attribute so the trigger button's\n * tabindex can be kept in sync. When a parent (e.g. rc-toolbar) places the\n * host under roving-tabindex management it sets/removes tabindex on the\n * host; we mirror that to the trigger so it doesn't become an independent\n * tab stop. When the host has no tabindex the trigger keeps its natural\n * tabindex=0 for standalone usage.\n */\n private _tabObserver?: MutationObserver;\n\n connectedCallback() {\n super.connectedCallback();\n\n this._tabObserver = new MutationObserver(() => this._syncTriggerTabindex());\n this._tabObserver.observe(this, { attributes: true, attributeFilter: ['tabindex'] });\n\n document.addEventListener('click', this._boundHandleDocumentClick, true);\n\n if (this.defaultOpen) {\n this._setOpen(true, false);\n }\n }\n\n disconnectedCallback() {\n this._tabObserver?.disconnect();\n this._tabObserver = undefined;\n\n super.disconnectedCallback();\n document.removeEventListener('click', this._boundHandleDocumentClick, true);\n }\n\n /** Opens the menu and focuses the first or last item */\n openMenu(focusTarget: 'first' | 'last' = 'first') {\n if (this.open) return;\n\n this._anchorCtrl.setOptions({ placement: this._effectivePlacement });\n this._setOpen(true, true);\n this._anchorCtrl.update();\n\n this.updateComplete.then(() => {\n const menu = this._menu?.deref();\n focusTarget === 'last' ? menu?.focusLast() : menu?.focusFirst();\n });\n }\n\n /** Closes the menu and optionally returns focus to trigger */\n closeMenu(returnFocus = true) {\n if (!this.open) return;\n\n this._setOpen(false, true);\n\n if (returnFocus) {\n const trigger = this._trigger?.deref();\n trigger?.focus();\n }\n }\n\n /** Toggles the menu open/closed */\n toggleMenu() {\n if (this.open) {\n this.closeMenu();\n } else {\n this.openMenu();\n }\n }\n\n private _dispatchToggle() {\n this.dispatchEvent(\n new CustomEvent<RCMenuButtonToggleEvent>('rc-menu-button-toggle', {\n bubbles: true,\n composed: true,\n detail: { open: this.open },\n }),\n );\n }\n\n private _handleTriggerSlotChange(e: Event) {\n const slot = e.currentTarget as HTMLSlotElement;\n const trigger = slot.assignedElements()[0] as HTMLElement | undefined;\n\n if (trigger) {\n this._trigger = new WeakRef(trigger);\n this._syncTriggerAria();\n this._syncTriggerTabindex();\n }\n }\n\n /**\n * Mirrors the host's `tabindex` attribute to the trigger button.\n *\n * When a roving-tabindex parent (e.g. rc-toolbar) marks this host inactive\n * with `tabindex=\"-1\"`, the trigger must also be suppressed so it does not\n * become an independent tab stop. When the host is the active item\n * (`tabindex=\"0\"`) or standalone (no tabindex), the trigger retains its\n * natural focusability so that Chrome's delegatesFocus can route sequential\n * Tab navigation through the host to the trigger.\n */\n private _syncTriggerTabindex() {\n const trigger = this._trigger?.deref();\n\n if (!trigger) return;\n\n if (this.getAttribute('tabindex') === '-1') {\n trigger.setAttribute('tabindex', '-1');\n } else {\n trigger.removeAttribute('tabindex');\n }\n }\n\n /**\n * Overrides the default focus() so that programmatic focus calls (e.g. from\n * a roving-tabindex parent navigating back to this element via arrow keys)\n * always reach the trigger.\n *\n * Chrome's delegatesFocus will not delegate to a tabindex=\"-1\" element, so\n * when the toolbar marks this host inactive it suppresses the trigger — and\n * the next focusItem() call silently fails. Lifting the suppression here and\n * directly calling trigger.focus() bypasses that restriction.\n */\n override focus(options?: FocusOptions) {\n const trigger = this._trigger?.deref();\n\n if (!trigger) {\n super.focus(options);\n return;\n }\n\n trigger.removeAttribute('tabindex');\n trigger.focus(options);\n }\n\n\n private _handleMenuSlotChange(e: Event) {\n const slot = e.currentTarget as HTMLSlotElement;\n const elements = slot.assignedElements();\n const menu = elements.find((el) => el.tagName === 'RC-MENU') as\n | RCMenu\n | undefined;\n\n if (menu) {\n this._menu = new WeakRef(menu);\n }\n }\n\n private _onNavigate = (action: KeyboardNavigationAction) => {\n switch (action) {\n case 'open-to-first':\n case 'activate':\n this.openMenu();\n break;\n case 'open-to-last':\n this.openMenu('last');\n break;\n case 'escape':\n this.closeMenu();\n break;\n }\n };\n\n private _handleTriggerClick(e: MouseEvent) {\n e.preventDefault();\n this.toggleMenu();\n }\n\n private _handleMenuClose() {\n this.closeMenu();\n }\n\n private _handleMenuActivate() {\n // Close menu after item activation\n this.closeMenu();\n }\n\n private _handleDocumentClick(e: MouseEvent) {\n if (!this.open) return;\n\n const path = e.composedPath();\n\n // Check if click was inside this component\n if (path.includes(this)) return;\n\n // Close menu on outside click\n this.closeMenu(false);\n }\n\n protected updated(changedProperties: Map<PropertyKey, unknown>) {\n super.updated(changedProperties);\n\n // Keep aria-expanded in sync\n if (changedProperties.has('open')) {\n this._syncTriggerAria();\n }\n\n if (\n changedProperties.has('placement') ||\n changedProperties.has('orientation')\n ) {\n this._anchorCtrl.setOptions({ placement: this._effectivePlacement });\n }\n }\n\n private _setOpen(open: boolean, dispatch: boolean): void {\n if (this._open === open) return;\n\n const oldValue = this._open;\n\n this._open = open;\n this.requestUpdate('open', oldValue);\n this._syncTriggerAria();\n\n if (dispatch) this._dispatchToggle();\n }\n\n private _syncTriggerAria(): void {\n const trigger = this._trigger?.deref();\n if (!trigger?.isConnected) return;\n\n trigger.setAttribute('aria-haspopup', 'menu');\n trigger.setAttribute('aria-expanded', String(this.open));\n }\n\n protected render() {\n return html`\n <div id=\"root\" part=\"root\">\n <div\n id=\"trigger-wrap\"\n ${keyNavigation(this._onNavigate, {\n navigationAxis: this._resolvedOrientation,\n handleNavAxis: false,\n handleOpenAxis: true,\n handleActivate: true,\n handleEscape: this.open,\n })}\n @click=${this._handleTriggerClick}\n >\n <slot\n name=\"trigger\"\n @slotchange=${this._handleTriggerSlotChange}\n ></slot>\n </div>\n <div\n id=\"popup\"\n part=\"popup\"\n ?hidden=${!this.open}\n @rc-menu-close=${this._handleMenuClose}\n @rc-menu-activate=${this._handleMenuActivate}\n >\n <slot @slotchange=${this._handleMenuSlotChange}></slot>\n </div>\n </div>\n `;\n }\n}\n\nexport default RCMenuButton;\n"],"names":["menuButtonStyles","css","_RCMenuButton","LitElement","AnchorController","action","value","oldValue","menubar","orientation","focusTarget","menu","returnFocus","e","trigger","options","el","changedProperties","open","dispatch","html","keyNavigation","RCMenuButton","__decorateClass","property","query","state"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rc-menu-button-define.js","sources":["../src/define.ts"],"sourcesContent":["import '@rcarls/rc-menu/define';\nimport { RCMenuButton } from './index.js';\n\ncustomElements.get('rc-menu-button') || customElements.define('rc-menu-button', RCMenuButton);\n\nexport * from './index.js';\n"],"names":["RCMenuButton"],"mappings":";;AAGA,eAAe,IAAI,gBAAgB,KAAK,eAAe,OAAO,kBAAkBA,CAAY;"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rc-menu-button.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './index.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './rc-menu-button';
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { LitElement } from 'lit';
|
|
2
|
+
import { AnchorPlacement } from '@rcarls/rc-common';
|
|
3
|
+
export interface RCMenuButtonToggleEvent {
|
|
4
|
+
open: boolean;
|
|
5
|
+
}
|
|
6
|
+
declare global {
|
|
7
|
+
interface HTMLElementTagNameMap {
|
|
8
|
+
'rc-menu-button': RCMenuButton;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* A menu button component that opens/closes a menu popup
|
|
13
|
+
*
|
|
14
|
+
* @see https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/
|
|
15
|
+
* @slot trigger - The button element that triggers the menu
|
|
16
|
+
* @slot default - The rc-menu element to display as popup
|
|
17
|
+
* @fires rc-menu-button-toggle - Fired when the menu opens or closes
|
|
18
|
+
* @cssprop [--rc-menu-button-trigger-block-size=var(--rc-control-block-size)] - Minimum block size of the trigger
|
|
19
|
+
* @cssprop [--rc-menu-button-trigger-padding-block=var(--rc-control-padding-block)] - Trigger block-axis padding
|
|
20
|
+
* @cssprop [--rc-menu-button-trigger-padding-inline=var(--rc-control-padding-inline)] - Trigger inline-axis padding
|
|
21
|
+
* @cssprop [--rc-menu-button-trigger-border=var(--rc-border)] - Trigger border
|
|
22
|
+
* @cssprop [--rc-menu-button-trigger-radius=var(--rc-control-radius)] - Trigger border radius
|
|
23
|
+
* @cssprop [--rc-menu-button-trigger-background=var(--rc-button-bg)] - Trigger background
|
|
24
|
+
* @cssprop [--rc-menu-button-trigger-color=var(--rc-button-text)] - Trigger text color
|
|
25
|
+
* @cssprop [--rc-menu-button-popup-z-index=1000] - Z-index of the popup overlay
|
|
26
|
+
* @csspart root - The root container element
|
|
27
|
+
* @csspart popup - The popup container element
|
|
28
|
+
* @attr placement - Preferred placement of the popup relative to the trigger.
|
|
29
|
+
*/
|
|
30
|
+
export declare class RCMenuButton extends LitElement {
|
|
31
|
+
static styles: import('lit').CSSResult[];
|
|
32
|
+
/**
|
|
33
|
+
* Enables delegatesFocus so the browser routes focus() on the host element
|
|
34
|
+
* to the slotted trigger button. This lets the host participate in a
|
|
35
|
+
* roving-tabindex group (e.g. rc-toolbar) as a single tab stop while the
|
|
36
|
+
* trigger remains the visible focus target.
|
|
37
|
+
*/
|
|
38
|
+
static shadowRootOptions: ShadowRootInit;
|
|
39
|
+
private _defaultOpen;
|
|
40
|
+
private _open;
|
|
41
|
+
/** Whether the menu is currently open. Host writes update silently. */
|
|
42
|
+
get open(): boolean;
|
|
43
|
+
/** Whether the menu is currently open. Host writes update silently. */
|
|
44
|
+
set open(value: boolean | undefined);
|
|
45
|
+
/** Initial uncontrolled open state. */
|
|
46
|
+
get defaultOpen(): boolean;
|
|
47
|
+
/** Initial uncontrolled open state. */
|
|
48
|
+
set defaultOpen(value: boolean);
|
|
49
|
+
/**
|
|
50
|
+
* Orientation of this menu button, affects which arrow keys open/close the menu.
|
|
51
|
+
* If not set, inherits from a parent rc-menubar or element with role="menubar".
|
|
52
|
+
*/
|
|
53
|
+
orientation: 'horizontal' | 'vertical' | undefined;
|
|
54
|
+
/** Preferred placement of the popup relative to the trigger button. */
|
|
55
|
+
placement: AnchorPlacement;
|
|
56
|
+
private get _effectivePlacement();
|
|
57
|
+
/**
|
|
58
|
+
* Resolved orientation, considering inheritance from parent menubar.
|
|
59
|
+
* Falls back to 'horizontal' if no orientation is set or inherited.
|
|
60
|
+
*/
|
|
61
|
+
private get _resolvedOrientation();
|
|
62
|
+
private _$root;
|
|
63
|
+
private _$popup;
|
|
64
|
+
/** Reference to the trigger button element */
|
|
65
|
+
private _trigger;
|
|
66
|
+
/** Reference to the menu element */
|
|
67
|
+
private _menu;
|
|
68
|
+
private _anchorCtrl;
|
|
69
|
+
/** Bound handler for document click (light dismiss) */
|
|
70
|
+
private _boundHandleDocumentClick;
|
|
71
|
+
/**
|
|
72
|
+
* Watches the host's own `tabindex` attribute so the trigger button's
|
|
73
|
+
* tabindex can be kept in sync. When a parent (e.g. rc-toolbar) places the
|
|
74
|
+
* host under roving-tabindex management it sets/removes tabindex on the
|
|
75
|
+
* host; we mirror that to the trigger so it doesn't become an independent
|
|
76
|
+
* tab stop. When the host has no tabindex the trigger keeps its natural
|
|
77
|
+
* tabindex=0 for standalone usage.
|
|
78
|
+
*/
|
|
79
|
+
private _tabObserver?;
|
|
80
|
+
connectedCallback(): void;
|
|
81
|
+
disconnectedCallback(): void;
|
|
82
|
+
/** Opens the menu and focuses the first or last item */
|
|
83
|
+
openMenu(focusTarget?: 'first' | 'last'): void;
|
|
84
|
+
/** Closes the menu and optionally returns focus to trigger */
|
|
85
|
+
closeMenu(returnFocus?: boolean): void;
|
|
86
|
+
/** Toggles the menu open/closed */
|
|
87
|
+
toggleMenu(): void;
|
|
88
|
+
private _dispatchToggle;
|
|
89
|
+
private _handleTriggerSlotChange;
|
|
90
|
+
/**
|
|
91
|
+
* Mirrors the host's `tabindex` attribute to the trigger button.
|
|
92
|
+
*
|
|
93
|
+
* When a roving-tabindex parent (e.g. rc-toolbar) marks this host inactive
|
|
94
|
+
* with `tabindex="-1"`, the trigger must also be suppressed so it does not
|
|
95
|
+
* become an independent tab stop. When the host is the active item
|
|
96
|
+
* (`tabindex="0"`) or standalone (no tabindex), the trigger retains its
|
|
97
|
+
* natural focusability so that Chrome's delegatesFocus can route sequential
|
|
98
|
+
* Tab navigation through the host to the trigger.
|
|
99
|
+
*/
|
|
100
|
+
private _syncTriggerTabindex;
|
|
101
|
+
/**
|
|
102
|
+
* Overrides the default focus() so that programmatic focus calls (e.g. from
|
|
103
|
+
* a roving-tabindex parent navigating back to this element via arrow keys)
|
|
104
|
+
* always reach the trigger.
|
|
105
|
+
*
|
|
106
|
+
* Chrome's delegatesFocus will not delegate to a tabindex="-1" element, so
|
|
107
|
+
* when the toolbar marks this host inactive it suppresses the trigger — and
|
|
108
|
+
* the next focusItem() call silently fails. Lifting the suppression here and
|
|
109
|
+
* directly calling trigger.focus() bypasses that restriction.
|
|
110
|
+
*/
|
|
111
|
+
focus(options?: FocusOptions): void;
|
|
112
|
+
private _handleMenuSlotChange;
|
|
113
|
+
private _onNavigate;
|
|
114
|
+
private _handleTriggerClick;
|
|
115
|
+
private _handleMenuClose;
|
|
116
|
+
private _handleMenuActivate;
|
|
117
|
+
private _handleDocumentClick;
|
|
118
|
+
protected updated(changedProperties: Map<PropertyKey, unknown>): void;
|
|
119
|
+
private _setOpen;
|
|
120
|
+
private _syncTriggerAria;
|
|
121
|
+
protected render(): import('lit').TemplateResult<1>;
|
|
122
|
+
}
|
|
123
|
+
export default RCMenuButton;
|
|
File without changes
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rcarls/rc-menu-button",
|
|
3
|
+
"publishConfig": {
|
|
4
|
+
"access": "public"
|
|
5
|
+
},
|
|
6
|
+
"version": "0.1.0",
|
|
7
|
+
"description": "Headless WAI-ARIA menu button web component built with Lit",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/richardcarls/rc-webcomponents.git",
|
|
11
|
+
"directory": "packages/rc-menu-button"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://github.com/richardcarls/rc-webcomponents#readme",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"type": "module",
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"module": "./dist/rc-menu-button.js",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"import": {
|
|
23
|
+
"types": "./dist/types/packages/rc-menu-button/src/index.d.ts",
|
|
24
|
+
"default": "./dist/rc-menu-button.js"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"./define": {
|
|
28
|
+
"import": {
|
|
29
|
+
"types": "./dist/types/packages/rc-menu-button/src/define.d.ts",
|
|
30
|
+
"default": "./dist/rc-menu-button-define.js"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"types": "./dist/types/packages/rc-menu-button/src/index.d.ts",
|
|
35
|
+
"sideEffects": [
|
|
36
|
+
"./dist/rc-menu-button-define.js"
|
|
37
|
+
],
|
|
38
|
+
"customElements": "dist/custom-elements.json",
|
|
39
|
+
"scripts": {
|
|
40
|
+
"dev": "vite",
|
|
41
|
+
"build": "tsc && vite build && cem analyze",
|
|
42
|
+
"cem:analyze": "cem analyze",
|
|
43
|
+
"preview": "vite preview",
|
|
44
|
+
"test:browser": "vitest",
|
|
45
|
+
"test:browser:chrome": "vitest --project=chromium",
|
|
46
|
+
"test:browser:firefox": "vitest --project=firefox",
|
|
47
|
+
"yalc:publish": "yalc publish --push"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@rcarls/rc-common": "workspace:^",
|
|
51
|
+
"@rcarls/rc-menu": "workspace:^",
|
|
52
|
+
"@types/node": "^25.6.0"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@custom-elements-manifest/analyzer": "0.11.0",
|
|
56
|
+
"@guanghechen/rollup-plugin-copy": "^6.0.9",
|
|
57
|
+
"@vitest/browser-playwright": "4.1.5",
|
|
58
|
+
"lit": "^3.0.0",
|
|
59
|
+
"playwright": "^1.56.0",
|
|
60
|
+
"rollup": "^4.60.2",
|
|
61
|
+
"typescript": "~5.9.3",
|
|
62
|
+
"vite": "^7.1.7",
|
|
63
|
+
"vite-plugin-dts": "^4.5.4",
|
|
64
|
+
"vitest": "^4.0.6",
|
|
65
|
+
"vitest-browser-lit": "^1.0.1"
|
|
66
|
+
},
|
|
67
|
+
"peerDependencies": {
|
|
68
|
+
"lit": "^3.0.0"
|
|
69
|
+
}
|
|
70
|
+
}
|