-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Federico Zivolo
committed
Dec 17, 2018
1 parent
89432d6
commit 4e281c1
Showing
7 changed files
with
306 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
`react-mouse-outside` enables to listen for mouse events that happen | ||
to the target reference elements. | ||
|
||
```jsx | ||
const MouseOutside = require('./src').default; | ||
|
||
initialState = { txt: 'click outside' }; | ||
const callback = () => | ||
setState({ txt: 'clicked!' }, () => | ||
setTimeout(() => setState({ txt: 'click outside' }), 300) | ||
); | ||
|
||
<MouseOutside onClickOutside={callback}> | ||
{getRef => <div ref={getRef}>{state.txt}</div>} | ||
</MouseOutside>; | ||
``` | ||
|
||
The component accepts 3 properties, included `children`: | ||
|
||
##### `onClickOutside: Event => void` | ||
|
||
A function that will take as first argument the event object | ||
and gets triggered anytime the user clicks outside. | ||
|
||
##### `onMoveOutside: Event => void` | ||
|
||
A function that will take as first argument the event object | ||
and gets triggered anytime the user moves the mouse outside. | ||
|
||
##### `children: React.ElementRef => void` | ||
|
||
It takes as `children` a function with a `React.createRef` function | ||
as only argument. You can assign it as `ref` to any React element. | ||
|
||
#### Migration from @quid/react-components#MouseOutside | ||
|
||
The component takes as child a render-prop rather than a React element. | ||
This allows for more flexibility and removes the requirement of an extra wrapper. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
{ | ||
"name": "@quid/react-mouse-outside", | ||
"version": "1.0.0", | ||
"description": "Detect clicks and other mouse events outside of a React element", | ||
"main": "dist/index.js", | ||
"main:umd": "dist/index.umd.js", | ||
"module": "dist/index.es.js", | ||
"license": "MIT", | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/quid/ui-framework.git" | ||
}, | ||
"scripts": { | ||
"start": "microbundle watch", | ||
"prepare": "microbundle build --jsx React.createElement && flow-copy-source --ignore '{__mocks__/*,*.test}.js' src dist", | ||
"test": "cd ../.. && yarn test --testPathPattern packages/react-mouse-outside" | ||
}, | ||
"devDependencies": { | ||
"flow-copy-source": "^2.0.2", | ||
"microbundle": "^0.8.3" | ||
} | ||
} |
3 changes: 3 additions & 0 deletions
3
packages/react-mouse-outside/src/__snapshots__/index.test.js.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
// Jest Snapshot v1, https://goo.gl/fbAQLP | ||
|
||
exports[`should render expected markup 1`] = `<div />`; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
// @flow | ||
export const debounce = ( | ||
callback: Function, | ||
delay: number = 250, | ||
interval?: TimeoutID | ||
) => (...args: any) => | ||
// $FlowFixMe(fzivolo): Flow definition doesn't support the advanced usage of setTimeout | ||
clearTimeout(interval, (interval = setTimeout(callback, delay, ...args))); | ||
|
||
// This is mostly to avoid async troubles during tests, but it also | ||
// prevents to make asynchronous the callback when the delay is set to 0 | ||
// and it should actually be synchronous | ||
export const conditionalDebounce = (callback: Function, delay: number) => | ||
delay > 0 ? debounce(callback, delay) : callback; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
// @flow | ||
import { debounce, conditionalDebounce } from './debounce'; | ||
|
||
it('should debounce the function with custom delay', async done => { | ||
const fn = jest.fn(); | ||
const debFn = debounce(fn, 5); | ||
|
||
debFn(); | ||
debFn(); | ||
|
||
setTimeout(() => { | ||
expect(fn.mock.calls.length).toBe(1); | ||
done(); | ||
}, 20); | ||
}); | ||
|
||
it('should debounce the function with default delay', async done => { | ||
const fn = jest.fn(); | ||
const debFn = debounce(fn); | ||
|
||
debFn(); | ||
debFn(); | ||
|
||
setTimeout(() => { | ||
expect(fn.mock.calls.length).toBe(1); | ||
done(); | ||
}, 500); | ||
}); | ||
|
||
it('should not debounce if delay is zero', () => { | ||
const fn = jest.fn(); | ||
const debFn = conditionalDebounce(fn, 0); | ||
|
||
debFn(); | ||
debFn(); | ||
|
||
expect(fn.mock.calls.length).toBe(2); | ||
}); | ||
|
||
it('should debounce if delay is non zero', async done => { | ||
const fn = jest.fn(); | ||
const debFn = conditionalDebounce(fn, 10); | ||
|
||
debFn(); | ||
debFn(); | ||
|
||
setTimeout(() => { | ||
expect(fn.mock.calls.length).toBe(1); | ||
done(); | ||
}, 20); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
// @flow | ||
import * as React from 'react'; | ||
import { conditionalDebounce } from './debounce'; | ||
|
||
type RenderProp<P> = P => React.Node; | ||
|
||
type Props = { | ||
onClickOutside?: Function, | ||
onMoveOutside?: Function, | ||
delay: number, | ||
children: RenderProp<React.ElementRef<any>>, | ||
}; | ||
|
||
export default class MouseOutside extends React.Component<Props> { | ||
static defaultProps = { | ||
delay: 0, | ||
tag: 'div', | ||
}; | ||
|
||
container = React.createRef /*:: <HTMLElement> */(); | ||
|
||
isTargetOutside = (target: EventTarget) => { | ||
return ( | ||
// $FlowFixMe HTMLDocument isn't supported (https://github.com/facebook/flow/issues/2839) | ||
(target instanceof HTMLElement || target instanceof HTMLDocument) && | ||
(this.container.current instanceof HTMLElement && | ||
!this.container.current.contains(target) && | ||
document.contains(target)) | ||
); | ||
}; | ||
|
||
handleClickOutside = (evt: Event) => { | ||
if (this.props.onClickOutside && this.isTargetOutside(evt.target)) { | ||
this.props.onClickOutside(evt); | ||
} | ||
}; | ||
|
||
handleMoveOutside = conditionalDebounce((evt: Event) => { | ||
if (this.props.onMoveOutside && this.isTargetOutside(evt.target)) { | ||
this.props.onMoveOutside(evt); | ||
} | ||
}, this.props.delay); | ||
|
||
componentDidMount() { | ||
if (this.props.onClickOutside) { | ||
document.addEventListener('click', this.handleClickOutside, true); | ||
} | ||
if (this.props.onMoveOutside) { | ||
document.addEventListener('mousemove', this.handleMoveOutside, true); | ||
} | ||
} | ||
|
||
componentWillUnmount() { | ||
document.removeEventListener('click', this.handleClickOutside, true); | ||
document.removeEventListener('mousemove', this.handleMoveOutside, true); | ||
} | ||
|
||
render() { | ||
return this.props.children(this.container); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
// @flow | ||
import * as React from 'react'; | ||
import { shallow, mount } from 'enzyme'; | ||
import MouseOutside from './index'; | ||
|
||
// $FlowFixMe(fzivolo): forwardRef not yet supported | ||
const Child = React.forwardRef((props, ref) => <div ref={ref} />); | ||
|
||
it('should render expected markup', () => { | ||
const wrapper = shallow( | ||
<MouseOutside onClickOutside={f => f}> | ||
{ref => <div ref={ref} />} | ||
</MouseOutside> | ||
); | ||
expect(wrapper).toMatchSnapshot(); | ||
}); | ||
|
||
it('should not call onClickOutside on click inside', () => { | ||
const handleClickOutside = jest.fn(); | ||
const wrapper = mount( | ||
<MouseOutside onClickOutside={handleClickOutside}> | ||
{ref => <Child ref={ref} />} | ||
</MouseOutside> | ||
); | ||
|
||
wrapper.find(Child).simulate('click'); | ||
expect(handleClickOutside).not.toBeCalled(); | ||
}); | ||
|
||
it('should call onClickOutside on... click outside', () => { | ||
const handleClickOutside = jest.fn(); | ||
mount( | ||
<MouseOutside onClickOutside={handleClickOutside}> | ||
{ref => <Child ref={ref} />} | ||
</MouseOutside> | ||
); | ||
|
||
document.dispatchEvent(new Event('click')); | ||
expect(handleClickOutside).toBeCalled(); | ||
}); | ||
|
||
it('should not call onClickOutside on click outside if callback pro has been removed', () => { | ||
const handleClickOutside = jest.fn(); | ||
const wrapper = mount( | ||
<MouseOutside onClickOutside={handleClickOutside}> | ||
{ref => <Child ref={ref} />} | ||
</MouseOutside> | ||
); | ||
|
||
wrapper.setProps({ onClickOutside: undefined }); | ||
|
||
document.dispatchEvent(new Event('click')); | ||
expect(handleClickOutside).not.toBeCalled(); | ||
}); | ||
|
||
it('should not call onMoveOutside on click outside if callback pro has been removed', () => { | ||
const handleMouseMoveOutside = jest.fn(); | ||
const wrapper = mount( | ||
<MouseOutside onMoveOutside={handleMouseMoveOutside}> | ||
{ref => <div ref={ref} />} | ||
</MouseOutside> | ||
); | ||
|
||
wrapper.setProps({ onMoveOutside: undefined }); | ||
|
||
document.dispatchEvent(new Event('mousemove')); | ||
expect(handleMouseMoveOutside).not.toBeCalled(); | ||
}); | ||
|
||
it('should remove event listener on unmount', () => { | ||
const handleClickOutside = jest.fn(); | ||
const wrapper = mount( | ||
<MouseOutside onClickOutside={handleClickOutside}> | ||
{ref => <div ref={ref} />} | ||
</MouseOutside> | ||
); | ||
wrapper.unmount(); | ||
document.dispatchEvent(new Event('click')); | ||
expect(handleClickOutside).not.toBeCalled(); | ||
}); | ||
|
||
it('should not call onMoveOutside on mouse move inside', () => { | ||
const handleMouseMoveOutside = jest.fn(); | ||
const wrapper = mount( | ||
<MouseOutside onMoveOutside={handleMouseMoveOutside}> | ||
{ref => <Child ref={ref} />} | ||
</MouseOutside> | ||
); | ||
|
||
const child = wrapper.find(Child); | ||
child.simulate('mousemove', { target: child }); | ||
expect(handleMouseMoveOutside).not.toBeCalled(); | ||
}); | ||
|
||
it('should call onMoveOutside when mouse moves outside', () => { | ||
const handleMouseMoveOutside = jest.fn(); | ||
mount( | ||
<MouseOutside onMoveOutside={handleMouseMoveOutside}> | ||
{ref => <Child ref={ref} />} | ||
</MouseOutside> | ||
); | ||
|
||
document.dispatchEvent(new Event('mousemove')); | ||
expect(handleMouseMoveOutside).toBeCalled(); | ||
}); | ||
|
||
it('should remove event listener on unmount', () => { | ||
const handleMouseMoveOutside = jest.fn(); | ||
const wrapper = mount( | ||
<MouseOutside onMoveOutside={handleMouseMoveOutside}> | ||
{ref => <div ref={ref} />} | ||
</MouseOutside> | ||
); | ||
wrapper.unmount(); | ||
document.dispatchEvent(new Event('mousemove')); | ||
expect(handleMouseMoveOutside).not.toBeCalled(); | ||
}); |