Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

react-onclickoutside

Package Overview
Dependencies
Maintainers
1
Versions
79
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

react-onclickoutside - npm Package Compare versions

Comparing version 5.11.1 to 6.0.0

504

index.js

@@ -0,309 +1,279 @@

(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('react'), require('react-dom')) :
typeof define === 'function' && define.amd ? define(['exports', 'react', 'react-dom'], factory) :
(factory((global.onClickOutside = global.onClickOutside || {}),global.React,global.ReactDOM));
}(this, (function (exports,react,reactDom) { 'use strict';
/**
* A higher-order-component for handling onClickOutside for React components.
* Check whether some DOM node is our Component's node.
*/
(function(root) {
function isNodeFound(current, componentNode, ignoreClass) {
if (current === componentNode) {
return true;
}
// SVG <use/> elements do not technically reside in the rendered DOM, so
// they do not have classList directly, but they offer a link to their
// corresponding element, which can have classList. This extra check is for
// that case.
// See: http://www.w3.org/TR/SVG11/struct.html#InterfaceSVGUseElement
// Discussion: https://github.com/Pomax/react-onclickoutside/pull/17
if (current.correspondingElement) {
return current.correspondingElement.classList.contains(ignoreClass);
}
return current.classList.contains(ignoreClass);
}
// administrative
var registeredComponents = [];
var handlers = [];
var IGNORE_CLASS = 'ignore-react-onclickoutside';
var DEFAULT_EVENTS = ['mousedown', 'touchstart'];
/**
* Try to find our node in a hierarchy of nodes, returning the document
* node as highest node if our node is not found in the path up.
*/
function findHighest(current, componentNode, ignoreClass) {
if (current === componentNode) {
return true;
}
/**
* Check whether some DOM node is our Component's node.
*/
var isNodeFound = function(current, componentNode, ignoreClass) {
if (current === componentNode) {
// If source=local then this event came from 'somewhere'
// inside and should be ignored. We could handle this with
// a layered approach, too, but that requires going back to
// thinking in terms of Dom node nesting, running counter
// to React's 'you shouldn't care about the DOM' philosophy.
while (current.parentNode) {
if (isNodeFound(current, componentNode, ignoreClass)) {
return true;
}
// SVG <use/> elements do not technically reside in the rendered DOM, so
// they do not have classList directly, but they offer a link to their
// corresponding element, which can have classList. This extra check is for
// that case.
// See: http://www.w3.org/TR/SVG11/struct.html#InterfaceSVGUseElement
// Discussion: https://github.com/Pomax/react-onclickoutside/pull/17
if (current.correspondingElement) {
return current.correspondingElement.classList.contains(ignoreClass);
current = current.parentNode;
}
return current;
}
/**
* Check if the browser scrollbar was clicked
*/
function clickedScrollbar(evt) {
return document.documentElement.clientWidth <= evt.clientX || document.documentElement.clientHeight <= evt.clientY;
}
/**
* Generate the event handler that checks whether a clicked DOM node
* is inside of, or lives outside of, our Component's node tree.
*/
function generateOutsideCheck(componentNode, eventHandler, ignoreClass, excludeScrollbar, preventDefault, stopPropagation) {
return function (evt) {
if (preventDefault) {
evt.preventDefault();
}
return current.classList.contains(ignoreClass);
if (stopPropagation) {
evt.stopPropagation();
}
const current = evt.target;
if (excludeScrollbar && clickedScrollbar(evt) || findHighest(current, componentNode, ignoreClass) !== document) {
return;
}
eventHandler(evt);
};
}
/**
* Try to find our node in a hierarchy of nodes, returning the document
* node as highest noode if our node is not found in the path up.
*/
var findHighest = function(current, componentNode, ignoreClass) {
if (current === componentNode) {
return true;
/**
* A higher-order-component for handling onClickOutside for React components.
*/
const registeredComponents = [];
const handlers = [];
/**
* This function generates the HOC function that you'll use
* in order to impart onOutsideClick listening to an
* arbitrary component. It gets called at the end of the
* bootstrapping code to yield an instance of the
* onClickOutsideHOC function defined inside setupHOC().
*/
function onClickOutsideHOC(WrappedComponent, config) {
var _class, _temp2;
return _temp2 = _class = class onClickOutside extends react.Component {
constructor(...args) {
var _temp;
return _temp = super(...args), this.__outsideClickHandler = null, this.enableOnClickOutside = () => {
const fn = this.__outsideClickHandler;
if (fn && typeof document !== 'undefined') {
let events = this.props.eventTypes;
if (!events.forEach) {
events = [events];
}
events.forEach(eventName => {
const handlerOptions = !this.props.preventDefault && ['touchstart', 'touchmove'].indexOf(eventName) !== -1 ? { passive: true } : null;
document.addEventListener(eventName, fn, handlerOptions);
});
}
}, this.disableOnClickOutside = () => {
const fn = this.__outsideClickHandler;
if (fn && typeof document !== 'undefined') {
let events = this.props.eventTypes;
if (!events.forEach) {
events = [events];
}
events.forEach(eventName => document.removeEventListener(eventName, fn));
}
}, this.getRef = ref => this.instanceRef = ref, _temp;
}
// If source=local then this event came from 'somewhere'
// inside and should be ignored. We could handle this with
// a layered approach, too, but that requires going back to
// thinking in terms of Dom node nesting, running counter
// to React's 'you shouldn't care about the DOM' philosophy.
while(current.parentNode) {
if (isNodeFound(current, componentNode, ignoreClass)) {
return true;
/**
* Access the WrappedComponent's instance.
*/
getInstance() {
if (!WrappedComponent.prototype.isReactComponent) {
return this;
}
current = current.parentNode;
const ref = this.instanceRef;
return ref.getInstance ? ref.getInstance() : ref;
}
return current;
};
/**
* Check if the browser scrollbar was clicked
*/
var clickedScrollbar = function(evt) {
return document.documentElement.clientWidth <= evt.clientX || document.documentElement.clientHeight <= evt.clientY;
};
// this is given meaning in componentDidMount/componentDidUpdate
/**
* Generate the event handler that checks whether a clicked DOM node
* is inside of, or lives outside of, our Component's node tree.
*/
var generateOutsideCheck = function(componentNode, componentInstance, eventHandler, ignoreClass, excludeScrollbar, preventDefault, stopPropagation) {
return function(evt) {
if (preventDefault) {
evt.preventDefault();
/**
* Add click listeners to the current document,
* linked to this component's state.
*/
componentDidMount() {
// If we are in an environment without a DOM such
// as shallow rendering or snapshots then we exit
// early to prevent any unhandled errors being thrown.
if (typeof document === 'undefined' || !document.createElement) {
return;
}
if (stopPropagation) {
evt.stopPropagation();
const instance = this.getInstance();
if (config && typeof config.handleClickOutside === 'function') {
this.__clickOutsideHandlerProp = config.handleClickOutside(instance);
if (typeof this.__clickOutsideHandlerProp !== 'function') {
throw new Error('WrappedComponent lacks a function for processing outside click events specified by the handleClickOutside config option.');
}
} else if (typeof instance.handleClickOutside === 'function') {
if (react.Component.prototype.isPrototypeOf(instance)) {
this.__clickOutsideHandlerProp = instance.handleClickOutside.bind(instance);
} else {
this.__clickOutsideHandlerProp = instance.handleClickOutside;
}
} else if (typeof instance.props.handleClickOutside === 'function') {
this.__clickOutsideHandlerProp = instance.props.handleClickOutside;
} else {
throw new Error('WrappedComponent lacks a handleClickOutside(event) function for processing outside click events.');
}
var current = evt.target;
if((excludeScrollbar && clickedScrollbar(evt)) || (findHighest(current, componentNode, ignoreClass) !== document)) {
// TODO: try to get rid of this, could be done with function ref, might be problematic for SFC though, they do not expose refs
if (reactDom.findDOMNode(instance) === null) {
return;
}
eventHandler(evt);
};
};
/**
* This function generates the HOC function that you'll use
* in order to impart onOutsideClick listening to an
* arbitrary component. It gets called at the end of the
* bootstrapping code to yield an instance of the
* onClickOutsideHOC function defined inside setupHOC().
*/
function setupHOC(root, React, ReactDOM, createReactClass) {
this.addOutsideClickHandler();
}
// The actual Component-wrapping HOC:
return function onClickOutsideHOC(Component, config) {
var wrapComponentWithOnClickOutsideHandling = createReactClass({
statics: {
/**
* Access the wrapped Component's class.
*/
getClass: function() {
if (Component.getClass) {
return Component.getClass();
}
return Component;
}
},
/**
* Track for disableOnClickOutside props changes and enable/disable click outside
*/
componentWillReceiveProps(nextProps) {
if (this.props.disableOnClickOutside && !nextProps.disableOnClickOutside) {
this.enableOnClickOutside();
} else if (!this.props.disableOnClickOutside && nextProps.disableOnClickOutside) {
this.disableOnClickOutside();
}
}
/**
* Access the wrapped Component's instance.
*/
getInstance: function() {
return Component.prototype.isReactComponent ? this.refs.instance : this;
},
componentDidUpdate() {
const componentNode = reactDom.findDOMNode(this.getInstance());
// this is given meaning in componentDidMount
__outsideClickHandler: function() {},
if (componentNode === null && this.__outsideClickHandler) {
this.removeOutsideClickHandler();
return;
}
getDefaultProps: function() {
return {
excludeScrollbar: config && config.excludeScrollbar
};
},
if (componentNode !== null && !this.__outsideClickHandler) {
this.addOutsideClickHandler();
return;
}
}
/**
* Add click listeners to the current document,
* linked to this component's state.
*/
componentDidMount: function() {
// If we are in an environment without a DOM such
// as shallow rendering or snapshots then we exit
// early to prevent any unhandled errors being thrown.
if (typeof document === 'undefined' || !document.createElement){
return;
}
/**
* Remove all document's event listeners for this component
*/
componentWillUnmount() {
this.removeOutsideClickHandler();
}
var instance = this.getInstance();
var clickOutsideHandler;
/**
* Can be called to explicitly enable event listening
* for clicks and touches outside of this element.
*/
if(config && typeof config.handleClickOutside === 'function') {
clickOutsideHandler = config.handleClickOutside(instance);
if(typeof clickOutsideHandler !== 'function') {
throw new Error('Component lacks a function for processing outside click events specified by the handleClickOutside config option.');
}
} else if(typeof instance.handleClickOutside === 'function') {
if (React.Component.prototype.isPrototypeOf(instance)) {
clickOutsideHandler = instance.handleClickOutside.bind(instance);
} else {
clickOutsideHandler = instance.handleClickOutside;
}
} else if(typeof instance.props.handleClickOutside === 'function') {
clickOutsideHandler = instance.props.handleClickOutside;
} else {
throw new Error('Component lacks a handleClickOutside(event) function for processing outside click events.');
}
var componentNode = ReactDOM.findDOMNode(instance);
if (componentNode === null) {
console.warn('Antipattern warning: there was no DOM node associated with the component that is being wrapped by outsideClick.');
console.warn([
'This is typically caused by having a component that starts life with a render function that',
'returns `null` (due to a state or props value), so that the component \'exist\' in the React',
'chain of components, but not in the DOM.\n\nInstead, you need to refactor your code so that the',
'decision of whether or not to show your component is handled by the parent, in their render()',
'function.\n\nIn code, rather than:\n\n A{render(){return check? <.../> : null;}\n B{render(){<A check=... />}\n\nmake sure that you',
'use:\n\n A{render(){return <.../>}\n B{render(){return <...>{ check ? <A/> : null }<...>}}\n\nThat is:',
'the parent is always responsible for deciding whether or not to render any of its children.',
'It is not the child\'s responsibility to decide whether a render instruction from above should',
'get ignored or not by returning `null`.\n\nWhen any component gets its render() function called,',
'that is the signal that it should be rendering its part of the UI. It may in turn decide not to',
'render all of *its* children, but it should never return `null` for itself. It is not responsible',
'for that decision.'
].join(' '));
}
/**
* Can be called to explicitly disable event listening
* for clicks and touches outside of this element.
*/
var fn = this.__outsideClickHandler = generateOutsideCheck(
componentNode,
instance,
clickOutsideHandler,
this.props.outsideClickIgnoreClass || IGNORE_CLASS,
this.props.excludeScrollbar, // fallback not needed, prop always exists because of getDefaultProps
this.props.preventDefault || false,
this.props.stopPropagation || false
);
var pos = registeredComponents.length;
registeredComponents.push(this);
handlers[pos] = fn;
addOutsideClickHandler() {
const fn = this.__outsideClickHandler = generateOutsideCheck(reactDom.findDOMNode(this.getInstance()), this.__clickOutsideHandlerProp, this.props.outsideClickIgnoreClass, this.props.excludeScrollbar, this.props.preventDefault, this.props.stopPropagation);
// If there is a truthy disableOnClickOutside property for this
// component, don't immediately start listening for outside events.
if (!this.props.disableOnClickOutside) {
this.enableOnClickOutside();
}
},
const pos = registeredComponents.length;
registeredComponents.push(this);
handlers[pos] = fn;
/**
* Track for disableOnClickOutside props changes and enable/disable click outside
*/
componentWillReceiveProps: function(nextProps) {
if (this.props.disableOnClickOutside && !nextProps.disableOnClickOutside) {
this.enableOnClickOutside();
} else if (!this.props.disableOnClickOutside && nextProps.disableOnClickOutside) {
this.disableOnClickOutside();
}
},
// If there is a truthy disableOnClickOutside property for this
// component, don't immediately start listening for outside events.
if (!this.props.disableOnClickOutside) {
this.enableOnClickOutside();
}
}
/**
* Remove the document's event listeners
*/
componentWillUnmount: function() {
this.disableOnClickOutside();
this.__outsideClickHandler = false;
var pos = registeredComponents.indexOf(this);
if( pos>-1) {
// clean up so we don't leak memory
if (handlers[pos]) { handlers.splice(pos, 1); }
registeredComponents.splice(pos, 1);
}
},
removeOutsideClickHandler() {
this.disableOnClickOutside();
this.__outsideClickHandler = false;
/**
* Can be called to explicitly enable event listening
* for clicks and touches outside of this element.
*/
enableOnClickOutside: function() {
var fn = this.__outsideClickHandler;
if (typeof document !== 'undefined') {
var events = this.props.eventTypes || DEFAULT_EVENTS;
if (!events.forEach) {
events = [events];
}
events.forEach(function (eventName) {
document.addEventListener(eventName, fn);
});
}
},
var pos = registeredComponents.indexOf(this);
/**
* Can be called to explicitly disable event listening
* for clicks and touches outside of this element.
*/
disableOnClickOutside: function() {
var fn = this.__outsideClickHandler;
if (typeof document !== 'undefined') {
var events = this.props.eventTypes || DEFAULT_EVENTS;
if (!events.forEach) {
events = [events];
}
events.forEach(function (eventName) {
document.removeEventListener(eventName, fn);
});
}
},
/**
* Pass-through render
*/
render: function() {
var passedProps = this.props;
var props = {};
Object.keys(this.props).forEach(function(key) {
if (key !== 'excludeScrollbar') {
props[key] = passedProps[key];
}
});
if (Component.prototype.isReactComponent) {
props.ref = 'instance';
}
props.disableOnClickOutside = this.disableOnClickOutside;
props.enableOnClickOutside = this.enableOnClickOutside;
return React.createElement(Component, props);
if (pos > -1) {
// clean up so we don't leak memory
if (handlers[pos]) {
handlers.splice(pos, 1);
}
});
registeredComponents.splice(pos, 1);
}
}
// Add display name for React devtools
(function bindWrappedComponentName(c, wrapper) {
var componentName = c.displayName || c.name || 'Component';
wrapper.displayName = 'OnClickOutside(' + componentName + ')';
}(Component, wrapComponentWithOnClickOutsideHandling));
/**
* Pass-through render
*/
render() {
var props = Object.keys(this.props).filter(prop => prop !== 'excludeScrollbar').reduce((props, prop) => {
props[prop] = this.props[prop];
return props;
}, {});
return wrapComponentWithOnClickOutsideHandling;
};
}
if (WrappedComponent.prototype.isReactComponent) {
props.ref = this.getRef;
} else {
props.wrappedRef = this.getRef;
}
/**
* This function sets up the library in ways that
* work with the various modulde loading solutions
* used in JavaScript land today.
*/
function setupBinding(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['react','react-dom','create-react-class'], function(React, ReactDom, createReactClass) {
if (!createReactClass) createReactClass = React.createClass;
return factory(root, React, ReactDom, createReactClass);
});
} else if (typeof exports === 'object') {
// Node. Note that this does not work with strict
// CommonJS, but only CommonJS-like environments
// that support module.exports
module.exports = factory(root, require('react'), require('react-dom'), require('create-react-class'));
} else {
// Browser globals (root is window)
var createReactClass = React.createClass ? React.createClass : window.createReactClass;
root.onClickOutside = factory(root, React, ReactDOM, createReactClass);
props.disableOnClickOutside = this.disableOnClickOutside;
props.enableOnClickOutside = this.enableOnClickOutside;
return react.createElement(WrappedComponent, props);
}
}
}, _class.displayName = `OnClickOutside(${WrappedComponent.displayName || WrappedComponent.name || 'Component'})`, _class.defaultProps = {
eventTypes: ['mousedown', 'touchstart'],
excludeScrollbar: config && config.excludeScrollbar || false,
outsideClickIgnoreClass: 'ignore-react-onclickoutside',
preventDefault: false,
stopPropagation: false
}, _class.getClass = () => WrappedComponent.getClass ? WrappedComponent.getClass() : WrappedComponent, _temp2;
}
// Make it all happen
setupBinding(root, setupHOC);
exports['default'] = onClickOutsideHOC;
}(this));
Object.defineProperty(exports, '__esModule', { value: true });
})));
{
"name": "react-onclickoutside",
"version": "5.11.1",
"version": "6.0.0",
"description": "An onClickOutside wrapper for React components",
"main": "index.js",
"main": "./index.js",
"files": [

@@ -11,3 +11,4 @@ "index.js"

"authors": [
"Pomax <pomax@nihongoresources.com>"
"Pomax <pomax@nihongoresources.com>",
"Andarist <mateuszburzynski@gmail.com>"
],

@@ -29,16 +30,20 @@ "keywords": [

"scripts": {
"test": "npm run lint && karma start test/karma.conf.js --single-run && npm run test:nodom",
"build": "rollup -c -i src/index.js -o ./index.js",
"lint": "eslint src/*.js ./test",
"test": "run-s test:**",
"test:basic": "run-s lint build",
"test:karma": "karma start test/karma.conf.js --single-run",
"test:nodom": "mocha test/no-dom-test.js",
"lint": "eslint index.js ./test",
"precommit": "npm run lint"
"precommit": "npm test && lint-staged"
},
"dependencies": {
"create-react-class": "^15.5.x"
},
"devDependencies": {
"babel-preset-es2015": "^6.14.0",
"babel-cli": "^6.24.1",
"babel-eslint": "^7.2.3",
"babel-loader": "^6.x",
"babel-plugin-transform-class-properties": "^6.24.1",
"babel-preset-es2015": "^6.24.1",
"chai": "^3.5.0",
"eslint": "^3.4.0",
"husky": "^0.13.3",
"karma": "^1.4.0",
"karma-babel-preprocessor": "^6.0.1",
"karma-chai": "^0.1.0",

@@ -49,11 +54,33 @@ "karma-mocha": "^1.3.0",

"karma-webpack": "^2.0.2",
"lint-staged": "^3.4.2",
"mocha": "^3.2.0",
"npm-run-all": "^4.0.2",
"phantomjs-prebuilt": "^2.1.7",
"prettier": "^1.3.1",
"react": "^15.5.x",
"react-addons-test-utils": "^15.5.x",
"react-dom": "^15.5.x",
"react-test-renderer": "^15.5.x",
"require-hijack": "^1.2.1",
"webpack": "^1.12.14"
"rimraf": "^2.6.1",
"rollup": "^0.41.6",
"rollup-plugin-babel": "^2.7.1",
"rollup-plugin-node-resolve": "^3.0.0",
"webpack": "^1.15.0"
},
"peerDependencies": {
"react": "^15.5.x",
"react-dom": "^15.5.x"
},
"babel": {
"plugins": [
"transform-class-properties"
]
},
"lint-staged": {
"{src,test}/**/*.js": [
"prettier --print-width=120 --single-quote --trailing-comma=all --write",
"eslint --fix",
"git add"
]
}
}

@@ -7,3 +7,3 @@ # An onClickOutside wrapper for React components

This HOC supports stateless components as of v5.7.0
This HOC supports stateless components as of v5.7.0, and uses pure class notation rather than `createClass` as of v6.

@@ -77,95 +77,2 @@ ## Installation

### IMPORTANT: Make sure there are DOM nodes to work with.
If you are using this HOC to toggle visibility of UI elements, make sure you understand how responsibility for this works in React. While in a traditional web setting you would simply call something like `.show()` and `.hide()` on a part of the UI you want to toggle visibility for, using CSS properties, React instead is about *simply not showing UI unless it should be visible*.
As such, doing **the following is a guaranteed error** for onClickOutside:
```js
class InitiallyHidden extends React.Component {
constructor(props) {
super(props);
}
render() {
return this.props.hidden ? null : <div>...loads of content...</div>;
}
handleClickOutside() {
this.props.hide();
}
}
const A = onClickOutside(InitiallyHidden);
class UI extends React.Component {
constructor(props) {
super(props);
this.state = {
hideThing: true
}
}
render() {
return <div>
<button onClick={e => this.showContent() }>click to show content</button>
<A hidden={this.state.hideThing} hide={e => this.hideContent() }/>
</div>;
}
showContent() {
this.setState({ hideThing: false });
}
hideContent() {
this.setState({ hideThing: true });
}
}
```
Running this code will result in a console log that looks like this:
![](warning.png)
The reason this code will fail is that this component can mount *without* a DOM node backing it. Writing a `render()` function like this is somewhat of an antipattern: a component should assume that *if* its render function is called, it should render. It should *not* potentially render nothing.
Instead, the parent should decide whether some child component should render at all, and any component should assume that when its `render()` function is called, it should render itself.
A refactor is typically trivially effected, and **the following code will work fine**:
```js
class InitiallyHidden extends React.Component {
constructor(props) {
super(props);
}
render() {
return <div>...loads of content...</div>;
}
handleClickOutside() {
this.props.hide();
}
}
const A = onClickOutside(InitiallyHidden);
class UI extends React.Component {
constructor(props) {
super(props);
this.state = {
hideThing: true
}
}
render() {
return <div>
<button onClick={e => this.showContent() }>click to show content</button>
{ this.state.hideThing ? null : <A hide={e => this.hideContent() }/> }
</div>;
}
showContent() {
this.setState({ hideThing: false });
}
hideContent() {
this.setState({ hideThing: true });
}
}
```
Here we have code where each component trusts that its `render()` will only get called when there is in fact something to render, and the `UI` component does this by making sure to check what *it* needs to render.
The onOutsideClick HOC will work just fine with this kind of code.
## Regulate which events to listen for

@@ -301,4 +208,6 @@

If you use **React 15.5** (or higher), you can use **v5.11.x, which works with the externalised `create-react-class` rather than `React.createClass`.
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.

@@ -305,0 +214,0 @@

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc