Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(popover): new delay property #487

Merged
merged 15 commits into from
Feb 28, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
27 changes: 25 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 @@ -263,7 +266,7 @@ export class PopoverFoundation extends BaseClass implements IPopoverFoundation {
return;
}
}

window.clearTimeout(this._hoverTimeout);
this._tryRemoveHoverListeners();
this._requestClose('hover');
}
Expand Down Expand Up @@ -304,7 +307,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 +326,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 +536,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
27 changes: 26 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,27 @@ 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);

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

it('should set the default hoverDelay value if NaN', async () => {
const harness = await createFixture({ triggerType: 'hover', hoverDelay: 'Testing' });
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 +1450,7 @@ interface IPopoverFixtureConfig {
animationType?: PopoverAnimationType;
triggerType?: PopoverTriggerType;
persistentHover?: boolean;
hoverDelay?: any;
nickonometry marked this conversation as resolved.
Show resolved Hide resolved
}

async function createFixture({
Expand All @@ -1437,7 +1460,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 +1473,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.delay = coerceNumber(newValue);
nickonometry marked this conversation as resolved.
Show resolved Hide resolved
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;
}
11 changes: 11 additions & 0 deletions src/lib/tooltip/tooltip.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,17 @@ describe('Tooltip', () => {
expect(harness.isOpen).to.be.true;
});

it('should open when hovering the trigger button and a delay is set', async () => {
nickonometry marked this conversation as resolved.
Show resolved Hide resolved
const harness = await createFixture({ triggerType: 'hover', delay: 500 });
nickonometry marked this conversation as resolved.
Show resolved Hide resolved

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

await harness.hoverTrigger();
await timer(harness.tooltipElement.delay);

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

it('should open and close when hovering and unhovering the trigger button', async () => {
const harness = await createFixture({ triggerType: 'hover' });

Expand Down
Loading