单击按钮不会隐藏React.js Dropdown组件

时间:2019-10-14 08:10:36

标签: reactjs

我在制作可点击的Dropdown组件时遇到了问题。我的任务是单击一个按钮时显示一个菜单,而当用户单击文档中的任意位置或是否单击同一按钮时,则隐藏菜单,所有组件也应该是功能组件。

我正在使用名为classnames的第三方软件包,该软件包有助于有条件地加入CSS类,还使用React ContextAPI将道具传递给Dropdown子组件。

Dropdown组件取决于2个子组件。

DropdownToggle- (呈现可点击的按钮)

DropdownMenu- (使div带有菜单项)

问题:

每当我打开菜单并单击文档菜单中的任何位置时,效果都很好,但是当我打开菜单并想用按钮隐藏时,单击它不起作用。我认为问题出在useEffect组件的Dropdown挂钩内。

Codesandbox

演示:

Dropdown

这里是主要的App组件,呈现所有组件。

App.js

import React, { Component } from "react";
import Dropdown from "./Dropdown";
import DropdownToggle from "./DropdownToggle";
import DropdownMenu from "./DropdownMenu";
import "./dropdown.css";

// App component
class App extends Component {
  state = {
    isOpen: false
  };

  toggle = () => {
    alert("Button is clicked");
    this.setState({
      isOpen: !this.state.isOpen
    });
  };

  render() {
    return (
      <div className="app">
        <Dropdown isOpen={this.state.isOpen} toggle={this.toggle}>
          <DropdownToggle>Dropdown</DropdownToggle>
          <DropdownMenu>
            <div>Item 1</div>
            <div>Item 2</div>
          </DropdownMenu>
        </Dropdown>
      </div>
    );
  }
}

export default App;

主要 src 代码:

DropdownContext.js

import {createContext} from 'react';
// It is used on child components.
export const DropdownContext = createContext({});
// Wrap Dropdown with this Provider.
export const DropdownProvider = DropdownContext.Provider;

Dropdown.js

import React, { useEffect } from "react";
import classNames from "classnames";
import { DropdownProvider } from "./DropdownContext";


/**
 * Returns a new object with the key/value pairs from `obj` that are not in the array `omitKeys`.
 * @param obj
 * @param omitKeys
 */
const omit = (obj, omitKeys) => {
  const result = {};
  // Get object properties as an array
  const propsArray = Object.keys(obj);
  propsArray.forEach(key => {
    // Searches the array for the specified item, if the item is not found it returns -1 then
    // construct a new object and return it.
    if (omitKeys.indexOf(key) === -1) {
      result[key] = obj[key];
    }
  });
  return result;
};

// Dropdown component
const Dropdown = props => {
  // Populate context value based on the props
  const getContextValue = () => {
    return {
      toggle: props.toggle,
      isOpen: props.isOpen
    };
  };

  // toggle function
  const toggle = e => {
    // Execute toggle function which is came from the parent component
    return props.toggle(e);
  };

  // handle click for the document object
  const handleDocumentClick = e => {
    // Execute toggle function of the parent
    toggle(e);
  };

  // Remove event listeners
  const removeEvents = () => {
    ["click", "touchstart"].forEach(event =>
      document.removeEventListener(event, handleDocumentClick, true)
    );
  };

  // Add event listeners
  const addEvents = () => {
    ["click", "touchstart"].forEach(event =>
      document.addEventListener(event, handleDocumentClick, true)
    );
  };

  useEffect(() => {
    const handleProps = () => {
      if (props.isOpen) {
        addEvents();
      } else {
        removeEvents();
      }
    };
    // mount
    handleProps();
    // unmount
    return () => {
      removeEvents();
    };
  }, [props.isOpen]);

  // Condense all other attributes except toggle `prop`.
  const { className, isOpen, ...attrs } = omit(props, ["toggle"]);
  // Conditionally join all classes
  const classes = classNames(className, "dropdown", { show: isOpen });

  return (
    <DropdownProvider value={getContextValue()}>
      <div className={classes} {...attrs} />
    </DropdownProvider>
  );
};

export default Dropdown;

Dropdown组件有一个父组件,即Provider会在Provider值更改子组件时访问这些值。 其次,在DOM上它将呈现一个div标记结构,该标记结构由Dropdown组成。

DropdownToggle.js

import React, {useContext} from 'react';
import classNames from 'classnames';
import {DropdownContext} from './DropdownContext';

// DropdownToggle component
const DropdownToggle = (props) => {

    const {toggle} = useContext(DropdownContext);

    const onClick = (e) => {
        // If props onClick is not undefined
        if (props.onClick) {
            // execute the function
            props.onClick(e);
        }
        toggle(e);
    };

    const {className, ...attrs} = props;

    const classes = classNames(className);


    return (
        // All children would be render inside this. e.g. `svg` & `text`
        <button type="button" className={classes} onClick={onClick} {...attrs}/>
    );
};

export default DropdownToggle;

DropdownMenu.js

import React, { useContext } from "react";
import classNames from "classnames";
import { DropdownContext } from "./DropdownContext";

// DropdownMenu component
const DropdownMenu = props => {
  const { isOpen } = useContext(DropdownContext);

  const { className, ...attrs } = props;
  // add show class if isOpen is true
  const classes = classNames(className, "dropdown-menu", { show: isOpen });

  return (
    // All children would be render inside this `div`
    <div className={classes} {...attrs} />
  );
};

export default DropdownMenu;

2 个答案:

答案 0 :(得分:0)

切换功能链接到文档和按钮本身。因此,当您单击按钮时,它将触发一次,然后事件冒泡到document并再次触发。必须小心将事件侦听器附加到整个document对象。在您的Dropdown.js文件中添加一行以停止事件传播:

// toggle function
const toggle = e => {
 // Execute toggle function which is came from the parent component
 e.stopPropagation(); // this stops it bubbling up to the document and firing again
 return props.toggle(e);
};

答案 1 :(得分:0)

Jayce444的答案是正确的。当您单击按钮时,它将触发一次,然后事件冒泡到文档并再次触发。

我只想为您添加另一个替代解决方案。您可以使用useRef钩子来创建Dropdown节点的引用,并检查当前事件目标是否为button元素。将此代码添加到您的Dropdown.js文件中。

import React, { useRef } from "react";

const Dropdown = props => {
  const containerRef = useRef(null);

  // get reference of the current div
  const getReferenceDomNode = () => {
    return containerRef.current;
  };

  // handle click for the document object
  const handleDocumentClick = e => {
    const container = getReferenceDomNode();
    if (container.contains(e.target) && container !== e.target) {
      return;
    }
    toggle(e);
  };

  //....

  return (
    <DropdownProvider value={getContextValue()}>
      <div className={classes} {...attrs} ref={containerRef} />
    </DropdownProvider>
  );
};
export default Dropdown;