Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”.

This article has participated in the “Digitalstar Project” and won a creative gift package to challenge the creative incentive money.

preface

Hello everyone! I’m front-end nameless

background

In mobile TERMINAL H5 business, we often encounter more buttons in the upper right corner. Click more buttons, and a drop-down menu will appear. When clicking other areas of the menu, not only the popup window will be closed, but also the event of clicking the area will be triggered.

The diagram below:

If you click the red box area, the popover will disappear. If you click on the “Open another popover” button, the click event of this button will be triggered.

The difficulties in

Because the prompt popup is usually positioned relative to the element being clicked, React components like to hang a child div element directly under the button to control the explicit and implicit state of the popup. How to close the popover by clicking outside the popover element? This is a tricky question.

Plan a

We made the popover area as big as the width of the screen. Since we need to click on other areas to trigger click events in other areas, we made the popover mask transparent and set the mask CSS “Pointer-enents: None”. The content display area is set to not be hit through, but this is not good for capturing click events in other areas. The penetration event can only be triggered by CSS passively. The implementation should be implementable, with greater complexity and drawbacks.

Scheme 2

Click more buttons, register the click event on the body when the popup window (content area size only) displays, and unregister the event when the popup closes. Since clicking will send an event.target, we can distinguish whether the event.target is an element in the popover area. If it is not, we can close the popover directly.

The effect

Component code

Code added a detailed note, we can refer to.

DropDownMenu.tsx

import React from 'react';
import BaseComponent from '.. /BaseComponent';
import { isContains, addEventListenerWrap } from './popUtils';
import './index.scss';
import Button from '.. /Button';

interface Props {
  /** * button *@memberOf Props* /
  renderBtnView: () = > JSX.Element;

  /** ** * popover contents *@memberOf Props* /
  renderPopContentView: () = > JSX.Element;

  /** ** Popup root node range *@memberOf Props* /getRootDomNode? :() = > HTMLElement;

  /** * style *@type {string}
   * @memberOf Props* /className? : string; } interface State {/** ** Whether to display pop *@type {boolean}
   * @memberOf State* /
  showPop: boolean;
}

/** ** more - dropdown menu pop-ups *@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) {
      // The popover state changes from hide to show, adding listeners
      this.setListener();
    } else if (prevState.showPop && !this.state.showPop) {
      //// Popover state changes from hide to show, cancel listener
      this.cancelListener(); }}/** ** * Set listening *@memberOf DropDownMenu* /
  setListener = () = > {
    // Get the root node
    const rootDom = this.getRootDomNode();
    // Cancel a listener by default
    this.cancelListener();
    this.domListener = addEventListenerWrap(rootDom, 'click'.event= > {
      const { target } = event;
      const root = this.getRootDomNode();
      const popupNode = this.getPopupDomNode();
      // Determine to be the click event in the root node box, and not the popover area, is the disappear area of any click.
      if(isContains(root, target) && ! isContains(popupNode, target)) {console.log('Directly close ===', target, isContains(popupNode, target));
        // Close it
        this.hidePop(); }}); };/** ** * Cancel listening *@memberOf DropDownMenu* /
  cancelListener = () = > {
    if (this.domListener) {
      this.domListener? .remove();this.domListener = null; }};/** ** get the pop popover node *@returns
   *
   * @memberOf DropDownMenu* /
  getPopupDomNode() {
    return this.popupRef.current || null;
  }

  /** ** * Obtain the default root node *@memberOf DropDownMenu* /
  getRootDomNode = (): HTMLElement= > {
    const { getRootDomNode } = this.props;
    if (getRootDomNode) {
      return getRootDomNode();
    }
    return window.document.body;
  };

  /** ** * displays popup *@memberOf DropDownMenu* /
  showPop = () = > {
    const { showPop } = this.state;
    console.log('click = = =', showPop);
    // Click the button again to close the popover
    if (showPop) {
      this.setState({
        showPop: false});return;
    }

    this.setState({
      showPop: true}); };/** ** * Hide popover *@memberOf DropDownMenu* /
  hidePop = () = > {
    this.setState({
      showPop: false}); };render() {
    const { className } = this.props;
    const { showPop } = this.state;
    return (
      <div className={`tip-popThe ${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>); }}Copy the code

popUtils.ts

/** ** Check whether the command contains *@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);
}

/** ** Add listener *@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); }}}; }Copy the code

call


   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">The rules</Button>
      <Button className="history-btn">record</Button>
    </>
  );

  render() {
    console.log('this.props=='.this.props);
    return (
      <div className="home">
        <div className="home-title">test</div>
        <DropDownMenu
          className="more-btn"
          renderBtnView={this.renderBtnView}
          renderPopContentView={this.renderPopContentView}
        />
        <Button
          className="other-btn"
          onClick={()= >{window.alert(' trigger '); }} > Open another popover</Button>
      </div>
    );
  }
Copy the code

After the language

Your comments are welcome. This article mainly records the daily work of the more interesting demand solutions, we have a good plan to see the comment area, a praise once! Comments are welcome.