Skip to content

Commit 8422f1c

Browse files
authored
fix: prevent closing submenu when switching between items (#10152)
1 parent f9d5f09 commit 8422f1c

File tree

3 files changed

+72
-8
lines changed

3 files changed

+72
-8
lines changed

packages/context-menu/src/vaadin-contextmenu-items-mixin.js

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -138,10 +138,10 @@ export const ItemsMixin = (superClass) =>
138138

139139
/** @private */
140140
__openSubMenu(subMenu, itemElement) {
141+
// Update sub-menu items and position target
141142
this.__updateSubMenuForItem(subMenu, itemElement);
142143

143144
const parent = this._overlayElement;
144-
145145
const subMenuOverlay = subMenu._overlayElement;
146146
// Store the reference parent overlay
147147
subMenuOverlay._setParentOverlay(parent);
@@ -170,6 +170,7 @@ export const ItemsMixin = (superClass) =>
170170
subMenu.items = itemElement._item.children;
171171
subMenu.listenOn = itemElement;
172172
subMenu._positionTarget = itemElement;
173+
subMenu._overlayElement.requestContentUpdate();
173174
}
174175

175176
/**
@@ -371,24 +372,32 @@ export const ItemsMixin = (superClass) =>
371372
}
372373

373374
const subMenu = this._subMenu;
375+
const expandedItem = this._listBox.querySelector('[expanded]');
374376

375-
if (item) {
377+
if (item && item !== expandedItem) {
376378
const { children } = item._item;
377379

378380
// Check if the sub-menu was focused before closing it.
379381
const child = subMenu._overlayElement._contentRoot.firstElementChild;
380382
const isSubmenuFocused = child && child.focused;
381383

382-
if (subMenu.items !== children) {
384+
// Mark previously expanded item as collapsed
385+
if (expandedItem) {
386+
this.__updateExpanded(expandedItem, false);
387+
}
388+
389+
// Close sub-menu if there are no children for the new item
390+
if (!children || !children.length) {
383391
subMenu.close();
384392
}
393+
385394
if (!this.opened) {
386395
return;
387396
}
388397

389398
if (children && children.length) {
399+
// Open or update the submenu if the new item has children
390400
this.__updateExpanded(item, true);
391-
392401
this.__openSubMenu(subMenu, item);
393402
} else if (isSubmenuFocused) {
394403
// If the sub-menu item was focused, focus its parent item.

packages/context-menu/test/helpers.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,15 @@ export async function openMenu(target, event = isTouch ? 'click' : 'mouseover')
1919
}
2020
// Disable logic that delays opening submenu
2121
menu.__openListenerActive = true;
22+
23+
// Open the submenu
24+
const wasOpened = menu._overlayElement.opened;
2225
activateItem(target, event);
23-
await oneEvent(menu._overlayElement, 'vaadin-overlay-open');
26+
27+
// Wait for the submenu to open, unless it was already opened for a different item
28+
if (!wasOpened) {
29+
await oneEvent(menu._overlayElement, 'vaadin-overlay-open');
30+
}
2431
}
2532

2633
export function getMenuItems(menu) {

packages/context-menu/test/items.test.js

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,7 @@ describe('items', () => {
242242
it('should not have a checked item', async () => {
243243
rootMenu.items[0].children[0].checked = false;
244244
subMenu.close();
245+
await nextRender();
245246
await openMenu(getMenuItems(rootMenu)[0]);
246247
expect(getMenuItems(subMenu)[0].hasAttribute('menu-item-checked')).to.be.false;
247248
});
@@ -250,6 +251,27 @@ describe('items', () => {
250251
expect(getMenuItems(subMenu)[1].disabled).to.be.true;
251252
});
252253

254+
it('should update the submenu when activating other parent item', () => {
255+
activateItem(getMenuItems(rootMenu)[3]);
256+
257+
expect(subMenu.opened).to.be.true;
258+
259+
const items = getMenuItems(subMenu);
260+
expect(items.length).to.equal(3);
261+
expect(items[0].textContent).to.equal('foo-3-0');
262+
expect(items[1].textContent).to.equal('foo-3-1');
263+
expect(items[2].textContent).to.equal('foo-3-2');
264+
});
265+
266+
it('should not change opened state of the submenu when activating other parent item', () => {
267+
const openedChangeSpy = sinon.spy();
268+
subMenu.addEventListener('opened-changed', openedChangeSpy);
269+
270+
activateItem(getMenuItems(rootMenu)[3]);
271+
272+
expect(openedChangeSpy.called).to.be.false;
273+
});
274+
253275
it('should close the submenu on activating non-parent item', () => {
254276
activateItem(getMenuItems(rootMenu)[1]);
255277
expect(subMenu.opened).to.be.false;
@@ -349,8 +371,9 @@ describe('items', () => {
349371
expect(getMenuItems(rootMenu)[0].getAttribute('aria-haspopup')).to.equal('false');
350372
});
351373

352-
it('should open item on right arrow', () => {
374+
it('should open item on right arrow', async () => {
353375
subMenu.close();
376+
await nextRender();
354377
arrowRightKeyDown(getMenuItems(rootMenu)[0]);
355378
expect(subMenu.opened).to.be.true;
356379
});
@@ -359,25 +382,29 @@ describe('items', () => {
359382
document.documentElement.setAttribute('dir', 'rtl');
360383
await nextFrame();
361384
subMenu.close();
385+
await nextRender();
362386
arrowLeftKeyDown(getMenuItems(rootMenu)[0]);
363387
expect(subMenu.opened).to.be.true;
364388
document.documentElement.setAttribute('dir', 'ltr');
365389
});
366390

367-
it('should open item on enter', () => {
391+
it('should open item on enter', async () => {
368392
subMenu.close();
393+
await nextRender();
369394
enterKeyDown(getMenuItems(rootMenu)[0]);
370395
expect(subMenu.opened).to.be.true;
371396
});
372397

373-
it('should open item on space', () => {
398+
it('should open item on space', async () => {
374399
subMenu.close();
400+
await nextRender();
375401
spaceKeyDown(getMenuItems(rootMenu)[0]);
376402
expect(subMenu.opened).to.be.true;
377403
});
378404

379405
it('should not focus item if parent item is not focused', async () => {
380406
subMenu.close();
407+
await nextRender();
381408
rootOverlay.focus();
382409
await openMenu(getMenuItems(rootMenu)[0]);
383410
expect(subMenu.opened).to.be.true;
@@ -387,6 +414,7 @@ describe('items', () => {
387414

388415
it('should focus first item in submenu on overlay element arrow down', async () => {
389416
subMenu.close();
417+
await nextRender();
390418
rootOverlay.focus();
391419
await openMenu(getMenuItems(rootMenu)[0]);
392420
const item = getMenuItems(subMenu)[0];
@@ -397,6 +425,7 @@ describe('items', () => {
397425

398426
it('should focus last item in submenu on overlay element arrow up', async () => {
399427
subMenu.close();
428+
await nextRender();
400429
rootOverlay.focus();
401430
await openMenu(getMenuItems(rootMenu)[0]);
402431
const items = getMenuItems(subMenu);
@@ -408,6 +437,7 @@ describe('items', () => {
408437

409438
it('should focus first item after re-opening when using components', async () => {
410439
subMenu.close();
440+
await nextRender();
411441
rootOverlay.focus();
412442

413443
const rootItem = getMenuItems(rootMenu)[3];
@@ -434,6 +464,7 @@ describe('items', () => {
434464

435465
it('should focus first non-disabled item after re-opening when using components', async () => {
436466
subMenu.close();
467+
await nextRender();
437468
rootOverlay.focus();
438469

439470
rootMenu.items[3].children[0].disabled = true;
@@ -529,6 +560,23 @@ describe('items', () => {
529560
expect(getMenuItems(rootMenu)[0].getAttribute('aria-expanded')).to.equal('false');
530561
});
531562

563+
it('should update expanded attributes when activating different parent items', async () => {
564+
expect(getMenuItems(rootMenu)[0].hasAttribute('expanded')).to.be.true;
565+
expect(getMenuItems(rootMenu)[0].getAttribute('aria-expanded')).to.equal('true');
566+
567+
await activateItem(getMenuItems(rootMenu)[3]);
568+
expect(getMenuItems(rootMenu)[0].hasAttribute('expanded')).to.be.false;
569+
expect(getMenuItems(rootMenu)[0].getAttribute('aria-expanded')).to.equal('false');
570+
expect(getMenuItems(rootMenu)[3].hasAttribute('expanded')).to.be.true;
571+
expect(getMenuItems(rootMenu)[3].getAttribute('aria-expanded')).to.equal('true');
572+
573+
await activateItem(getMenuItems(rootMenu)[0]);
574+
expect(getMenuItems(rootMenu)[0].hasAttribute('expanded')).to.be.true;
575+
expect(getMenuItems(rootMenu)[0].getAttribute('aria-expanded')).to.equal('true');
576+
expect(getMenuItems(rootMenu)[3].hasAttribute('expanded')).to.be.false;
577+
expect(getMenuItems(rootMenu)[3].getAttribute('aria-expanded')).to.equal('false');
578+
});
579+
532580
(isTouch ? describe.skip : describe)('scrolling', () => {
533581
let rootOverlay, subOverlay1, subOverlay2, scrollElm;
534582

0 commit comments

Comments
 (0)