diff --git a/packages/popover/src/vaadin-popover.d.ts b/packages/popover/src/vaadin-popover.d.ts index beb9e5bc26..cf1ed27269 100644 --- a/packages/popover/src/vaadin-popover.d.ts +++ b/packages/popover/src/vaadin-popover.d.ts @@ -77,6 +77,24 @@ export type PopoverEventMap = HTMLElementEventMap & PopoverCustomEventMap; declare class Popover extends PopoverPositionMixin( PopoverTargetMixin(OverlayClassMixin(ThemePropertyMixin(ElementMixin(HTMLElement)))), ) { + /** + * Sets the default focus delay to be used by all popover instances, + * except for those that have focus delay configured using property. + */ + static setDefaultFocusDelay(focusDelay: number): void; + + /** + * Sets the default hide delay to be used by all popover instances, + * except for those that have hide delay configured using property. + */ + static setDefaultHideDelay(hideDelay: number): void; + + /** + * Sets the default hover delay to be used by all popover instances, + * except for those that have hover delay configured using property. + */ + static setDefaultHoverDelay(delay: number): void; + /** * String used to label the overlay to screen reader users. * @@ -114,6 +132,9 @@ declare class Popover extends PopoverPositionMixin( /** * The delay in milliseconds before the popover is opened * on focus when the corresponding trigger is used. + * + * When not specified, the global default (500ms) is used. + * * @attr {number} focus-delay */ focusDelay: number; @@ -122,6 +143,9 @@ declare class Popover extends PopoverPositionMixin( * The delay in milliseconds before the popover is closed * on losing hover, when the corresponding trigger is used. * On blur, the popover is closed immediately. + * + * When not specified, the global default (500ms) is used. + * * @attr {number} hide-delay */ hideDelay: number; @@ -129,6 +153,9 @@ declare class Popover extends PopoverPositionMixin( /** * The delay in milliseconds before the popover is opened * on hover when the corresponding trigger is used. + * + * When not specified, the global default (500ms) is used. + * * @attr {number} hover-delay */ hoverDelay: number; diff --git a/packages/popover/src/vaadin-popover.js b/packages/popover/src/vaadin-popover.js index f572956067..86c442beb5 100644 --- a/packages/popover/src/vaadin-popover.js +++ b/packages/popover/src/vaadin-popover.js @@ -22,6 +22,12 @@ import { ThemePropertyMixin } from '@vaadin/vaadin-themable-mixin/vaadin-theme-p import { PopoverPositionMixin } from './vaadin-popover-position-mixin.js'; import { PopoverTargetMixin } from './vaadin-popover-target-mixin.js'; +const DEFAULT_DELAY = 500; + +let defaultFocusDelay = DEFAULT_DELAY; +let defaultHoverDelay = DEFAULT_DELAY; +let defaultHideDelay = DEFAULT_DELAY; + /** * Controller for handling popover opened state. */ @@ -40,17 +46,20 @@ class PopoverOpenedStateController { /** @private */ get __focusDelay() { - return this.host.focusDelay || 0; + const popover = this.host; + return popover.focusDelay != null && popover.focusDelay > 0 ? popover.focusDelay : defaultFocusDelay; } /** @private */ get __hoverDelay() { - return this.host.hoverDelay || 0; + const popover = this.host; + return popover.hoverDelay != null && popover.hoverDelay > 0 ? popover.hoverDelay : defaultHoverDelay; } /** @private */ get __hideDelay() { - return this.host.hideDelay || 0; + const popover = this.host; + return popover.hideDelay != null && popover.hideDelay > 0 ? popover.hideDelay : defaultHideDelay; } /** @@ -247,6 +256,9 @@ class Popover extends PopoverPositionMixin( /** * The delay in milliseconds before the popover is opened * on focus when the corresponding trigger is used. + * + * When not specified, the global default (500ms) is used. + * * @attr {number} focus-delay */ focusDelay: { @@ -257,6 +269,9 @@ class Popover extends PopoverPositionMixin( * The delay in milliseconds before the popover is closed * on losing hover, when the corresponding trigger is used. * On blur, the popover is closed immediately. + * + * When not specified, the global default (500ms) is used. + * * @attr {number} hide-delay */ hideDelay: { @@ -266,6 +281,9 @@ class Popover extends PopoverPositionMixin( /** * The delay in milliseconds before the popover is opened * on hover when the corresponding trigger is used. + * + * When not specified, the global default (500ms) is used. + * * @attr {number} hover-delay */ hoverDelay: { @@ -393,6 +411,36 @@ class Popover extends PopoverPositionMixin( ]; } + /** + * Sets the default focus delay to be used by all popover instances, + * except for those that have focus delay configured using property. + * + * @param {number} delay + */ + static setDefaultFocusDelay(focusDelay) { + defaultFocusDelay = focusDelay != null && focusDelay >= 0 ? focusDelay : DEFAULT_DELAY; + } + + /** + * Sets the default hide delay to be used by all popover instances, + * except for those that have hide delay configured using property. + * + * @param {number} hideDelay + */ + static setDefaultHideDelay(hideDelay) { + defaultHideDelay = hideDelay != null && hideDelay >= 0 ? hideDelay : DEFAULT_DELAY; + } + + /** + * Sets the default hover delay to be used by all popover instances, + * except for those that have hover delay configured using property. + * + * @param {number} delay + */ + static setDefaultHoverDelay(hoverDelay) { + defaultHoverDelay = hoverDelay != null && hoverDelay >= 0 ? hoverDelay : DEFAULT_DELAY; + } + constructor() { super(); diff --git a/packages/popover/test/a11y.test.js b/packages/popover/test/a11y.test.js index cf09b284a4..3a38f1213d 100644 --- a/packages/popover/test/a11y.test.js +++ b/packages/popover/test/a11y.test.js @@ -12,13 +12,19 @@ import { import { sendKeys } from '@web/test-runner-commands'; import sinon from 'sinon'; import './not-animated-styles.js'; -import '../vaadin-popover.js'; import { getDeepActiveElement } from '@vaadin/a11y-base/src/focus-utils.js'; +import { Popover } from '../vaadin-popover.js'; import { mouseenter, mouseleave } from './helpers.js'; describe('a11y', () => { let popover, target, overlay; + before(() => { + Popover.setDefaultFocusDelay(0); + Popover.setDefaultHoverDelay(0); + Popover.setDefaultHideDelay(0); + }); + beforeEach(async () => { popover = fixtureSync(''); target = fixtureSync(''); diff --git a/packages/popover/test/nested.test.js b/packages/popover/test/nested.test.js index 2ac9c63e8a..7c383aa828 100644 --- a/packages/popover/test/nested.test.js +++ b/packages/popover/test/nested.test.js @@ -1,12 +1,18 @@ 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'; +import { Popover } from '../vaadin-popover.js'; import { mouseenter, mouseleave } from './helpers.js'; describe('nested popover', () => { let popover, target, secondPopover, secondTarget; + before(() => { + Popover.setDefaultFocusDelay(0); + Popover.setDefaultHoverDelay(0); + Popover.setDefaultHideDelay(0); + }); + beforeEach(async () => { popover = fixtureSync(''); target = fixtureSync(''); diff --git a/packages/popover/test/timers.test.js b/packages/popover/test/timers.test.js index cbf813bc9a..f9764daedc 100644 --- a/packages/popover/test/timers.test.js +++ b/packages/popover/test/timers.test.js @@ -1,29 +1,38 @@ import { expect } from '@vaadin/chai-plugins'; -import { - aTimeout, - esc, - fire, - fixtureSync, - focusout, - nextRender, - nextUpdate, - outsideClick, -} from '@vaadin/testing-helpers'; +import { aTimeout, esc, fixtureSync, focusout, nextRender, nextUpdate, outsideClick } from '@vaadin/testing-helpers'; +import sinon from 'sinon'; import './not-animated-styles.js'; -import '../src/vaadin-popover.js'; +import { Popover } from '../src/vaadin-popover.js'; +import { mouseenter, mouseleave } from './helpers.js'; describe('timers', () => { - let popover, target, overlay; + let popover, target, overlay, clock; - function mouseenter(target) { - fire(target, 'mouseenter'); - } + // Used as a fallback delay + const DEFAULT_DELAY = 500; + + async function createPopover(target, focus) { + const element = fixtureSync(''); + element.target = target; + element.trigger = focus ? ['focus'] : ['hover']; - function mouseleave(target, relatedTarget) { - const eventProps = relatedTarget ? { relatedTarget } : {}; - fire(target, 'mouseleave', undefined, eventProps); + // We use fake timers in reset tests, so native timers won't work. + // Trigger a timeout to ensure LitElement popover initial render. + if (clock) { + await clock.tickAsync(1); + } else { + await nextUpdate(element); + } + + return element.shadowRoot.querySelector('vaadin-popover-overlay'); } + before(() => { + Popover.setDefaultFocusDelay(0); + Popover.setDefaultHoverDelay(0); + Popover.setDefaultHideDelay(0); + }); + beforeEach(async () => { popover = fixtureSync(''); popover.renderer = (root) => { @@ -208,4 +217,268 @@ describe('timers', () => { expect(overlay.opened).to.be.false; }); }); + + describe('setDefaultHoverDelay', () => { + let target, overlay; + + beforeEach(() => { + target = fixtureSync('
Target
'); + }); + + afterEach(() => { + Popover.setDefaultHoverDelay(0); + }); + + it('should change default delay for newly created popover', async () => { + Popover.setDefaultHoverDelay(2); + + overlay = await createPopover(target); + + mouseenter(target); + await aTimeout(2); + + expect(overlay.opened).to.be.true; + }); + + it('should change default hover delay for existing popover', async () => { + overlay = await createPopover(target); + + Popover.setDefaultHoverDelay(2); + + mouseenter(target); + await aTimeout(2); + + expect(overlay.opened).to.be.true; + }); + + describe('reset hover delay', () => { + beforeEach(() => { + clock = sinon.useFakeTimers({ + shouldClearNativeTimers: true, + }); + }); + + afterEach(() => { + // Hide tooltip + mouseleave(target); + clock.tick(DEFAULT_DELAY); + + // Reset timers + clock.restore(); + }); + + it('should reset hover delay when providing a negative number', async () => { + Popover.setDefaultHoverDelay(-1); + + overlay = await createPopover(target); + + mouseenter(target); + await clock.tickAsync(DEFAULT_DELAY); + + expect(overlay.opened).to.be.true; + }); + + it('should reset hover delay when providing null instead of number', async () => { + Popover.setDefaultHoverDelay(null); + + overlay = await createPopover(target); + + mouseenter(target); + await clock.tickAsync(DEFAULT_DELAY); + + expect(overlay.opened).to.be.true; + }); + + it('should reset hover delay when providing undefined instead of number', async () => { + Popover.setDefaultHoverDelay(undefined); + + overlay = await createPopover(target); + + mouseenter(target); + await clock.tickAsync(DEFAULT_DELAY); + + expect(overlay.opened).to.be.true; + }); + }); + }); + + describe('setDefaultFocusDelay', () => { + let target, overlay; + + beforeEach(() => { + target = fixtureSync('
Target
'); + }); + + afterEach(() => { + Popover.setDefaultFocusDelay(0); + }); + + it('should change default delay for newly created popover', async () => { + Popover.setDefaultFocusDelay(2); + + overlay = await createPopover(target, true); + + target.focus(); + await aTimeout(2); + + expect(overlay.opened).to.be.true; + }); + + it('should change default focus delay for existing popover', async () => { + overlay = await createPopover(target, true); + + Popover.setDefaultFocusDelay(2); + + target.focus(); + await aTimeout(2); + + expect(overlay.opened).to.be.true; + }); + + describe('reset focus delay', () => { + beforeEach(() => { + clock = sinon.useFakeTimers({ + shouldClearNativeTimers: true, + }); + }); + + afterEach(() => { + // Hide tooltip + focusout(target); + clock.tick(DEFAULT_DELAY); + + // Reset timers + clock.restore(); + }); + + it('should reset focus delay when providing a negative number', async () => { + Popover.setDefaultFocusDelay(-1); + + overlay = await createPopover(target, true); + + target.focus(); + await clock.tickAsync(DEFAULT_DELAY); + + expect(overlay.opened).to.be.true; + }); + + it('should reset focus delay when providing null instead of number', async () => { + Popover.setDefaultFocusDelay(null); + + overlay = await createPopover(target, true); + + target.focus(); + await clock.tickAsync(DEFAULT_DELAY); + + expect(overlay.opened).to.be.true; + }); + + it('should reset focus delay when providing undefined instead of number', async () => { + Popover.setDefaultFocusDelay(undefined); + + overlay = await createPopover(target, true); + + target.focus(); + await clock.tickAsync(DEFAULT_DELAY); + + expect(overlay.opened).to.be.true; + }); + }); + }); + + describe('setDefaultHideDelay', () => { + let target, overlay; + + beforeEach(() => { + target = fixtureSync('
Target
'); + }); + + afterEach(() => { + Popover.setDefaultHideDelay(0); + }); + + it('should change default hide delay for newly created popover', async () => { + Popover.setDefaultHideDelay(2); + + overlay = await createPopover(target); + + mouseenter(target); + await nextUpdate(overlay); + expect(overlay.opened).to.be.true; + + mouseleave(target); + await aTimeout(2); + + expect(overlay.opened).to.be.false; + }); + + it('should change default hide delay for existing popover', async () => { + overlay = await createPopover(target); + + Popover.setDefaultHideDelay(2); + + mouseenter(target); + await nextUpdate(overlay); + expect(overlay.opened).to.be.true; + + mouseleave(target); + await aTimeout(2); + + expect(overlay.opened).to.be.false; + }); + + describe('reset hide delay', () => { + beforeEach(() => { + clock = sinon.useFakeTimers({ + shouldClearNativeTimers: true, + }); + }); + + afterEach(() => { + clock.restore(); + }); + + it('should reset hide delay when providing a negative number', async () => { + Popover.setDefaultHideDelay(-1); + + overlay = await createPopover(target); + + mouseenter(target); + await clock.tickAsync(DEFAULT_DELAY); + + mouseleave(target); + await clock.tickAsync(DEFAULT_DELAY); + + expect(overlay.opened).to.be.false; + }); + + it('should reset hide delay when providing null instead of number', async () => { + Popover.setDefaultHideDelay(null); + + overlay = await createPopover(target); + + mouseenter(target); + await clock.tickAsync(DEFAULT_DELAY); + + mouseleave(target); + await clock.tickAsync(DEFAULT_DELAY); + + expect(overlay.opened).to.be.false; + }); + + it('should reset hide delay when providing undefined instead of number', async () => { + Popover.setDefaultHideDelay(undefined); + + overlay = await createPopover(target); + + mouseenter(target); + await clock.tickAsync(DEFAULT_DELAY); + + mouseleave(target); + await clock.tickAsync(DEFAULT_DELAY); + + expect(overlay.opened).to.be.false; + }); + }); + }); }); diff --git a/packages/popover/test/trigger.test.js b/packages/popover/test/trigger.test.js index 002dd1807f..bdc5d82228 100644 --- a/packages/popover/test/trigger.test.js +++ b/packages/popover/test/trigger.test.js @@ -12,12 +12,18 @@ import { } from '@vaadin/testing-helpers'; import { resetMouse, sendKeys, sendMouse } from '@web/test-runner-commands'; import './not-animated-styles.js'; -import '../vaadin-popover.js'; +import { Popover } from '../vaadin-popover.js'; import { mouseenter, mouseleave } from './helpers.js'; describe('trigger', () => { let popover, target, overlay; + before(() => { + Popover.setDefaultFocusDelay(0); + Popover.setDefaultHoverDelay(0); + Popover.setDefaultHideDelay(0); + }); + beforeEach(async () => { popover = fixtureSync(''); target = fixtureSync(''); diff --git a/packages/popover/test/typings/popover.types.ts b/packages/popover/test/typings/popover.types.ts index b629b3b7f8..1052863aec 100644 --- a/packages/popover/test/typings/popover.types.ts +++ b/packages/popover/test/typings/popover.types.ts @@ -1,4 +1,3 @@ -import '../../vaadin-popover.js'; import type { ElementMixinClass } from '@vaadin/component-base/src/element-mixin.js'; import type { OverlayClassMixinClass } from '@vaadin/component-base/src/overlay-class-mixin.js'; import type { ThemePropertyMixinClass } from '@vaadin/vaadin-themable-mixin/vaadin-theme-property-mixin.js'; @@ -10,6 +9,7 @@ import type { PopoverRenderer, PopoverTrigger, } from '../../vaadin-popover.js'; +import { Popover } from '../../vaadin-popover.js'; const assertType = (actual: TExpected) => actual; @@ -53,3 +53,7 @@ popover.addEventListener('opened-changed', (event) => { popover.addEventListener('closed', (event) => { assertType(event); }); + +assertType<(delay: number) => void>(Popover.setDefaultFocusDelay); +assertType<(delay: number) => void>(Popover.setDefaultHideDelay); +assertType<(delay: number) => void>(Popover.setDefaultHoverDelay);