From c9c41af35ebbbed96bdfec100db1638f63197e7b Mon Sep 17 00:00:00 2001 From: Serhii Kulykov Date: Fri, 16 Aug 2024 16:00:47 +0300 Subject: [PATCH] fix: do not close popover on focusout after mousedown inside (#7656) --- packages/popover/src/vaadin-popover.js | 24 ++++++- packages/popover/test/trigger.test.js | 86 +++++++++++++++++++++++++- 2 files changed, 107 insertions(+), 3 deletions(-) diff --git a/packages/popover/src/vaadin-popover.js b/packages/popover/src/vaadin-popover.js index 75268bd92a..ec4808ba2c 100644 --- a/packages/popover/src/vaadin-popover.js +++ b/packages/popover/src/vaadin-popover.js @@ -424,6 +424,7 @@ class Popover extends PopoverPositionMixin( ?no-vertical-overlap="${this.__computeNoVerticalOverlap(effectivePosition)}" .horizontalAlign="${this.__computeHorizontalAlign(effectivePosition)}" .verticalAlign="${this.__computeVerticalAlign(effectivePosition)}" + @mousedown="${this.__onOverlayMouseDown}" @mouseenter="${this.__onOverlayMouseEnter}" @mouseleave="${this.__onOverlayMouseLeave}" @focusin="${this.__onOverlayFocusIn}" @@ -692,7 +693,7 @@ class Popover extends PopoverPositionMixin( /** @private */ __onTargetFocusOut(event) { - if (this._overlayElement.contains(event.relatedTarget)) { + if ((this.__hasTrigger('focus') && this.__mouseDownInside) || this._overlayElement.contains(event.relatedTarget)) { return; } @@ -734,13 +735,32 @@ class Popover extends PopoverPositionMixin( /** @private */ __onOverlayFocusOut(event) { - if (event.relatedTarget === this.target || this._overlayElement.contains(event.relatedTarget)) { + if ( + (this.__hasTrigger('focus') && this.__mouseDownInside) || + event.relatedTarget === this.target || + this._overlayElement.contains(event.relatedTarget) + ) { return; } this.__handleFocusout(); } + /** @private */ + __onOverlayMouseDown() { + if (this.__hasTrigger('focus')) { + this.__mouseDownInside = true; + + document.addEventListener( + 'mouseup', + () => { + this.__mouseDownInside = false; + }, + { once: true }, + ); + } + } + /** @private */ __onOverlayMouseEnter() { this.__hoverInside = true; diff --git a/packages/popover/test/trigger.test.js b/packages/popover/test/trigger.test.js index 0215242efd..002dd1807f 100644 --- a/packages/popover/test/trigger.test.js +++ b/packages/popover/test/trigger.test.js @@ -1,5 +1,16 @@ import { expect } from '@vaadin/chai-plugins'; -import { esc, fixtureSync, focusin, focusout, nextRender, nextUpdate, outsideClick } from '@vaadin/testing-helpers'; +import { + esc, + fixtureSync, + focusin, + focusout, + middleOfNode, + mousedown, + nextRender, + nextUpdate, + outsideClick, +} from '@vaadin/testing-helpers'; +import { resetMouse, sendKeys, sendMouse } from '@web/test-runner-commands'; import './not-animated-styles.js'; import '../vaadin-popover.js'; import { mouseenter, mouseleave } from './helpers.js'; @@ -14,6 +25,10 @@ describe('trigger', () => { popover.renderer = (root) => { if (!root.firstChild) { root.appendChild(document.createElement('input')); + + const div = document.createElement('div'); + div.textContent = 'Some text content'; + root.appendChild(div); } }; await nextRender(); @@ -184,6 +199,73 @@ describe('trigger', () => { await nextRender(); expect(overlay.opened).to.be.true; }); + + describe('overlay mousedown', () => { + let input; + + beforeEach(async () => { + input = document.createElement('input'); + target.parentNode.appendChild(input); + + target.focus(); + await nextRender(); + }); + + afterEach(async () => { + input.remove(); + await resetMouse(); + }); + + it('should not close on overlay mousedown when target has focus', async () => { + const { x, y } = middleOfNode(overlay.querySelector('div')); + await sendMouse({ type: 'click', position: [Math.round(x), Math.round(y)] }); + await nextUpdate(); + + expect(overlay.opened).to.be.true; + }); + + it('should not close on overlay mousedown when overlay has focus', async () => { + overlay.querySelector('input').focus(); + + const { x, y } = middleOfNode(overlay.querySelector('div')); + await sendMouse({ type: 'click', position: [Math.round(x), Math.round(y)] }); + await nextUpdate(); + + expect(overlay.opened).to.be.true; + }); + + it('should only cancel one target focusout after the overlay mousedown', async () => { + // Remove the input so that first Tab would leave popover + overlay.querySelector('input').remove(); + + const { x, y } = middleOfNode(overlay.querySelector('div')); + await sendMouse({ type: 'click', position: [Math.round(x), Math.round(y)] }); + await nextUpdate(); + + // Tab to focus input next to the target + await sendKeys({ press: 'Tab' }); + + // Ensure the flag for ignoring next focusout was cleared + expect(overlay.opened).to.be.false; + }); + + it('should only cancel one overlay focusout after the overlay mousedown', async () => { + overlay.querySelector('input').focus(); + + const { x, y } = middleOfNode(overlay.querySelector('div')); + await sendMouse({ type: 'click', position: [Math.round(x), Math.round(y)] }); + await nextUpdate(); + + // Tab to focus input inside the popover + await sendKeys({ press: 'Tab' }); + + // Tab to focus input next to the target + await sendKeys({ press: 'Tab' }); + + // Ensure the flag for ignoring next focusout was cleared + expect(overlay.opened).to.be.false; + }); + }); }); describe('hover and focus', () => { @@ -285,6 +367,7 @@ describe('trigger', () => { }); it('should not immediately close on target click when opened on focusin', async () => { + mousedown(target); target.focus(); target.click(); await nextRender(); @@ -292,6 +375,7 @@ describe('trigger', () => { }); it('should close on target click after a delay when opened on focusin', async () => { + mousedown(target); target.focus(); target.click(); await nextRender();