React:使用混合 React 和 DOM 事件时停止单击事件传播

sos*_*ick 6 css reactjs react-redux

我们有菜单。如果菜单是打开的,我们应该可以通过单击任意位置来关闭它:

class Menu extends Component {
  componentWillMount() {
    document.addEventListener("click", this.handleClickOutside);
  }

  componentWillUnmount() {
    document.removeEventListener("click", this.handleClickOutside);
  }

  openModal = () => {
    this.props.showModal();
  };

  handleClickOutside = ({ target }) => {
    const { displayMenu, toggleMenu, displayModal } = this.props;

    if (displayMenu) {
      if (displayModal || this.node.contains(target)) {
        return;
      }
      toggleMenu();
    }
  };
  render() {
    return (
      <section ref={node => (this.node = node)}>
        <p>
          <button onClick={this.openModal}>open modal</button>
        </p>
        <p>
          <button onClick={this.openModal}>open modal</button>
        </p>
        <p>
          <button onClick={this.openModal}>open modal</button>
        </p>
      </section>
    );
  }
}
Run Code Online (Sandbox Code Playgroud)

从菜单中,我们可以通过单击菜单内的按钮打开一个模态。我们可以通过两种方式关闭模态:通过单击模态内的关闭模态按钮,或单击模态外的 bakcdrop/overlay:

class Modal extends Component {
  hideModal = () => {
    this.props.hideModal();
  };

  onOverlayClick = ({ target, currentTarget }) => {
    if (target === currentTarget) {
      this.hideModal();
    }
  };

  render() {
    return (
      <div className="modal-container" onClick={this.onOverlayClick}>
        <div className="modal">
          <button onClick={this.hideModal}>close modal</button>
        </div>
      </div>
    );
  }
}
Run Code Online (Sandbox Code Playgroud)

现在,当菜单和模态打开时,在关闭模态单击或模态叠加单击时我只想关闭模态,菜单应该仍然打开。仅在第二次单击时(模式关闭时)。乍一看,它看起来非常清晰和简单,这种情况应该是造成这种情况的原因:

if (displayModal || this.node.contains(target)) {
  return;
}
Run Code Online (Sandbox Code Playgroud)

如果 displayModal 是true,什么都不应该发生。I ts 不起作用,因为在我的情况下,当您单击关闭模式按钮或覆盖时,hideModal将比 完成得更快toggleMenu,而当我们调用handleClickOutsidedisplayModal 时,将已经有false.

开始时带有打开菜单和模态的完整测试用例:

https://codesandbox.io/s/reactredux-rkso6

for*_*d04 8

这会有点长,因为我最近调查了类似的问题。如果您不想阅读所有内容,只需查看解决方案即可。

解决方案

我想到了两个解决方案 - 第一个是简单的修复,第二个更干净,但需要额外的单击处理程序组件。

1.) 轻松修复

在 中Modal.js onOverlayClickstopImmediatePropagation像这样添加:

  onOverlayClick = e => {
    // this is to stop click propagation in the react event system
    e.stopPropagation();
    // this is to stop click propagation to the native document click
    // listener in Menu.js
    e.nativeEvent.stopImmediatePropagation();
    if (e.target === e.currentTarget) {
      this.hideModal();
    }
  };
Run Code Online (Sandbox Code Playgroud)

在 上document,注册了两个点击侦听器:a) 第一个是 React 的顶级侦听器 b) 中的点击侦听器Menu.js。这样e.nativeEvent你就得到了 React 包装的原生 DOM 事件。stopImmediatePropagation当您只想关闭模式时,将取消第二个侦听器 - 并阻止关闭菜单。您可以在解释下阅读更多内容。

代码沙箱

2.) 干净的

有了这个解决方案,您只需使用event.stopPropagation. 所有事件处理(包括外部点击处理程序)都是由 React 完成的,因此您不必再使用document.addEventListener("click",...)。下面click-handler.js只是一些代理,它捕获顶层的所有点击事件,并将它们在 React 事件系统中转发到您注册的组件。

创造click-handler.jsx

import React from "react";

export const clickListenerApi = { addClickListener, removeClickListener };

export const ClickHandler = ({ children }) => {
  return (
    <div
      // span click handler over the whole viewport to catch all clicks
      style={{ minHeight: "100vh" }}
      onClick={e => {
        clickListeners.forEach(cb => cb(e));
      }}
    >
      {children}
    </div>
  );
};

// state of registered click listeners
let clickListeners = [];

function addClickListener(cb) {
  clickListeners.push(cb);
}

function removeClickListener(cb) {
  clickListeners = clickListeners.filter(l => l !== cb);
}
Run Code Online (Sandbox Code Playgroud)

菜单.js:

class Menu extends Component {

  componentDidMount() {
    clickListenerApi.addClickListener(this.handleClickOutside);
  }

  componentWillUnmount() {
    clickListenerApi.removeClickListener(this.handleClickOutside);
  }

  openModal = e => {
    // This click shall not close the menu,
    // so don't propagate the event to our clickListener API.
    e.stopPropagation();
    const { showModal } = this.props;
    showModal();
  };

  render() {... }
}
Run Code Online (Sandbox Code Playgroud)

索引.js:

const App = () => (
  <Provider store={store}>
    <ClickHandler>
      <Page />
    </ClickHandler>
  </Provider>
);
Run Code Online (Sandbox Code Playgroud)

代码沙箱

解释:

当您同时打开模式对话框和菜单并在模式外部单击一次时,使用当前代码,行为是正确的 - 两个元素都关闭。这是因为 DOM 中document已经收到了单击事件并准备好调用handleClickOutside中的单击处理程序Menu。所以你没有机会再通过回调e.stopPropagation()来 取消它onOverlayClickModal

为了理解两个点击事件触发的顺序,我们必须理解 React 有自己的合成事件处理系统(1、2 。这里的要点是 React 使用顶级事件委托并在 DOM 中为所有事件类型添加一个侦听器。document

假设您<button id="foo" onClick={...}>Click it</button>在 DOM 中的某个位置有一个按钮。当您单击该按钮时,它会在浏览器中触发常规单击事件,该事件会document不断向上冒泡,直到到达 DOM 根。React 使用其单个侦听器捕获此点击事件document,然后再次在内部遍历其虚拟 DOM(类似于本机 DOM 的捕获和冒泡阶段)并收集您onClick={...}在组件中设置的所有相关点击回调。onClick因此稍后将找到并调用您的按钮。

这是有趣的部分:当 React 处理点击事件(现在是合成的 React 事件)时,原生点击事件已经在 DOM 中经历了完整的捕获/冒泡周期,并且在原生 DOM 中不存在不再了!这就是为什么在组件的 JSX 中混合使用本机单击处理程序 ( document.addEventListener) 和 ReactonEvent属性有时会很难处理且难以预测。React 事件处理程序应该始终是首选。

阅读链接:

希望能帮助到你。