原生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。