Skip to content

Commit

Permalink
feat(Tooltip): add delay prop (#143)
Browse files Browse the repository at this point in the history
* feat: add delay prop to Tooltip

Previous delay was harcoded value of 250ms for the 'hide', and 0 for the
'show'.
New prop 'delay' allows for either an object of form: { show: 100, hide:
200 } or simply a number to set these delays.
Default is { show: 0, hide: 250 }

Closes #115

* test: add tests for delay prop object/number

* feat: allow partial delay object and add tests
  • Loading branch information
alisd23 authored and eddywashere committed Sep 18, 2016
1 parent d747987 commit b18fb74
Show file tree
Hide file tree
Showing 3 changed files with 201 additions and 23 deletions.
5 changes: 5 additions & 0 deletions docs/lib/Components/TooltipsPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ export default class TooltipsPage extends React.Component {
// target div ID, popover is attached to this element
tether: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]),
// optionally overide tether config http://tether.io/#options
delay: PropTypes.oneOfType([
PropTypes.shape({ show: PropTypes.number, hide: PropTypes.number }),
PropTypes.number
]),
// optionally override show/hide delays - default { show: 0, hide: 250 }
placement: PropTypes.oneOf([
'top',
'bottom',
Expand Down
64 changes: 49 additions & 15 deletions src/Tooltip.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,22 @@ const propTypes = {
disabled: PropTypes.bool,
tether: PropTypes.object,
toggle: PropTypes.func,
children: PropTypes.node
children: PropTypes.node,
delay: PropTypes.oneOfType([
PropTypes.shape({ show: PropTypes.number, hide: PropTypes.number }),
PropTypes.number
])
};

const DEFAULT_DELAYS = {
show: 0,
hide: 250
};

const defaultProps = {
isOpen: false,
placement: 'bottom'
placement: 'bottom',
delay: DEFAULT_DELAYS
};

const defaultTetherConfig = {
Expand All @@ -37,7 +47,8 @@ class Tooltip extends React.Component {
this.toggle = this.toggle.bind(this);
this.onMouseOverTooltip = this.onMouseOverTooltip.bind(this);
this.onMouseLeaveTooltip = this.onMouseLeaveTooltip.bind(this);
this.onTimeout = this.onTimeout.bind(this);
this.show = this.show.bind(this);
this.hide = this.hide.bind(this);
}

componentDidMount() {
Expand All @@ -50,23 +61,25 @@ class Tooltip extends React.Component {
}

onMouseOverTooltip() {
if (this._hoverTimeout) {
clearTimeout(this._hoverTimeout);
}

if (!this.props.isOpen) {
this.toggle();
if (this._hideTimeout) {
this.clearHideTimeout();
}
this._showTimeout = setTimeout(this.show, this.getDelay('show'));
}

onMouseLeaveTooltip() {
this._hoverTimeout = setTimeout(this.onTimeout, 250);
if (this._showTimeout) {
this.clearShowTimeout();
}
this._hideTimeout = setTimeout(this.hide, this.getDelay('hide'));
}

onTimeout() {
if (this.props.isOpen) {
this.toggle();
getDelay(key) {
const { delay } = this.props;
if (typeof delay === 'object') {
return isNaN(delay[key]) ? DEFAULT_DELAYS[key] : delay[key];
}
return delay;
}

getTetherConfig() {
Expand All @@ -79,10 +92,31 @@ class Tooltip extends React.Component {
};
}

show() {
if (!this.props.isOpen) {
this.toggle();
}
}
hide() {
if (this.props.isOpen) {
this.toggle();
}
}

clearShowTimeout() {
clearTimeout(this._showTimeout);
this._showTimeout = undefined;
}

clearHideTimeout() {
clearTimeout(this._hideTimeout);
this._hideTimeout = undefined;
}

handleDocumentClick(e) {
if (e.target === this._target || this._target.contains(e.target)) {
if (this._hoverTimeout) {
clearTimeout(this._hoverTimeout);
if (this._hideTimeout) {
this.clearHideTimeout();
}

if (!this.props.isOpen) {
Expand Down
155 changes: 147 additions & 8 deletions test/Tooltip.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,9 @@ describe('Tooltip', () => {
wrapper.detach();
});

it('should clear timeout if it exists on target click', () => {
it('should clear hide timeout if it exists on target click', () => {
const wrapper = mount(
<Tooltip target="target" isOpen={isOpen} toggle={toggle}>
<Tooltip target="target" isOpen={isOpen} toggle={toggle} delay={200}>
Tooltip Content
</Tooltip>,
{ attachTo: container }
Expand All @@ -147,7 +147,7 @@ describe('Tooltip', () => {
instance.onMouseLeaveTooltip();
expect(isOpen).toBe(false);
instance.handleDocumentClick({ target: target });
jasmine.clock().tick(250);
jasmine.clock().tick(200);
expect(isOpen).toBe(true);
wrapper.setProps({ isOpen: isOpen });
instance.handleDocumentClick({ target: target });
Expand Down Expand Up @@ -176,7 +176,57 @@ describe('Tooltip', () => {
wrapper.detach();
});

describe('onTimeout', () => {
describe('delay', () => {
it('should accept a number', () => {
isOpen = true;
const wrapper = mount(
<Tooltip target="target" isOpen={isOpen} toggle={toggle} delay={200}>
Tooltip Content
</Tooltip>,
{ attachTo: container }
);
const instance = wrapper.instance();

instance.onMouseLeaveTooltip();
expect(isOpen).toBe(true);
jasmine.clock().tick(200);
expect(isOpen).toBe(false);
});

it('should accept an object', () => {
isOpen = true;
const wrapper = mount(
<Tooltip target="target" isOpen={isOpen} toggle={toggle} delay={{ show: 200, hide: 200 }}>
Tooltip Content
</Tooltip>,
{ attachTo: container }
);
const instance = wrapper.instance();

instance.onMouseLeaveTooltip();
expect(isOpen).toBe(true);
jasmine.clock().tick(200);
expect(isOpen).toBe(false);
});

it('should use default value if value is missing from object', () => {
isOpen = true;
const wrapper = mount(
<Tooltip target="target" isOpen={isOpen} toggle={toggle} delay={{ show: 0 }}>
Tooltip Content
</Tooltip>,
{ attachTo: container }
);
const instance = wrapper.instance();

instance.onMouseLeaveTooltip();
expect(isOpen).toBe(true);
jasmine.clock().tick(250); // Default hide value: 250
expect(isOpen).toBe(false);
});
});

describe('hide', () => {
it('should call toggle when isOpen', () => {
spyOn(Tooltip.prototype, 'toggle').and.callThrough();
isOpen = true;
Expand All @@ -190,7 +240,7 @@ describe('Tooltip', () => {

expect(Tooltip.prototype.toggle).not.toHaveBeenCalled();

instance.onTimeout();
instance.hide();

expect(Tooltip.prototype.toggle).toHaveBeenCalled();

Expand All @@ -209,7 +259,48 @@ describe('Tooltip', () => {

expect(Tooltip.prototype.toggle).not.toHaveBeenCalled();

instance.onTimeout();
instance.hide();

expect(Tooltip.prototype.toggle).not.toHaveBeenCalled();

wrapper.detach();
});
});

describe('show', () => {
it('should call toggle when isOpen is false', () => {
spyOn(Tooltip.prototype, 'toggle').and.callThrough();
const wrapper = mount(
<Tooltip target="target" isOpen={isOpen} toggle={toggle}>
Tooltip Content
</Tooltip>,
{ attachTo: container }
);
const instance = wrapper.instance();

expect(Tooltip.prototype.toggle).not.toHaveBeenCalled();

instance.show();

expect(Tooltip.prototype.toggle).toHaveBeenCalled();

wrapper.detach();
});

it('should not call toggle when isOpen', () => {
spyOn(Tooltip.prototype, 'toggle').and.callThrough();
isOpen = true;
const wrapper = mount(
<Tooltip target="target" isOpen={isOpen} toggle={toggle}>
Tooltip Content
</Tooltip>,
{ attachTo: container }
);
const instance = wrapper.instance();

expect(Tooltip.prototype.toggle).not.toHaveBeenCalled();

instance.show();

expect(Tooltip.prototype.toggle).not.toHaveBeenCalled();

Expand All @@ -221,7 +312,7 @@ describe('Tooltip', () => {
it('should clear timeout if it exists on target click', () => {
spyOn(Tooltip.prototype, 'toggle').and.callThrough();
const wrapper = mount(
<Tooltip target="target" isOpen={isOpen} toggle={toggle}>
<Tooltip target="target" isOpen={isOpen} toggle={toggle} delay={200}>
Tooltip Content
</Tooltip>,
{ attachTo: container }
Expand All @@ -234,6 +325,7 @@ describe('Tooltip', () => {
expect(Tooltip.prototype.toggle).not.toHaveBeenCalled();

instance.onMouseOverTooltip();
jasmine.clock().tick(200);

expect(Tooltip.prototype.toggle).toHaveBeenCalled();

Expand All @@ -244,19 +336,66 @@ describe('Tooltip', () => {
spyOn(Tooltip.prototype, 'toggle').and.callThrough();
isOpen = true;
const wrapper = mount(
<Tooltip target="target" isOpen={isOpen} toggle={toggle}>
<Tooltip target="target" isOpen={isOpen} toggle={toggle} delay={0}>
Tooltip Content
</Tooltip>,
{ attachTo: container }
);
const instance = wrapper.instance();

instance.onMouseOverTooltip();
jasmine.clock().tick(0); // delay: 0 toggle is still async

expect(isOpen).toBe(true);
expect(Tooltip.prototype.toggle).not.toHaveBeenCalled();

wrapper.detach();
});
});

describe('onMouseLeaveTooltip', () => {
it('should clear timeout if it exists on target click', () => {
spyOn(Tooltip.prototype, 'toggle').and.callThrough();
isOpen = true;
const wrapper = mount(
<Tooltip target="target" isOpen={isOpen} toggle={toggle} delay={200}>
Tooltip Content
</Tooltip>,
{ attachTo: container }
);
const instance = wrapper.instance();

instance.onMouseOverTooltip();

expect(isOpen).toBe(true);
expect(Tooltip.prototype.toggle).not.toHaveBeenCalled();

instance.onMouseLeaveTooltip();
jasmine.clock().tick(200);

expect(Tooltip.prototype.toggle).toHaveBeenCalled();

wrapper.detach();
});

it('should not call .toggle if isOpen is false', () => {
spyOn(Tooltip.prototype, 'toggle').and.callThrough();
isOpen = false;
const wrapper = mount(
<Tooltip target="target" isOpen={isOpen} toggle={toggle} delay={0}>
Tooltip Content
</Tooltip>,
{ attachTo: container }
);
const instance = wrapper.instance();

instance.onMouseLeaveTooltip();
jasmine.clock().tick(0); // delay: 0 toggle is still async

expect(isOpen).toBe(false);
expect(Tooltip.prototype.toggle).not.toHaveBeenCalled();

wrapper.detach();
});
});
});

0 comments on commit b18fb74

Please sign in to comment.