Skip to content

Commit

Permalink
feat: add focusDelay, hoverDelay and hideDelay to popover
Browse files Browse the repository at this point in the history
  • Loading branch information
web-padawan committed May 23, 2024
1 parent 74586f5 commit 36e0d9d
Show file tree
Hide file tree
Showing 4 changed files with 383 additions and 8 deletions.
22 changes: 22 additions & 0 deletions packages/popover/src/vaadin-popover.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,28 @@ export type PopoverEventMap = HTMLElementEventMap & PopoverCustomEventMap;
declare class Popover extends PopoverPositionMixin(
PopoverTargetMixin(OverlayClassMixin(ThemePropertyMixin(ElementMixin(HTMLElement)))),
) {
/**
* The delay in milliseconds before the popover is opened
* on focus when the corresponding trigger is used.
* @attr {number} focus-delay
*/
focusDelay: number;

/**
* 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.
* @attr {number} hide-delay
*/
hideDelay: number;

/**
* The delay in milliseconds before the popover is opened
* on hover when the corresponding trigger is used.
* @attr {number} hover-delay
*/
hoverDelay: number;

/**
* True if the popover overlay is opened, false otherwise.
*/
Expand Down
155 changes: 147 additions & 8 deletions packages/popover/src/vaadin-popover.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,106 @@ 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';

/**
* Controller for handling popover opened state.
*/
class PopoverStateController {
constructor(host) {
this.host = host;
}

/**
* Whether closing is currently in progress.
* @return {boolean}
*/
get isClosing() {
return this.__closeTimeout != null;
}

/** @private */
get focusDelay() {
const popover = this.host;
return popover.focusDelay != null && popover.focusDelay > 0 ? popover.focusDelay : 0;
}

/** @private */
get hoverDelay() {
const popover = this.host;
return popover.hoverDelay != null && popover.hoverDelay > 0 ? popover.hoverDelay : 0;
}

/** @private */
get hideDelay() {
const popover = this.host;
return popover.hideDelay != null && popover.hideDelay > 0 ? popover.hideDelay : 0;
}

/**
* Schedule opening the popover.
* @param {Object} options
*/
open(options = { immediate: false }) {
const { immediate, hover, focus } = options;
const isHover = hover && this.hoverDelay > 0;
const isFocus = focus && this.focusDelay > 0;

if (!immediate && (isHover || isFocus) && !this.__closeTimeout) {
this.__scheduleOpen(isFocus);
} else {
this.__showPopover();
}
}

/**
* Schedule closing the popover.
* @param {boolean} immediate
*/
close(immediate) {
if (!immediate && this.hideDelay > 0) {
this.__scheduleClose();
} else {
this.__abortClose();
this.__setOpened(false);
}
}

/** @private */
__setOpened(opened) {
this.host.opened = opened;
}

/** @private */
__showPopover() {
this.__abortClose();
this.__setOpened(true);
}

/** @private */
__abortClose() {
if (this.__closeTimeout) {
clearTimeout(this.__closeTimeout);
this.__closeTimeout = null;
}
}

/** @private */
__scheduleClose() {
this.__closeTimeout = setTimeout(() => {
this.__closeTimeout = null;
this.__setOpened(false);
}, this.hideDelay);
}

/** @private */
__scheduleOpen(isFocus) {
const delay = isFocus ? this.focusDelay : this.hoverDelay;
this.__openTimeout = setTimeout(() => {
this.__openTimeout = null;
this.__showPopover();
}, delay);
}
}

/**
* `<vaadin-popover>` is a Web Component for creating overlays
* that are positioned next to specified DOM element (target).
Expand All @@ -41,6 +141,34 @@ class Popover extends PopoverPositionMixin(

static get properties() {
return {
/**
* The delay in milliseconds before the popover is opened
* on focus when the corresponding trigger is used.
* @attr {number} focus-delay
*/
focusDelay: {
type: Number,
},

/**
* 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.
* @attr {number} hide-delay
*/
hideDelay: {
type: Number,
},

/**
* The delay in milliseconds before the popover is opened
* on hover when the corresponding trigger is used.
* @attr {number} hover-delay
*/
hoverDelay: {
type: Number,
},

/**
* True if the popover overlay is opened, false otherwise.
*/
Expand Down Expand Up @@ -139,6 +267,8 @@ class Popover extends PopoverPositionMixin(
this.__onTargetFocusOut = this.__onTargetFocusOut.bind(this);
this.__onTargetMouseEnter = this.__onTargetMouseEnter.bind(this);
this.__onTargetMouseLeave = this.__onTargetMouseLeave.bind(this);

this._stateController = new PopoverStateController(this);
}

/** @protected */
Expand Down Expand Up @@ -206,7 +336,7 @@ class Popover extends PopoverPositionMixin(

document.removeEventListener('click', this.__onGlobalClick, true);

this.opened = false;
this._stateController.close(true);
}

/**
Expand Down Expand Up @@ -250,14 +380,18 @@ class Popover extends PopoverPositionMixin(
!event.composedPath().some((el) => el === this._overlayElement || el === this.target) &&
!this.noCloseOnOutsideClick
) {
this.opened = false;
this._stateController.close(true);
}
}

/** @private */
__onTargetClick() {
if (this.__hasTrigger('click')) {
this.opened = !this.opened;
if (this.opened) {
this._stateController.close(true);
} else {
this._stateController.open({ immediate: true });
}
}
}

Expand All @@ -266,7 +400,7 @@ class Popover extends PopoverPositionMixin(
if (event.key === 'Escape' && !this.noCloseOnEsc && this.opened && !this.__isManual) {
// Prevent closing parent overlay (e.g. dialog)
event.stopPropagation();
this.opened = false;
this._stateController.close(true);
}
}

Expand All @@ -282,7 +416,7 @@ class Popover extends PopoverPositionMixin(
return;
}

this.opened = true;
this._stateController.open({ focus: true });
}
}

Expand All @@ -300,7 +434,7 @@ class Popover extends PopoverPositionMixin(
this.__hoverInside = true;

if (this.__hasTrigger('hover')) {
this.opened = true;
this._stateController.open({ hover: true });
}
}

Expand Down Expand Up @@ -330,6 +464,11 @@ class Popover extends PopoverPositionMixin(
/** @private */
__onOverlayMouseEnter() {
this.__hoverInside = true;

// Prevent closing if cursor moves to the overlay during hide delay.
if (this.__hasTrigger('hover') && this._stateController.isClosing) {
this._stateController.open({ immediate: true });
}
}

/** @private */
Expand All @@ -350,7 +489,7 @@ class Popover extends PopoverPositionMixin(
}

if (this.__hasTrigger('focus')) {
this.opened = false;
this._stateController.close(true);
}
}

Expand All @@ -363,7 +502,7 @@ class Popover extends PopoverPositionMixin(
}

if (this.__hasTrigger('hover')) {
this.opened = false;
this._stateController.close();
}
}

Expand Down
Loading

0 comments on commit 36e0d9d

Please sign in to comment.