@m3e/menu 1.0.0-rc.1 → 1.0.0-rc.3
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 +1 -2
- package/dist/custom-elements.json +3262 -34
- package/dist/html-custom-data.json +12 -6
- package/dist/index.js +8 -8
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +7 -7
- package/dist/index.min.js.map +1 -1
- package/dist/src/MenuElement.d.ts +0 -1
- package/dist/src/MenuElement.d.ts.map +1 -1
- package/dist/src/MenuItemCheckboxElement.d.ts +2 -1
- package/dist/src/MenuItemCheckboxElement.d.ts.map +1 -1
- package/dist/src/MenuItemElement.d.ts +2 -1
- package/dist/src/MenuItemElement.d.ts.map +1 -1
- package/dist/src/MenuItemGroupElement.d.ts +0 -1
- package/dist/src/MenuItemGroupElement.d.ts.map +1 -1
- package/dist/src/MenuItemRadioElement.d.ts +2 -1
- package/dist/src/MenuItemRadioElement.d.ts.map +1 -1
- package/dist/src/MenuTriggerElement.d.ts +1 -2
- package/dist/src/MenuTriggerElement.d.ts.map +1 -1
- package/package.json +4 -4
- package/cem.config.mjs +0 -16
- package/demo/index.html +0 -112
- package/eslint.config.mjs +0 -13
- package/rollup.config.js +0 -32
- package/src/MenuElement.ts +0 -449
- package/src/MenuItemCheckboxElement.ts +0 -178
- package/src/MenuItemElement.ts +0 -210
- package/src/MenuItemElementBase.ts +0 -158
- package/src/MenuItemGroupElement.ts +0 -37
- package/src/MenuItemRadioElement.ts +0 -169
- package/src/MenuPosition.ts +0 -5
- package/src/MenuTriggerElement.ts +0 -154
- package/src/index.ts +0 -7
- package/tsconfig.json +0 -9
package/src/MenuElement.ts
DELETED
|
@@ -1,449 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */
|
|
2
|
-
import { css, CSSResultGroup, html, LitElement, unsafeCSS } from "lit";
|
|
3
|
-
import { customElement, property } from "lit/decorators.js";
|
|
4
|
-
|
|
5
|
-
import { DesignToken, ScrollController, Role } from "@m3e/core";
|
|
6
|
-
import { RovingTabIndexManager } from "@m3e/core/a11y";
|
|
7
|
-
import { positionAnchor } from "@m3e/core/anchoring";
|
|
8
|
-
|
|
9
|
-
import { M3eMenuItemElement } from "./MenuItemElement";
|
|
10
|
-
import { MenuPositionX, MenuPositionY } from "./MenuPosition";
|
|
11
|
-
import { MenuItemElementBase } from "./MenuItemElementBase";
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* @summary
|
|
15
|
-
* Presents a list of choices on a temporary surface.
|
|
16
|
-
*
|
|
17
|
-
* @description
|
|
18
|
-
* The `m3e-menu` component presents a list of choices on a temporary surface, typically anchored to a trigger element.
|
|
19
|
-
* It supports dynamic positioning via `position-x` and `position-y` attributes, and renders its contents through the default slot.
|
|
20
|
-
*
|
|
21
|
-
* @example
|
|
22
|
-
* The following example illustrates a basic menu. The `m3e-menu-trigger` is used to trigger a `m3e-menu` specified
|
|
23
|
-
* by the `for` attribute when its parenting element is activated.
|
|
24
|
-
* ```html
|
|
25
|
-
* <m3e-button>
|
|
26
|
-
* <m3e-menu-trigger for="menu1">Basic menu</m3e-menu-trigger>
|
|
27
|
-
* </m3e-button>
|
|
28
|
-
* <m3e-menu id="menu1">
|
|
29
|
-
* <m3e-menu-item>Apple</m3e-menu-item>
|
|
30
|
-
* <m3e-menu-item>Apricot</m3e-menu-item>
|
|
31
|
-
* <m3e-menu-item>Avocado</m3e-menu-item>
|
|
32
|
-
* <m3e-menu-item>Green Apple</m3e-menu-item>
|
|
33
|
-
* <m3e-menu-item>Green Grapes</m3e-menu-item>
|
|
34
|
-
* <m3e-menu-item>Olive</m3e-menu-item>
|
|
35
|
-
* <m3e-menu-item>Orange</m3e-menu-item>
|
|
36
|
-
* </m3e-menu>
|
|
37
|
-
* ```
|
|
38
|
-
*
|
|
39
|
-
* @example
|
|
40
|
-
* The next example illustrates nested menus. Submenus are triggered by placing a `m3e-menu-trigger` inside a `m3e-menu-item`.
|
|
41
|
-
* ```html
|
|
42
|
-
* <m3e-button>
|
|
43
|
-
* <m3e-menu-trigger for="menu2">Nested menus</m3e-menu-trigger>
|
|
44
|
-
* </m3e-button>
|
|
45
|
-
* <m3e-menu id="menu2">
|
|
46
|
-
* <m3e-menu-item>
|
|
47
|
-
* <m3e-menu-trigger for="menu3">Fruits with A</m3e-menu-trigger>
|
|
48
|
-
* </m3e-menu-item>
|
|
49
|
-
* <m3e-menu-item>Grapes</m3e-menu-item>
|
|
50
|
-
* <m3e-menu-item>Olive</m3e-menu-item>
|
|
51
|
-
* <m3e-menu-item>Orange</m3e-menu-item>
|
|
52
|
-
* </m3e-menu>
|
|
53
|
-
* <m3e-menu id="menu3">
|
|
54
|
-
* <m3e-menu-item>Apricot</m3e-menu-item>
|
|
55
|
-
* <m3e-menu-item>Avocado</m3e-menu-item>
|
|
56
|
-
* <m3e-menu-item>
|
|
57
|
-
* <m3e-menu-trigger for="menu4">Apples</m3e-menu-trigger>
|
|
58
|
-
* </m3e-menu-item>
|
|
59
|
-
* </m3e-menu>
|
|
60
|
-
* <m3e-menu id="menu4">
|
|
61
|
-
* <m3e-menu-item>Fuji</m3e-menu-item>
|
|
62
|
-
* <m3e-menu-item>Granny Smith</m3e-menu-item>
|
|
63
|
-
* <m3e-menu-item>Red Delicious</m3e-menu-item>
|
|
64
|
-
* </m3e-menu>
|
|
65
|
-
* ```
|
|
66
|
-
*
|
|
67
|
-
* @tag m3e-menu
|
|
68
|
-
*
|
|
69
|
-
* @slot - Renders the contents of the menu.
|
|
70
|
-
*
|
|
71
|
-
* @attr position-x - The position of the menu, on the x-axis.
|
|
72
|
-
* @attr position-y - The position of the menu, on the y-axis.
|
|
73
|
-
*
|
|
74
|
-
* @fires beforetoggle - Dispatched before the toggle state changes.
|
|
75
|
-
* @fires toggle - Dispatched after the toggle state has changed.
|
|
76
|
-
*
|
|
77
|
-
* @cssprop --m3e-menu-container-shape - Controls the corner radius of the menu container.
|
|
78
|
-
* @cssprop --m3e-menu-container-min-width - Minimum width of the menu container.
|
|
79
|
-
* @cssprop --m3e-menu-container-max-width - Maximum width of the menu container.
|
|
80
|
-
* @cssprop --m3e-menu-container-max-height - Maximum height of the menu container.
|
|
81
|
-
* @cssprop --m3e-menu-container-padding-block - Vertical padding inside the menu container.
|
|
82
|
-
* @cssprop --m3e-menu-container-color - Background color of the menu container.
|
|
83
|
-
* @cssprop --m3e-menu-container-elevation - Box shadow elevation of the menu container.
|
|
84
|
-
* @cssprop --m3e-menu-divider-spacing - Vertical spacing around slotted `m3e-divider` elements.
|
|
85
|
-
*/
|
|
86
|
-
@customElement("m3e-menu")
|
|
87
|
-
export class M3eMenuElement extends Role(LitElement, "menu") {
|
|
88
|
-
/** The styles of the element. */
|
|
89
|
-
static override styles: CSSResultGroup = css`
|
|
90
|
-
:host {
|
|
91
|
-
position: absolute;
|
|
92
|
-
flex-direction: column;
|
|
93
|
-
padding: unset;
|
|
94
|
-
margin: unset;
|
|
95
|
-
border: unset;
|
|
96
|
-
overflow-y: auto;
|
|
97
|
-
scrollbar-width: ${DesignToken.scrollbar.thinWidth};
|
|
98
|
-
scrollbar-color: ${DesignToken.scrollbar.color};
|
|
99
|
-
border-radius: var(--m3e-menu-container-shape, ${DesignToken.shape.corner.extraSmall});
|
|
100
|
-
min-width: var(--m3e-menu-container-min-width, 7rem);
|
|
101
|
-
max-width: var(--m3e-menu-container-max-width, 17.5rem);
|
|
102
|
-
max-height: var(--m3e-menu-container-max-height, 17.5rem);
|
|
103
|
-
padding-block: var(--m3e-menu-container-padding-block, 0.5rem);
|
|
104
|
-
background-color: var(--m3e-menu-container-color, ${DesignToken.color.surfaceContainer});
|
|
105
|
-
box-shadow: var(--m3e-menu-container-elevation, ${DesignToken.elevation.level3});
|
|
106
|
-
opacity: 0;
|
|
107
|
-
display: none;
|
|
108
|
-
transition: ${unsafeCSS(
|
|
109
|
-
`opacity ${DesignToken.motion.duration.short2} ${DesignToken.motion.easing.standard},
|
|
110
|
-
transform ${DesignToken.motion.duration.short2} ${DesignToken.motion.easing.standard},
|
|
111
|
-
overlay ${DesignToken.motion.duration.short2} ${DesignToken.motion.easing.standard} allow-discrete,
|
|
112
|
-
display ${DesignToken.motion.duration.short2} ${DesignToken.motion.easing.standard} allow-discrete`
|
|
113
|
-
)};
|
|
114
|
-
}
|
|
115
|
-
:host(:not([submenu])) {
|
|
116
|
-
transform: scaleY(0.8);
|
|
117
|
-
}
|
|
118
|
-
:host(:not([submenu]):popover-open) {
|
|
119
|
-
transform: scaleY(1);
|
|
120
|
-
}
|
|
121
|
-
:host::backdrop {
|
|
122
|
-
background-color: transparent;
|
|
123
|
-
}
|
|
124
|
-
:host(:popover-open) {
|
|
125
|
-
display: inline-flex;
|
|
126
|
-
opacity: 1;
|
|
127
|
-
}
|
|
128
|
-
:host(.-bottom) {
|
|
129
|
-
transform-origin: top;
|
|
130
|
-
}
|
|
131
|
-
:host(.-top) {
|
|
132
|
-
transform-origin: bottom;
|
|
133
|
-
}
|
|
134
|
-
:host(.-shift-down) {
|
|
135
|
-
margin-top: calc(0px - var(--m3e-menu-container-padding-block, 0.5rem));
|
|
136
|
-
}
|
|
137
|
-
:host(.-shift-up) {
|
|
138
|
-
margin-top: var(--m3e-menu-container-padding-block, 0.5rem);
|
|
139
|
-
}
|
|
140
|
-
::slotted(m3e-divider) {
|
|
141
|
-
margin-block: var(--m3e-menu-divider-spacing, 0.5rem);
|
|
142
|
-
}
|
|
143
|
-
@starting-style {
|
|
144
|
-
:host(:popover-open) {
|
|
145
|
-
opacity: 0;
|
|
146
|
-
}
|
|
147
|
-
:host(:not([submenu]):popover-open) {
|
|
148
|
-
transform: scaleY(0.8);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
@media (prefers-reduced-motion) {
|
|
152
|
-
:host {
|
|
153
|
-
transition: none;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
@media (forced-colors: active) {
|
|
157
|
-
:host {
|
|
158
|
-
background-color: Menu;
|
|
159
|
-
color: MenuText;
|
|
160
|
-
border: 1px solid CanvasText;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
`;
|
|
164
|
-
|
|
165
|
-
/** @private */ #trigger?: HTMLElement;
|
|
166
|
-
/** @private */ #anchorCleanup?: () => void;
|
|
167
|
-
|
|
168
|
-
/** @private */ readonly #listManager = new RovingTabIndexManager<MenuItemElementBase>()
|
|
169
|
-
.withWrap()
|
|
170
|
-
.withHomeAndEnd()
|
|
171
|
-
.withVerticalOrientation();
|
|
172
|
-
|
|
173
|
-
/** @private */ readonly #keyDownHandler = (e: KeyboardEvent) => this.#handleKeyDown(e);
|
|
174
|
-
/** @private */ readonly #documentClickHandler = (e: MouseEvent) => this.#handleDocumentClick(e);
|
|
175
|
-
/** @private */ readonly #scrollController = new ScrollController(this, {
|
|
176
|
-
target: null,
|
|
177
|
-
callback: (target) =>
|
|
178
|
-
target instanceof M3eMenuElement
|
|
179
|
-
? target.items.filter((x) => x instanceof M3eMenuItemElement).forEach((x) => x.submenu?.hide())
|
|
180
|
-
: this.hideAll(),
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
/** @private */ readonly #toggleHandler = (e: ToggleEvent) => {
|
|
184
|
-
if (e.newState === "closed") {
|
|
185
|
-
this.#anchorCleanup?.();
|
|
186
|
-
this.#anchorCleanup = undefined;
|
|
187
|
-
} else {
|
|
188
|
-
setTimeout(() => {
|
|
189
|
-
this.#listManager.setActiveItem(this.#listManager.items.find((x) => !x.disabled));
|
|
190
|
-
}, 40);
|
|
191
|
-
}
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* The position of the menu, on the x-axis.
|
|
196
|
-
* @default "after"
|
|
197
|
-
*/
|
|
198
|
-
@property({ attribute: "position-x" }) positionX: MenuPositionX = "after";
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* The position of the menu, on the y-axis.
|
|
202
|
-
* @default "below"
|
|
203
|
-
*/
|
|
204
|
-
@property({ attribute: "position-y" }) positionY: MenuPositionY = "below";
|
|
205
|
-
|
|
206
|
-
/** The items of the menu. */
|
|
207
|
-
get items(): ReadonlyArray<MenuItemElementBase> {
|
|
208
|
-
return this.#listManager.items;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/** A value indicating whether the menu is open. */
|
|
212
|
-
get isOpen() {
|
|
213
|
-
return this.#trigger !== undefined;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/** A value indicating whether the menu is a submenu. */
|
|
217
|
-
@property({ type: Boolean, reflect: true }) submenu = false;
|
|
218
|
-
|
|
219
|
-
/** @inheritdoc */
|
|
220
|
-
override connectedCallback(): void {
|
|
221
|
-
super.connectedCallback();
|
|
222
|
-
|
|
223
|
-
this.tabIndex = -1;
|
|
224
|
-
this.setAttribute("popover", "manual");
|
|
225
|
-
this.addEventListener("keydown", this.#keyDownHandler);
|
|
226
|
-
this.addEventListener("toggle", this.#toggleHandler);
|
|
227
|
-
document.addEventListener("click", this.#documentClickHandler);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/** @inheritdoc */
|
|
231
|
-
override disconnectedCallback(): void {
|
|
232
|
-
super.disconnectedCallback();
|
|
233
|
-
|
|
234
|
-
this.removeEventListener("keydown", this.#keyDownHandler);
|
|
235
|
-
this.removeEventListener("toggle", this.#toggleHandler);
|
|
236
|
-
document.removeEventListener("click", this.#documentClickHandler);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Opens the menu.
|
|
241
|
-
* @param {HTMLElement} trigger The element that triggered the menu.
|
|
242
|
-
* @returns {Promise<void>} A `Promise` that resolves when the menu is opened.
|
|
243
|
-
*/
|
|
244
|
-
async show(trigger: HTMLElement): Promise<void> {
|
|
245
|
-
if (this.#trigger && this.#trigger !== trigger) {
|
|
246
|
-
this.hide();
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
this.#anchorCleanup = await positionAnchor(
|
|
250
|
-
this,
|
|
251
|
-
trigger,
|
|
252
|
-
{
|
|
253
|
-
position: this.submenu
|
|
254
|
-
? this.positionX === "before"
|
|
255
|
-
? "left-start"
|
|
256
|
-
: "right-start"
|
|
257
|
-
: this.positionY === "above"
|
|
258
|
-
? this.positionX === "before"
|
|
259
|
-
? "top-end"
|
|
260
|
-
: "top-start"
|
|
261
|
-
: this.positionX === "before"
|
|
262
|
-
? "bottom-end"
|
|
263
|
-
: "bottom-start",
|
|
264
|
-
inline: true,
|
|
265
|
-
flip: true,
|
|
266
|
-
shift: true,
|
|
267
|
-
offset: !this.submenu ? 4 : undefined,
|
|
268
|
-
},
|
|
269
|
-
(x, y, position) => {
|
|
270
|
-
if (!this.submenu) {
|
|
271
|
-
this.classList.toggle("-top", position.includes("top"));
|
|
272
|
-
this.classList.toggle("-bottom", position.includes("bottom"));
|
|
273
|
-
} else if (this.#trigger) {
|
|
274
|
-
const top = this.#getAbsolutePosition(this.#trigger).y;
|
|
275
|
-
this.classList.toggle("-shift-down", false);
|
|
276
|
-
this.classList.toggle("-shift-up", false);
|
|
277
|
-
this.classList.toggle(Math.round(y) === Math.round(top) ? "-shift-down" : "-shift-up", true);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
this.style.left = `${x}px`;
|
|
281
|
-
this.style.top = `${y}px`;
|
|
282
|
-
}
|
|
283
|
-
);
|
|
284
|
-
|
|
285
|
-
this.showPopover();
|
|
286
|
-
|
|
287
|
-
this.#trigger = trigger;
|
|
288
|
-
this.#trigger.ariaExpanded = "true";
|
|
289
|
-
this.#scrollController.observe(this.#trigger);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
/**
|
|
293
|
-
* Hides the menu.
|
|
294
|
-
* @param {boolean} [restoreFocus=false] A value indicating whether to restore focus to the menu's trigger.
|
|
295
|
-
*/
|
|
296
|
-
hide(restoreFocus: boolean = false): void {
|
|
297
|
-
for (const item of this.#listManager.items) {
|
|
298
|
-
const submenu = (<M3eMenuItemElement>item).submenu;
|
|
299
|
-
if (submenu && submenu.isOpen) {
|
|
300
|
-
submenu.hide();
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
this.hidePopover();
|
|
305
|
-
|
|
306
|
-
if (this.#trigger) {
|
|
307
|
-
this.#trigger.ariaExpanded = "false";
|
|
308
|
-
if (restoreFocus) {
|
|
309
|
-
this.#trigger.focus();
|
|
310
|
-
}
|
|
311
|
-
this.#scrollController.unobserve(this.#trigger);
|
|
312
|
-
this.#trigger = undefined;
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
/**
|
|
317
|
-
* Closes this menu and any parenting menus.
|
|
318
|
-
* @param {boolean} [restoreFocus=false] A value indicating whether to restore focus to the menu's trigger.
|
|
319
|
-
*/
|
|
320
|
-
hideAll(restoreFocus: boolean = false): void {
|
|
321
|
-
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
322
|
-
let menu: M3eMenuElement = this;
|
|
323
|
-
while (menu.#trigger) {
|
|
324
|
-
const parent = menu.#trigger.closest("m3e-menu");
|
|
325
|
-
if (!parent) {
|
|
326
|
-
break;
|
|
327
|
-
}
|
|
328
|
-
menu = parent;
|
|
329
|
-
}
|
|
330
|
-
menu.hide(restoreFocus);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
/**
|
|
334
|
-
* Toggles the menu.
|
|
335
|
-
* @param {HTMLElement} trigger The element that triggered the menu.
|
|
336
|
-
* @returns {Promise<void>} A `Promise` that resolves when the menu is opened or closed.
|
|
337
|
-
*/
|
|
338
|
-
async toggle(trigger: HTMLElement): Promise<void> {
|
|
339
|
-
if (this.#trigger) {
|
|
340
|
-
this.hide();
|
|
341
|
-
} else {
|
|
342
|
-
await this.show(trigger);
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
/** @inheritdoc */
|
|
347
|
-
protected override render(): unknown {
|
|
348
|
-
return html`<slot @slotchange="${this.#handleSlotChange}"></slot>`;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
/** @private */
|
|
352
|
-
#handleSlotChange(): void {
|
|
353
|
-
const { added } = this.#listManager.setItems(
|
|
354
|
-
[
|
|
355
|
-
...this.querySelectorAll<MenuItemElementBase>("m3e-menu-item,m3e-menu-item-checkbox,m3e-menu-item-radio"),
|
|
356
|
-
].filter((x) => x.closest("m3e-menu") === this)
|
|
357
|
-
);
|
|
358
|
-
|
|
359
|
-
if (!this.#listManager.activeItem) {
|
|
360
|
-
this.#listManager.updateActiveItem(added.find((x) => !x.disabled));
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
/** @private */
|
|
365
|
-
#handleKeyDown(e: KeyboardEvent): void {
|
|
366
|
-
switch (e.key) {
|
|
367
|
-
case "Left":
|
|
368
|
-
case "ArrowLeft":
|
|
369
|
-
e.preventDefault();
|
|
370
|
-
this.hide(true);
|
|
371
|
-
break;
|
|
372
|
-
|
|
373
|
-
case "Tab":
|
|
374
|
-
this.hideAll();
|
|
375
|
-
break;
|
|
376
|
-
|
|
377
|
-
case "Escape":
|
|
378
|
-
if (!e.shiftKey && !e.ctrlKey) {
|
|
379
|
-
this.hide(true);
|
|
380
|
-
}
|
|
381
|
-
break;
|
|
382
|
-
|
|
383
|
-
default:
|
|
384
|
-
this.#listManager.onKeyDown(e);
|
|
385
|
-
break;
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
/** @private */
|
|
390
|
-
#handleDocumentClick(e: MouseEvent): void {
|
|
391
|
-
if (!this.submenu && !e.composedPath().some((x) => x instanceof M3eMenuElement || x === this.#trigger)) {
|
|
392
|
-
this.hide();
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
/** @private */
|
|
397
|
-
#getAbsolutePosition(element: HTMLElement): { x: number; y: number } {
|
|
398
|
-
let x = 0,
|
|
399
|
-
y = 0;
|
|
400
|
-
|
|
401
|
-
for (
|
|
402
|
-
let current: HTMLElement | null = element;
|
|
403
|
-
current;
|
|
404
|
-
current = current.offsetParent instanceof HTMLElement ? current.offsetParent : null
|
|
405
|
-
) {
|
|
406
|
-
x += current.offsetLeft - current.scrollLeft + current.clientLeft;
|
|
407
|
-
y += current.offsetTop - current.scrollTop + current.clientTop;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
return { x, y };
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
interface M3eMenuElementEventMap extends HTMLElementEventMap {
|
|
415
|
-
beforetoggle: ToggleEvent;
|
|
416
|
-
toggle: ToggleEvent;
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
export interface M3eMenuElement {
|
|
420
|
-
addEventListener<K extends keyof M3eMenuElementEventMap>(
|
|
421
|
-
type: K,
|
|
422
|
-
listener: (this: M3eMenuElement, ev: M3eMenuElementEventMap[K]) => void,
|
|
423
|
-
options?: boolean | AddEventListenerOptions
|
|
424
|
-
): void;
|
|
425
|
-
|
|
426
|
-
addEventListener(
|
|
427
|
-
type: string,
|
|
428
|
-
listener: EventListenerOrEventListenerObject,
|
|
429
|
-
options?: boolean | AddEventListenerOptions
|
|
430
|
-
): void;
|
|
431
|
-
|
|
432
|
-
removeEventListener<K extends keyof M3eMenuElementEventMap>(
|
|
433
|
-
type: K,
|
|
434
|
-
listener: (this: M3eMenuElement, ev: M3eMenuElementEventMap[K]) => void,
|
|
435
|
-
options?: boolean | EventListenerOptions
|
|
436
|
-
): void;
|
|
437
|
-
|
|
438
|
-
removeEventListener(
|
|
439
|
-
type: string,
|
|
440
|
-
listener: EventListenerOrEventListenerObject,
|
|
441
|
-
options?: boolean | EventListenerOptions
|
|
442
|
-
): void;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
declare global {
|
|
446
|
-
interface HTMLElementTagNameMap {
|
|
447
|
-
"m3e-menu": M3eMenuElement;
|
|
448
|
-
}
|
|
449
|
-
}
|
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
import { css, CSSResultGroup, html } from "lit";
|
|
2
|
-
import { customElement } from "lit/decorators.js";
|
|
3
|
-
|
|
4
|
-
import { Checked, hasAssignedNodes, Role } from "@m3e/core";
|
|
5
|
-
|
|
6
|
-
import { M3eMenuItemElement } from "./MenuItemElement";
|
|
7
|
-
import { MenuItemElementBase } from "./MenuItemElementBase";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* @summary
|
|
11
|
-
* An item of a menu which supports a checkable state.
|
|
12
|
-
*
|
|
13
|
-
* @description
|
|
14
|
-
* The `m3e-menu-item-checkbox` component represents a menu item that supports an independent checkable state.
|
|
15
|
-
* It allows users to toggle options on or off without affecting other items in the menu, making it ideal for
|
|
16
|
-
* multi-select scenarios such as filters, visibility toggles, or feature flags. This component encodes a persistent
|
|
17
|
-
* selection contract and can coexist with other checkbox or radio items within the same menu.
|
|
18
|
-
*
|
|
19
|
-
* @example
|
|
20
|
-
* The following example illustrates use of the `m3e-menu-item-checkbox` to present multiple independent checkable
|
|
21
|
-
* items in a menu.
|
|
22
|
-
* ```html
|
|
23
|
-
* <m3e-button>
|
|
24
|
-
* <m3e-menu-trigger for="menu">Format</m3e-menu-trigger>
|
|
25
|
-
* </m3e-button>
|
|
26
|
-
* <m3e-menu id="menu">
|
|
27
|
-
* <m3e-menu-item-checkbox>Bold</m3e-menu-item-checkbox>
|
|
28
|
-
* <m3e-menu-item-checkbox>Italic</m3e-menu-item-checkbox>
|
|
29
|
-
* <m3e-menu-item-checkbox>Underline</m3e-menu-item-checkbox>
|
|
30
|
-
* </m3e-menu>
|
|
31
|
-
* ```
|
|
32
|
-
*
|
|
33
|
-
* @tag m3e-menu-item-checkbox
|
|
34
|
-
*
|
|
35
|
-
* @slot - Renders the label of the item.
|
|
36
|
-
* @slot icon - Renders an icon before the items's label.
|
|
37
|
-
* @slot trailing-icon - Renders an icon after the item's label.
|
|
38
|
-
*
|
|
39
|
-
* @attr disabled - Whether the element is disabled.
|
|
40
|
-
* @attr checked - Whether the element is checked.
|
|
41
|
-
*
|
|
42
|
-
* @cssprop --m3e-menu-item-container-height - Height of the menu item container.
|
|
43
|
-
* @cssprop --m3e-menu-item-color - Text color for unselected, enabled menu items.
|
|
44
|
-
* @cssprop --m3e-menu-item-container-hover-color - State layer hover color for unselected items.
|
|
45
|
-
* @cssprop --m3e-menu-item-container-focus-color - State layer focus color for unselected items.
|
|
46
|
-
* @cssprop --m3e-menu-item-ripple-color - Ripple color for unselected items.
|
|
47
|
-
* @cssprop --m3e-menu-selected-color - Text color for selected or expanded items.
|
|
48
|
-
* @cssprop --m3e-menu-selected-container-color - Background color for selected or expanded items.
|
|
49
|
-
* @cssprop --m3e-menu-item-selected-container-hover-color - State layer hover color for selected items.
|
|
50
|
-
* @cssprop --m3e-menu-item-selected-container-focus-color - State layer focus color for selected items.
|
|
51
|
-
* @cssprop --m3e-menu-item-selected-ripple-color - Ripple color for selected items.
|
|
52
|
-
* @cssprop --m3e-menu-item-disabled-color - Base color for disabled items.
|
|
53
|
-
* @cssprop --m3e-menu-item-disabled-opacity - Opacity percentage for disabled item color mix.
|
|
54
|
-
* @cssprop --m3e-menu-item-icon-label-space - Horizontal gap between icon and content.
|
|
55
|
-
* @cssprop --m3e-menu-item-padding-start - Start padding for the item wrapper.
|
|
56
|
-
* @cssprop --m3e-menu-item-padding-end - End padding for the item wrapper.
|
|
57
|
-
* @cssprop --m3e-menu-item-label-text-font-size - Font size for menu item text.
|
|
58
|
-
* @cssprop --m3e-menu-item-label-text-font-weight - Font weight for menu item text.
|
|
59
|
-
* @cssprop --m3e-menu-item-label-text-line-height - Line height for menu item text.
|
|
60
|
-
* @cssprop --m3e-menu-item-label-text-tracking - Letter spacing for menu item text.
|
|
61
|
-
* @cssprop --m3e-menu-item-focus-ring-shape - Border radius for the focus ring.
|
|
62
|
-
* @cssprop --m3e-menu-item-icon-size - Font size for leading and trailing icons.
|
|
63
|
-
*/
|
|
64
|
-
@customElement("m3e-menu-item-checkbox")
|
|
65
|
-
export class M3eMenuItemCheckboxElement extends Checked(Role(MenuItemElementBase, "menuitemcheckbox")) {
|
|
66
|
-
/** The styles of the element. */
|
|
67
|
-
static override styles: CSSResultGroup = [
|
|
68
|
-
MenuItemElementBase.styles,
|
|
69
|
-
css`
|
|
70
|
-
.icon {
|
|
71
|
-
display: flex;
|
|
72
|
-
align-items: center;
|
|
73
|
-
justify-content: center;
|
|
74
|
-
}
|
|
75
|
-
:host(:not(.-with-icon)) .icon {
|
|
76
|
-
margin-inline-start: calc(0px - var(--m3e-menu-item-icon-label-space, 0.75rem));
|
|
77
|
-
}
|
|
78
|
-
.check {
|
|
79
|
-
width: 1em;
|
|
80
|
-
font-size: var(--m3e-menu-item-icon-size, 1.5rem) !important;
|
|
81
|
-
}
|
|
82
|
-
:host(:not([checked])) .check {
|
|
83
|
-
display: none;
|
|
84
|
-
}
|
|
85
|
-
:host([checked]) .icon {
|
|
86
|
-
margin-inline-start: 0;
|
|
87
|
-
}
|
|
88
|
-
:host([checked]) ::slotted([slot="icon"]) {
|
|
89
|
-
display: none !important;
|
|
90
|
-
}
|
|
91
|
-
`,
|
|
92
|
-
];
|
|
93
|
-
|
|
94
|
-
/** @internal */ readonly #clickHandler = (e: Event) => this.#handleClick(e);
|
|
95
|
-
/** @internal */ readonly #keyDownHandler = (e: KeyboardEvent) => this.#handleKeyDown(e);
|
|
96
|
-
/** @internal */ readonly #keyUpHandler = () => this.#handleKeyUp();
|
|
97
|
-
/** @internal */ readonly #mouseEnterHandler = () => this.#handleMouseEnter();
|
|
98
|
-
/** @internal */ #spacePressed = false;
|
|
99
|
-
|
|
100
|
-
/** @inheritdoc */
|
|
101
|
-
override connectedCallback(): void {
|
|
102
|
-
super.connectedCallback();
|
|
103
|
-
|
|
104
|
-
this.addEventListener("click", this.#clickHandler);
|
|
105
|
-
this.addEventListener("keydown", this.#keyDownHandler);
|
|
106
|
-
this.addEventListener("keyup", this.#keyUpHandler);
|
|
107
|
-
this.addEventListener("mouseenter", this.#mouseEnterHandler);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/** @inheritdoc */
|
|
111
|
-
override disconnectedCallback(): void {
|
|
112
|
-
super.disconnectedCallback();
|
|
113
|
-
|
|
114
|
-
this.removeEventListener("click", this.#clickHandler);
|
|
115
|
-
this.removeEventListener("keydown", this.#keyDownHandler);
|
|
116
|
-
this.removeEventListener("keyup", this.#keyUpHandler);
|
|
117
|
-
this.removeEventListener("mouseenter", this.#mouseEnterHandler);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/** @internal @inheritdoc */
|
|
121
|
-
protected override _renderContent(): unknown {
|
|
122
|
-
return html` <div class="icon">
|
|
123
|
-
<svg class="check" viewBox="0 -960 960 960" aria-hidden="true">
|
|
124
|
-
<path fill="currentColor" d="M382-240 154-468l57-57 171 171 367-367 57 57-424 424Z" />
|
|
125
|
-
</svg>
|
|
126
|
-
<slot name="icon" @slotchange="${this.#handleIconSlotChange}"></slot>
|
|
127
|
-
</div>
|
|
128
|
-
<slot></slot>
|
|
129
|
-
<slot name="trailing-icon" aria-hidden="true" @slotchange="${this.#handleTrailingIconSlotChange}"></slot>`;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/** @internal */
|
|
133
|
-
#handleIconSlotChange(e: Event): void {
|
|
134
|
-
this.classList.toggle("-with-icon", hasAssignedNodes(<HTMLSlotElement>e.target));
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/** @internal */
|
|
138
|
-
#handleTrailingIconSlotChange(e: Event): void {
|
|
139
|
-
this.classList.toggle("-with-trailing-icon", hasAssignedNodes(<HTMLSlotElement>e.target));
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/** @internal */
|
|
143
|
-
#handleClick(e: Event): void {
|
|
144
|
-
if (!e.defaultPrevented) {
|
|
145
|
-
this.checked = !this.checked;
|
|
146
|
-
this.performUpdate();
|
|
147
|
-
|
|
148
|
-
if (!this.#spacePressed) {
|
|
149
|
-
this.menu?.hideAll(true);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/** @internal */
|
|
155
|
-
#handleKeyDown(e: KeyboardEvent): void {
|
|
156
|
-
this.#spacePressed = e.key === " ";
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/** @internal */
|
|
160
|
-
#handleKeyUp(): void {
|
|
161
|
-
this.#spacePressed = false;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/** @internal */
|
|
165
|
-
#handleMouseEnter(): void {
|
|
166
|
-
this.menu?.items.forEach((item) => {
|
|
167
|
-
if (item instanceof M3eMenuItemElement && item.submenu?.isOpen) {
|
|
168
|
-
item.submenu.hide();
|
|
169
|
-
}
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
declare global {
|
|
175
|
-
interface HTMLElementTagNameMap {
|
|
176
|
-
"m3e-menu-item-checkbox": M3eMenuItemCheckboxElement;
|
|
177
|
-
}
|
|
178
|
-
}
|