Skip to content

Commit 5606cf0

Browse files
authored
fix: allow popovers to close when interacting with non-nested overlays (#10322)
1 parent d1d78d2 commit 5606cf0

File tree

3 files changed

+155
-48
lines changed

3 files changed

+155
-48
lines changed

packages/overlay/src/vaadin-overlay-stack-mixin.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const getAttachedInstances = () => [...attachedInstances].filter((el) => !el.has
1717
* Returns true if all the instances on top of the overlay are nested overlays.
1818
* @private
1919
*/
20-
const hasOnlyNestedOverlays = (overlay) => {
20+
export const hasOnlyNestedOverlays = (overlay) => {
2121
const instances = getAttachedInstances();
2222
const next = instances[instances.indexOf(overlay) + 1];
2323
if (!next) {

packages/popover/src/vaadin-popover.js

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ import { defineCustomElement } from '@vaadin/component-base/src/define.js';
1616
import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
1717
import { PolylitMixin } from '@vaadin/component-base/src/polylit-mixin.js';
1818
import { generateUniqueId } from '@vaadin/component-base/src/unique-id-utils.js';
19-
import { isLastOverlay as isLastOverlayBase } from '@vaadin/overlay/src/vaadin-overlay-stack-mixin.js';
19+
import {
20+
hasOnlyNestedOverlays,
21+
isLastOverlay as isLastOverlayBase,
22+
} from '@vaadin/overlay/src/vaadin-overlay-stack-mixin.js';
2023
import { ThemePropertyMixin } from '@vaadin/vaadin-themable-mixin/vaadin-theme-property-mixin.js';
2124
import { PopoverPositionMixin } from './vaadin-popover-position-mixin.js';
2225
import { PopoverTargetMixin } from './vaadin-popover-target-mixin.js';
@@ -823,10 +826,14 @@ class Popover extends PopoverPositionMixin(
823826

824827
/** @private */
825828
__onTargetFocusOut(event) {
826-
// Do not close the popover on overlay focusout if it's not the last one.
829+
// Do not close if there is a nested overlay that should be closed through some method first.
827830
// This covers the case when focus moves to the nested popover opened
828831
// without focusing parent popover overlay (e.g. using hover trigger).
829-
if (this._overlayElement.opened && !isLastOverlay(this._overlayElement)) {
832+
if (
833+
this._overlayElement.opened &&
834+
!isLastOverlay(this._overlayElement) &&
835+
hasOnlyNestedOverlays(this._overlayElement)
836+
) {
830837
return;
831838
}
832839

@@ -852,13 +859,16 @@ class Popover extends PopoverPositionMixin(
852859

853860
/** @private */
854861
__onTargetMouseLeave(event) {
855-
// Do not close the popover on target focusout if the overlay is not the last one.
856-
// This happens e.g. when opening the nested popover that uses non-modal overlay.
857-
if (this._overlayElement.opened && !isLastOverlay(this._overlayElement)) {
862+
// Do not close if the pointer moves to the overlay
863+
if (this.contains(event.relatedTarget)) {
858864
return;
859865
}
860-
861-
if (this.contains(event.relatedTarget)) {
866+
// Do not close if there is a nested overlay that should be closed through some method first.
867+
if (
868+
this._overlayElement.opened &&
869+
!isLastOverlay(this._overlayElement) &&
870+
hasOnlyNestedOverlays(this._overlayElement)
871+
) {
862872
return;
863873
}
864874

@@ -878,11 +888,11 @@ class Popover extends PopoverPositionMixin(
878888

879889
/** @private */
880890
__onFocusOut(event) {
881-
// Do not close the popover on overlay focusout if it's not the last one.
891+
// Do not close if there is a nested overlay that should be closed through some method first.
882892
// This covers the following cases of nested overlay based components:
883893
// 1. Moving focus to the nested overlay (e.g. vaadin-select, vaadin-menu-bar)
884894
// 2. Closing not focused nested overlay on outside (e.g. vaadin-combo-box)
885-
if (!isLastOverlay(this._overlayElement)) {
895+
if (!isLastOverlay(this._overlayElement) && hasOnlyNestedOverlays(this._overlayElement)) {
886896
return;
887897
}
888898

@@ -924,14 +934,12 @@ class Popover extends PopoverPositionMixin(
924934

925935
/** @private */
926936
__onOverlayMouseLeave(event) {
927-
// Do not close the popover on overlay focusout if it's not the last one.
928-
// This happens when opening the nested component that uses "modal" overlay
929-
// setting `pointer-events: none` on the body (combo-box, date-picker etc).
930-
if (!isLastOverlay(this._overlayElement)) {
937+
// Do not close if the pointer moves to the target
938+
if (event.relatedTarget === this.target) {
931939
return;
932940
}
933-
934-
if (event.relatedTarget === this.target) {
941+
// Do not close if there is a nested overlay that should be closed through some method first.
942+
if (!isLastOverlay(this._overlayElement) && hasOnlyNestedOverlays(this._overlayElement)) {
935943
return;
936944
}
937945

packages/popover/test/nested.test.js

Lines changed: 130 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { Popover } from '../src/vaadin-popover.js';
55
import { mouseenter, mouseleave } from './helpers.js';
66

77
describe('nested popover', () => {
8-
let popover, target, secondPopover, secondTarget;
8+
let popover, target, nestedPopover, nestedTarget;
99

1010
before(() => {
1111
Popover.setDefaultFocusDelay(0);
@@ -22,11 +22,11 @@ describe('nested popover', () => {
2222
return;
2323
}
2424
root.innerHTML = `
25-
<button id="second-target">Second target</button>
26-
<vaadin-popover for="second-target"></vaadin-popover>
25+
<button id="nested-target">Nested target</button>
26+
<vaadin-popover for="nested-target"></vaadin-popover>
2727
`;
28-
[secondTarget, secondPopover] = root.children;
29-
secondPopover.renderer = (root2) => {
28+
[nestedTarget, nestedPopover] = root.children;
29+
nestedPopover.renderer = (root2) => {
3030
root2.textContent = 'Nested';
3131
};
3232
};
@@ -39,45 +39,45 @@ describe('nested popover', () => {
3939
target.click();
4040
await nextRender();
4141

42-
// Open the second popover
43-
secondTarget.click();
42+
// Open the nested popover
43+
nestedTarget.click();
4444
await nextRender();
4545

4646
// Expect both popovers to be opened
4747
expect(popover.opened).to.be.true;
48-
expect(secondPopover.opened).to.be.true;
48+
expect(nestedPopover.opened).to.be.true;
4949
});
5050

5151
it('should close the topmost overlay on global Escape press', async () => {
5252
esc(document.body);
5353
await nextRender();
5454

55-
// Expect only the second popover to be closed
55+
// Expect only the nested popover to be closed
5656
expect(popover.opened).to.be.true;
57-
expect(secondPopover.opened).to.be.false;
57+
expect(nestedPopover.opened).to.be.false;
5858

5959
esc(document.body);
6060
await nextRender();
6161

6262
// Expect both popovers to be closed
6363
expect(popover.opened).to.be.false;
64-
expect(secondPopover.opened).to.be.false;
64+
expect(nestedPopover.opened).to.be.false;
6565
});
6666

6767
it('should close the topmost overlay on outside click', async () => {
6868
outsideClick();
6969
await nextRender();
7070

71-
// Expect only the second popover to be closed
71+
// Expect only the nested popover to be closed
7272
expect(popover.opened).to.be.true;
73-
expect(secondPopover.opened).to.be.false;
73+
expect(nestedPopover.opened).to.be.false;
7474

7575
outsideClick();
7676
await nextRender();
7777

7878
// Expect both popovers to be closed
7979
expect(popover.opened).to.be.false;
80-
expect(secondPopover.opened).to.be.false;
80+
expect(nestedPopover.opened).to.be.false;
8181
});
8282
});
8383

@@ -91,11 +91,11 @@ describe('nested popover', () => {
9191
target.focus();
9292
await nextRender();
9393

94-
secondPopover.modal = true;
95-
await nextUpdate(secondPopover);
94+
nestedPopover.modal = true;
95+
await nextUpdate(nestedPopover);
9696

9797
// Open programmatically so focus stays on target
98-
secondPopover.opened = true;
98+
nestedPopover.opened = true;
9999
await nextRender();
100100

101101
expect(popover.opened).to.be.true;
@@ -105,11 +105,11 @@ describe('nested popover', () => {
105105
target.focus();
106106
await nextRender();
107107

108-
secondPopover.modal = true;
109-
await nextUpdate(secondPopover);
108+
nestedPopover.modal = true;
109+
await nextUpdate(nestedPopover);
110110

111-
secondTarget.focus();
112-
secondTarget.click();
111+
nestedTarget.focus();
112+
nestedTarget.click();
113113
await nextRender();
114114

115115
expect(popover.opened).to.be.true;
@@ -119,8 +119,8 @@ describe('nested popover', () => {
119119
target.focus();
120120
await nextRender();
121121

122-
secondTarget.focus();
123-
secondTarget.click();
122+
nestedTarget.focus();
123+
nestedTarget.click();
124124
await nextRender();
125125

126126
outsideClick();
@@ -136,15 +136,15 @@ describe('nested popover', () => {
136136
await nextUpdate(popover);
137137
});
138138

139-
it('should not close when mouse leaves the target if popover is not the last one', async () => {
139+
it('should not close when mouse leaves the target to nested popover', async () => {
140140
mouseenter(target);
141141
await nextRender();
142142

143-
mouseleave(target, secondTarget);
144-
secondTarget.click();
143+
mouseleave(target, nestedTarget);
144+
nestedTarget.click();
145145
await nextRender();
146146

147-
mouseleave(secondTarget, target);
147+
mouseleave(nestedTarget, target);
148148

149149
mouseenter(target);
150150
mouseleave(target);
@@ -153,18 +153,117 @@ describe('nested popover', () => {
153153
expect(popover.opened).to.be.true;
154154
});
155155

156-
it('should not close when mouse leaves the overlay if popover is not the last one', async () => {
156+
it('should not close when mouse leaves the overlay to nested popover', async () => {
157157
mouseenter(target);
158158
await nextRender();
159159

160-
mouseleave(target, secondTarget);
161-
secondTarget.click();
160+
mouseleave(target, nestedTarget);
161+
nestedTarget.click();
162162
await nextRender();
163163

164-
mouseleave(secondTarget);
164+
mouseleave(nestedTarget);
165165
await nextUpdate(popover);
166166

167167
expect(popover.opened).to.be.true;
168168
});
169169
});
170170
});
171+
172+
describe('not nested popover', () => {
173+
let popover, popoverContent, target, secondPopover, secondPopoverContent, secondTarget;
174+
175+
beforeEach(async () => {
176+
popover = fixtureSync('<vaadin-popover></vaadin-popover>');
177+
target = fixtureSync('<button>Target</button>');
178+
popover.target = target;
179+
popover.renderer = (root) => {
180+
root.innerHTML = '<button>Popover content</button>';
181+
popoverContent = root.firstElementChild;
182+
};
183+
184+
secondPopover = fixtureSync('<vaadin-popover></vaadin-popover>');
185+
secondTarget = fixtureSync('<button>Second Target</button>');
186+
secondPopover.target = secondTarget;
187+
secondPopover.trigger = [];
188+
secondPopover.renderer = (root) => {
189+
root.innerHTML = '<button>Second Popover content</button>';
190+
secondPopoverContent = root.firstElementChild;
191+
};
192+
await nextRender();
193+
});
194+
195+
describe('focus', () => {
196+
beforeEach(() => {
197+
popover.trigger = ['focus'];
198+
});
199+
200+
it('should close the popover when focus moves from target to non-nested popover', async () => {
201+
target.focus();
202+
await nextRender();
203+
204+
// open second popover
205+
secondPopover.opened = true;
206+
await nextRender();
207+
208+
secondPopoverContent.focus();
209+
210+
expect(popover.opened).to.be.false;
211+
});
212+
213+
it('should close when focus moves from the overlay to non-nested popover', async () => {
214+
target.focus();
215+
await nextRender();
216+
217+
popoverContent.focus();
218+
await nextRender();
219+
expect(popover.opened).to.be.true;
220+
221+
// open second popover
222+
secondPopover.opened = true;
223+
await nextRender();
224+
225+
secondPopoverContent.focus();
226+
await nextRender();
227+
228+
expect(popover.opened).to.be.false;
229+
});
230+
});
231+
232+
describe('hover', () => {
233+
beforeEach(() => {
234+
popover.trigger = ['hover'];
235+
});
236+
237+
it('should close the popover when mouse leaves target to non-nested popover', async () => {
238+
mouseenter(target);
239+
await nextRender();
240+
241+
// open second popover
242+
secondPopover.opened = true;
243+
await nextRender();
244+
245+
mouseleave(target, secondPopoverContent);
246+
await nextUpdate(popover);
247+
248+
expect(popover.opened).to.be.false;
249+
});
250+
251+
it('should close when mouse leaves the overlay to non-nested popover', async () => {
252+
mouseenter(target);
253+
await nextRender();
254+
255+
mouseenter(popoverContent);
256+
await nextRender();
257+
expect(popover.opened).to.be.true;
258+
259+
// open second popover
260+
secondPopover.opened = true;
261+
await nextRender();
262+
263+
mouseleave(popoverContent, secondPopoverContent);
264+
await nextUpdate(popover);
265+
266+
expect(popover.opened).to.be.false;
267+
});
268+
});
269+
});

0 commit comments

Comments
 (0)