Skip to content
This repository has been archived by the owner on Jan 13, 2025. It is now read-only.

Commit

Permalink
feat(tooltip): Add 500ms delay before showing tooltip.
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 328255505
  • Loading branch information
sayris authored and copybara-github committed Aug 25, 2020
1 parent 238216f commit a1c6559
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 24 deletions.
1 change: 1 addition & 0 deletions packages/mdc-tooltip/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const numbers = {
UNBOUNDED_ANCHOR_GAP: 8,
MIN_VIEWPORT_TOOLTIP_THRESHOLD: 32,
HIDE_DELAY_MS: 600,
SHOW_DELAY_MS: 500,
};

const events = {
Expand Down
29 changes: 27 additions & 2 deletions packages/mdc-tooltip/foundation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,11 @@ export class MDCTooltipFoundation extends MDCFoundation<MDCTooltipAdapter> {
private readonly minViewportTooltipThreshold =
numbers.MIN_VIEWPORT_TOOLTIP_THRESHOLD;
private readonly hideDelayMs = numbers.HIDE_DELAY_MS;
private readonly showDelayMs = numbers.SHOW_DELAY_MS;

private frameId: number|null = null;
private hideTimeout: number|null = null;
private showTimeout: number|null = null;
private readonly documentClickHandler: SpecificEventListener<'click'>;
private readonly documentKeydownHandler: SpecificEventListener<'keydown'>;

Expand All @@ -79,17 +81,30 @@ export class MDCTooltipFoundation extends MDCFoundation<MDCTooltipAdapter> {
}

handleAnchorMouseEnter() {
this.show();
if (this.isShown) {
// Covers the instance where a user hovers over the anchor to reveal the
// tooltip, and then quickly navigates away and then back to the anchor.
// The tooltip should stay visible without animating out and then back in
// again.
this.show();
} else {
this.showTimeout = setTimeout(() => {
this.show();
}, this.showDelayMs);
}
}

handleAnchorFocus() {
// TODO(b/157075286): Need to add some way to distinguish keyboard
// navigation focus events from other focus events, and only show the
// tooltip on the former of these events.
this.show();
this.showTimeout = setTimeout(() => {
this.show();
}, this.showDelayMs);
}

handleAnchorMouseLeave() {
this.clearShowTimeout();
this.hideTimeout = setTimeout(() => {
this.hide();
}, this.hideDelayMs);
Expand All @@ -115,6 +130,7 @@ export class MDCTooltipFoundation extends MDCFoundation<MDCTooltipAdapter> {

show() {
this.clearHideTimeout();
this.clearShowTimeout();

if (this.isShown) {
return;
Expand Down Expand Up @@ -145,6 +161,7 @@ export class MDCTooltipFoundation extends MDCFoundation<MDCTooltipAdapter> {

hide() {
this.clearHideTimeout();
this.clearShowTimeout();

if (!this.isShown) {
return;
Expand Down Expand Up @@ -418,6 +435,13 @@ export class MDCTooltipFoundation extends MDCFoundation<MDCTooltipAdapter> {
return yPos + tooltipHeight <= viewportHeight && yPos >= 0;
}

private clearShowTimeout() {
if (this.showTimeout) {
clearTimeout(this.showTimeout);
this.showTimeout = null;
}
}

private clearHideTimeout() {
if (this.hideTimeout) {
clearTimeout(this.hideTimeout);
Expand All @@ -432,6 +456,7 @@ export class MDCTooltipFoundation extends MDCFoundation<MDCTooltipAdapter> {
}

this.clearHideTimeout();
this.clearShowTimeout();

this.adapter.removeClass(SHOWN);
this.adapter.removeClass(SHOWING_TRANSITION);
Expand Down
6 changes: 3 additions & 3 deletions packages/mdc-tooltip/test/component.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {getFixture} from '../../../testing/dom';
import {emitEvent} from '../../../testing/dom/events';
import {createMockFoundation} from '../../../testing/helpers/foundation';
import {setUpMdcTestEnvironment} from '../../../testing/helpers/setup';
import {AnchorBoundaryType, XPosition, YPosition} from '../constants';
import {AnchorBoundaryType, numbers, XPosition, YPosition} from '../constants';
import {MDCTooltip, MDCTooltipFoundation} from '../index';

function setupTestWithMockFoundation(fixture: HTMLElement) {
Expand Down Expand Up @@ -185,7 +185,7 @@ describe('MDCTooltip', () => {
MDCTooltip.attachTo(tooltipElem);

emitEvent(anchorElem, 'mouseenter');
jasmine.clock().tick(1);
jasmine.clock().tick(numbers.SHOW_DELAY_MS);
expect(tooltipElem.getAttribute('aria-hidden')).toEqual('false');
});

Expand All @@ -212,7 +212,7 @@ describe('MDCTooltip', () => {
MDCTooltip.attachTo(tooltipElem);

emitEvent(anchorElem, 'mouseenter');
jasmine.clock().tick(1);
jasmine.clock().tick(numbers.SHOW_DELAY_MS);
expect(tooltipElem.getAttribute('aria-hidden')).toEqual('true');
});
});
148 changes: 129 additions & 19 deletions packages/mdc-tooltip/test/foundation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -335,41 +335,151 @@ describe('MDCTooltipFoundation', () => {
expect(foundation.hideTimeout).toEqual(null);
});

it(`#handleAnchorBlur hides the tooltip immediately`,
it(`#handleAnchorBlur hides the tooltip immediately`, () => {
const {foundation, mockAdapter} = setUpFoundationTest(MDCTooltipFoundation);
foundation.show();
foundation.handleAnchorBlur();

expect(foundation.hideTimeout).toEqual(null);
expect(mockAdapter.addClass).toHaveBeenCalledWith(CssClasses.HIDE);
expect(mockAdapter.addClass)
.toHaveBeenCalledWith(CssClasses.HIDE_TRANSITION);
expect(mockAdapter.removeClass).toHaveBeenCalledWith(CssClasses.SHOWN);
expect(mockAdapter.removeClass)
.toHaveBeenCalledWith(CssClasses.SHOWING_TRANSITION);

expect(foundation.hideTimeout).toEqual(null);
});

it(`#handleClick hides the tooltip immediately`, () => {
const {foundation, mockAdapter} = setUpFoundationTest(MDCTooltipFoundation);
foundation.show();
foundation.handleClick();

expect(foundation.hideTimeout).toEqual(null);
expect(mockAdapter.addClass).toHaveBeenCalledWith(CssClasses.HIDE);
expect(mockAdapter.addClass)
.toHaveBeenCalledWith(CssClasses.HIDE_TRANSITION);
expect(mockAdapter.removeClass).toHaveBeenCalledWith(CssClasses.SHOWN);
expect(mockAdapter.removeClass)
.toHaveBeenCalledWith(CssClasses.SHOWING_TRANSITION);
expect(foundation.hideTimeout).toEqual(null);
});

it(`#handleAnchorMouseEnter shows the tooltip after a ${
numbers.SHOW_DELAY_MS}ms delay`,
() => {
const {foundation, mockAdapter} =
setUpFoundationTest(MDCTooltipFoundation);
foundation.show();
foundation.handleAnchorBlur();
foundation.handleAnchorMouseEnter();
expect(foundation.showTimeout).not.toEqual(null);

expect(foundation.hideTimeout).toEqual(null);
expect(mockAdapter.addClass).toHaveBeenCalledWith(CssClasses.HIDE);
expect(mockAdapter.addClass)
.toHaveBeenCalledWith(CssClasses.HIDE_TRANSITION);
expect(mockAdapter.removeClass).toHaveBeenCalledWith(CssClasses.SHOWN);
expect(mockAdapter.removeClass)
.toHaveBeenCalledWith(CssClasses.SHOWING_TRANSITION);
jasmine.clock().tick(numbers.SHOW_DELAY_MS);
expect(mockAdapter.setAttribute)
.toHaveBeenCalledWith('aria-hidden', 'false');
expect(mockAdapter.removeClass).toHaveBeenCalledWith(CssClasses.HIDE);
expect(mockAdapter.addClass).toHaveBeenCalledWith(CssClasses.SHOWING);
expect(foundation.showTimeout).toEqual(null);
});

expect(foundation.hideTimeout).toEqual(null);
it(`#handleAnchorFocus shows the tooltip after a ${
numbers.SHOW_DELAY_MS}ms delay`,
() => {
const {foundation, mockAdapter} =
setUpFoundationTest(MDCTooltipFoundation);
foundation.handleAnchorFocus();
expect(foundation.showTimeout).not.toEqual(null);

jasmine.clock().tick(numbers.SHOW_DELAY_MS);
expect(mockAdapter.setAttribute)
.toHaveBeenCalledWith('aria-hidden', 'false');
expect(mockAdapter.removeClass).toHaveBeenCalledWith(CssClasses.HIDE);
expect(mockAdapter.addClass).toHaveBeenCalledWith(CssClasses.SHOWING);
expect(foundation.showTimeout).toEqual(null);
});

it(`#handleClick hides the tooltip immediately`,
it(`does not re-animate a tooltip already shown in the dom (from focus)`,
() => {
const {foundation, mockAdapter} =
setUpFoundationTest(MDCTooltipFoundation);
foundation.show();
foundation.handleClick();
foundation.handleAnchorFocus();
jasmine.clock().tick(numbers.SHOW_DELAY_MS);

expect(foundation.hideTimeout).toEqual(null);
expect(mockAdapter.addClass).toHaveBeenCalledWith(CssClasses.HIDE);
expect(mockAdapter.addClass)
foundation.handleAnchorMouseLeave();
jasmine.clock().tick(numbers.HIDE_DELAY_MS / 2);
foundation.handleAnchorFocus();

expect(mockAdapter.setAttribute)
.toHaveBeenCalledWith('aria-hidden', 'false');
expect(mockAdapter.setAttribute).toHaveBeenCalledTimes(1);
expect(mockAdapter.removeClass).toHaveBeenCalledWith(CssClasses.HIDE);
expect(mockAdapter.removeClass)
.toHaveBeenCalledWith(CssClasses.SHOWING_TRANSITION);
expect(mockAdapter.removeClass)
.toHaveBeenCalledWith(CssClasses.HIDE_TRANSITION);
expect(mockAdapter.removeClass).toHaveBeenCalledWith(CssClasses.SHOWN);
expect(mockAdapter.removeClass).toHaveBeenCalledTimes(3);
expect(mockAdapter.addClass).toHaveBeenCalledWith(CssClasses.SHOWING);
expect(mockAdapter.addClass).toHaveBeenCalledWith(CssClasses.SHOWN);
expect(mockAdapter.addClass)
.toHaveBeenCalledWith(CssClasses.SHOWING_TRANSITION);
expect(mockAdapter.addClass).toHaveBeenCalledTimes(3);
});

it(`does not re-animate a tooltip already shown in the dom (from mouseenter)`,
() => {
const {foundation, mockAdapter} =
setUpFoundationTest(MDCTooltipFoundation);
foundation.handleAnchorMouseEnter();
jasmine.clock().tick(numbers.SHOW_DELAY_MS);

foundation.handleAnchorMouseLeave();
jasmine.clock().tick(numbers.HIDE_DELAY_MS / 2);
foundation.handleAnchorMouseEnter();

expect(mockAdapter.setAttribute)
.toHaveBeenCalledWith('aria-hidden', 'false');
expect(mockAdapter.setAttribute).toHaveBeenCalledTimes(1);
expect(mockAdapter.removeClass).toHaveBeenCalledWith(CssClasses.HIDE);
expect(mockAdapter.removeClass)
.toHaveBeenCalledWith(CssClasses.SHOWING_TRANSITION);
expect(foundation.hideTimeout).toEqual(null);
expect(mockAdapter.removeClass)
.toHaveBeenCalledWith(CssClasses.HIDE_TRANSITION);
expect(mockAdapter.removeClass).toHaveBeenCalledTimes(3);
expect(mockAdapter.addClass).toHaveBeenCalledWith(CssClasses.SHOWING);
expect(mockAdapter.addClass).toHaveBeenCalledWith(CssClasses.SHOWN);
expect(mockAdapter.addClass)
.toHaveBeenCalledWith(CssClasses.SHOWING_TRANSITION);
expect(mockAdapter.addClass).toHaveBeenCalledTimes(3);
});

it('#handleAnchorMouseLeave clears any pending showTimeout', () => {
const {foundation, mockAdapter} = setUpFoundationTest(MDCTooltipFoundation);
foundation.handleAnchorMouseEnter();
expect(foundation.showTimeout).not.toEqual(null);
foundation.handleAnchorMouseLeave();
expect(foundation.showTimeout).toEqual(null);

jasmine.clock().tick(numbers.SHOW_DELAY_MS);
expect(mockAdapter.setAttribute)
.not.toHaveBeenCalledWith('aria-hidden', 'false');
expect(mockAdapter.removeClass).not.toHaveBeenCalledWith(CssClasses.HIDE);
expect(mockAdapter.addClass).not.toHaveBeenCalledWith(CssClasses.SHOWING);
});

it('#hide clears any pending showTimeout', () => {
const {foundation, mockAdapter} = setUpFoundationTest(MDCTooltipFoundation);
foundation.handleAnchorMouseEnter();
expect(foundation.showTimeout).not.toEqual(null);
foundation.hide();
expect(foundation.showTimeout).toEqual(null);

jasmine.clock().tick(numbers.SHOW_DELAY_MS);
expect(mockAdapter.setAttribute)
.not.toHaveBeenCalledWith('aria-hidden', 'false');
expect(mockAdapter.removeClass).not.toHaveBeenCalledWith(CssClasses.HIDE);
expect(mockAdapter.addClass).not.toHaveBeenCalledWith(CssClasses.SHOWING);
});

it('properly calculates tooltip position (START alignment)', () => {
const anchorHeight = 35;
const expectedTooltipHeight = anchorHeight + numbers.BOUNDED_ANCHOR_GAP;
Expand Down

0 comments on commit a1c6559

Please sign in to comment.