Skip to content

Commit 3ade965

Browse files
refactor: do not propagate overlay events through shadow roots (#9855)
* refactor: do not propagate overlay events through the DOM * fix test * improve integration test * fix rte visual test * allow open and close events to propagate to document --------- Co-authored-by: Serhii Kulykov <iamkulykov@gmail.com>
1 parent 19b42ee commit 3ade965

File tree

9 files changed

+156
-46
lines changed

9 files changed

+156
-46
lines changed

packages/context-menu/test/helpers.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,16 @@ export function activateItem(target, event = isTouch ? 'click' : 'mouseover') {
77
}
88

99
export async function openMenu(target, event = isTouch ? 'click' : 'mouseover') {
10-
const menu = target.closest('vaadin-context-menu');
10+
// Try to find a submenu first
11+
let menu;
12+
const overlayContent = target.closest('[slot="overlay"]');
13+
if (overlayContent) {
14+
menu = overlayContent.parentElement.querySelector('[slot="submenu"]');
15+
}
16+
// If no submenu is found, use the closest context menu
17+
if (!menu) {
18+
menu = target.closest('vaadin-context-menu');
19+
}
1120
// Disable logic that delays opening submenu
1221
menu.__openListenerActive = true;
1322
activateItem(target, event);

packages/multi-select-combo-box/test/selecting-items.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ describe('selecting items', () => {
163163
await sendMouse({ type: 'click', position: [400, 400] });
164164

165165
await sendKeys({ press: 'ArrowDown' });
166-
await oneEvent(comboBox, 'vaadin-overlay-open');
166+
await oneEvent(comboBox._overlayElement, 'vaadin-overlay-open');
167167

168168
const item = getFirstItem(comboBox);
169169
expect(item.hasAttribute('focused')).to.be.false;

packages/notification/test/notification.test.js

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -136,34 +136,6 @@ describe('vaadin-notification', () => {
136136
expect(hidePopoverSpy.calledBefore(showPopoverSpy)).to.be.true;
137137
});
138138

139-
it('should cancel vaadin-overlay-close events when the source event occurred within the container', (done) => {
140-
listenOnce(document, 'click', (clickEvent) => {
141-
const overlayCloseEvent = new CustomEvent('vaadin-overlay-close', {
142-
cancelable: true,
143-
detail: { sourceEvent: clickEvent },
144-
});
145-
document.dispatchEvent(overlayCloseEvent);
146-
147-
expect(overlayCloseEvent.defaultPrevented).to.be.true;
148-
done();
149-
});
150-
notification._card.click();
151-
});
152-
153-
it('should not cancel vaadin-overlay-close events when the source event occurred outside of the container', (done) => {
154-
listenOnce(document, 'click', (clickEvent) => {
155-
const overlayCloseEvent = new CustomEvent('vaadin-overlay-close', {
156-
cancelable: true,
157-
detail: { sourceEvent: clickEvent },
158-
});
159-
document.dispatchEvent(overlayCloseEvent);
160-
161-
expect(overlayCloseEvent.defaultPrevented).to.be.false;
162-
done();
163-
});
164-
document.body.click();
165-
});
166-
167139
(isIOS ? describe : describe.skip)('iOS incorrect viewport height workaround', () => {
168140
let container;
169141

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

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -178,13 +178,17 @@ export const OverlayMixin = (superClass) =>
178178
* @param {Event=} sourceEvent
179179
*/
180180
close(sourceEvent) {
181-
const evt = new CustomEvent('vaadin-overlay-close', {
181+
// Dispatch the event on the overlay. Not using composed, as propagating the event through shadow roots could have
182+
// side effects when nesting overlays
183+
const event = new CustomEvent('vaadin-overlay-close', {
182184
bubbles: true,
183185
cancelable: true,
184186
detail: { sourceEvent },
185187
});
186-
this.dispatchEvent(evt);
187-
if (!evt.defaultPrevented) {
188+
this.dispatchEvent(event);
189+
// To allow listening for the event globally, also dispatch it on the document body
190+
document.body.dispatchEvent(event);
191+
if (!event.defaultPrevented) {
188192
this.opened = false;
189193
}
190194
}
@@ -311,7 +315,12 @@ export const OverlayMixin = (superClass) =>
311315
setTimeout(() => {
312316
this._trapFocus();
313317

314-
this.dispatchEvent(new CustomEvent('vaadin-overlay-open', { bubbles: true, composed: true }));
318+
// Dispatch the event on the overlay. Not using composed, as propagating the event through shadow roots
319+
// could have side effects when nesting overlays
320+
const event = new CustomEvent('vaadin-overlay-open', { bubbles: true });
321+
this.dispatchEvent(event);
322+
// To allow listening for the event globally, also dispatch it on the document body
323+
document.body.dispatchEvent(event);
315324
});
316325
});
317326

@@ -493,7 +502,6 @@ export const OverlayMixin = (superClass) =>
493502
}
494503

495504
const evt = new CustomEvent('vaadin-overlay-outside-click', {
496-
bubbles: true,
497505
cancelable: true,
498506
detail: { sourceEvent: event },
499507
});
@@ -520,7 +528,6 @@ export const OverlayMixin = (superClass) =>
520528

521529
if (event.key === 'Escape') {
522530
const evt = new CustomEvent('vaadin-overlay-escape-press', {
523-
bubbles: true,
524531
cancelable: true,
525532
detail: { sourceEvent: event },
526533
});

packages/overlay/test/basic.test.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,34 @@ describe('vaadin-overlay', () => {
3535
await aTimeout(0);
3636
expect(spy).to.not.be.called;
3737
});
38+
39+
it('should not propagate through shadow roots', async () => {
40+
overlay.opened = true;
41+
await nextFrame();
42+
await aTimeout(0);
43+
44+
expect(spy.firstCall.args[0].composed).to.be.false;
45+
});
46+
47+
describe('global', () => {
48+
let globalSpy;
49+
50+
beforeEach(() => {
51+
globalSpy = sinon.spy();
52+
document.addEventListener('vaadin-overlay-open', globalSpy);
53+
});
54+
55+
afterEach(() => {
56+
document.removeEventListener('vaadin-overlay-open', globalSpy);
57+
});
58+
59+
it('should fire a global event on the document body when opened', async () => {
60+
overlay.opened = true;
61+
await nextFrame();
62+
await aTimeout(0);
63+
expect(globalSpy).to.be.called;
64+
});
65+
});
3866
});
3967

4068
describe('popover', () => {

packages/overlay/test/interactions.test.js

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { expect } from '@vaadin/chai-plugins';
22
import { resetMouse, sendMouseToElement } from '@vaadin/test-runner-commands';
33
import {
4+
aTimeout,
45
click,
56
enterKeyDown,
67
escKeyDown,
@@ -153,16 +154,42 @@ describe('interactions', () => {
153154
});
154155

155156
describe('vaadin-overlay-close event', () => {
156-
it('should prevent closing the overlay if the event was prevented', (done) => {
157-
overlay.addEventListener('vaadin-overlay-close', (e) => {
158-
e.preventDefault();
159-
160-
setTimeout(() => {
161-
expect(overlay.opened).to.be.true;
162-
done();
163-
}, 1);
164-
});
157+
const preventDefaultListener = (e) => {
158+
e.preventDefault();
159+
};
160+
161+
it('should not propagate through shadow roots', () => {
162+
const spy = sinon.spy();
163+
overlay.addEventListener('vaadin-overlay-close', spy);
164+
165165
click(parent);
166+
167+
expect(spy.firstCall.args[0].composed).to.be.false;
168+
});
169+
170+
it('should prevent closing the overlay if the event was prevented', async () => {
171+
overlay.addEventListener('vaadin-overlay-close', preventDefaultListener);
172+
click(parent);
173+
await aTimeout(1);
174+
175+
expect(overlay.opened).to.be.true;
176+
});
177+
178+
describe('global', () => {
179+
beforeEach(() => {
180+
document.addEventListener('vaadin-overlay-close', preventDefaultListener);
181+
});
182+
183+
afterEach(() => {
184+
document.removeEventListener('vaadin-overlay-close', preventDefaultListener);
185+
});
186+
187+
it('should prevent closing the overlay if the global event was prevented', async () => {
188+
click(parent);
189+
await aTimeout(1);
190+
191+
expect(overlay.opened).to.be.true;
192+
});
166193
});
167194
});
168195

packages/rich-text-editor/src/vaadin-rich-text-editor-popup.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ class RichTextEditorPopup extends PolylitMixin(LitElement) {
7070
render() {
7171
return html`
7272
<vaadin-rich-text-editor-popup-overlay
73+
id="overlay"
7374
.owner="${this}"
7475
.opened="${this.opened}"
7576
.positionTarget="${this.target}"

packages/rich-text-editor/test/visual/base/rich-text-editor.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ describe('rich-text-editor', () => {
103103
it('color popup', async () => {
104104
element.style.minHeight = '200px';
105105
element.shadowRoot.querySelector('[part~="toolbar-button-color"]').click();
106-
await oneEvent(element, 'vaadin-overlay-open');
106+
await oneEvent(element.querySelector('[slot="color-popup"]').$.overlay, 'vaadin-overlay-open');
107107
return visualDiff(div, 'color-popup');
108108
});
109109
});
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { expect } from '@vaadin/chai-plugins';
2+
import { sendKeys } from '@vaadin/test-runner-commands';
3+
import { fixtureSync, nextRender } from '@vaadin/testing-helpers';
4+
import './not-animated-styles.js';
5+
import '@vaadin/dialog/src/vaadin-dialog.js';
6+
import '@vaadin/notification/src/vaadin-notification.js';
7+
8+
describe('notification and dialog', () => {
9+
let dialog, dialogInput, notification, notificationInput;
10+
11+
beforeEach(async () => {
12+
const container = fixtureSync(`
13+
<div>
14+
<vaadin-dialog></vaadin-dialog>
15+
<vaadin-notification></vaadin-notification>
16+
</div>
17+
`);
18+
dialog = container.querySelector('vaadin-dialog');
19+
dialog.renderer = (root) => {
20+
if (root.firstElementChild) {
21+
return;
22+
}
23+
dialogInput = document.createElement('input');
24+
root.append(dialogInput);
25+
};
26+
dialog.opened = true;
27+
28+
notification = container.querySelector('vaadin-notification');
29+
notification.renderer = (root) => {
30+
if (root.firstElementChild) {
31+
return;
32+
}
33+
notificationInput = document.createElement('input');
34+
root.append(notificationInput);
35+
};
36+
notification.opened = true;
37+
38+
await nextRender();
39+
});
40+
41+
it('should prevent the dialog from closing when pressing Escape in the notification', async () => {
42+
notificationInput.focus();
43+
await sendKeys({ press: 'Escape' });
44+
45+
expect(dialog.opened).to.be.true;
46+
});
47+
48+
it('should allow the dialog to close when pressing Escape in the dialog', async () => {
49+
dialogInput.focus();
50+
await sendKeys({ press: 'Escape' });
51+
52+
expect(dialog.opened).to.be.false;
53+
});
54+
55+
it('should prevent the dialog from closing when clicking in the notification', () => {
56+
notificationInput.click();
57+
58+
expect(dialog.opened).to.be.true;
59+
});
60+
61+
it('should allow the dialog to close when clicking outside', () => {
62+
document.body.click();
63+
64+
expect(dialog.opened).to.be.false;
65+
});
66+
});

0 commit comments

Comments
 (0)