阅读 584
H5实现 popWindow 下拉更多效果

H5实现 popWindow 下拉更多效果

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。

前言

Hello 大家好! 我是前端 无名

背景

做移动端H5业务我们经常会遇到右上角有个更多按钮,点击更多按钮,出现下拉菜单,点击菜单其他区域不仅要关闭弹窗而且还要触发点击区域的事件。

如下图:

image.png

点击红框区域,弹窗要消失,如果点击在“打开另外一个弹窗”按钮上,还要触发这个按钮的点击事件。

难点

由于这种提示弹窗一般是相对于点击元素来确定位置的,所以一般写React组件喜欢直接在按钮下面挂一个子div元素,直接控制显隐状态来展示弹窗。那怎么点击弹窗元素以外的区域来关闭弹窗???这是一个比较纠结的问题。

方案一

我们把弹窗区域做大,做成和屏幕宽高一样,由于要点击其他区域触发其他区域的点击事件,把弹窗蒙层做成透明,然后设置蒙层css "pointer-enents:none",内容显示区域设置成点击不穿透,但这样不好捕获点击其他区域的事件,只能被动的由css触发穿透事件。实现应该可以实现,复杂度较大,缺点较多。

方案二

点击更多按钮,弹窗(仅内容区域大小)显示的时候,在body上注册点击事件,弹窗关闭的时候取消注册事件。由于点击会有event.target传过来,我们来区分event.target是否是弹窗区域内的元素,如果是非弹窗区域元素,我们直接关闭弹窗。

效果

QQ录屏20211013111210 00_00_00-00_00_30.gif

组件代码

代码上加了详细的注释,大家可以参考一下。

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份掘金周边,抽奖详情见活动文章

文章分类
前端
文章标签