Skip to content

Commit 76435e7

Browse files
authored
refactor: update popover to use global overlay listeners (#9972)
1 parent 858c565 commit 76435e7

File tree

5 files changed

+122
-42
lines changed

5 files changed

+122
-42
lines changed

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

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -237,8 +237,25 @@ export const OverlayMixin = (superClass) =>
237237
}
238238
}
239239

240+
/**
241+
* Whether to add global listeners for closing on outside click.
242+
* By default, listeners are not added for a modeless overlay.
243+
*
244+
* @return {boolean}
245+
* @protected
246+
*/
247+
_shouldAddGlobalListeners() {
248+
return !this.modeless;
249+
}
250+
240251
/** @private */
241252
_addGlobalListeners() {
253+
if (this.__hasGlobalListeners) {
254+
return;
255+
}
256+
257+
this.__hasGlobalListeners = true;
258+
242259
document.addEventListener('mousedown', this._boundMouseDownListener);
243260
document.addEventListener('mouseup', this._boundMouseUpListener);
244261
// Firefox leaks click to document on contextmenu even if prevented
@@ -248,6 +265,12 @@ export const OverlayMixin = (superClass) =>
248265

249266
/** @private */
250267
_removeGlobalListeners() {
268+
if (!this.__hasGlobalListeners) {
269+
return;
270+
}
271+
272+
this.__hasGlobalListeners = false;
273+
251274
document.removeEventListener('mousedown', this._boundMouseDownListener);
252275
document.removeEventListener('mouseup', this._boundMouseUpListener);
253276
document.documentElement.removeEventListener('click', this._boundOutsideClickListener, true);
@@ -281,13 +304,20 @@ export const OverlayMixin = (superClass) =>
281304

282305
/** @private */
283306
_modelessChanged(modeless) {
307+
if (this.opened) {
308+
// Add / remove listeners if modeless is changed while opened
309+
if (this._shouldAddGlobalListeners()) {
310+
this._addGlobalListeners();
311+
} else {
312+
this._removeGlobalListeners();
313+
}
314+
}
315+
284316
if (!modeless) {
285317
if (this.opened) {
286-
this._addGlobalListeners();
287318
this._enterModalState();
288319
}
289320
} else {
290-
this._removeGlobalListeners();
291321
this._exitModalState();
292322
}
293323
setOverlayStateAttribute(this, 'modeless', modeless);
@@ -326,7 +356,7 @@ export const OverlayMixin = (superClass) =>
326356

327357
document.addEventListener('keydown', this._boundKeydownListener);
328358

329-
if (!this.modeless) {
359+
if (this._shouldAddGlobalListeners()) {
330360
this._addGlobalListeners();
331361
}
332362
} else if (wasOpened) {
@@ -341,7 +371,7 @@ export const OverlayMixin = (superClass) =>
341371

342372
document.removeEventListener('keydown', this._boundKeydownListener);
343373

344-
if (!this.modeless) {
374+
if (this._shouldAddGlobalListeners()) {
345375
this._removeGlobalListeners();
346376
}
347377
}
@@ -522,7 +552,7 @@ export const OverlayMixin = (superClass) =>
522552
}
523553

524554
// Only close modeless overlay on Esc press when it contains focus
525-
if (this.modeless && !event.composedPath().includes(this.$.overlay)) {
555+
if (!this._shouldAddGlobalListeners() && !event.composedPath().includes(this.$.overlay)) {
526556
return;
527557
}
528558

packages/overlay/test/interactions.test.js

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

101+
it('should not close on outside click when modeless', () => {
102+
overlay.modeless = true;
103+
click(parent);
104+
105+
expect(overlay.opened).to.be.true;
106+
});
107+
101108
it('should close on backdrop click', () => {
102109
overlay.withBackdrop = true;
103110

@@ -151,6 +158,30 @@ describe('interactions', () => {
151158

152159
expect(overlay.opened).to.be.true;
153160
});
161+
162+
it('should not fire the event on outside click when modeless set to true', () => {
163+
overlay.modeless = true;
164+
165+
const spy = sinon.spy();
166+
overlay.addEventListener('vaadin-overlay-outside-click', spy);
167+
168+
click(parent);
169+
170+
expect(spy).to.be.not.called;
171+
});
172+
173+
it('should not fire the event on outside click when modeless set back to false', () => {
174+
overlay.modeless = true;
175+
176+
const spy = sinon.spy();
177+
overlay.addEventListener('vaadin-overlay-outside-click', spy);
178+
179+
overlay.modeless = false;
180+
181+
click(parent);
182+
183+
expect(spy.calledOnce).to.be.true;
184+
});
154185
});
155186

156187
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
@@ -75,6 +75,17 @@ class PopoverOverlay extends PopoverOverlayMixin(
7575
get _modalRoot() {
7676
return this.owner;
7777
}
78+
79+
/**
80+
* Override method from `OverlayMixin` to always add outside
81+
* click listener so that it can be used by modeless popover.
82+
* @return {boolean}
83+
* @protected
84+
* @override
85+
*/
86+
_shouldAddGlobalListeners() {
87+
return true;
88+
}
7889
}
7990

8091
defineCustomElement(PopoverOverlay);

packages/popover/src/vaadin-popover.js

Lines changed: 1 addition & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -461,7 +461,6 @@ class Popover extends PopoverPositionMixin(
461461

462462
this.__generatedId = `vaadin-popover-${generateUniqueId()}`;
463463

464-
this.__onGlobalClick = this.__onGlobalClick.bind(this);
465464
this.__onGlobalKeyDown = this.__onGlobalKeyDown.bind(this);
466465
this.__onTargetClick = this.__onTargetClick.bind(this);
467466
this.__onTargetFocusIn = this.__onTargetFocusIn.bind(this);
@@ -574,16 +573,12 @@ class Popover extends PopoverPositionMixin(
574573
if (!this.id) {
575574
this.id = this.__generatedId;
576575
}
577-
578-
document.documentElement.addEventListener('click', this.__onGlobalClick, true);
579576
}
580577

581578
/** @protected */
582579
disconnectedCallback() {
583580
super.disconnectedCallback();
584581

585-
document.documentElement.removeEventListener('click', this.__onGlobalClick, true);
586-
587582
// Automatically close popover when it is removed from DOM
588583
// Avoid closing if the popover is just moved in the DOM
589584
queueMicrotask(() => {
@@ -655,23 +650,6 @@ class Popover extends PopoverPositionMixin(
655650
}
656651
}
657652

658-
/**
659-
* Overlay's global outside click listener doesn't work when
660-
* the overlay is modeless, so we use a separate listener.
661-
* @private
662-
*/
663-
__onGlobalClick(event) {
664-
if (
665-
this.opened &&
666-
!this.modal &&
667-
!event.composedPath().some((el) => el === this._overlayElement || el === this.target) &&
668-
!this.noCloseOnOutsideClick &&
669-
isLastOverlay(this._overlayElement)
670-
) {
671-
this._openedStateController.close(true);
672-
}
673-
}
674-
675653
/** @private */
676654
__onTargetClick() {
677655
if (this.__hasTrigger('click')) {
@@ -692,17 +670,11 @@ class Popover extends PopoverPositionMixin(
692670
* @private
693671
*/
694672
__onGlobalKeyDown(event) {
695-
// Modal popover uses overlay logic for Esc key and focus trap.
673+
// Modal popover uses overlay logic focus trap.
696674
if (this.modal) {
697675
return;
698676
}
699677

700-
if (event.key === 'Escape' && !this.noCloseOnEsc && this.opened && isLastOverlay(this._overlayElement)) {
701-
// Prevent closing parent overlay (e.g. dialog)
702-
event.stopPropagation();
703-
this._openedStateController.close(true);
704-
}
705-
706678
// Include popover content in the Tab order after the target.
707679
if (event.key === 'Tab') {
708680
if (event.shiftKey) {

packages/popover/test/basic.test.js

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

303+
it('should not close on outside click if overlay close event is prevented', async () => {
304+
target.click();
305+
await oneEvent(overlay, 'vaadin-overlay-open');
306+
307+
document.addEventListener('vaadin-overlay-close', (e) => e.preventDefault(), { once: true });
308+
309+
outsideClick();
310+
await nextRender();
311+
expect(overlay.opened).to.be.true;
312+
});
313+
314+
it('should not close on outside click if overlay close event is prevented when modal', async () => {
315+
popover.modal = true;
316+
await nextUpdate(popover);
317+
318+
target.click();
319+
await oneEvent(overlay, 'vaadin-overlay-open');
320+
321+
document.addEventListener('vaadin-overlay-close', (e) => e.preventDefault(), { once: true });
322+
323+
outsideClick();
324+
await nextRender();
325+
expect(overlay.opened).to.be.true;
326+
});
327+
303328
it('should close overlay when popover is detached', async () => {
304329
target.click();
305330
await oneEvent(overlay, 'vaadin-overlay-open');
@@ -320,14 +345,6 @@ describe('popover', () => {
320345
expect(overlay.opened).to.be.true;
321346
});
322347

323-
it('should remove document click listener when popover is detached', async () => {
324-
const spy = sinon.spy(document.documentElement, 'removeEventListener');
325-
popover.remove();
326-
await nextRender();
327-
expect(spy).to.be.called;
328-
expect(spy.firstCall.args[0]).to.equal('click');
329-
});
330-
331348
describe('Escape press', () => {
332349
beforeEach(async () => {
333350
target.click();
@@ -368,6 +385,25 @@ describe('popover', () => {
368385
await nextRender();
369386
expect(overlay.opened).to.be.true;
370387
});
388+
389+
it('should not close on global Escape press if overlay close event was prevented', async () => {
390+
document.addEventListener('vaadin-overlay-close', (e) => e.preventDefault(), { once: true });
391+
392+
esc(document.body);
393+
await nextRender();
394+
expect(overlay.opened).to.be.true;
395+
});
396+
397+
it('should not close on global Escape press if overlay close event was prevented when modal', async () => {
398+
popover.modal = true;
399+
await nextUpdate(popover);
400+
401+
document.addEventListener('vaadin-overlay-close', (e) => e.preventDefault(), { once: true });
402+
403+
esc(document.body);
404+
await nextRender();
405+
expect(overlay.opened).to.be.true;
406+
});
371407
});
372408

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

0 commit comments

Comments
 (0)