@alifd/next
Version:
A configurable component library for web built on React.
622 lines (621 loc) • 26.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var tslib_1 = require("tslib");
var react_1 = tslib_1.__importStar(require("react"));
var react_dom_1 = require("react-dom");
var prop_types_1 = tslib_1.__importDefault(require("prop-types"));
var classnames_1 = tslib_1.__importDefault(require("classnames"));
var react_lifecycles_compat_1 = require("react-lifecycles-compat");
var util_1 = require("../util");
var manager_1 = tslib_1.__importDefault(require("./manager"));
var gateway_1 = tslib_1.__importDefault(require("./gateway"));
var position_1 = tslib_1.__importDefault(require("./position"));
var find_node_1 = tslib_1.__importDefault(require("./utils/find-node"));
var saveLastFocusNode = util_1.focus.saveLastFocusNode, getFocusNodeList = util_1.focus.getFocusNodeList, backLastFocusNode = util_1.focus.backLastFocusNode;
var makeChain = util_1.func.makeChain, noop = util_1.func.noop, bindCtx = util_1.func.bindCtx;
var getContainerNode = function (props) {
var targetNode = (0, find_node_1.default)(props.target);
return (0, find_node_1.default)(props.container, targetNode);
};
var prefixes = ['-webkit-', '-moz-', '-o-', 'ms-', ''];
var getStyleProperty = function (node, name) {
var style = window.getComputedStyle(node);
var ret = '';
for (var i = 0; i < prefixes.length; i++) {
ret = style.getPropertyValue(prefixes[i] + name);
if (ret) {
break;
}
}
return ret;
};
// 存 containerNode 信息
var containerNodeList = [];
/**
* Overlay
*/
var Overlay = /** @class */ (function (_super) {
tslib_1.__extends(Overlay, _super);
function Overlay(props) {
var _this = _super.call(this, props) || this;
_this.saveContentRef = function (ref) {
_this.contentRef = ref;
};
_this.saveGatewayRef = function (ref) {
_this.gatewayRef = ref;
};
_this.lastAlign = props.align;
bindCtx(_this, [
'handlePosition',
'handleAnimateEnd',
'handleDocumentKeyDown',
'handleDocumentClick',
'handleMaskClick',
'beforeOpen',
'beforeClose',
]);
_this.state = {
visible: false,
status: 'none',
animation: _this.getAnimation(props),
willOpen: false,
willClose: false,
};
_this.timeoutMap = {};
return _this;
}
Overlay.getDerivedStateFromProps = function (nextProps, prevState) {
var willOpen = !prevState.visible && nextProps.visible;
var willClose = prevState.visible && !nextProps.visible;
var nextState = {
willOpen: willOpen,
willClose: willClose,
};
if (willOpen) {
nextProps.beforeOpen && nextProps.beforeOpen();
}
else if (willClose) {
nextProps.beforeClose && nextProps.beforeClose();
}
if (nextProps.animation || nextProps.animation === false) {
nextState.animation = nextProps.animation;
}
if (nextProps.animation !== false && util_1.support.animation) {
if (willOpen) {
nextState.visible = true;
nextState.status = 'mounting';
}
else if (willClose) {
// can not set visible=false directly, otherwise animation not work without dom
// nextState.visible = false;
nextState.status = 'leaving';
}
}
else if ('visible' in nextProps && nextProps.visible !== prevState.visible) {
nextState.visible = nextProps.visible;
}
return nextState;
};
Overlay.prototype.componentDidMount = function () {
if (this.state.willOpen) {
this.beforeOpen();
}
else if (this.state.willClose) {
this.beforeClose();
}
if (this.state.visible) {
this.doAnimation(true, false);
this._isMounted = true;
}
this.addDocumentEvents();
manager_1.default.addOverlay(this);
};
Overlay.prototype.componentDidUpdate = function (prevProps) {
if (this.state.willOpen) {
this.beforeOpen();
}
else if (this.state.willClose) {
this.beforeClose();
}
if (!this._isMounted && this.props.visible) {
this._isMounted = true;
}
if (this.props.align !== prevProps.align) {
this.lastAlign = prevProps.align;
}
var willOpen = !prevProps.visible && this.props.visible;
var willClose = prevProps.visible && !this.props.visible;
(willOpen || willClose) && this.doAnimation(willOpen, willClose);
};
Overlay.prototype.componentWillUnmount = function () {
this._isDestroyed = true;
this._isMounted = false;
manager_1.default.removeOverlay(this);
this.removeDocumentEvents();
if (this.focusTimeout) {
clearTimeout(this.focusTimeout);
}
if (this._animation) {
this._animation.off();
this._animation = null;
}
this.beforeClose();
};
Overlay.prototype.doAnimation = function (open, close) {
var _this = this;
if (this.state.animation && util_1.support.animation) {
if (open) {
this.onEntering();
}
else if (close) {
this.onLeaving();
}
this.addAnimationEvents();
}
else {
if (open) {
// fix https://github.com/alibaba-fusion/next/issues/1901
setTimeout(function () {
_this.props.onOpen();
util_1.dom.addClass(_this.getWrapperNode(), 'opened');
manager_1.default.addOverlay(_this);
_this.props.afterOpen();
});
}
else if (close) {
this.props.onClose();
util_1.dom.removeClass(this.getWrapperNode(), 'opened');
manager_1.default.removeOverlay(this);
this.props.afterClose();
}
this.setFocusNode();
}
};
Overlay.prototype.getAnimation = function (props) {
if (props.animation === false) {
return false;
}
if (props.animation) {
return props.animation;
}
return this.getAnimationByAlign(props.align);
};
Overlay.prototype.getAnimationByAlign = function (align) {
switch (align[0]) {
case 't':
return {
// 为了防止有的用户 js 升级了而 css 没升级,所以把两个动画都保留了。
// 动画不会叠加,会替代,顺序根据 src/animate/main.scss 中的样式先后顺序遵循 css 覆盖原则
// fadeInDownSmall fadeOutUpSmall 优先级更高
in: 'expandInDown fadeInDownSmall',
out: 'expandOutUp fadeOutUpSmall',
};
case 'b':
return {
in: 'fadeInUp',
out: 'fadeOutDown',
};
default:
return {
in: 'expandInDown fadeInDownSmall',
out: 'expandOutUp fadeOutUpSmall',
};
}
};
Overlay.prototype.addAnimationEvents = function () {
var _this = this;
if (typeof window === 'undefined') {
return;
}
setTimeout(function () {
var node = _this.getContentNode();
if (node) {
var id_1 = (0, util_1.guid)();
_this._animation = util_1.events.on(node, util_1.support.animation.end, _this.handleAnimateEnd.bind(_this, id_1));
var animationDelay = parseFloat(getStyleProperty(node, 'animation-delay')) || 0;
var animationDuration = parseFloat(getStyleProperty(node, 'animation-duration')) || 0;
var time = animationDelay + animationDuration;
if (time) {
_this.timeoutMap[id_1] = window.setTimeout(function () {
_this.handleAnimateEnd(id_1);
}, time * 1000 + 200);
}
}
});
};
Overlay.prototype.handlePosition = function (config) {
var align = config.align.join(' ');
if (!('animation' in this.props) && this.props.needAdjust && this.lastAlign !== align) {
this.setState({
animation: this.getAnimationByAlign(align),
});
}
var status = this.state.status;
if (status === 'mounting') {
this.setState({
status: 'entering',
});
}
this.lastAlign = align;
};
Overlay.prototype.handleAnimateEnd = function (id) {
if (this.timeoutMap[id]) {
clearTimeout(this.timeoutMap[id]);
}
delete this.timeoutMap[id];
if (this._animation) {
this._animation.off();
this._animation = null;
}
if (!this._isMounted) {
return;
}
if (this.state.status === 'leaving') {
this.setState({
visible: false,
status: 'none',
});
this.onLeaved();
// dom 结构首次出现 触发的是 entering
// dom 结构已经存在(例如设置了 cache),触发的是 mounting
}
else if (this.state.status === 'entering' || this.state.status === 'mounting') {
this.setState({
status: 'none',
});
this.onEntered();
}
};
Overlay.prototype.onEntering = function () {
var _this = this;
if (this._isDestroyed) {
return;
}
// make sure overlay.ref has been called (eg: menu/popup-item called overlay.getInstance().getContentNode().)
setTimeout(function () {
var wrapperNode = _this.getWrapperNode();
util_1.dom.addClass(wrapperNode, 'opened');
_this.props.onOpen();
});
};
Overlay.prototype.onLeaving = function () {
var wrapperNode = this.getWrapperNode();
util_1.dom.removeClass(wrapperNode, 'opened');
this.props.onClose();
};
Overlay.prototype.onEntered = function () {
manager_1.default.addOverlay(this);
this.setFocusNode();
this.props.afterOpen();
};
Overlay.prototype.onLeaved = function () {
manager_1.default.removeOverlay(this);
this.setFocusNode();
this.props.afterClose();
};
Overlay.prototype.beforeOpen = function () {
if (this.props.disableScroll) {
var containerNode_1 = getContainerNode(this.props) || document.body;
var _a = containerNode_1.style, overflow = _a.overflow, paddingRight = _a.paddingRight;
var cnInfo = containerNodeList.find(function (m) { return m.containerNode === containerNode_1; }) || {
containerNode: containerNode_1,
count: 0,
};
/**
* container 节点初始状态已经是 overflow=hidden 则忽略
* See {@link https://codesandbox.io/s/next-overlay-overflow-2-fulpq?file=/src/App.js}
*/
if (cnInfo.count === 0 && overflow !== 'hidden') {
var style = {
overflow: 'hidden',
};
cnInfo.overflow = overflow;
if (util_1.dom.hasScroll(containerNode_1)) {
cnInfo.paddingRight = paddingRight;
style.paddingRight = "".concat(util_1.dom.getStyle(containerNode_1, 'paddingRight') +
util_1.dom.scrollbar().width, "px");
}
util_1.dom.setStyle(containerNode_1, style);
containerNodeList.push(cnInfo);
cnInfo.count++;
}
else if (cnInfo.count) {
cnInfo.count++;
}
this._containerNode = containerNode_1;
}
};
Overlay.prototype.beforeClose = function () {
var _this = this;
if (this.props.disableScroll) {
var idx = containerNodeList.findIndex(function (cn) { return cn.containerNode === _this._containerNode; });
if (idx !== -1) {
var cnInfo = containerNodeList[idx];
var overflow = cnInfo.overflow, paddingRight = cnInfo.paddingRight;
// 最后一个 overlay 的时候再将样式重置回去
// 此时 overflow 应该值在 beforeOpen 中设置的 hidden
if (cnInfo.count === 1 &&
this._containerNode &&
this._containerNode.style.overflow === 'hidden') {
var style = {
overflow: overflow,
};
if (paddingRight !== undefined) {
style.paddingRight = paddingRight;
}
util_1.dom.setStyle(this._containerNode, style);
}
cnInfo.count--;
if (cnInfo.count === 0) {
containerNodeList.splice(idx, 1);
}
}
this._containerNode = undefined;
}
};
Overlay.prototype.setFocusNode = function () {
var _this = this;
if (!this.props.autoFocus) {
return;
}
if (this.state.visible && !this._hasFocused) {
saveLastFocusNode();
// 这个时候很可能上一个弹层的关闭事件还未触发,导致焦点已经回到触发的元素
// 这里延时处理一下,延时的时间为 document.click 捕获触发的延时时间
this.focusTimeout = window.setTimeout(function () {
var node = _this.getContentNode();
if (node) {
var focusNodeList = getFocusNodeList(node);
if (focusNodeList.length) {
focusNodeList[0].focus();
}
_this._hasFocused = true;
}
}, 100);
}
else if (!this.state.visible && this._hasFocused) {
backLastFocusNode();
this._hasFocused = false;
}
};
Overlay.prototype.getContent = function () {
return this.contentRef;
};
Overlay.prototype.getContentNode = function () {
try {
return (0, react_dom_1.findDOMNode)(this.contentRef);
}
catch (err) {
return null;
}
};
Overlay.prototype.getWrapperNode = function () {
return this.gatewayRef ? this.gatewayRef.getChildNode() : null;
};
/**
* document global event
*/
Overlay.prototype.addDocumentEvents = function () {
// FIXME: canCloseByEsc、canCloseByOutSideClick、canCloseByMask 仅在 didMount 时生效,update 时不生效
var useCapture = this.props.useCapture;
// use capture phase listener to be compatible with react17
// https://reactjs.org/blog/2020/08/10/react-v17-rc.html#fixing-potential-issues
if (typeof document === 'undefined')
return;
if (this.props.canCloseByEsc) {
this._keydownEvents = util_1.events.on(document, 'keydown', this.handleDocumentKeyDown, useCapture);
}
if (this.props.canCloseByOutSideClick) {
this._clickEvents = util_1.events.on(document, 'click', this.handleDocumentClick, useCapture);
this._touchEvents = util_1.events.on(document, 'touchend', this.handleDocumentClick, useCapture);
}
};
Overlay.prototype.removeDocumentEvents = function () {
var _this = this;
['_keydownEvents', '_clickEvents', '_touchEvents'].forEach(function (event) {
if (_this[event]) {
_this[event].off();
_this[event] = null;
}
});
};
Overlay.prototype.handleDocumentKeyDown = function (e) {
if (this.state.visible &&
e.keyCode === util_1.KEYCODE.ESC &&
manager_1.default.isCurrentOverlay(this)) {
this.props.onRequestClose('keyboard', e);
}
};
Overlay.prototype.isInShadowDOM = function (node) {
return node.getRootNode ? node.getRootNode().nodeType === 11 : false;
};
Overlay.prototype.getEventPath = function (event) {
// 参考 https://github.com/spring-media/react-shadow-dom-retarget-events/blob/master/index.js#L29
return (event.path ||
(event.composedPath && event.composedPath()) ||
this.composedPath(event.target));
};
Overlay.prototype.composedPath = function (el) {
var path = [];
while (el) {
path.push(el);
if (el.tagName === 'HTML') {
path.push(document);
path.push(window);
return path;
}
el = el.parentElement;
}
};
Overlay.prototype.matchInShadowDOM = function (node, e) {
if (this.isInShadowDOM(node)) {
// Shadow DOM 环境中,触发点击事件,监听 document click 事件获得的事件源
// 并非实际触发的 dom 节点,而是 Shadow DOM 的 host 节点
// 进而会导致如 Select 组件的下拉弹层打开后立即关闭等问题
// 因此额外增加 node 和 eventPath 的判断
var eventPath = this.getEventPath(e);
return node === eventPath[0] || node.contains(eventPath[0]);
}
return false;
};
Overlay.prototype.handleDocumentClick = function (e) {
var _this = this;
if (this.state.visible) {
var safeNode = this.props.safeNode;
var safeNodes = Array.isArray(safeNode) ? tslib_1.__spreadArray([], tslib_1.__read(safeNode), false) : [safeNode];
safeNodes.unshift(function () { return _this.getWrapperNode(); });
for (var i = 0; i < safeNodes.length; i++) {
var node = (0, find_node_1.default)(safeNodes[i], this.props);
// HACK: 如果触发点击的节点是弹层内部的节点,并且在被点击后立即销毁,那么此时无法使用 node.contains(e.target)
// 来判断此时点击的节点是否是弹层内部的节点,额外判断
if (node &&
(node === e.target ||
node.contains(e.target) ||
this.matchInShadowDOM(node, e) ||
(e.target !== document &&
!document.documentElement.contains(e.target)))) {
return;
}
}
this.props.onRequestClose('docClick', e);
}
};
Overlay.prototype.handleMaskClick = function (e) {
if (e.currentTarget === e.target && this.props.canCloseByMask) {
this.props.onRequestClose('maskClick', e);
}
};
// 兼容过去的用法:this.popupRef.getInstance().overlay.getInstance().getContentNode()
Overlay.prototype.getInstance = function () {
return this;
};
Overlay.prototype.render = function () {
var _a, _b;
var _c = this.props, prefix = _c.prefix, className = _c.className, style = _c.style, propChildren = _c.children, target = _c.target, align = _c.align, offset = _c.offset, container = _c.container, hasMask = _c.hasMask, needAdjust = _c.needAdjust, autoFit = _c.autoFit, beforePosition = _c.beforePosition, onPosition = _c.onPosition, wrapperStyle = _c.wrapperStyle, rtl = _c.rtl, propShouldUpdatePosition = _c.shouldUpdatePosition, cache = _c.cache, wrapperClassName = _c.wrapperClassName, onMaskMouseEnter = _c.onMaskMouseEnter, onMaskMouseLeave = _c.onMaskMouseLeave, maskClass = _c.maskClass, isChildrenInMask = _c.isChildrenInMask, pinFollowBaseElementWhenFixed = _c.pinFollowBaseElementWhenFixed;
var _d = this.state, stateVisible = _d.visible, status = _d.status, animation = _d.animation;
var children = stateVisible || (cache && this._isMounted) ? propChildren : null;
if (children) {
var child = react_1.Children.only(children);
// if chlild is a functional component wrap in a component to allow a ref to be set
if (typeof child.type === 'function' && !(child.type.prototype instanceof react_1.Component)) {
child = react_1.default.createElement("div", { role: "none" }, child);
}
var childClazz = (0, classnames_1.default)((_a = {},
_a["".concat(prefix, "overlay-inner")] = true,
_a[animation.in] = status === 'entering' || status === 'mounting',
_a[animation.out] = status === 'leaving',
_a[child.props.className] = !!child.props.className,
_a[className] = !!className,
_a));
if (typeof child.ref === 'string') {
throw new Error('Can not set ref by string in Overlay, use function instead.');
}
children = (0, react_1.cloneElement)(child, {
className: childClazz,
style: tslib_1.__assign(tslib_1.__assign({}, child.props.style), style),
ref: makeChain(this.saveContentRef, child.ref),
'aria-hidden': !stateVisible && cache && this._isMounted,
onClick: makeChain(this.props.onClick, child.props.onClick),
onTouchEnd: makeChain(this.props.onTouchEnd, child.props.onTouchEnd),
});
if (align) {
var shouldUpdatePosition = status !== 'leaving' && propShouldUpdatePosition;
children = (react_1.default.createElement(position_1.default, { children: children, target: target, align: align, offset: offset, autoFit: autoFit, container: container, needAdjust: needAdjust, pinFollowBaseElementWhenFixed: pinFollowBaseElementWhenFixed, beforePosition: beforePosition, onPosition: makeChain(this.handlePosition, onPosition), shouldUpdatePosition: shouldUpdatePosition, rtl: rtl }));
}
var wrapperClazz = (0, classnames_1.default)(["".concat(prefix, "overlay-wrapper"), wrapperClassName]);
var newWrapperStyle = Object.assign({}, {
display: stateVisible ? '' : 'none',
}, wrapperStyle);
var maskClazz = (0, classnames_1.default)((_b = {},
_b["".concat(prefix, "overlay-backdrop")] = true,
_b[maskClass] = !!maskClass,
_b));
children = (react_1.default.createElement("div", { className: wrapperClazz, style: newWrapperStyle, dir: rtl ? 'rtl' : undefined },
hasMask ? (react_1.default.createElement("div", { className: maskClazz, onClick: this.handleMaskClick, onMouseEnter: onMaskMouseEnter, onMouseLeave: onMaskMouseLeave, dir: rtl ? 'rtl' : undefined }, isChildrenInMask && children)) : null,
!isChildrenInMask && children));
}
return react_1.default.createElement(gateway_1.default, { container: container, target: target, children: children, ref: this.saveGatewayRef });
};
Overlay.propTypes = {
prefix: prop_types_1.default.string,
pure: prop_types_1.default.bool,
rtl: prop_types_1.default.bool,
className: prop_types_1.default.string,
style: prop_types_1.default.object,
children: prop_types_1.default.any,
visible: prop_types_1.default.bool,
onRequestClose: prop_types_1.default.func,
target: prop_types_1.default.any,
align: prop_types_1.default.string,
offset: prop_types_1.default.array,
container: prop_types_1.default.any,
hasMask: prop_types_1.default.bool,
canCloseByEsc: prop_types_1.default.bool,
canCloseByOutSideClick: prop_types_1.default.bool,
canCloseByMask: prop_types_1.default.bool,
beforeOpen: prop_types_1.default.func,
onOpen: prop_types_1.default.func,
afterOpen: prop_types_1.default.func,
beforeClose: prop_types_1.default.func,
onClose: prop_types_1.default.func,
afterClose: prop_types_1.default.func,
beforePosition: prop_types_1.default.func,
onPosition: prop_types_1.default.func,
shouldUpdatePosition: prop_types_1.default.bool,
autoFocus: prop_types_1.default.bool,
needAdjust: prop_types_1.default.bool,
disableScroll: prop_types_1.default.bool,
useCapture: prop_types_1.default.bool,
cache: prop_types_1.default.bool,
safeNode: prop_types_1.default.any,
wrapperClassName: prop_types_1.default.string,
wrapperStyle: prop_types_1.default.object,
animation: prop_types_1.default.oneOfType([prop_types_1.default.object, prop_types_1.default.bool]),
onMaskMouseEnter: prop_types_1.default.func,
onMaskMouseLeave: prop_types_1.default.func,
onClick: prop_types_1.default.func,
maskClass: prop_types_1.default.string,
isChildrenInMask: prop_types_1.default.bool,
pinFollowBaseElementWhenFixed: prop_types_1.default.bool,
v2: prop_types_1.default.bool,
points: prop_types_1.default.array,
};
Overlay.defaultProps = {
prefix: 'next-',
pure: false,
visible: false,
onRequestClose: noop,
target: position_1.default.VIEWPORT,
align: 'tl bl',
offset: [0, 0],
hasMask: false,
canCloseByEsc: true,
canCloseByOutSideClick: true,
canCloseByMask: true,
beforeOpen: noop,
onOpen: noop,
afterOpen: noop,
beforeClose: noop,
onClose: noop,
afterClose: noop,
beforePosition: noop,
onPosition: noop,
onMaskMouseEnter: noop,
onMaskMouseLeave: noop,
shouldUpdatePosition: false,
autoFocus: false,
needAdjust: true,
disableScroll: false,
cache: false,
isChildrenInMask: false,
onTouchEnd: function (event) {
event.stopPropagation();
},
onClick: function (event) { return event.stopPropagation(); },
maskClass: '',
useCapture: true,
};
Overlay.displayName = 'Overlay';
return Overlay;
}(react_1.Component));
exports.default = (0, react_lifecycles_compat_1.polyfill)(Overlay);