Skip to content

Commit

Permalink
feat(popover): new delay property (#487)
Browse files Browse the repository at this point in the history
* feat(popover): added a delay property for the mouseenter event

* chore(popover): added a test for hover delay

* chore(popover): added an ejs demo control for delay

* chore(popover): added delay to the defaults value test

* chore(popover): added hoverTimeout var and added in clearTimeout where needed

* chore(popover): added comment docs for the delay property and attribute

* chore(popover): changed the property/attribute name to hoverDelay

* chore(popover): added a test for the hoverDelay property

* chore(popover): fixing issues brought up in PR comments

* chore(popover): added new tests for NaN and < 0

* chore(tooltip): removed duplicate test

* chore(popover): updated typings issue in the popover test file

* chore(popover): fixed a variable name issue causing a build error

* chore(popover): ensure all timeouts are cleared when element is disconnected

* chore: update test to avoid race condition

---------

Co-authored-by: Nichols, Kieran <kieran.nichols@tylertech.com>
  • Loading branch information
nickonometry and DRiFTy17 authored Feb 28, 2024
1 parent 7790f55 commit c48a748
Show file tree
Hide file tree
Showing 6 changed files with 75 additions and 4 deletions.
1 change: 1 addition & 0 deletions src/dev/pages/popover/popover.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
title: 'Popover',
includePath: './pages/popover/popover.ejs',
options: [
{ type: 'text-field', inputType: 'number', id: 'opt-hover-delay', label: 'Hover Delay', defaultValue: 0 },
{
type: 'select',
label: 'Placement',
Expand Down
3 changes: 3 additions & 0 deletions src/dev/pages/popover/popover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,16 @@ import '@tylertech/forge/checkbox';
import '@tylertech/forge/label';
import './popover.scss';

const delayInput = document.querySelector('#opt-hover-delay') as HTMLInputElement;
const popover = document.querySelector('#my-popover') as IPopoverComponent;
const showPopoverButton = document.querySelector('#popover-trigger') as HTMLButtonElement;
const closeButton = document.querySelector('#close-button') as HTMLButtonElement;
const clippingContainer = document.querySelector('.clipping-container') as HTMLElement;
const preventCloseToggle = document.querySelector('#opt-prevent-close') as ISwitchComponent;
const richTooltipPopover = document.querySelector('#rich-tooltip-popover') as IPopoverComponent;

delayInput.addEventListener('input', (e) => popover.hoverDelay = Number(delayInput.value));

popover.addEventListener('forge-popover-beforetoggle', (evt: CustomEvent<IPopoverToggleEventData>) => {
console.log('forge-popover-beforetoggle', evt.detail);
if (preventCloseToggle.on && evt.detail.newState === 'closed' && evt.cancelable) {
Expand Down
4 changes: 3 additions & 1 deletion src/lib/popover/popover-constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const attributes = {
TRIGGER_TYPE: 'trigger-type',
LONGPRESS_DELAY: 'longpress-delay',
PERSISTENT_HOVER: 'persistent-hover',
HOVER_DELAY: 'hover-delay',
HOVER_DISMISS_DELAY: 'hover-dismiss-delay'
};

Expand All @@ -31,7 +32,8 @@ const events = {
};

const defaults = {
TRIGGER_TYPE: 'click' as PopoverTriggerType
TRIGGER_TYPE: 'click' as PopoverTriggerType,
HOVER_DELAY: 0
};

export const POPOVER_CONSTANTS = {
Expand Down
32 changes: 30 additions & 2 deletions src/lib/popover/popover-foundation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface IPopoverFoundation extends IOverlayAwareFoundation {
longpressDelay: number;
persistentHover: boolean;
hoverDismissDelay: number;
hoverDelay: number;
dispatchBeforeToggleEvent(state: IDismissibleStackState): boolean;
}

Expand All @@ -25,12 +26,14 @@ export class PopoverFoundation extends BaseClass implements IPopoverFoundation {
private _triggerTypes: PopoverTriggerType[] = [POPOVER_CONSTANTS.defaults.TRIGGER_TYPE];
private _persistentHover = false;
private _hoverDismissDelay = POPOVER_HOVER_TIMEOUT;
private _hoverDelay = POPOVER_CONSTANTS.defaults.HOVER_DELAY;
private _previouslyFocusedElement: HTMLElement | null = null;

// Hover trigger state
private _hoverAnchorLeaveTimeout: undefined | number;
private _popoverMouseleaveTimeout: undefined | number;
private _currentHoverCoords: undefined | { x: number; y: number };
private _hoverTimeout: number | undefined;

// Click trigger listeners
private _anchorClickListener = this._onAnchorClick.bind(this);
Expand Down Expand Up @@ -69,6 +72,11 @@ export class PopoverFoundation extends BaseClass implements IPopoverFoundation {

public override destroy(): void {
super.destroy();

window.clearTimeout(this._hoverTimeout);
window.clearTimeout(this._hoverAnchorLeaveTimeout);
window.clearTimeout(this._popoverMouseleaveTimeout);

this._previouslyFocusedElement = null;

if (this.open) {
Expand Down Expand Up @@ -263,7 +271,7 @@ export class PopoverFoundation extends BaseClass implements IPopoverFoundation {
return;
}
}

window.clearTimeout(this._hoverTimeout);
this._tryRemoveHoverListeners();
this._requestClose('hover');
}
Expand Down Expand Up @@ -304,7 +312,13 @@ export class PopoverFoundation extends BaseClass implements IPopoverFoundation {
if (!this._persistentHover) {
this._adapter.addAnchorListener('mouseleave', this._anchorMouseleaveListener);
}
this._openPopover();
if (this._hoverDelay) {
this._hoverTimeout = window.setTimeout(() => {
this._openPopover();
}, this._hoverDelay);
} else {
this._openPopover();
}
}
}

Expand All @@ -317,6 +331,7 @@ export class PopoverFoundation extends BaseClass implements IPopoverFoundation {
*/
private _onAnchorMouseleave(): void {
this._startHoverListeners();
window.clearTimeout(this._hoverTimeout);

this._hoverAnchorLeaveTimeout = window.setTimeout(() => {
this._hoverAnchorLeaveTimeout = undefined;
Expand Down Expand Up @@ -526,6 +541,19 @@ export class PopoverFoundation extends BaseClass implements IPopoverFoundation {
}
}

public get hoverDelay(): number {
return this._hoverDelay;
}
public set hoverDelay(value: number) {
if (isNaN(value) || value < 0) {
value = POPOVER_CONSTANTS.defaults.HOVER_DELAY;
}
if (this._hoverDelay !== value) {
this._hoverDelay = value;
this._adapter.setHostAttribute(POPOVER_CONSTANTS.attributes.HOVER_DELAY, String(this._hoverDelay));
}
}

public get hoverDismissDelay(): number {
return this._hoverDismissDelay;
}
Expand Down
29 changes: 28 additions & 1 deletion src/lib/popover/popover.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ describe('Popover', () => {
expect(harness.popoverElement.triggerType).to.equal('click');
expect(harness.popoverElement.longpressDelay).to.equal(LONGPRESS_TRIGGER_DELAY);
expect(harness.popoverElement.persistentHover).to.be.false;
expect(harness.popoverElement.hoverDelay).to.equal(POPOVER_CONSTANTS.defaults.HOVER_DELAY);
expect(harness.popoverElement.hoverDismissDelay).to.equal(POPOVER_HOVER_TIMEOUT);
});

Expand Down Expand Up @@ -699,6 +700,29 @@ describe('Popover', () => {
expect(harness.isOpen).to.be.false;
});

it('should open with a delay when hovering over the trigger button and a delay is set', async () => {
const harness = await createFixture({ triggerType: 'hover', hoverDelay: 500 });

expect(harness.isOpen).to.be.false;

await harness.hoverTrigger();
await timer(harness.popoverElement.hoverDelay + 100);

expect(harness.isOpen).to.be.true;
});

it('should set the default hoverDelay value if NaN', async () => {
const harness = await createFixture({ triggerType: 'hover', hoverDelay: 'Testing' as any });

expect(harness.popoverElement.hoverDelay).to.equal(0);
});

it('should set the default hoverDelay value if the hoverDelay < 0', async () => {
const harness = await createFixture({ triggerType: 'hover', hoverDelay: -400 });

expect(harness.popoverElement.hoverDelay).to.equal(0);
});

it('should not close if persistent hover is enabled', async () => {
const harness = await createFixture({ triggerType: 'hover', persistentHover: true });

Expand Down Expand Up @@ -1428,6 +1452,7 @@ interface IPopoverFixtureConfig {
animationType?: PopoverAnimationType;
triggerType?: PopoverTriggerType;
persistentHover?: boolean;
hoverDelay?: number;
}

async function createFixture({
Expand All @@ -1437,7 +1462,8 @@ async function createFixture({
anchor,
animationType,
triggerType,
persistentHover = false
persistentHover = false,
hoverDelay
}: IPopoverFixtureConfig = {}): Promise<PopoverHarness> {
const container = await fixture(html`
<div style="display: flex; justify-content: center; align-items: center; height: 300px; width: 300px;">
Expand All @@ -1449,6 +1475,7 @@ async function createFixture({
?persistent=${persistent}
?arrow=${arrow}
?persistent-hover=${persistentHover}
?hoverDelay=${hoverDelay}
animation-type=${animationType ?? nothing}
trigger-type=${triggerType ?? nothing}>
<span>Test popover content</span>
Expand Down
10 changes: 10 additions & 0 deletions src/lib/popover/popover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export interface IPopoverComponent extends IOverlayAware, IDismissible {
triggerType: PopoverTriggerType | PopoverTriggerType[];
longpressDelay: number;
persistentHover: boolean;
hoverDelay: number;
hoverDismissDelay: number;
}

Expand All @@ -41,13 +42,15 @@ declare global {
* @property {number} longpressDelay - The delay in milliseconds before a longpress event is detected.
* @property {boolean} persistentHover - Whether or not the popover should remain open when the user hovers outside the popover.
* @property {number} hoverDismissDelay - The delay in milliseconds before the popover is dismissed when the user hovers outside of the popover.
* @property {number} hoverDelay - The delay in milliseconds before the popover is shown.
*
* @attribute {string} arrow - Whether or not the popover should render an arrow.
* @attribute {string} animation-type - The animation type to use for the popover. Valid values are `'none'`, `'fade'`, `'slide'`, and `'zoom'` (default).
* @attribute {string} trigger-type - The trigger type(s) to use for the popover. Valid values are `'click'` (default), `'hover'`, `'focus'`, and `'longpress'`. Multiple can be specified.
* @attribute {string} longpress-delay - The delay in milliseconds before a longpress event is detected.
* @attribute {string} persistent-hover - Whether or not the popover should remain open when the user hovers outside the popover.
* @attribute {string} hover-dismiss-delay - The delay in milliseconds before the popover is dismissed when the user hovers outside of the popover.
* @attribute {number} hover-delay - The delay in milliseconds before the popover is shown.
*
* @event {CustomEvent<IPopoverToggleEventData} forge-popover-beforetoggle - Dispatches before the popover is toggled, and is cancelable.
* @event {CustomEvent<IPopoverToggleEventData} forge-popover-toggle - Dispatches after the popover is toggled.
Expand Down Expand Up @@ -99,6 +102,7 @@ export class PopoverComponent extends OverlayAware<IPopoverFoundation> implement
POPOVER_CONSTANTS.attributes.TRIGGER_TYPE,
POPOVER_CONSTANTS.attributes.LONGPRESS_DELAY,
POPOVER_CONSTANTS.attributes.PERSISTENT_HOVER,
POPOVER_CONSTANTS.attributes.HOVER_DELAY,
POPOVER_CONSTANTS.attributes.HOVER_DISMISS_DELAY
];
}
Expand Down Expand Up @@ -138,6 +142,9 @@ export class PopoverComponent extends OverlayAware<IPopoverFoundation> implement
case POPOVER_CONSTANTS.attributes.PERSISTENT_HOVER:
this.persistentHover = coerceBoolean(newValue);
return;
case POPOVER_CONSTANTS.attributes.HOVER_DELAY:
this.hoverDelay = coerceNumber(newValue);
return;
case POPOVER_CONSTANTS.attributes.HOVER_DISMISS_DELAY:
this.hoverDismissDelay = coerceNumber(newValue);
return;
Expand All @@ -160,6 +167,9 @@ export class PopoverComponent extends OverlayAware<IPopoverFoundation> implement
@FoundationProperty()
public declare persistentHover: boolean;

@FoundationProperty()
public declare hoverDelay: number;

@FoundationProperty()
public declare hoverDismissDelay: number;
}

0 comments on commit c48a748

Please sign in to comment.