Skip to content

Commit b18fb74

Browse files
alisd23eddywashere
authored andcommitted
feat(Tooltip): add delay prop (#143)
* 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
1 parent d747987 commit b18fb74

3 files changed

Lines changed: 201 additions & 23 deletions

File tree

docs/lib/Components/TooltipsPage.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ export default class TooltipsPage extends React.Component {
3434
// target div ID, popover is attached to this element
3535
tether: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]),
3636
// optionally overide tether config http://tether.io/#options
37+
delay: PropTypes.oneOfType([
38+
PropTypes.shape({ show: PropTypes.number, hide: PropTypes.number }),
39+
PropTypes.number
40+
]),
41+
// optionally override show/hide delays - default { show: 0, hide: 250 }
3742
placement: PropTypes.oneOf([
3843
'top',
3944
'bottom',

src/Tooltip.js

Lines changed: 49 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,22 @@ const propTypes = {
99
disabled: PropTypes.bool,
1010
tether: PropTypes.object,
1111
toggle: PropTypes.func,
12-
children: PropTypes.node
12+
children: PropTypes.node,
13+
delay: PropTypes.oneOfType([
14+
PropTypes.shape({ show: PropTypes.number, hide: PropTypes.number }),
15+
PropTypes.number
16+
])
17+
};
18+
19+
const DEFAULT_DELAYS = {
20+
show: 0,
21+
hide: 250
1322
};
1423

1524
const defaultProps = {
1625
isOpen: false,
17-
placement: 'bottom'
26+
placement: 'bottom',
27+
delay: DEFAULT_DELAYS
1828
};
1929

2030
const defaultTetherConfig = {
@@ -37,7 +47,8 @@ class Tooltip extends React.Component {
3747
this.toggle = this.toggle.bind(this);
3848
this.onMouseOverTooltip = this.onMouseOverTooltip.bind(this);
3949
this.onMouseLeaveTooltip = this.onMouseLeaveTooltip.bind(this);
40-
this.onTimeout = this.onTimeout.bind(this);
50+
this.show = this.show.bind(this);
51+
this.hide = this.hide.bind(this);
4152
}
4253

4354
componentDidMount() {
@@ -50,23 +61,25 @@ class Tooltip extends React.Component {
5061
}
5162

5263
onMouseOverTooltip() {
53-
if (this._hoverTimeout) {
54-
clearTimeout(this._hoverTimeout);
55-
}
56-
57-
if (!this.props.isOpen) {
58-
this.toggle();
64+
if (this._hideTimeout) {
65+
this.clearHideTimeout();
5966
}
67+
this._showTimeout = setTimeout(this.show, this.getDelay('show'));
6068
}
6169

6270
onMouseLeaveTooltip() {
63-
this._hoverTimeout = setTimeout(this.onTimeout, 250);
71+
if (this._showTimeout) {
72+
this.clearShowTimeout();
73+
}
74+
this._hideTimeout = setTimeout(this.hide, this.getDelay('hide'));
6475
}
6576

66-
onTimeout() {
67-
if (this.props.isOpen) {
68-
this.toggle();
77+
getDelay(key) {
78+
const { delay } = this.props;
79+
if (typeof delay === 'object') {
80+
return isNaN(delay[key]) ? DEFAULT_DELAYS[key] : delay[key];
6981
}
82+
return delay;
7083
}
7184

7285
getTetherConfig() {
@@ -79,10 +92,31 @@ class Tooltip extends React.Component {
7992
};
8093
}
8194

95+
show() {
96+
if (!this.props.isOpen) {
97+
this.toggle();
98+
}
99+
}
100+
hide() {
101+
if (this.props.isOpen) {
102+
this.toggle();
103+
}
104+
}
105+
106+
clearShowTimeout() {
107+
clearTimeout(this._showTimeout);
108+
this._showTimeout = undefined;
109+
}
110+
111+
clearHideTimeout() {
112+
clearTimeout(this._hideTimeout);
113+
this._hideTimeout = undefined;
114+
}
115+
82116
handleDocumentClick(e) {
83117
if (e.target === this._target || this._target.contains(e.target)) {
84-
if (this._hoverTimeout) {
85-
clearTimeout(this._hoverTimeout);
118+
if (this._hideTimeout) {
119+
this.clearHideTimeout();
86120
}
87121

88122
if (!this.props.isOpen) {

test/Tooltip.spec.js

Lines changed: 147 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -135,9 +135,9 @@ describe('Tooltip', () => {
135135
wrapper.detach();
136136
});
137137

138-
it('should clear timeout if it exists on target click', () => {
138+
it('should clear hide timeout if it exists on target click', () => {
139139
const wrapper = mount(
140-
<Tooltip target="target" isOpen={isOpen} toggle={toggle}>
140+
<Tooltip target="target" isOpen={isOpen} toggle={toggle} delay={200}>
141141
Tooltip Content
142142
</Tooltip>,
143143
{ attachTo: container }
@@ -147,7 +147,7 @@ describe('Tooltip', () => {
147147
instance.onMouseLeaveTooltip();
148148
expect(isOpen).toBe(false);
149149
instance.handleDocumentClick({ target: target });
150-
jasmine.clock().tick(250);
150+
jasmine.clock().tick(200);
151151
expect(isOpen).toBe(true);
152152
wrapper.setProps({ isOpen: isOpen });
153153
instance.handleDocumentClick({ target: target });
@@ -176,7 +176,57 @@ describe('Tooltip', () => {
176176
wrapper.detach();
177177
});
178178

179-
describe('onTimeout', () => {
179+
describe('delay', () => {
180+
it('should accept a number', () => {
181+
isOpen = true;
182+
const wrapper = mount(
183+
<Tooltip target="target" isOpen={isOpen} toggle={toggle} delay={200}>
184+
Tooltip Content
185+
</Tooltip>,
186+
{ attachTo: container }
187+
);
188+
const instance = wrapper.instance();
189+
190+
instance.onMouseLeaveTooltip();
191+
expect(isOpen).toBe(true);
192+
jasmine.clock().tick(200);
193+
expect(isOpen).toBe(false);
194+
});
195+
196+
it('should accept an object', () => {
197+
isOpen = true;
198+
const wrapper = mount(
199+
<Tooltip target="target" isOpen={isOpen} toggle={toggle} delay={{ show: 200, hide: 200 }}>
200+
Tooltip Content
201+
</Tooltip>,
202+
{ attachTo: container }
203+
);
204+
const instance = wrapper.instance();
205+
206+
instance.onMouseLeaveTooltip();
207+
expect(isOpen).toBe(true);
208+
jasmine.clock().tick(200);
209+
expect(isOpen).toBe(false);
210+
});
211+
212+
it('should use default value if value is missing from object', () => {
213+
isOpen = true;
214+
const wrapper = mount(
215+
<Tooltip target="target" isOpen={isOpen} toggle={toggle} delay={{ show: 0 }}>
216+
Tooltip Content
217+
</Tooltip>,
218+
{ attachTo: container }
219+
);
220+
const instance = wrapper.instance();
221+
222+
instance.onMouseLeaveTooltip();
223+
expect(isOpen).toBe(true);
224+
jasmine.clock().tick(250); // Default hide value: 250
225+
expect(isOpen).toBe(false);
226+
});
227+
});
228+
229+
describe('hide', () => {
180230
it('should call toggle when isOpen', () => {
181231
spyOn(Tooltip.prototype, 'toggle').and.callThrough();
182232
isOpen = true;
@@ -190,7 +240,7 @@ describe('Tooltip', () => {
190240

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

193-
instance.onTimeout();
243+
instance.hide();
194244

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

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

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

212-
instance.onTimeout();
262+
instance.hide();
263+
264+
expect(Tooltip.prototype.toggle).not.toHaveBeenCalled();
265+
266+
wrapper.detach();
267+
});
268+
});
269+
270+
describe('show', () => {
271+
it('should call toggle when isOpen is false', () => {
272+
spyOn(Tooltip.prototype, 'toggle').and.callThrough();
273+
const wrapper = mount(
274+
<Tooltip target="target" isOpen={isOpen} toggle={toggle}>
275+
Tooltip Content
276+
</Tooltip>,
277+
{ attachTo: container }
278+
);
279+
const instance = wrapper.instance();
280+
281+
expect(Tooltip.prototype.toggle).not.toHaveBeenCalled();
282+
283+
instance.show();
284+
285+
expect(Tooltip.prototype.toggle).toHaveBeenCalled();
286+
287+
wrapper.detach();
288+
});
289+
290+
it('should not call toggle when isOpen', () => {
291+
spyOn(Tooltip.prototype, 'toggle').and.callThrough();
292+
isOpen = true;
293+
const wrapper = mount(
294+
<Tooltip target="target" isOpen={isOpen} toggle={toggle}>
295+
Tooltip Content
296+
</Tooltip>,
297+
{ attachTo: container }
298+
);
299+
const instance = wrapper.instance();
300+
301+
expect(Tooltip.prototype.toggle).not.toHaveBeenCalled();
302+
303+
instance.show();
213304

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

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

236327
instance.onMouseOverTooltip();
328+
jasmine.clock().tick(200);
237329

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

@@ -244,19 +336,66 @@ describe('Tooltip', () => {
244336
spyOn(Tooltip.prototype, 'toggle').and.callThrough();
245337
isOpen = true;
246338
const wrapper = mount(
247-
<Tooltip target="target" isOpen={isOpen} toggle={toggle}>
339+
<Tooltip target="target" isOpen={isOpen} toggle={toggle} delay={0}>
248340
Tooltip Content
249341
</Tooltip>,
250342
{ attachTo: container }
251343
);
252344
const instance = wrapper.instance();
253345

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

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

259352
wrapper.detach();
260353
});
261354
});
355+
356+
describe('onMouseLeaveTooltip', () => {
357+
it('should clear timeout if it exists on target click', () => {
358+
spyOn(Tooltip.prototype, 'toggle').and.callThrough();
359+
isOpen = true;
360+
const wrapper = mount(
361+
<Tooltip target="target" isOpen={isOpen} toggle={toggle} delay={200}>
362+
Tooltip Content
363+
</Tooltip>,
364+
{ attachTo: container }
365+
);
366+
const instance = wrapper.instance();
367+
368+
instance.onMouseOverTooltip();
369+
370+
expect(isOpen).toBe(true);
371+
expect(Tooltip.prototype.toggle).not.toHaveBeenCalled();
372+
373+
instance.onMouseLeaveTooltip();
374+
jasmine.clock().tick(200);
375+
376+
expect(Tooltip.prototype.toggle).toHaveBeenCalled();
377+
378+
wrapper.detach();
379+
});
380+
381+
it('should not call .toggle if isOpen is false', () => {
382+
spyOn(Tooltip.prototype, 'toggle').and.callThrough();
383+
isOpen = false;
384+
const wrapper = mount(
385+
<Tooltip target="target" isOpen={isOpen} toggle={toggle} delay={0}>
386+
Tooltip Content
387+
</Tooltip>,
388+
{ attachTo: container }
389+
);
390+
const instance = wrapper.instance();
391+
392+
instance.onMouseLeaveTooltip();
393+
jasmine.clock().tick(0); // delay: 0 toggle is still async
394+
395+
expect(isOpen).toBe(false);
396+
expect(Tooltip.prototype.toggle).not.toHaveBeenCalled();
397+
398+
wrapper.detach();
399+
});
400+
});
262401
});

0 commit comments

Comments
 (0)