├── .babelrc.js
├── .eslintrc.json
├── .github
├── FUNDING.yml
└── workflows
│ └── node.js.yml.diabled
├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── FUNDING.md
├── LICENSE
├── README.md
├── dist
├── react-onclickoutside.js
└── react-onclickoutside.min.js
├── package-lock.json
├── package.json
├── rollup.config.js
├── src
├── detect-passive-events.js
├── dom-helpers.js
├── index.js
└── uid.js
└── test
├── .eslintrc.json
├── browser
├── index.html
├── style2.css
├── test1.js
├── test2.js
└── test3.js
├── no-dom-test.js
└── test.js
/.babelrc.js:
--------------------------------------------------------------------------------
1 | const { BABEL_ENV, NODE_ENV } = process.env;
2 | const modules = BABEL_ENV === 'cjs' || NODE_ENV === 'test' ? 'commonjs' : false;
3 | const loose = true;
4 |
5 | module.exports = {
6 | presets: [
7 | ['@babel/env', {
8 | loose,
9 | modules: modules,
10 | exclude: ['transform-typeof-symbol'],
11 | }],
12 | ],
13 | plugins: [
14 | ['@babel/proposal-class-properties', { loose }],
15 | '@babel/proposal-object-rest-spread',
16 | ],
17 | };
18 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "commonjs": true,
5 | "amd": true,
6 | "mocha": true
7 | },
8 | "parser": "babel-eslint",
9 | "parserOptions": {
10 | "ecmaVersion": 6,
11 | "ecmaFeatures": {
12 | "impliedStrict ": true
13 | }
14 | },
15 | "extends": "eslint:recommended",
16 | "globals": {
17 | "React": true,
18 | "ReactDOM": true,
19 | "assert": true,
20 | "process": true
21 | },
22 | "rules": {
23 | "indent": [
24 | "error",
25 | 2
26 | ],
27 | "linebreak-style": [
28 | "error",
29 | "unix"
30 | ],
31 | "quotes": [
32 | "error",
33 | "single"
34 | ],
35 | "semi": [
36 | "error",
37 | "always"
38 | ],
39 | "no-console": 0
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | # github: Pomax
4 | patreon: Bezierinfo
5 |
--------------------------------------------------------------------------------
/.github/workflows/node.js.yml.diabled:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3 |
4 | name: Node.js CI
5 |
6 | on:
7 | push:
8 | branches: [ master ]
9 | pull_request:
10 | branches: [ master ]
11 |
12 | jobs:
13 | build:
14 |
15 | runs-on: ubuntu-latest
16 |
17 | strategy:
18 | matrix:
19 | node-version: [12.x, 14.x, 16.x]
20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
21 |
22 | steps:
23 | - uses: actions/checkout@v2
24 | - name: Use Node.js ${{ matrix.node-version }}
25 | uses: actions/setup-node@v2
26 | with:
27 | node-version: ${{ matrix.node-version }}
28 | - run: npm install
29 | - run: npm test
30 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | lib
3 | es
4 | dist/react-onclickoutside.es.js
5 | dist/react-onclickoutside.cjs.js
6 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "7"
4 |
5 | script:
6 | - xvfb-run npm test
7 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | v6.10.0
2 |
3 | - Update react & react-dom peer-dep to support 17.x
4 |
5 | v6.9.0
6 |
7 | - Added strictMode compatibility.
8 | - Added a changelog, cursorily filled in previous version information.
9 |
10 | v6.x
11 |
12 | Updated for React 16, using `class` notation
13 |
14 | v5.x
15 |
16 | Removed React mixin
17 |
18 | v4.10
19 |
20 | Updated for React 15, as both mixin and HOC.
21 |
22 | v2.5 through v4.9
23 |
24 | Updates based on React 0.14
25 |
26 | v2.4 and below
27 |
28 | React 0.12/0.13
29 |
--------------------------------------------------------------------------------
/FUNDING.md:
--------------------------------------------------------------------------------
1 | **This package needs your support to stay maintained.** If you work for an organization
2 | whose website is better off using react-onclickoutside than rolling its own code
3 | solution, please consider talking to your manager to help fund this project.
4 |
5 | Open Source is free to use, but certainly not free to develop. If you have the
6 | means to reward those whose work you rely on, please consider doing so.
7 |
8 | If you wish to help keep this library maintained through a financial contribution,
9 | please visit the [Paypal donation page](https://www.paypal.com/donate/?cmd=_s-xclick&hosted_button_id=QPRDLNGDANJSW),
10 | and send me an email if you want me to know your contribution was specifically
11 | for this library.
12 |
13 | Thank you,
14 |
15 | - Pomax
16 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2015-2022 Mike "Pomax" Kamermans
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://www.npmjs.com/package/react-onclickoutside)
2 | [](https://travis-ci.org/Pomax/react-onclickoutside)
3 | [](https://www.npmjs.com/package/react-onclickoutside)
4 |
5 | # :warning: Open source is free, but developer time isn't :warning:
6 |
7 | **This package needs your support to stay maintained.** If you work for an organization
8 | whose website is better off using react-onclickoutside than rolling its own code
9 | solution, please consider talking to your manager to help
10 | [fund this project](https://www.patreon.com/c/bezierinfo/membership).
11 | Open Source is free to use, but certainly not free to develop. If you have the
12 | means to reward those whose work you rely on, please consider doing so.
13 |
14 |
15 | # An onClickOutside wrapper for React components
16 |
17 | This is a React Higher Order Component (HOC) that you can use with your own
18 | React components if you want to have them listen for clicks that occur somewhere
19 | in the document, outside of the element itself (for instance, if you need to
20 | hide a menu when people click anywhere else on your page).
21 |
22 | Note that this HOC relies on the `.classList` property, which is supported by
23 | all modern browsers, but not by deprecated and obsolete browsers like IE (noting
24 | that Microsoft Edge is not Microsoft Internet Explorer. Edge does not have any
25 | problems with the `classList` property for SVG elements). If your code relies on
26 | classList in any way, you want to use a polyfill like
27 | [dom4](https://github.com/WebReflection/dom4).
28 |
29 | This HOC supports stateless components as of v5.7.0, and switched to using
30 | transpiled es6 classes rather than `createClass` as of v6.
31 |
32 | ## Sections covered in this README
33 |
34 | * [Installation](#installation)
35 | * [Usage:](#usage)
36 | * [Functional Component with UseState Hook](#functional-component-with-usestate-hook)
37 | * [ES6 Class Component](#es6-class-component)
38 | * [CommonJS Require](#commonjs-require)
39 | * [Ensuring there's a click handler](#ensuring-there-is-a-click-handler)
40 | * [Regulate which events to listen for](#regulate-which-events-to-listen-for)
41 | * [Regulate whether or not to listen for outside clicks](#regulate-whether-or-not-to-listen-for-outside-clicks)
42 | * [Regulate whether or not to listen to scrollbar clicks](#regulate-whether-or-not-to-listen-to-scrollbar-clicks)
43 | * [Regulating `evt.preventDefault()` and `evt.stopPropagation()`](#regulating-evtpreventdefault-and-evtstoppropagation)
44 | * [Marking elements as "skip over this one" during the event loop](#marking-elements-as-skip-over-this-one-during-the-event-loop)
45 | * [Older React code: "What happened to the Mixin??"](#older-react-code-what-happened-to-the-mixin)
46 | * [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)
47 | * [Which version do I need for which version of React?](#which-version-do-i-need-for-which-version-of-react)
48 | * [Support-wise, only the latest version will receive updates and bug fixes.](#support-wise-only-the-latest-version-will-receive-updates-and-bug-fixes)
49 | * [IE does not support classList for SVG elements!](#ie-does-not-support-classlist-for-svg-elements)
50 | * [I can't find what I need in the README](#i-cant-find-what-i-need-in-the-readme)
51 |
52 | ## Installation
53 |
54 | **Only install this HoC if you're still extending the `Component` class**, something which the React documentation doesn't even cover anymore because they went all-in on functional components with hooks.
55 |
56 | If you're using hooks, which React says you should be, don't install this HoC and instead read [this section, below](#functional-component-with-usestate-hook).
57 |
58 | If you're still stuck with Component classes, then you can install this HoC using `npm`:
59 |
60 | ```
61 | $> npm install react-onclickoutside --save
62 | ```
63 |
64 | (or `--save-dev` depending on your needs). You then use it in your components
65 | as:
66 |
67 |
68 | ## Usage
69 |
70 | ### Functional Component with UseState Hook
71 |
72 | 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.:
73 |
74 | ```js
75 | function listenForOutsideClicks(listening, setListening, menuRef, yourClickHandler) {
76 | return () => {
77 | if (listening) return;
78 | if (!menuRef.current) return;
79 | setListening(true);
80 | [`click`, `touchstart`].forEach((type) => {
81 | document.addEventListener(type, (evt) => {
82 | if (menuRef.current.contains(evt.target)) return;
83 | yourClickHandler();
84 | });
85 | });
86 | }
87 | }
88 | ```
89 |
90 | Used in a functional component as:
91 |
92 | ```js
93 | import React, { useEffect, useState, useRef } from "react";
94 | import listenForOutsideClicks from "./somewhere";
95 |
96 | const Menu = () => {
97 | const menuRef = useRef(null);
98 | const [listening, setListening] = useState(false);
99 | const [isOpen, setIsOpen] = useState(false);
100 | const toggle = () => setIsOpen(!isOpen);
101 |
102 | useEffect(listenForOutsideClick(
103 | listening,
104 | setListening,
105 | menuRef,
106 | // let's say our custom handler closes our menu on an outside click:
107 | () => setIsOpen(false),
108 | ));
109 |
110 | return (
111 |
112 |
...
113 |
...
114 |
115 | );
116 | };
117 |
118 | export default Menu;
119 | ```
120 |
121 | Example: https://codesandbox.io/s/trusting-dubinsky-k3mve
122 |
123 | ### ES6 Class Component
124 |
125 | ```js
126 | import React, { Component } from "react";
127 | import onClickOutside from "react-onclickoutside";
128 |
129 | class MyComponent extends Component {
130 | handleClickOutside = evt => {
131 | // ..handling code goes here...
132 | };
133 | }
134 |
135 | export default onClickOutside(MyComponent);
136 | ```
137 |
138 | ### CommonJS Require
139 |
140 | ```js
141 | // .default is needed because library is bundled as ES6 module
142 | var onClickOutside = require("react-onclickoutside").default;
143 | var createReactClass = require("create-react-class");
144 |
145 | // create a new component, wrapped by this onclickoutside HOC:
146 | var MyComponent = onClickOutside(
147 | createReactClass({
148 | // ...,
149 | handleClickOutside: function(evt) {
150 | // ...handling code goes here...
151 | }
152 | // ...
153 | })
154 | );
155 | ```
156 |
157 | ### Ensuring there is a click handler
158 |
159 | Note that if you try to wrap a React component class without a
160 | `handleClickOutside(evt)` handler like this, the HOC will throw an error. In
161 | order to use a custom event handler, you can specify the function to be used by
162 | the HOC as second parameter (this can be useful in environments like TypeScript,
163 | where the fact that the wrapped component does not implement the handler can be
164 | flagged at compile-time):
165 |
166 | ```js
167 | // load the HOC:
168 | import React, { Component } from "react";
169 | import onClickOutside from "react-onclickoutside";
170 |
171 | // create a new component, wrapped below by onClickOutside HOC:
172 | class MyComponent extends Component {
173 | // ...
174 | myClickOutsideHandler(evt) {
175 | // ...handling code goes here...
176 | }
177 | // ...
178 | }
179 | var clickOutsideConfig = {
180 | handleClickOutside: function(instance) {
181 | return instance.myClickOutsideHandler;
182 | }
183 | };
184 | var EnhancedComponent = onClickOutside(MyComponent, clickOutsideConfig);
185 | ```
186 |
187 | Note that if you try to wrap a React component with a custom handler that the
188 | component does not implement, the HOC will throw an error at run-time.
189 |
190 | ## Regulate which events to listen for
191 |
192 | By default, "outside clicks" are based on both `mousedown` and `touchstart`
193 | events; if that is what you need, then you do not need to specify anything
194 | special. However, if you need different events, you can specify these using the
195 | `eventTypes` property. If you just need one event, you can pass in the event
196 | name as plain string:
197 |
198 | ```js
199 |
200 | ```
201 |
202 | For multiple events, you can pass in the array of event names you need to listen
203 | for:
204 |
205 | ```js
206 |
207 | ```
208 |
209 | ## Regulate whether or not to listen for outside clicks
210 |
211 | Wrapped components have two functions that can be used to explicitly listen for,
212 | or do nothing with, outside clicks
213 |
214 | * `enableOnClickOutside()` - Enables outside click listening by setting up the
215 | event listening bindings.
216 | * `disableOnClickOutside()` - Disables outside click listening by explicitly
217 | removing the event listening bindings.
218 |
219 | In addition, you can create a component that uses this HOC such that it has the
220 | code set up and ready to go, but not listening for outside click events until
221 | you explicitly issue its `enableOnClickOutside()`, by passing in a properly
222 | called `disableOnClickOutside`:
223 |
224 | ```js
225 | import React, { Component } from "react";
226 | import onClickOutside from "react-onclickoutside";
227 |
228 | class MyComponent extends Component {
229 | // ...
230 | handleClickOutside(evt) {
231 | // ...
232 | }
233 | // ...
234 | }
235 | var EnhancedComponent = onClickOutside(MyComponent);
236 |
237 | class Container extends Component {
238 | render(evt) {
239 | return ;
240 | }
241 | }
242 | ```
243 |
244 | Using `disableOnClickOutside()` or `enableOnClickOutside()` within
245 | `componentDidMount` or `componentWillMount` is considered an anti-pattern, and
246 | does not have consistent behaviour when using the mixin and HOC/ES7 Decorator.
247 | Favour setting the `disableOnClickOutside` property on the component.
248 |
249 | ## Regulate whether or not to listen to scrollbar clicks
250 |
251 | By default this HOC will listen for "clicks inside the document", which may
252 | include clicks that occur on the scrollbar. Quite often clicking on the
253 | scrollbar _should_ close whatever is open but in case your project invalidates
254 | that assumption you can use the `excludeScrollbar` property to explicitly tell
255 | the HOC that clicks on the scrollbar should be ignored:
256 |
257 | ```js
258 | import React, { Component } from "react";
259 | import onClickOutside from "react-onclickoutside";
260 |
261 | class MyComponent extends Component {
262 | // ...
263 | }
264 | var EnhancedComponent = onClickOutside(MyComponent);
265 |
266 | class Container extends Component {
267 | render(evt) {
268 | return ;
269 | }
270 | }
271 | ```
272 |
273 | Alternatively, you can specify this behavior as default for all instances of
274 | your component passing a configuration object as second parameter:
275 |
276 | ```js
277 | import React, { Component } from "react";
278 | import onClickOutside from "react-onclickoutside";
279 |
280 | class MyComponent extends Component {
281 | // ...
282 | }
283 | var clickOutsideConfig = {
284 | excludeScrollbar: true
285 | };
286 | var EnhancedComponent = onClickOutside(MyComponent, clickOutsideConfig);
287 | ```
288 |
289 | ## Regulating `evt.preventDefault()` and `evt.stopPropagation()`
290 |
291 | Technically this HOC lets you pass in `preventDefault={true/false}` and
292 | `stopPropagation={true/false}` to regulate what happens to the event when it
293 | hits your `handleClickOutside(evt)` function, but beware: `stopPropagation` may
294 | not do what you expect it to do.
295 |
296 | Each component adds new event listeners to the document, which may or may not
297 | cause as many event triggers as there are event listening bindings. In the test
298 | file found in `./test/browser/index.html`, the coded uses
299 | `stopPropagation={true}` but sibling events still make it to "parents".
300 |
301 | ## Marking elements as "skip over this one" during the event loop
302 |
303 | If you want the HOC to ignore certain elements, you can tell the HOC which CSS
304 | class name it should use for this purposes. If you want explicit control over
305 | the class name, use `outsideClickIgnoreClass={some string}` as component
306 | property, or if you don't, the default string used is
307 | `ignore-react-onclickoutside`.
308 |
309 | ## Older React code: "What happened to the Mixin??"
310 |
311 | Due to ES2015/ES6 `class` syntax making mixins essentially impossible, and the
312 | fact that HOC wrapping works perfectly fine in ES5 and older versions of React,
313 | as of this package's version 5.0.0 no Mixin is offered anymore.
314 |
315 | If you _absolutely_ need a mixin... you really don't.
316 |
317 | ### But how can I access my component? It has an API that I rely on!
318 |
319 | No, I get that. I constantly have that problem myself, so while there is no
320 | universal agreement on how to do that, this HOC offers a `getInstance()`
321 | function that you can call for a reference to the component you wrapped, so that
322 | you can call its API without headaches:
323 |
324 | ```js
325 | import React, { Component } from 'react'
326 | import onClickOutside from 'react-onclickoutside'
327 |
328 | class MyComponent extends Component {
329 | // ...
330 | handleClickOutside(evt) {
331 | // ...
332 | }
333 | ...
334 | }
335 | var EnhancedComponent = onClickOutside(MyComponent);
336 |
337 | class Container extends Component {
338 | constructor(props) {
339 | super(props);
340 | this.getMyComponentRef = this.getMyComponentRef.bind(this);
341 | }
342 |
343 | someFunction() {
344 | var ref = this.myComponentRef;
345 | // 1) Get the wrapped component instance:
346 | var superTrueMyComponent = ref.getInstance();
347 | // and call instance functions defined for it:
348 | superTrueMyComponent.customFunction();
349 | }
350 |
351 | getMyComponentRef(ref) {
352 | this.myComponentRef = ref;
353 | }
354 |
355 | render(evt) {
356 | return
357 | }
358 | }
359 | ```
360 |
361 | Note that there is also a `getClass()` function, to get the original Class that
362 | was passed into the HOC wrapper, but if you find yourself needing this you're
363 | probably doing something wrong: you really want to define your classes as real,
364 | require'able etc. units, and then write wrapped components separately, so that
365 | you can always access the original class's `statics` etc. properties without
366 | needing to extract them out of a HOC.
367 |
368 | ## Which version do I need for which version of React?
369 |
370 | If you use **React 0.12 or 0.13**, **version 2.4 and below** will work.
371 |
372 | If you use **React 0.14**, use **v2.5 through v4.9**, as these specifically use
373 | `react-DOM` for the necessary DOM event bindings.
374 |
375 | If you use **React 15**, you can use **v4.x, which offers both a mixin and HOC,
376 | or use v5.x, which is HOC-only**.
377 |
378 | If you use **React 15.5**, you can use **v5.11.x**, which relies on
379 | `createClass` as supplied by `create-react-class` rather than
380 | `React.createClass`.
381 |
382 | If you use **React 16** or 15.5 in preparation of 16, use v6.x, which uses pure
383 | class notation.
384 |
385 | ### Support-wise, only the latest version will receive updates and bug fixes.
386 |
387 | I do not believe in perpetual support for outdated libraries, so if you find one
388 | of the older versions is not playing nice with an even older React: you know
389 | what to do, and it's not "keep using that old version of React".
390 |
391 | ## IE does not support classList for SVG elements!
392 |
393 | This is true, but also an edge-case problem that only exists for IE11 (as all
394 | 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
395 | than by thousands of individual libraries that assume browsers have proper
396 | HTML API implementations (IE Edge has proper `classList` support even for SVG).
397 |
398 | If you need this to work, you can add a shim for `classList` to your page(s),
399 | loaded before you load your React code, and you'll have instantly fixed _every_
400 | library that you might remotely rely on that makes use of the `classList`
401 | property. You can find several shims quite easily, a good one to start with is
402 | the [dom4](https://github.com/WebReflection/dom4) shim, which adds all manner of
403 | good DOM4 properties to "not quite at DOM4 yet" browser implementations.
404 |
405 | Eventually this problem will stop being one, but in the mean time _you_ are
406 | responsible for making _your_ site work by shimming everything that needs
407 | shimming for IE. As such, **if you file a PR to fix classList-and-SVG issues
408 | specifically for this library, your PR will be closed and I will politely point
409 | you to this README.md section**. I will not accept PRs to fix this issue. You
410 | already have the power to fix it, and I expect you to take responsibility as a
411 | fellow developer to shim what you need instead of getting obsolete quirks
412 | supported by libraries whose job isn't to support obsolete quirks.
413 |
414 | To work around the issue you can use this simple shim:
415 |
416 | ```js
417 | if (!("classList" in SVGElement.prototype)) {
418 | Object.defineProperty(SVGElement.prototype, "classList", {
419 | get() {
420 | return {
421 | contains: className => {
422 | return this.className.baseVal.split(" ").indexOf(className) !== -1;
423 | }
424 | };
425 | }
426 | });
427 | }
428 | ```
429 |
430 | ## I can't find what I need in the README
431 |
432 | If you've read the whole thing and you still can't find what you were looking
433 | for, then the README is missing important information that should be added in.
434 | Please [file an issue](https://github.com/Pomax/react-onclickoutside/issues) with a request for additional documentation,
435 | describing what you were hoping to find in enough detail that it can be used to
436 | write up the information you needed.
437 |
--------------------------------------------------------------------------------
/dist/react-onclickoutside.js:
--------------------------------------------------------------------------------
1 | (function(g,f){typeof exports==='object'&&typeof module!=='undefined'?f(exports,require('react'),require('react-dom')):typeof define==='function'&&define.amd?define(['exports','react','react-dom'],f):(g=typeof globalThis!=='undefined'?globalThis:g||self,f(g.onClickOutside={},g.React,g.ReactDOM));}(this,(function(exports, react, reactDom){'use strict';function _inheritsLoose(subClass, superClass) {
2 | subClass.prototype = Object.create(superClass.prototype);
3 | subClass.prototype.constructor = subClass;
4 |
5 | _setPrototypeOf(subClass, superClass);
6 | }
7 |
8 | function _setPrototypeOf(o, p) {
9 | _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) {
10 | o.__proto__ = p;
11 | return o;
12 | };
13 |
14 | return _setPrototypeOf(o, p);
15 | }
16 |
17 | function _objectWithoutPropertiesLoose(source, excluded) {
18 | if (source == null) return {};
19 | var target = {};
20 | var sourceKeys = Object.keys(source);
21 | var key, i;
22 |
23 | for (i = 0; i < sourceKeys.length; i++) {
24 | key = sourceKeys[i];
25 | if (excluded.indexOf(key) >= 0) continue;
26 | target[key] = source[key];
27 | }
28 |
29 | return target;
30 | }
31 |
32 | function _assertThisInitialized(self) {
33 | if (self === void 0) {
34 | throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
35 | }
36 |
37 | return self;
38 | }/**
39 | * Check whether some DOM node is our Component's node.
40 | */
41 | function isNodeFound(current, componentNode, ignoreClass) {
42 | if (current === componentNode) {
43 | return true;
44 | } // SVG elements do not technically reside in the rendered DOM, so
45 | // they do not have classList directly, but they offer a link to their
46 | // corresponding element, which can have classList. This extra check is for
47 | // that case.
48 | // See: http://www.w3.org/TR/SVG11/struct.html#InterfaceSVGUseElement
49 | // Discussion: https://github.com/Pomax/react-onclickoutside/pull/17
50 |
51 |
52 | if (current.correspondingElement) {
53 | return current.correspondingElement.classList.contains(ignoreClass);
54 | }
55 |
56 | return current.classList.contains(ignoreClass);
57 | }
58 | /**
59 | * Try to find our node in a hierarchy of nodes, returning the document
60 | * node as highest node if our node is not found in the path up.
61 | */
62 |
63 | function findHighest(current, componentNode, ignoreClass) {
64 | if (current === componentNode) {
65 | return true;
66 | } // If source=local then this event came from 'somewhere'
67 | // inside and should be ignored. We could handle this with
68 | // a layered approach, too, but that requires going back to
69 | // thinking in terms of Dom node nesting, running counter
70 | // to React's 'you shouldn't care about the DOM' philosophy.
71 | // Also cover shadowRoot node by checking current.host
72 |
73 |
74 | while (current.parentNode || current.host) {
75 | // Only check normal node without shadowRoot
76 | if (current.parentNode && isNodeFound(current, componentNode, ignoreClass)) {
77 | return true;
78 | }
79 |
80 | current = current.parentNode || current.host;
81 | }
82 |
83 | return current;
84 | }
85 | /**
86 | * Check if the browser scrollbar was clicked
87 | */
88 |
89 | function clickedScrollbar(evt) {
90 | return document.documentElement.clientWidth <= evt.clientX || document.documentElement.clientHeight <= evt.clientY;
91 | }// ideally will get replaced with external dep
92 | // when rafrex/detect-passive-events#4 and rafrex/detect-passive-events#5 get merged in
93 | var testPassiveEventSupport = function testPassiveEventSupport() {
94 | if (typeof window === 'undefined' || typeof window.addEventListener !== 'function') {
95 | return;
96 | }
97 |
98 | var passive = false;
99 | var options = Object.defineProperty({}, 'passive', {
100 | get: function get() {
101 | passive = true;
102 | }
103 | });
104 |
105 | var noop = function noop() {};
106 |
107 | window.addEventListener('testPassiveEventSupport', noop, options);
108 | window.removeEventListener('testPassiveEventSupport', noop, options);
109 | return passive;
110 | };function autoInc(seed) {
111 | if (seed === void 0) {
112 | seed = 0;
113 | }
114 |
115 | return function () {
116 | return ++seed;
117 | };
118 | }
119 |
120 | var uid = autoInc();var passiveEventSupport;
121 | var handlersMap = {};
122 | var enabledInstances = {};
123 | var touchEvents = ['touchstart', 'touchmove'];
124 | var IGNORE_CLASS_NAME = 'ignore-react-onclickoutside';
125 | /**
126 | * Options for addEventHandler and removeEventHandler
127 | */
128 |
129 | function getEventHandlerOptions(instance, eventName) {
130 | var handlerOptions = {};
131 | var isTouchEvent = touchEvents.indexOf(eventName) !== -1;
132 |
133 | if (isTouchEvent && passiveEventSupport) {
134 | handlerOptions.passive = !instance.props.preventDefault;
135 | }
136 |
137 | return handlerOptions;
138 | }
139 | /**
140 | * This function generates the HOC function that you'll use
141 | * in order to impart onOutsideClick listening to an
142 | * arbitrary component. It gets called at the end of the
143 | * bootstrapping code to yield an instance of the
144 | * onClickOutsideHOC function defined inside setupHOC().
145 | */
146 |
147 |
148 | function onClickOutsideHOC(WrappedComponent, config) {
149 | var _class, _temp;
150 |
151 | var componentName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
152 | return _temp = _class = /*#__PURE__*/function (_Component) {
153 | _inheritsLoose(onClickOutside, _Component);
154 |
155 | function onClickOutside(props) {
156 | var _this;
157 |
158 | _this = _Component.call(this, props) || this;
159 |
160 | _this.__outsideClickHandler = function (event) {
161 | if (typeof _this.__clickOutsideHandlerProp === 'function') {
162 | _this.__clickOutsideHandlerProp(event);
163 |
164 | return;
165 | }
166 |
167 | var instance = _this.getInstance();
168 |
169 | if (typeof instance.props.handleClickOutside === 'function') {
170 | instance.props.handleClickOutside(event);
171 | return;
172 | }
173 |
174 | if (typeof instance.handleClickOutside === 'function') {
175 | instance.handleClickOutside(event);
176 | return;
177 | }
178 |
179 | throw new Error("WrappedComponent: " + componentName + " lacks a handleClickOutside(event) function for processing outside click events.");
180 | };
181 |
182 | _this.__getComponentNode = function () {
183 | var instance = _this.getInstance();
184 |
185 | if (config && typeof config.setClickOutsideRef === 'function') {
186 | return config.setClickOutsideRef()(instance);
187 | }
188 |
189 | if (typeof instance.setClickOutsideRef === 'function') {
190 | return instance.setClickOutsideRef();
191 | }
192 |
193 | return reactDom.findDOMNode(instance);
194 | };
195 |
196 | _this.enableOnClickOutside = function () {
197 | if (typeof document === 'undefined' || enabledInstances[_this._uid]) {
198 | return;
199 | }
200 |
201 | if (typeof passiveEventSupport === 'undefined') {
202 | passiveEventSupport = testPassiveEventSupport();
203 | }
204 |
205 | enabledInstances[_this._uid] = true;
206 | var events = _this.props.eventTypes;
207 |
208 | if (!events.forEach) {
209 | events = [events];
210 | }
211 |
212 | handlersMap[_this._uid] = function (event) {
213 | if (_this.componentNode === null) return;
214 | if (_this.initTimeStamp > event.timeStamp) return;
215 |
216 | if (_this.props.preventDefault) {
217 | event.preventDefault();
218 | }
219 |
220 | if (_this.props.stopPropagation) {
221 | event.stopPropagation();
222 | }
223 |
224 | if (_this.props.excludeScrollbar && clickedScrollbar(event)) return;
225 | var current = event.composed && event.composedPath && event.composedPath().shift() || event.target;
226 |
227 | if (findHighest(current, _this.componentNode, _this.props.outsideClickIgnoreClass) !== document) {
228 | return;
229 | }
230 |
231 | _this.__outsideClickHandler(event);
232 | };
233 |
234 | events.forEach(function (eventName) {
235 | document.addEventListener(eventName, handlersMap[_this._uid], getEventHandlerOptions(_assertThisInitialized(_this), eventName));
236 | });
237 | };
238 |
239 | _this.disableOnClickOutside = function () {
240 | delete enabledInstances[_this._uid];
241 | var fn = handlersMap[_this._uid];
242 |
243 | if (fn && typeof document !== 'undefined') {
244 | var events = _this.props.eventTypes;
245 |
246 | if (!events.forEach) {
247 | events = [events];
248 | }
249 |
250 | events.forEach(function (eventName) {
251 | return document.removeEventListener(eventName, fn, getEventHandlerOptions(_assertThisInitialized(_this), eventName));
252 | });
253 | delete handlersMap[_this._uid];
254 | }
255 | };
256 |
257 | _this.getRef = function (ref) {
258 | return _this.instanceRef = ref;
259 | };
260 |
261 | _this._uid = uid();
262 | _this.initTimeStamp = performance.now();
263 | return _this;
264 | }
265 | /**
266 | * Access the WrappedComponent's instance.
267 | */
268 |
269 |
270 | var _proto = onClickOutside.prototype;
271 |
272 | _proto.getInstance = function getInstance() {
273 | if (WrappedComponent.prototype && !WrappedComponent.prototype.isReactComponent) {
274 | return this;
275 | }
276 |
277 | var ref = this.instanceRef;
278 | return ref.getInstance ? ref.getInstance() : ref;
279 | };
280 |
281 | /**
282 | * Add click listeners to the current document,
283 | * linked to this component's state.
284 | */
285 | _proto.componentDidMount = function componentDidMount() {
286 | // If we are in an environment without a DOM such
287 | // as shallow rendering or snapshots then we exit
288 | // early to prevent any unhandled errors being thrown.
289 | if (typeof document === 'undefined' || !document.createElement) {
290 | return;
291 | }
292 |
293 | var instance = this.getInstance();
294 |
295 | if (config && typeof config.handleClickOutside === 'function') {
296 | this.__clickOutsideHandlerProp = config.handleClickOutside(instance);
297 |
298 | if (typeof this.__clickOutsideHandlerProp !== 'function') {
299 | throw new Error("WrappedComponent: " + componentName + " lacks a function for processing outside click events specified by the handleClickOutside config option.");
300 | }
301 | }
302 |
303 | this.componentNode = this.__getComponentNode(); // return early so we dont initiate onClickOutside
304 |
305 | if (this.props.disableOnClickOutside) return;
306 | this.enableOnClickOutside();
307 | };
308 |
309 | _proto.componentDidUpdate = function componentDidUpdate() {
310 | this.componentNode = this.__getComponentNode();
311 | }
312 | /**
313 | * Remove all document's event listeners for this component
314 | */
315 | ;
316 |
317 | _proto.componentWillUnmount = function componentWillUnmount() {
318 | this.disableOnClickOutside();
319 | }
320 | /**
321 | * Can be called to explicitly enable event listening
322 | * for clicks and touches outside of this element.
323 | */
324 | ;
325 |
326 | /**
327 | * Pass-through render
328 | */
329 | _proto.render = function render() {
330 | // eslint-disable-next-line no-unused-vars
331 | var _this$props = this.props;
332 | _this$props.excludeScrollbar;
333 | var props = _objectWithoutPropertiesLoose(_this$props, ["excludeScrollbar"]);
334 |
335 | if (WrappedComponent.prototype && WrappedComponent.prototype.isReactComponent) {
336 | props.ref = this.getRef;
337 | } else {
338 | props.wrappedRef = this.getRef;
339 | }
340 |
341 | props.disableOnClickOutside = this.disableOnClickOutside;
342 | props.enableOnClickOutside = this.enableOnClickOutside;
343 | return react.createElement(WrappedComponent, props);
344 | };
345 |
346 | return onClickOutside;
347 | }(react.Component), _class.displayName = "OnClickOutside(" + componentName + ")", _class.defaultProps = {
348 | eventTypes: ['mousedown', 'touchstart'],
349 | excludeScrollbar: config && config.excludeScrollbar || false,
350 | outsideClickIgnoreClass: IGNORE_CLASS_NAME,
351 | preventDefault: false,
352 | stopPropagation: false
353 | }, _class.getClass = function () {
354 | return WrappedComponent.getClass ? WrappedComponent.getClass() : WrappedComponent;
355 | }, _temp;
356 | }exports.IGNORE_CLASS_NAME=IGNORE_CLASS_NAME;exports.default=onClickOutsideHOC;Object.defineProperty(exports,'__esModule',{value:true});})));
--------------------------------------------------------------------------------
/dist/react-onclickoutside.min.js:
--------------------------------------------------------------------------------
1 | (function(g,f){typeof exports==='object'&&typeof module!=='undefined'?f(exports,require('react'),require('react-dom')):typeof define==='function'&&define.amd?define(['exports','react','react-dom'],f):(g=typeof globalThis!=='undefined'?globalThis:g||self,f(g.onClickOutside={},g.React,g.ReactDOM));}(this,(function(exports, react, reactDom){'use strict';function _inheritsLoose(subClass, superClass) {
2 | subClass.prototype = Object.create(superClass.prototype);
3 | subClass.prototype.constructor = subClass;
4 |
5 | _setPrototypeOf(subClass, superClass);
6 | }
7 |
8 | function _setPrototypeOf(o, p) {
9 | _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) {
10 | o.__proto__ = p;
11 | return o;
12 | };
13 |
14 | return _setPrototypeOf(o, p);
15 | }
16 |
17 | function _objectWithoutPropertiesLoose(source, excluded) {
18 | if (source == null) return {};
19 | var target = {};
20 | var sourceKeys = Object.keys(source);
21 | var key, i;
22 |
23 | for (i = 0; i < sourceKeys.length; i++) {
24 | key = sourceKeys[i];
25 | if (excluded.indexOf(key) >= 0) continue;
26 | target[key] = source[key];
27 | }
28 |
29 | return target;
30 | }
31 |
32 | function _assertThisInitialized(self) {
33 | if (self === void 0) {
34 | throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
35 | }
36 |
37 | return self;
38 | }/**
39 | * Check whether some DOM node is our Component's node.
40 | */
41 | function isNodeFound(current, componentNode, ignoreClass) {
42 | if (current === componentNode) {
43 | return true;
44 | } // SVG elements do not technically reside in the rendered DOM, so
45 | // they do not have classList directly, but they offer a link to their
46 | // corresponding element, which can have classList. This extra check is for
47 | // that case.
48 | // See: http://www.w3.org/TR/SVG11/struct.html#InterfaceSVGUseElement
49 | // Discussion: https://github.com/Pomax/react-onclickoutside/pull/17
50 |
51 |
52 | if (current.correspondingElement) {
53 | return current.correspondingElement.classList.contains(ignoreClass);
54 | }
55 |
56 | return current.classList.contains(ignoreClass);
57 | }
58 | /**
59 | * Try to find our node in a hierarchy of nodes, returning the document
60 | * node as highest node if our node is not found in the path up.
61 | */
62 |
63 | function findHighest(current, componentNode, ignoreClass) {
64 | if (current === componentNode) {
65 | return true;
66 | } // If source=local then this event came from 'somewhere'
67 | // inside and should be ignored. We could handle this with
68 | // a layered approach, too, but that requires going back to
69 | // thinking in terms of Dom node nesting, running counter
70 | // to React's 'you shouldn't care about the DOM' philosophy.
71 | // Also cover shadowRoot node by checking current.host
72 |
73 |
74 | while (current.parentNode || current.host) {
75 | // Only check normal node without shadowRoot
76 | if (current.parentNode && isNodeFound(current, componentNode, ignoreClass)) {
77 | return true;
78 | }
79 |
80 | current = current.parentNode || current.host;
81 | }
82 |
83 | return current;
84 | }
85 | /**
86 | * Check if the browser scrollbar was clicked
87 | */
88 |
89 | function clickedScrollbar(evt) {
90 | return document.documentElement.clientWidth <= evt.clientX || document.documentElement.clientHeight <= evt.clientY;
91 | }// ideally will get replaced with external dep
92 | // when rafrex/detect-passive-events#4 and rafrex/detect-passive-events#5 get merged in
93 | var testPassiveEventSupport = function testPassiveEventSupport() {
94 | if (typeof window === 'undefined' || typeof window.addEventListener !== 'function') {
95 | return;
96 | }
97 |
98 | var passive = false;
99 | var options = Object.defineProperty({}, 'passive', {
100 | get: function get() {
101 | passive = true;
102 | }
103 | });
104 |
105 | var noop = function noop() {};
106 |
107 | window.addEventListener('testPassiveEventSupport', noop, options);
108 | window.removeEventListener('testPassiveEventSupport', noop, options);
109 | return passive;
110 | };function autoInc(seed) {
111 | if (seed === void 0) {
112 | seed = 0;
113 | }
114 |
115 | return function () {
116 | return ++seed;
117 | };
118 | }
119 |
120 | var uid = autoInc();var passiveEventSupport;
121 | var handlersMap = {};
122 | var enabledInstances = {};
123 | var touchEvents = ['touchstart', 'touchmove'];
124 | var IGNORE_CLASS_NAME = 'ignore-react-onclickoutside';
125 | /**
126 | * Options for addEventHandler and removeEventHandler
127 | */
128 |
129 | function getEventHandlerOptions(instance, eventName) {
130 | var handlerOptions = {};
131 | var isTouchEvent = touchEvents.indexOf(eventName) !== -1;
132 |
133 | if (isTouchEvent && passiveEventSupport) {
134 | handlerOptions.passive = !instance.props.preventDefault;
135 | }
136 |
137 | return handlerOptions;
138 | }
139 | /**
140 | * This function generates the HOC function that you'll use
141 | * in order to impart onOutsideClick listening to an
142 | * arbitrary component. It gets called at the end of the
143 | * bootstrapping code to yield an instance of the
144 | * onClickOutsideHOC function defined inside setupHOC().
145 | */
146 |
147 |
148 | function onClickOutsideHOC(WrappedComponent, config) {
149 | var _class, _temp;
150 |
151 | var componentName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
152 | return _temp = _class = /*#__PURE__*/function (_Component) {
153 | _inheritsLoose(onClickOutside, _Component);
154 |
155 | function onClickOutside(props) {
156 | var _this;
157 |
158 | _this = _Component.call(this, props) || this;
159 |
160 | _this.__outsideClickHandler = function (event) {
161 | if (typeof _this.__clickOutsideHandlerProp === 'function') {
162 | _this.__clickOutsideHandlerProp(event);
163 |
164 | return;
165 | }
166 |
167 | var instance = _this.getInstance();
168 |
169 | if (typeof instance.props.handleClickOutside === 'function') {
170 | instance.props.handleClickOutside(event);
171 | return;
172 | }
173 |
174 | if (typeof instance.handleClickOutside === 'function') {
175 | instance.handleClickOutside(event);
176 | return;
177 | }
178 |
179 | throw new Error("WrappedComponent: " + componentName + " lacks a handleClickOutside(event) function for processing outside click events.");
180 | };
181 |
182 | _this.__getComponentNode = function () {
183 | var instance = _this.getInstance();
184 |
185 | if (config && typeof config.setClickOutsideRef === 'function') {
186 | return config.setClickOutsideRef()(instance);
187 | }
188 |
189 | if (typeof instance.setClickOutsideRef === 'function') {
190 | return instance.setClickOutsideRef();
191 | }
192 |
193 | return reactDom.findDOMNode(instance);
194 | };
195 |
196 | _this.enableOnClickOutside = function () {
197 | if (typeof document === 'undefined' || enabledInstances[_this._uid]) {
198 | return;
199 | }
200 |
201 | if (typeof passiveEventSupport === 'undefined') {
202 | passiveEventSupport = testPassiveEventSupport();
203 | }
204 |
205 | enabledInstances[_this._uid] = true;
206 | var events = _this.props.eventTypes;
207 |
208 | if (!events.forEach) {
209 | events = [events];
210 | }
211 |
212 | handlersMap[_this._uid] = function (event) {
213 | if (_this.componentNode === null) return;
214 | if (_this.initTimeStamp > event.timeStamp) return;
215 |
216 | if (_this.props.preventDefault) {
217 | event.preventDefault();
218 | }
219 |
220 | if (_this.props.stopPropagation) {
221 | event.stopPropagation();
222 | }
223 |
224 | if (_this.props.excludeScrollbar && clickedScrollbar(event)) return;
225 | var current = event.composed && event.composedPath && event.composedPath().shift() || event.target;
226 |
227 | if (findHighest(current, _this.componentNode, _this.props.outsideClickIgnoreClass) !== document) {
228 | return;
229 | }
230 |
231 | _this.__outsideClickHandler(event);
232 | };
233 |
234 | events.forEach(function (eventName) {
235 | document.addEventListener(eventName, handlersMap[_this._uid], getEventHandlerOptions(_assertThisInitialized(_this), eventName));
236 | });
237 | };
238 |
239 | _this.disableOnClickOutside = function () {
240 | delete enabledInstances[_this._uid];
241 | var fn = handlersMap[_this._uid];
242 |
243 | if (fn && typeof document !== 'undefined') {
244 | var events = _this.props.eventTypes;
245 |
246 | if (!events.forEach) {
247 | events = [events];
248 | }
249 |
250 | events.forEach(function (eventName) {
251 | return document.removeEventListener(eventName, fn, getEventHandlerOptions(_assertThisInitialized(_this), eventName));
252 | });
253 | delete handlersMap[_this._uid];
254 | }
255 | };
256 |
257 | _this.getRef = function (ref) {
258 | return _this.instanceRef = ref;
259 | };
260 |
261 | _this._uid = uid();
262 | _this.initTimeStamp = performance.now();
263 | return _this;
264 | }
265 | /**
266 | * Access the WrappedComponent's instance.
267 | */
268 |
269 |
270 | var _proto = onClickOutside.prototype;
271 |
272 | _proto.getInstance = function getInstance() {
273 | if (WrappedComponent.prototype && !WrappedComponent.prototype.isReactComponent) {
274 | return this;
275 | }
276 |
277 | var ref = this.instanceRef;
278 | return ref.getInstance ? ref.getInstance() : ref;
279 | };
280 |
281 | /**
282 | * Add click listeners to the current document,
283 | * linked to this component's state.
284 | */
285 | _proto.componentDidMount = function componentDidMount() {
286 | // If we are in an environment without a DOM such
287 | // as shallow rendering or snapshots then we exit
288 | // early to prevent any unhandled errors being thrown.
289 | if (typeof document === 'undefined' || !document.createElement) {
290 | return;
291 | }
292 |
293 | var instance = this.getInstance();
294 |
295 | if (config && typeof config.handleClickOutside === 'function') {
296 | this.__clickOutsideHandlerProp = config.handleClickOutside(instance);
297 |
298 | if (typeof this.__clickOutsideHandlerProp !== 'function') {
299 | throw new Error("WrappedComponent: " + componentName + " lacks a function for processing outside click events specified by the handleClickOutside config option.");
300 | }
301 | }
302 |
303 | this.componentNode = this.__getComponentNode(); // return early so we dont initiate onClickOutside
304 |
305 | if (this.props.disableOnClickOutside) return;
306 | this.enableOnClickOutside();
307 | };
308 |
309 | _proto.componentDidUpdate = function componentDidUpdate() {
310 | this.componentNode = this.__getComponentNode();
311 | }
312 | /**
313 | * Remove all document's event listeners for this component
314 | */
315 | ;
316 |
317 | _proto.componentWillUnmount = function componentWillUnmount() {
318 | this.disableOnClickOutside();
319 | }
320 | /**
321 | * Can be called to explicitly enable event listening
322 | * for clicks and touches outside of this element.
323 | */
324 | ;
325 |
326 | /**
327 | * Pass-through render
328 | */
329 | _proto.render = function render() {
330 | // eslint-disable-next-line no-unused-vars
331 | var _this$props = this.props;
332 | _this$props.excludeScrollbar;
333 | var props = _objectWithoutPropertiesLoose(_this$props, ["excludeScrollbar"]);
334 |
335 | if (WrappedComponent.prototype && WrappedComponent.prototype.isReactComponent) {
336 | props.ref = this.getRef;
337 | } else {
338 | props.wrappedRef = this.getRef;
339 | }
340 |
341 | props.disableOnClickOutside = this.disableOnClickOutside;
342 | props.enableOnClickOutside = this.enableOnClickOutside;
343 | return react.createElement(WrappedComponent, props);
344 | };
345 |
346 | return onClickOutside;
347 | }(react.Component), _class.displayName = "OnClickOutside(" + componentName + ")", _class.defaultProps = {
348 | eventTypes: ['mousedown', 'touchstart'],
349 | excludeScrollbar: config && config.excludeScrollbar || false,
350 | outsideClickIgnoreClass: IGNORE_CLASS_NAME,
351 | preventDefault: false,
352 | stopPropagation: false
353 | }, _class.getClass = function () {
354 | return WrappedComponent.getClass ? WrappedComponent.getClass() : WrappedComponent;
355 | }, _temp;
356 | }exports.IGNORE_CLASS_NAME=IGNORE_CLASS_NAME;exports.default=onClickOutsideHOC;Object.defineProperty(exports,'__esModule',{value:true});})));
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-onclickoutside",
3 | "version": "6.13.2",
4 | "description": "An onClickOutside wrapper for React components",
5 | "main": "dist/react-onclickoutside.cjs.js",
6 | "module": "dist/react-onclickoutside.es.js",
7 | "jsnext:main": "dist/react-onclickoutside.es.js",
8 | "files": [
9 | "dist"
10 | ],
11 | "unpkg": "dist/react-onclickoutside.min.js",
12 | "homepage": "https://github.com/Pomax/react-onclickoutside",
13 | "authors": [
14 | "Pomax ",
15 | "Andarist "
16 | ],
17 | "funding": {
18 | "type": "individual",
19 | "url": "https://github.com/Pomax/react-onclickoutside/blob/master/FUNDING.md"
20 | },
21 | "keywords": [
22 | "react",
23 | "onclick",
24 | "outside",
25 | "onclickoutside"
26 | ],
27 | "license": "MIT",
28 | "repository": {
29 | "type": "git",
30 | "url": "https://github.com/Pomax/react-onclickoutside.git"
31 | },
32 | "bugs": {
33 | "url": "https://github.com/Pomax/react-onclickoutside/issues"
34 | },
35 | "scripts": {
36 | "release:patch": "npm run prerelease && npm version patch && npm publish && git push --follow-tags",
37 | "release:minor": "npm run prerelease && npm version minor && npm publish && git push --follow-tags",
38 | "release:major": "npm run prerelease && npm version major && npm publish && git push --follow-tags",
39 | "prerelease": "npm run test",
40 | "test": "run-s test:**",
41 | "test:basic": "run-s lint build",
42 | "test:nodom": "mocha test/no-dom-test.js",
43 | "lint": "eslint src/*.js ./test",
44 | "build": "rollup -c rollup.config.js --compact",
45 | "prebuild": "npm run clean",
46 | "clean": "rimraf dist",
47 | "precommit": "npm test && lint-staged"
48 | },
49 | "devDependencies": {
50 | "@babel/core": "^7.14.2",
51 | "@babel/plugin-proposal-class-properties": "^7.13.0",
52 | "@babel/plugin-proposal-object-rest-spread": "^7.14.2",
53 | "@babel/preset-env": "^7.14.2",
54 | "@babel/preset-stage-0": "^7.8.3",
55 | "@rollup/plugin-babel": "^5.3.0",
56 | "babel-eslint": "^8.0.2",
57 | "babel-loader": "^8.2.2",
58 | "chai": "^4.1.2",
59 | "eslint": "^4.12.0",
60 | "husky": "^0.14.3",
61 | "lint-staged": "^5.0.0",
62 | "mocha": "^8.4.0",
63 | "npm-run-all": "^4.0.2",
64 | "prettier": "^1.8.2",
65 | "react": "^15.5.x",
66 | "react-dom": "^15.5.x",
67 | "react-test-renderer": "^15.5.x",
68 | "require-hijack": "^1.2.1",
69 | "rimraf": "^2.6.2",
70 | "rollup": "^2.50.1",
71 | "webpack": "^5.37.1"
72 | },
73 | "peerDependencies": {
74 | "react": "^15.5.x || ^16.x || ^17.x || ^18.x",
75 | "react-dom": "^15.5.x || ^16.x || ^17.x || ^18.x"
76 | },
77 | "lint-staged": {
78 | "{src,test}/**/*.js": [
79 | "prettier --print-width=120 --single-quote --trailing-comma=all --write",
80 | "eslint --fix",
81 | "git add"
82 | ],
83 | "*.md": [
84 | "prettier --write",
85 | "git add"
86 | ]
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import babel from '@rollup/plugin-babel';
2 | import pkg from './package.json';
3 |
4 | const mergeAll = objs => Object.assign({}, ...objs);
5 |
6 | const commonPlugins = [
7 | babel({
8 | exclude: 'node_modules/**',
9 | babelHelpers: 'bundled',
10 | }),
11 | ];
12 |
13 | const configBase = {
14 | input: 'src/index.js',
15 | external: Object.keys(pkg.dependencies || {}).concat(Object.keys(pkg.peerDependencies || {})),
16 | plugins: commonPlugins,
17 | };
18 |
19 | const devUmdConfig = mergeAll([
20 | configBase,
21 | {
22 | output: {
23 | file: pkg.unpkg.replace(/\.min\.js$/, '.js'),
24 | format: 'umd',
25 | name: 'onClickOutside',
26 | exports: 'named',
27 | globals: { react: 'React', 'react-dom': 'ReactDOM' },
28 | },
29 | external: Object.keys(pkg.peerDependencies || {}),
30 | },
31 | ]);
32 |
33 | const prodUmdConfig = mergeAll([
34 | devUmdConfig,
35 | { output: mergeAll([devUmdConfig.output, { file: pkg.unpkg }]) },
36 | {
37 | plugins: devUmdConfig.plugins
38 | },
39 | ]);
40 |
41 | const webConfig = mergeAll([
42 | configBase,
43 | {
44 | output: [
45 | { file: pkg.module, format: 'es' },
46 | { file: pkg.main, format: 'cjs', exports: 'named' }
47 | ],
48 | },
49 | ]);
50 |
51 | export default [devUmdConfig, prodUmdConfig, webConfig];
52 |
--------------------------------------------------------------------------------
/src/detect-passive-events.js:
--------------------------------------------------------------------------------
1 | // ideally will get replaced with external dep
2 | // when rafrex/detect-passive-events#4 and rafrex/detect-passive-events#5 get merged in
3 | export const testPassiveEventSupport = () => {
4 | if (typeof window === 'undefined' || typeof window.addEventListener !== 'function') {
5 | return;
6 | }
7 |
8 | let passive = false;
9 |
10 | const options = Object.defineProperty({}, 'passive', {
11 | get() {
12 | passive = true;
13 | },
14 | });
15 |
16 | const noop = () => {};
17 |
18 | window.addEventListener('testPassiveEventSupport', noop, options);
19 | window.removeEventListener('testPassiveEventSupport', noop, options);
20 |
21 | return passive;
22 | };
23 |
--------------------------------------------------------------------------------
/src/dom-helpers.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Check whether some DOM node is our Component's node.
3 | */
4 | export function isNodeFound(current, componentNode, ignoreClass) {
5 | if (current === componentNode) {
6 | return true;
7 | }
8 | // SVG elements do not technically reside in the rendered DOM, so
9 | // they do not have classList directly, but they offer a link to their
10 | // corresponding element, which can have classList. This extra check is for
11 | // that case.
12 | // See: http://www.w3.org/TR/SVG11/struct.html#InterfaceSVGUseElement
13 | // Discussion: https://github.com/Pomax/react-onclickoutside/pull/17
14 | if (current.correspondingElement) {
15 | return current.correspondingElement.classList.contains(ignoreClass);
16 | }
17 | return current.classList.contains(ignoreClass);
18 | }
19 |
20 | /**
21 | * Try to find our node in a hierarchy of nodes, returning the document
22 | * node as highest node if our node is not found in the path up.
23 | */
24 | export function findHighest(current, componentNode, ignoreClass) {
25 | if (current === componentNode) {
26 | return true;
27 | }
28 |
29 | // If source=local then this event came from 'somewhere'
30 | // inside and should be ignored. We could handle this with
31 | // a layered approach, too, but that requires going back to
32 | // thinking in terms of Dom node nesting, running counter
33 | // to React's 'you shouldn't care about the DOM' philosophy.
34 | // Also cover shadowRoot node by checking current.host
35 | while (current.parentNode || current.host) {
36 | // Only check normal node without shadowRoot
37 | if (current.parentNode && isNodeFound(current, componentNode, ignoreClass)) {
38 | return true;
39 | }
40 | current = current.parentNode || current.host;
41 | }
42 | return current;
43 | }
44 |
45 | /**
46 | * Check if the browser scrollbar was clicked
47 | */
48 | export function clickedScrollbar(evt) {
49 | return document.documentElement.clientWidth <= evt.clientX || document.documentElement.clientHeight <= evt.clientY;
50 | }
51 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import { createElement, Component } from 'react';
2 | import { findDOMNode } from 'react-dom';
3 | import * as DOMHelpers from './dom-helpers';
4 | import { testPassiveEventSupport } from './detect-passive-events';
5 | import uid from './uid';
6 |
7 | let passiveEventSupport;
8 |
9 | const handlersMap = {};
10 | const enabledInstances = {};
11 |
12 | const touchEvents = ['touchstart', 'touchmove'];
13 | export const IGNORE_CLASS_NAME = 'ignore-react-onclickoutside';
14 |
15 | /**
16 | * Options for addEventHandler and removeEventHandler
17 | */
18 | function getEventHandlerOptions(instance, eventName) {
19 | const handlerOptions = {};
20 | const isTouchEvent = touchEvents.indexOf(eventName) !== -1;
21 |
22 | if (isTouchEvent && passiveEventSupport) {
23 | handlerOptions.passive = !instance.props.preventDefault;
24 | }
25 |
26 | return handlerOptions;
27 | }
28 |
29 | /**
30 | * This function generates the HOC function that you'll use
31 | * in order to impart onOutsideClick listening to an
32 | * arbitrary component. It gets called at the end of the
33 | * bootstrapping code to yield an instance of the
34 | * onClickOutsideHOC function defined inside setupHOC().
35 | */
36 | export default function onClickOutsideHOC(WrappedComponent, config) {
37 | const componentName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
38 | return class onClickOutside extends Component {
39 | static displayName = `OnClickOutside(${componentName})`;
40 |
41 | static defaultProps = {
42 | eventTypes: ['mousedown', 'touchstart'],
43 | excludeScrollbar: (config && config.excludeScrollbar) || false,
44 | outsideClickIgnoreClass: IGNORE_CLASS_NAME,
45 | preventDefault: false,
46 | stopPropagation: false,
47 | };
48 |
49 | static getClass = () => (WrappedComponent.getClass ? WrappedComponent.getClass() : WrappedComponent);
50 |
51 | constructor(props) {
52 | super(props);
53 | this._uid = uid();
54 | this.initTimeStamp = performance.now();
55 | }
56 |
57 | /**
58 | * Access the WrappedComponent's instance.
59 | */
60 | getInstance() {
61 | if (WrappedComponent.prototype && !WrappedComponent.prototype.isReactComponent) {
62 | return this;
63 | }
64 | const ref = this.instanceRef;
65 | return ref.getInstance ? ref.getInstance() : ref;
66 | }
67 |
68 | __outsideClickHandler = event => {
69 | if (typeof this.__clickOutsideHandlerProp === 'function') {
70 | this.__clickOutsideHandlerProp(event);
71 | return;
72 | }
73 |
74 | const instance = this.getInstance();
75 |
76 | if (typeof instance.props.handleClickOutside === 'function') {
77 | instance.props.handleClickOutside(event);
78 | return;
79 | }
80 |
81 | if (typeof instance.handleClickOutside === 'function') {
82 | instance.handleClickOutside(event);
83 | return;
84 | }
85 |
86 | throw new Error(
87 | `WrappedComponent: ${componentName} lacks a handleClickOutside(event) function for processing outside click events.`,
88 | );
89 | };
90 |
91 | __getComponentNode = () => {
92 | const instance = this.getInstance();
93 |
94 | if (config && typeof config.setClickOutsideRef === 'function') {
95 | return config.setClickOutsideRef()(instance);
96 | }
97 |
98 | if (typeof instance.setClickOutsideRef === 'function') {
99 | return instance.setClickOutsideRef();
100 | }
101 |
102 | return findDOMNode(instance);
103 | };
104 |
105 | /**
106 | * Add click listeners to the current document,
107 | * linked to this component's state.
108 | */
109 | componentDidMount() {
110 | // If we are in an environment without a DOM such
111 | // as shallow rendering or snapshots then we exit
112 | // early to prevent any unhandled errors being thrown.
113 | if (typeof document === 'undefined' || !document.createElement) {
114 | return;
115 | }
116 |
117 | const instance = this.getInstance();
118 |
119 | if (config && typeof config.handleClickOutside === 'function') {
120 | this.__clickOutsideHandlerProp = config.handleClickOutside(instance);
121 | if (typeof this.__clickOutsideHandlerProp !== 'function') {
122 | throw new Error(
123 | `WrappedComponent: ${componentName} lacks a function for processing outside click events specified by the handleClickOutside config option.`,
124 | );
125 | }
126 | }
127 |
128 | this.componentNode = this.__getComponentNode();
129 | // return early so we dont initiate onClickOutside
130 | if (this.props.disableOnClickOutside) return;
131 | this.enableOnClickOutside();
132 | }
133 |
134 | componentDidUpdate() {
135 | this.componentNode = this.__getComponentNode();
136 | }
137 |
138 | /**
139 | * Remove all document's event listeners for this component
140 | */
141 | componentWillUnmount() {
142 | this.disableOnClickOutside();
143 | }
144 |
145 | /**
146 | * Can be called to explicitly enable event listening
147 | * for clicks and touches outside of this element.
148 | */
149 | enableOnClickOutside = () => {
150 | if (typeof document === 'undefined' || enabledInstances[this._uid]) {
151 | return;
152 | }
153 |
154 | if (typeof passiveEventSupport === 'undefined') {
155 | passiveEventSupport = testPassiveEventSupport();
156 | }
157 |
158 | enabledInstances[this._uid] = true;
159 |
160 | let events = this.props.eventTypes;
161 | if (!events.forEach) {
162 | events = [events];
163 | }
164 |
165 | handlersMap[this._uid] = event => {
166 | if (this.componentNode === null) return;
167 |
168 | if (this.initTimeStamp > event.timeStamp) return;
169 |
170 | if (this.props.preventDefault) {
171 | event.preventDefault();
172 | }
173 |
174 | if (this.props.stopPropagation) {
175 | event.stopPropagation();
176 | }
177 |
178 | if (this.props.excludeScrollbar && DOMHelpers.clickedScrollbar(event)) return;
179 |
180 | const current = (event.composed && event.composedPath && event.composedPath().shift()) || event.target;
181 |
182 | if (DOMHelpers.findHighest(current, this.componentNode, this.props.outsideClickIgnoreClass) !== document) {
183 | return;
184 | }
185 |
186 | this.__outsideClickHandler(event);
187 | };
188 |
189 | events.forEach(eventName => {
190 | document.addEventListener(eventName, handlersMap[this._uid], getEventHandlerOptions(this, eventName));
191 | });
192 | };
193 |
194 | /**
195 | * Can be called to explicitly disable event listening
196 | * for clicks and touches outside of this element.
197 | */
198 | disableOnClickOutside = () => {
199 | delete enabledInstances[this._uid];
200 | const fn = handlersMap[this._uid];
201 |
202 | if (fn && typeof document !== 'undefined') {
203 | let events = this.props.eventTypes;
204 | if (!events.forEach) {
205 | events = [events];
206 | }
207 | events.forEach(eventName =>
208 | document.removeEventListener(eventName, fn, getEventHandlerOptions(this, eventName)),
209 | );
210 | delete handlersMap[this._uid];
211 | }
212 | };
213 |
214 | getRef = ref => (this.instanceRef = ref);
215 |
216 | /**
217 | * Pass-through render
218 | */
219 | render() {
220 | // eslint-disable-next-line no-unused-vars
221 | let { excludeScrollbar, ...props } = this.props;
222 |
223 | if (WrappedComponent.prototype && WrappedComponent.prototype.isReactComponent) {
224 | props.ref = this.getRef;
225 | } else {
226 | props.wrappedRef = this.getRef;
227 | }
228 |
229 | props.disableOnClickOutside = this.disableOnClickOutside;
230 | props.enableOnClickOutside = this.enableOnClickOutside;
231 |
232 | return createElement(WrappedComponent, props);
233 | }
234 | };
235 | }
236 |
--------------------------------------------------------------------------------
/src/uid.js:
--------------------------------------------------------------------------------
1 | function autoInc(seed = 0) {
2 | return () => ++seed;
3 | }
4 |
5 | export default autoInc();
6 |
--------------------------------------------------------------------------------
/test/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "parserOptions": {
3 | "ecmaVersion": 6
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/test/browser/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | OnOutsideClick
6 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
This test presents three concentric circles. Clicking inside a circle marks it as lit, clicking outside of it unmarks it.
44 |
45 |
46 |
47 |
48 |
49 |
This test presents a text, revealed with a button press. Clicking the text should preserve it, while clicking anywhere else should hide it.
50 |
51 |
52 |
53 |
54 |
This test is using custom element to render react app inside shadow DOM.
55 |
56 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/test/browser/style2.css:
--------------------------------------------------------------------------------
1 | .popup {
2 | padding: 15px;
3 | border: 1px solid black;
4 | }
5 |
6 | .workspace {
7 | padding: 15px;
8 | height: 200px;
9 | border: 1px dashed black;
10 | }
11 |
--------------------------------------------------------------------------------
/test/browser/test1.js:
--------------------------------------------------------------------------------
1 | (function test1(onClickOutside) {
2 | onClickOutside = onClickOutside.default;
3 |
4 | /**
5 | * Human-triggered for now, this should become a normal phantom test instead
6 | */
7 | class BaseComponent extends React.Component {
8 | constructor(props) {
9 | super(props);
10 | this.state = {
11 | highlight: false,
12 | };
13 | }
14 |
15 | handleClickOutside() {
16 | console.log('remove highlight', this.props.id);
17 | this.setState({ highlight: false });
18 | }
19 |
20 | highlight() {
21 | console.log('highlight', this.props.id);
22 | this.setState({ highlight: true });
23 | }
24 |
25 | render() {
26 | var className = 'concentric' + (this.state.highlight ? ' highlight' : '');
27 | return React.createElement('div', {
28 | className: className,
29 | children: this.props.children,
30 | onClick: e => this.highlight(e),
31 | });
32 | }
33 | }
34 |
35 | const Nested = onClickOutside(BaseComponent);
36 |
37 | const App = function() {
38 | return React.createElement(Nested, {
39 | id: 1,
40 | stopPropagation: true,
41 | children: React.createElement(Nested, {
42 | id: 2,
43 | stopPropagation: true,
44 | children: React.createElement(Nested, {
45 | id: 3,
46 | stopPropagation: true,
47 | children: React.createElement('div', { className: 'label', children: ['test'] }),
48 | }),
49 | }),
50 | });
51 | };
52 |
53 | ReactDOM.render(React.createElement(App), document.getElementById('app1'));
54 | })(onClickOutside); /* global onClickOutside */
55 |
--------------------------------------------------------------------------------
/test/browser/test2.js:
--------------------------------------------------------------------------------
1 | (function test2(onClickOutside) {
2 | onClickOutside = onClickOutside.default;
3 |
4 | class BasePopup extends React.Component {
5 | constructor(props) {
6 | super(props);
7 | }
8 | render() {
9 | return React.createElement('span', { children: 'click this text' });
10 | }
11 | handleClickOutside() {
12 | this.props.hide();
13 | }
14 | }
15 |
16 | const Popup = onClickOutside(BasePopup);
17 |
18 | class App extends React.Component {
19 | constructor(props) {
20 | super(props);
21 | this.state = {
22 | hideToolbox: true,
23 | };
24 | }
25 | render() {
26 | return React.createElement('div', {
27 | children: [
28 | React.createElement('button', {
29 | onClick: e => this.state.hideToolbox && this.show(e),
30 | children: 'show text',
31 | }),
32 | this.state.hideToolbox ? null : React.createElement(Popup, { hide: e => this.hide(e) }),
33 | ],
34 | });
35 | }
36 | show() {
37 | console.log('test2 - show');
38 | this.setState({ hideToolbox: false });
39 | }
40 | hide() {
41 | console.log('test2 - hide');
42 | this.setState({ hideToolbox: true });
43 | }
44 | }
45 |
46 | ReactDOM.render(React.createElement(App), document.getElementById('app2'));
47 | })(onClickOutside); /* global onClickOutside */
48 |
--------------------------------------------------------------------------------
/test/browser/test3.js:
--------------------------------------------------------------------------------
1 | (function test3(onClickOutside) {
2 | onClickOutside = onClickOutside.default;
3 |
4 | class BasePopup extends React.Component {
5 | constructor(props) {
6 | super(props);
7 | }
8 | render() {
9 | return React.createElement('p', { children: 'click this text (This is inside shadow DOM)' });
10 | }
11 | handleClickOutside() {
12 | this.props.hide();
13 | }
14 | }
15 |
16 | const Popup = onClickOutside(BasePopup);
17 |
18 | class App extends React.Component {
19 | constructor(props) {
20 | super(props);
21 | this.state = {
22 | hideToolbox: true,
23 | };
24 | }
25 | render() {
26 | return React.createElement('div', {
27 | children: [
28 | React.createElement('button', {
29 | onClick: e => this.state.hideToolbox && this.show(e),
30 | children: 'show text inside shadow DOM',
31 | }),
32 | this.state.hideToolbox ? null : React.createElement(Popup, { hide: e => this.hide(e) }),
33 | ],
34 | });
35 | }
36 | show() {
37 | this.setState({ hideToolbox: false });
38 | }
39 | hide() {
40 | this.setState({ hideToolbox: true });
41 | }
42 | }
43 |
44 | customElements.define('test-app-3',
45 | class extends HTMLElement {
46 | constructor() {
47 | super();
48 |
49 | let shadowRoot = this.attachShadow({ mode: 'open' });
50 | ReactDOM.render(React.createElement(App), shadowRoot);
51 | }
52 | });
53 |
54 | })(onClickOutside); /* global onClickOutside */
55 |
--------------------------------------------------------------------------------
/test/no-dom-test.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var assert = require('assert');
3 | var React = require('react');
4 | var renderer = require('react-test-renderer');
5 | var requireHijack = require('require-hijack');
6 |
7 | describe('onclickoutside hoc with no DOM', function() {
8 | class Component extends React.Component {
9 | handleClickOutside() {}
10 |
11 | render() {
12 | return React.createElement('div');
13 | }
14 | }
15 |
16 | // tests
17 |
18 | it('should not throw an error if rendered in an environment with no DOM', function() {
19 | // Needed until React 15.4 lands due to https://github.com/facebook/react/issues/7386.
20 | requireHijack.replace('react-dom').with({
21 | render: function() {},
22 | });
23 |
24 | // Must import this after we mock out ReactDOM to prevent the inject error.
25 | var wrapComponent = require('../').default;
26 | var WrappedComponent = wrapComponent(Component);
27 |
28 | var element = React.createElement(WrappedComponent);
29 | assert(element, 'element can be created');
30 | var renderInstance = renderer.create(element);
31 | assert(renderInstance.toJSON().type === 'div', 'wrapped component renders a div');
32 | });
33 | });
34 |
--------------------------------------------------------------------------------
/test/test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import TestUtils from 'react-dom/test-utils';
4 | import wrapComponent from '../';
5 |
6 | describe('onclickoutside hoc', function() {
7 | class Component extends React.Component {
8 | constructor(props) {
9 | super(props);
10 | this.state = {
11 | clickOutsideHandled: false,
12 | timesHandlerCalled: 0,
13 | };
14 | }
15 |
16 | toggleEnableClickOutside(on) {
17 | if (on) {
18 | this.props.enableOnClickOutside();
19 | } else {
20 | this.props.disableOnClickOutside();
21 | }
22 | }
23 |
24 | handleClickOutside(event) {
25 | if (event === undefined) {
26 | throw new Error('event cannot be undefined');
27 | }
28 |
29 | this.setState({
30 | clickOutsideHandled: true,
31 | timesHandlerCalled: this.state.timesHandlerCalled + 1,
32 | });
33 | }
34 |
35 | render() {
36 | return React.createElement('div');
37 | }
38 | }
39 |
40 | var WrappedComponent = wrapComponent(Component);
41 |
42 | // tests
43 |
44 | it('should call handleClickOutside when clicking the document', function() {
45 | var element = React.createElement(WrappedComponent);
46 | assert(element, 'element can be created');
47 | var component = TestUtils.renderIntoDocument(element);
48 | assert(component, 'component renders correctly');
49 | document.dispatchEvent(new Event('mousedown'));
50 | var instance = component.getInstance();
51 | assert(instance.state.clickOutsideHandled, 'clickOutsideHandled got flipped');
52 | });
53 |
54 | it('should throw an error when a component without handleClickOutside(evt) is wrapped', function() {
55 | class BadComponent extends React.Component {
56 | render() {
57 | return React.createElement('div');
58 | }
59 | }
60 |
61 | try {
62 | wrapComponent(BadComponent);
63 | assert(false, 'component was wrapped, despite not implementing handleClickOutside(evt)');
64 | } catch (e) {
65 | assert(e, 'component was not wrapped');
66 | }
67 | });
68 |
69 | describe('with instance method', function() {
70 | it('and class inheritance, should call the specified handler when clicking the document', function() {
71 | class Component extends React.Component {
72 | constructor(...args) {
73 | super(...args);
74 | this.state = {
75 | clickOutsideHandled: false,
76 | };
77 | }
78 |
79 | handleClickOutside(event) {
80 | if (event === undefined) {
81 | throw new Error('event cannot be undefined');
82 | }
83 |
84 | this.setState({
85 | clickOutsideHandled: true,
86 | });
87 | }
88 |
89 | render() {
90 | return React.createElement('div');
91 | }
92 | }
93 |
94 | var WrappedWithCustomHandler = wrapComponent(Component);
95 |
96 | var element = React.createElement(WrappedWithCustomHandler);
97 | assert(element, 'element can be created');
98 | var component = TestUtils.renderIntoDocument(element);
99 | assert(component, 'component renders correctly');
100 | document.dispatchEvent(new Event('mousedown'));
101 | var instance = component.getInstance();
102 | assert(instance.state.clickOutsideHandled, 'clickOutsideHandled got flipped');
103 | });
104 |
105 | it('and createClass method, should call the specified handler when clicking the document', function() {
106 | class Component extends React.Component {
107 | constructor(props) {
108 | super(props);
109 | this.state = {
110 | clickOutsideHandled: false,
111 | };
112 | }
113 |
114 | handleClickOutside(event) {
115 | if (event === undefined) {
116 | throw new Error('event cannot be undefined');
117 | }
118 |
119 | this.setState({
120 | clickOutsideHandled: true,
121 | });
122 | }
123 |
124 | render() {
125 | return React.createElement('div');
126 | }
127 | }
128 |
129 | var WrappedWithCustomHandler = wrapComponent(Component);
130 |
131 | var element = React.createElement(WrappedWithCustomHandler);
132 | assert(element, 'element can be created');
133 | var component = TestUtils.renderIntoDocument(element);
134 | assert(component, 'component renders correctly');
135 | document.dispatchEvent(new Event('mousedown'));
136 | var instance = component.getInstance();
137 | assert(instance.state.clickOutsideHandled, 'clickOutsideHandled got flipped');
138 | });
139 | });
140 |
141 | describe('with property', function() {
142 | it('and class inheritance, should call the specified handler when clicking the document', function() {
143 | class Component extends React.Component {
144 | render() {
145 | return React.createElement('div');
146 | }
147 | }
148 |
149 | var clickOutsideHandled = false;
150 | var handleClickOutside = function(event) {
151 | if (event === undefined) {
152 | throw new Error('event cannot be undefined');
153 | }
154 |
155 | clickOutsideHandled = true;
156 | };
157 |
158 | var WrappedWithCustomHandler = wrapComponent(Component);
159 |
160 | var element = React.createElement(WrappedWithCustomHandler, { handleClickOutside: handleClickOutside });
161 | assert(element, 'element can be created');
162 | var component = TestUtils.renderIntoDocument(element);
163 | assert(component, 'component renders correctly');
164 | document.dispatchEvent(new Event('mousedown'));
165 | assert(clickOutsideHandled, 'clickOutsideHandled got flipped');
166 | });
167 |
168 | it('and createClass method, should call the specified handler when clicking the document', function() {
169 | class Component extends React.Component {
170 | render() {
171 | return React.createElement('div');
172 | }
173 | }
174 |
175 | var clickOutsideHandled = false;
176 | var handleClickOutside = function(event) {
177 | if (event === undefined) {
178 | throw new Error('event cannot be undefined');
179 | }
180 |
181 | clickOutsideHandled = true;
182 | };
183 |
184 | var WrappedWithCustomHandler = wrapComponent(Component);
185 |
186 | var element = React.createElement(WrappedWithCustomHandler, { handleClickOutside: handleClickOutside });
187 | assert(element, 'element can be created');
188 | var component = TestUtils.renderIntoDocument(element);
189 | assert(component, 'component renders correctly');
190 | document.dispatchEvent(new Event('mousedown'));
191 | assert(clickOutsideHandled, 'clickOutsideHandled got flipped');
192 | });
193 |
194 | it('and stateless function, should call the specified handler when clicking the document', function() {
195 | var Component = function() {
196 | return React.createElement('div');
197 | };
198 |
199 | var clickOutsideHandled = false;
200 | var handleClickOutside = function(event) {
201 | if (event === undefined) {
202 | throw new Error('event cannot be undefined');
203 | }
204 |
205 | clickOutsideHandled = true;
206 | };
207 |
208 | var WrappedWithCustomHandler = wrapComponent(Component);
209 |
210 | var element = React.createElement(WrappedWithCustomHandler, { handleClickOutside: handleClickOutside });
211 | assert(element, 'element can be created');
212 | var component = TestUtils.renderIntoDocument(element);
213 | assert(component, 'component renders correctly');
214 | document.dispatchEvent(new Event('mousedown'));
215 | assert(clickOutsideHandled, 'clickOutsideHandled got flipped');
216 | });
217 | });
218 |
219 | it('should throw an error when a custom handler is specified, but the component does not implement it', function() {
220 | class BadComponent extends React.Component {
221 | render() {
222 | return React.createElement('div');
223 | }
224 | }
225 |
226 | try {
227 | wrapComponent(BadComponent, {
228 | handleClickOutside: function(instance) {
229 | return instance.nonExistentMethod;
230 | },
231 | });
232 | assert(false, 'component was wrapped, despite not implementing the custom handler');
233 | } catch (e) {
234 | assert(e, 'component was not wrapped');
235 | }
236 | });
237 |
238 | it('should not call handleClickOutside if this.props.disableOnClickOutside() is called, until this.props.enableOnClickOutside() is called.', function() {
239 | var element = React.createElement(WrappedComponent);
240 | var component = TestUtils.renderIntoDocument(element);
241 | document.dispatchEvent(new Event('mousedown'));
242 | var instance = component.getInstance();
243 | assert(instance.state.timesHandlerCalled === 1, 'handleClickOutside called');
244 |
245 | try {
246 | instance.toggleEnableClickOutside(false);
247 | } catch (error) {
248 | assert(false, 'this.props.disableOnClickOutside() should not be undefined.');
249 | }
250 |
251 | document.dispatchEvent(new Event('mousedown'));
252 | assert(instance.state.timesHandlerCalled === 1, 'handleClickOutside not called after disableOnClickOutside()');
253 |
254 | instance.toggleEnableClickOutside(true);
255 | document.dispatchEvent(new Event('mousedown'));
256 | assert(instance.state.timesHandlerCalled === 2, 'handleClickOutside called after enableOnClickOutside()');
257 | });
258 |
259 | it('should fallback to call component.props.handleClickOutside when no component.handleClickOutside is defined', function() {
260 | var StatelessComponent = () => React.createElement('div');
261 | var clickOutsideHandled = false;
262 | var WrappedStatelessComponent = wrapComponent(StatelessComponent);
263 | var element = React.createElement(WrappedStatelessComponent, {
264 | handleClickOutside: function(event) {
265 | if (event === undefined) {
266 | throw new Error('event cannot be undefined');
267 | }
268 |
269 | clickOutsideHandled = true;
270 | },
271 | });
272 |
273 | assert(element, 'element can be created');
274 | var component = TestUtils.renderIntoDocument(element);
275 | assert(component, 'component renders correctly');
276 | document.dispatchEvent(new Event('mousedown'));
277 | component.getInstance();
278 | assert(clickOutsideHandled, 'clickOutsideHandled got flipped');
279 | });
280 |
281 | it('should register only one click outside listener per instance', function() {
282 | var i = 0;
283 |
284 | var Component = wrapComponent(
285 | class extends React.Component {
286 | componentDidMount() {
287 | this.props.enableOnClickOutside();
288 | }
289 |
290 | handleClickOutside() {
291 | ++i;
292 | }
293 |
294 | render() {
295 | return React.createElement('div');
296 | }
297 | },
298 | );
299 |
300 | TestUtils.renderIntoDocument(React.createElement(Component));
301 | document.dispatchEvent(new Event('mousedown'));
302 | assert(i === 1, 'listener called only once');
303 | });
304 |
305 | describe('with child rendering as null', function() {
306 | var counter;
307 |
308 | beforeEach(function() {
309 | counter = 0;
310 | });
311 |
312 | it('shouldn\'t throw an error when wrapped SFC renders as null', function() {
313 | var StatelessComponent = () => null;
314 | try {
315 | wrapComponent(StatelessComponent);
316 | assert(true, 'component was wrapped despite having no DOM node on mount');
317 | } catch (err) {
318 | assert(false, 'an error was thrown');
319 | }
320 | });
321 |
322 | class ClassComponent extends React.Component {
323 | handleClickOutside() {
324 | counter++;
325 | }
326 | componentDidUpdate() {
327 | this.props.callDisableOnClickOutside && this.props.disableOnClickOutside();
328 | this.props.callEnableOnClickOutside && this.props.enableOnClickOutside();
329 | }
330 | render() {
331 | return this.props.renderNull ? null : React.createElement('div');
332 | }
333 | }
334 |
335 | var container = document.createElement('div');
336 | var WrappedComponent = wrapComponent(ClassComponent);
337 |
338 | const rerender = function(props) {
339 | return ReactDOM.render(React.createElement(WrappedComponent, props), container);
340 | };
341 |
342 | it('should render fine when wrapped component renders as null', function() {
343 | var component = rerender({ renderNull: true });
344 | assert(component, 'component was wrapped despite having no DOM node on mount');
345 | document.dispatchEvent(new Event('mousedown'));
346 | assert(counter === 0, 'should not fire handleClickOutside when having no DOM node');
347 | });
348 |
349 | it('should attach and deattach event listener on updates', function() {
350 | rerender({ renderNull: false });
351 | document.dispatchEvent(new Event('mousedown'));
352 | assert(counter === 1, 'should fire handleClickOutside when DOM node gets created after rerender');
353 |
354 | rerender({ renderNull: true });
355 | document.dispatchEvent(new Event('mousedown'));
356 | assert(counter === 1, 'should stop firing handleClickOutside when DOM node gets removed');
357 | });
358 |
359 | it('should handle disabling and enabling onClickOutside listener when having no DOM node', function() {
360 | rerender({ renderNull: true, callEnableOnClickOutside: true });
361 | document.dispatchEvent(new Event('mousedown'));
362 | assert(
363 | counter === 0,
364 | 'should not call handleClickOutside when onClickOutside gets enabled when having no DOM node',
365 | );
366 |
367 | rerender({ renderNull: true, callDisableOnClickOutside: true });
368 | document.dispatchEvent(new Event('mousedown'));
369 | assert(
370 | counter === 0,
371 | 'should not call handleClickOutside when onClickOutside gets disabled when having no DOM node',
372 | );
373 | });
374 | });
375 |
376 | describe('with advanced settings disableOnclickOutside', function() {
377 | class Component extends React.Component {
378 | constructor(...args) {
379 | super(...args);
380 | this.state = {
381 | clickOutsideHandled: false,
382 | };
383 | }
384 | handleClickOutside(event) {
385 | if (event === undefined) {
386 | throw new Error('event cannot be undefined');
387 | }
388 | this.setState({
389 | clickOutsideHandled: true,
390 | });
391 | }
392 |
393 | render() {
394 | return React.createElement('div');
395 | }
396 | }
397 |
398 | it('disableOnclickOutside as true should not call handleClickOutside', function() {
399 | var component = TestUtils.renderIntoDocument(
400 | React.createElement(wrapComponent(Component), { disableOnClickOutside: true }),
401 | );
402 | document.dispatchEvent(new Event('mousedown'));
403 | var instance = component.getInstance();
404 | assert(instance.state.clickOutsideHandled === false, 'clickOutsideHandled should not get flipped');
405 | });
406 |
407 | it('disableOnclickOutside as true should not call handleClickOutside until enableOnClickOutside is called', function() {
408 | var component = TestUtils.renderIntoDocument(
409 | React.createElement(wrapComponent(Component), { disableOnClickOutside: true }),
410 | );
411 | var instance = component.getInstance();
412 | document.dispatchEvent(new Event('mousedown'));
413 | assert(instance.state.clickOutsideHandled === false, 'clickOutsideHandled should not get flipped');
414 |
415 | instance.props.enableOnClickOutside();
416 | document.dispatchEvent(new Event('mousedown'));
417 | assert(instance.state.clickOutsideHandled === true, 'clickOutsideHandled should not get flipped');
418 | });
419 | });
420 |
421 | describe('with setClickOutsideRef configured instead of findDOMNode', function() {
422 | class Component extends React.Component {
423 | callbackCalled = false;
424 | clickOutsideRef = null;
425 |
426 | handleClickOutside() {}
427 |
428 | setClickOutsideRef() {
429 | this.callbackCalled = true;
430 | return this.clickOutsideRef;
431 | }
432 |
433 | render() {
434 | return React.createElement('div', {
435 | ref: c => {
436 | this.clickOutsideRef = c;
437 | },
438 | });
439 | }
440 | }
441 |
442 | it('uses the DOM node defined by setClickOutsideRef in a class', function() {
443 | var component = TestUtils.renderIntoDocument(React.createElement(wrapComponent(Component)));
444 | document.dispatchEvent(new Event('mousedown'));
445 | var instance = component.getInstance();
446 | assert(instance.callbackCalled === true, 'setClickOutsideRef was called in class component');
447 | });
448 |
449 | let callbackCalled = false;
450 | let ref = null;
451 | function FuncComponent() {
452 | FuncComponent.setClickOutsideRef = () => {
453 | callbackCalled = true;
454 | return ref;
455 | };
456 | return React.createElement('div', {
457 | ref: c => {
458 | ref = c;
459 | },
460 | });
461 | }
462 |
463 | it('uses the DOM node defined by setClickOutsideRef in a function', function() {
464 | TestUtils.renderIntoDocument(
465 | React.createElement(
466 | wrapComponent(FuncComponent, {
467 | setClickOutsideRef: () => FuncComponent.setClickOutsideRef,
468 | handleClickOutside: () => () => {},
469 | }),
470 | ),
471 | );
472 | document.dispatchEvent(new Event('mousedown'));
473 |
474 | assert(callbackCalled === true, 'setClickOutsideRef was called in function component');
475 | });
476 | });
477 |
478 | describe('using onclickoutside when react app is rendered inside shadow DOM', function() {
479 | it('should call the specified handler when clicking the document', function() {
480 | class Component extends React.Component {
481 | constructor(...args) {
482 | super(...args);
483 | this.state = {
484 | clickOutsideHandled: false,
485 | };
486 | }
487 |
488 | handleClickOutside(event) {
489 | if (event === undefined) {
490 | throw new Error('event cannot be undefined');
491 | }
492 |
493 | this.setState({
494 | clickOutsideHandled: true,
495 | });
496 | }
497 |
498 | render() {
499 | return React.createElement('div');
500 | }
501 | }
502 |
503 | var WrappedWithCustomHandler = wrapComponent(Component);
504 |
505 | var element = React.createElement(WrappedWithCustomHandler);
506 | assert(element, 'element can be created');
507 |
508 | var container = document.createElement('div');
509 | container.attachShadow({ mode: 'open' });
510 |
511 | var component = ReactDOM.render(element, container.shadowRoot);
512 | assert(component, 'component renders correctly');
513 |
514 | document.dispatchEvent(new Event('mousedown'));
515 | var instance = component.getInstance();
516 | assert(instance.state.clickOutsideHandled, 'clickOutsideHandled got flipped');
517 | });
518 | });
519 | });
520 |
--------------------------------------------------------------------------------