@m3e/fab-menu 1.0.0-rc.1

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.
@@ -0,0 +1,391 @@
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, DisabledMixin, Role, ScrollController } from "@m3e/core";
6
+ import { RovingTabIndexManager } from "@m3e/core/a11y";
7
+ import { M3eDirectionality } from "@m3e/core/bidi";
8
+ import { positionAnchor } from "@m3e/core/anchoring";
9
+
10
+ import { M3eFabElement } from "@m3e/fab";
11
+
12
+ import { FabMenuVariant } from "./FabMenuVariant";
13
+ import { M3eFabMenuItemElement } from "./FabMenuItemElement";
14
+
15
+ /**
16
+ * @summary
17
+ * A menu, opened from a floating action button (FAB), used to display multiple related actions.
18
+ *
19
+ * @description
20
+ * The `m3e-fab-menu` component presents a dynamic menu of related actions, elegantly revealed from a
21
+ * floating action button (FAB). Designed using expressive, adaptive surfaces, it enables seamless access
22
+ * to contextual actions in modern, visually rich interfaces.
23
+ *
24
+ * @example
25
+ * The following example illustrates triggering a `m3e-fab-menu` from an `m3e-fab` using a `m3e-fab-menu-trigger`.
26
+ * ```html
27
+ * <m3e-fab variant="primary" size="large">
28
+ * <m3e-fab-menu-trigger for="fabmenu">
29
+ * <m3e-icon name="edit"></m3e-icon>
30
+ * </m3e-fab-menu-trigger>
31
+ * </m3e-fab>
32
+ * <m3e-fab-menu id="fabmenu" variant="secondary">
33
+ * <m3e-fab-menu-item>First</m3e-fab-menu-item>
34
+ * <m3e-fab-menu-item>Second</m3e-fab-menu-item>
35
+ * <m3e-fab-menu-item>Third</m3e-fab-menu-item>
36
+ * <m3e-fab-menu-item>Forth</m3e-fab-menu-item>
37
+ * <m3e-fab-menu-item>Fifth</m3e-fab-menu-item>
38
+ * <m3e-fab-menu-item>Sixth</m3e-fab-menu-item>
39
+ * </m3e-fab-menu>
40
+ * ```
41
+ *
42
+ * @tag m3e-fab-menu
43
+ *
44
+ * @slot - Renders the contents of the menu.
45
+ *
46
+ * @attr variant - The appearance variant of the menu.
47
+ *
48
+ * @cssprop --m3e-fab-menu-spacing - Vertical gap between menu items.
49
+ * @cssprop --m3e-fab-menu-max-width - Maximum width of the menu.
50
+ * @cssprop --m3e-primary-fab-color - Foreground color for primary variant items.
51
+ * @cssprop --m3e-primary-fab-container-color - Container color for primary variant items.
52
+ * @cssprop --m3e-primary-fab-hover-color - Hover background color for primary variant items.
53
+ * @cssprop --m3e-primary-fab-focus-color - Focus background color for primary variant items.
54
+ * @cssprop --m3e-primary-fab-ripple-color - Ripple color for primary variant items.
55
+ * @cssprop --m3e-secondary-fab-color - Foreground color for secondary variant items.
56
+ * @cssprop --m3e-secondary-fab-container-color - Container color for secondary variant items.
57
+ * @cssprop --m3e-secondary-fab-hover-color - Hover background color for secondary variant items.
58
+ * @cssprop --m3e-secondary-fab-focus-color - Focus background color for secondary variant items.
59
+ * @cssprop --m3e-secondary-fab-ripple-color - Ripple color for secondary variant items.
60
+ * @cssprop --m3e-tertiary-fab-color - Foreground color for tertiary variant items.
61
+ * @cssprop --m3e-tertiary-fab-container-color - Container color for tertiary variant items.
62
+ * @cssprop --m3e-tertiary-fab-hover-color - Hover background color for tertiary variant items.
63
+ * @cssprop --m3e-tertiary-fab-focus-color - Focus background color for tertiary variant items.
64
+ * @cssprop --m3e-tertiary-fab-ripple-color - Ripple color for tertiary variant items.
65
+ */
66
+ @customElement("m3e-fab-menu")
67
+ export class M3eFabMenuElement extends Role(LitElement, "menu") {
68
+ /** The styles of the element. */
69
+ static override styles: CSSResultGroup = css`
70
+ :host {
71
+ position: absolute;
72
+ flex-direction: column;
73
+ row-gap: var(--m3e-fab-menu-spacing, 0.25rem);
74
+ padding: unset;
75
+ margin: unset;
76
+ border: unset;
77
+ overflow: visible;
78
+ max-width: var(--m3e-fab-menu-max-width, 17.5rem);
79
+ opacity: 0;
80
+ background-color: transparent;
81
+ display: none;
82
+ transition: ${unsafeCSS(
83
+ `opacity ${DesignToken.motion.spring.fastEffects},
84
+ transform ${DesignToken.motion.spring.fastSpatial},
85
+ overlay ${DesignToken.motion.spring.fastEffects} allow-discrete,
86
+ display ${DesignToken.motion.spring.fastEffects} allow-discrete`
87
+ )};
88
+ }
89
+ .base {
90
+ display: contents;
91
+ }
92
+ :host([variant="primary"]) .base {
93
+ --_fab-menu-item-color: var(--m3e-primary-fab-color, ${DesignToken.color.onPrimaryContainer});
94
+ --_fab-menu-item-container-color: var(--m3e-primary-fab-container-color, ${DesignToken.color.primaryContainer});
95
+ --_fab-menu-background-hover-color: var(--m3e-primary-fab-hover-color, ${DesignToken.color.onPrimaryContainer});
96
+ --_fab-menu-background-focus-color: var(--m3e-primary-fab-focus-color, ${DesignToken.color.onPrimaryContainer});
97
+ --_fab-menu-ripple-color: var(--m3e-primary-fab-ripple-color, ${DesignToken.color.onPrimaryContainer});
98
+ }
99
+ :host([variant="secondary"]) .base {
100
+ --_fab-menu-item-color: var(--m3e-secondary-fab-color, ${DesignToken.color.onSecondaryContainer});
101
+ --_fab-menu-item-container-color: var(
102
+ --m3e-secondary-fab-container-color,
103
+ ${DesignToken.color.secondaryContainer}
104
+ );
105
+ --_fab-menu-background-hover-color: var(
106
+ --m3e-secondary-fab-hover-color,
107
+ ${DesignToken.color.onSecondaryContainer}
108
+ );
109
+ --_fab-menu-background-focus-color: var(
110
+ --m3e-secondary-fab-focus-color,
111
+ ${DesignToken.color.onSecondaryContainer}
112
+ );
113
+ --_fab-menu-ripple-color: var(--m3e-secondary-fab-ripple-color, ${DesignToken.color.onSecondaryContainer});
114
+ }
115
+ :host([variant="tertiary"]) .base {
116
+ --_fab-menu-item-color: var(--m3e-tertiary-fab-color, ${DesignToken.color.onTertiaryContainer});
117
+ --_fab-menu-item-container-color: var(--m3e-tertiary-fab-container-color, ${DesignToken.color.tertiaryContainer});
118
+ --_fab-menu-background-hover-color: var(--m3e-tertiary-fab-hover-color, ${DesignToken.color.onTertiaryContainer});
119
+ --_fab-menu-background-focus-color: var(--m3e-tertiary-fab-focus-color, ${DesignToken.color.onTertiaryContainer});
120
+ --_fab-menu-ripple-color: var(--m3e-tertiary-fab-ripple-color, ${DesignToken.color.onTertiaryContainer});
121
+ }
122
+ :host {
123
+ transform: scaleX(0.8);
124
+ }
125
+ :host(.-left) {
126
+ align-items: flex-start;
127
+ transform-origin: left;
128
+ }
129
+ :host(.-right) {
130
+ align-items: flex-end;
131
+ transform-origin: right;
132
+ }
133
+ :host(:popover-open) {
134
+ transform: scaleX(1);
135
+ display: inline-flex;
136
+ opacity: 1;
137
+ }
138
+ :host::backdrop {
139
+ background-color: transparent;
140
+ }
141
+ @starting-style {
142
+ :host(:popover-open) {
143
+ opacity: 0;
144
+ }
145
+ :host(:popover-open) {
146
+ transform: scaleX(0.8);
147
+ }
148
+ }
149
+ @media (prefers-reduced-motion) {
150
+ :host {
151
+ transition: none;
152
+ }
153
+ }
154
+ @media (forced-colors: active) {
155
+ :host {
156
+ border-radius: ${DesignToken.shape.corner.medium};
157
+ border: 1px solid MenuText;
158
+ background-color: Menu;
159
+ }
160
+ }
161
+ `;
162
+
163
+ /** @private */ #fabTabIndex?: number;
164
+ /** @private */ #trigger?: HTMLElement;
165
+ /** @private */ #anchoringCleanup?: () => void;
166
+
167
+ /** @private */
168
+ readonly #listManager = new RovingTabIndexManager<LitElement & DisabledMixin>()
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
+
176
+ /** @private */
177
+ readonly #scrollController = new ScrollController(this, { target: null, callback: () => this.hide() });
178
+
179
+ /** @private */
180
+ readonly #toggleHandler = (e: ToggleEvent) => {
181
+ if (e.newState === "closed") {
182
+ this.#anchoringCleanup?.();
183
+ this.#anchoringCleanup = undefined;
184
+ } else {
185
+ setTimeout(() => {
186
+ this.#listManager.setActiveItem(this.#listManager.items.find((x) => !x.disabled));
187
+ }, 40);
188
+ }
189
+ };
190
+
191
+ /**
192
+ * The appearance variant of the menu.
193
+ * @default "primary"
194
+ */
195
+ @property({ reflect: true }) variant: FabMenuVariant = "primary";
196
+
197
+ /** Whether the menu is open. */
198
+ get isOpen() {
199
+ return this.#trigger !== undefined;
200
+ }
201
+
202
+ /**
203
+ * Opens the menu.
204
+ * @param {HTMLElement} trigger The element that triggered the menu.
205
+ * @returns {Promise<void>} A `Promise` that resolves when the menu is opened.
206
+ */
207
+ async show(trigger: HTMLElement): Promise<void> {
208
+ if (this.#trigger && this.#trigger !== trigger) {
209
+ this.hide();
210
+ }
211
+
212
+ this.#anchoringCleanup?.();
213
+ this.#anchoringCleanup = await positionAnchor(
214
+ this,
215
+ trigger,
216
+ {
217
+ position: M3eDirectionality.current === "ltr" ? "top-end" : "top-start",
218
+ inline: true,
219
+ shift: true,
220
+ flip: true,
221
+ offset: 8,
222
+ },
223
+ (x, y, position) => {
224
+ this.classList.toggle("-right", position.includes("end"));
225
+ this.classList.toggle("-left", position.includes("start"));
226
+ this.style.left = `${x}px`;
227
+ this.style.top = `${y}px`;
228
+ }
229
+ );
230
+
231
+ this.showPopover();
232
+
233
+ this.#trigger = trigger;
234
+ this.#trigger.ariaExpanded = "true";
235
+ this.#scrollController.observe(this.#trigger);
236
+
237
+ this.#attachFab();
238
+ }
239
+
240
+ /**
241
+ * Hides the menu.
242
+ * @param {boolean} [restoreFocus=false] A value indicating whether to restore focus to the menu's trigger.
243
+ */
244
+ hide(restoreFocus: boolean = false): void {
245
+ this.hidePopover();
246
+
247
+ if (this.#trigger) {
248
+ this.#trigger.ariaExpanded = "false";
249
+ if (restoreFocus) {
250
+ this.#trigger.focus();
251
+ }
252
+
253
+ this.#detachFab();
254
+
255
+ this.#scrollController.unobserve(this.#trigger);
256
+ this.#trigger = undefined;
257
+ }
258
+ }
259
+
260
+ /**
261
+ * Toggles the menu.
262
+ * @param {HTMLElement} trigger The element that triggered the menu.
263
+ * @returns {Promise<void>} A `Promise` that resolves when the menu is opened or closed.
264
+ */
265
+ async toggle(trigger: HTMLElement): Promise<void> {
266
+ if (this.#trigger) {
267
+ this.hide();
268
+ } else {
269
+ await this.show(trigger);
270
+ }
271
+ }
272
+
273
+ /** @inheritdoc */
274
+ override connectedCallback(): void {
275
+ super.connectedCallback();
276
+
277
+ this.tabIndex = -1;
278
+ this.setAttribute("popover", "manual");
279
+
280
+ this.addEventListener("keydown", this.#keyDownHandler);
281
+ this.addEventListener("toggle", this.#toggleHandler);
282
+ document.addEventListener("click", this.#documentClickHandler);
283
+ }
284
+
285
+ /** @inheritdoc */
286
+ override disconnectedCallback(): void {
287
+ super.disconnectedCallback();
288
+
289
+ this.removeEventListener("keydown", this.#keyDownHandler);
290
+ this.removeEventListener("toggle", this.#toggleHandler);
291
+ document.removeEventListener("click", this.#documentClickHandler);
292
+ }
293
+
294
+ /** @inheritdoc */
295
+ protected override render(): unknown {
296
+ return html`<div class="base"><slot @slotchange="${this.#handleSlotChange}"></slot></div>`;
297
+ }
298
+
299
+ /** @private */
300
+ #handleSlotChange(): void {
301
+ const { added } = this.#listManager.setItems([...this.querySelectorAll("m3e-fab-menu-item")]);
302
+ if (!this.#listManager.activeItem) {
303
+ this.#listManager.updateActiveItem(added.find((x) => !x.disabled));
304
+ }
305
+ }
306
+
307
+ /** @private */
308
+ #handleKeyDown(e: KeyboardEvent): void {
309
+ switch (e.key) {
310
+ case "Tab":
311
+ this.hide();
312
+ break;
313
+
314
+ case "Escape":
315
+ if (!e.shiftKey && !e.ctrlKey) {
316
+ this.hide(true);
317
+ }
318
+ break;
319
+
320
+ default:
321
+ this.#listManager.onKeyDown(e);
322
+ break;
323
+ }
324
+ }
325
+
326
+ /** @private */
327
+ #handleDocumentClick(e: MouseEvent): void {
328
+ if (!e.composedPath().some((x) => x instanceof M3eFabMenuItemElement || x === this.#trigger)) {
329
+ this.hide();
330
+ }
331
+ }
332
+
333
+ /** @private */
334
+ #attachFab(): void {
335
+ const fab = this.#trigger?.closest<M3eFabElement>("m3e-fab");
336
+ if (fab) {
337
+ this.#fabTabIndex = fab.tabIndex;
338
+ fab.addEventListener("keydown", this.#keyDownHandler);
339
+ this.#listManager.setItems([...this.#listManager.items, fab]);
340
+ }
341
+ }
342
+
343
+ /** @private */
344
+ #detachFab(): void {
345
+ const fab = this.#trigger?.closest<M3eFabElement>("m3e-fab");
346
+ if (fab) {
347
+ if (this.#fabTabIndex !== undefined) {
348
+ fab.tabIndex = this.#fabTabIndex;
349
+ }
350
+ fab.removeEventListener("keydown", this.#keyDownHandler);
351
+ this.#listManager.setItems([...this.#listManager.items.filter((x) => x !== fab)]);
352
+ }
353
+ }
354
+ }
355
+
356
+ interface M3eFabMenuElementEventMap extends HTMLElementEventMap {
357
+ beforetoggle: ToggleEvent;
358
+ toggle: ToggleEvent;
359
+ }
360
+
361
+ export interface M3eFabMenuElement {
362
+ addEventListener<K extends keyof M3eFabMenuElementEventMap>(
363
+ type: K,
364
+ listener: (this: M3eFabMenuElement, ev: M3eFabMenuElementEventMap[K]) => void,
365
+ options?: boolean | AddEventListenerOptions
366
+ ): void;
367
+
368
+ addEventListener(
369
+ type: string,
370
+ listener: EventListenerOrEventListenerObject,
371
+ options?: boolean | AddEventListenerOptions
372
+ ): void;
373
+
374
+ removeEventListener<K extends keyof M3eFabMenuElementEventMap>(
375
+ type: K,
376
+ listener: (this: M3eFabMenuElement, ev: M3eFabMenuElementEventMap[K]) => void,
377
+ options?: boolean | EventListenerOptions
378
+ ): void;
379
+
380
+ removeEventListener(
381
+ type: string,
382
+ listener: EventListenerOrEventListenerObject,
383
+ options?: boolean | EventListenerOptions
384
+ ): void;
385
+ }
386
+
387
+ declare global {
388
+ interface HTMLElementTagNameMap {
389
+ "m3e-fab-menu": M3eFabMenuElement;
390
+ }
391
+ }
@@ -0,0 +1,253 @@
1
+ import { css, CSSResultGroup, html, LitElement, PropertyValues, unsafeCSS } from "lit";
2
+ import { customElement, query } from "lit/decorators.js";
3
+
4
+ import {
5
+ AttachInternals,
6
+ DesignToken,
7
+ Disabled,
8
+ KeyboardClick,
9
+ LinkButton,
10
+ M3eElevationElement,
11
+ M3eFocusRingElement,
12
+ M3eRippleElement,
13
+ M3eStateLayerElement,
14
+ renderPseudoLink,
15
+ Role,
16
+ } from "@m3e/core";
17
+
18
+ import type { M3eFabMenuElement } from "./FabMenuElement";
19
+
20
+ /**
21
+ * An item of a floating action button (FAB) menu.
22
+ *
23
+ * @example
24
+ * The following example illustrates triggering a `m3e-fab-menu` from an `m3e-fab` using a `m3e-fab-menu-trigger`.
25
+ * ```html
26
+ * <m3e-fab variant="primary" size="large">
27
+ * <m3e-fab-menu-trigger for="fabmenu">
28
+ * <m3e-icon name="edit"></m3e-icon>
29
+ * </m3e-fab-menu-trigger>
30
+ * </m3e-fab>
31
+ * <m3e-fab-menu id="fabmenu" variant="secondary">
32
+ * <m3e-fab-menu-item>First</m3e-fab-menu-item>
33
+ * <m3e-fab-menu-item>Second</m3e-fab-menu-item>
34
+ * <m3e-fab-menu-item>Third</m3e-fab-menu-item>
35
+ * <m3e-fab-menu-item>Forth</m3e-fab-menu-item>
36
+ * <m3e-fab-menu-item>Fifth</m3e-fab-menu-item>
37
+ * <m3e-fab-menu-item>Sixth</m3e-fab-menu-item>
38
+ * </m3e-fab-menu>
39
+ * ```
40
+ *
41
+ * @tag m3e-menu-item
42
+ *
43
+ * @slot - Renders the label of the item.
44
+ * @slot icon - Renders an icon before the items's label.
45
+ *
46
+ * @attr disabled - Whether the element is disabled.
47
+ * @attr download - A value indicating whether the `target` of the link button will be downloaded, optionally specifying the new name of the file.
48
+ * @attr href - The URL to which the link button points.
49
+ * @attr rel - The relationship between the `target` of the link button and the document.
50
+ * @attr target - The target of the link button.
51
+ *
52
+ * @cssprop --m3e-fab-menu-item-height - Height of the menu item.
53
+ * @cssprop --m3e-fab-menu-item-font-size - Font size of the menu item label.
54
+ * @cssprop --m3e-fab-menu-item-font-weight - Font weight of the menu item label.
55
+ * @cssprop --m3e-fab-menu-item-line-height - Line height of the menu item label.
56
+ * @cssprop --m3e-fab-menu-item-tracking - Letter spacing of the menu item label.
57
+ * @cssprop --m3e-fab-menu-item-shape - Border radius of the menu item.
58
+ * @cssprop --m3e-fab-menu-item-leading-space - Padding at the start of the menu item.
59
+ * @cssprop --m3e-fab-menu-item-trailing-space - Padding at the end of the menu item.
60
+ * @cssprop --m3e-fab-menu-item-spacing - Gap between icon and label.
61
+ * @cssprop --m3e-fab-menu-item-icon-size - Size of the icon in the menu item.
62
+ */
63
+ @customElement("m3e-fab-menu-item")
64
+ export class M3eFabMenuItemElement extends KeyboardClick(
65
+ LinkButton(Disabled(AttachInternals(Role(LitElement, "menuitem"), true)))
66
+ ) {
67
+ /** The styles of the element. */
68
+ static override styles: CSSResultGroup = css`
69
+ :host {
70
+ display: inline-block;
71
+ outline: none;
72
+ user-select: none;
73
+ }
74
+ .base {
75
+ box-sizing: border-box;
76
+ vertical-align: middle;
77
+ display: inline-flex;
78
+ align-items: center;
79
+ justify-content: center;
80
+ position: relative;
81
+ width: 100%;
82
+ transition: ${unsafeCSS(
83
+ `background-color ${DesignToken.motion.duration.short4} ${DesignToken.motion.easing.standard}`
84
+ )};
85
+ height: var(--m3e-fab-menu-item-height, 3.5rem);
86
+ font-size: var(--m3e-fab-menu-item-font-size, ${DesignToken.typescale.standard.label.large.fontSize});
87
+ font-weight: var(--m3e-fab-menu-item-font-weight, ${DesignToken.typescale.standard.label.large.fontWeight});
88
+ line-height: var(--m3e-fab-menu-item-line-height, ${DesignToken.typescale.standard.label.large.lineHeight});
89
+ letter-spacing: var(--m3e-fab-menu-item-tracking, ${DesignToken.typescale.standard.label.large.tracking});
90
+ border-radius: var(--m3e-fab-menu-item-shape, ${DesignToken.shape.corner.full});
91
+ }
92
+ :host(:not(:disabled)) .label,
93
+ :host(:not(:disabled)) .icon {
94
+ color: var(--_fab-menu-item-color);
95
+ }
96
+ :host(:not(:disabled)) .base {
97
+ background-color: var(--_fab-menu-item-container-color);
98
+ --m3e-state-layer-hover-color: var(--_fab-menu-background-hover-color);
99
+ --m3e-state-layer-focus-color: var(--_fab-menu-background-focus-color);
100
+ --m3e-ripple-color: var(--_fab-menu-ripple-color);
101
+ }
102
+ :host(:disabled) .base {
103
+ background-color: color-mix(
104
+ in srgb,
105
+ var(--m3e-fab-menu-item-disabled-container-color, ${DesignToken.color.onSurface})
106
+ var(--m3e-fab-menu-item-disabled-container-opacity, 10%),
107
+ transparent
108
+ );
109
+ }
110
+ :host(:disabled) .label,
111
+ :host(:disabled) .icon {
112
+ color: color-mix(
113
+ in srgb,
114
+ var(--m3e-fab-menu-item-disabled-color, ${DesignToken.color.onSurface})
115
+ var(--m3e-fab-menu-item-disabled-opacity, 38%),
116
+ transparent
117
+ );
118
+ }
119
+ .touch {
120
+ position: absolute;
121
+ height: 3rem;
122
+ left: 0;
123
+ right: 0;
124
+ }
125
+ .wrapper {
126
+ width: 100%;
127
+ overflow: hidden;
128
+ display: inline-flex;
129
+ align-items: center;
130
+ padding-inline-start: var(--m3e-fab-menu-item-leading-space, 1.5rem);
131
+ padding-inline-end: var(--m3e-fab-menu-item-trailing-space, 1.5rem);
132
+ column-gap: var(--m3e-fab-menu-item-spacing, 0.5rem);
133
+ }
134
+ .label {
135
+ justify-self: center;
136
+ flex: 1 1 auto;
137
+ text-align: center;
138
+ white-space: nowrap;
139
+ overflow: hidden;
140
+ text-overflow: ellipsis;
141
+ transition: ${unsafeCSS(`color ${DesignToken.motion.duration.short4} ${DesignToken.motion.easing.standard}`)};
142
+ }
143
+ .icon {
144
+ font-size: var(--m3e-fab-menu-item-icon-size, 1.5rem);
145
+ transition: ${unsafeCSS(`color ${DesignToken.motion.duration.short4} ${DesignToken.motion.easing.standard}`)};
146
+ }
147
+ :host(:not(:disabled)) {
148
+ cursor: pointer;
149
+ }
150
+ ::slotted([slot="icon"]) {
151
+ font-size: inherit !important;
152
+ flex: none;
153
+ }
154
+ ::slotted(svg[slot="icon"]) {
155
+ width: 1em;
156
+ height: 1em;
157
+ }
158
+ a {
159
+ all: unset;
160
+ display: block;
161
+ position: absolute;
162
+ top: 0px;
163
+ left: 0px;
164
+ right: 0px;
165
+ bottom: 0px;
166
+ z-index: 1;
167
+ }
168
+ @media (prefers-reduced-motion) {
169
+ .base,
170
+ .label,
171
+ .icon {
172
+ transition: none;
173
+ }
174
+ }
175
+ @media (forced-colors: active) {
176
+ .base,
177
+ .label,
178
+ .icon {
179
+ transition: none;
180
+ }
181
+ :host(:not(:disabled)) .base {
182
+ background-color: Menu;
183
+ }
184
+ :host(:not(:disabled)) .label,
185
+ :host(:not(:disabled)) .icon {
186
+ color: MenuText;
187
+ }
188
+ :host(:disabled) .label,
189
+ :host(:disabled) .icon {
190
+ color: GrayText;
191
+ }
192
+ }
193
+ `;
194
+
195
+ /** @private */ @query(".elevation") private readonly _elevation?: M3eElevationElement;
196
+ /** @private */ @query(".focus-ring") private readonly _focusRing?: M3eFocusRingElement;
197
+ /** @private */ @query(".state-layer") private readonly _stateLayer?: M3eStateLayerElement;
198
+ /** @private */ @query(".ripple") private readonly _ripple?: M3eRippleElement;
199
+
200
+ /** @private */ readonly #clickHandler = (e: Event) => this.#handleClick(e);
201
+
202
+ /** The floating action button (FAB) menu to which this item belongs. */
203
+ get menu(): M3eFabMenuElement | null {
204
+ return this.closest("m3e-fab-menu");
205
+ }
206
+
207
+ /** @inheritdoc */
208
+ override connectedCallback(): void {
209
+ super.connectedCallback();
210
+ this.addEventListener("click", this.#clickHandler);
211
+ }
212
+
213
+ /** @inheritdoc */
214
+ override disconnectedCallback(): void {
215
+ super.disconnectedCallback();
216
+ this.removeEventListener("click", this.#clickHandler);
217
+ }
218
+
219
+ /** @inheritdoc */
220
+ protected override firstUpdated(_changedProperties: PropertyValues<this>): void {
221
+ super.firstUpdated(_changedProperties);
222
+ [this._elevation, this._focusRing, this._stateLayer, this._ripple].forEach((x) => x?.attach(this));
223
+ }
224
+
225
+ /** @inheritdoc */
226
+ override render(): unknown {
227
+ return html`<div class="base">
228
+ <m3e-elevation class="elevation" ?disabled="${this.disabled}"></m3e-elevation>
229
+ <m3e-state-layer class="state-layer" ?disabled="${this.disabled}"></m3e-state-layer>
230
+ <m3e-focus-ring class="focus-ring" ?disabled="${this.disabled}"></m3e-focus-ring>
231
+ <m3e-ripple class="ripple" ?disabled="${this.disabled}"></m3e-ripple>
232
+ <div class="touch" aria-hidden="true"></div>
233
+ ${this[renderPseudoLink]()}
234
+ <div class="wrapper">
235
+ <slot class="icon" name="icon" aria-hidden="true"></slot>
236
+ <div class="label"><slot></slot></div>
237
+ </div>
238
+ </div>`;
239
+ }
240
+
241
+ /** @private */
242
+ #handleClick(e: Event): void {
243
+ if (!e.defaultPrevented) {
244
+ this.menu?.hide(true);
245
+ }
246
+ }
247
+ }
248
+
249
+ declare global {
250
+ interface HTMLElementTagNameMap {
251
+ "m3e-fab-menu-item": M3eFabMenuItemElement;
252
+ }
253
+ }