Skip to content

Commit

Permalink
fix: only close popover on focusout if it is the last one (#7661)
Browse files Browse the repository at this point in the history
  • Loading branch information
web-padawan authored Aug 19, 2024
1 parent 9edf652 commit aed2f31
Show file tree
Hide file tree
Showing 2 changed files with 140 additions and 0 deletions.
15 changes: 15 additions & 0 deletions packages/popover/src/vaadin-popover.js
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,13 @@ class Popover extends PopoverPositionMixin(

/** @private */
__onTargetFocusOut(event) {
// Do not close the popover on overlay focusout if it's not the last one.
// This covers the case when focus moves to the nested popover opened
// without focusing parent popover overlay (e.g. using hover trigger).
if (!isLastOverlay(this._overlayElement)) {
return;
}

if ((this.__hasTrigger('focus') && this.__mouseDownInside) || this._overlayElement.contains(event.relatedTarget)) {
return;
}
Expand Down Expand Up @@ -741,6 +748,14 @@ class Popover extends PopoverPositionMixin(

/** @private */
__onOverlayFocusOut(event) {
// Do not close the popover on overlay focusout if it's not the last one.
// This covers the following cases of nested overlay based components:
// 1. Moving focus to the nested overlay (e.g. vaadin-select, vaadin-menu-bar)
// 2. Closing not focused nested overlay on outside (e.g. vaadin-combo-box)
if (!isLastOverlay(this._overlayElement)) {
return;
}

if (
(this.__hasTrigger('focus') && this.__mouseDownInside) ||
event.relatedTarget === this.target ||
Expand Down
125 changes: 125 additions & 0 deletions packages/popover/test/nested.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { expect } from '@vaadin/chai-plugins';
import { esc, fixtureSync, nextRender, nextUpdate, outsideClick } from '@vaadin/testing-helpers';
import './not-animated-styles.js';
import '../vaadin-popover.js';

describe('nested popover', () => {
let popover, target, secondPopover, secondTarget;

beforeEach(async () => {
popover = fixtureSync('<vaadin-popover></vaadin-popover>');
target = fixtureSync('<button>Target</button>');
popover.target = target;
popover.renderer = (root) => {
if (root.firstChild) {
return;
}
root.innerHTML = `
<button id="second-target">Second target</button>
<vaadin-popover for="second-target"></vaadin-popover>
`;
[secondTarget, secondPopover] = root.children;
secondPopover.renderer = (root2) => {
root2.textContent = 'Nested';
};
};
await nextRender();
});

describe('closing', () => {
beforeEach(async () => {
// Open the first popover
target.click();
await nextRender();

// Open the second popover
secondTarget.click();
await nextRender();

// Expect both popovers to be opened
expect(popover.opened).to.be.true;
expect(secondPopover.opened).to.be.true;
});

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

// Expect only the second popover to be closed
expect(popover.opened).to.be.true;
expect(secondPopover.opened).to.be.false;

esc(document.body);
await nextRender();

// Expect both popovers to be closed
expect(popover.opened).to.be.false;
expect(secondPopover.opened).to.be.false;
});

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

// Expect only the second popover to be closed
expect(popover.opened).to.be.true;
expect(secondPopover.opened).to.be.false;

outsideClick();
await nextRender();

// Expect both popovers to be closed
expect(popover.opened).to.be.false;
expect(secondPopover.opened).to.be.false;
});
});

describe('focus', () => {
beforeEach(async () => {
popover.trigger = ['focus'];
await nextUpdate(popover);
});

it('should not close when focus moves from the target to the nested popover', async () => {
target.focus();
await nextRender();

secondPopover.modal = true;
await nextUpdate(secondPopover);

// Open programmatically so focus stays on target
secondPopover.opened = true;
await nextRender();

expect(popover.opened).to.be.true;
});

it('should not close when focus moves from the overlay to the nested popover', async () => {
target.focus();
await nextRender();

secondPopover.modal = true;
await nextUpdate(secondPopover);

secondTarget.focus();
secondTarget.click();
await nextRender();

expect(popover.opened).to.be.true;
});

it('should not close on focusout caused by nested popover outside click', async () => {
target.focus();
await nextRender();

secondTarget.focus();
secondTarget.click();
await nextRender();

outsideClick();
await nextUpdate(popover);

expect(popover.opened).to.be.true;
});
});
});

0 comments on commit aed2f31

Please sign in to comment.