Skip to content

Commit b7cb153

Browse files
authored
feat(events): add support for focusin and focusout events (#55)
Add support for focus events.
1 parent d7b9974 commit b7cb153

File tree

2 files changed

+123
-74
lines changed

2 files changed

+123
-74
lines changed

__tests__/index.tsx

Lines changed: 111 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -27,79 +27,118 @@ describe('ClickAway Listener', () => {
2727
expect(container.firstElementChild.tagName).toBe('DIV');
2828
});
2929

30-
it('should trigger onClickAway only when an element is clicked outside', () => {
31-
const handleClickAway = jest.fn();
32-
const { getByText } = render(
33-
<React.Fragment>
34-
<ClickAwayListener onClickAway={handleClickAway}>
35-
<div>Hello World</div>
36-
</ClickAwayListener>
37-
<button>A button</button>
38-
<p>A text element</p>
39-
</React.Fragment>
40-
);
41-
jest.runOnlyPendingTimers();
42-
43-
fireEvent.click(getByText(/A button/i));
44-
fireEvent.click(getByText(/A text element/i));
45-
fireEvent.click(getByText(/Hello World/i));
46-
expect(handleClickAway).toBeCalledTimes(2);
47-
});
48-
49-
it('works with different mouse events', () => {
50-
const handleClickAway = jest.fn();
51-
const { getByText } = render(
52-
<React.Fragment>
53-
<ClickAwayListener onClickAway={handleClickAway} mouseEvent="mousedown">
54-
<div>Hello World</div>
55-
</ClickAwayListener>
56-
<button>A button</button>
57-
<p>A text element</p>
58-
</React.Fragment>
59-
);
60-
jest.runOnlyPendingTimers();
61-
62-
fireEvent.mouseDown(getByText(/A button/i));
63-
fireEvent.mouseDown(getByText(/A text element/i));
64-
fireEvent.mouseDown(getByText(/Hello World/i));
65-
expect(handleClickAway).toBeCalledTimes(2);
66-
});
67-
68-
it('returns the event object', () => {
69-
const handleClick = (event: MouseEvent | TouchEvent) => {
70-
expect(event.type).toBe('click');
71-
};
72-
73-
const { getByText } = render(
74-
<React.Fragment>
75-
<ClickAwayListener onClickAway={handleClick}>
76-
<div>Hello World</div>
77-
</ClickAwayListener>
78-
<button>A button</button>
79-
</React.Fragment>
80-
);
30+
it.each`
31+
fireEventFn | expectedEventType
32+
${'focusIn'} | ${'focusin'}
33+
${'click'} | ${'click'}
34+
${'touchEnd'} | ${'touchend'}
35+
`(
36+
'should return the "$expectedEventType" event object, when the "$fireEventFn" event is fired on the outside element',
37+
({ fireEventFn, expectedEventType }) => {
38+
const handleClick = (event: FocusEvent | MouseEvent | TouchEvent) => {
39+
expect(event.type).toBe(expectedEventType);
40+
};
8141

82-
fireEvent.click(getByText(/A button/i));
83-
});
42+
const { getByText } = render(
43+
<React.Fragment>
44+
<ClickAwayListener onClickAway={handleClick}>
45+
<div>An inside Hello World element</div>
46+
</ClickAwayListener>
47+
<button>An outside button</button>
48+
</React.Fragment>
49+
);
8450

85-
it('works with different touch events', () => {
86-
const handleClickAway = jest.fn();
87-
const { getByText } = render(
88-
<React.Fragment>
89-
<ClickAwayListener onClickAway={handleClickAway} touchEvent="touchend">
90-
<div>Hello World</div>
91-
</ClickAwayListener>
92-
<button>A button</button>
93-
<p>A text element</p>
94-
</React.Fragment>
95-
);
96-
jest.runOnlyPendingTimers();
51+
fireEvent[fireEventFn](getByText(/outside button/i));
52+
}
53+
);
54+
55+
it.each`
56+
mouseEvent | fireEventFn
57+
${'click'} | ${'click'}
58+
${'mousedown'} | ${'mouseDown'}
59+
${'mouseup'} | ${'mouseUp'}
60+
`(
61+
'should invoke the provided onClickAway listener, only when the "$fireEventFn" mouse event is fired on the outside elements',
62+
({ mouseEvent, fireEventFn }) => {
63+
const handleClickAway = jest.fn();
64+
const { getByText } = render(
65+
<React.Fragment>
66+
<ClickAwayListener
67+
onClickAway={handleClickAway}
68+
mouseEvent={mouseEvent}
69+
>
70+
<div>Hello World</div>
71+
</ClickAwayListener>
72+
<button>A button</button>
73+
<p>A text element</p>
74+
</React.Fragment>
75+
);
76+
jest.runOnlyPendingTimers();
77+
78+
fireEvent[fireEventFn](getByText(/A button/i));
79+
fireEvent[fireEventFn](getByText(/A text element/i));
80+
fireEvent[fireEventFn](getByText(/Hello World/i));
81+
expect(handleClickAway).toBeCalledTimes(2);
82+
}
83+
);
84+
85+
it.each`
86+
touchEvent | fireEventFn
87+
${'touchstart'} | ${'touchStart'}
88+
${'touchend'} | ${'touchEnd'}
89+
`(
90+
'should invoke the provided onClickAway listener, only when the "$fireEventFn" touch event is fired on the outside elements',
91+
({ touchEvent, fireEventFn }) => {
92+
const handleClickAway = jest.fn();
93+
const { getByText } = render(
94+
<React.Fragment>
95+
<ClickAwayListener
96+
onClickAway={handleClickAway}
97+
touchEvent={touchEvent}
98+
>
99+
<div>Hello World</div>
100+
</ClickAwayListener>
101+
<button>A button</button>
102+
<p>A text element</p>
103+
</React.Fragment>
104+
);
105+
jest.runOnlyPendingTimers();
106+
107+
fireEvent[fireEventFn](getByText(/A button/i));
108+
fireEvent[fireEventFn](getByText(/A text element/i));
109+
fireEvent[fireEventFn](getByText(/Hello World/i));
110+
expect(handleClickAway).toBeCalledTimes(2);
111+
}
112+
);
113+
114+
it.each`
115+
focusEvent | fireEventFn
116+
${'focusin'} | ${'focusIn'}
117+
${'focusout'} | ${'focusOut'}
118+
`(
119+
'should invoke the provided onClickAway listener, only when the "$fireEventFn" focus event is fired on the outside elements',
120+
({ focusEvent, fireEventFn }) => {
121+
const handleClickAway = jest.fn();
122+
const { getByText } = render(
123+
<React.Fragment>
124+
<ClickAwayListener
125+
onClickAway={handleClickAway}
126+
focusEvent={focusEvent}
127+
>
128+
<div>Hello World</div>
129+
</ClickAwayListener>
130+
<button>A button</button>
131+
<p>A text element</p>
132+
</React.Fragment>
133+
);
134+
jest.runOnlyPendingTimers();
97135

98-
fireEvent.touchEnd(getByText(/A button/i));
99-
fireEvent.touchEnd(getByText(/A text element/i));
100-
fireEvent.touchEnd(getByText(/Hello World/i));
101-
expect(handleClickAway).toBeCalledTimes(2);
102-
});
136+
fireEvent[fireEventFn](getByText(/A button/i));
137+
fireEvent[fireEventFn](getByText(/A text element/i));
138+
fireEvent[fireEventFn](getByText(/Hello World/i));
139+
expect(handleClickAway).toBeCalledTimes(2);
140+
}
141+
);
103142

104143
it('should handle multiple cases', () => {
105144
const handleClickAway = jest.fn();
@@ -144,7 +183,7 @@ describe('ClickAway Listener', () => {
144183
});
145184
Input.displayName = 'Input';
146185

147-
it('should not replace previously added refs', () => {
186+
it('shouldn’t replace previously added refs', () => {
148187
const { result } = renderHook(() => {
149188
const ref = React.useRef();
150189

@@ -179,7 +218,7 @@ describe('ClickAway Listener', () => {
179218
expect(result.current.ref).toStrictEqual(inputRef);
180219
});
181220

182-
it("shouldn't hijack the onClick listener", () => {
221+
it('shouldn’t hijack the onClick listener', () => {
183222
const handleClick = jest.fn();
184223
const handleClickAway = jest.fn();
185224

src/index.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,23 @@ import React, {
99
FunctionComponent
1010
} from 'react';
1111

12+
type FocusEvents = 'focusin' | 'focusout';
1213
type MouseEvents = 'click' | 'mousedown' | 'mouseup';
1314
type TouchEvents = 'touchstart' | 'touchend';
14-
type Events = MouseEvent | TouchEvent;
15+
type Events = FocusEvent | MouseEvent | TouchEvent;
16+
1517
interface Props extends HTMLAttributes<HTMLElement> {
1618
onClickAway: (event: Events) => void;
19+
focusEvent?: FocusEvents;
1720
mouseEvent?: MouseEvents;
1821
touchEvent?: TouchEvents;
1922
children: ReactElement<any>;
2023
}
2124

2225
const eventTypeMapping = {
2326
click: 'onClick',
27+
focusin: 'onFocus',
28+
focusout: 'onFocus',
2429
mousedown: 'onMouseDown',
2530
mouseup: 'onMouseUp',
2631
touchstart: 'onTouchStart',
@@ -30,6 +35,7 @@ const eventTypeMapping = {
3035
const ClickAwayListener: FunctionComponent<Props> = ({
3136
children,
3237
onClickAway,
38+
focusEvent = 'focusin',
3339
mouseEvent = 'click',
3440
touchEvent = 'touchend'
3541
}) => {
@@ -94,19 +100,23 @@ const ClickAwayListener: FunctionComponent<Props> = ({
94100

95101
document.addEventListener(mouseEvent, handleEvents);
96102
document.addEventListener(touchEvent, handleEvents);
103+
document.addEventListener(focusEvent, handleEvents);
97104

98105
return () => {
99106
document.removeEventListener(mouseEvent, handleEvents);
100107
document.removeEventListener(touchEvent, handleEvents);
108+
document.removeEventListener(focusEvent, handleEvents);
101109
};
102-
}, [mouseEvent, onClickAway, touchEvent]);
110+
}, [focusEvent, mouseEvent, onClickAway, touchEvent]);
103111

104112
const mappedMouseEvent = eventTypeMapping[mouseEvent];
105113
const mappedTouchEvent = eventTypeMapping[touchEvent];
114+
const mappedFocusEvent = eventTypeMapping[focusEvent];
106115

107116
return React.Children.only(
108117
cloneElement(children as ReactElement<any>, {
109118
ref: handleChildRef,
119+
[mappedFocusEvent]: handleBubbledEvents(mappedFocusEvent),
110120
[mappedMouseEvent]: handleBubbledEvents(mappedMouseEvent),
111121
[mappedTouchEvent]: handleBubbledEvents(mappedTouchEvent)
112122
})

0 commit comments

Comments
 (0)