@alifd/overlay
Version:
overlay base component
407 lines (392 loc) • 19.4 kB
JavaScript
;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
exports.__esModule = true;
exports["default"] = exports.RefWrapper = void 0;
var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
var _objectWithoutPropertiesLoose2 = _interopRequireDefault(require("@babel/runtime/helpers/objectWithoutPropertiesLoose"));
var _inheritsLoose2 = _interopRequireDefault(require("@babel/runtime/helpers/inheritsLoose"));
var _react = _interopRequireWildcard(require("react"));
var _reactDom = require("react-dom");
var _resizeObserverPolyfill = _interopRequireDefault(require("resize-observer-polyfill"));
var _placement = _interopRequireDefault(require("./placement"));
var _utils = require("./utils");
var _overlayContext = _interopRequireDefault(require("./overlay-context"));
var _excluded = ["target", "children", "wrapperClassName", "maskClassName", "maskStyle", "hasMask", "canCloseByMask", "maskRender", "points", "offset", "fixed", "visible", "onRequestClose", "onOpen", "onClose", "container", "placement", "placementOffset", "disableScroll", "canCloseByOutSideClick", "canCloseByEsc", "safeNode", "beforePosition", "onPosition", "cache", "autoAdjust", "autoFocus", "isAnimationEnd", "rtl", "wrapperStyle"],
_excluded2 = ["setVisibleOverlayToParent"];
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { "default": e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n["default"] = e, t && t.set(e, n), n; }
function _createForOfIteratorHelperLoose(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (t) return (t = t.call(r)).next.bind(t); if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var o = 0; return function () { return o >= r.length ? { done: !0 } : { done: !1, value: r[o++] }; }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
var isScrollDisplay = function isScrollDisplay(element) {
try {
var scrollbarStyle = window.getComputedStyle(element, '::-webkit-scrollbar');
return !scrollbarStyle || scrollbarStyle.getPropertyValue('display') !== 'none';
} catch (e) {
// ignore error for firefox
}
return true;
};
var hasScroll = function hasScroll(containerNode) {
var overflow = (0, _utils.getStyle)(containerNode, 'overflow');
if (overflow === 'hidden') {
return false;
}
var parentNode = containerNode.parentNode;
return parentNode && parentNode.scrollHeight > parentNode.clientHeight && (0, _utils.getScrollbarWidth)() > 0 && isScrollDisplay(parentNode) && isScrollDisplay(containerNode);
};
/**
* 传入的组件可能是没有 forwardRef 包裹的 Functional Component, 会导致取不到 ref
*/
var RefWrapper = exports.RefWrapper = /*#__PURE__*/function (_React$Component) {
function RefWrapper() {
return _React$Component.apply(this, arguments) || this;
}
(0, _inheritsLoose2["default"])(RefWrapper, _React$Component);
var _proto = RefWrapper.prototype;
_proto.render = function render() {
return this.props.children;
};
return RefWrapper;
}(_react["default"].Component);
var Overlay = /*#__PURE__*/_react["default"].forwardRef(function (props, ref) {
var _overflowRef$current, _overflowRef$current2;
var body = function body() {
return document.body;
};
var target = props.target,
children = props.children,
wrapperClassName = props.wrapperClassName,
maskClassName = props.maskClassName,
maskStyle = props.maskStyle,
hasMask = props.hasMask,
_props$canCloseByMask = props.canCloseByMask,
canCloseByMask = _props$canCloseByMask === void 0 ? true : _props$canCloseByMask,
maskRender = props.maskRender,
points = props.points,
offset = props.offset,
fixed = props.fixed,
visible = props.visible,
_props$onRequestClose = props.onRequestClose,
onRequestClose = _props$onRequestClose === void 0 ? function () {} : _props$onRequestClose,
onOpen = props.onOpen,
onClose = props.onClose,
_props$container = props.container,
popupContainer = _props$container === void 0 ? body : _props$container,
placement = props.placement,
placementOffset = props.placementOffset,
_props$disableScroll = props.disableScroll,
disableScroll = _props$disableScroll === void 0 ? false : _props$disableScroll,
_props$canCloseByOutS = props.canCloseByOutSideClick,
canCloseByOutSideClick = _props$canCloseByOutS === void 0 ? true : _props$canCloseByOutS,
_props$canCloseByEsc = props.canCloseByEsc,
canCloseByEsc = _props$canCloseByEsc === void 0 ? true : _props$canCloseByEsc,
safeNode = props.safeNode,
beforePosition = props.beforePosition,
onPosition = props.onPosition,
_props$cache = props.cache,
cache = _props$cache === void 0 ? false : _props$cache,
autoAdjust = props.autoAdjust,
_props$autoFocus = props.autoFocus,
autoFocus = _props$autoFocus === void 0 ? false : _props$autoFocus,
_props$isAnimationEnd = props.isAnimationEnd,
isAnimationEnd = _props$isAnimationEnd === void 0 ? true : _props$isAnimationEnd,
rtl = props.rtl,
owrapperStyle = props.wrapperStyle,
others = (0, _objectWithoutPropertiesLoose2["default"])(props, _excluded);
var position = fixed ? 'fixed' : 'absolute';
var _useState = (0, _react.useState)(visible),
firstVisible = _useState[0],
setFirst = _useState[1];
var _useState2 = (0, _react.useState)(null),
forceUpdate = _useState2[1];
var positionStyleRef = (0, _react.useRef)({
position: position
});
var getContainer = typeof popupContainer === 'string' ? function () {
return document.getElementById(popupContainer);
} : typeof popupContainer !== 'function' ? function () {
return popupContainer;
} : popupContainer;
var _useState3 = (0, _react.useState)(null),
container = _useState3[0],
setContainer = _useState3[1];
var targetRef = (0, _react.useRef)(null);
var preTarget = (0, _react.useRef)(target);
var overlayRef = (0, _react.useRef)(null);
var containerRef = (0, _react.useRef)(null);
var maskRef = (0, _react.useRef)(null);
var overflowRef = (0, _react.useRef)([]);
var lastFocus = (0, _react.useRef)(null);
var ro = (0, _react.useRef)(null);
var _useState4 = (0, _react.useState)(Date.now().toString(36)),
uuid = _useState4[0];
var _useContext = (0, _react.useContext)(_overlayContext["default"]),
setVisibleOverlayToParent = _useContext.setVisibleOverlayToParent,
otherContext = (0, _objectWithoutPropertiesLoose2["default"])(_useContext, _excluded2);
var childIDMap = (0, _react.useRef)(new Map());
var handleOpen = function handleOpen(node) {
setVisibleOverlayToParent(uuid, node);
onOpen === null || onOpen === void 0 ? void 0 : onOpen(node);
};
var handleClose = function handleClose() {
positionStyleRef.current = null;
setVisibleOverlayToParent(uuid, null);
onClose === null || onClose === void 0 ? void 0 : onClose();
};
var getVisibleOverlayFromChild = function getVisibleOverlayFromChild(id, node) {
if (node) {
childIDMap.current.set(id, node);
} else {
childIDMap.current["delete"](id);
}
// 让父级也感知
setVisibleOverlayToParent(id, node);
};
var child = _react["default"].Children.only(children);
if (typeof child.ref === 'string') {
throw new Error('Can not set ref by string in Overlay, use function instead.');
}
var updatePosition = (0, _utils.useEvent)(function () {
var overlayNode = overlayRef.current;
var containerNode = containerRef.current;
var targetNode = targetRef.current;
if (!overlayNode || !containerNode || !targetNode) {
return;
}
var placements = (0, _placement["default"])({
target: targetNode,
overlay: overlayNode,
container: containerNode,
scrollNode: overflowRef.current,
points: points,
offset: offset,
position: position,
placement: placement,
placementOffset: placementOffset,
beforePosition: beforePosition,
autoAdjust: autoAdjust,
rtl: rtl,
autoHideScrollOverflow: others.autoHideScrollOverflow
});
if (!(0, _utils.isSameObject)(positionStyleRef.current, placements.style)) {
positionStyleRef.current = placements.style;
(0, _utils.setStyle)(overlayNode, placements.style);
typeof onPosition === 'function' && onPosition(placements);
}
});
// 弹窗挂载
var overlayRefCallback = (0, _react.useCallback)(function (nodeRef) {
var node = (0, _reactDom.findDOMNode)(nodeRef);
overlayRef.current = node;
(0, _utils.callRef)(ref, node);
if (node !== null && container) {
var containerNode = (0, _utils.getRelativeContainer)((0, _utils.getHTMLElement)(container));
containerRef.current = containerNode;
var targetElement = target === 'viewport' ? hasMask ? maskRef.current : body() : (0, _utils.getTargetNode)(target) || body();
var targetNode = (0, _utils.getHTMLElement)(targetElement);
targetRef.current = targetNode;
overflowRef.current = (0, _utils.getOverflowNodes)(targetNode, containerNode);
// fixme: 在followTrigger且空间受限且overlay自动宽度情况下,overlay宽度会跟随left设定自动撑满containing block最右侧,这里建议手动设定overlay宽度或拥有固定内容宽度的overlay来解决,这里暂时使用原来的-1000位置的方案隐藏overlay并不影响容器宽高
(0, _utils.setStyle)(node, {
position: fixed ? 'fixed' : 'absolute',
top: -1000,
left: -1000
});
var waitTime = 100;
var throttledUpdatePosition = (0, _utils.throttle)(updatePosition, waitTime);
ro.current = new _resizeObserverPolyfill["default"](throttledUpdatePosition);
ro.current.observe(containerNode);
ro.current.observe(node);
// fist call, 不依赖 ResizeObserver observe时的首次执行(测试环境不会执行),因为 throttle 原因也不会执行两次
throttledUpdatePosition();
forceUpdate({});
if (autoFocus) {
// 这里setTimeout是等弹窗位置计算完成再进行 focus,否则弹窗还在页面最低端,会出现突然滚动到页面最下方的情况
setTimeout(function () {
var focusableNodes = (0, _utils.getFocusNodeList)(node);
if (focusableNodes.length > 0 && focusableNodes[0]) {
lastFocus.current = document.activeElement;
focusableNodes[0].focus();
}
}, waitTime);
}
!cache && handleOpen(node);
} else {
!cache && handleClose();
if (ro.current) {
ro.current.disconnect();
ro.current = null;
}
}
}, [container]);
var clickEvent = function clickEvent(e) {
// 点击在子元素上面,则忽略。为了兼容 react16,这里用 contains 判断而不利用 e.stopPropagation() 阻止冒泡的特性来处理
for (var _iterator = _createForOfIteratorHelperLoose(childIDMap.current.entries()), _step; !(_step = _iterator()).done;) {
var _step$value = _step.value,
oNode = _step$value[1];
var _node = (0, _utils.getHTMLElement)(oNode);
if (_node && (_node === e.target || _node.contains(e.target))) {
return;
}
}
if (!visible) {
return;
}
// 点击遮罩关闭
if (hasMask && maskRef.current === e.target) {
if (canCloseByMask) {
onRequestClose('maskClick', e); // TODO: will rename to `mask` in 1.0
}
return;
}
var safeNodeList = Array.isArray(safeNode) ? safeNode : [safeNode];
// 弹层默认是安全节点
if (overlayRef.current) {
safeNodeList.push(function () {
return overlayRef.current;
});
}
// 安全节点不关闭
for (var i = 0; i < safeNodeList.length; i++) {
var _safeNode = (0, _utils.getTargetNode)(safeNodeList[i]);
var node = (0, _utils.getHTMLElement)(_safeNode);
if (node && (node === e.target || node.contains(e.target))) {
return;
}
}
if (canCloseByOutSideClick) {
onRequestClose('docClick', e); // TODO: will rename to `doc` in 1.0
}
};
// 这里用 mousedown 而不是用 click。因为 click 是 mouseup 才触发。
// 如果用 click 带来的问题: mousedown 在弹窗内部,然后按住鼠标不放拖动到弹窗外触发 mouseup 结果弹窗关了,这是不期望的展示。 https://github.com/alibaba-fusion/next/issues/742
// react 17 冒泡问题:
// - react17 中,如果弹窗 mousedown 阻止了 e.stopPropagation(), 那么 document 就不会监听到事件,因为事件冒泡到挂载节点 rootElement 就中断了。
// - https://reactjs.org/blog/2020/08/10/react-v17-rc.html#changes-to-event-delegation
(0, _utils.useListener)(typeof document !== 'undefined' ? document : null, 'mousedown', clickEvent, false, !!(visible && overlayRef.current && (canCloseByOutSideClick || hasMask && canCloseByMask)));
var keydownEvent = function keydownEvent(e) {
if (!visible) {
return;
}
// 无子元素才能 esc 取消关闭
if (e.keyCode === 27 && canCloseByEsc && !childIDMap.current.size) {
onRequestClose('esc', e);
}
};
(0, _utils.useListener)(typeof document !== 'undefined' ? document : null, 'keydown', keydownEvent, false, !!(visible && overlayRef.current && canCloseByEsc));
var scrollEvent = function scrollEvent(e) {
if (!visible) {
return;
}
updatePosition();
};
(0, _utils.useListener)(typeof document !== 'undefined' ? (_overflowRef$current = overflowRef.current) === null || _overflowRef$current === void 0 ? void 0 : _overflowRef$current.map(function (t) {
return t === document.documentElement ? document : t;
}) : null, 'scroll', scrollEvent, false, !!(visible && overlayRef.current && (_overflowRef$current2 = overflowRef.current) !== null && _overflowRef$current2 !== void 0 && _overflowRef$current2.length));
// 有弹窗情况下在 body 增加 overflow:hidden,两个弹窗同时存在也没问题,会按照堆的方式依次 pop
(0, _react.useEffect)(function () {
if (visible && disableScroll) {
var originStyle = document.body.getAttribute('style');
(0, _utils.setStyle)(document.body, 'overflow', 'hidden');
if (hasScroll(document.body)) {
var scrollWidth = (0, _utils.getScrollbarWidth)();
if (scrollWidth) {
(0, _utils.setStyle)(document.body, 'padding-right', "calc(" + (0, _utils.getStyle)(document.body, 'padding-right') + " + " + scrollWidth + "px)");
}
}
return function () {
document.body.setAttribute('style', originStyle || '');
};
}
return undefined;
}, [visible && disableScroll]);
// 第一次加载并且 visible=false 的情况不挂载弹窗
(0, _react.useEffect)(function () {
if (!firstVisible && visible) {
setFirst(true);
}
}, [visible]);
// cache 情况下的模拟 onOpen/onClose
var overlayNode = overlayRef.current; // overlayRef.current 可能会异步变化,所以要先接下
(0, _react.useEffect)(function () {
if (cache && overlayNode) {
if (visible) {
updatePosition();
handleOpen(overlayNode);
} else {
handleClose();
}
}
}, [visible, cache && overlayNode]);
// target 动态更新则重新刷新定位
(0, _react.useEffect)(function () {
if (visible && overlayNode) {
if (target && targetRef.current && preTarget.current !== target) {
var targetElement = target === 'viewport' ? hasMask ? maskRef.current : body() : (0, _utils.getTargetNode)(target) || body();
var targetNode = (0, _utils.getHTMLElement)(targetElement);
if (targetNode && targetRef.current !== targetNode) {
targetRef.current = targetNode;
updatePosition();
}
preTarget.current = target;
}
}
}, [target]);
(0, _react.useEffect)(function () {
if (visible && overlayNode) {
updatePosition();
}
}, [offset, placement, placementOffset, points, autoAdjust, rtl]);
// autoFocus 弹窗关闭后回到触发点
(0, _react.useEffect)(function () {
if (!visible && autoFocus && lastFocus.current) {
lastFocus.current.focus();
lastFocus.current = null;
}
}, [!visible && autoFocus && lastFocus.current]);
// container 异步加载, 因为 container 很可能还没渲染完成,所以 visible 后这里异步设置下
(0, _react.useEffect)(function () {
if (visible) {
// 首次更新
if (!container) {
setContainer(getContainer());
} else if (getContainer() !== container) {
setContainer(getContainer());
}
}
}, [visible, popupContainer]);
if (firstVisible === false || !container) {
return null;
}
if (!visible && !cache && isAnimationEnd) {
return null;
}
var newChildren = child ? /*#__PURE__*/_react["default"].createElement(RefWrapper, {
ref: overlayRefCallback
}, /*#__PURE__*/(0, _react.cloneElement)(child, (0, _extends2["default"])({}, others, {
style: (0, _extends2["default"])({
top: 0,
left: 0
}, child.props.style, positionStyleRef.current)
}))) : null;
var wrapperStyle = (0, _extends2["default"])({}, owrapperStyle);
if (cache && !visible && isAnimationEnd) {
wrapperStyle.display = 'none';
}
var maskNode = /*#__PURE__*/_react["default"].createElement("div", {
className: maskClassName,
style: maskStyle,
ref: maskRef
});
var content = /*#__PURE__*/_react["default"].createElement("div", {
className: wrapperClassName,
style: wrapperStyle
}, hasMask ? maskRender ? maskRender(maskNode) : maskNode : null, newChildren);
return /*#__PURE__*/_react["default"].createElement(_overlayContext["default"].Provider, {
value: (0, _extends2["default"])({}, otherContext, {
setVisibleOverlayToParent: getVisibleOverlayFromChild
})
}, /*#__PURE__*/(0, _reactDom.createPortal)(content, container));
});
var _default = exports["default"] = Overlay;