Skip to content

Commit d23ceda

Browse files
refactor: update popover to use global overlay listeners (#9972) (#10392)
Co-authored-by: Serhii Kulykov <[email protected]>
1 parent ac11076 commit d23ceda

File tree

5 files changed

+122
-47
lines changed

5 files changed

+122
-47
lines changed

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

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -203,8 +203,25 @@ export const OverlayMixin = (superClass) =>
203203
}
204204
}
205205

206+
/**
207+
* Whether to add global listeners for closing on outside click.
208+
* By default, listeners are not added for a modeless overlay.
209+
*
210+
* @return {boolean}
211+
* @protected
212+
*/
213+
_shouldAddGlobalListeners() {
214+
return !this.modeless;
215+
}
216+
206217
/** @private */
207218
_addGlobalListeners() {
219+
if (this.__hasGlobalListeners) {
220+
return;
221+
}
222+
223+
this.__hasGlobalListeners = true;
224+
208225
document.addEventListener('mousedown', this._boundMouseDownListener);
209226
document.addEventListener('mouseup', this._boundMouseUpListener);
210227
// Firefox leaks click to document on contextmenu even if prevented
@@ -214,6 +231,12 @@ export const OverlayMixin = (superClass) =>
214231

215232
/** @private */
216233
_removeGlobalListeners() {
234+
if (!this.__hasGlobalListeners) {
235+
return;
236+
}
237+
238+
this.__hasGlobalListeners = false;
239+
217240
document.removeEventListener('mousedown', this._boundMouseDownListener);
218241
document.removeEventListener('mouseup', this._boundMouseUpListener);
219242
document.documentElement.removeEventListener('click', this._boundOutsideClickListener, true);
@@ -247,13 +270,20 @@ export const OverlayMixin = (superClass) =>
247270

248271
/** @private */
249272
_modelessChanged(modeless) {
273+
if (this.opened) {
274+
// Add / remove listeners if modeless is changed while opened
275+
if (this._shouldAddGlobalListeners()) {
276+
this._addGlobalListeners();
277+
} else {
278+
this._removeGlobalListeners();
279+
}
280+
}
281+
250282
if (!modeless) {
251283
if (this.opened) {
252-
this._addGlobalListeners();
253284
this._enterModalState();
254285
}
255286
} else {
256-
this._removeGlobalListeners();
257287
this._exitModalState();
258288
}
259289
}
@@ -275,7 +305,7 @@ export const OverlayMixin = (superClass) =>
275305

276306
document.addEventListener('keydown', this._boundKeydownListener);
277307

278-
if (!this.modeless) {
308+
if (this._shouldAddGlobalListeners()) {
279309
this._addGlobalListeners();
280310
}
281311
} else if (wasOpened) {
@@ -290,7 +320,7 @@ export const OverlayMixin = (superClass) =>
290320

291321
document.removeEventListener('keydown', this._boundKeydownListener);
292322

293-
if (!this.modeless) {
323+
if (this._shouldAddGlobalListeners()) {
294324
this._removeGlobalListeners();
295325
}
296326
}
@@ -473,7 +503,7 @@ export const OverlayMixin = (superClass) =>
473503
}
474504

475505
// Only close modeless overlay on Esc press when it contains focus
476-
if (this.modeless && !event.composedPath().includes(this.$.overlay)) {
506+
if (!this._shouldAddGlobalListeners() && !event.composedPath().includes(this.$.overlay)) {
477507
return;
478508
}
479509

packages/overlay/test/interactions.test.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,13 @@ describe('interactions', () => {
115115
expect(overlay.opened).to.be.false;
116116
});
117117

118+
it('should not close on outside click when modeless', () => {
119+
overlay.modeless = true;
120+
click(parent);
121+
122+
expect(overlay.opened).to.be.true;
123+
});
124+
118125
it('should close on backdrop click', () => {
119126
overlay.withBackdrop = true;
120127

@@ -168,6 +175,30 @@ describe('interactions', () => {
168175

169176
expect(overlay.opened).to.be.true;
170177
});
178+
179+
it('should not fire the event on outside click when modeless set to true', () => {
180+
overlay.modeless = true;
181+
182+
const spy = sinon.spy();
183+
overlay.addEventListener('vaadin-overlay-outside-click', spy);
184+
185+
click(parent);
186+
187+
expect(spy).to.be.not.called;
188+
});
189+
190+
it('should not fire the event on outside click when modeless set back to false', () => {
191+
overlay.modeless = true;
192+
193+
const spy = sinon.spy();
194+
overlay.addEventListener('vaadin-overlay-outside-click', spy);
195+
196+
overlay.modeless = false;
197+
198+
click(parent);
199+
200+
expect(spy.calledOnce).to.be.true;
201+
});
171202
});
172203

173204
describe('vaadin-overlay-close event', () => {

packages/popover/src/vaadin-popover-overlay.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,17 @@ class PopoverOverlay extends PopoverOverlayMixin(DirMixin(ThemableMixin(PolylitM
123123
}
124124
}
125125
}
126+
127+
/**
128+
* Override method from `OverlayMixin` to always add outside
129+
* click listener so that it can be used by modeless popover.
130+
* @return {boolean}
131+
* @protected
132+
* @override
133+
*/
134+
_shouldAddGlobalListeners() {
135+
return true;
136+
}
126137
}
127138

128139
defineCustomElement(PopoverOverlay);

packages/popover/src/vaadin-popover.js

Lines changed: 1 addition & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -468,7 +468,6 @@ class Popover extends PopoverPositionMixin(
468468

469469
this.__overlayId = `vaadin-popover-${generateUniqueId()}`;
470470

471-
this.__onGlobalClick = this.__onGlobalClick.bind(this);
472471
this.__onGlobalKeyDown = this.__onGlobalKeyDown.bind(this);
473472
this.__onTargetClick = this.__onTargetClick.bind(this);
474473
this.__onTargetFocusIn = this.__onTargetFocusIn.bind(this);
@@ -539,19 +538,10 @@ class Popover extends PopoverPositionMixin(
539538
this._overlayElement = this.$[this.__overlayId];
540539
}
541540

542-
/** @protected */
543-
connectedCallback() {
544-
super.connectedCallback();
545-
546-
document.documentElement.addEventListener('click', this.__onGlobalClick, true);
547-
}
548-
549541
/** @protected */
550542
disconnectedCallback() {
551543
super.disconnectedCallback();
552544

553-
document.documentElement.removeEventListener('click', this.__onGlobalClick, true);
554-
555545
// Automatically close popover when it is removed from DOM
556546
// Avoid closing if the popover is just moved in the DOM
557547
queueMicrotask(() => {
@@ -623,23 +613,6 @@ class Popover extends PopoverPositionMixin(
623613
}
624614
}
625615

626-
/**
627-
* Overlay's global outside click listener doesn't work when
628-
* the overlay is modeless, so we use a separate listener.
629-
* @private
630-
*/
631-
__onGlobalClick(event) {
632-
if (
633-
this.opened &&
634-
!this.modal &&
635-
!event.composedPath().some((el) => el === this._overlayElement || el === this.target) &&
636-
!this.noCloseOnOutsideClick &&
637-
isLastOverlay(this._overlayElement)
638-
) {
639-
this._openedStateController.close(true);
640-
}
641-
}
642-
643616
/** @private */
644617
__onTargetClick() {
645618
if (this.__hasTrigger('click')) {
@@ -660,17 +633,11 @@ class Popover extends PopoverPositionMixin(
660633
* @private
661634
*/
662635
__onGlobalKeyDown(event) {
663-
// Modal popover uses overlay logic for Esc key and focus trap.
636+
// Modal popover uses overlay logic focus trap.
664637
if (this.modal) {
665638
return;
666639
}
667640

668-
if (event.key === 'Escape' && !this.noCloseOnEsc && this.opened && isLastOverlay(this._overlayElement)) {
669-
// Prevent closing parent overlay (e.g. dialog)
670-
event.stopPropagation();
671-
this._openedStateController.close(true);
672-
}
673-
674641
// Include popover content in the Tab order after the target.
675642
if (event.key === 'Tab') {
676643
if (event.shiftKey) {

packages/popover/test/basic.test.js

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,31 @@ describe('popover', () => {
295295
expect(overlay.opened).to.be.true;
296296
});
297297

298+
it('should not close on outside click if overlay close event is prevented', async () => {
299+
target.click();
300+
await oneEvent(overlay, 'vaadin-overlay-open');
301+
302+
document.addEventListener('vaadin-overlay-close', (e) => e.preventDefault(), { once: true });
303+
304+
outsideClick();
305+
await nextRender();
306+
expect(overlay.opened).to.be.true;
307+
});
308+
309+
it('should not close on outside click if overlay close event is prevented when modal', async () => {
310+
popover.modal = true;
311+
await nextUpdate(popover);
312+
313+
target.click();
314+
await oneEvent(overlay, 'vaadin-overlay-open');
315+
316+
document.addEventListener('vaadin-overlay-close', (e) => e.preventDefault(), { once: true });
317+
318+
outsideClick();
319+
await nextRender();
320+
expect(overlay.opened).to.be.true;
321+
});
322+
298323
it('should close overlay when popover is detached', async () => {
299324
target.click();
300325
await oneEvent(overlay, 'vaadin-overlay-open');
@@ -315,14 +340,6 @@ describe('popover', () => {
315340
expect(overlay.opened).to.be.true;
316341
});
317342

318-
it('should remove document click listener when popover is detached', async () => {
319-
const spy = sinon.spy(document.documentElement, 'removeEventListener');
320-
popover.remove();
321-
await nextRender();
322-
expect(spy).to.be.called;
323-
expect(spy.firstCall.args[0]).to.equal('click');
324-
});
325-
326343
describe('Escape press', () => {
327344
beforeEach(async () => {
328345
target.click();
@@ -363,6 +380,25 @@ describe('popover', () => {
363380
await nextRender();
364381
expect(overlay.opened).to.be.true;
365382
});
383+
384+
it('should not close on global Escape press if overlay close event was prevented', async () => {
385+
document.addEventListener('vaadin-overlay-close', (e) => e.preventDefault(), { once: true });
386+
387+
esc(document.body);
388+
await nextRender();
389+
expect(overlay.opened).to.be.true;
390+
});
391+
392+
it('should not close on global Escape press if overlay close event was prevented when modal', async () => {
393+
popover.modal = true;
394+
await nextUpdate(popover);
395+
396+
document.addEventListener('vaadin-overlay-close', (e) => e.preventDefault(), { once: true });
397+
398+
esc(document.body);
399+
await nextRender();
400+
expect(overlay.opened).to.be.true;
401+
});
366402
});
367403

368404
describe('nested popovers', () => {

0 commit comments

Comments
 (0)