弹框可见性的控制
总体来说就是通过click、hover、改变焦点的方式,打开或关闭弹层。
使用场景如:
- 当鼠标移入帮助符号时,显示文本提示
- 当鼠标移出时,隐藏文本提示。
视图层有两种要素:页面元素和用户行为。
仿照胡克定律可以得出 view = handler(action)
rc-trigger 集成了弹层显示隐藏的处理逻辑,以便在操作挂载元素时显示和隐藏弹层。
为了使弹层不受触发元素位置及大小的影响,rc-trigger 统一将弹层插入到 document 根节点中
Popup 组件既用于绘制蒙层,又用于组织弹层的动效以及调整弹层的位置
PopupInner 对接 Trigger,弹层实际内容外围挂载鼠标移入移出事件对弹层的影响
LazyRenderBox 根据 props.visible 等属性,决定是否需要绘制弹层实际内容,还是绘制空的 div
Popup 用于渲染弹层的实际内容
Trigger
- 渲染时将弹层插入 document 根节点: 因为如果把弹窗渲染在正常文档流中,组件的定位会受到父元素定位的影响。如果父元素为静态定位,定位标的就会变成再父一级元素。将弹窗挂载在 body 下,就能解决这个问题。
- 通过 props 中的 action 、showAction、hideAction 指定切换弹层显示隐藏状态的事件
- 实现 onClick 等方法以切换弹层的显示隐藏状态
- 实现 onPopupMouseLeave 等方法,透传到 PopupInner 组件中,以使鼠标移出弹层时隐藏弹层
- PopupInner 渲染了个div,添了个 LazyRenderBox 功能组件
- Trigger 将弹层动效、位置调整相关属性传入 Popup 组件中
弹层渲染
- React 16,借助 ReactDOM.createPortal
- React 15,通过 rc-utils 提供的 ContainerRender 组件完成

// 没有指定 props.getPopupContainer 时,弹层在根节点中创建 div 元素并完成渲染getContainer = () => {const { props } = this;const popupContainer = document.createElement('div');// Make sure default popup container will never cause scrollbar appearing// https://github.com/react-component/trigger/issues/41popupContainer.style.position = 'absolute';popupContainer.style.top = '0';popupContainer.style.left = '0';popupContainer.style.width = '100%';const mountNode = props.getPopupContainer ?props.getPopupContainer(findDOMNode(this)) : props.getDocument().body;mountNode.appendChild(popupContainer);return popupContainer;}render(){// ...// getComponent 渲染 Popup 等弹层组件if (!IS_REACT_16) {return (<ContainerRenderparent={this}visible={popupVisible}autoMount={false}forceRender={forceRender}getComponent={this.getComponent}getContainer={this.getContainer}>{({ renderComponent }) => {this.renderComponent = renderComponent;return trigger;}}</ContainerRender>);}let portal;// prevent unmounting after it's renderedif (popupVisible || this._component || forceRender) {portal = (<Portalkey="portal"getContainer={this.getContainer}didUpdate={this.handlePortalUpdate}>{this.getComponent()}</Portal>);}return [trigger,portal,];}
触发元素的绑定事件
显示弹层:
- onClick
- onMouseDown
- onMouseEnter
- onFocus
- onTouchStart
- onContextMenu
隐藏弹层:
- onClick
- onMouseLeave
- onBlur
rc-trigger 有两层处理逻辑:
- 若所处理的事件不影响弹层的显隐,将 createTwoChains 创建的兜底函数作为绑定函数,直接调用外层 Trigger 元素或 children 触发元素的 props 同名方法;
- 若影响,使用内置的 onClick 方法作为事件的绑定函数,以切换弹层的显示隐藏状态。
// 1. 事件的兜底处理函数// const ALL_HANDLERS = ['onClick', 'onMouseDown', 'onTouchStart', 'onMouseEnter',// 'onMouseLeave', 'onFocus', 'onBlur', 'onContextMenu'];componentWillMount() {ALL_HANDLERS.forEach((h) => {this[`fire${h}`] = (e) => {this.fireEvents(h, e);};});}// 先调用触发元素 children 的 props 方法,再调用 Trigger 元素的 props 方法fireEvents(type, e) {const childCallback = this.props.children.props[type];if (childCallback) {childCallback(e);}const callback = this.props[type];if (callback) {callback(e);}}// 在 render 时作为触发元素 children 实际绑定的方法createTwoChains(event) {const childPros = this.props.children.props;const props = this.props;if (childPros[event] && props[event]) {return this[`fire${event}`];}return childPros[event] || props[event];}// 2. 通过事件显隐弹层// 实时或延迟显隐弹窗delaySetPopupVisible(visible, delayS, event) {const delay = delayS * 1000;this.clearDelayTimer();if (delay) {const point = event ? { pageX: event.pageX, pageY: event.pageY } : null;this.delayTimer = setTimeout(() => {this.setPopupVisible(visible, point);this.clearDelayTimer();}, delay);} else {this.setPopupVisible(visible, event);}}onClick = (event) => {this.fireEvents('onClick', event);// focus will trigger click// 聚焦时快速点击,不必隐藏弹层if (this.focusTime) {let preTime;if (this.preClickTime && this.preTouchTime) {preTime = Math.min(this.preClickTime, this.preTouchTime);} else if (this.preClickTime) {preTime = this.preClickTime;} else if (this.preTouchTime) {preTime = this.preTouchTime;}if (Math.abs(preTime - this.focusTime) < 20) {return;}this.focusTime = 0;}this.preClickTime = 0;this.preTouchTime = 0;if (event && event.preventDefault) {event.preventDefault();}const nextVisible = !this.state.popupVisible;if (this.isClickToHide() && !nextVisible || nextVisible && this.isClickToShow()) {this.setPopupVisible(!this.state.popupVisible, event);}}onMouseDown = (e) => {this.fireEvents('onMouseDown', e);this.preClickTime = Date.now();}onTouchStart = (e) => {this.fireEvents('onTouchStart', e);this.preTouchTime = Date.now();}onFocus = (e) => {this.fireEvents('onFocus', e);// incase focusin and focusoutthis.clearDelayTimer();if (this.isFocusToShow()) {this.focusTime = Date.now();this.delaySetPopupVisible(true, this.props.focusDelay);}}
弹层的绑定事件
- 支持在文档被点击时隐藏弹层:对于文档中挂载的事件,Trigger 组件在 componentDidUpdate 生命周期中对 document 节点绑定事件,所绑定的事件不限于点击,还包含文档滚动、窗口失焦;
- 支持在鼠标移出弹层时隐藏弹层:对于弹层挂载的事件,Trigger 组件实现了 onPopupMouseEnter, onPopupMouseLeave, onPopupMouseDown 方法,并透传给 PopupInner 组件,作为该组件渲染内容的绑定函数
// 1. 文档绑定事件处理函数// 根据可操控弹层显隐的事件,对 document 或 window 绑定事件处理函数componentDidUpdate(_, prevState) {const props = this.props;const state = this.state;const triggerAfterPopupVisibleChange = () => {if (prevState.popupVisible !== state.popupVisible) {props.afterPopupVisibleChange(state.popupVisible);}};if (!IS_REACT_16) {this.renderComponent(null, triggerAfterPopupVisibleChange);}this.prevPopupVisible = prevState.popupVisible;// We must listen to `mousedown` or `touchstart`, edge case:// https://github.com/ant-design/ant-design/issues/5804// https://github.com/react-component/calendar/issues/250// https://github.com/react-component/trigger/issues/50if (state.popupVisible) {let currentDocument;if (!this.clickOutsideHandler && (this.isClickToHide() || this.isContextMenuToShow())) {currentDocument = props.getDocument();this.clickOutsideHandler = addEventListener(currentDocument,'mousedown', this.onDocumentClick);}// always hide on mobileif (!this.touchOutsideHandler) {currentDocument = currentDocument || props.getDocument();this.touchOutsideHandler = addEventListener(currentDocument,'touchstart', this.onDocumentClick);}// close popup when trigger type contains 'onContextMenu' and document is scrolling.if (!this.contextMenuOutsideHandler1 && this.isContextMenuToShow()) {currentDocument = currentDocument || props.getDocument();this.contextMenuOutsideHandler1 = addEventListener(currentDocument,'scroll', this.onContextMenuClose);}// close popup when trigger type contains 'onContextMenu' and window is blur.if (!this.contextMenuOutsideHandler2 && this.isContextMenuToShow()) {this.contextMenuOutsideHandler2 = addEventListener(window,'blur', this.onContextMenuClose);}return;}this.clearOutsideHandler();}// 当蒙层可关闭时,点击文档关闭弹层onDocumentClick = (event) => {if (this.props.mask && !this.props.maskClosable) {return;}const target = event.target;const root = findDOMNode(this);if (!contains(root, target) && !this.hasPopupMouseDown) {this.close();}}// 鼠标右键可显示弹层时,通过文档滚动、窗口失焦可隐藏弹层onContextMenuClose = () => {if (this.isContextMenuToShow()) {this.close();}}close() {this.setPopupVisible(false);}// 2. 弹层绑定事件处理函数onPopupMouseEnter = () => {this.clearDelayTimer();}// 当鼠标移出弹层时,隐藏弹层// this._component 即 Popup 组件实例// this._component.getPopupDomNode 用于获取 PopupInner 组件绘制的节点内容onPopupMouseLeave = (e) => {// https://github.com/react-component/trigger/pull/13// react bug?if (e.relatedTarget && !e.relatedTarget.setTimeout &&this._component &&this._component.getPopupDomNode &&contains(this._component.getPopupDomNode(), e.relatedTarget)) {return;}this.delaySetPopupVisible(false, this.props.mouseLeaveDelay);}// 点击弹层变更 Trigger 实例的 hasPopupMouseDown 属性,以指定文档点击区域不是弹层内部// 当弹层相互嵌套时,向上递归调用 onPopupMouseDown 方法也用于阻止祖先弹层的隐藏onPopupMouseDown = (...args) => {const { rcTrigger = {} } = this.context;this.hasPopupMouseDown = true;clearTimeout(this.mouseDownTimeout);this.mouseDownTimeout = setTimeout(() => {this.hasPopupMouseDown = false;}, 0);if (rcTrigger.onPopupMouseDown) {rcTrigger.onPopupMouseDown(...args);}};
弹层的位置调整
Popup -> Animate -> AnimateChild -> rcAlign -> PopupInner -> LazyRenderBox -> content
Popup 组件实现了:
- 是否需要遮罩
- zIndex 设为多少
- 隐藏要不要卸载掉弹框
弹层的位置调整基于 rc-align 类库。Popup 脱离了正常文档流,挂在了 document.body 上。那如何把两个风马牛不相及的元素对齐呢?还要考虑到窗口改变时也能对得上。靠的是元素的可视矩阵(区域)计算。
在实现上,通过将 props.alignPoint 置为真值,弹层位置即可根据鼠标移动情况进行调整;默认情况下,弹层位置取决于触发元素的显示位置。Trigger 将动态计算鼠标的位置 state.point,随后将 props.align 注入到 Popup 组件。若 Popup 组件接受的 props.align 为否值,弹层位置即取决于触发元素的显示位置;否则,由鼠标位置决定。在位置调整过程中,Trigger 组件接受的 props.onPopupAlign 可用于监听弹层位置的调整状况,以便于动态微调。
// 获取实际注入 dom-align 类库的 alignConfig 配置,用于调整弹层位置// 参见 https://github.com/yiminghe/dom-alignfunction getAlignFromPlacement(builtinPlacements, placementStr, align) {const baseAlign = builtinPlacements[placementStr] || {};return {...baseAlign,...align,};}class Trigger extends React.Component {// props.builtinPlacements 内置多种弹层放置策略,实际使用 props.popupPlacement 放置策略// props.popupAlign 作为 dom-align 库获得的 alignConfig 配置,用于调整位置getPopupAlign() {const props = this.props;const { popupPlacement, popupAlign, builtinPlacements } = props;if (popupPlacement && builtinPlacements) {return getAlignFromPlacement(builtinPlacements, popupPlacement, popupAlign);}return popupAlign;}// 当 props.alignPoint 为真值,Popup 组件获得的 props.align 即为 state.point// 意义是弹层的定位位置将根据鼠标位置进行调整setPoint = (point) => {const { alignPoint } = this.props;if (!alignPoint || !point) return;this.setState({point: {pageX: point.pageX,pageY: point.pageY,},});}}class Popup extends Component {// 位置调整时调用onAlign = (popupDomNode, align) => {const props = this.props;const currentAlignClassName = props.getClassNameFromAlign(align);// FIX: https://github.com/react-component/trigger/issues/56// FIX: https://github.com/react-component/tooltip/issues/79if (this.currentAlignClassName !== currentAlignClassName) {this.currentAlignClassName = currentAlignClassName;popupDomNode.className = this.getClassName(currentAlignClassName);}props.onAlign(popupDomNode, align);}// 获取弹层的定位位置,有两种可能:根据鼠标调整,或者触发元素的位置getAlignTarget = () => {const { point } = this.props;if (point) {return point;}// getTargetElement 方法由 Trigger 注入,用于获取 Trigger 元素return this.getTargetElement;}// 使用 Align 虚拟组件包裹实际渲染内容,调整弹层的显示位置getPopupElement() {// ...<Align target={this.getAlignTarget()} key="popup" ref={this.saveAlignRef}monitorWindowResize align={align} onAlign={this.onAlign}><PopupInner visible {...popupInnerProps}>{children}</PopupInner></Align>// ...}}
弹层动效
弹层的动效基于 rc-animate 类库实现,包含蒙层和弹层实际内容的动效,它会调用子组件中的钩子函数,来控制动画效果,Antd 动画主要是 CSS3 动画。
- props.popupAnimation
- props.popupTransitionName
- props.maskAnimation
- props.maskTransitionName
资源
rc-trigger@3.0.0 - base abstract trigger component for react
