@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 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,7 @@
1
+ import "@rcarls/rc-menu/define";
2
+ import { R as t } from "./rc-menu-button-CmzW7P4h.js";
3
+ customElements.get("rc-menu-button") || customElements.define("rc-menu-button", t);
4
+ export {
5
+ t as RCMenuButton
6
+ };
7
+ //# sourceMappingURL=rc-menu-button-define.js.map
@@ -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,5 @@
1
+ import { R as e } from "./rc-menu-button-CmzW7P4h.js";
2
+ export {
3
+ e as RCMenuButton
4
+ };
5
+ //# sourceMappingURL=rc-menu-button.js.map
@@ -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;
@@ -0,0 +1,2 @@
1
+ export declare const menuButtonStyles: import('lit').CSSResult;
2
+ export default menuButtonStyles;
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
+ }