Skip to content

Commit fcd342c

Browse files
authored
fix: only close the topmost popover on esc and outside click (#7480)
1 parent 9ccd96c commit fcd342c

File tree

3 files changed

+134
-2
lines changed

3 files changed

+134
-2
lines changed

integration/tests/dialog-popover.test.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,66 @@ describe('popover in dialog', () => {
7171
});
7272
});
7373
});
74+
75+
describe('dialog in popover', () => {
76+
let popover, target, button, dialog;
77+
78+
beforeEach(async () => {
79+
[target, popover] = fixtureSync(`
80+
<div>
81+
<button id="target">Open popover</button>
82+
<vaadin-popover for="target"></vaadin-popover>
83+
</div>
84+
`).children;
85+
86+
popover.renderer = (root) => {
87+
root.innerHTML = `
88+
<button>Open dialog</button>
89+
<vaadin-dialog></vaadin-dialog>
90+
`;
91+
[button, dialog] = root.children;
92+
93+
button.addEventListener('click', () => {
94+
dialog.opened = true;
95+
});
96+
97+
dialog.renderer = (dialogRoot) => {
98+
dialogRoot.textContent = 'Dialog content';
99+
};
100+
};
101+
102+
await nextRender();
103+
target.click();
104+
await nextRender();
105+
});
106+
107+
['modal', 'modeless'].forEach((type) => {
108+
describe(`${type} popover`, () => {
109+
beforeEach(async () => {
110+
if (type === 'modal') {
111+
popover.modal = true;
112+
await nextUpdate(popover);
113+
}
114+
115+
button.focus();
116+
button.click();
117+
await nextRender();
118+
});
119+
120+
it(`should not close the ${type} popover when closing a child dialog on Escape`, async () => {
121+
await sendKeys({ press: 'Escape' });
122+
123+
expect(dialog.opened).to.be.false;
124+
expect(popover.opened).to.be.true;
125+
});
126+
127+
it(`should close the ${type} popover on subsequent Escape after the child dialog is closed`, async () => {
128+
await sendKeys({ press: 'Escape' });
129+
130+
await sendKeys({ press: 'Escape' });
131+
132+
expect(popover.opened).to.be.false;
133+
});
134+
});
135+
});
136+
});

packages/popover/src/vaadin-popover.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
1212
import { OverlayClassMixin } from '@vaadin/component-base/src/overlay-class-mixin.js';
1313
import { PolylitMixin } from '@vaadin/component-base/src/polylit-mixin.js';
1414
import { generateUniqueId } from '@vaadin/component-base/src/unique-id-utils.js';
15+
import { isLastOverlay } from '@vaadin/overlay/src/vaadin-overlay-stack-mixin.js';
1516
import { ThemePropertyMixin } from '@vaadin/vaadin-themable-mixin/vaadin-theme-property-mixin.js';
1617
import { PopoverPositionMixin } from './vaadin-popover-position-mixin.js';
1718
import { PopoverTargetMixin } from './vaadin-popover-target-mixin.js';
@@ -500,7 +501,8 @@ class Popover extends PopoverPositionMixin(
500501
!this.__isManual &&
501502
!this.modal &&
502503
!event.composedPath().some((el) => el === this._overlayElement || el === this.target) &&
503-
!this.noCloseOnOutsideClick
504+
!this.noCloseOnOutsideClick &&
505+
isLastOverlay(this._overlayElement)
504506
) {
505507
this._openedStateController.close(true);
506508
}
@@ -526,7 +528,14 @@ class Popover extends PopoverPositionMixin(
526528
* @private
527529
*/
528530
__onGlobalKeyDown(event) {
529-
if (event.key === 'Escape' && !this.modal && !this.noCloseOnEsc && this.opened && !this.__isManual) {
531+
if (
532+
event.key === 'Escape' &&
533+
!this.modal &&
534+
!this.noCloseOnEsc &&
535+
this.opened &&
536+
!this.__isManual &&
537+
isLastOverlay(this._overlayElement)
538+
) {
530539
// Prevent closing parent overlay (e.g. dialog)
531540
event.stopPropagation();
532541
this._openedStateController.close(true);

packages/popover/test/basic.test.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,66 @@ describe('popover', () => {
269269
});
270270
});
271271

272+
describe('nested popovers', () => {
273+
let secondPopover, secondTarget;
274+
275+
function nestedRenderer(root) {
276+
root.innerHTML = `
277+
<button id="second-target">Second target</button>
278+
<vaadin-popover for="second-target"></vaadin-popover>
279+
`;
280+
[secondTarget, secondPopover] = root.children;
281+
}
282+
283+
beforeEach(async () => {
284+
popover.renderer = nestedRenderer;
285+
286+
// Open the first popover
287+
target.click();
288+
await nextRender();
289+
290+
// Open the second popover
291+
secondTarget.click();
292+
await nextRender();
293+
294+
// Expect both popovers to be opened
295+
expect(popover.opened).to.be.true;
296+
expect(secondPopover.opened).to.be.true;
297+
});
298+
299+
it('should close the topmost overlay on global Escape press', async () => {
300+
esc(document.body);
301+
await nextRender();
302+
303+
// Expect only the second popover to be closed
304+
expect(popover.opened).to.be.true;
305+
expect(secondPopover.opened).to.be.false;
306+
307+
esc(document.body);
308+
await nextRender();
309+
310+
// Expect both popovers to be closed
311+
expect(popover.opened).to.be.false;
312+
expect(secondPopover.opened).to.be.false;
313+
});
314+
315+
it('should close the topmost overlay on outside click', async () => {
316+
outsideClick();
317+
await nextRender();
318+
319+
// Expect only the second popover to be closed
320+
expect(popover.opened).to.be.true;
321+
expect(secondPopover.opened).to.be.false;
322+
323+
outsideClick();
324+
await nextRender();
325+
326+
// Expect both popovers to be closed
327+
expect(popover.opened).to.be.false;
328+
expect(secondPopover.opened).to.be.false;
329+
});
330+
});
331+
272332
describe('backdrop', () => {
273333
beforeEach(async () => {
274334
popover.withBackdrop = true;

0 commit comments

Comments
 (0)