原生dom可以很容易的实现简单的dropdown,却很难满足我们的各种需求,因此各式各样的dropdown第三方实现就出现了。ant-design是基于react实现的一组UI组件,我们选择对其中的dropdown进行分析。
Dropdown 的主要组成:
-
一个弹出的下拉列表
-
一个当前的选中项
实现时需要注意的几个问题:什么时候弹出下拉选项, 下拉选项挂放在哪个位置。直观上下拉选项应该是一个绝对定位的Popup div。区别用户事件是发生在下拉选项内部还是外部,毕竟外部可能是期望收起这个下拉列表。
带着这几个问题我们开始看
react-component/dropdown
对外的接口:
onOverlayClick: func
onVisibleChange: func
animation: any
align: object
placement: string
overlay: node
trigger: array
alignPoint: bool
showAction:
hideAction
getPopupContainer: func复制代码
通过这几个接口可以发现我们的那些问题都有对应的答案
dropdown react节点图
dropdown 的源码主要是调用了Trigger这个抽象组件实现大部分的逻辑。
Trigger组件是一个抽象化组件,用来指定popup类型的UI。涉及到的包括popup, alignment。
Trigger创建、关闭popup
创建Popup在react 16之后提供了
Portal可以实现将内容指定到非当前节点所在层级的div上,方便了popup,而为兼容之前的版本使用的ReactDOM.unstable_renderSubtreeIntoContainer 等方法,实现较为复杂,具体可看util中的
ContainerRender。
在Trigger component的componentDidUpdate生命周期里面监听事件。
if (state.popupVisible) {
let currentDocument;
if (!this.clickOutsideHandler && (this.isClickToHide() || this.isContextMenuToShow())) {
currentDocument = props.getDocument();
this.clickOutsideHandler = addEventListener(currentDocument,
'mousedown', this.onDocumentClick);
}
// always hide on mobile
if (!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;
}复制代码
判断鼠标是否离开popup
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);
}复制代码
操作在popup上还是其他区域
onDocumentClick = (event) => {
if (this.props.mask && !this.props.maskClosable) {
return;
}
const target = event.target;
const root = findDOMNode(this);
const popupNode = this.getPopupDomNode();
if (!contains(root, target) && !contains(popupNode, target)) {
this.close();
}
}复制代码
Trigger Alignment
Trigger 做的另一件事情就是对popup的位置进行定位。介绍下dom-align库,用于指定dom节点对齐位置。API主要接口定义source、target、offset、overflow。