@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.
@@ -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
- }