You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					433 lines
				
				16 KiB
			
		
		
			
		
	
	
					433 lines
				
				16 KiB
			| 
											3 years ago
										 | [](https://www.npmjs.com/package/react-onclickoutside) | ||
|  | [](https://travis-ci.org/Pomax/react-onclickoutside) | ||
|  | [](https://www.npmjs.com/package/react-onclickoutside) | ||
|  | 
 | ||
|  | # :warning: Open source is free, but developer time isn't :warning:
 | ||
|  | 
 | ||
|  | **This package needs your support to stay maintained.** If you work for an organization | ||
|  | whose website is better off using react-onclickoutside than rolling its own code | ||
|  | solution, please consider talking to your manager to help | ||
|  | [fund this project](https://www.paypal.com/donate/?cmd=_s-xclick&hosted_button_id=QPRDLNGDANJSW). | ||
|  | Open Source is free to use, but certainly not free to develop. If you have the | ||
|  | means to reward those whose work you rely on, please consider doing so. | ||
|  | 
 | ||
|  | 
 | ||
|  | # An onClickOutside wrapper for React components
 | ||
|  | 
 | ||
|  | This is a React Higher Order Component (HOC) that you can use with your own | ||
|  | React components if you want to have them listen for clicks that occur somewhere | ||
|  | in the document, outside of the element itself (for instance, if you need to | ||
|  | hide a menu when people click anywhere else on your page). | ||
|  | 
 | ||
|  | Note that this HOC relies on the `.classList` property, which is supported by | ||
|  | all modern browsers, but not by deprecated and obsolete browsers like IE (noting | ||
|  | that Microsoft Edge is not Microsoft Internet Explorer. Edge does not have any | ||
|  | problems with the `classList` property for SVG elements). If your code relies on | ||
|  | classList in any way, you want to use a polyfill like | ||
|  | [dom4](https://github.com/WebReflection/dom4). | ||
|  | 
 | ||
|  | This HOC supports stateless components as of v5.7.0, and switched to using | ||
|  | transpiled es6 classes rather than `createClass` as of v6. | ||
|  | 
 | ||
|  | ## Sections covered in this README
 | ||
|  | 
 | ||
|  | * [Installation](#installation) | ||
|  | * [Usage:](#usage) | ||
|  |   * [ES6 Class Component](#es6-class-component) | ||
|  |   * [Functional Component with UseState Hook](#functional-component-with-usestate-hook) | ||
|  |   * [CommonJS Require](#commonjs-require) | ||
|  | * [Ensuring there's a click handler](#ensuring-there-is-a-click-handler) | ||
|  | * [Regulate which events to listen for](#regulate-which-events-to-listen-for) | ||
|  | * [Regulate whether or not to listen for outside clicks](#regulate-whether-or-not-to-listen-for-outside-clicks) | ||
|  | * [Regulate whether or not to listen to scrollbar clicks](#regulate-whether-or-not-to-listen-to-scrollbar-clicks) | ||
|  | * [Regulating `evt.preventDefault()` and `evt.stopPropagation()`](#regulating-evtpreventdefault-and-evtstoppropagation) | ||
|  | * [Marking elements as "skip over this one" during the event loop](#marking-elements-as-skip-over-this-one-during-the-event-loop) | ||
|  | * [Older React code: "What happened to the Mixin??"](#older-react-code-what-happened-to-the-mixin) | ||
|  |   * [But how can I access my component? It has an API that I rely on!](#but-how-can-i-access-my-component-it-has-an-api-that-i-rely-on) | ||
|  | * [Which version do I need for which version of React?](#which-version-do-i-need-for-which-version-of-react) | ||
|  |   * [Support-wise, only the latest version will receive updates and bug fixes.](#support-wise-only-the-latest-version-will-receive-updates-and-bug-fixes) | ||
|  | * [IE does not support classList for SVG elements!](#ie-does-not-support-classlist-for-svg-elements) | ||
|  | * [I can't find what I need in the README](#i-cant-find-what-i-need-in-the-readme) | ||
|  | 
 | ||
|  | ## Installation
 | ||
|  | 
 | ||
|  | Use `npm`: | ||
|  | 
 | ||
|  | ``` | ||
|  | $> npm install react-onclickoutside --save | ||
|  | ``` | ||
|  | 
 | ||
|  | (or `--save-dev` depending on your needs). You then use it in your components | ||
|  | as: | ||
|  | 
 | ||
|  | 
 | ||
|  | ## Usage
 | ||
|  | 
 | ||
|  | ### ES6 Class Component
 | ||
|  | 
 | ||
|  | ```js | ||
|  | import React, { Component } from "react"; | ||
|  | import onClickOutside from "react-onclickoutside"; | ||
|  | 
 | ||
|  | class MyComponent extends Component { | ||
|  |   handleClickOutside = evt => { | ||
|  |     // ..handling code goes here... | ||
|  |   }; | ||
|  | } | ||
|  | 
 | ||
|  | export default onClickOutside(MyComponent); | ||
|  | ``` | ||
|  | 
 | ||
|  | ### Functional Component with UseState Hook
 | ||
|  | 
 | ||
|  | This HoC does not support functional components, as it relies on class properties and component instances. However, you almost certainly don't need this HoC in modern (React 16+) functional component code, as a simple function will do the trick just fine. E.g.: | ||
|  | 
 | ||
|  | ```js | ||
|  | function listenForOutsideClicks(listening, setListening, menuRef, setIsOpen) { | ||
|  |   return () => { | ||
|  |     if (listening) return; | ||
|  |     if (!menuRef.current) return; | ||
|  |     setListening(true); | ||
|  |     [`click`, `touchstart`].forEach((type) => { | ||
|  |       document.addEventListener(`click`, (evt) => { | ||
|  |         if (menuRef.current.contains(evt.target)) return; | ||
|  |         setIsOpen(false); | ||
|  |       }); | ||
|  |     }); | ||
|  |   } | ||
|  | } | ||
|  | ``` | ||
|  | 
 | ||
|  | Used in a functional component as: | ||
|  | 
 | ||
|  | ```js | ||
|  | import React, { useEffect, useState, useRef } from "react"; | ||
|  | import listenForOutsideClicks from "./somewhere"; | ||
|  | 
 | ||
|  | const Menu = () => { | ||
|  |   const menuRef = useRef(null); | ||
|  |   const [listening, setListening] = useState(false); | ||
|  |   const [isOpen, setIsOpen] = useState(false);   | ||
|  |   const toggle = () => setIsOpen(!isOpen); | ||
|  | 
 | ||
|  |   useEffect(listenForOutsideClick( | ||
|  |     listening, | ||
|  |     setListening, | ||
|  |     menuRef, | ||
|  |     setIsOpen, | ||
|  |   )); | ||
|  | 
 | ||
|  |   return ( | ||
|  |     <div ref={menuRef} className={isOpen ? "open" : "hidden"}> | ||
|  |       <h1 onClick={toggle}>...</h1> | ||
|  |       <ul>...</ul> | ||
|  |     </div> | ||
|  |   ); | ||
|  | }; | ||
|  | 
 | ||
|  | export default Menu; | ||
|  | ``` | ||
|  | 
 | ||
|  | Example: https://codesandbox.io/s/trusting-dubinsky-k3mve | ||
|  | 
 | ||
|  | 
 | ||
|  | ### CommonJS Require
 | ||
|  | 
 | ||
|  | ```js | ||
|  | // .default is needed because library is bundled as ES6 module | ||
|  | var onClickOutside = require("react-onclickoutside").default; | ||
|  | var createReactClass = require("create-react-class"); | ||
|  | 
 | ||
|  | // create a new component, wrapped by this onclickoutside HOC: | ||
|  | var MyComponent = onClickOutside( | ||
|  |   createReactClass({ | ||
|  |     // ..., | ||
|  |     handleClickOutside: function(evt) { | ||
|  |       // ...handling code goes here... | ||
|  |     } | ||
|  |     // ... | ||
|  |   }) | ||
|  | ); | ||
|  | ``` | ||
|  | 
 | ||
|  | ### Ensuring there is a click handler
 | ||
|  | 
 | ||
|  | Note that if you try to wrap a React component class without a | ||
|  | `handleClickOutside(evt)` handler like this, the HOC will throw an error. In | ||
|  | order to use a custom event handler, you can specify the function to be used by | ||
|  | the HOC as second parameter (this can be useful in environments like TypeScript, | ||
|  | where the fact that the wrapped component does not implement the handler can be | ||
|  | flagged at compile-time): | ||
|  | 
 | ||
|  | ```js | ||
|  | // load the HOC: | ||
|  | import React, { Component } from "react"; | ||
|  | import onClickOutside from "react-onclickoutside"; | ||
|  | 
 | ||
|  | // create a new component, wrapped below by onClickOutside HOC: | ||
|  | class MyComponent extends Component { | ||
|  |   // ... | ||
|  |   myClickOutsideHandler(evt) { | ||
|  |     // ...handling code goes here... | ||
|  |   } | ||
|  |   // ... | ||
|  | } | ||
|  | var clickOutsideConfig = { | ||
|  |   handleClickOutside: function(instance) { | ||
|  |     return instance.myClickOutsideHandler; | ||
|  |   } | ||
|  | }; | ||
|  | var EnhancedComponent = onClickOutside(MyComponent, clickOutsideConfig); | ||
|  | ``` | ||
|  | 
 | ||
|  | Note that if you try to wrap a React component with a custom handler that the | ||
|  | component does not implement, the HOC will throw an error at run-time. | ||
|  | 
 | ||
|  | ## Regulate which events to listen for
 | ||
|  | 
 | ||
|  | By default, "outside clicks" are based on both `mousedown` and `touchstart` | ||
|  | events; if that is what you need, then you do not need to specify anything | ||
|  | special. However, if you need different events, you can specify these using the | ||
|  | `eventTypes` property. If you just need one event, you can pass in the event | ||
|  | name as plain string: | ||
|  | 
 | ||
|  | ```js | ||
|  | <MyComponent eventTypes="click" ... /> | ||
|  | ``` | ||
|  | 
 | ||
|  | For multiple events, you can pass in the array of event names you need to listen | ||
|  | for: | ||
|  | 
 | ||
|  | ```js | ||
|  | <MyComponent eventTypes={["click", "touchend"]} ... /> | ||
|  | ``` | ||
|  | 
 | ||
|  | ## Regulate whether or not to listen for outside clicks
 | ||
|  | 
 | ||
|  | Wrapped components have two functions that can be used to explicitly listen for, | ||
|  | or do nothing with, outside clicks | ||
|  | 
 | ||
|  | * `enableOnClickOutside()` - Enables outside click listening by setting up the | ||
|  |   event listening bindings. | ||
|  | * `disableOnClickOutside()` - Disables outside click listening by explicitly | ||
|  |   removing the event listening bindings. | ||
|  | 
 | ||
|  | In addition, you can create a component that uses this HOC such that it has the | ||
|  | code set up and ready to go, but not listening for outside click events until | ||
|  | you explicitly issue its `enableOnClickOutside()`, by passing in a properly | ||
|  | called `disableOnClickOutside`: | ||
|  | 
 | ||
|  | ```js | ||
|  | import React, { Component } from "react"; | ||
|  | import onClickOutside from "react-onclickoutside"; | ||
|  | 
 | ||
|  | class MyComponent extends Component { | ||
|  |   // ... | ||
|  |   handleClickOutside(evt) { | ||
|  |     // ... | ||
|  |   } | ||
|  |   // ... | ||
|  | } | ||
|  | var EnhancedComponent = onClickOutside(MyComponent); | ||
|  | 
 | ||
|  | class Container extends Component { | ||
|  |   render(evt) { | ||
|  |     return <EnhancedComponent disableOnClickOutside={true} />; | ||
|  |   } | ||
|  | } | ||
|  | ``` | ||
|  | 
 | ||
|  | Using `disableOnClickOutside()` or `enableOnClickOutside()` within | ||
|  | `componentDidMount` or `componentWillMount` is considered an anti-pattern, and | ||
|  | does not have consistent behaviour when using the mixin and HOC/ES7 Decorator. | ||
|  | Favour setting the `disableOnClickOutside` property on the component. | ||
|  | 
 | ||
|  | ## Regulate whether or not to listen to scrollbar clicks
 | ||
|  | 
 | ||
|  | By default this HOC will listen for "clicks inside the document", which may | ||
|  | include clicks that occur on the scrollbar. Quite often clicking on the | ||
|  | scrollbar _should_ close whatever is open but in case your project invalidates | ||
|  | that assumption you can use the `excludeScrollbar` property to explicitly tell | ||
|  | the HOC that clicks on the scrollbar should be ignored: | ||
|  | 
 | ||
|  | ```js | ||
|  | import React, { Component } from "react"; | ||
|  | import onClickOutside from "react-onclickoutside"; | ||
|  | 
 | ||
|  | class MyComponent extends Component { | ||
|  |   // ... | ||
|  | } | ||
|  | var EnhancedComponent = onClickOutside(MyComponent); | ||
|  | 
 | ||
|  | class Container extends Component { | ||
|  |   render(evt) { | ||
|  |     return <EnhancedComponent excludeScrollbar={true} />; | ||
|  |   } | ||
|  | } | ||
|  | ``` | ||
|  | 
 | ||
|  | Alternatively, you can specify this behavior as default for all instances of | ||
|  | your component passing a configuration object as second parameter: | ||
|  | 
 | ||
|  | ```js | ||
|  | import React, { Component } from "react"; | ||
|  | import onClickOutside from "react-onclickoutside"; | ||
|  | 
 | ||
|  | class MyComponent extends Component { | ||
|  |   // ... | ||
|  | } | ||
|  | var clickOutsideConfig = { | ||
|  |   excludeScrollbar: true | ||
|  | }; | ||
|  | var EnhancedComponent = onClickOutside(MyComponent, clickOutsideConfig); | ||
|  | ``` | ||
|  | 
 | ||
|  | ## Regulating `evt.preventDefault()` and `evt.stopPropagation()`
 | ||
|  | 
 | ||
|  | Technically this HOC lets you pass in `preventDefault={true/false}` and | ||
|  | `stopPropagation={true/false}` to regulate what happens to the event when it | ||
|  | hits your `handleClickOutside(evt)` function, but beware: `stopPropagation` may | ||
|  | not do what you expect it to do. | ||
|  | 
 | ||
|  | Each component adds new event listeners to the document, which may or may not | ||
|  | cause as many event triggers as there are event listening bindings. In the test | ||
|  | file found in `./test/browser/index.html`, the coded uses | ||
|  | `stopPropagation={true}` but sibling events still make it to "parents". | ||
|  | 
 | ||
|  | ## Marking elements as "skip over this one" during the event loop
 | ||
|  | 
 | ||
|  | If you want the HOC to ignore certain elements, you can tell the HOC which CSS | ||
|  | class name it should use for this purposes. If you want explicit control over | ||
|  | the class name, use `outsideClickIgnoreClass={some string}` as component | ||
|  | property, or if you don't, the default string used is | ||
|  | `ignore-react-onclickoutside`. | ||
|  | 
 | ||
|  | ## Older React code: "What happened to the Mixin??"
 | ||
|  | 
 | ||
|  | Due to ES2015/ES6 `class` syntax making mixins essentially impossible, and the | ||
|  | fact that HOC wrapping works perfectly fine in ES5 and older versions of React, | ||
|  | as of this package's version 5.0.0 no Mixin is offered anymore. | ||
|  | 
 | ||
|  | If you _absolutely_ need a mixin... you really don't. | ||
|  | 
 | ||
|  | ### But how can I access my component? It has an API that I rely on!
 | ||
|  | 
 | ||
|  | No, I get that. I constantly have that problem myself, so while there is no | ||
|  | universal agreement on how to do that, this HOC offers a `getInstance()` | ||
|  | function that you can call for a reference to the component you wrapped, so that | ||
|  | you can call its API without headaches: | ||
|  | 
 | ||
|  | ```js | ||
|  | import React, { Component } from 'react' | ||
|  | import onClickOutside from 'react-onclickoutside' | ||
|  | 
 | ||
|  | class MyComponent extends Component { | ||
|  |   // ... | ||
|  |   handleClickOutside(evt) { | ||
|  |     // ... | ||
|  |   } | ||
|  |   ... | ||
|  | } | ||
|  | var EnhancedComponent = onClickOutside(MyComponent); | ||
|  | 
 | ||
|  | class Container extends Component { | ||
|  |   constructor(props) { | ||
|  |     super(props); | ||
|  |     this.getMyComponentRef = this.getMyComponentRef.bind(this); | ||
|  |   } | ||
|  | 
 | ||
|  |   someFunction() { | ||
|  |     var ref = this.myComponentRef; | ||
|  |     // 1) Get the wrapped component instance: | ||
|  |     var superTrueMyComponent = ref.getInstance(); | ||
|  |     // and call instance functions defined for it: | ||
|  |     superTrueMyComponent.customFunction(); | ||
|  |   } | ||
|  | 
 | ||
|  |   getMyComponentRef(ref) { | ||
|  |     this.myComponentRef = ref; | ||
|  |   } | ||
|  | 
 | ||
|  |   render(evt) { | ||
|  |     return <EnhancedComponent disableOnClickOutside={true} ref={this.getMyComponentRef}/> | ||
|  |   } | ||
|  | } | ||
|  | ``` | ||
|  | 
 | ||
|  | Note that there is also a `getClass()` function, to get the original Class that | ||
|  | was passed into the HOC wrapper, but if you find yourself needing this you're | ||
|  | probably doing something wrong: you really want to define your classes as real, | ||
|  | require'able etc. units, and then write wrapped components separately, so that | ||
|  | you can always access the original class's `statics` etc. properties without | ||
|  | needing to extract them out of a HOC. | ||
|  | 
 | ||
|  | ## Which version do I need for which version of React?
 | ||
|  | 
 | ||
|  | If you use **React 0.12 or 0.13**, **version 2.4 and below** will work. | ||
|  | 
 | ||
|  | If you use **React 0.14**, use **v2.5 through v4.9**, as these specifically use | ||
|  | `react-DOM` for the necessary DOM event bindings. | ||
|  | 
 | ||
|  | If you use **React 15**, you can use **v4.x, which offers both a mixin and HOC, | ||
|  | or use v5.x, which is HOC-only**. | ||
|  | 
 | ||
|  | If you use **React 15.5**, you can use **v5.11.x**, which relies on | ||
|  | `createClass` as supplied by `create-react-class` rather than | ||
|  | `React.createClass`. | ||
|  | 
 | ||
|  | If you use **React 16** or 15.5 in preparation of 16, use v6.x, which uses pure | ||
|  | class notation. | ||
|  | 
 | ||
|  | ### Support-wise, only the latest version will receive updates and bug fixes.
 | ||
|  | 
 | ||
|  | I do not believe in perpetual support for outdated libraries, so if you find one | ||
|  | of the older versions is not playing nice with an even older React: you know | ||
|  | what to do, and it's not "keep using that old version of React". | ||
|  | 
 | ||
|  | ## IE does not support classList for SVG elements!
 | ||
|  | 
 | ||
|  | This is true, but also an edge-case problem that only exists for IE11 (as all | ||
|  | versions prior to 11 [no longer exist](https://support.microsoft.com/en-us/help/17454/lifecycle-faq-internet-explorer)), and should be addressed by you, rather | ||
|  | than by thousands of individual libraries that assume browsers have proper | ||
|  | HTML API  implementations (IE Edge has proper `classList` support even for SVG). | ||
|  | 
 | ||
|  | If you need this to work, you can add a shim for `classList` to your page(s), | ||
|  | loaded before you load your React code, and you'll have instantly fixed _every_ | ||
|  | library that you might remotely rely on that makes use of the `classList` | ||
|  | property. You can find several shims quite easily, a good one to start with is | ||
|  | the [dom4](https://github.com/WebReflection/dom4) shim, which adds all manner of | ||
|  | good DOM4 properties to "not quite at DOM4 yet" browser implementations. | ||
|  | 
 | ||
|  | Eventually this problem will stop being one, but in the mean time _you_ are | ||
|  | responsible for making _your_ site work by shimming everything that needs | ||
|  | shimming for IE. As such, **if you file a PR to fix classList-and-SVG issues | ||
|  | specifically for this library, your PR will be closed and I will politely point | ||
|  | you to this README.md section**. I will not accept PRs to fix this issue. You | ||
|  | already have the power to fix it, and I expect you to take responsibility as a | ||
|  | fellow developer to shim what you need instead of getting obsolete quirks | ||
|  | supported by libraries whose job isn't to support obsolete quirks. | ||
|  | 
 | ||
|  | To work around the issue you can use this simple shim: | ||
|  | 
 | ||
|  | ```js | ||
|  | if (!("classList" in SVGElement.prototype)) { | ||
|  |   Object.defineProperty(SVGElement.prototype, "classList", { | ||
|  |     get() { | ||
|  |       return { | ||
|  |         contains: className => { | ||
|  |           return this.className.baseVal.split(" ").indexOf(className) !== -1; | ||
|  |         } | ||
|  |       }; | ||
|  |     } | ||
|  |   }); | ||
|  | } | ||
|  | ``` | ||
|  | 
 | ||
|  | ## I can't find what I need in the README
 | ||
|  | 
 | ||
|  | If you've read the whole thing and you still can't find what you were looking | ||
|  | for, then the README is missing important information that should be added in. | ||
|  | Please [file an issue](https://github.com/Pomax/react-onclickoutside/issues) with a request for additional documentation, | ||
|  | describing what you were hoping to find in enough detail that it can be used to | ||
|  | write up the information you needed. |