UNPKG

@alifd/overlay

Version:
407 lines (392 loc) 19.4 kB
"use strict"; 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;