Skip to content

Commit

Permalink
feat: add focusDelay, hoverDelay and hideDelay to popover (#7440)
Browse files Browse the repository at this point in the history
  • Loading branch information
web-padawan committed May 28, 2024
1 parent f30bd33 commit 64f6954
Show file tree
Hide file tree
Showing 4 changed files with 390 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 @@ -66,6 +66,28 @@ declare class Popover extends PopoverPositionMixin(
*/
contentWidth: string;

/**
* 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
162 changes: 154 additions & 8 deletions packages/popover/src/vaadin-popover.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,113 @@ 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 PopoverOpenedStateController {
constructor(host) {
this.host = host;
}

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

/** @private */
get __focusDelay() {
return this.host.focusDelay || 0;
}

/** @private */
get __hoverDelay() {
return this.host.hoverDelay || 0;
}

/** @private */
get __hideDelay() {
return this.host.hideDelay || 0;
}

/**
* Schedule opening the popover.
* @param {Object} options
*/
open(options = { immediate: false }) {
const { immediate, trigger } = options;
const shouldDelayHover = trigger === 'hover' && this.__hoverDelay > 0;
const shouldDelayFocus = trigger === 'focus' && this.__focusDelay > 0;

if (!immediate && (shouldDelayHover || shouldDelayFocus) && !this.__closeTimeout) {
this.__scheduleOpen(trigger);
} 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 */
__abortOpen() {
if (this.__openTimeout) {
clearTimeout(this.__openTimeout);
this.__openTimeout = null;
}
}

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

/** @private */
__scheduleOpen(trigger) {
this.__abortOpen();

const delay = trigger === 'focus' ? 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 Down Expand Up @@ -78,6 +185,34 @@ class Popover extends PopoverPositionMixin(
type: String,
},

/**
* 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 @@ -212,6 +347,8 @@ class Popover extends PopoverPositionMixin(
this.__onTargetFocusOut = this.__onTargetFocusOut.bind(this);
this.__onTargetMouseEnter = this.__onTargetMouseEnter.bind(this);
this.__onTargetMouseLeave = this.__onTargetMouseLeave.bind(this);

this._openedStateController = new PopoverOpenedStateController(this);
}

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

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

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

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

Expand All @@ -375,7 +512,11 @@ class Popover extends PopoverPositionMixin(
if (!this.opened) {
this.__shouldRestoreFocus = true;
}
this.opened = !this.opened;
if (this.opened) {
this._openedStateController.close(true);
} else {
this._openedStateController.open({ immediate: true });
}
}
}

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

Expand All @@ -415,7 +556,7 @@ class Popover extends PopoverPositionMixin(
// Prevent overlay re-opening when restoring focus on close.
if (!this.__shouldRestoreFocus) {
this.__shouldRestoreFocus = true;
this.opened = true;
this._openedStateController.open({ trigger: 'focus' });
}
}
}
Expand All @@ -438,7 +579,7 @@ class Popover extends PopoverPositionMixin(
if (this.modal) {
this.target.style.pointerEvents = 'auto';
}
this.opened = true;
this._openedStateController.open({ trigger: 'hover' });
}
}

Expand Down Expand Up @@ -474,6 +615,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._openedStateController.isClosing) {
this._openedStateController.open({ immediate: true });
}
}

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

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

Expand All @@ -507,7 +653,7 @@ class Popover extends PopoverPositionMixin(
}

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

Expand Down
Loading

0 comments on commit 64f6954

Please sign in to comment.