react-portal-hoc
Version:
a hoc that portalizes components
205 lines (178 loc) • 9.14 kB
JavaScript
Object.defineProperty(exports, "__esModule", {
value: true
});
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
var _react = require('react');
var _react2 = _interopRequireDefault(_react);
var _reactDom = require('react-dom');
var _targetIsDescendant = require('./targetIsDescendant');
var _targetIsDescendant2 = _interopRequireDefault(_targetIsDescendant);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
exports.default = function () {
var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
return function (ComposedComponent) {
return function (_Component) {
_inherits(PortalHoc, _Component);
function PortalHoc() {
var _ref;
var _temp, _this, _ret;
_classCallCheck(this, PortalHoc);
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
return _ret = (_temp = (_this = _possibleConstructorReturn(this, (_ref = PortalHoc.__proto__ || Object.getPrototypeOf(PortalHoc)).call.apply(_ref, [this].concat(args))), _this), _this.ensureClosedPortal = function () {
if (_this.node && !_this.isClosing) {
_this.isClosing = true;
_this.handleListeners('removeEventListener');
var closeAfter = _this.makeSureValue('closeAfter');
_this.portal = (0, _reactDom.unstable_renderSubtreeIntoContainer)(_this, _react2.default.createElement(ComposedComponent, _extends({}, _this.props, {
closeAfter: closeAfter,
closePortal: _this.ensureClosedPortal,
isClosing: true
})), _this.node);
if (closeAfter === undefined || closeAfter === null) {
// unmount right away if there is no animation to wait for
_this.unmount();
} else if (closeAfter === 'animation') {
// listen for keyframe animations and unmount after it finishes
var removeNode = function removeNode() {
_this.node.removeEventListener('animationend', removeNode);
_this.unmount();
};
_this.node.addEventListener('animationend', removeNode);
} else if (typeof closeAfter === 'function') {
// let the user-defined promise tell us when to ummount
closeAfter(_this.node).then(function () {
_this.unmount();
});
} else {
var delay = parseInt(closeAfter) || 0;
setTimeout(function () {
_this.unmount();
}, delay);
}
}
}, _this.ensureOpenPortal = function () {
if (!_this.node) {
_this.node = document.createElement('div');
_this.node.id = _this.makeSureValue('nodeId') || 'portal';
_this.node.setAttribute('react-portal', '');
document.body.appendChild(_this.node);
_this.handleListeners('addEventListener');
}
// we could make some performance gains by doing a shallow equal on the props
var closeAfter = _this.makeSureValue('closeAfter');
_this.portal = (0, _reactDom.unstable_renderSubtreeIntoContainer)(_this, _react2.default.createElement(ComposedComponent, _extends({}, _this.props, {
closeAfter: closeAfter,
closePortal: _this.ensureClosedPortal
})), _this.node);
}, _this.handleDocumentClick = function (e) {
// close as long as they didn't click the modal or the toggle
if (!(0, _targetIsDescendant2.default)(e.target, (0, _reactDom.findDOMNode)(_this.portal))) {
if (!_this.toggle || !(0, _targetIsDescendant2.default)(e.target, (0, _reactDom.findDOMNode)(_this.toggle))) {
_this.ensureClosedPortal();
}
}
}, _this.handleKeydown = function (e) {
if (e.key === 'Escape') {
_this.ensureClosedPortal();
}
}, _this.togglePortal = function () {
if (_this.node) {
_this.ensureClosedPortal();
} else {
_this.ensureOpenPortal();
}
}, _this.unmount = function () {
var node = _this.node;
_this.portal = null;
_this.node = null;
_this.toggle = null;
_this.isClosing = null;
// calling this will cause a rerender, which could cause a re-open if this.node is not null
(0, _reactDom.unmountComponentAtNode)(node);
document.body.removeChild(node);
}, _temp), _possibleConstructorReturn(_this, _ret);
}
_createClass(PortalHoc, [{
key: 'componentDidMount',
value: function componentDidMount() {
var isOpen = this.makeSureValue('isOpen');
this.ensurePortalState(isOpen);
}
// use componentDidUpdate because variables might get passed to the modal & those should be the latest
}, {
key: 'componentDidUpdate',
value: function componentDidUpdate() {
var isOpen = this.props.isOpen;
var keepOpen = this.isClosing ? false : isOpen;
var openState = this.toggle ? Boolean(this.node) : keepOpen;
// we can't wrap this in a prevProps.isOpen !== this.props.isOpen conditional
// because it's possible data changed without an unmount
this.ensurePortalState(openState);
}
}, {
key: 'componentWillUnmount',
value: function componentWillUnmount() {
this.ensureClosedPortal();
}
}, {
key: 'ensurePortalState',
value: function ensurePortalState(isOpen) {
if (isOpen) {
this.ensureOpenPortal();
} else {
this.ensureClosedPortal();
}
}
}, {
key: 'handleListeners',
value: function handleListeners(method) {
var escToClose = this.makeSureValue('escToClose');
var clickToClose = this.makeSureValue('clickToClose');
var handle = document[method].bind(document);
if (escToClose) {
handle('keydown', this.handleKeydown);
}
if (clickToClose) {
handle('click', this.handleDocumentClick);
handle('touchstart', this.handleDocumentClick);
}
}
}, {
key: 'makeSureValue',
value: function makeSureValue(val) {
return this.props[val] === undefined ? options[val] : this.props[val];
}
}, {
key: 'render',
value: function render() {
var _this2 = this;
var toggle = this.makeSureValue('toggle');
if (toggle) {
var onClick = function onClick(e) {
if (toggle.props.onClick) {
toggle.props.onClick(e);
}
// the above click handler might be setting the state for the portal, and setState is async
setTimeout(function () {
return _this2.togglePortal();
}, 0);
};
var ref = function ref(c) {
_this2.toggle = c;
};
return (0, _react.cloneElement)(toggle, { onClick: onClick, ref: ref });
}
return null;
}
}]);
return PortalHoc;
}(_react.Component);
};
};
;