From 7e379c8d4cf41f18ac98003972502fb81ce14fc8 Mon Sep 17 00:00:00 2001 From: web-padawan Date: Wed, 22 May 2024 12:12:54 +0300 Subject: [PATCH] refactor: update to use array syntax for trigger property --- packages/popover/src/vaadin-popover.d.ts | 25 +- packages/popover/src/vaadin-popover.js | 62 ++-- packages/popover/test/trigger.test.js | 296 +++++++++--------- .../popover/test/typings/popover.types.ts | 2 +- 4 files changed, 198 insertions(+), 187 deletions(-) diff --git a/packages/popover/src/vaadin-popover.d.ts b/packages/popover/src/vaadin-popover.d.ts index 2e731904f9e..84cb677ca75 100644 --- a/packages/popover/src/vaadin-popover.d.ts +++ b/packages/popover/src/vaadin-popover.d.ts @@ -13,7 +13,7 @@ export type { PopoverPosition } from './vaadin-popover-position-mixin.js'; export type PopoverRenderer = (root: HTMLElement, popover: Popover) => void; -export type PopoverTrigger = 'click' | 'hover-or-click' | 'hover-or-focus' | 'manual'; +export type PopoverTrigger = 'click' | 'focus' | 'hover'; /** * Fired when the `opened` property changes. @@ -77,19 +77,24 @@ declare class Popover extends PopoverPositionMixin( noCloseOnEsc: boolean; /** - * Used to configure the way how the popover overlay is opened or closed, based on - * the user interactions with the target element. + * Popover trigger mode, used to configure the way how the overlay is opened or closed. + * Could be set to multiple by providing an array, e.g. `trigger = ['hover', 'focus']`. * * Supported values: - * - `click` (default) - opens on target click, closes on outside click and Escape - * - `hover-or-click` - opens on target mouseenter, closes on target mouseleave. - * Also opens on click but only in case it originates from the keyboard (Enter / Space). - * - `hover-or-focus` - opens on mouseenter and focus, closes on mouseleave and blur - * - `manual` - only can be opened by setting `opened` property on the host + * - `click` (default) - opens and closes on target click. + * - `hover` - opens on target mouseenter, closes on target mouseleave. Moving mouse + * to the popover overlay content keeps the overlay opened. + * - `focus` - opens on target focus, closes on target blur. Moving focus to the + * popover overlay content keeps the overlay opened. * - * Note: moving mouse or focus inside the popover overlay content does not close it. + * In addition to the behavior specified by `trigger`, the popover can be closed by: + * - pressing Escape key (unless `noCloseOnEsc` property is true) + * - outside click (unless `noCloseOnOutsideClick` property is true) + * + * When setting `trigger` property to `null`, `undefined` or empty array, the popover + * can be only opened or closed programmatically by changing `opened` property. */ - trigger: PopoverTrigger; + trigger: PopoverTrigger[] | null | undefined; /** * When true, the overlay has a backdrop (modality curtain) on top of the diff --git a/packages/popover/src/vaadin-popover.js b/packages/popover/src/vaadin-popover.js index 421d3896713..2bc225a3aff 100644 --- a/packages/popover/src/vaadin-popover.js +++ b/packages/popover/src/vaadin-popover.js @@ -6,7 +6,6 @@ import './vaadin-popover-overlay.js'; import { html, LitElement } from 'lit'; import { ifDefined } from 'lit/directives/if-defined.js'; -import { isKeyboardActive } from '@vaadin/a11y-base/src/focus-utils.js'; import { defineCustomElement } from '@vaadin/component-base/src/define.js'; import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js'; import { OverlayClassMixin } from '@vaadin/component-base/src/overlay-class-mixin.js'; @@ -95,21 +94,26 @@ class Popover extends PopoverPositionMixin( }, /** - * Used to configure the way how the popover overlay is opened or closed, based on - * the user interactions with the target element. + * Popover trigger mode, used to configure the way how the overlay is opened or closed. + * Could be set to multiple by providing an array, e.g. `trigger = ['hover', 'focus']`. * * Supported values: - * - `click` (default) - opens on target click, closes on outside click and Escape - * - `hover-or-click` - opens on target mouseenter, closes on target mouseleave. - * Also opens on click but only in case it originates from the keyboard (Enter / Space). - * - `hover-or-focus` - opens on mouseenter and focus, closes on mouseleave and blur - * - `manual` - only can be opened by setting `opened` property on the host + * - `click` (default) - opens and closes on target click. + * - `hover` - opens on target mouseenter, closes on target mouseleave. Moving mouse + * to the popover overlay content keeps the overlay opened. + * - `focus` - opens on target focus, closes on target blur. Moving focus to the + * popover overlay content keeps the overlay opened. * - * Note: moving mouse or focus inside the popover overlay content does not close it. + * In addition to the behavior specified by `trigger`, the popover can be closed by: + * - pressing Escape key (unless `noCloseOnEsc` property is true) + * - outside click (unless `noCloseOnOutsideClick` property is true) + * + * When setting `trigger` property to `null`, `undefined` or empty array, the popover + * can be only opened or closed programmatically by changing `opened` property. */ trigger: { - type: String, - value: 'click', + type: Array, + value: () => ['click'], }, /** @@ -160,7 +164,7 @@ class Popover extends PopoverPositionMixin( @focusin="${this.__onOverlayFocusin}" @focusout="${this.__onOverlayFocusout}" @opened-changed="${this.__onOpenedChanged}" - .restoreFocusOnClose="${this.trigger === 'click'}" + .restoreFocusOnClose="${this.__isTrigger('click') && this.trigger.length === 1}" .restoreFocusNode="${this.target}" @vaadin-overlay-escape-press="${this.__onEscapePress}" @vaadin-overlay-outside-click="${this.__onOutsideClick}" @@ -241,7 +245,7 @@ class Popover extends PopoverPositionMixin( __onGlobalClick(event) { if ( this.opened && - this.trigger !== 'manual' && + !this.__isManual() && !this.modal && !event.composedPath().some((el) => el === this._overlayElement || el === this.target) && !this.noCloseOnOutsideClick @@ -252,14 +256,14 @@ class Popover extends PopoverPositionMixin( /** @private */ __onTargetClick() { - if (this.trigger === 'click' || (this.trigger === 'hover-or-click' && isKeyboardActive())) { + if (this.__isTrigger('click')) { this.opened = !this.opened; } } /** @private */ __onTargetKeydown(event) { - if (event.key === 'Escape' && !this.noCloseOnEsc && this.opened && this.trigger !== 'manual') { + if (event.key === 'Escape' && !this.noCloseOnEsc && this.opened && !this.__isManual()) { // Prevent closing parent overlay (e.g. dialog) event.stopPropagation(); this.opened = false; @@ -270,7 +274,7 @@ class Popover extends PopoverPositionMixin( __onTargetFocusin() { this.__focusInside = true; - if (this.trigger === 'hover-or-focus') { + if (this.__isTrigger('focus')) { this.opened = true; } } @@ -288,7 +292,7 @@ class Popover extends PopoverPositionMixin( __onTargetMouseEnter() { this.__hoverInside = true; - if (this.trigger === 'hover-or-click' || this.trigger === 'hover-or-focus') { + if (this.__isTrigger('hover')) { this.opened = true; } } @@ -334,7 +338,11 @@ class Popover extends PopoverPositionMixin( __handleFocusout() { this.__focusInside = false; - if (this.trigger === 'hover-or-focus' && !this.__hoverInside) { + if (this.__isTrigger('hover') && this.__hoverInside) { + return; + } + + if (this.__isTrigger('focus')) { this.opened = false; } } @@ -343,11 +351,11 @@ class Popover extends PopoverPositionMixin( __handleMouseLeave() { this.__hoverInside = false; - if (this.trigger === 'hover-or-focus' && this.__focusInside) { + if (this.__isTrigger('focus') && this.__focusInside) { return; } - if (this.trigger === 'hover-or-click' || this.trigger === 'hover-or-focus') { + if (this.__isTrigger('hover')) { this.opened = false; } } @@ -362,7 +370,7 @@ class Popover extends PopoverPositionMixin( * @private */ __onEscapePress(e) { - if (this.noCloseOnEsc || this.trigger === 'manual') { + if (this.noCloseOnEsc || this.__isManual()) { e.preventDefault(); } } @@ -372,10 +380,20 @@ class Popover extends PopoverPositionMixin( * @private */ __onOutsideClick(e) { - if (this.noCloseOnOutsideClick || this.trigger === 'manual') { + if (this.noCloseOnOutsideClick || this.__isManual()) { e.preventDefault(); } } + + /** @private */ + __isTrigger(trigger) { + return Array.isArray(this.trigger) && this.trigger.includes(trigger); + } + + /** @private */ + __isManual() { + return this.trigger == null || (Array.isArray(this.trigger) && this.trigger.length === 0); + } } defineCustomElement(Popover); diff --git a/packages/popover/test/trigger.test.js b/packages/popover/test/trigger.test.js index 1ec341820a5..aa6cda99576 100644 --- a/packages/popover/test/trigger.test.js +++ b/packages/popover/test/trigger.test.js @@ -41,7 +41,7 @@ describe('trigger', () => { describe('click', () => { beforeEach(() => { - popover.trigger = 'click'; + popover.trigger = ['click']; }); it('should open on target click', async () => { @@ -76,9 +76,9 @@ describe('trigger', () => { }); }); - describe('hover-or-click', () => { + describe('hover', () => { beforeEach(async () => { - popover.trigger = 'hover-or-click'; + popover.trigger = ['hover']; await nextUpdate(popover); }); @@ -133,48 +133,11 @@ describe('trigger', () => { expect(overlay.opened).to.be.true; }); - - it('should not open on target click from mouse', async () => { - mousedown(target); - enter(target); - target.click(); - await nextRender(); - expect(overlay.opened).to.be.true; - }); - - it('should open on target click from keyboard', async () => { - enter(target); - target.click(); - await nextRender(); - expect(overlay.opened).to.be.true; - }); - - it('should not close on target click from mouse', async () => { - mouseenter(target); - await nextRender(); - - mousedown(target); - target.click(); - await nextRender(); - - expect(overlay.opened).to.be.true; - }); - - it('should close on target click from keyboard', async () => { - mouseenter(target); - await nextRender(); - - enter(target); - target.click(); - await nextRender(); - - expect(overlay.opened).to.be.false; - }); }); - describe('hover-or-focus', () => { + describe('focus', () => { beforeEach(async () => { - popover.trigger = 'hover-or-focus'; + popover.trigger = ['focus']; await nextUpdate(popover); }); @@ -182,12 +145,6 @@ describe('trigger', () => { expect(overlay.restoreFocusOnClose).to.be.false; }); - it('should open on target mouseenter', async () => { - mouseenter(target); - await nextRender(); - expect(overlay.opened).to.be.true; - }); - it('should open on target focusin', async () => { focusin(target); await nextRender(); @@ -203,48 +160,6 @@ describe('trigger', () => { expect(overlay.opened).to.be.false; }); - it('should not close on target focusout if target has hover', async () => { - focusin(target); - await nextRender(); - - mouseenter(target); - focusout(target); - await nextUpdate(popover); - expect(overlay.opened).to.be.true; - }); - - it('should close on target focusout if target has lost hover', async () => { - focusin(target); - await nextRender(); - - mouseenter(target); - mouseleave(target); - focusout(target); - await nextUpdate(popover); - expect(overlay.opened).to.be.false; - }); - - it('should not close on target focusout if overlay has hover', async () => { - focusin(target); - await nextRender(); - - mouseenter(overlay); - focusout(target); - await nextUpdate(popover); - expect(overlay.opened).to.be.true; - }); - - it('should close on target focusout if overlay has lost hover', async () => { - focusin(target); - await nextRender(); - - mouseenter(overlay); - mouseleave(overlay); - focusout(target); - await nextUpdate(popover); - expect(overlay.opened).to.be.false; - }); - it('should not close on target focusout to the overlay', async () => { focusin(target); await nextRender(); @@ -282,121 +197,194 @@ describe('trigger', () => { await nextUpdate(popover); expect(overlay.opened).to.be.true; }); + }); - it('should not close on target mouseleave if target has focus', async () => { - mouseenter(target); - await nextRender(); - - focusin(target); - mouseleave(target); + describe('hover and focus', () => { + beforeEach(async () => { + popover.trigger = ['hover', 'focus']; await nextUpdate(popover); - expect(overlay.opened).to.be.true; }); - it('should not close on target mouseleave if overlay has focus', async () => { + it('should open on target mouseenter', async () => { mouseenter(target); await nextRender(); - - focusin(overlay); - mouseleave(target); - await nextUpdate(popover); expect(overlay.opened).to.be.true; }); - it('should not close on overlay mouseleave if overlay has focus', async () => { - mouseenter(target); + it('should open on target focusin', async () => { + focusin(target); await nextRender(); - - focusin(overlay); - mouseenter(overlay); - mouseleave(overlay); - await nextUpdate(popover); expect(overlay.opened).to.be.true; }); - }); - describe('manual', () => { - beforeEach(async () => { - popover.trigger = 'manual'; - await nextUpdate(popover); - }); + it('should not close on target focusout if target has hover', async () => { + focusin(target); + await nextRender(); - it('should set restoreFocusOnClose to false', () => { - expect(overlay.restoreFocusOnClose).to.be.false; + mouseenter(target); + focusout(target); + await nextUpdate(popover); + expect(overlay.opened).to.be.true; }); - it('should not open on target click', async () => { - target.click(); + it('should close on target focusout if target has lost hover', async () => { + focusin(target); await nextRender(); - expect(overlay.opened).to.be.false; - }); - it('should not open on target mouseenter', async () => { mouseenter(target); - await nextRender(); + mouseleave(target); + focusout(target); + await nextUpdate(popover); expect(overlay.opened).to.be.false; }); - it('should open on setting opened to true', async () => { - popover.opened = true; - await nextRender(); - expect(overlay.opened).to.be.true; - }); - - it('should not close on global Escape press', async () => { - popover.opened = true; + it('should not close on target focusout if overlay has hover', async () => { + focusin(target); await nextRender(); - esc(document.body); + mouseenter(overlay); + focusout(target); await nextUpdate(popover); expect(overlay.opened).to.be.true; }); - it('should not close on target Escape press', async () => { - popover.opened = true; + it('should close on target focusout if overlay has lost hover', async () => { + focusin(target); await nextRender(); - esc(target); + mouseenter(overlay); + mouseleave(overlay); + focusout(target); await nextUpdate(popover); - expect(overlay.opened).to.be.true; + expect(overlay.opened).to.be.false; }); - it('should not close on global Escape press when modal', async () => { - popover.modal = true; - popover.opened = true; + it('should not close on target mouseleave if target has focus', async () => { + mouseenter(target); await nextRender(); - esc(document.body); + focusin(target); + mouseleave(target); await nextUpdate(popover); expect(overlay.opened).to.be.true; }); - it('should not close on outside click when not modal', async () => { - popover.opened = true; + it('should not close on target mouseleave if overlay has focus', async () => { + mouseenter(target); await nextRender(); - outsideClick(); + focusin(overlay); + mouseleave(target); await nextUpdate(popover); expect(overlay.opened).to.be.true; }); - it('should not close on outside click when modal', async () => { - popover.modal = true; - popover.opened = true; + it('should not close on overlay mouseleave if overlay has focus', async () => { + mouseenter(target); await nextRender(); - outsideClick(); + focusin(overlay); + mouseenter(overlay); + mouseleave(overlay); await nextUpdate(popover); expect(overlay.opened).to.be.true; }); + }); - it('should close when setting opened to false', async () => { - popover.opened = true; - await nextRender(); - - popover.opened = false; - await nextUpdate(popover); - expect(overlay.opened).to.be.false; + describe('manual', () => { + [null, undefined, []].forEach((value) => { + const trigger = Array.isArray(value) ? 'empty array' : value; + + describe(`trigger set to ${trigger}`, () => { + beforeEach(async () => { + popover.trigger = value; + await nextUpdate(popover); + }); + + it(`should set restoreFocusOnClose to false with trigger set to ${trigger}`, () => { + expect(overlay.restoreFocusOnClose).to.be.false; + }); + + it(`should not open on target click with trigger set to ${trigger}`, async () => { + target.click(); + await nextRender(); + expect(overlay.opened).to.be.false; + }); + + it(`should not open on target mouseenter with trigger set to ${trigger}`, async () => { + mouseenter(target); + await nextRender(); + expect(overlay.opened).to.be.false; + }); + + it(`should not open on target focusin with trigger set to ${trigger}`, async () => { + focusin(target); + await nextRender(); + expect(overlay.opened).to.be.false; + }); + + it(`should open on setting opened to true with trigger set to ${trigger}`, async () => { + popover.opened = true; + await nextRender(); + expect(overlay.opened).to.be.true; + }); + + it(`should not close on global Escape press with trigger set to ${trigger}`, async () => { + popover.opened = true; + await nextRender(); + + esc(document.body); + await nextUpdate(popover); + expect(overlay.opened).to.be.true; + }); + + it(`should not close on target Escape press with trigger set to ${trigger}`, async () => { + popover.opened = true; + await nextRender(); + + esc(target); + await nextUpdate(popover); + expect(overlay.opened).to.be.true; + }); + + it(`should not close on global Escape press when modal with trigger set to ${trigger}`, async () => { + popover.modal = true; + popover.opened = true; + await nextRender(); + + esc(document.body); + await nextUpdate(popover); + expect(overlay.opened).to.be.true; + }); + + it(`should not close on outside click when not modal with trigger set to ${trigger}`, async () => { + popover.opened = true; + await nextRender(); + + outsideClick(); + await nextUpdate(popover); + expect(overlay.opened).to.be.true; + }); + + it(`should not close on outside click when modal with trigger set to ${trigger}`, async () => { + popover.modal = true; + popover.opened = true; + await nextRender(); + + outsideClick(); + await nextUpdate(popover); + expect(overlay.opened).to.be.true; + }); + + it(`should close when setting opened to false with trigger set to ${trigger}`, async () => { + popover.opened = true; + await nextRender(); + + popover.opened = false; + await nextUpdate(popover); + expect(overlay.opened).to.be.false; + }); + }); }); }); }); diff --git a/packages/popover/test/typings/popover.types.ts b/packages/popover/test/typings/popover.types.ts index 314f01613ff..71fc1a03cfe 100644 --- a/packages/popover/test/typings/popover.types.ts +++ b/packages/popover/test/typings/popover.types.ts @@ -27,7 +27,7 @@ assertType(popover.for); assertType(popover.target); assertType(popover.position); assertType(popover.renderer); -assertType(popover.trigger); +assertType(popover.trigger); assertType(popover.overlayClass); assertType(popover.opened); assertType(popover.modal);