@c2n/menu 0.0.7
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/LICENSE +21 -0
- package/README.md +28 -0
- package/dist/menu-item-C08rN4HS.js +195 -0
- package/dist/menu-item.js +2 -0
- package/dist/menu.js +381 -0
- package/package.json +81 -0
- package/react.d.ts +26 -0
- package/react.js +3 -0
- package/types/src/menu-item.d.ts +156 -0
- package/types/src/menu-item.d.ts.map +1 -0
- package/types/src/menu.d.ts +182 -0
- package/types/src/menu.d.ts.map +1 -0
- package/vue.d.ts +47 -0
- package/vue.js +3 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Nguyen Thai Vinh
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# @c2n/menu
|
|
2
|
+
|
|
3
|
+
`c2-menu` is a menu of commands: a trigger plus a popover of `c2-menu-item` rows, positioned by `c2-overlay` on the
|
|
4
|
+
browser's top layer.
|
|
5
|
+
|
|
6
|
+
```html
|
|
7
|
+
<c2-menu aria-label="File">
|
|
8
|
+
<button slot="trigger">Actions</button>
|
|
9
|
+
<c2-menu-item value="new">New file<kbd slot="shortcut">⌘N</kbd></c2-menu-item>
|
|
10
|
+
<c2-menu-item value="rename" disabled>Rename</c2-menu-item>
|
|
11
|
+
<hr />
|
|
12
|
+
<c2-menu-item value="delete" destructive>Delete file</c2-menu-item>
|
|
13
|
+
</c2-menu>
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
- The trigger goes in the `trigger` slot, or stays anywhere on the page and is named by `anchor` (an element id).
|
|
17
|
+
- Rows are `c2-menu-item` children: a command, a link (`href`), a `checkbox` or `radio` (`type` + `checked`, radios
|
|
18
|
+
grouped by `name`), or the parent of a nested `c2-menu` slotted into `submenu`. `<hr>` separates, `<h1>`–`<h6>` head
|
|
19
|
+
a group.
|
|
20
|
+
- `menu-select` bubbles from the activated row with `detail.value`, `detail.checked` and `detail.data`; the menu then
|
|
21
|
+
closes unless it (or that row) is marked `keep-open`.
|
|
22
|
+
- `open`, `placement`, `fit-anchor`, `disabled`, plus `show()`, `hide()` and `toggle()`.
|
|
23
|
+
- Width: as wide as the rows, with `--c2-menu--min-width` (128px) as a floor, and never narrower than a slotted
|
|
24
|
+
trigger. `fit-anchor` pins it to exactly the trigger's width.
|
|
25
|
+
- Keyboard: Enter / Space / ArrowDown opens on the first row (ArrowUp on the last); arrows, Home / End and typeahead
|
|
26
|
+
move; ArrowRight and ArrowLeft enter and leave a submenu; Escape and Tab close and restore focus to the trigger.
|
|
27
|
+
- Theming: `--c2-menu--*` (surface), `--c2-menu__separator--*`, `--c2-menu__heading--*`, and `--c2-menu-item--*` for
|
|
28
|
+
the rows. See the docs site for the full list.
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { LitElement, html, nothing, svg, unsafeCSS } from "lit";
|
|
2
|
+
import { property, query, state } from "lit/decorators.js";
|
|
3
|
+
import { customElement } from "@c2n/core/element-helper.js";
|
|
4
|
+
import { ifDefined } from "lit/directives/if-defined.js";
|
|
5
|
+
//#region src/menu-item.scss?inline
|
|
6
|
+
var menu_item_default = "/* ex : var((width: 24px), width, c2-checkbox) returns var(--c2-checkbox-width, 24px) */\n:host {\n display: block;\n flex: none;\n user-select: none;\n}\n\n:host([hidden]) {\n display: none;\n}\n\n.c2-menu-item {\n display: flex;\n align-items: center;\n box-sizing: border-box;\n cursor: pointer;\n text-decoration: none;\n min-height: var(--c2-menu-item--min-height,32px);\n gap: var(--c2-menu-item--gap,8px);\n font-size: var(--c2-menu-item--font-size,14px);\n font-weight: var(--c2-menu-item--font-weight);\n font-style: var(--c2-menu-item--font-style);\n font-family: var(--c2-menu-item--font-family);\n line-height: var(--c2-menu-item--line-height,20px);\n border-top-left-radius: var(--c2-menu-item--border-top-left-radius,6px);\n border-top-right-radius: var(--c2-menu-item--border-top-right-radius,6px);\n border-bottom-left-radius: var(--c2-menu-item--border-bottom-left-radius,6px);\n border-bottom-right-radius: var(--c2-menu-item--border-bottom-right-radius,6px);\n padding-top: var(--c2-menu-item--padding-top,6px);\n padding-right: var(--c2-menu-item--padding-right,8px);\n padding-bottom: var(--c2-menu-item--padding-bottom,6px);\n padding-left: var(--c2-menu-item--padding-left,8px);\n background: var(--c2-menu-item--background,transparent);\n color: var(--c2-menu-item--color,#18181b);\n will-change: background, color;\n transition: background 250ms cubic-bezier(0.2, 0, 0, 1), color 250ms cubic-bezier(0.2, 0, 0, 1);\n}\n.c2-menu-item:hover {\n background: var(--c2-menu-item__hover--background,var(--c2-menu-item--background,#f4f4f5));\n color: var(--c2-menu-item__hover--color,var(--c2-menu-item--color,#18181b));\n}\n\n.content {\n flex: 1;\n min-width: 0;\n}\n\n.description {\n margin-top: var(--c2-menu-item__description--margin-top,1px);\n color: var(--c2-menu-item__description--color,#71717a);\n font-size: var(--c2-menu-item__description--font-size,12px);\n line-height: var(--c2-menu-item__description--line-height,16px);\n font-weight: normal;\n}\n.description[hidden] {\n display: none;\n}\n\n.indicator {\n flex: none;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: var(--c2-menu-item__indicator--size,16px);\n height: var(--c2-menu-item__indicator--size,16px);\n color: var(--c2-menu-item__indicator--color,var(--c2-menu-item--color,#18181b));\n}\n.indicator svg {\n width: 100%;\n height: 100%;\n opacity: 0;\n transition: opacity 250ms cubic-bezier(0.2, 0, 0, 1);\n}\n.indicator[data-checked] svg {\n opacity: 1;\n}\n\n::slotted([slot=prefix-icon]),\n::slotted([slot=suffix-icon]),\n.submenu-icon {\n flex: none;\n width: var(--c2-menu-item__icon--size,16px);\n height: var(--c2-menu-item__icon--size,16px);\n color: var(--c2-menu-item__icon--color,#71717a);\n --c2-feather-icon--size: var(--c2-menu-item__icon--size,16px);\n --c2-mat-icon--font-size: var(--c2-menu-item__icon--size,16px);\n}\n\n::slotted([slot=shortcut]) {\n flex: none;\n margin-left: auto;\n color: var(--c2-menu-item__shortcut--color,#52525b);\n font-size: var(--c2-menu-item__shortcut--font-size,11px);\n letter-spacing: var(--c2-menu-item__shortcut--letter-spacing,0.08em);\n --c2-kbd--color: var(--c2-menu-item__shortcut--color,#52525b);\n --c2-kbd--font-size: var(--c2-menu-item__shortcut--font-size,11px);\n --c2-kbd--letter-spacing: var(--c2-menu-item__shortcut--letter-spacing,0.08em);\n}\n\n:host(:focus) {\n outline: none;\n}\n\n:host(:focus-visible) .c2-menu-item {\n background: var(--c2-menu-item__hover--background,var(--c2-menu-item--background,#f4f4f5));\n color: var(--c2-menu-item__hover--color,var(--c2-menu-item--color,#18181b));\n outline: var(--c2-menu-item__focus--outline,2px solid rgba(2, 101, 220, 0.4));\n outline-offset: var(--c2-menu-item__focus--outline-offset,-2px);\n}\n\n:host([destructive]) .c2-menu-item {\n color: var(--c2-menu-item__destructive--color,#dc2626);\n}\n:host([destructive]) .c2-menu-item:hover {\n background: var(--c2-menu-item__destructive__hover--background,#fef2f2);\n color: var(--c2-menu-item__destructive--color,#dc2626);\n}\n:host([destructive]) ::slotted([slot=prefix-icon]),\n:host([destructive]) ::slotted([slot=suffix-icon]),\n:host([destructive]) .indicator {\n color: var(--c2-menu-item__destructive--color,#dc2626);\n}\n\n:host([destructive]:focus-visible) .c2-menu-item {\n background: var(--c2-menu-item__destructive__hover--background,#fef2f2);\n color: var(--c2-menu-item__destructive--color,#dc2626);\n}\n\n:host([disabled]) .c2-menu-item {\n pointer-events: none;\n cursor: default;\n opacity: var(--c2-menu-item__disabled--opacity,0.38);\n}";
|
|
7
|
+
//#endregion
|
|
8
|
+
//#region \0@oxc-project+runtime@0.148.0/helpers/esm/decorate.js
|
|
9
|
+
function __decorate(decorators, target, key, desc) {
|
|
10
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
11
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
12
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
13
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
14
|
+
}
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/menu-item.ts
|
|
17
|
+
var MenuItem = class MenuItem extends LitElement {
|
|
18
|
+
constructor(..._args) {
|
|
19
|
+
super(..._args);
|
|
20
|
+
this.value = "";
|
|
21
|
+
this.disabled = false;
|
|
22
|
+
this.type = "item";
|
|
23
|
+
this.checked = false;
|
|
24
|
+
this.destructive = false;
|
|
25
|
+
this.keepOpen = false;
|
|
26
|
+
this.expanded = false;
|
|
27
|
+
this.reserveIndicator = false;
|
|
28
|
+
this.hasDescription = false;
|
|
29
|
+
this.submenuPresent = false;
|
|
30
|
+
this.authorRole = null;
|
|
31
|
+
this.handleClick = (event) => {
|
|
32
|
+
if (this.disabled) {
|
|
33
|
+
event.preventDefault();
|
|
34
|
+
event.stopPropagation();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (this.hasSubmenu) return;
|
|
38
|
+
this.select();
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
static {
|
|
42
|
+
this.styles = unsafeCSS(menu_item_default);
|
|
43
|
+
}
|
|
44
|
+
/** Whether a menu is slotted into `submenu`. */
|
|
45
|
+
get hasSubmenu() {
|
|
46
|
+
return this.submenuPresent;
|
|
47
|
+
}
|
|
48
|
+
/** The element slotted into `submenu`, if any. The parent menu opens and closes it. */
|
|
49
|
+
get submenuElement() {
|
|
50
|
+
return (this.submenuSlot?.assignedElements({ flatten: true }) ?? []).find((element) => element instanceof HTMLElement) ?? null;
|
|
51
|
+
}
|
|
52
|
+
/** The text the menu matches while typing: `label` when set, otherwise the slotted label text. */
|
|
53
|
+
get displayText() {
|
|
54
|
+
if (this.label !== void 0) return this.label;
|
|
55
|
+
return ((this.contentSlot?.assignedNodes({ flatten: true }) ?? []).map((node) => node.innerText ?? node.textContent ?? "").join(" ").trim() || this.contentSlot?.textContent?.trim() || this.value).trim();
|
|
56
|
+
}
|
|
57
|
+
connectedCallback() {
|
|
58
|
+
super.connectedCallback();
|
|
59
|
+
if (this.authorRole === null) this.authorRole = this.getAttribute("role");
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Activates the row the way Enter does: a link is navigated, a checkable row toggles, and `menu-select` fires.
|
|
63
|
+
* A row with a submenu does nothing — the menu opens the submenu instead.
|
|
64
|
+
*/
|
|
65
|
+
activate() {
|
|
66
|
+
if (this.disabled || this.hasSubmenu) return;
|
|
67
|
+
if (this.href) {
|
|
68
|
+
this.renderRoot.querySelector("a")?.click();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
this.select();
|
|
72
|
+
}
|
|
73
|
+
select() {
|
|
74
|
+
if (this.type === "checkbox") this.checked = !this.checked;
|
|
75
|
+
else if (this.type === "radio") this.checked = true;
|
|
76
|
+
this.dispatchEvent(new CustomEvent("menu-select", {
|
|
77
|
+
bubbles: true,
|
|
78
|
+
composed: true,
|
|
79
|
+
cancelable: true,
|
|
80
|
+
detail: {
|
|
81
|
+
value: this.value,
|
|
82
|
+
checked: this.checked,
|
|
83
|
+
data: this.data
|
|
84
|
+
}
|
|
85
|
+
}));
|
|
86
|
+
}
|
|
87
|
+
handleDescriptionSlotChange(event) {
|
|
88
|
+
const slot = event.target;
|
|
89
|
+
this.hasDescription = slot.assignedNodes({ flatten: true }).some((node) => node.nodeType === Node.ELEMENT_NODE || (node.textContent ?? "").trim() !== "");
|
|
90
|
+
}
|
|
91
|
+
handleSubmenuSlotChange(event) {
|
|
92
|
+
this.submenuPresent = event.target.assignedElements({ flatten: true }).length > 0;
|
|
93
|
+
}
|
|
94
|
+
updated(changed) {
|
|
95
|
+
if (changed.has("checked") && changed.get("checked") !== void 0) this.dispatchEvent(new CustomEvent("checked-change", {
|
|
96
|
+
bubbles: true,
|
|
97
|
+
composed: true,
|
|
98
|
+
detail: {
|
|
99
|
+
checked: this.checked,
|
|
100
|
+
value: this.value
|
|
101
|
+
}
|
|
102
|
+
}));
|
|
103
|
+
this.syncAria();
|
|
104
|
+
}
|
|
105
|
+
syncAria() {
|
|
106
|
+
const role = this.authorRole ?? (this.href ? "menuitem" : this.type === "checkbox" ? "menuitemcheckbox" : this.type === "radio" ? "menuitemradio" : "menuitem");
|
|
107
|
+
this.setAttribute("role", role);
|
|
108
|
+
if (this.type === "checkbox" || this.type === "radio") this.setAttribute("aria-checked", String(this.checked));
|
|
109
|
+
else this.removeAttribute("aria-checked");
|
|
110
|
+
if (this.hasSubmenu) {
|
|
111
|
+
this.setAttribute("aria-haspopup", "menu");
|
|
112
|
+
this.setAttribute("aria-expanded", String(this.expanded));
|
|
113
|
+
} else {
|
|
114
|
+
this.removeAttribute("aria-haspopup");
|
|
115
|
+
this.removeAttribute("aria-expanded");
|
|
116
|
+
}
|
|
117
|
+
if (this.disabled) this.setAttribute("aria-disabled", "true");
|
|
118
|
+
else this.removeAttribute("aria-disabled");
|
|
119
|
+
if (this.disabled) this.tabIndex = -1;
|
|
120
|
+
else if (!this.hasAttribute("tabindex")) this.tabIndex = -1;
|
|
121
|
+
}
|
|
122
|
+
renderIndicator() {
|
|
123
|
+
if (this.type === "item" && !this.reserveIndicator) return nothing;
|
|
124
|
+
const glyph = this.type === "radio" ? svg`<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="5"></circle></svg>` : svg`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M20 6 9 17l-5-5"></path></svg>`;
|
|
125
|
+
return html`<span class="indicator" ?data-checked=${this.checked}>${this.type === "item" ? nothing : glyph}</span>`;
|
|
126
|
+
}
|
|
127
|
+
renderSubmenuIcon() {
|
|
128
|
+
return svg`<svg class="submenu-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m9 6 6 6-6 6"></path></svg>`;
|
|
129
|
+
}
|
|
130
|
+
renderInner() {
|
|
131
|
+
return html`
|
|
132
|
+
${this.renderIndicator()}
|
|
133
|
+
<slot name="prefix-icon"></slot>
|
|
134
|
+
<div class="content">
|
|
135
|
+
<div class="text"><slot>${this.label ?? this.value}</slot></div>
|
|
136
|
+
<div class="description" ?hidden=${!this.hasDescription}>
|
|
137
|
+
<slot name="description" @slotchange=${this.handleDescriptionSlotChange}></slot>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
<slot name="shortcut"></slot>
|
|
141
|
+
<slot name="suffix-icon">${this.hasSubmenu ? this.renderSubmenuIcon() : nothing}</slot>
|
|
142
|
+
`;
|
|
143
|
+
}
|
|
144
|
+
render() {
|
|
145
|
+
const row = this.href && !this.disabled ? html`<a
|
|
146
|
+
class="c2-menu-item"
|
|
147
|
+
href=${this.href}
|
|
148
|
+
target=${ifDefined(this.target)}
|
|
149
|
+
rel=${this.target === "_blank" ? "noopener noreferrer" : nothing}
|
|
150
|
+
tabindex="-1"
|
|
151
|
+
@click=${this.handleClick}
|
|
152
|
+
>${this.renderInner()}</a
|
|
153
|
+
>` : html`<div class="c2-menu-item" @click=${this.handleClick}>${this.renderInner()}</div>`;
|
|
154
|
+
return html`${row}<slot name="submenu" @slotchange=${this.handleSubmenuSlotChange}></slot>`;
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
__decorate([property({ reflect: true })], MenuItem.prototype, "value", void 0);
|
|
158
|
+
__decorate([property({
|
|
159
|
+
type: Boolean,
|
|
160
|
+
reflect: true
|
|
161
|
+
})], MenuItem.prototype, "disabled", void 0);
|
|
162
|
+
__decorate([property({ reflect: true })], MenuItem.prototype, "type", void 0);
|
|
163
|
+
__decorate([property({
|
|
164
|
+
type: Boolean,
|
|
165
|
+
reflect: true
|
|
166
|
+
})], MenuItem.prototype, "checked", void 0);
|
|
167
|
+
__decorate([property({ reflect: true })], MenuItem.prototype, "name", void 0);
|
|
168
|
+
__decorate([property()], MenuItem.prototype, "href", void 0);
|
|
169
|
+
__decorate([property()], MenuItem.prototype, "target", void 0);
|
|
170
|
+
__decorate([property({
|
|
171
|
+
type: Boolean,
|
|
172
|
+
reflect: true
|
|
173
|
+
})], MenuItem.prototype, "destructive", void 0);
|
|
174
|
+
__decorate([property({
|
|
175
|
+
type: Boolean,
|
|
176
|
+
attribute: "keep-open"
|
|
177
|
+
})], MenuItem.prototype, "keepOpen", void 0);
|
|
178
|
+
__decorate([property({ reflect: true })], MenuItem.prototype, "label", void 0);
|
|
179
|
+
__decorate([property({ attribute: false })], MenuItem.prototype, "data", void 0);
|
|
180
|
+
__decorate([property({
|
|
181
|
+
type: Boolean,
|
|
182
|
+
reflect: true
|
|
183
|
+
})], MenuItem.prototype, "expanded", void 0);
|
|
184
|
+
__decorate([property({
|
|
185
|
+
type: Boolean,
|
|
186
|
+
reflect: true,
|
|
187
|
+
attribute: "reserve-indicator"
|
|
188
|
+
})], MenuItem.prototype, "reserveIndicator", void 0);
|
|
189
|
+
__decorate([state()], MenuItem.prototype, "hasDescription", void 0);
|
|
190
|
+
__decorate([state()], MenuItem.prototype, "submenuPresent", void 0);
|
|
191
|
+
__decorate([query("slot:not([name])")], MenuItem.prototype, "contentSlot", void 0);
|
|
192
|
+
__decorate([query("slot[name=\"submenu\"]")], MenuItem.prototype, "submenuSlot", void 0);
|
|
193
|
+
MenuItem = __decorate([customElement("c2-menu-item")], MenuItem);
|
|
194
|
+
//#endregion
|
|
195
|
+
export { __decorate as n, MenuItem as t };
|
package/dist/menu.js
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import { n as __decorate, t as MenuItem } from "./menu-item-C08rN4HS.js";
|
|
2
|
+
import { LitElement, html, nothing, unsafeCSS } from "lit";
|
|
3
|
+
import { isServer } from "lit-html/is-server.js";
|
|
4
|
+
import { property, query, state } from "lit/decorators.js";
|
|
5
|
+
import { customElement } from "@c2n/core/element-helper.js";
|
|
6
|
+
import { redispatchEvent } from "@c2n/core/dom-helper.js";
|
|
7
|
+
import "@c2n/overlay";
|
|
8
|
+
//#region src/menu.scss?inline
|
|
9
|
+
var menu_default = "/* ex : var((width: 24px), width, c2-checkbox) returns var(--c2-checkbox-width, 24px) */\n:host {\n display: contents;\n}\n\n:host([has-trigger]) {\n display: inline-flex;\n vertical-align: middle;\n}\n\n:host([hidden]) {\n display: none;\n}\n\n:host([slot=submenu]) {\n --c2-overlay--offset-y: 2px;\n --c2-overlay--offset-x: -4px;\n}\n\n.menu {\n display: flex;\n flex-direction: column;\n box-sizing: border-box;\n margin: 0;\n gap: var(--c2-menu--gap);\n overflow-y: auto;\n overscroll-behavior: contain;\n min-width: var(--c2-menu--min-width,128px);\n max-height: var(--c2-menu--max-height,320px);\n background: var(--c2-menu--background,#ffffff);\n box-shadow: var(--c2-menu--box-shadow,0 8px 24px rgba(24, 24, 27, 0.08));\n border-top: var(--c2-menu--border-top,1px solid #e4e4e7);\n border-right: var(--c2-menu--border-right,1px solid #e4e4e7);\n border-bottom: var(--c2-menu--border-bottom,1px solid #e4e4e7);\n border-left: var(--c2-menu--border-left,1px solid #e4e4e7);\n border-top-left-radius: var(--c2-menu--border-top-left-radius,8px);\n border-top-right-radius: var(--c2-menu--border-top-right-radius,8px);\n border-bottom-left-radius: var(--c2-menu--border-bottom-left-radius,8px);\n border-bottom-right-radius: var(--c2-menu--border-bottom-right-radius,8px);\n padding-top: var(--c2-menu--padding-top,4px);\n padding-right: var(--c2-menu--padding-right,4px);\n padding-bottom: var(--c2-menu--padding-bottom,4px);\n padding-left: var(--c2-menu--padding-left,4px);\n}\n.menu:focus {\n outline: none;\n}\n\n::slotted(hr) {\n flex: none;\n border: none;\n border-top: 1px solid var(--c2-menu__separator--color,#e4e4e7);\n margin: var(--c2-menu__separator--margin,4px 0);\n}\n\n::slotted(:is(h1, h2, h3, h4, h5, h6)) {\n flex: none;\n margin: 0;\n color: var(--c2-menu__heading--color,#71717a);\n font-size: var(--c2-menu__heading--font-size,11px);\n font-weight: var(--c2-menu__heading--font-weight,600);\n letter-spacing: var(--c2-menu__heading--letter-spacing,0.06em);\n text-transform: var(--c2-menu__heading--text-transform,uppercase);\n line-height: 1.4;\n padding-top: var(--c2-menu__heading--padding-top,8px);\n padding-right: var(--c2-menu__heading--padding-right,8px);\n padding-bottom: var(--c2-menu__heading--padding-bottom,4px);\n padding-left: var(--c2-menu__heading--padding-left,8px);\n user-select: none;\n}\n\n:host([disabled]) ::slotted([slot=trigger]) {\n pointer-events: none;\n opacity: 0.38;\n}";
|
|
10
|
+
//#endregion
|
|
11
|
+
//#region src/menu.ts
|
|
12
|
+
var _Menu;
|
|
13
|
+
var Menu = class Menu extends LitElement {
|
|
14
|
+
static {
|
|
15
|
+
_Menu = this;
|
|
16
|
+
}
|
|
17
|
+
constructor(..._args) {
|
|
18
|
+
super(..._args);
|
|
19
|
+
this.open = false;
|
|
20
|
+
this.placement = "bottom-start";
|
|
21
|
+
this.offset = void 0;
|
|
22
|
+
this.crossOffset = void 0;
|
|
23
|
+
this.anchor = void 0;
|
|
24
|
+
this.fitAnchor = false;
|
|
25
|
+
this.keepOpen = false;
|
|
26
|
+
this.disabled = false;
|
|
27
|
+
this.ariaLabel = null;
|
|
28
|
+
this.anchorElement = null;
|
|
29
|
+
this.hasTrigger = false;
|
|
30
|
+
this.focusedItem = void 0;
|
|
31
|
+
this.pendingFocus = "first";
|
|
32
|
+
this.typeahead = "";
|
|
33
|
+
this.openBeforePress = void 0;
|
|
34
|
+
this.handleTriggerPointerDown = () => {
|
|
35
|
+
this.openBeforePress = this.open;
|
|
36
|
+
};
|
|
37
|
+
this.handleTriggerClick = (event) => {
|
|
38
|
+
const wasOpen = this.openBeforePress ?? this.open;
|
|
39
|
+
this.openBeforePress = void 0;
|
|
40
|
+
if (this.disabled) return;
|
|
41
|
+
event.preventDefault();
|
|
42
|
+
if (wasOpen) this.hide();
|
|
43
|
+
else this.show("first");
|
|
44
|
+
};
|
|
45
|
+
this.handleTriggerKeydown = (event) => {
|
|
46
|
+
if (this.disabled) return;
|
|
47
|
+
switch (event.key) {
|
|
48
|
+
case "ArrowDown":
|
|
49
|
+
case "Enter":
|
|
50
|
+
case " ":
|
|
51
|
+
event.preventDefault();
|
|
52
|
+
this.show("first");
|
|
53
|
+
return;
|
|
54
|
+
case "ArrowUp":
|
|
55
|
+
event.preventDefault();
|
|
56
|
+
this.show("last");
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
this.handleItemClick = (event) => {
|
|
61
|
+
const item = this.itemFromEvent(event);
|
|
62
|
+
if (!item || item.disabled) return;
|
|
63
|
+
if (item.hasSubmenu) this.openSubmenu(item, "none");
|
|
64
|
+
};
|
|
65
|
+
this.handlePointerOver = (event) => {
|
|
66
|
+
const item = this.itemFromEvent(event);
|
|
67
|
+
if (!item) return;
|
|
68
|
+
if (!item.disabled && this.focusedItem !== item) {
|
|
69
|
+
this.focusedItem = item;
|
|
70
|
+
for (const other of this.items) other.tabIndex = other === item ? 0 : -1;
|
|
71
|
+
}
|
|
72
|
+
if (item.hasSubmenu && !item.disabled) this.openSubmenu(item, "none");
|
|
73
|
+
else this.closeSubmenus();
|
|
74
|
+
};
|
|
75
|
+
this.handleKeydown = (event) => {
|
|
76
|
+
const enabled = this.enabledItems;
|
|
77
|
+
const current = this.itemFromEvent(event) ?? this.focusedItem;
|
|
78
|
+
const index = current ? enabled.indexOf(current) : -1;
|
|
79
|
+
switch (event.key) {
|
|
80
|
+
case "ArrowDown":
|
|
81
|
+
event.preventDefault();
|
|
82
|
+
event.stopPropagation();
|
|
83
|
+
this.focusItem(enabled[index + 1] ?? enabled[0]);
|
|
84
|
+
return;
|
|
85
|
+
case "ArrowUp":
|
|
86
|
+
event.preventDefault();
|
|
87
|
+
event.stopPropagation();
|
|
88
|
+
this.focusItem(index > 0 ? enabled[index - 1] : enabled[enabled.length - 1]);
|
|
89
|
+
return;
|
|
90
|
+
case "Home":
|
|
91
|
+
event.preventDefault();
|
|
92
|
+
event.stopPropagation();
|
|
93
|
+
this.focusItem(enabled[0]);
|
|
94
|
+
return;
|
|
95
|
+
case "End":
|
|
96
|
+
event.preventDefault();
|
|
97
|
+
event.stopPropagation();
|
|
98
|
+
this.focusItem(enabled[enabled.length - 1]);
|
|
99
|
+
return;
|
|
100
|
+
case "ArrowRight":
|
|
101
|
+
if (current?.hasSubmenu) {
|
|
102
|
+
event.preventDefault();
|
|
103
|
+
event.stopPropagation();
|
|
104
|
+
this.openSubmenu(current, "first");
|
|
105
|
+
}
|
|
106
|
+
return;
|
|
107
|
+
case "ArrowLeft":
|
|
108
|
+
if (this.ownerItem) {
|
|
109
|
+
event.preventDefault();
|
|
110
|
+
event.stopPropagation();
|
|
111
|
+
this.hide();
|
|
112
|
+
this.ownerItem.focus();
|
|
113
|
+
}
|
|
114
|
+
return;
|
|
115
|
+
case "Enter":
|
|
116
|
+
case " ":
|
|
117
|
+
if (!current) return;
|
|
118
|
+
event.preventDefault();
|
|
119
|
+
event.stopPropagation();
|
|
120
|
+
if (current.hasSubmenu) this.openSubmenu(current, "first");
|
|
121
|
+
else current.activate();
|
|
122
|
+
return;
|
|
123
|
+
case "Tab":
|
|
124
|
+
event.preventDefault();
|
|
125
|
+
event.stopPropagation();
|
|
126
|
+
this.hide();
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey) {
|
|
130
|
+
event.stopPropagation();
|
|
131
|
+
clearTimeout(this.typeaheadTimer);
|
|
132
|
+
this.typeahead += event.key.toLowerCase();
|
|
133
|
+
this.typeaheadTimer = setTimeout(() => this.typeahead = "", 600);
|
|
134
|
+
const start = Math.max(this.typeahead.length === 1 ? index + 1 : index, 0);
|
|
135
|
+
const match = [...enabled.slice(start), ...enabled.slice(0, start)].find((item) => item.displayText.toLowerCase().startsWith(this.typeahead));
|
|
136
|
+
if (match) this.focusItem(match);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
this.handleMenuSelect = (event) => {
|
|
140
|
+
const source = event.target instanceof MenuItem ? event.target : void 0;
|
|
141
|
+
const own = source && this.items.includes(source) ? source : void 0;
|
|
142
|
+
if (own?.type === "radio") {
|
|
143
|
+
for (const item of this.items) if (item !== own && item.type === "radio" && item.name === own.name) item.checked = false;
|
|
144
|
+
}
|
|
145
|
+
if (this.keepOpen || source?.keepOpen) return;
|
|
146
|
+
this.hide();
|
|
147
|
+
};
|
|
148
|
+
this.handleOverlayToggle = (event) => {
|
|
149
|
+
const isOpen = event.newState === "open";
|
|
150
|
+
this.open = isOpen;
|
|
151
|
+
if (isOpen) {
|
|
152
|
+
this.syncItems();
|
|
153
|
+
if (this.pendingFocus === "last") this.focusLastItem();
|
|
154
|
+
else if (this.pendingFocus === "first") this.focusFirstItem();
|
|
155
|
+
} else {
|
|
156
|
+
this.closeSubmenus();
|
|
157
|
+
if (this.shouldReturnFocus()) this.returnFocus();
|
|
158
|
+
}
|
|
159
|
+
this.pendingFocus = "first";
|
|
160
|
+
this.syncTriggerAria();
|
|
161
|
+
redispatchEvent(this, event);
|
|
162
|
+
};
|
|
163
|
+
this.handleSlotChange = () => {
|
|
164
|
+
this.syncItems();
|
|
165
|
+
};
|
|
166
|
+
this.handleTriggerSlotChange = () => {
|
|
167
|
+
this.syncAnchor();
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
static {
|
|
171
|
+
this.styles = unsafeCSS(menu_default);
|
|
172
|
+
}
|
|
173
|
+
/** Slotted rows, in DOM order (separators and headings excluded). */
|
|
174
|
+
get items() {
|
|
175
|
+
return (this.itemSlot?.assignedElements({ flatten: true }) ?? []).map((element) => element instanceof MenuItem ? element : element.firstElementChild).filter((element) => element instanceof MenuItem);
|
|
176
|
+
}
|
|
177
|
+
get enabledItems() {
|
|
178
|
+
return this.items.filter((item) => !item.disabled);
|
|
179
|
+
}
|
|
180
|
+
/** The element that opens this menu: the slotted trigger, or the resolved `anchor`. */
|
|
181
|
+
get triggerElement() {
|
|
182
|
+
return (this.triggerSlot?.assignedElements({ flatten: true }) ?? []).find((element) => element instanceof HTMLElement) ?? null;
|
|
183
|
+
}
|
|
184
|
+
/** The row this menu is a submenu of, when it is slotted into a `c2-menu-item`. */
|
|
185
|
+
get ownerItem() {
|
|
186
|
+
return this.parentElement instanceof MenuItem ? this.parentElement : null;
|
|
187
|
+
}
|
|
188
|
+
connectedCallback() {
|
|
189
|
+
super.connectedCallback();
|
|
190
|
+
if (!isServer && this.getAttribute("slot") === "submenu" && !this.hasAttribute("placement")) this.placement = "right-start";
|
|
191
|
+
}
|
|
192
|
+
/** Opens the menu. Pass `'last'` to start on the last row, `'none'` to leave focus where it is. */
|
|
193
|
+
show(focus = "first") {
|
|
194
|
+
if (this.disabled) return;
|
|
195
|
+
this.pendingFocus = focus;
|
|
196
|
+
this.open = true;
|
|
197
|
+
}
|
|
198
|
+
hide() {
|
|
199
|
+
this.open = false;
|
|
200
|
+
}
|
|
201
|
+
toggle(force) {
|
|
202
|
+
if (force ?? !this.open) this.show();
|
|
203
|
+
else this.hide();
|
|
204
|
+
}
|
|
205
|
+
/** Moves focus to the first enabled row. */
|
|
206
|
+
focusFirstItem() {
|
|
207
|
+
this.focusItem(this.enabledItems[0]);
|
|
208
|
+
}
|
|
209
|
+
/** Moves focus to the last enabled row. */
|
|
210
|
+
focusLastItem() {
|
|
211
|
+
const enabled = this.enabledItems;
|
|
212
|
+
this.focusItem(enabled[enabled.length - 1]);
|
|
213
|
+
}
|
|
214
|
+
resolveAnchor() {
|
|
215
|
+
if (this.anchor instanceof HTMLElement) return this.anchor;
|
|
216
|
+
if (typeof this.anchor === "string" && this.anchor) return this.getRootNode().querySelector?.(`#${CSS.escape(this.anchor)}`) ?? null;
|
|
217
|
+
return this.triggerElement;
|
|
218
|
+
}
|
|
219
|
+
syncAnchor() {
|
|
220
|
+
const resolved = this.resolveAnchor();
|
|
221
|
+
if (resolved !== this.anchorElement) this.anchorElement = resolved;
|
|
222
|
+
this.hasTrigger = !!this.triggerElement;
|
|
223
|
+
this.toggleAttribute("has-trigger", this.hasTrigger);
|
|
224
|
+
this.syncTriggerAria();
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* `aria-haspopup` / `aria-expanded` belong on the element that carries the button role. For a `c2-*` trigger that is
|
|
228
|
+
* the control inside its shadow root, not the host, which is a plain generic element.
|
|
229
|
+
*/
|
|
230
|
+
async syncTriggerAria() {
|
|
231
|
+
const trigger = this.triggerElement;
|
|
232
|
+
if (!trigger) return;
|
|
233
|
+
const element = trigger;
|
|
234
|
+
if (element.updateComplete) await element.updateComplete;
|
|
235
|
+
const target = (trigger.localName.includes("-") ? trigger.shadowRoot?.querySelector("button, a, [role=\"button\"]") : null) ?? trigger;
|
|
236
|
+
target.setAttribute("aria-haspopup", "menu");
|
|
237
|
+
target.setAttribute("aria-expanded", String(this.open));
|
|
238
|
+
}
|
|
239
|
+
focusItem(item) {
|
|
240
|
+
if (!item) return;
|
|
241
|
+
this.focusedItem = item;
|
|
242
|
+
for (const other of this.items) other.tabIndex = other === item ? 0 : -1;
|
|
243
|
+
item.focus();
|
|
244
|
+
}
|
|
245
|
+
/** Assigns the roving `tabindex` and reserves the indicator column when any row is checkable. */
|
|
246
|
+
syncItems() {
|
|
247
|
+
const items = this.items;
|
|
248
|
+
const enabled = this.enabledItems;
|
|
249
|
+
if (!this.focusedItem || !enabled.includes(this.focusedItem)) this.focusedItem = enabled[0];
|
|
250
|
+
const checkable = items.some((item) => item.type !== "item");
|
|
251
|
+
for (const item of items) {
|
|
252
|
+
item.reserveIndicator = checkable;
|
|
253
|
+
item.tabIndex = item === this.focusedItem ? 0 : -1;
|
|
254
|
+
}
|
|
255
|
+
for (const element of this.itemSlot?.assignedElements({ flatten: true }) ?? []) if (/^h[1-6]$/.test(element.localName)) element.setAttribute("role", "presentation");
|
|
256
|
+
}
|
|
257
|
+
openSubmenu(item, focus) {
|
|
258
|
+
const submenu = item.submenuElement;
|
|
259
|
+
if (!(submenu instanceof _Menu) || item.disabled) return;
|
|
260
|
+
if (submenu.open) {
|
|
261
|
+
if (focus === "first") submenu.focusFirstItem();
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
this.closeSubmenus(item);
|
|
265
|
+
submenu.anchor = item;
|
|
266
|
+
item.expanded = true;
|
|
267
|
+
submenu.show(focus);
|
|
268
|
+
}
|
|
269
|
+
closeSubmenus(except) {
|
|
270
|
+
for (const item of this.items) {
|
|
271
|
+
if (item === except || !item.hasSubmenu) continue;
|
|
272
|
+
const submenu = item.submenuElement;
|
|
273
|
+
if (submenu instanceof _Menu) submenu.hide();
|
|
274
|
+
item.expanded = false;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
itemFromEvent(event) {
|
|
278
|
+
const target = event.target;
|
|
279
|
+
if (!(target instanceof Element)) return void 0;
|
|
280
|
+
const item = target instanceof MenuItem ? target : target.closest("c2-menu-item") ?? void 0;
|
|
281
|
+
return item && this.items.includes(item) ? item : void 0;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Focus goes back to the trigger unless the dismissal handed it to a real control elsewhere — clicking a row leaves
|
|
285
|
+
* the focus on that row in Chromium and WebKit but on the body in Firefox, and either way the trigger is where the
|
|
286
|
+
* user was.
|
|
287
|
+
*/
|
|
288
|
+
shouldReturnFocus() {
|
|
289
|
+
let active = document.activeElement;
|
|
290
|
+
while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement;
|
|
291
|
+
if (!active || active === document.body || active === document.documentElement) return true;
|
|
292
|
+
return this.items.some((item) => item === active || item.contains(active)) || !!this.overlay?.contains(active);
|
|
293
|
+
}
|
|
294
|
+
/** Focus goes back to the row that owns this submenu, or to the trigger. */
|
|
295
|
+
returnFocus() {
|
|
296
|
+
const owner = this.ownerItem;
|
|
297
|
+
if (owner) {
|
|
298
|
+
owner.focus();
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
(this.triggerElement ?? this.anchorElement)?.focus();
|
|
302
|
+
}
|
|
303
|
+
firstUpdated() {
|
|
304
|
+
this.syncAnchor();
|
|
305
|
+
this.syncItems();
|
|
306
|
+
}
|
|
307
|
+
willUpdate(changed) {
|
|
308
|
+
if (changed.has("anchor") && this.anchor) this.anchorElement = this.resolveAnchor();
|
|
309
|
+
else if (!this.anchorElement) this.anchorElement = this.querySelector(":scope > [slot=\"trigger\"]");
|
|
310
|
+
}
|
|
311
|
+
updated(changed) {
|
|
312
|
+
if (changed.has("open") && this.ownerItem) this.ownerItem.expanded = this.open;
|
|
313
|
+
if (changed.has("disabled") && this.disabled) this.hide();
|
|
314
|
+
}
|
|
315
|
+
render() {
|
|
316
|
+
return html`<slot
|
|
317
|
+
name="trigger"
|
|
318
|
+
@slotchange=${this.handleTriggerSlotChange}
|
|
319
|
+
@pointerdown=${this.handleTriggerPointerDown}
|
|
320
|
+
@click=${this.handleTriggerClick}
|
|
321
|
+
@keydown=${this.handleTriggerKeydown}
|
|
322
|
+
></slot>
|
|
323
|
+
<c2-overlay
|
|
324
|
+
id="overlay"
|
|
325
|
+
class="overlay"
|
|
326
|
+
.anchor=${this.anchorElement ?? void 0}
|
|
327
|
+
.placement=${this.placement}
|
|
328
|
+
.offset=${this.offset}
|
|
329
|
+
.crossOffset=${this.crossOffset}
|
|
330
|
+
.open=${this.open}
|
|
331
|
+
?fit-anchor=${this.fitAnchor}
|
|
332
|
+
?free-width=${!this.hasTrigger}
|
|
333
|
+
@toggle=${this.handleOverlayToggle}
|
|
334
|
+
>
|
|
335
|
+
<div
|
|
336
|
+
class="menu"
|
|
337
|
+
role="menu"
|
|
338
|
+
aria-orientation="vertical"
|
|
339
|
+
aria-label=${this.ariaLabel || nothing}
|
|
340
|
+
@click=${this.handleItemClick}
|
|
341
|
+
@pointerover=${this.handlePointerOver}
|
|
342
|
+
@keydown=${this.handleKeydown}
|
|
343
|
+
@menu-select=${this.handleMenuSelect}
|
|
344
|
+
>
|
|
345
|
+
<slot @slotchange=${this.handleSlotChange}></slot>
|
|
346
|
+
</div>
|
|
347
|
+
</c2-overlay>`;
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
__decorate([property({
|
|
351
|
+
type: Boolean,
|
|
352
|
+
reflect: true
|
|
353
|
+
})], Menu.prototype, "open", void 0);
|
|
354
|
+
__decorate([property({ reflect: true })], Menu.prototype, "placement", void 0);
|
|
355
|
+
__decorate([property({ type: Number })], Menu.prototype, "offset", void 0);
|
|
356
|
+
__decorate([property({
|
|
357
|
+
type: Number,
|
|
358
|
+
attribute: "cross-offset"
|
|
359
|
+
})], Menu.prototype, "crossOffset", void 0);
|
|
360
|
+
__decorate([property()], Menu.prototype, "anchor", void 0);
|
|
361
|
+
__decorate([property({
|
|
362
|
+
type: Boolean,
|
|
363
|
+
attribute: "fit-anchor"
|
|
364
|
+
})], Menu.prototype, "fitAnchor", void 0);
|
|
365
|
+
__decorate([property({
|
|
366
|
+
type: Boolean,
|
|
367
|
+
attribute: "keep-open"
|
|
368
|
+
})], Menu.prototype, "keepOpen", void 0);
|
|
369
|
+
__decorate([property({
|
|
370
|
+
type: Boolean,
|
|
371
|
+
reflect: true
|
|
372
|
+
})], Menu.prototype, "disabled", void 0);
|
|
373
|
+
__decorate([property({ attribute: "aria-label" })], Menu.prototype, "ariaLabel", void 0);
|
|
374
|
+
__decorate([query("#overlay")], Menu.prototype, "overlay", void 0);
|
|
375
|
+
__decorate([query("slot[name=\"trigger\"]")], Menu.prototype, "triggerSlot", void 0);
|
|
376
|
+
__decorate([query("slot:not([name])")], Menu.prototype, "itemSlot", void 0);
|
|
377
|
+
__decorate([state()], Menu.prototype, "anchorElement", void 0);
|
|
378
|
+
__decorate([state()], Menu.prototype, "hasTrigger", void 0);
|
|
379
|
+
Menu = _Menu = __decorate([customElement("c2-menu")], Menu);
|
|
380
|
+
//#endregion
|
|
381
|
+
export { Menu };
|
package/package.json
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@c2n/menu",
|
|
3
|
+
"version": "0.0.7",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/menu.js",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./types/src/menu.d.ts",
|
|
9
|
+
"default": "./dist/menu.js"
|
|
10
|
+
},
|
|
11
|
+
"./menu-item.js": {
|
|
12
|
+
"types": "./types/src/menu-item.d.ts",
|
|
13
|
+
"default": "./dist/menu-item.js"
|
|
14
|
+
},
|
|
15
|
+
"./react": {
|
|
16
|
+
"types": "./react.d.ts",
|
|
17
|
+
"default": "./react.js"
|
|
18
|
+
},
|
|
19
|
+
"./vue": {
|
|
20
|
+
"types": "./vue.d.ts",
|
|
21
|
+
"default": "./vue.js"
|
|
22
|
+
},
|
|
23
|
+
"./custom-elements.json": "./custom-elements.json"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"types",
|
|
28
|
+
"react.d.ts",
|
|
29
|
+
"react.js",
|
|
30
|
+
"vue.d.ts",
|
|
31
|
+
"vue.js"
|
|
32
|
+
],
|
|
33
|
+
"keywords": [
|
|
34
|
+
"menu",
|
|
35
|
+
"dropdown menu",
|
|
36
|
+
"context menu",
|
|
37
|
+
"web component",
|
|
38
|
+
"lit"
|
|
39
|
+
],
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"author": "code2nguyen@gmail.com",
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"registry": "https://registry.npmjs.org",
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"repository": {
|
|
47
|
+
"type": "git",
|
|
48
|
+
"url": "https://github.com/code2nguyen/web-components.git"
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"dev": "vite",
|
|
52
|
+
"build": "wireit",
|
|
53
|
+
"build:only": "vite build",
|
|
54
|
+
"type-check": "wireit"
|
|
55
|
+
},
|
|
56
|
+
"wireit": {
|
|
57
|
+
"type-check": {
|
|
58
|
+
"dependencies": [
|
|
59
|
+
"../../core:build",
|
|
60
|
+
"../overlay:build"
|
|
61
|
+
],
|
|
62
|
+
"command": "tsc -p tsconfig.lib.json --composite false"
|
|
63
|
+
},
|
|
64
|
+
"build": {
|
|
65
|
+
"dependencies": [
|
|
66
|
+
"type-check"
|
|
67
|
+
],
|
|
68
|
+
"command": "vite build"
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"dependencies": {
|
|
72
|
+
"@c2n/core": "0.0.7",
|
|
73
|
+
"@c2n/overlay": "0.0.7",
|
|
74
|
+
"lit": "3.3.3"
|
|
75
|
+
},
|
|
76
|
+
"devDependencies": {
|
|
77
|
+
"@c2n/config": "*"
|
|
78
|
+
},
|
|
79
|
+
"customElements": "custom-elements.json",
|
|
80
|
+
"gitHead": "c8db398a27f7cd417d665fdf5da455fee2d4e910"
|
|
81
|
+
}
|
package/react.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// GENERATED by @c2n/framework-types. Do not edit by hand.
|
|
2
|
+
//
|
|
3
|
+
// JSX types for the custom elements of @c2n/menu. Import once, anywhere in the program:
|
|
4
|
+
//
|
|
5
|
+
// import '@c2n/menu/react'
|
|
6
|
+
//
|
|
7
|
+
// Register the elements at module scope before React renders, or React writes an object prop as an
|
|
8
|
+
// attribute and it stringifies.
|
|
9
|
+
|
|
10
|
+
import type { DetailedHTMLProps, HTMLAttributes } from 'react'
|
|
11
|
+
import type { Menu } from '@c2n/menu'
|
|
12
|
+
import type { MenuItem } from '@c2n/menu/menu-item.js'
|
|
13
|
+
|
|
14
|
+
/** Standard React host-element attributes plus the element's own public properties. */
|
|
15
|
+
type C2Props<T> = DetailedHTMLProps<HTMLAttributes<T>, T> & Partial<Omit<T, keyof HTMLElement>>
|
|
16
|
+
|
|
17
|
+
declare module 'react' {
|
|
18
|
+
namespace JSX {
|
|
19
|
+
interface IntrinsicElements {
|
|
20
|
+
'c2-menu': C2Props<Menu>
|
|
21
|
+
'c2-menu-item': C2Props<MenuItem>
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export {}
|
package/react.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { LitElement, type PropertyValues } from 'lit';
|
|
2
|
+
import type { TypedAddEventListener, TypedRemoveEventListener } from '@c2n/core/event-helper.js';
|
|
3
|
+
/** Plain command (`item`), or a checkable row announced as `menuitemcheckbox` / `menuitemradio`. */
|
|
4
|
+
export type MenuItemType = 'item' | 'checkbox' | 'radio';
|
|
5
|
+
export interface MenuSelectEventDetail {
|
|
6
|
+
/** The `value` of the activated row. */
|
|
7
|
+
value: string;
|
|
8
|
+
/** The `checked` state after the activation; always `false` for a plain row. */
|
|
9
|
+
checked: boolean;
|
|
10
|
+
/** The `data` payload of the activated row. */
|
|
11
|
+
data: unknown;
|
|
12
|
+
}
|
|
13
|
+
/** Events fired by {@link MenuItem}, keyed for `addEventListener`. */
|
|
14
|
+
export interface MenuItemEventMap {
|
|
15
|
+
'menu-select': CustomEvent<MenuSelectEventDetail>;
|
|
16
|
+
'checked-change': CustomEvent<{
|
|
17
|
+
checked: boolean;
|
|
18
|
+
value: string;
|
|
19
|
+
}>;
|
|
20
|
+
}
|
|
21
|
+
export interface MenuItem {
|
|
22
|
+
addEventListener: TypedAddEventListener<MenuItem, MenuItemEventMap>;
|
|
23
|
+
removeEventListener: TypedRemoveEventListener<MenuItem, MenuItemEventMap>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A row of a `c2-menu`: a command, a link (`href`), a checkbox or a radio (`type`), or the parent of a nested menu
|
|
27
|
+
* slotted into `submenu`. Activating a row fires `menu-select` and closes the menu unless the row is marked
|
|
28
|
+
* `keep-open`. The default slot is the label, `description` a second muted line, `prefix-icon` / `suffix-icon` take an
|
|
29
|
+
* inline SVG, a `c2-feather-*` icon or a `c2-mat-icon`, and `shortcut` holds a keyboard hint (plain text or a `c2-kbd`).
|
|
30
|
+
*
|
|
31
|
+
* Rows never handle the keyboard themselves: the menu owns arrow keys, typeahead and the roving `tabindex`, and marks
|
|
32
|
+
* every row `reserve-indicator` while any sibling is checkable so the labels of plain and checkable rows line up.
|
|
33
|
+
*
|
|
34
|
+
* @tag c2-menu-item
|
|
35
|
+
*
|
|
36
|
+
* @slot default - The row label. Falls back to `label`, then `value`, when empty.
|
|
37
|
+
* @slot description - Secondary line under the label (muted, smaller).
|
|
38
|
+
* @slot prefix-icon - Icon shown before the label.
|
|
39
|
+
* @slot suffix-icon - Icon shown after the label; replaced by the chevron when the row has a submenu.
|
|
40
|
+
* @slot shortcut - Keyboard hint, aligned to the end of the row.
|
|
41
|
+
* @slot submenu - A nested `c2-menu` opened by hover, click, Enter or ArrowRight.
|
|
42
|
+
*
|
|
43
|
+
* @event {CustomEvent<MenuSelectEventDetail>} menu-select - Fired after the row is activated by a click, Enter or Space. Cancelable; a row with a submenu never fires it.
|
|
44
|
+
* @event {CustomEvent<{ checked: boolean; value: string }>} checked-change - Fired after the `checked` state of a `checkbox` / `radio` row changes.
|
|
45
|
+
*
|
|
46
|
+
* @cssproperty {pixel} [--c2-menu-item--min-height=32px]
|
|
47
|
+
* @cssproperty {pixel} [--c2-menu-item--gap=8px] - Space between the indicator, icons, label and shortcut.
|
|
48
|
+
*
|
|
49
|
+
* @cssproperty {border-radius} [--c2-menu-item--border-top-left-radius=6px]
|
|
50
|
+
* @cssproperty {border-radius} [--c2-menu-item--border-top-right-radius=6px]
|
|
51
|
+
* @cssproperty {border-radius} [--c2-menu-item--border-bottom-left-radius=6px]
|
|
52
|
+
* @cssproperty {border-radius} [--c2-menu-item--border-bottom-right-radius=6px]
|
|
53
|
+
*
|
|
54
|
+
* @cssproperty {padding} [--c2-menu-item--padding-top=6px]
|
|
55
|
+
* @cssproperty {padding} [--c2-menu-item--padding-right=8px]
|
|
56
|
+
* @cssproperty {padding} [--c2-menu-item--padding-bottom=6px]
|
|
57
|
+
* @cssproperty {padding} [--c2-menu-item--padding-left=8px]
|
|
58
|
+
*
|
|
59
|
+
* @cssproperty {font-size} [--c2-menu-item--font-size=14px]
|
|
60
|
+
* @cssproperty {font-weight} --c2-menu-item--font-weight
|
|
61
|
+
* @cssproperty {font-style} --c2-menu-item--font-style
|
|
62
|
+
* @cssproperty {font-family} --c2-menu-item--font-family
|
|
63
|
+
* @cssproperty {pixel} [--c2-menu-item--line-height=20px]
|
|
64
|
+
*
|
|
65
|
+
* @cssproperty {color} [--c2-menu-item--color=#18181b]
|
|
66
|
+
* @cssproperty {color} [--c2-menu-item--background=transparent]
|
|
67
|
+
*
|
|
68
|
+
* @cssproperty {color} [--c2-menu-item__hover--background=#f4f4f5] - Also the highlight of the keyboard-focused row.
|
|
69
|
+
* @cssproperty {color} --c2-menu-item__hover--color - Defaults to the row colour.
|
|
70
|
+
*
|
|
71
|
+
* @cssproperty {color} [--c2-menu-item__description--color=#71717a]
|
|
72
|
+
* @cssproperty {font-size} [--c2-menu-item__description--font-size=12px]
|
|
73
|
+
* @cssproperty {pixel} [--c2-menu-item__description--line-height=16px]
|
|
74
|
+
* @cssproperty {pixel} [--c2-menu-item__description--margin-top=1px]
|
|
75
|
+
*
|
|
76
|
+
* @cssproperty {color} [--c2-menu-item__shortcut--color=#52525b]
|
|
77
|
+
* @cssproperty {font-size} [--c2-menu-item__shortcut--font-size=11px]
|
|
78
|
+
* @cssproperty {letter-spacing} [--c2-menu-item__shortcut--letter-spacing=0.08em]
|
|
79
|
+
*
|
|
80
|
+
* @cssproperty {pixel} [--c2-menu-item__icon--size=16px]
|
|
81
|
+
* @cssproperty {color} [--c2-menu-item__icon--color=#71717a]
|
|
82
|
+
*
|
|
83
|
+
* @cssproperty {pixel} [--c2-menu-item__indicator--size=16px]
|
|
84
|
+
* @cssproperty {color} --c2-menu-item__indicator--color - The check mark / radio dot. Defaults to the row colour.
|
|
85
|
+
*
|
|
86
|
+
* @cssproperty {color} [--c2-menu-item__destructive--color=#dc2626] - Label and icon colour of a `destructive` row.
|
|
87
|
+
* @cssproperty {color} [--c2-menu-item__destructive__hover--background=#fef2f2]
|
|
88
|
+
*
|
|
89
|
+
* @cssproperty {outline} [--c2-menu-item__focus--outline=2px solid rgba(2, 101, 220, 0.4)]
|
|
90
|
+
* @cssproperty {pixel} [--c2-menu-item__focus--outline-offset=-2px]
|
|
91
|
+
*
|
|
92
|
+
* @cssproperty {opacity} [--c2-menu-item__disabled--opacity=0.38]
|
|
93
|
+
*/
|
|
94
|
+
export declare class MenuItem extends LitElement {
|
|
95
|
+
static styles: import("lit").CSSResult;
|
|
96
|
+
/** The value reported in `menu-select`. */
|
|
97
|
+
value: string;
|
|
98
|
+
/** Dims the row and ignores clicks and keyboard. */
|
|
99
|
+
disabled: boolean;
|
|
100
|
+
/** `item` is a command, `checkbox` toggles, `radio` picks one row of a `name` group. */
|
|
101
|
+
type: MenuItemType;
|
|
102
|
+
/** Checked state of a `checkbox` / `radio` row; shown as a check mark or a dot. */
|
|
103
|
+
checked: boolean;
|
|
104
|
+
/** Radio group name. Selecting a `radio` row unchecks the other rows of the same group in that menu. */
|
|
105
|
+
name?: string;
|
|
106
|
+
/** Makes the row a link; Enter and a click navigate, and `menu-select` still fires. */
|
|
107
|
+
href?: string;
|
|
108
|
+
/** Browsing context used when `href` is set, such as `_blank`. */
|
|
109
|
+
target?: string;
|
|
110
|
+
/** Paints the row in the destructive colour, for delete-style commands. */
|
|
111
|
+
destructive: boolean;
|
|
112
|
+
/** Keeps the menu open after this row is activated, e.g. for a checkbox row. */
|
|
113
|
+
keepOpen: boolean;
|
|
114
|
+
/** Label used for typeahead and as the default slot's fallback content. */
|
|
115
|
+
label?: string;
|
|
116
|
+
/** Arbitrary payload returned alongside `value` in `menu-select`. Not an attribute. */
|
|
117
|
+
data?: unknown;
|
|
118
|
+
/** Set by the parent menu while this row's submenu is open. Announced as `aria-expanded`. */
|
|
119
|
+
expanded: boolean;
|
|
120
|
+
/** Private: set by the parent menu on every row while any sibling is checkable, so the labels line up. */
|
|
121
|
+
reserveIndicator: boolean;
|
|
122
|
+
private hasDescription;
|
|
123
|
+
private submenuPresent;
|
|
124
|
+
private contentSlot;
|
|
125
|
+
private submenuSlot?;
|
|
126
|
+
/** `role` written by the author, kept over the automatic one. */
|
|
127
|
+
private authorRole;
|
|
128
|
+
/** Whether a menu is slotted into `submenu`. */
|
|
129
|
+
get hasSubmenu(): boolean;
|
|
130
|
+
/** The element slotted into `submenu`, if any. The parent menu opens and closes it. */
|
|
131
|
+
get submenuElement(): HTMLElement | null;
|
|
132
|
+
/** The text the menu matches while typing: `label` when set, otherwise the slotted label text. */
|
|
133
|
+
get displayText(): string;
|
|
134
|
+
connectedCallback(): void;
|
|
135
|
+
/**
|
|
136
|
+
* Activates the row the way Enter does: a link is navigated, a checkable row toggles, and `menu-select` fires.
|
|
137
|
+
* A row with a submenu does nothing — the menu opens the submenu instead.
|
|
138
|
+
*/
|
|
139
|
+
activate(): void;
|
|
140
|
+
private select;
|
|
141
|
+
private handleClick;
|
|
142
|
+
private handleDescriptionSlotChange;
|
|
143
|
+
private handleSubmenuSlotChange;
|
|
144
|
+
protected updated(changed: PropertyValues<this>): void;
|
|
145
|
+
private syncAria;
|
|
146
|
+
private renderIndicator;
|
|
147
|
+
private renderSubmenuIcon;
|
|
148
|
+
private renderInner;
|
|
149
|
+
render(): import("lit-html").TemplateResult<1>;
|
|
150
|
+
}
|
|
151
|
+
declare global {
|
|
152
|
+
interface HTMLElementTagNameMap {
|
|
153
|
+
'c2-menu-item': MenuItem;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
//# sourceMappingURL=menu-item.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"menu-item.d.ts","sourceRoot":"","sources":["../../src/menu-item.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAiC,KAAK,cAAc,EAAE,MAAM,KAAK,CAAA;AAGpF,OAAO,KAAK,EAAE,qBAAqB,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAA;AAIhG,oGAAoG;AACpG,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,UAAU,GAAG,OAAO,CAAA;AAExD,MAAM,WAAW,qBAAqB;IACpC,wCAAwC;IACxC,KAAK,EAAE,MAAM,CAAA;IACb,gFAAgF;IAChF,OAAO,EAAE,OAAO,CAAA;IAChB,+CAA+C;IAC/C,IAAI,EAAE,OAAO,CAAA;CACd;AAED,sEAAsE;AACtE,MAAM,WAAW,gBAAgB;IAC/B,aAAa,EAAE,WAAW,CAAC,qBAAqB,CAAC,CAAA;IACjD,gBAAgB,EAAE,WAAW,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CACnE;AAED,MAAM,WAAW,QAAQ;IACvB,gBAAgB,EAAE,qBAAqB,CAAC,QAAQ,EAAE,gBAAgB,CAAC,CAAA;IACnE,mBAAmB,EAAE,wBAAwB,CAAC,QAAQ,EAAE,gBAAgB,CAAC,CAAA;CAC1E;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoEG;AACH,qBACa,QAAS,SAAQ,UAAU;IACtC,OAAgB,MAAM,0BAAoB;IAE1C,2CAA2C;IACd,KAAK,SAAK;IAEvC,oDAAoD;IACR,QAAQ,UAAQ;IAE5D,wFAAwF;IAC3D,IAAI,EAAE,YAAY,CAAS;IAExD,mFAAmF;IACvC,OAAO,UAAQ;IAE3D,wGAAwG;IAC3E,IAAI,CAAC,EAAE,MAAM,CAAA;IAE1C,uFAAuF;IAC3E,IAAI,CAAC,EAAE,MAAM,CAAA;IAEzB,kEAAkE;IACtD,MAAM,CAAC,EAAE,MAAM,CAAA;IAE3B,2EAA2E;IAC/B,WAAW,UAAQ;IAE/D,gFAAgF;IAC3B,QAAQ,UAAQ;IAErE,2EAA2E;IAC9C,KAAK,CAAC,EAAE,MAAM,CAAA;IAE3C,uFAAuF;IACvD,IAAI,CAAC,EAAE,OAAO,CAAA;IAE9C,6FAA6F;IACjD,QAAQ,UAAQ;IAE5D,0GAA0G;IAC9B,gBAAgB,UAAQ;IAE3F,OAAO,CAAC,cAAc,CAAQ;IAE9B,OAAO,CAAC,cAAc,CAAQ;IAEZ,OAAO,CAAC,WAAW,CAAkB;IAEjC,OAAO,CAAC,WAAW,CAAC,CAAiB;IAEpE,iEAAiE;IACjE,OAAO,CAAC,UAAU,CAAsB;IAExC,gDAAgD;IAChD,IAAI,UAAU,IAAI,OAAO,CAExB;IAED,uFAAuF;IACvF,IAAI,cAAc,IAAI,WAAW,GAAG,IAAI,CAGvC;IAED,kGAAkG;IAClG,IAAI,WAAW,IAAI,MAAM,CAKxB;IAEQ,iBAAiB;IAK1B;;;OAGG;IACH,QAAQ,IAAI,IAAI;IAShB,OAAO,CAAC,MAAM;IAcd,OAAO,CAAC,WAAW,CASlB;IAED,OAAO,CAAC,2BAA2B;IAKnC,OAAO,CAAC,uBAAuB;cAIZ,OAAO,CAAC,OAAO,EAAE,cAAc,CAAC,IAAI,CAAC,GAAG,IAAI;IAO/D,OAAO,CAAC,QAAQ;IAwBhB,OAAO,CAAC,eAAe;IASvB,OAAO,CAAC,iBAAiB;IAIzB,OAAO,CAAC,WAAW;IAeV,MAAM;CAgBhB;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,cAAc,EAAE,QAAQ,CAAA;KACzB;CACF"}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { LitElement, type PropertyValues } from 'lit';
|
|
2
|
+
import type { TypedAddEventListener, TypedRemoveEventListener } from '@c2n/core/event-helper.js';
|
|
3
|
+
import type { Placement } from '@c2n/overlay';
|
|
4
|
+
import { MenuItem } from './menu-item';
|
|
5
|
+
import type { MenuSelectEventDetail } from './menu-item';
|
|
6
|
+
import '@c2n/overlay';
|
|
7
|
+
import './menu-item';
|
|
8
|
+
export type { Placement };
|
|
9
|
+
export type { MenuItemType, MenuSelectEventDetail } from './menu-item';
|
|
10
|
+
/** Events fired by {@link Menu}, keyed for `addEventListener`. */
|
|
11
|
+
export interface MenuEventMap {
|
|
12
|
+
'menu-select': CustomEvent<MenuSelectEventDetail>;
|
|
13
|
+
toggle: ToggleEvent;
|
|
14
|
+
}
|
|
15
|
+
export interface Menu {
|
|
16
|
+
addEventListener: TypedAddEventListener<Menu, MenuEventMap>;
|
|
17
|
+
removeEventListener: TypedRemoveEventListener<Menu, MenuEventMap>;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A menu of commands anchored to a trigger. The trigger is slotted into `trigger` (a `c2-button`, a `c2-icon-button`,
|
|
21
|
+
* a plain `<button>`, …) or named by `anchor`; the rows are `c2-menu-item` children, with `<hr>` separators and
|
|
22
|
+
* `<h1>`–`<h6>` group headings between them. The surface is a native popover on the top layer positioned by
|
|
23
|
+
* `c2-overlay`, so it flips near the viewport edge, light-dismisses on Escape and outside clicks, and is never clipped
|
|
24
|
+
* by an `overflow: hidden` parent.
|
|
25
|
+
*
|
|
26
|
+
* Keyboard: the trigger opens the menu with Enter, Space or ArrowDown (ArrowUp opens it on the last row). Inside,
|
|
27
|
+
* Arrow Up/Down move between enabled rows, Home/End jump to the first/last, typing letters jumps to a matching row,
|
|
28
|
+
* Enter and Space activate, ArrowRight opens a submenu, ArrowLeft returns to its parent row, and Escape or Tab closes
|
|
29
|
+
* the menu and returns focus to the trigger.
|
|
30
|
+
*
|
|
31
|
+
* Activating a row fires `menu-select` and closes the menu; `keep-open` on the menu (or on a single row) leaves it
|
|
32
|
+
* open, which is what a list of `checkbox` rows wants.
|
|
33
|
+
*
|
|
34
|
+
* The surface is as wide as its rows, never narrower than `--c2-menu--min-width` and, when the menu owns its trigger,
|
|
35
|
+
* never narrower than that trigger either. `fit-anchor` pins it to exactly the trigger's width instead.
|
|
36
|
+
*
|
|
37
|
+
* @tag c2-menu
|
|
38
|
+
*
|
|
39
|
+
* @slot default - The rows: `c2-menu-item` elements, plus `<hr>` separators and `<h1>`–`<h6>` group headings.
|
|
40
|
+
* @slot trigger - The element that opens the menu. Omit it and point `anchor` at an element elsewhere on the page.
|
|
41
|
+
*
|
|
42
|
+
* @event {CustomEvent<MenuSelectEventDetail>} menu-select - Bubbles from the activated row: `detail.value`, `detail.checked` and `detail.data` of that row.
|
|
43
|
+
* @event {ToggleEvent} toggle - Fired after the menu opens or closes; `newState` is `open` or `closed`.
|
|
44
|
+
*
|
|
45
|
+
* @cssproperty {color} [--c2-menu--background=#ffffff]
|
|
46
|
+
* @cssproperty {pixel} [--c2-menu--min-width=128px] - Floor for the surface, which is otherwise as wide as its content (and never narrower than a slotted trigger).
|
|
47
|
+
* @cssproperty {pixel} [--c2-menu--max-height=320px] - The menu scrolls past this height.
|
|
48
|
+
*
|
|
49
|
+
* @cssproperty {border} [--c2-menu--border-top=1px solid #e4e4e7]
|
|
50
|
+
* @cssproperty {border} [--c2-menu--border-right=1px solid #e4e4e7]
|
|
51
|
+
* @cssproperty {border} [--c2-menu--border-bottom=1px solid #e4e4e7]
|
|
52
|
+
* @cssproperty {border} [--c2-menu--border-left=1px solid #e4e4e7]
|
|
53
|
+
*
|
|
54
|
+
* @cssproperty {border-radius} [--c2-menu--border-top-left-radius=8px]
|
|
55
|
+
* @cssproperty {border-radius} [--c2-menu--border-top-right-radius=8px]
|
|
56
|
+
* @cssproperty {border-radius} [--c2-menu--border-bottom-left-radius=8px]
|
|
57
|
+
* @cssproperty {border-radius} [--c2-menu--border-bottom-right-radius=8px]
|
|
58
|
+
*
|
|
59
|
+
* @cssproperty {box-shadow} [--c2-menu--box-shadow=0 8px 24px rgba(24, 24, 27, 0.08)]
|
|
60
|
+
*
|
|
61
|
+
* @cssproperty {padding} [--c2-menu--padding-top=4px]
|
|
62
|
+
* @cssproperty {padding} [--c2-menu--padding-right=4px]
|
|
63
|
+
* @cssproperty {padding} [--c2-menu--padding-bottom=4px]
|
|
64
|
+
* @cssproperty {padding} [--c2-menu--padding-left=4px]
|
|
65
|
+
*
|
|
66
|
+
* @cssproperty {pixel} --c2-menu--gap - Space between the rows.
|
|
67
|
+
*
|
|
68
|
+
* @cssproperty {color} [--c2-menu__separator--color=#e4e4e7]
|
|
69
|
+
* @cssproperty {margin} [--c2-menu__separator--margin=4px 0]
|
|
70
|
+
*
|
|
71
|
+
* @cssproperty {color} [--c2-menu__heading--color=#71717a]
|
|
72
|
+
* @cssproperty {font-size} [--c2-menu__heading--font-size=11px]
|
|
73
|
+
* @cssproperty {font-weight} [--c2-menu__heading--font-weight=600]
|
|
74
|
+
* @cssproperty {letter-spacing} [--c2-menu__heading--letter-spacing=0.06em]
|
|
75
|
+
* @cssproperty {text-transform} [--c2-menu__heading--text-transform=uppercase]
|
|
76
|
+
* @cssproperty {padding} [--c2-menu__heading--padding-top=8px]
|
|
77
|
+
* @cssproperty {padding} [--c2-menu__heading--padding-right=8px]
|
|
78
|
+
* @cssproperty {padding} [--c2-menu__heading--padding-bottom=4px]
|
|
79
|
+
* @cssproperty {padding} [--c2-menu__heading--padding-left=8px]
|
|
80
|
+
*
|
|
81
|
+
* @internalcomponent c2-overlay
|
|
82
|
+
*
|
|
83
|
+
* @slotcomponent c2-menu-item
|
|
84
|
+
*/
|
|
85
|
+
export declare class Menu extends LitElement {
|
|
86
|
+
static styles: import("lit").CSSResult;
|
|
87
|
+
/** Whether the menu is showing. Set it to open or close the menu from code. */
|
|
88
|
+
open: boolean;
|
|
89
|
+
/** Preferred placement of the surface (floating-ui names). Submenus default to `right-start`. */
|
|
90
|
+
placement: 'top' | 'top-start' | 'top-end' | 'right' | 'right-start' | 'right-end' | 'bottom' | 'bottom-start' | 'bottom-end' | 'left' | 'left-start' | 'left-end';
|
|
91
|
+
/** Distance in pixels from the trigger along the placement direction. Defaults to the overlay's 8px gap. */
|
|
92
|
+
offset: number | undefined;
|
|
93
|
+
/** Shift in pixels perpendicular to the placement direction. Positive values move towards the aligned end. */
|
|
94
|
+
crossOffset: number | undefined;
|
|
95
|
+
/** Anchor element, or (attribute) the id of an element in the same tree. Defaults to the slotted trigger. */
|
|
96
|
+
anchor: string | HTMLElement | undefined;
|
|
97
|
+
/** Make the surface exactly as wide as the anchor instead of as wide as its content. */
|
|
98
|
+
fitAnchor: boolean;
|
|
99
|
+
/** Keep the menu open after a row is activated. Also available per row as `keep-open`. */
|
|
100
|
+
keepOpen: boolean;
|
|
101
|
+
/** Ignores the trigger and never opens. */
|
|
102
|
+
disabled: boolean;
|
|
103
|
+
/** Accessible name of the surface. */
|
|
104
|
+
ariaLabel: string | null;
|
|
105
|
+
private overlay?;
|
|
106
|
+
private triggerSlot?;
|
|
107
|
+
private itemSlot?;
|
|
108
|
+
private anchorElement;
|
|
109
|
+
/** Whether something is slotted into `trigger`; a menu opened from one is never narrower than it. */
|
|
110
|
+
private hasTrigger;
|
|
111
|
+
/** The row that holds the roving `tabindex`. */
|
|
112
|
+
private focusedItem;
|
|
113
|
+
/** Which row to focus once the surface is open. */
|
|
114
|
+
private pendingFocus;
|
|
115
|
+
private typeahead;
|
|
116
|
+
private typeaheadTimer;
|
|
117
|
+
/** Open state at the pointerdown that started the current press on the trigger, see `handleTriggerPointerDown`. */
|
|
118
|
+
private openBeforePress;
|
|
119
|
+
/** Slotted rows, in DOM order (separators and headings excluded). */
|
|
120
|
+
get items(): MenuItem[];
|
|
121
|
+
private get enabledItems();
|
|
122
|
+
/** The element that opens this menu: the slotted trigger, or the resolved `anchor`. */
|
|
123
|
+
get triggerElement(): HTMLElement | null;
|
|
124
|
+
/** The row this menu is a submenu of, when it is slotted into a `c2-menu-item`. */
|
|
125
|
+
get ownerItem(): MenuItem | null;
|
|
126
|
+
connectedCallback(): void;
|
|
127
|
+
/** Opens the menu. Pass `'last'` to start on the last row, `'none'` to leave focus where it is. */
|
|
128
|
+
show(focus?: 'first' | 'last' | 'none'): void;
|
|
129
|
+
hide(): void;
|
|
130
|
+
toggle(force?: boolean): void;
|
|
131
|
+
/** Moves focus to the first enabled row. */
|
|
132
|
+
focusFirstItem(): void;
|
|
133
|
+
/** Moves focus to the last enabled row. */
|
|
134
|
+
focusLastItem(): void;
|
|
135
|
+
private resolveAnchor;
|
|
136
|
+
private syncAnchor;
|
|
137
|
+
/**
|
|
138
|
+
* `aria-haspopup` / `aria-expanded` belong on the element that carries the button role. For a `c2-*` trigger that is
|
|
139
|
+
* the control inside its shadow root, not the host, which is a plain generic element.
|
|
140
|
+
*/
|
|
141
|
+
private syncTriggerAria;
|
|
142
|
+
private focusItem;
|
|
143
|
+
/** Assigns the roving `tabindex` and reserves the indicator column when any row is checkable. */
|
|
144
|
+
private syncItems;
|
|
145
|
+
private openSubmenu;
|
|
146
|
+
private closeSubmenus;
|
|
147
|
+
private itemFromEvent;
|
|
148
|
+
/**
|
|
149
|
+
* The popover light-dismisses itself when the press lands outside it — the trigger included — so by the time the
|
|
150
|
+
* click arrives the menu is already closed and a naive toggle would reopen it. Remember the state at pointerdown,
|
|
151
|
+
* which runs before the dismissal, and toggle from that instead.
|
|
152
|
+
*/
|
|
153
|
+
private handleTriggerPointerDown;
|
|
154
|
+
private handleTriggerClick;
|
|
155
|
+
private handleTriggerKeydown;
|
|
156
|
+
private handleItemClick;
|
|
157
|
+
/** Hovering a row highlights it and swaps which submenu is open, the way a desktop menu does. */
|
|
158
|
+
private handlePointerOver;
|
|
159
|
+
private handleKeydown;
|
|
160
|
+
private handleMenuSelect;
|
|
161
|
+
private handleOverlayToggle;
|
|
162
|
+
/**
|
|
163
|
+
* Focus goes back to the trigger unless the dismissal handed it to a real control elsewhere — clicking a row leaves
|
|
164
|
+
* the focus on that row in Chromium and WebKit but on the body in Firefox, and either way the trigger is where the
|
|
165
|
+
* user was.
|
|
166
|
+
*/
|
|
167
|
+
private shouldReturnFocus;
|
|
168
|
+
/** Focus goes back to the row that owns this submenu, or to the trigger. */
|
|
169
|
+
private returnFocus;
|
|
170
|
+
private handleSlotChange;
|
|
171
|
+
private handleTriggerSlotChange;
|
|
172
|
+
protected firstUpdated(): void;
|
|
173
|
+
protected willUpdate(changed: PropertyValues<this>): void;
|
|
174
|
+
protected updated(changed: PropertyValues<this>): void;
|
|
175
|
+
render(): import("lit-html").TemplateResult<1>;
|
|
176
|
+
}
|
|
177
|
+
declare global {
|
|
178
|
+
interface HTMLElementTagNameMap {
|
|
179
|
+
'c2-menu': Menu;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
//# sourceMappingURL=menu.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"menu.d.ts","sourceRoot":"","sources":["../../src/menu.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAA4B,KAAK,cAAc,EAAE,MAAM,KAAK,CAAA;AAI/E,OAAO,KAAK,EAAE,qBAAqB,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAA;AAEhG,OAAO,KAAK,EAAW,SAAS,EAAE,MAAM,cAAc,CAAA;AAEtD,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AACtC,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAA;AAExD,OAAO,cAAc,CAAA;AACrB,OAAO,aAAa,CAAA;AAEpB,YAAY,EAAE,SAAS,EAAE,CAAA;AACzB,YAAY,EAAE,YAAY,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAA;AAEtE,kEAAkE;AAClE,MAAM,WAAW,YAAY;IAC3B,aAAa,EAAE,WAAW,CAAC,qBAAqB,CAAC,CAAA;IACjD,MAAM,EAAE,WAAW,CAAA;CACpB;AAED,MAAM,WAAW,IAAI;IACnB,gBAAgB,EAAE,qBAAqB,CAAC,IAAI,EAAE,YAAY,CAAC,CAAA;IAC3D,mBAAmB,EAAE,wBAAwB,CAAC,IAAI,EAAE,YAAY,CAAC,CAAA;CAClE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiEG;AACH,qBACa,IAAK,SAAQ,UAAU;IAClC,OAAgB,MAAM,0BAAoB;IAE1C,+EAA+E;IACnC,IAAI,UAAQ;IAExD,iGAAiG;IAEjG,SAAS,EACP,KAAK,GAAG,WAAW,GAAG,SAAS,GAAG,OAAO,GAAG,aAAa,GAAG,WAAW,GAAG,QAAQ,GAAG,cAAc,GAAG,YAAY,GAAG,MAAM,GAAG,YAAY,GAAG,UAAU,CACzI;IAEhB,4GAA4G;IAChF,MAAM,EAAE,MAAM,GAAG,SAAS,CAAY;IAElE,8GAA8G;IACvD,WAAW,EAAE,MAAM,GAAG,SAAS,CAAY;IAElG,6GAA6G;IACjG,MAAM,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAAY;IAEhE,wFAAwF;IAClC,SAAS,UAAQ;IAEvE,0FAA0F;IACrC,QAAQ,UAAQ;IAErE,2CAA2C;IACC,QAAQ,UAAQ;IAE5D,sCAAsC;IACU,SAAS,EAAE,MAAM,GAAG,IAAI,CAAO;IAE5D,OAAO,CAAC,OAAO,CAAC,CAAS;IAEb,OAAO,CAAC,WAAW,CAAC,CAAiB;IAEzC,OAAO,CAAC,QAAQ,CAAC,CAAiB;IAEpD,OAAO,CAAC,aAAa,CAA2B;IAEzD,qGAAqG;IAC5F,OAAO,CAAC,UAAU,CAAQ;IAEnC,gDAAgD;IAChD,OAAO,CAAC,WAAW,CAAkC;IAErD,mDAAmD;IACnD,OAAO,CAAC,YAAY,CAAqC;IAEzD,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,cAAc,CAA2C;IAEjE,mHAAmH;IACnH,OAAO,CAAC,eAAe,CAAiC;IAExD,qEAAqE;IACrE,IAAI,KAAK,IAAI,QAAQ,EAAE,CAItB;IAED,OAAO,KAAK,YAAY,GAEvB;IAED,uFAAuF;IACvF,IAAI,cAAc,IAAI,WAAW,GAAG,IAAI,CAGvC;IAED,mFAAmF;IACnF,IAAI,SAAS,IAAI,QAAQ,GAAG,IAAI,CAE/B;IAEQ,iBAAiB;IAM1B,mGAAmG;IACnG,IAAI,CAAC,KAAK,GAAE,OAAO,GAAG,MAAM,GAAG,MAAgB,GAAG,IAAI;IAMtD,IAAI,IAAI,IAAI;IAIZ,MAAM,CAAC,KAAK,CAAC,EAAE,OAAO,GAAG,IAAI;IAM7B,4CAA4C;IAC5C,cAAc,IAAI,IAAI;IAItB,2CAA2C;IAC3C,aAAa,IAAI,IAAI;IAKrB,OAAO,CAAC,aAAa;IASrB,OAAO,CAAC,UAAU;IAQlB;;;OAGG;YACW,eAAe;IAW7B,OAAO,CAAC,SAAS;IAOjB,iGAAiG;IACjG,OAAO,CAAC,SAAS;IAiBjB,OAAO,CAAC,WAAW;IAanB,OAAO,CAAC,aAAa;IASrB,OAAO,CAAC,aAAa;IAOrB;;;;OAIG;IACH,OAAO,CAAC,wBAAwB,CAE/B;IAED,OAAO,CAAC,kBAAkB,CAOzB;IAED,OAAO,CAAC,oBAAoB,CAc3B;IAED,OAAO,CAAC,eAAe,CAItB;IAED,iGAAiG;IACjG,OAAO,CAAC,iBAAiB,CASxB;IAED,OAAO,CAAC,aAAa,CAmEpB;IAED,OAAO,CAAC,gBAAgB,CAYvB;IAED,OAAO,CAAC,mBAAmB,CAe1B;IAED;;;;OAIG;IACH,OAAO,CAAC,iBAAiB;IAOzB,4EAA4E;IAC5E,OAAO,CAAC,WAAW;IAUnB,OAAO,CAAC,gBAAgB,CAEvB;IAED,OAAO,CAAC,uBAAuB,CAE9B;cAEkB,YAAY,IAAI,IAAI;cAKpB,UAAU,CAAC,OAAO,EAAE,cAAc,CAAC,IAAI,CAAC,GAAG,IAAI;cAM/C,OAAO,CAAC,OAAO,EAAE,cAAc,CAAC,IAAI,CAAC,GAAG,IAAI;IAKtD,MAAM;CAkChB;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,SAAS,EAAE,IAAI,CAAA;KAChB;CACF"}
|
package/vue.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// GENERATED by @c2n/framework-types. Do not edit by hand.
|
|
2
|
+
//
|
|
3
|
+
// Vue template types for the custom elements of @c2n/menu. Import once, anywhere in the program:
|
|
4
|
+
//
|
|
5
|
+
// import '@c2n/menu/vue'
|
|
6
|
+
//
|
|
7
|
+
// Tell the compiler about the tags as well, or every c2-* tag is resolved as a Vue component and renders
|
|
8
|
+
// nothing: template.compilerOptions.isCustomElement = (tag) => tag.startsWith('c2-') in vite.config.ts.
|
|
9
|
+
|
|
10
|
+
import type { DefineComponent, HTMLAttributes } from 'vue'
|
|
11
|
+
import type { Menu, MenuEventMap } from '@c2n/menu'
|
|
12
|
+
import type { MenuItem, MenuItemEventMap } from '@c2n/menu/menu-item.js'
|
|
13
|
+
|
|
14
|
+
/** The element's own public properties, plus every attribute Vue understands on a host element. */
|
|
15
|
+
type C2Props<T> = Partial<Omit<T, keyof HTMLElement>> & HTMLAttributes
|
|
16
|
+
|
|
17
|
+
declare module 'vue' {
|
|
18
|
+
interface GlobalComponents {
|
|
19
|
+
'c2-menu': DefineComponent<
|
|
20
|
+
C2Props<Menu> & {
|
|
21
|
+
'cross-offset'?: unknown
|
|
22
|
+
'fit-anchor'?: unknown
|
|
23
|
+
'keep-open'?: unknown
|
|
24
|
+
'aria-label'?: unknown
|
|
25
|
+
onMenuSelect?: (event: MenuEventMap['menu-select']) => void
|
|
26
|
+
onToggle?: (event: MenuEventMap['toggle']) => void
|
|
27
|
+
}
|
|
28
|
+
>
|
|
29
|
+
'c2-menu-item': DefineComponent<
|
|
30
|
+
C2Props<MenuItem> & {
|
|
31
|
+
'keep-open'?: unknown
|
|
32
|
+
'reserve-indicator'?: unknown
|
|
33
|
+
onMenuSelect?: (event: MenuItemEventMap['menu-select']) => void
|
|
34
|
+
onCheckedChange?: (event: MenuItemEventMap['checked-change']) => void
|
|
35
|
+
}
|
|
36
|
+
>
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
declare module '@vue/runtime-dom' {
|
|
41
|
+
interface HTMLAttributes {
|
|
42
|
+
/** Vue's own definition omits it, and slotting a plain element into a component needs it. */
|
|
43
|
+
slot?: string
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export {}
|
package/vue.js
ADDED