本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。
前言
Hello 大家好! 我是前端 无名
背景
做移动端H5业务我们经常会遇到右上角有个更多按钮,点击更多按钮,出现下拉菜单,点击菜单其他区域不仅要关闭弹窗而且还要触发点击区域的事件。
如下图:
点击红框区域,弹窗要消失,如果点击在“打开另外一个弹窗”按钮上,还要触发这个按钮的点击事件。
难点
由于这种提示弹窗一般是相对于点击元素来确定位置的,所以一般写React组件喜欢直接在按钮下面挂一个子div元素,直接控制显隐状态来展示弹窗。那怎么点击弹窗元素以外的区域来关闭弹窗???这是一个比较纠结的问题。
方案一
我们把弹窗区域做大,做成和屏幕宽高一样,由于要点击其他区域触发其他区域的点击事件,把弹窗蒙层做成透明,然后设置蒙层css "pointer-enents:none",内容显示区域设置成点击不穿透,但这样不好捕获点击其他区域的事件,只能被动的由css触发穿透事件。实现应该可以实现,复杂度较大,缺点较多。
方案二
点击更多按钮,弹窗(仅内容区域大小)显示的时候,在body上注册点击事件,弹窗关闭的时候取消注册事件。由于点击会有event.target传过来,我们来区分event.target是否是弹窗区域内的元素,如果是非弹窗区域元素,我们直接关闭弹窗。
效果
组件代码
代码上加了详细的注释,大家可以参考一下。
DropDownMenu.tsx
import React from 'react';
import BaseComponent from '../BaseComponent';
import { isContains, addEventListenerWrap } from './popUtils';
import './index.scss';
import Button from '../Button';
interface Props {
/**
* 按钮
* @memberOf Props
*/
renderBtnView: () => JSX.Element;
/**
*
*
* 弹窗内容
* @memberOf Props
*/
renderPopContentView: () => JSX.Element;
/**
*
* 弹窗根节点范围
* @memberOf Props
*/
getRootDomNode?: () => HTMLElement;
/**
* 样式
* @type {string}
* @memberOf Props
*/
className?: string;
}
interface State {
/**
*
* 是否显示pop
* @type {boolean}
* @memberOf State
*/
showPop: boolean;
}
/**
*
* 更多-下拉菜单弹窗
* @export
* @class TipPop
* @extends {BaseComponent<Props, State>}
*/
export default class DropDownMenu extends BaseComponent<Props, State> {
private domListener = null;
private popupRef = null;
constructor(props) {
super(props);
this.state = {
showPop: false,
};
this.popupRef = React.createRef();
}
componentDidUpdate(_privProps, prevState) {
if (!prevState.showPop && this.state.showPop) {
//弹窗状态发生改变,从隐藏到显示,添加监听器
this.setListener();
} else if (prevState.showPop && !this.state.showPop) {
////弹窗状态发生改变,从隐藏到显示,取消监听器
this.cancelListener();
}
}
/**
*
*
* 设置监听
* @memberOf DropDownMenu
*/
setListener = () => {
//获取根节点
const rootDom = this.getRootDomNode();
//默认取消一次监听
this.cancelListener();
this.domListener = addEventListenerWrap(rootDom, 'click', event => {
const { target } = event;
const root = this.getRootDomNode();
const popupNode = this.getPopupDomNode();
//判断是根节点框中的点击事件,并且不是弹窗区域,为任意点击消失区域。
if (isContains(root, target) && !isContains(popupNode, target)) {
console.log('直接关闭===', target, isContains(popupNode, target));
//直接关闭
this.hidePop();
}
},true);
};
/**
*
*
* 取消监听
* @memberOf DropDownMenu
*/
cancelListener = () => {
if (this.domListener) {
this.domListener?.remove();
this.domListener = null;
}
};
/**
*
* 获取pop弹窗节点
* @returns
*
* @memberOf DropDownMenu
*/
getPopupDomNode() {
return this.popupRef.current || null;
}
/**
*
*
* 获取默认根节点
* @memberOf DropDownMenu
*/
getRootDomNode = (): HTMLElement => {
const { getRootDomNode } = this.props;
if (getRootDomNode) {
return getRootDomNode();
}
return window.document.body;
};
/**
*
*
* 显示弹窗
* @memberOf DropDownMenu
*/
showPop = () => {
const { showPop } = this.state;
console.log('点击===', showPop);
//这里弹窗打开,再次点击按钮,可以关闭弹窗
if (showPop) {
this.setState({
showPop: false,
});
return;
}
this.setState({
showPop: true,
});
};
/**
*
*
* 隐藏弹窗
* @memberOf DropDownMenu
*/
hidePop = () => {
this.setState({
showPop: false,
});
};
render() {
const { className } = this.props;
const { showPop } = this.state;
return (
<div className={`tip-pop ${className}`} ref={this.popupRef}>
<Button className="tip-pop-btn" onClick={this.showPop}>
{this.props.renderBtnView()}
</Button>
{showPop ? (
<div className="tip_pop-content">{this.props.renderPopContentView()}</div>
) : null}
</div>
);
}
}
popUtils.ts
/**
*
* 判断是否包含
* @export
* @param {(Node | null | undefined)} root
* @param {Node} [n]
* @returns
*/
export function isContains(root: Node | null | undefined, n?: Node) {
if (!root) {
return false;
}
return root.contains(n);
}
/**
*
* 添加监听
* @export
* @param {any} target
* @param {any} eventType
* @param {any} cb
* @param {any} [option]
* @returns
*/
export function addEventListenerWrap(target, eventType, cb, option?) {
if (target.addEventListener) {
target.addEventListener(eventType, cb, option);
}
return {
remove: () => {
if (target.removeEventListener) {
target.removeEventListener(eventType, cb,option);
}
},
};
}
调用
renderBtnView = () => (
<div className="more-btn-style">
<div className="btn-dot-1" />
<div className="btn-dot-1" />
<div className="btn-dot-1" />
</div>
);
renderPopContentView = () => (
<>
<Button className="rule-btn">规则</Button>
<Button className="history-btn">记录</Button>
</>
);
render() {
console.log('this.props==', this.props);
return (
<div className="home">
<div className="home-title">测试</div>
<DropDownMenu
className="more-btn"
renderBtnView={this.renderBtnView}
renderPopContentView={this.renderPopContentView}
/>
<Button
className="other-btn"
onClick={() => {
window.alert('触发');
}}
>
打开另外一个弹窗
</Button>
</div>
);
}
后语
本文主要记录日常工作的比较有意思的需求解决方案。
欢迎在评论区讨论,掘金官方将在掘力星计划活动结束后,在评论区抽送100份掘金周边,抽奖详情见活动文章