From d3ee8dcc7024afa4b1b1314cc19015536a6e1835 Mon Sep 17 00:00:00 2001 From: DIonysos Dajka Date: Wed, 2 Oct 2019 12:19:52 +0200 Subject: [PATCH] feat: New DocumentClickListener component --- src/DocumentClickListener/Example.js | 38 ++++++++++++++++++ src/DocumentClickListener/README.mdx | 60 ++++++++++++++++++++++++++++ src/DocumentClickListener/index.js | 40 +++++++++++++++++++ src/index.js | 1 + 4 files changed, 139 insertions(+) create mode 100644 src/DocumentClickListener/Example.js create mode 100644 src/DocumentClickListener/README.mdx create mode 100644 src/DocumentClickListener/index.js diff --git a/src/DocumentClickListener/Example.js b/src/DocumentClickListener/Example.js new file mode 100644 index 00000000..2786ffd4 --- /dev/null +++ b/src/DocumentClickListener/Example.js @@ -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 ( + <> +

Document clicks: {count}

+

+ Clicks inside this box will be ignored. +

+ {isActive && ( + setCount(count + 1)} + excludedElementRef={excludedElement} + /> + )} + + setActive(prevActive => !prevActive)} + id="switch" + />{' '} + + + + ); +} + +export default DocumentClickExample; diff --git a/src/DocumentClickListener/README.mdx b/src/DocumentClickListener/README.mdx new file mode 100644 index 00000000..855d9975 --- /dev/null +++ b/src/DocumentClickListener/README.mdx @@ -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. + + + + + +```jsx +function DocumentClickExample() { + const [count, setCount] = useState(0); + const [isActive, setActive] = useState(true); + const excludedElement = useRef(null); + return ( + <> +

Document clicks: {count}

+

+ Clicks inside this box will be ignored. +

+ {isActive && ( + setCount(count + 1)} + excludedElementRef={excludedElement} + /> + )} + + setActive(prevActive => !prevActive)} + id="switch" + />{' '} + + + + ); +} +``` + +## Props + + diff --git a/src/DocumentClickListener/index.js b/src/DocumentClickListener/index.js new file mode 100644 index 00000000..72259285 --- /dev/null +++ b/src/DocumentClickListener/index.js @@ -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; diff --git a/src/index.js b/src/index.js index 630747b5..e507a81d 100644 --- a/src/index.js +++ b/src/index.js @@ -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';