@alifd/overlay
Version:
overlay base component
400 lines (386 loc) • 17.6 kB
JavaScript
import _extends from "@babel/runtime/helpers/extends";
import _objectWithoutPropertiesLoose from "@babel/runtime/helpers/objectWithoutPropertiesLoose";
import _inheritsLoose from "@babel/runtime/helpers/inheritsLoose";
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 _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; }
import React, { useEffect, useState, useCallback, useRef, cloneElement, useContext } from 'react';
import { findDOMNode, createPortal } from 'react-dom';
import ResizeObserver from 'resize-observer-polyfill';
import getPlacements from './placement';
import { useListener, getHTMLElement, getTargetNode, getStyle, setStyle, getRelativeContainer, throttle, callRef, getOverflowNodes, getScrollbarWidth, getFocusNodeList, isSameObject, useEvent } from './utils';
import OverlayContext from './overlay-context';
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 = getStyle(containerNode, 'overflow');
if (overflow === 'hidden') {
return false;
}
var parentNode = containerNode.parentNode;
return parentNode && parentNode.scrollHeight > parentNode.clientHeight && getScrollbarWidth() > 0 && isScrollDisplay(parentNode) && isScrollDisplay(containerNode);
};
/**
* 传入的组件可能是没有 forwardRef 包裹的 Functional Component, 会导致取不到 ref
*/
export var RefWrapper = /*#__PURE__*/function (_React$Component) {
function RefWrapper() {
return _React$Component.apply(this, arguments) || this;
}
_inheritsLoose(RefWrapper, _React$Component);
var _proto = RefWrapper.prototype;
_proto.render = function render() {
return this.props.children;
};
return RefWrapper;
}(React.Component);
var Overlay = /*#__PURE__*/React.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 = _objectWithoutPropertiesLoose(props, _excluded);
var position = fixed ? 'fixed' : 'absolute';
var _useState = useState(visible),
firstVisible = _useState[0],
setFirst = _useState[1];
var _useState2 = useState(null),
forceUpdate = _useState2[1];
var positionStyleRef = useRef({
position: position
});
var getContainer = typeof popupContainer === 'string' ? function () {
return document.getElementById(popupContainer);
} : typeof popupContainer !== 'function' ? function () {
return popupContainer;
} : popupContainer;
var _useState3 = useState(null),
container = _useState3[0],
setContainer = _useState3[1];
var targetRef = useRef(null);
var preTarget = useRef(target);
var overlayRef = useRef(null);
var containerRef = useRef(null);
var maskRef = useRef(null);
var overflowRef = useRef([]);
var lastFocus = useRef(null);
var ro = useRef(null);
var _useState4 = useState(Date.now().toString(36)),
uuid = _useState4[0];
var _useContext = useContext(OverlayContext),
setVisibleOverlayToParent = _useContext.setVisibleOverlayToParent,
otherContext = _objectWithoutPropertiesLoose(_useContext, _excluded2);
var childIDMap = 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.Children.only(children);
if (typeof child.ref === 'string') {
throw new Error('Can not set ref by string in Overlay, use function instead.');
}
var updatePosition = useEvent(function () {
var overlayNode = overlayRef.current;
var containerNode = containerRef.current;
var targetNode = targetRef.current;
if (!overlayNode || !containerNode || !targetNode) {
return;
}
var placements = getPlacements({
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 (!isSameObject(positionStyleRef.current, placements.style)) {
positionStyleRef.current = placements.style;
setStyle(overlayNode, placements.style);
typeof onPosition === 'function' && onPosition(placements);
}
});
// 弹窗挂载
var overlayRefCallback = useCallback(function (nodeRef) {
var node = findDOMNode(nodeRef);
overlayRef.current = node;
callRef(ref, node);
if (node !== null && container) {
var containerNode = getRelativeContainer(getHTMLElement(container));
containerRef.current = containerNode;
var targetElement = target === 'viewport' ? hasMask ? maskRef.current : body() : getTargetNode(target) || body();
var targetNode = getHTMLElement(targetElement);
targetRef.current = targetNode;
overflowRef.current = getOverflowNodes(targetNode, containerNode);
// fixme: 在followTrigger且空间受限且overlay自动宽度情况下,overlay宽度会跟随left设定自动撑满containing block最右侧,这里建议手动设定overlay宽度或拥有固定内容宽度的overlay来解决,这里暂时使用原来的-1000位置的方案隐藏overlay并不影响容器宽高
setStyle(node, {
position: fixed ? 'fixed' : 'absolute',
top: -1000,
left: -1000
});
var waitTime = 100;
var throttledUpdatePosition = throttle(updatePosition, waitTime);
ro.current = new ResizeObserver(throttledUpdatePosition);
ro.current.observe(containerNode);
ro.current.observe(node);
// fist call, 不依赖 ResizeObserver observe时的首次执行(测试环境不会执行),因为 throttle 原因也不会执行两次
throttledUpdatePosition();
forceUpdate({});
if (autoFocus) {
// 这里setTimeout是等弹窗位置计算完成再进行 focus,否则弹窗还在页面最低端,会出现突然滚动到页面最下方的情况
setTimeout(function () {
var focusableNodes = 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 = 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 = getTargetNode(safeNodeList[i]);
var node = 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
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);
}
};
useListener(typeof document !== 'undefined' ? document : null, 'keydown', keydownEvent, false, !!(visible && overlayRef.current && canCloseByEsc));
var scrollEvent = function scrollEvent(e) {
if (!visible) {
return;
}
updatePosition();
};
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
useEffect(function () {
if (visible && disableScroll) {
var originStyle = document.body.getAttribute('style');
setStyle(document.body, 'overflow', 'hidden');
if (hasScroll(document.body)) {
var scrollWidth = getScrollbarWidth();
if (scrollWidth) {
setStyle(document.body, 'padding-right', "calc(" + getStyle(document.body, 'padding-right') + " + " + scrollWidth + "px)");
}
}
return function () {
document.body.setAttribute('style', originStyle || '');
};
}
return undefined;
}, [visible && disableScroll]);
// 第一次加载并且 visible=false 的情况不挂载弹窗
useEffect(function () {
if (!firstVisible && visible) {
setFirst(true);
}
}, [visible]);
// cache 情况下的模拟 onOpen/onClose
var overlayNode = overlayRef.current; // overlayRef.current 可能会异步变化,所以要先接下
useEffect(function () {
if (cache && overlayNode) {
if (visible) {
updatePosition();
handleOpen(overlayNode);
} else {
handleClose();
}
}
}, [visible, cache && overlayNode]);
// target 动态更新则重新刷新定位
useEffect(function () {
if (visible && overlayNode) {
if (target && targetRef.current && preTarget.current !== target) {
var targetElement = target === 'viewport' ? hasMask ? maskRef.current : body() : getTargetNode(target) || body();
var targetNode = getHTMLElement(targetElement);
if (targetNode && targetRef.current !== targetNode) {
targetRef.current = targetNode;
updatePosition();
}
preTarget.current = target;
}
}
}, [target]);
useEffect(function () {
if (visible && overlayNode) {
updatePosition();
}
}, [offset, placement, placementOffset, points, autoAdjust, rtl]);
// autoFocus 弹窗关闭后回到触发点
useEffect(function () {
if (!visible && autoFocus && lastFocus.current) {
lastFocus.current.focus();
lastFocus.current = null;
}
}, [!visible && autoFocus && lastFocus.current]);
// container 异步加载, 因为 container 很可能还没渲染完成,所以 visible 后这里异步设置下
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.createElement(RefWrapper, {
ref: overlayRefCallback
}, /*#__PURE__*/cloneElement(child, _extends({}, others, {
style: _extends({
top: 0,
left: 0
}, child.props.style, positionStyleRef.current)
}))) : null;
var wrapperStyle = _extends({}, owrapperStyle);
if (cache && !visible && isAnimationEnd) {
wrapperStyle.display = 'none';
}
var maskNode = /*#__PURE__*/React.createElement("div", {
className: maskClassName,
style: maskStyle,
ref: maskRef
});
var content = /*#__PURE__*/React.createElement("div", {
className: wrapperClassName,
style: wrapperStyle
}, hasMask ? maskRender ? maskRender(maskNode) : maskNode : null, newChildren);
return /*#__PURE__*/React.createElement(OverlayContext.Provider, {
value: _extends({}, otherContext, {
setVisibleOverlayToParent: getVisibleOverlayFromChild
})
}, /*#__PURE__*/createPortal(content, container));
});
export default Overlay;