@spectrum-web-components/menu 1.12.0-testing.20260223092154 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/custom-elements.json +887 -88
- package/package.json +10 -10
- package/src/Menu.d.ts +158 -4
- package/src/Menu.dev.js +496 -5
- package/src/Menu.dev.js.map +3 -3
- package/src/Menu.js +29 -2
- package/src/Menu.js.map +3 -3
- package/src/MenuDivider.d.ts +1 -4
- package/src/MenuItem.d.ts +33 -2
- package/src/MenuItem.dev.js +66 -5
- package/src/MenuItem.dev.js.map +2 -2
- package/src/MenuItem.js +8 -3
- package/src/MenuItem.js.map +3 -3
- package/src/menu-item.css.dev.js +1 -1
- package/src/menu-item.css.dev.js.map +1 -1
- package/src/menu-item.css.js +1 -1
- package/src/menu-item.css.js.map +1 -1
- package/src/menu.css.dev.js +1 -1
- package/src/menu.css.dev.js.map +1 -1
- package/src/menu.css.js +1 -1
- package/src/menu.css.js.map +1 -1
package/src/Menu.dev.js
CHANGED
|
@@ -11,15 +11,20 @@ var __decorateClass = (decorators, target, key, kind) => {
|
|
|
11
11
|
};
|
|
12
12
|
import {
|
|
13
13
|
html,
|
|
14
|
+
render,
|
|
14
15
|
SizedMixin,
|
|
15
16
|
SpectrumElement
|
|
16
17
|
} from "@spectrum-web-components/base";
|
|
17
18
|
import {
|
|
18
19
|
property,
|
|
19
|
-
query
|
|
20
|
+
query,
|
|
21
|
+
state
|
|
20
22
|
} from "@spectrum-web-components/base/src/decorators.js";
|
|
21
23
|
import { RovingTabindexController } from "@spectrum-web-components/reactive-controllers/src/RovingTabindex.js";
|
|
24
|
+
import "@spectrum-web-components/icons-ui/icons/sp-icon-arrow500.js";
|
|
25
|
+
import "../sp-menu-divider.dev.js";
|
|
22
26
|
import menuStyles from "./menu.css.js";
|
|
27
|
+
import { MenuItem } from "./MenuItem.dev.js";
|
|
23
28
|
export class Menu extends SizedMixin(SpectrumElement, { noDefaultSize: true }) {
|
|
24
29
|
constructor() {
|
|
25
30
|
super();
|
|
@@ -81,8 +86,78 @@ export class Menu extends SizedMixin(SpectrumElement, { noDefaultSize: true }) {
|
|
|
81
86
|
* @see {@link packages/overlay/src/LongpressController.ts} for longpress duration
|
|
82
87
|
*/
|
|
83
88
|
this.scrollTimeThreshold = 300;
|
|
89
|
+
/**
|
|
90
|
+
* Capture-phase keydown handler that overrides the default
|
|
91
|
+
* `RovingTabindexController` wrap behavior at the mobile drill-down
|
|
92
|
+
* boundary:
|
|
93
|
+
* - ArrowUp on the first nested item moves focus to the back row.
|
|
94
|
+
* - ArrowDown on the back row moves focus to the first nested item.
|
|
95
|
+
*
|
|
96
|
+
* Runs in the capture phase so it preempts the projected submenu's
|
|
97
|
+
* controller, which handles arrow keys in the bubble phase.
|
|
98
|
+
*/
|
|
99
|
+
this.handleMobileDrilldownKeydownCapture = (event) => {
|
|
100
|
+
if (event.defaultPrevented || !this.rovingTabindexController) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const { key, target } = event;
|
|
104
|
+
if (!(target instanceof MenuItem)) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const mobileRoot = this._mobileViewRoot;
|
|
108
|
+
if ((mobileRoot == null ? void 0 : mobileRoot.currentMobileSubmenu) && key === "ArrowUp") {
|
|
109
|
+
const items = this.childItems.filter((c) => this.isFocusableElement(c));
|
|
110
|
+
const firstNonBack = items.find(
|
|
111
|
+
(c) => !c.classList.contains("mobile-back-button")
|
|
112
|
+
);
|
|
113
|
+
if (firstNonBack === target) {
|
|
114
|
+
event.preventDefault();
|
|
115
|
+
event.stopImmediatePropagation();
|
|
116
|
+
mobileRoot._focusMobileBackRow();
|
|
117
|
+
}
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (this.mobileView && this._mobileSubmenuStack.length > 0 && key === "ArrowDown" && target.classList.contains("mobile-back-button")) {
|
|
121
|
+
event.preventDefault();
|
|
122
|
+
event.stopImmediatePropagation();
|
|
123
|
+
this._focusFirstItemInCurrentNestedSubmenu();
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
/**
|
|
127
|
+
* Cleans up the `mobile-transition` attribute once the CSS slide
|
|
128
|
+
* animation finishes, preventing the animation from replaying on
|
|
129
|
+
* subsequent layout changes. Bound declaratively via `@animationend`
|
|
130
|
+
* on the wrapper, so it only fires for that element.
|
|
131
|
+
*/
|
|
132
|
+
this._handleAnimationEnd = (event) => {
|
|
133
|
+
const target = event.currentTarget;
|
|
134
|
+
target == null ? void 0 : target.removeAttribute("mobile-transition");
|
|
135
|
+
};
|
|
136
|
+
/**
|
|
137
|
+
* Handles click on the mobile back button. Stops the event from
|
|
138
|
+
* reaching parent menus and closes the current mobile submenu.
|
|
139
|
+
*/
|
|
140
|
+
this.handleMobileBackClick = (event) => {
|
|
141
|
+
event.stopPropagation();
|
|
142
|
+
event.preventDefault();
|
|
143
|
+
this.closeMobileSubmenu();
|
|
144
|
+
};
|
|
145
|
+
/**
|
|
146
|
+
* Maps each projected submenu element to its Lit render container so
|
|
147
|
+
* that back button elements can be individually cleaned up when a
|
|
148
|
+
* submenu is restored, without affecting other levels in the stack.
|
|
149
|
+
*/
|
|
150
|
+
this._mobileBackContainers = /* @__PURE__ */ new Map();
|
|
151
|
+
/**
|
|
152
|
+
* Maps each projected submenu element to its original parent so the
|
|
153
|
+
* element can be moved back when the submenu is closed.
|
|
154
|
+
*/
|
|
155
|
+
this._mobileSubmenuOriginalParents = /* @__PURE__ */ new Map();
|
|
84
156
|
this.label = "";
|
|
85
157
|
this.ignore = false;
|
|
158
|
+
this.mobileView = false;
|
|
159
|
+
this.mobileBackLabel = "Back";
|
|
160
|
+
this._mobileSubmenuStack = [];
|
|
86
161
|
this.value = "";
|
|
87
162
|
this.valueSeparator = ",";
|
|
88
163
|
this._selected = [];
|
|
@@ -104,6 +179,10 @@ export class Menu extends SizedMixin(SpectrumElement, { noDefaultSize: true }) {
|
|
|
104
179
|
this.descendentOverlays = /* @__PURE__ */ new Map();
|
|
105
180
|
this.handleSubmenuClosed = (event) => {
|
|
106
181
|
event.stopPropagation();
|
|
182
|
+
if (this.mobileView) {
|
|
183
|
+
this.resetMobileSubmenus();
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
107
186
|
const target = event.composedPath()[0];
|
|
108
187
|
target.dispatchEvent(
|
|
109
188
|
new Event("sp-menu-submenu-closed", {
|
|
@@ -170,6 +249,11 @@ export class Menu extends SizedMixin(SpectrumElement, { noDefaultSize: true }) {
|
|
|
170
249
|
this.addEventListener("touchend", this.handlePointerup);
|
|
171
250
|
this.addEventListener("focusout", this.handleFocusout);
|
|
172
251
|
this.addEventListener("sp-menu-item-keydown", this.handleKeydown);
|
|
252
|
+
this.addEventListener(
|
|
253
|
+
"keydown",
|
|
254
|
+
this.handleMobileDrilldownKeydownCapture,
|
|
255
|
+
true
|
|
256
|
+
);
|
|
173
257
|
this.addEventListener("pointerup", this.handlePointerup);
|
|
174
258
|
this.addEventListener("sp-opened", this.handleSubmenuOpened);
|
|
175
259
|
this.addEventListener("sp-closed", this.handleSubmenuClosed);
|
|
@@ -186,6 +270,12 @@ export class Menu extends SizedMixin(SpectrumElement, { noDefaultSize: true }) {
|
|
|
186
270
|
get isSubmenu() {
|
|
187
271
|
return this.slot === "submenu";
|
|
188
272
|
}
|
|
273
|
+
asMenu(element) {
|
|
274
|
+
return element;
|
|
275
|
+
}
|
|
276
|
+
get _mobileViewRoot() {
|
|
277
|
+
return this.closest("sp-menu[mobile-view]");
|
|
278
|
+
}
|
|
189
279
|
// milliseconds
|
|
190
280
|
/**
|
|
191
281
|
* Public getter for scrolling state
|
|
@@ -197,6 +287,328 @@ export class Menu extends SizedMixin(SpectrumElement, { noDefaultSize: true }) {
|
|
|
197
287
|
set isScrolling(value) {
|
|
198
288
|
this.isCurrentlyScrolling = value;
|
|
199
289
|
}
|
|
290
|
+
/**
|
|
291
|
+
* Returns the MenuItem whose submenu is currently displayed at the
|
|
292
|
+
* top of the mobile drill-down stack, or `undefined` when no submenu
|
|
293
|
+
* is open.
|
|
294
|
+
*/
|
|
295
|
+
get currentMobileSubmenu() {
|
|
296
|
+
return this._mobileSubmenuStack[this._mobileSubmenuStack.length - 1];
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Opens a mobile submenu by projecting its content into this menu's
|
|
300
|
+
* light DOM, pushing it onto the submenu stack, triggering the
|
|
301
|
+
* slide-in animation, and focusing the back button.
|
|
302
|
+
*
|
|
303
|
+
* @param item - The MenuItem whose submenu should be opened.
|
|
304
|
+
*/
|
|
305
|
+
openMobileSubmenu(item) {
|
|
306
|
+
this._projectMobileSubmenu(item);
|
|
307
|
+
this._mobileSubmenuStack = [...this._mobileSubmenuStack, item];
|
|
308
|
+
this._triggerMobileTransition("forward");
|
|
309
|
+
this._focusProjectedSubmenu(item);
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Closes the topmost mobile submenu by restoring it to its original
|
|
313
|
+
* parent MenuItem, popping it from the stack, and either re-focusing
|
|
314
|
+
* the previous submenu's back button or returning focus to the
|
|
315
|
+
* triggering MenuItem when no deeper level remains.
|
|
316
|
+
*/
|
|
317
|
+
closeMobileSubmenu() {
|
|
318
|
+
const closedItem = this.currentMobileSubmenu;
|
|
319
|
+
if (closedItem) {
|
|
320
|
+
this._restoreMobileSubmenu(closedItem);
|
|
321
|
+
}
|
|
322
|
+
this._mobileSubmenuStack = this._mobileSubmenuStack.slice(0, -1);
|
|
323
|
+
const previous = this.currentMobileSubmenu;
|
|
324
|
+
if (previous == null ? void 0 : previous.submenuElement) {
|
|
325
|
+
previous.submenuElement.setAttribute("slot", "mobile-submenu");
|
|
326
|
+
this._focusProjectedSubmenu(previous);
|
|
327
|
+
} else if (closedItem) {
|
|
328
|
+
this.updateComplete.then(() => {
|
|
329
|
+
closedItem.focus();
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
this._triggerMobileTransition("back");
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Focuses the mobile back button inside the given item's projected
|
|
336
|
+
* submenu. Explicitly resets `tabIndex` and `focused` on every other
|
|
337
|
+
* child of the submenu so only the back row is in the tab order
|
|
338
|
+
* after the drill-down opens. We avoid delegating to the projected
|
|
339
|
+
* submenu's `RovingTabindexController` here because the back button
|
|
340
|
+
* is appended dynamically via `render()` and may not yet be in the
|
|
341
|
+
* controller's element cache when this runs, which would cause
|
|
342
|
+
* focus to fall back to the first nested item instead.
|
|
343
|
+
*
|
|
344
|
+
* @param item - The MenuItem whose projected submenu should receive focus.
|
|
345
|
+
*/
|
|
346
|
+
async _focusProjectedSubmenu(item) {
|
|
347
|
+
const submenuEl = item.submenuElement;
|
|
348
|
+
if (!submenuEl) {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
const backItem = submenuEl.querySelector(
|
|
352
|
+
".mobile-back-button"
|
|
353
|
+
);
|
|
354
|
+
if (!backItem) {
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
await backItem.updateComplete;
|
|
358
|
+
const submenu = this.asMenu(submenuEl);
|
|
359
|
+
await submenu.updateComplete;
|
|
360
|
+
submenu.childItems.forEach((child) => {
|
|
361
|
+
child.tabIndex = -1;
|
|
362
|
+
child.focused = false;
|
|
363
|
+
});
|
|
364
|
+
backItem.tabIndex = 0;
|
|
365
|
+
backItem.focused = true;
|
|
366
|
+
if (submenu.rovingTabindexController) {
|
|
367
|
+
submenu.rovingTabindexController.currentIndex = submenu.childItems.indexOf(backItem);
|
|
368
|
+
}
|
|
369
|
+
backItem.focus();
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Focuses the mobile back button of the currently visible projected
|
|
373
|
+
* submenu. Used when the user presses ArrowUp from the first nested
|
|
374
|
+
* item so focus moves to the back row instead of wrapping. Manages
|
|
375
|
+
* `tabIndex`/`focused` explicitly to keep the back row as the only
|
|
376
|
+
* tabbable element in the projected submenu.
|
|
377
|
+
*/
|
|
378
|
+
_focusMobileBackRow() {
|
|
379
|
+
var _a;
|
|
380
|
+
const submenuEl = (_a = this.currentMobileSubmenu) == null ? void 0 : _a.submenuElement;
|
|
381
|
+
if (!submenuEl) {
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
const backItem = submenuEl.querySelector(
|
|
385
|
+
".mobile-back-button"
|
|
386
|
+
);
|
|
387
|
+
if (!backItem) {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
const submenu = this.asMenu(submenuEl);
|
|
391
|
+
submenu.childItems.forEach((child) => {
|
|
392
|
+
child.tabIndex = -1;
|
|
393
|
+
child.focused = false;
|
|
394
|
+
});
|
|
395
|
+
backItem.tabIndex = 0;
|
|
396
|
+
backItem.focused = true;
|
|
397
|
+
if (submenu.rovingTabindexController) {
|
|
398
|
+
submenu.rovingTabindexController.currentIndex = submenu.childItems.indexOf(backItem);
|
|
399
|
+
}
|
|
400
|
+
backItem.focus();
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Focuses the first focusable item inside the currently visible
|
|
404
|
+
* projected submenu, skipping the back row. Used when the user
|
|
405
|
+
* presses ArrowDown from the back row. Manages `tabIndex`/`focused`
|
|
406
|
+
* explicitly so only the focused item is in the tab order.
|
|
407
|
+
*/
|
|
408
|
+
_focusFirstItemInCurrentNestedSubmenu() {
|
|
409
|
+
var _a;
|
|
410
|
+
const submenuEl = (_a = this.currentMobileSubmenu) == null ? void 0 : _a.submenuElement;
|
|
411
|
+
if (!submenuEl) {
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
const submenu = this.asMenu(submenuEl);
|
|
415
|
+
const firstItem = submenu.childItems.find(
|
|
416
|
+
(child) => !child.disabled && !child.classList.contains("mobile-back-button")
|
|
417
|
+
);
|
|
418
|
+
if (!firstItem) {
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
submenu.childItems.forEach((child) => {
|
|
422
|
+
child.tabIndex = -1;
|
|
423
|
+
child.focused = false;
|
|
424
|
+
});
|
|
425
|
+
firstItem.tabIndex = 0;
|
|
426
|
+
firstItem.focused = true;
|
|
427
|
+
if (submenu.rovingTabindexController) {
|
|
428
|
+
submenu.rovingTabindexController.currentIndex = submenu.childItems.indexOf(firstItem);
|
|
429
|
+
}
|
|
430
|
+
firstItem.focus();
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Triggers a CSS slide animation on the mobile submenu wrapper.
|
|
434
|
+
* Waits for the current Lit update cycle, then sets the
|
|
435
|
+
* `mobile-transition` attribute so the keyframe animation plays.
|
|
436
|
+
*
|
|
437
|
+
* @param direction - `'forward'` slides content in from the right,
|
|
438
|
+
* `'back'` slides content in from the left.
|
|
439
|
+
*/
|
|
440
|
+
_triggerMobileTransition(direction) {
|
|
441
|
+
this.updateComplete.then(() => {
|
|
442
|
+
var _a;
|
|
443
|
+
const wrapper = (_a = this.shadowRoot) == null ? void 0 : _a.querySelector(
|
|
444
|
+
".mobile-submenu-animation-wrapper"
|
|
445
|
+
);
|
|
446
|
+
if (!wrapper) {
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
wrapper.removeAttribute("mobile-transition");
|
|
450
|
+
requestAnimationFrame(() => {
|
|
451
|
+
wrapper.setAttribute("mobile-transition", direction);
|
|
452
|
+
});
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Restores every projected submenu back to its original parent and
|
|
457
|
+
* clears the submenu stack. Called during `disconnectedCallback` or
|
|
458
|
+
* when the menu overlay closes to ensure a clean state.
|
|
459
|
+
*/
|
|
460
|
+
resetMobileSubmenus() {
|
|
461
|
+
for (let i = this._mobileSubmenuStack.length - 1; i >= 0; i--) {
|
|
462
|
+
this._restoreMobileSubmenu(this._mobileSubmenuStack[i]);
|
|
463
|
+
}
|
|
464
|
+
this._mobileSubmenuStack = [];
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Moves the submenu element from its MenuItem parent into this Menu's
|
|
468
|
+
* light DOM with a `mobile-submenu` slot, so it projects through the
|
|
469
|
+
* named slot in the shadow DOM. Any previously visible projected submenu
|
|
470
|
+
* is moved to a non-rendered slot to avoid both showing at once.
|
|
471
|
+
*/
|
|
472
|
+
_projectMobileSubmenu(item) {
|
|
473
|
+
var _a;
|
|
474
|
+
const submenuEl = item.submenuElement;
|
|
475
|
+
if (!submenuEl) {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
const currentlyVisible = (_a = this.currentMobileSubmenu) == null ? void 0 : _a.submenuElement;
|
|
479
|
+
if (currentlyVisible) {
|
|
480
|
+
currentlyVisible.setAttribute("slot", "mobile-submenu-stacked");
|
|
481
|
+
}
|
|
482
|
+
const parentElement = submenuEl.parentElement;
|
|
483
|
+
if (!parentElement) {
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
item._mobileSubmenuProjected = true;
|
|
487
|
+
this._mobileSubmenuOriginalParents.set(submenuEl, parentElement);
|
|
488
|
+
const submenu = this.asMenu(submenuEl);
|
|
489
|
+
const savedChildItems = new Set(submenu.childItemSet);
|
|
490
|
+
submenuEl.setAttribute("slot", "mobile-submenu");
|
|
491
|
+
this.appendChild(submenuEl);
|
|
492
|
+
this._restoreSubmenuChildState(submenu, savedChildItems);
|
|
493
|
+
this._renderMobileBackElements(submenuEl);
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Restores the submenu element back to its original MenuItem parent
|
|
497
|
+
* and resets the slot attribute.
|
|
498
|
+
*/
|
|
499
|
+
_restoreMobileSubmenu(item) {
|
|
500
|
+
const submenuEl = item.submenuElement;
|
|
501
|
+
if (!submenuEl) {
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
this._removeMobileBackElements(submenuEl);
|
|
505
|
+
const submenu = this.asMenu(submenuEl);
|
|
506
|
+
const originalParent = this._mobileSubmenuOriginalParents.get(submenuEl);
|
|
507
|
+
if (originalParent) {
|
|
508
|
+
const savedChildItems = new Set(submenu.childItemSet);
|
|
509
|
+
submenuEl.setAttribute("slot", "submenu");
|
|
510
|
+
originalParent.appendChild(submenuEl);
|
|
511
|
+
this._mobileSubmenuOriginalParents.delete(submenuEl);
|
|
512
|
+
this._restoreSubmenuChildState(submenu, savedChildItems);
|
|
513
|
+
}
|
|
514
|
+
item._mobileSubmenuProjected = false;
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Declaratively renders the mobile back button and divider into the
|
|
518
|
+
* projected submenu element using Lit's `render()`, so the back button
|
|
519
|
+
* lives inside the same `<sp-menu>` and participates in its
|
|
520
|
+
* `RovingTabindexController` for keyboard navigation. Re-renders
|
|
521
|
+
* whenever `dir` or `mobileBackLabel` change so the icon orientation
|
|
522
|
+
* and label stay in sync.
|
|
523
|
+
*/
|
|
524
|
+
_renderMobileBackElements(submenuEl) {
|
|
525
|
+
var _a;
|
|
526
|
+
this._removeMobileBackElements(submenuEl);
|
|
527
|
+
const container = document.createElement("div");
|
|
528
|
+
container.className = "mobile-back-container";
|
|
529
|
+
submenuEl.insertBefore(container, submenuEl.firstChild);
|
|
530
|
+
this._mobileBackContainers.set(submenuEl, container);
|
|
531
|
+
render(
|
|
532
|
+
html`
|
|
533
|
+
<sp-menu-item
|
|
534
|
+
class="mobile-back-button"
|
|
535
|
+
data-mobile-back
|
|
536
|
+
@click=${this.handleMobileBackClick}
|
|
537
|
+
>
|
|
538
|
+
<sp-icon-arrow500
|
|
539
|
+
slot="icon"
|
|
540
|
+
class="mobile-back-icon"
|
|
541
|
+
></sp-icon-arrow500>
|
|
542
|
+
${this.mobileBackLabel}
|
|
543
|
+
</sp-menu-item>
|
|
544
|
+
<sp-menu-divider data-mobile-back></sp-menu-divider>
|
|
545
|
+
`,
|
|
546
|
+
container
|
|
547
|
+
);
|
|
548
|
+
const submenu = this.asMenu(submenuEl);
|
|
549
|
+
const backItem = submenuEl.querySelector(
|
|
550
|
+
".mobile-back-button"
|
|
551
|
+
);
|
|
552
|
+
if (backItem) {
|
|
553
|
+
submenu.childItemSet.add(backItem);
|
|
554
|
+
}
|
|
555
|
+
submenu.cachedChildItems = void 0;
|
|
556
|
+
(_a = submenu.rovingTabindexController) == null ? void 0 : _a.clearElementCache();
|
|
557
|
+
if (submenu.rovingTabindexController) {
|
|
558
|
+
submenu.rovingTabindexController.currentIndex = 0;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Re-render any open mobile back containers so changes to
|
|
563
|
+
* `mobileBackLabel` (and other reactive values consumed by the
|
|
564
|
+
* back-button template) propagate without needing to close
|
|
565
|
+
* and re-open the drill-down.
|
|
566
|
+
*/
|
|
567
|
+
_refreshMobileBackElements() {
|
|
568
|
+
this._mobileBackContainers.forEach((_container, submenuEl) => {
|
|
569
|
+
this._renderMobileBackElements(submenuEl);
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Removes the mobile back button render container from the given
|
|
574
|
+
* projected submenu element.
|
|
575
|
+
*/
|
|
576
|
+
_removeMobileBackElements(submenuEl) {
|
|
577
|
+
var _a;
|
|
578
|
+
const container = this._mobileBackContainers.get(submenuEl);
|
|
579
|
+
if (container) {
|
|
580
|
+
const backItem = submenuEl.querySelector(
|
|
581
|
+
".mobile-back-button"
|
|
582
|
+
);
|
|
583
|
+
if (backItem) {
|
|
584
|
+
this.asMenu(submenuEl).childItemSet.delete(backItem);
|
|
585
|
+
}
|
|
586
|
+
container.remove();
|
|
587
|
+
this._mobileBackContainers.delete(submenuEl);
|
|
588
|
+
const submenu = this.asMenu(submenuEl);
|
|
589
|
+
submenu.cachedChildItems = void 0;
|
|
590
|
+
(_a = submenu.rovingTabindexController) == null ? void 0 : _a.clearElementCache();
|
|
591
|
+
if (submenu.rovingTabindexController) {
|
|
592
|
+
submenu.rovingTabindexController.currentIndex = 0;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Re-adds saved child items to the submenu's `childItemSet` and
|
|
598
|
+
* invalidates cached references after DOM re-parenting, so the
|
|
599
|
+
* `RovingTabindexController` picks up the correct set of focusable
|
|
600
|
+
* children.
|
|
601
|
+
*
|
|
602
|
+
* @param submenu - The submenu whose child state needs restoring.
|
|
603
|
+
* @param savedChildItems - The set of MenuItem children captured
|
|
604
|
+
* before the DOM move.
|
|
605
|
+
*/
|
|
606
|
+
_restoreSubmenuChildState(submenu, savedChildItems) {
|
|
607
|
+
var _a;
|
|
608
|
+
savedChildItems.forEach((child) => submenu.childItemSet.add(child));
|
|
609
|
+
submenu.cachedChildItems = void 0;
|
|
610
|
+
(_a = submenu.rovingTabindexController) == null ? void 0 : _a.clearElementCache();
|
|
611
|
+
}
|
|
200
612
|
get selected() {
|
|
201
613
|
return !this.selects ? [] : this._selected;
|
|
202
614
|
}
|
|
@@ -615,6 +1027,33 @@ export class Menu extends SizedMixin(SpectrumElement, { noDefaultSize: true }) {
|
|
|
615
1027
|
const shouldOpenSubmenu = dir === "ltr" && key === "ArrowRight" || dir === "rtl" && key === "ArrowLeft";
|
|
616
1028
|
const shouldCloseSelfAsSubmenu = dir === "ltr" && key === "ArrowLeft" || dir === "rtl" && key === "ArrowRight" || key === "Escape";
|
|
617
1029
|
const lastFocusedItem = root;
|
|
1030
|
+
if (this.mobileView) {
|
|
1031
|
+
if (shouldOpenSubmenu && (lastFocusedItem == null ? void 0 : lastFocusedItem.hasSubmenu)) {
|
|
1032
|
+
event.stopPropagation();
|
|
1033
|
+
this.openMobileSubmenu(lastFocusedItem);
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
if (shouldCloseSelfAsSubmenu && this._mobileSubmenuStack.length > 0) {
|
|
1037
|
+
event.stopPropagation();
|
|
1038
|
+
this.closeMobileSubmenu();
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
const mobileRoot = this._mobileViewRoot;
|
|
1044
|
+
if (mobileRoot) {
|
|
1045
|
+
if (shouldOpenSubmenu && (lastFocusedItem == null ? void 0 : lastFocusedItem.hasSubmenu)) {
|
|
1046
|
+
event.stopPropagation();
|
|
1047
|
+
mobileRoot.openMobileSubmenu(lastFocusedItem);
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
if (shouldCloseSelfAsSubmenu) {
|
|
1051
|
+
event.stopPropagation();
|
|
1052
|
+
mobileRoot.closeMobileSubmenu();
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
618
1057
|
if (shouldOpenSubmenu) {
|
|
619
1058
|
if (lastFocusedItem == null ? void 0 : lastFocusedItem.hasSubmenu) {
|
|
620
1059
|
event.stopPropagation();
|
|
@@ -631,6 +1070,17 @@ export class Menu extends SizedMixin(SpectrumElement, { noDefaultSize: true }) {
|
|
|
631
1070
|
if (event.defaultPrevented || !this.rovingTabindexController) {
|
|
632
1071
|
return;
|
|
633
1072
|
}
|
|
1073
|
+
if (this.mobileView && this._mobileSubmenuStack.length > 0) {
|
|
1074
|
+
const { key: key2 } = event;
|
|
1075
|
+
const dir = this.dir;
|
|
1076
|
+
const shouldClose = dir === "ltr" && key2 === "ArrowLeft" || dir === "rtl" && key2 === "ArrowRight" || key2 === "Escape";
|
|
1077
|
+
if (shouldClose) {
|
|
1078
|
+
event.stopPropagation();
|
|
1079
|
+
event.preventDefault();
|
|
1080
|
+
this.closeMobileSubmenu();
|
|
1081
|
+
}
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
634
1084
|
const { key, root, shiftKey, target } = event;
|
|
635
1085
|
const openSubmenuKey = ["Enter", " "].includes(key);
|
|
636
1086
|
if (shiftKey && target !== this && this.hasAttribute("tabindex")) {
|
|
@@ -650,7 +1100,14 @@ export class Menu extends SizedMixin(SpectrumElement, { noDefaultSize: true }) {
|
|
|
650
1100
|
}
|
|
651
1101
|
if (openSubmenuKey && (root == null ? void 0 : root.hasSubmenu) && !root.open) {
|
|
652
1102
|
event.preventDefault();
|
|
653
|
-
|
|
1103
|
+
const mobileRoot = this._mobileViewRoot;
|
|
1104
|
+
if (this.mobileView) {
|
|
1105
|
+
this.openMobileSubmenu(root);
|
|
1106
|
+
} else if (mobileRoot) {
|
|
1107
|
+
mobileRoot.openMobileSubmenu(root);
|
|
1108
|
+
} else {
|
|
1109
|
+
root.openOverlay(true);
|
|
1110
|
+
}
|
|
654
1111
|
return;
|
|
655
1112
|
}
|
|
656
1113
|
if (key === " " || key === "Enter") {
|
|
@@ -754,8 +1211,8 @@ export class Menu extends SizedMixin(SpectrumElement, { noDefaultSize: true }) {
|
|
|
754
1211
|
assignedElements.forEach((item) => {
|
|
755
1212
|
if (typeof item.triggerUpdate !== "undefined") {
|
|
756
1213
|
item.triggerUpdate();
|
|
757
|
-
} else if (typeof item.childItems !== "undefined") {
|
|
758
|
-
item.childItems.forEach((child) => {
|
|
1214
|
+
} else if (typeof this.asMenu(item).childItems !== "undefined") {
|
|
1215
|
+
this.asMenu(item).childItems.forEach((child) => {
|
|
759
1216
|
child.triggerUpdate();
|
|
760
1217
|
});
|
|
761
1218
|
}
|
|
@@ -776,7 +1233,22 @@ export class Menu extends SizedMixin(SpectrumElement, { noDefaultSize: true }) {
|
|
|
776
1233
|
`;
|
|
777
1234
|
}
|
|
778
1235
|
render() {
|
|
779
|
-
|
|
1236
|
+
const hasMobileSubmenu = this.mobileView && this._mobileSubmenuStack.length > 0;
|
|
1237
|
+
return html`
|
|
1238
|
+
<div
|
|
1239
|
+
class=${hasMobileSubmenu ? "mobile-slot-hidden" : "mobile-slot-wrapper"}
|
|
1240
|
+
>
|
|
1241
|
+
${this.renderMenuItemSlot()}
|
|
1242
|
+
</div>
|
|
1243
|
+
${hasMobileSubmenu ? html`
|
|
1244
|
+
<div
|
|
1245
|
+
class="mobile-submenu-animation-wrapper"
|
|
1246
|
+
@animationend=${this._handleAnimationEnd}
|
|
1247
|
+
>
|
|
1248
|
+
<slot name="mobile-submenu"></slot>
|
|
1249
|
+
</div>
|
|
1250
|
+
` : ""}
|
|
1251
|
+
`;
|
|
780
1252
|
}
|
|
781
1253
|
firstUpdated(changed) {
|
|
782
1254
|
super.firstUpdated(changed);
|
|
@@ -802,6 +1274,14 @@ export class Menu extends SizedMixin(SpectrumElement, { noDefaultSize: true }) {
|
|
|
802
1274
|
this.removeAttribute("aria-label");
|
|
803
1275
|
}
|
|
804
1276
|
}
|
|
1277
|
+
if (changes.has("mobileBackLabel") && this.hasUpdated) {
|
|
1278
|
+
this._refreshMobileBackElements();
|
|
1279
|
+
}
|
|
1280
|
+
if (changes.has("mobileView") && this.hasUpdated) {
|
|
1281
|
+
if (!this.mobileView && this._mobileSubmenuStack.length > 0) {
|
|
1282
|
+
this.resetMobileSubmenus();
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
805
1285
|
}
|
|
806
1286
|
selectsChanged() {
|
|
807
1287
|
const updates = [
|
|
@@ -828,6 +1308,8 @@ export class Menu extends SizedMixin(SpectrumElement, { noDefaultSize: true }) {
|
|
|
828
1308
|
this.selectedItemsMap.clear();
|
|
829
1309
|
this.childItemSet.clear();
|
|
830
1310
|
this.descendentOverlays = /* @__PURE__ */ new Map();
|
|
1311
|
+
this.resetMobileSubmenus();
|
|
1312
|
+
this._mobileSubmenuOriginalParents.clear();
|
|
831
1313
|
super.disconnectedCallback();
|
|
832
1314
|
}
|
|
833
1315
|
async getUpdateComplete() {
|
|
@@ -847,6 +1329,15 @@ __decorateClass([
|
|
|
847
1329
|
__decorateClass([
|
|
848
1330
|
property({ type: Boolean, reflect: true })
|
|
849
1331
|
], Menu.prototype, "ignore", 2);
|
|
1332
|
+
__decorateClass([
|
|
1333
|
+
property({ type: Boolean, attribute: "mobile-view", reflect: true })
|
|
1334
|
+
], Menu.prototype, "mobileView", 2);
|
|
1335
|
+
__decorateClass([
|
|
1336
|
+
property({ type: String, attribute: "mobile-back-label" })
|
|
1337
|
+
], Menu.prototype, "mobileBackLabel", 2);
|
|
1338
|
+
__decorateClass([
|
|
1339
|
+
state()
|
|
1340
|
+
], Menu.prototype, "_mobileSubmenuStack", 2);
|
|
850
1341
|
__decorateClass([
|
|
851
1342
|
property({ type: String, reflect: true })
|
|
852
1343
|
], Menu.prototype, "selects", 2);
|