Skip to content

Commit 8fd2b8b

Browse files
authored
Merge pull request #99 from sheminusminus/feature/tooltip-enhance-and-tests
Feature/Tooltip enhancements and tests
2 parents 285d5a0 + dac595d commit 8fd2b8b

File tree

2 files changed

+344
-16
lines changed

2 files changed

+344
-16
lines changed

src/components/Tooltip/Tooltip.js

Lines changed: 115 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -26,31 +26,110 @@ const Wrapper = styled.div`
2626

2727
const Tooltip = ({
2828
children,
29-
text,
30-
delay,
3129
className,
30+
disableFocusListener,
31+
disableMouseListener,
32+
enterDelay,
33+
leaveDelay,
34+
onBlur,
35+
onClose,
36+
onFocus,
37+
onMouseEnter,
38+
onMouseLeave,
39+
onOpen,
3240
style,
41+
testId,
42+
text,
3343
...otherProps
3444
}) => {
3545
const [show, setShow] = useState(false);
36-
const [delayTimer, setDelayTimer] = useState(null);
46+
const [openTimer, setOpenTimer] = useState(null);
47+
const [closeTimer, setCloseTimer] = useState(null);
48+
49+
const isUsingFocus = !disableFocusListener;
50+
const isUsingMouse = !disableMouseListener;
51+
52+
const handleOpen = evt => {
53+
clearTimeout(openTimer);
54+
clearTimeout(closeTimer);
3755

38-
const handleEnter = () => {
3956
const timer = setTimeout(() => {
4057
setShow(true);
41-
}, delay);
4258

43-
setDelayTimer(timer);
59+
if (onOpen) {
60+
onOpen(evt);
61+
}
62+
}, enterDelay);
63+
64+
setOpenTimer(timer);
65+
};
66+
67+
const handleEnter = evt => {
68+
evt.persist();
69+
70+
if (evt.type === 'focus' && onFocus) {
71+
onFocus(evt);
72+
} else if (evt.type === 'mouseenter' && onMouseEnter) {
73+
onMouseEnter(evt);
74+
}
75+
76+
handleOpen(evt);
77+
};
78+
79+
const handleClose = evt => {
80+
clearTimeout(openTimer);
81+
clearTimeout(closeTimer);
82+
83+
const timer = setTimeout(() => {
84+
setShow(false);
85+
86+
if (onClose) {
87+
onClose(evt);
88+
}
89+
}, leaveDelay);
90+
91+
setCloseTimer(timer);
4492
};
4593

46-
const handleLeave = () => {
47-
clearTimeout(delayTimer);
48-
setShow(false);
94+
const handleLeave = evt => {
95+
evt.persist();
96+
97+
if (evt.type === 'blur' && onBlur) {
98+
onBlur(evt);
99+
} else if (evt.type === 'mouseleave' && onMouseLeave) {
100+
onMouseLeave(evt);
101+
}
102+
103+
handleClose(evt);
49104
};
50105

106+
// set callbacks for onBlur and onFocus, unless disableFocusListener is true
107+
const blurCb = isUsingFocus ? handleLeave : undefined;
108+
const focusCb = isUsingFocus ? handleEnter : undefined;
109+
110+
// set callbacks for onMouseEnter and onMouseLeave, unless disableMouseListener is true
111+
const mouseEnterCb = isUsingMouse ? handleEnter : undefined;
112+
const mouseLeaveCb = isUsingMouse ? handleLeave : undefined;
113+
114+
// set the wrapper's tabIndex for focus events, unless disableFocusListener is true
115+
const tabIndex = isUsingFocus ? '0' : undefined;
116+
51117
return (
52-
<Wrapper onMouseEnter={handleEnter} onMouseLeave={handleLeave}>
53-
<Tip show={show} className={className} style={style} {...otherProps}>
118+
<Wrapper
119+
data-testid={testId ? `${testId}Wrapper` : undefined}
120+
onBlur={blurCb}
121+
onFocus={focusCb}
122+
onMouseEnter={mouseEnterCb}
123+
onMouseLeave={mouseLeaveCb}
124+
tabIndex={tabIndex}
125+
>
126+
<Tip
127+
className={className}
128+
data-testid={testId}
129+
show={show}
130+
style={style}
131+
{...otherProps}
132+
>
54133
{text}
55134
</Tip>
56135
{children}
@@ -59,17 +138,37 @@ const Tooltip = ({
59138
};
60139

61140
Tooltip.defaultProps = {
62-
delay: 1000,
63141
className: '',
64-
style: {}
142+
disableFocusListener: false,
143+
disableMouseListener: false,
144+
enterDelay: 1000,
145+
leaveDelay: 0,
146+
onBlur: undefined,
147+
onClose: undefined,
148+
onFocus: undefined,
149+
onMouseEnter: undefined,
150+
onMouseLeave: undefined,
151+
onOpen: undefined,
152+
style: {},
153+
testId: undefined
65154
};
66155

67156
Tooltip.propTypes = {
68157
children: propTypes.node.isRequired,
69-
text: propTypes.string.isRequired,
70158
className: propTypes.string,
71-
style: propTypes.shape([propTypes.string, propTypes.number]),
72-
delay: propTypes.number
159+
disableFocusListener: propTypes.bool,
160+
disableMouseListener: propTypes.bool,
161+
enterDelay: propTypes.number,
162+
leaveDelay: propTypes.number,
163+
onBlur: propTypes.func,
164+
onClose: propTypes.func,
165+
onFocus: propTypes.func,
166+
onMouseEnter: propTypes.func,
167+
onMouseLeave: propTypes.func,
168+
onOpen: propTypes.func,
169+
style: propTypes.shape({}),
170+
testId: propTypes.string,
171+
text: propTypes.string.isRequired
73172
};
74173

75174
export default Tooltip;
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
import React from 'react';
2+
import { fireEvent, render, waitForDomChange } from '@testing-library/react';
3+
4+
import Tooltip from './Tooltip';
5+
6+
const getProps = (props = {}) => ({
7+
className: props.className,
8+
disableFocusListener: props.disableFocusListener,
9+
disableMouseListener: props.disableMouseListener,
10+
enterDelay: props.enterDelay !== undefined ? props.enterDelay : 0,
11+
leaveDelay: props.leaveDelay !== undefined ? props.leaveDelay : 0,
12+
onBlur: jest.fn(),
13+
onClose: jest.fn(),
14+
onFocus: jest.fn(),
15+
onMouseEnter: jest.fn(),
16+
onMouseLeave: jest.fn(),
17+
onOpen: jest.fn(),
18+
style: props.style,
19+
testId: 'tip',
20+
text: 'I am the tooltip'
21+
});
22+
23+
const renderTooltip = props => (
24+
<Tooltip {...props}>
25+
<div>Kid</div>
26+
</Tooltip>
27+
);
28+
29+
describe('<Tooltip />', () => {
30+
describe('render', () => {
31+
it('should render wrapper element', () => {
32+
const { getByTestId } = render(renderTooltip(getProps()));
33+
34+
const tipWrapper = getByTestId('tipWrapper');
35+
36+
expect(tipWrapper).toBeInTheDocument();
37+
expect(tipWrapper.tagName).toBe('DIV');
38+
});
39+
40+
it('should render inner tooltip', () => {
41+
const { getByTestId } = render(renderTooltip(getProps()));
42+
43+
const tip = getByTestId('tip');
44+
45+
expect(tip).toBeInTheDocument();
46+
expect(tip.tagName).toBe('SPAN');
47+
});
48+
49+
it('should render children', () => {
50+
const { getByText } = render(renderTooltip(getProps()));
51+
52+
const children = getByText('Kid');
53+
54+
expect(children).toBeInTheDocument();
55+
expect(children.tagName).toBe('DIV');
56+
});
57+
58+
it('should render tooltip with provided className', () => {
59+
const { getByTestId } = render(
60+
renderTooltip(
61+
getProps({
62+
className: 'my-tip'
63+
})
64+
)
65+
);
66+
67+
const tip = getByTestId('tip');
68+
69+
expect(tip.className.includes('my-tip')).toBeTruthy();
70+
});
71+
});
72+
73+
describe('transition delays', () => {
74+
beforeEach(() => {
75+
jest.useFakeTimers();
76+
});
77+
78+
afterEach(() => {
79+
jest.useRealTimers();
80+
});
81+
82+
it('should respect enterDelay', async () => {
83+
const { getByTestId } = render(
84+
renderTooltip(
85+
getProps({
86+
enterDelay: 5
87+
})
88+
)
89+
);
90+
91+
const tipWrapper = getByTestId('tipWrapper');
92+
93+
fireEvent.focus(tipWrapper);
94+
95+
expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 5);
96+
});
97+
98+
it('should respect leaveDelay', async () => {
99+
const { getByTestId } = render(
100+
renderTooltip(
101+
getProps({
102+
leaveDelay: 6
103+
})
104+
)
105+
);
106+
107+
const tipWrapper = getByTestId('tipWrapper');
108+
109+
fireEvent.blur(tipWrapper);
110+
111+
expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 6);
112+
});
113+
});
114+
115+
describe('event callbacks', () => {
116+
it('should handle onFocus events, and call onOpen', async () => {
117+
const props = getProps();
118+
119+
const { getByTestId } = render(renderTooltip(props));
120+
121+
const tip = getByTestId('tip');
122+
const tipWrapper = getByTestId('tipWrapper');
123+
124+
fireEvent.focus(tipWrapper);
125+
126+
await waitForDomChange({ container: tip });
127+
128+
expect(props.onFocus).toHaveBeenCalled();
129+
expect(props.onOpen).toHaveBeenCalled();
130+
});
131+
132+
it('should handle onBlur events, and call onClose', async () => {
133+
const props = getProps();
134+
135+
const { getByTestId } = render(renderTooltip(props));
136+
137+
const tip = getByTestId('tip');
138+
const tipWrapper = getByTestId('tipWrapper');
139+
140+
fireEvent.focus(tipWrapper);
141+
await waitForDomChange({ container: tip });
142+
fireEvent.blur(tipWrapper);
143+
await waitForDomChange({ container: tip });
144+
145+
expect(props.onBlur).toHaveBeenCalled();
146+
expect(props.onClose).toHaveBeenCalled();
147+
});
148+
149+
it('should handle onMouseEnter events, and call onOpen', async () => {
150+
const props = getProps();
151+
152+
const { getByTestId } = render(renderTooltip(props));
153+
154+
const tip = getByTestId('tip');
155+
const tipWrapper = getByTestId('tipWrapper');
156+
157+
fireEvent.mouseEnter(tipWrapper);
158+
await waitForDomChange({ container: tip });
159+
160+
expect(props.onMouseEnter).toHaveBeenCalled();
161+
expect(props.onOpen).toHaveBeenCalled();
162+
});
163+
164+
it('should handle onMouseLeave events, and call onClose', async () => {
165+
const props = getProps();
166+
167+
const { getByTestId } = render(renderTooltip(props));
168+
169+
const tip = getByTestId('tip');
170+
const tipWrapper = getByTestId('tipWrapper');
171+
172+
fireEvent.mouseEnter(tipWrapper);
173+
await waitForDomChange({ container: tip });
174+
fireEvent.mouseLeave(tipWrapper);
175+
await waitForDomChange({ container: tip });
176+
177+
expect(props.onMouseLeave).toHaveBeenCalled();
178+
expect(props.onClose).toHaveBeenCalled();
179+
});
180+
181+
it('should not handle onFocus events when disableFocusListener is true', () => {
182+
const props = getProps({ disableFocusListener: true });
183+
184+
const { getByTestId } = render(renderTooltip(props));
185+
186+
const tipWrapper = getByTestId('tipWrapper');
187+
188+
fireEvent.focus(tipWrapper);
189+
190+
expect(props.onFocus).not.toHaveBeenCalled();
191+
});
192+
193+
it('should not handle onBlur events when disableFocusListener is true', () => {
194+
const props = getProps({ disableFocusListener: true });
195+
196+
const { getByTestId } = render(renderTooltip(props));
197+
198+
const tipWrapper = getByTestId('tipWrapper');
199+
200+
fireEvent.blur(tipWrapper);
201+
202+
expect(props.onBlur).not.toHaveBeenCalled();
203+
});
204+
205+
it('should not handle onMouseEnter events when disableMouseListener is true', () => {
206+
const props = getProps({ disableMouseListener: true });
207+
208+
const { getByTestId } = render(renderTooltip(props));
209+
210+
const tipWrapper = getByTestId('tipWrapper');
211+
212+
fireEvent.mouseEnter(tipWrapper);
213+
214+
expect(props.onMouseEnter).not.toHaveBeenCalled();
215+
});
216+
217+
it('should not handle onMouseLeave events when disableMouseListener is true', () => {
218+
const props = getProps({ disableMouseListener: true });
219+
220+
const { getByTestId } = render(renderTooltip(props));
221+
222+
const tipWrapper = getByTestId('tipWrapper');
223+
224+
fireEvent.mouseLeave(tipWrapper);
225+
226+
expect(props.onMouseLeave).not.toHaveBeenCalled();
227+
});
228+
});
229+
});

0 commit comments

Comments
 (0)