Skip to content
This repository has been archived by the owner on Jun 5, 2023. It is now read-only.

Commit

Permalink
feat: New DocumentClickListener component
Browse files Browse the repository at this point in the history
  • Loading branch information
diondiondion committed Oct 2, 2019
1 parent 1890a9c commit d3ee8dc
Show file tree
Hide file tree
Showing 4 changed files with 139 additions and 0 deletions.
38 changes: 38 additions & 0 deletions src/DocumentClickListener/Example.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React, {useState, useRef} from 'react';
import DocumentClickListener from './';
import Switch from '../Switch';
import Box from '../Box';

function DocumentClickExample() {
const [count, setCount] = useState(0);
const [isActive, setActive] = useState(true);
const excludedElement = useRef(null);

return (
<>
<h1>Document clicks: {count}</h1>
<p
ref={excludedElement}
style={{padding: '1em', border: '2px dashed grey'}}
>
Clicks inside this box will be ignored.
</p>
{isActive && (
<DocumentClickListener
onClick={() => setCount(count + 1)}
excludedElementRef={excludedElement}
/>
)}
<Box mt="m">
<Switch
checked={isActive}
onChange={() => setActive(prevActive => !prevActive)}
id="switch"
/>{' '}
<label htmlFor="switch">Count clicks</label>
</Box>
</>
);
}

export default DocumentClickExample;
60 changes: 60 additions & 0 deletions src/DocumentClickListener/README.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
---
name: DocumentClickListener
menu: Components
---

import {Playground, Props} from 'docz';
import DocumentClickListener from './';
import Example from './Example';

# DocumentClickListener

A helper component that does not render any elements, but sets up a click listener on the body.
If you want to ignore clicks on a certain element, you can pass in the ref of that element using the `excludedElementRef` prop.

Useful for tooltips and dropdown menus.

## Examples

The below example shows how to use this component and also demonstrates why this is a component and not just a hook: Due to the "[rules of hooks](https://reactjs.org/docs/hooks-rules.html)", it's not possible to conditionally call a hook, while it's very easy to conditionally render a component.

<Playground>
<Example />
</Playground>

```jsx
function DocumentClickExample() {
const [count, setCount] = useState(0);
const [isActive, setActive] = useState(true);
const excludedElement = useRef(null);
return (
<>
<h1>Document clicks: {count}</h1>
<p
ref={excludedElement}
style={{padding: '1em', border: '2px dashed grey'}}
>
Clicks inside this box will be ignored.
</p>
{isActive && (
<DocumentClickListener
onClick={() => setCount(count + 1)}
excludedElementRef={excludedElement}
/>
)}
<Box mt="m">
<Switch
checked={isActive}
onChange={() => setActive(prevActive => !prevActive)}
id="switch"
/>{' '}
<label htmlFor="switch">Count clicks</label>
</Box>
</>
);
}
```

## Props

<Props of={DocumentClickListener} />
40 changes: 40 additions & 0 deletions src/DocumentClickListener/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import PropTypes from 'prop-types';

import useEventListener from '../useEventListener';

/**
* Helper component that sets up a global click event listener.
* Use the `excludedElement` prop to ignore clicks on that element.
*/

function DocumentClickListener({onClick, excludedElementRef}) {
useEventListener('click', event => {
const excludedElement =
excludedElementRef && excludedElementRef.current;
// Bail out if the clicked element or the currently focused element
// is inside of excludedElement. We need to check the focused element
// to prevent an issue in Chrome where initiating a drag inside of an
// input (to select the text inside of it) and ending that drag outside
// of the input fires a click event, breaking our excludedElement rule.
if (
excludedElement &&
(excludedElement === event.target ||
excludedElement.contains(event.target) ||
excludedElement === document.activeElement ||
excludedElement.contains(document.activeElement))
) {
return null;
}

onClick(event);
});

return null;
}

DocumentClickListener.propTypes = {
onClick: PropTypes.func.isRequired,
excludedElementRef: PropTypes.object,
};

export default DocumentClickListener;
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export {default as Box} from './Box';
export {default as Button} from './Button';
export {default as ButtonCore} from './ButtonCore';
export {default as CenterContent} from './CenterContent';
export {default as DocumentClickListener} from './DocumentClickListener';
export {default as Duration} from './Duration';
export {default as Flex} from './Flex';
export {default as Icon} from './Icon';
Expand Down

0 comments on commit d3ee8dc

Please sign in to comment.