react-onclickoutside
Advanced tools
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 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
22542
2
28
240
221
1
+ Addedasap@2.0.6(transitive)
+ Addedcore-js@1.2.7(transitive)
+ Addedencoding@0.1.13(transitive)
+ Addedfbjs@0.8.18(transitive)
+ Addediconv-lite@0.6.3(transitive)
+ Addedis-stream@1.1.0(transitive)
+ Addedisomorphic-fetch@2.2.1(transitive)
+ Addednode-fetch@1.7.3(transitive)
+ Addedpromise@7.3.1(transitive)
+ Addedprop-types@15.8.1(transitive)
+ Addedreact@15.7.0(transitive)
+ Addedreact-dom@15.7.0(transitive)
+ Addedreact-is@16.13.1(transitive)
+ Addedsafer-buffer@2.1.2(transitive)
+ Addedsetimmediate@1.0.5(transitive)
+ Addedua-parser-js@0.7.39(transitive)
+ Addedwhatwg-fetch@3.6.20(transitive)
- Removedcreate-react-class@^15.5.x