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.
|