@spectrum-web-components/menu 0.36.0 → 0.38.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.
Files changed (48) hide show
  1. package/custom-elements.json +459 -159
  2. package/package.json +9 -9
  3. package/src/Menu.d.ts +16 -6
  4. package/src/Menu.dev.js +179 -65
  5. package/src/Menu.dev.js.map +2 -2
  6. package/src/Menu.js +7 -3
  7. package/src/Menu.js.map +3 -3
  8. package/src/MenuGroup.d.ts +0 -2
  9. package/src/MenuGroup.dev.js +8 -12
  10. package/src/MenuGroup.dev.js.map +2 -2
  11. package/src/MenuGroup.js +3 -5
  12. package/src/MenuGroup.js.map +3 -3
  13. package/src/MenuItem.d.ts +30 -23
  14. package/src/MenuItem.dev.js +209 -215
  15. package/src/MenuItem.dev.js.map +3 -3
  16. package/src/MenuItem.js +36 -18
  17. package/src/MenuItem.js.map +3 -3
  18. package/src/menu-item.css.dev.js +9 -9
  19. package/src/menu-item.css.dev.js.map +1 -1
  20. package/src/menu-item.css.js +9 -9
  21. package/src/menu-item.css.js.map +1 -1
  22. package/src/menu.css.dev.js +1 -1
  23. package/src/menu.css.dev.js.map +1 -1
  24. package/src/menu.css.js +1 -1
  25. package/src/menu.css.js.map +1 -1
  26. package/src/spectrum-config.js +27 -1
  27. package/src/spectrum-menu-item.css.dev.js +8 -8
  28. package/src/spectrum-menu-item.css.dev.js.map +1 -1
  29. package/src/spectrum-menu-item.css.js +8 -8
  30. package/src/spectrum-menu-item.css.js.map +1 -1
  31. package/stories/index.js +4 -0
  32. package/stories/index.js.map +2 -2
  33. package/stories/menu-item.stories.js +10 -0
  34. package/stories/menu-item.stories.js.map +2 -2
  35. package/stories/menu.stories.js +47 -0
  36. package/stories/menu.stories.js.map +2 -2
  37. package/stories/submenu.stories.js +117 -104
  38. package/stories/submenu.stories.js.map +3 -3
  39. package/test/menu-group.test.js +14 -1
  40. package/test/menu-group.test.js.map +2 -2
  41. package/test/menu-item.test.js +36 -0
  42. package/test/menu-item.test.js.map +2 -2
  43. package/test/menu-selects.test.js +3 -1
  44. package/test/menu-selects.test.js.map +2 -2
  45. package/test/menu.test.js +9 -1
  46. package/test/menu.test.js.map +2 -2
  47. package/test/submenu.test.js +208 -84
  48. package/test/submenu.test.js.map +2 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spectrum-web-components/menu",
3
- "version": "0.36.0",
3
+ "version": "0.38.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -85,13 +85,13 @@
85
85
  ],
86
86
  "dependencies": {
87
87
  "@lit-labs/observers": "^2.0.0",
88
- "@spectrum-web-components/action-button": "^0.36.0",
89
- "@spectrum-web-components/base": "^0.36.0",
90
- "@spectrum-web-components/divider": "^0.36.0",
91
- "@spectrum-web-components/icon": "^0.36.0",
92
- "@spectrum-web-components/icons-ui": "^0.36.0",
93
- "@spectrum-web-components/overlay": "^0.36.0",
94
- "@spectrum-web-components/shared": "^0.36.0"
88
+ "@spectrum-web-components/action-button": "^0.38.0",
89
+ "@spectrum-web-components/base": "^0.38.0",
90
+ "@spectrum-web-components/divider": "^0.38.0",
91
+ "@spectrum-web-components/icon": "^0.38.0",
92
+ "@spectrum-web-components/icons-ui": "^0.38.0",
93
+ "@spectrum-web-components/overlay": "^0.38.0",
94
+ "@spectrum-web-components/shared": "^0.38.0"
95
95
  },
96
96
  "devDependencies": {
97
97
  "@spectrum-css/menu": "^5.0.2"
@@ -102,5 +102,5 @@
102
102
  "./sp-*.js",
103
103
  "./**/*.dev.js"
104
104
  ],
105
- "gitHead": "a532ff8a410abeefb54d9638a2316ae82570566e"
105
+ "gitHead": "9a099b7543672f2fd4030833ab813b16c2cad62e"
106
106
  }
package/src/Menu.d.ts CHANGED
@@ -26,7 +26,7 @@ declare const Menu_base: typeof SpectrumElement & {
26
26
  */
27
27
  export declare class Menu extends Menu_base {
28
28
  static get styles(): CSSResultArray;
29
- isSubmenu: boolean;
29
+ private get isSubmenu();
30
30
  label: string;
31
31
  ignore: boolean;
32
32
  selects: undefined | 'inherit' | 'single' | 'multiple';
@@ -55,7 +55,7 @@ export declare class Menu extends Menu_base {
55
55
  private resolvedRole?;
56
56
  /**
57
57
  * When a descendant `<sp-menu-item>` element is added or updated it will dispatch
58
- * this event to announce its presence in the DOM. During the capture phase the first
58
+ * this event to announce its presence in the DOM. During the CAPTURE phase the first
59
59
  * Menu based element that the event encounters will manage the focus state of the
60
60
  * dispatching `<sp-menu-item>` element.
61
61
  * @param event
@@ -63,7 +63,7 @@ export declare class Menu extends Menu_base {
63
63
  private onFocusableItemAddedOrUpdated;
64
64
  /**
65
65
  * When a descendant `<sp-menu-item>` element is added or updated it will dispatch
66
- * this event to announce its presence in the DOM. During the bubble phase the first
66
+ * this event to announce its presence in the DOM. During the BUBBLE phase the first
67
67
  * Menu based element that the event encounters that does not inherit selection will
68
68
  * manage the selection state of the dispatching `<sp-menu-item>` element.
69
69
  * @param event
@@ -73,24 +73,33 @@ export declare class Menu extends Menu_base {
73
73
  private removeChildItem;
74
74
  constructor();
75
75
  focus({ preventScroll }?: FocusOptions): void;
76
- private onClick;
76
+ private handleClick;
77
77
  handleFocusin(event: FocusEvent): void;
78
78
  startListeningToKeyboard(): void;
79
79
  handleFocusout(event: FocusEvent): void;
80
80
  stopListeningToKeyboard(): void;
81
+ private descendentOverlays;
82
+ protected handleDescendentOverlayOpened(event: Event): void;
83
+ protected handleDescendentOverlayClosed(event: Event): void;
84
+ handleSubmenuClosed: (event: Event) => void;
85
+ handleSubmenuOpened: (event: Event) => void;
81
86
  selectOrToggleItem(targetItem: MenuItem): Promise<void>;
82
87
  protected navigateWithinMenu(event: KeyboardEvent): void;
83
- protected navigateBetweenRelatedMenus(code: string): void;
88
+ protected navigateBetweenRelatedMenus(event: KeyboardEvent): void;
84
89
  handleKeydown(event: KeyboardEvent): void;
85
90
  focusMenuItemByOffset(offset: number): MenuItem;
86
91
  private prepareToCleanUp;
92
+ private _hasUpdatedSelectedItemIndex;
87
93
  updateSelectedItemIndex(): void;
88
94
  private _willUpdateItems;
89
95
  private handleItemsChanged;
96
+ private updateCache;
90
97
  private updateItemFocus;
98
+ closeDescendentOverlays(): void;
91
99
  private forwardFocusVisibleToItem;
100
+ private handleSlotchange;
101
+ protected renderMenuItemSlot(): TemplateResult;
92
102
  render(): TemplateResult;
93
- private _notFirstUpdated;
94
103
  protected firstUpdated(changed: PropertyValues): void;
95
104
  protected updated(changes: PropertyValues<this>): void;
96
105
  protected selectsChanged(): void;
@@ -98,6 +107,7 @@ export declare class Menu extends Menu_base {
98
107
  disconnectedCallback(): void;
99
108
  protected childItemsUpdated: Promise<unknown[]>;
100
109
  protected cacheUpdated: Promise<void>;
110
+ protected resolveCacheUpdated: () => void;
101
111
  protected getUpdateComplete(): Promise<boolean>;
102
112
  }
103
113
  export {};
package/src/Menu.dev.js CHANGED
@@ -24,10 +24,9 @@ import menuStyles from "./menu.css.js";
24
24
  function elementIsOrContains(el, isOrContains) {
25
25
  return !!isOrContains && (el === isOrContains || el.contains(isOrContains));
26
26
  }
27
- export class Menu extends SizedMixin(SpectrumElement) {
27
+ export class Menu extends SizedMixin(SpectrumElement, { noDefaultSize: true }) {
28
28
  constructor() {
29
29
  super();
30
- this.isSubmenu = false;
31
30
  this.label = "";
32
31
  this.ignore = false;
33
32
  this.value = "";
@@ -38,9 +37,44 @@ export class Menu extends SizedMixin(SpectrumElement) {
38
37
  this.focusedItemIndex = 0;
39
38
  this.focusInItemIndex = 0;
40
39
  this.selectedItemsMap = /* @__PURE__ */ new Map();
40
+ this.descendentOverlays = /* @__PURE__ */ new Map();
41
+ this.handleSubmenuClosed = (event) => {
42
+ event.stopPropagation();
43
+ const target = event.composedPath()[0];
44
+ target.dispatchEvent(
45
+ new Event("sp-menu-submenu-closed", {
46
+ bubbles: true,
47
+ composed: true
48
+ })
49
+ );
50
+ };
51
+ this.handleSubmenuOpened = (event) => {
52
+ event.stopPropagation();
53
+ const target = event.composedPath()[0];
54
+ target.dispatchEvent(
55
+ new Event("sp-menu-submenu-opened", {
56
+ bubbles: true,
57
+ composed: true
58
+ })
59
+ );
60
+ const focusedItem = this.childItems[this.focusedItemIndex];
61
+ if (focusedItem) {
62
+ focusedItem.focused = false;
63
+ }
64
+ const openedItem = event.composedPath().find((el) => this.childItemSet.has(el));
65
+ if (!openedItem)
66
+ return;
67
+ const openedItemIndex = this.childItems.indexOf(openedItem);
68
+ this.focusedItemIndex = openedItemIndex;
69
+ this.focusInItemIndex = openedItemIndex;
70
+ };
71
+ this._hasUpdatedSelectedItemIndex = false;
41
72
  this._willUpdateItems = false;
42
- this._notFirstUpdated = false;
43
73
  this.cacheUpdated = Promise.resolve();
74
+ /* c8 ignore next 3 */
75
+ this.resolveCacheUpdated = () => {
76
+ return;
77
+ };
44
78
  this.addEventListener(
45
79
  "sp-menu-item-added-or-updated",
46
80
  this.onSelectableItemAddedOrUpdated
@@ -52,13 +86,18 @@ export class Menu extends SizedMixin(SpectrumElement) {
52
86
  capture: true
53
87
  }
54
88
  );
55
- this.addEventListener("sp-menu-item-removed", this.removeChildItem);
56
- this.addEventListener("click", this.onClick);
89
+ this.addEventListener("click", this.handleClick);
57
90
  this.addEventListener("focusin", this.handleFocusin);
91
+ this.addEventListener("focusout", this.handleFocusout);
92
+ this.addEventListener("sp-opened", this.handleSubmenuOpened);
93
+ this.addEventListener("sp-closed", this.handleSubmenuClosed);
58
94
  }
59
95
  static get styles() {
60
96
  return [menuStyles];
61
97
  }
98
+ get isSubmenu() {
99
+ return this.slot === "submenu";
100
+ }
62
101
  get childItems() {
63
102
  if (!this.cachedChildItems) {
64
103
  this.cachedChildItems = this.updateCachedMenuItems();
@@ -67,7 +106,7 @@ export class Menu extends SizedMixin(SpectrumElement) {
67
106
  }
68
107
  updateCachedMenuItems() {
69
108
  this.cachedChildItems = [];
70
- const slotElements = this.menuSlot ? this.menuSlot.assignedElements({ flatten: true }) : [];
109
+ const slotElements = this.menuSlot.assignedElements({ flatten: true });
71
110
  for (const slotElement of slotElements) {
72
111
  const childMenuItems = slotElement instanceof MenuItem ? [slotElement] : [...slotElement.querySelectorAll(`*`)];
73
112
  for (const childMenuItem of childMenuItems) {
@@ -103,17 +142,37 @@ export class Menu extends SizedMixin(SpectrumElement) {
103
142
  }
104
143
  /**
105
144
  * When a descendant `<sp-menu-item>` element is added or updated it will dispatch
106
- * this event to announce its presence in the DOM. During the capture phase the first
145
+ * this event to announce its presence in the DOM. During the CAPTURE phase the first
107
146
  * Menu based element that the event encounters will manage the focus state of the
108
147
  * dispatching `<sp-menu-item>` element.
109
148
  * @param event
110
149
  */
111
150
  onFocusableItemAddedOrUpdated(event) {
151
+ event.menuCascade.set(this, {
152
+ hadFocusRoot: !!event.item.menuData.focusRoot,
153
+ ancestorWithSelects: event.currentAncestorWithSelects
154
+ });
155
+ if (this.selects) {
156
+ event.currentAncestorWithSelects = this;
157
+ }
158
+ event.item.menuData.focusRoot = event.item.menuData.focusRoot || this;
159
+ }
160
+ /**
161
+ * When a descendant `<sp-menu-item>` element is added or updated it will dispatch
162
+ * this event to announce its presence in the DOM. During the BUBBLE phase the first
163
+ * Menu based element that the event encounters that does not inherit selection will
164
+ * manage the selection state of the dispatching `<sp-menu-item>` element.
165
+ * @param event
166
+ */
167
+ onSelectableItemAddedOrUpdated(event) {
112
168
  var _a, _b;
113
- if (event.item.menuData.focusRoot && !this.ignore) {
169
+ const cascadeData = event.menuCascade.get(this);
170
+ if (!cascadeData)
171
+ return;
172
+ event.item.menuData.parentMenu = event.item.menuData.parentMenu || this;
173
+ if (cascadeData.hadFocusRoot && !this.ignore) {
114
174
  this.tabIndex = -1;
115
175
  }
116
- event.focusRoot = this;
117
176
  this.addChildItem(event.item);
118
177
  if (this.selects === "inherit") {
119
178
  this.resolvedSelects = "inherit";
@@ -122,34 +181,33 @@ export class Menu extends SizedMixin(SpectrumElement) {
122
181
  } else if (this.selects) {
123
182
  this.resolvedRole = this.ignore ? "none" : this.getAttribute("role") || void 0;
124
183
  this.resolvedSelects = this.selects;
125
- event.currentAncestorWithSelects = this;
126
184
  } else {
127
185
  this.resolvedRole = this.ignore ? "none" : this.getAttribute("role") || void 0;
128
186
  this.resolvedSelects = this.resolvedRole === "none" ? "ignore" : "none";
129
187
  }
130
- }
131
- /**
132
- * When a descendant `<sp-menu-item>` element is added or updated it will dispatch
133
- * this event to announce its presence in the DOM. During the bubble phase the first
134
- * Menu based element that the event encounters that does not inherit selection will
135
- * manage the selection state of the dispatching `<sp-menu-item>` element.
136
- * @param event
137
- */
138
- onSelectableItemAddedOrUpdated(event) {
139
188
  const selects = this.resolvedSelects === "single" || this.resolvedSelects === "multiple";
189
+ event.item.menuData.cleanupSteps.push(
190
+ (item) => this.removeChildItem(item)
191
+ );
140
192
  if ((selects || !this.selects && this.resolvedSelects !== "ignore") && !event.item.menuData.selectionRoot) {
141
193
  event.item.setRole(this.childRole);
142
- event.selectionRoot = this;
194
+ event.item.menuData.selectionRoot = event.item.menuData.selectionRoot || this;
195
+ if (event.item.selected) {
196
+ this.selectedItemsMap.set(event.item, true);
197
+ this.selectedItems = [...this.selectedItems, event.item];
198
+ this.selected = [...this.selected, event.item.value];
199
+ this.value = this.selected.join(this.valueSeparator);
200
+ }
143
201
  }
144
202
  }
145
203
  addChildItem(item) {
146
204
  this.childItemSet.add(item);
147
205
  this.handleItemsChanged();
148
206
  }
149
- async removeChildItem(event) {
150
- this.childItemSet.delete(event.item);
207
+ async removeChildItem(item) {
208
+ this.childItemSet.delete(item);
151
209
  this.cachedChildItems = void 0;
152
- if (event.item.focused) {
210
+ if (item.focused) {
153
211
  this.handleItemsChanged();
154
212
  await this.updateComplete;
155
213
  this.focus();
@@ -167,12 +225,12 @@ export class Menu extends SizedMixin(SpectrumElement) {
167
225
  }
168
226
  this.focusMenuItemByOffset(0);
169
227
  super.focus({ preventScroll });
170
- const selectedItem = this.querySelector("[selected]");
228
+ const selectedItem = this.selectedItems[0];
171
229
  if (selectedItem && !preventScroll) {
172
230
  selectedItem.scrollIntoView({ block: "nearest" });
173
231
  }
174
232
  }
175
- onClick(event) {
233
+ handleClick(event) {
176
234
  if (event.defaultPrevented) {
177
235
  return;
178
236
  }
@@ -204,18 +262,18 @@ export class Menu extends SizedMixin(SpectrumElement) {
204
262
  }
205
263
  handleFocusin(event) {
206
264
  var _a;
207
- const isOrContainsRelatedTarget = elementIsOrContains(
265
+ const wasOrContainedRelatedTarget = elementIsOrContains(
208
266
  this,
209
267
  event.relatedTarget
210
268
  );
211
- if (isOrContainsRelatedTarget || this.childItems.some(
269
+ if (this.childItems.some(
212
270
  (childItem) => childItem.menuData.focusRoot !== this
213
271
  )) {
214
272
  return;
215
273
  }
216
274
  const activeElement = this.getRootNode().activeElement;
217
275
  const selectionRoot = ((_a = this.childItems[this.focusedItemIndex]) == null ? void 0 : _a.menuData.selectionRoot) || this;
218
- if (activeElement !== selectionRoot || !isOrContainsRelatedTarget) {
276
+ if (activeElement !== selectionRoot || !wasOrContainedRelatedTarget && event.target !== this) {
219
277
  selectionRoot.focus({ preventScroll: true });
220
278
  if (activeElement && this.focusedItemIndex === 0) {
221
279
  const offset = this.childItems.findIndex(
@@ -230,27 +288,32 @@ export class Menu extends SizedMixin(SpectrumElement) {
230
288
  }
231
289
  startListeningToKeyboard() {
232
290
  this.addEventListener("keydown", this.handleKeydown);
233
- this.addEventListener("focusout", this.handleFocusout);
234
291
  }
235
292
  handleFocusout(event) {
236
293
  if (elementIsOrContains(this, event.relatedTarget)) {
237
- event.composedPath()[0].focused = false;
238
294
  return;
239
295
  }
240
296
  this.stopListeningToKeyboard();
241
- if (event.target === this && this.childItems.some(
242
- (childItem) => childItem.menuData.focusRoot === this
243
- )) {
244
- const focusedItem = this.childItems[this.focusedItemIndex];
245
- if (focusedItem) {
246
- focusedItem.focused = false;
247
- }
248
- }
297
+ this.childItems.forEach((child) => child.focused = false);
249
298
  this.removeAttribute("aria-activedescendant");
250
299
  }
251
300
  stopListeningToKeyboard() {
252
301
  this.removeEventListener("keydown", this.handleKeydown);
253
- this.removeEventListener("focusout", this.handleFocusout);
302
+ }
303
+ handleDescendentOverlayOpened(event) {
304
+ const target = event.composedPath()[0];
305
+ if (!target.overlayElement)
306
+ return;
307
+ this.descendentOverlays.set(
308
+ target.overlayElement,
309
+ target.overlayElement
310
+ );
311
+ }
312
+ handleDescendentOverlayClosed(event) {
313
+ const target = event.composedPath()[0];
314
+ if (!target.overlayElement)
315
+ return;
316
+ this.descendentOverlays.delete(target.overlayElement);
254
317
  }
255
318
  async selectOrToggleItem(targetItem) {
256
319
  const resolvedSelects = this.resolvedSelects;
@@ -258,7 +321,11 @@ export class Menu extends SizedMixin(SpectrumElement) {
258
321
  const oldSelected = this.selected.slice();
259
322
  const oldSelectedItems = this.selectedItems.slice();
260
323
  const oldValue = this.value;
261
- this.childItems[this.focusedItemIndex].focused = false;
324
+ const focusedChild = this.childItems[this.focusedItemIndex];
325
+ if (focusedChild) {
326
+ focusedChild.focused = false;
327
+ focusedChild.active = false;
328
+ }
262
329
  this.focusedItemIndex = this.childItems.indexOf(targetItem);
263
330
  this.forwardFocusVisibleToItem(targetItem);
264
331
  if (resolvedSelects === "multiple") {
@@ -322,32 +389,42 @@ export class Menu extends SizedMixin(SpectrumElement) {
322
389
  return;
323
390
  }
324
391
  event.preventDefault();
392
+ event.stopPropagation();
325
393
  itemToFocus.scrollIntoView({ block: "nearest" });
326
394
  }
327
- navigateBetweenRelatedMenus(code) {
395
+ navigateBetweenRelatedMenus(event) {
396
+ const { code } = event;
328
397
  const shouldOpenSubmenu = this.isLTR && code === "ArrowRight" || !this.isLTR && code === "ArrowLeft";
329
398
  const shouldCloseSelfAsSubmenu = this.isLTR && code === "ArrowLeft" || !this.isLTR && code === "ArrowRight";
330
399
  if (shouldOpenSubmenu) {
400
+ event.stopPropagation();
331
401
  const lastFocusedItem = this.childItems[this.focusedItemIndex];
332
402
  if (lastFocusedItem == null ? void 0 : lastFocusedItem.hasSubmenu) {
333
- this.blur();
334
403
  lastFocusedItem.openOverlay();
335
404
  }
336
405
  } else if (shouldCloseSelfAsSubmenu && this.isSubmenu) {
406
+ event.stopPropagation();
337
407
  this.dispatchEvent(new Event("close", { bubbles: true }));
408
+ this.updateSelectedItemIndex();
338
409
  }
339
410
  }
340
411
  handleKeydown(event) {
341
412
  var _a;
413
+ const isNotThisOrDirectChild = event.target !== this && this !== event.target.parentElement;
414
+ if (isNotThisOrDirectChild || event.defaultPrevented) {
415
+ return;
416
+ }
417
+ const lastFocusedItem = this.childItems[this.focusedItemIndex];
418
+ if (lastFocusedItem) {
419
+ lastFocusedItem.focused = true;
420
+ }
342
421
  const { code } = event;
343
422
  if (code === "Tab") {
344
423
  this.prepareToCleanUp();
345
424
  return;
346
425
  }
347
426
  if (code === "Space") {
348
- const lastFocusedItem = this.childItems[this.focusedItemIndex];
349
427
  if (lastFocusedItem == null ? void 0 : lastFocusedItem.hasSubmenu) {
350
- this.blur();
351
428
  lastFocusedItem.openOverlay();
352
429
  return;
353
430
  }
@@ -360,12 +437,15 @@ export class Menu extends SizedMixin(SpectrumElement) {
360
437
  this.navigateWithinMenu(event);
361
438
  return;
362
439
  }
363
- this.navigateBetweenRelatedMenus(code);
440
+ this.navigateBetweenRelatedMenus(event);
364
441
  }
365
442
  focusMenuItemByOffset(offset) {
366
443
  const step = offset || 1;
367
444
  const focusedItem = this.childItems[this.focusedItemIndex];
368
- focusedItem.focused = false;
445
+ if (focusedItem) {
446
+ focusedItem.focused = false;
447
+ focusedItem.active = focusedItem.open;
448
+ }
369
449
  this.focusedItemIndex = (this.childItems.length + this.focusedItemIndex + offset) % this.childItems.length;
370
450
  let itemToFocus = this.childItems[this.focusedItemIndex];
371
451
  let availableItems = this.childItems.length;
@@ -404,7 +484,7 @@ export class Menu extends SizedMixin(SpectrumElement) {
404
484
  itemIndex -= 1;
405
485
  const childItem = this.childItems[itemIndex];
406
486
  if (childItem.menuData.selectionRoot === this) {
407
- if (childItem.selected) {
487
+ if (childItem.selected || !this._hasUpdatedSelectedItemIndex && this.selected.includes(childItem.value)) {
408
488
  firstOrFirstSelectedIndex = itemIndex;
409
489
  selectedItemsMap.set(childItem, true);
410
490
  selected.unshift(childItem.value);
@@ -430,20 +510,24 @@ export class Menu extends SizedMixin(SpectrumElement) {
430
510
  handleItemsChanged() {
431
511
  this.cachedChildItems = void 0;
432
512
  if (!this._willUpdateItems) {
433
- let resolve = () => {
434
- return;
435
- };
436
- this.cacheUpdated = new Promise((res) => resolve = res);
437
513
  this._willUpdateItems = true;
438
- window.requestAnimationFrame(() => {
439
- if (this.cachedChildItems === void 0) {
440
- this.updateSelectedItemIndex();
441
- this.updateItemFocus();
442
- }
443
- this._willUpdateItems = false;
444
- resolve();
445
- });
514
+ this.cacheUpdated = this.updateCache();
515
+ }
516
+ }
517
+ async updateCache() {
518
+ if (!this.hasUpdated) {
519
+ await Promise.all([
520
+ new Promise((res) => requestAnimationFrame(() => res(true))),
521
+ this.updateComplete
522
+ ]);
523
+ } else {
524
+ await new Promise((res) => requestAnimationFrame(() => res(true)));
525
+ }
526
+ if (this.cachedChildItems === void 0) {
527
+ this.updateSelectedItemIndex();
528
+ this.updateItemFocus();
446
529
  }
530
+ this._willUpdateItems = false;
447
531
  }
448
532
  updateItemFocus() {
449
533
  if (this.childItems.length == 0) {
@@ -454,21 +538,52 @@ export class Menu extends SizedMixin(SpectrumElement) {
454
538
  this.forwardFocusVisibleToItem(focusInItem);
455
539
  }
456
540
  }
541
+ closeDescendentOverlays() {
542
+ this.descendentOverlays.forEach((overlay) => {
543
+ overlay.open = false;
544
+ });
545
+ this.descendentOverlays = /* @__PURE__ */ new Map();
546
+ }
457
547
  forwardFocusVisibleToItem(item) {
458
548
  if (item.menuData.focusRoot !== this) {
459
549
  return;
460
550
  }
461
- item.focused = this.hasVisibleFocusInTree();
551
+ this.closeDescendentOverlays();
552
+ const focused = this.hasVisibleFocusInTree() || !!this.childItems.find((child) => {
553
+ return child.hasVisibleFocusInTree();
554
+ });
555
+ item.focused = focused;
462
556
  this.setAttribute("aria-activedescendant", item.id);
463
557
  if (item.menuData.selectionRoot && item.menuData.selectionRoot !== this) {
464
558
  item.menuData.selectionRoot.focus();
465
559
  }
466
560
  }
467
- render() {
561
+ handleSlotchange({
562
+ target
563
+ }) {
564
+ const assignedElement = target.assignedElements({
565
+ flatten: true
566
+ });
567
+ if (this.childItems.length !== assignedElement.length) {
568
+ assignedElement.forEach((item) => {
569
+ if (typeof item.triggerUpdate !== "undefined") {
570
+ item.triggerUpdate();
571
+ }
572
+ });
573
+ }
574
+ }
575
+ renderMenuItemSlot() {
468
576
  return html`
469
- <slot></slot>
577
+ <slot
578
+ @sp-menu-submenu-opened=${this.handleDescendentOverlayOpened}
579
+ @sp-menu-submenu-closed=${this.handleDescendentOverlayClosed}
580
+ @slotchange=${this.handleSlotchange}
581
+ ></slot>
470
582
  `;
471
583
  }
584
+ render() {
585
+ return this.renderMenuItemSlot();
586
+ }
472
587
  firstUpdated(changed) {
473
588
  super.firstUpdated(changed);
474
589
  if (!this.hasAttribute("tabindex") && !this.ignore) {
@@ -491,17 +606,16 @@ export class Menu extends SizedMixin(SpectrumElement) {
491
606
  }
492
607
  updated(changes) {
493
608
  super.updated(changes);
494
- if (changes.has("selects") && this._notFirstUpdated) {
609
+ if (changes.has("selects") && this.hasUpdated) {
495
610
  this.selectsChanged();
496
611
  }
497
- if (changes.has("label")) {
612
+ if (changes.has("label") && (this.label || typeof changes.get("label") !== "undefined")) {
498
613
  if (this.label) {
499
614
  this.setAttribute("aria-label", this.label);
500
615
  } else {
501
616
  this.removeAttribute("aria-label");
502
617
  }
503
618
  }
504
- this._notFirstUpdated = true;
505
619
  }
506
620
  selectsChanged() {
507
621
  const updates = [