如何在React中创建一个上下文菜单

605 阅读7分钟

根据维基百科,上下文菜单(也称为右键菜单)是图形用户界面(GUI)中的一个菜单,在用户互动时出现,例如右键鼠标操作。上下文菜单提供了一组有限的选择,这些选择在该菜单所属的操作系统或应用程序的当前状态或上下文中可用。

如果你在访问一个网站时右键点击浏览器,你可以看到你的操作系统的原生上下文菜单。你可以保存、打印、为网页创建二维码,以及更多。你还应该看到不同的选项,这取决于你在页面上点击的位置;如果你突出显示文本,你可以看到复制、粘贴和剪切等选项。

你还可能看到一些定制的上下文菜单,比如在电子邮件或列表应用程序,以及Trello和Notion等协作应用程序上。这些右键菜单让用户在使用应用程序时有更多选择。

在这篇文章中,我们将探讨如何在React中创建右键菜单,激活右键菜单的快捷方式,如何创建一个自定义的上下文菜单钩子,以及如果你不想自己实现的话,一些包。

你可以看到下面的项目演示,并在Github或部署的网站上查看完整的代码

Gif of a context menu demonstration in a React app

创建一个自定义的右键菜单

为了创建一个右键菜单,我们需要使用contextmenu 事件监听器。当用户试图打开一个上下文菜单时,这个事件就会触发。它通常是通过点击鼠标右键,或按下上下文菜单的键盘快捷键来触发的。

为上下文菜单创建一个键盘快捷方式

在Windows上,上下文菜单的键盘快捷键是Shift+F10。在Mac上,你可以使用Ctrl+单击右键+单击;似乎没有一个纯键盘的快捷方式。

要在Mac上为上下文菜单创建一个键盘快捷键,首先进入系统偏好设置辅助功能指针控制备用控制方法→ **✅**启用备用指针动作。要使用该快捷方式,请使用F12。如果你不知道如何看到功能键,因为你的Mac有一个触摸条,请查看这个快速指南

Screenshot of pointer control settings on a mac

建立一个上下文菜单

为了禁用默认的右键菜单,我们需要使用event.preventDefault()

document.addEventListener("contextmenu", (event) => {
        event.preventDefault()
});

接下来,我们需要捕捉点击的x和y坐标,在用户点击页面的地方显示菜单。我们可以从事件对象中获得pageXpageY 属性,并将坐标应用于CSS中的topleft 属性。

const [anchorPoint, setAnchorPoint] = useState({ x: 0, y: 0 });

document.addEventListener("contextmenu", (event) => {
        event.preventDefault()
        setAnchorPoint({ x: event.pageX, y: event.pageY });
});

我们使用useState 钩子来管理锚点,并将在用户右键点击页面时更新它们。

定制上下文菜单

现在我们需要向用户显示右键菜单。该菜单的默认值是false ,所以我们将隐藏它。当用户点击右键时,将setShow 设置为true

const [anchorPoint, setAnchorPoint] = useState({ x: 0, y: 0 });
const [show, setShow] = useState(false);

document.addEventListener("contextmenu", (event) => {
    event.preventDefault()
    setAnchorPoint({ x: event.pageX, y: event.pageY });
    setShow(true);
});

我们还需要使用useCallback 钩子;这将返回一个记忆化版本的回调,只有在其中一个依赖关系发生变化时才会改变。这可以防止不必要的渲染。React将只保留我们函数的一个副本。

为此,我们需要把所有东西都包在一个useCallback 函数中,并在一个数组中传递依赖关系。

const [anchorPoint, setAnchorPoint] = useState({ x: 0, y: 0 });
const [show, setShow] = useState(false);

document.addEventListener(
    "contextmenu",
    useCallback(
      (event) => {
        event.preventDefault();
        setAnchorPoint({ x: event.pageX, y: event.pageY });
        setShow(true);
      },
      [setAnchorPoint, setShow]
    )
  );

利用useEffect

最后要注意的是当我们在DOM上触发事件时, useEffect 钩子。这个钩子用于包括API调用和DOM操作的副作用。当我们监听事件的时候,我们还需要添加一个清理函数。

当我们运行代码时,它将首先清理旧状态,然后运行更新的状态。这将删除不必要的行为,防止内存泄漏问题。

我们可以通过传入一个返回函数来做到这一点。

const handleContextMenu = useCallback(
    (event) => {
      event.preventDefault();
      setAnchorPoint({ x: event.pageX, y: event.pageY });
      setShow(true);
    },
    [setAnchorPoint, setShow]
  );

useEffect(() => {
    document.addEventListener("contextmenu", handleContextMenu);
} return () => {
    document.removeEventListener("contextmenu", handleContextMenu);
});

定位上下文菜单

为了显示菜单,我们需要根据它的x和y坐标来改变顶部和左侧的位置。我们将把它们放在我们的App.js 组件中。

下面是菜单的CSS代码。

.menu {
  font-size: 14px;
  background-color: #fff;
  border-radius: 2px;
  padding: 5px 0 5px 0;
  width: 150px;
  height: auto;
  margin: 0;
/* use absolute positioning  */
  position: absolute;
  list-style: none;
  box-shadow: 0 0 20px 0 #ccc;
  opacity: 1;
  transition: opacity 0.5s linear;
}

为了改变菜单的顶部和左侧位置,我们将根据用户在网页上点击的位置动态地更新其位置。为此,为menu 类添加内联样式,并且只在我们将show 设置为true 时显示菜单;否则不要显示任何东西。

// App.js

function App() {
  const [anchorPoint, setAnchorPoint] = useState({ x: 0, y: 0 });
  const [show, setShow] = useState(false);

  const handleContextMenu = useCallback(
    (event) => {
      event.preventDefault();
      setAnchorPoint({ x: event.pageX, y: event.pageY });
      setShow(true);
    },
    [setAnchorPoint, setShow]
  );

  useEffect(() => {
    document.addEventListener("contextmenu", handleContextMenu);
    return () => {
      document.removeEventListener("contextmenu", handleContextMenu);
    };
  });

  return (
    <div className="app">
      <h1>Right click somewhere on the page..</h1>
      {show ? (
        <ul
          className="menu"
          style={{
            top: anchorPoint.y,
            left: anchorPoint.x
          }}
        >
          <li>Share to..</li>
          <li>Cut</li>
          <li>Copy</li>
          <li>Paste</li>
          <hr className="divider" />
          <li>Refresh</li>
          <li>Exit</li>
        </ul>
      ) : (
        <> </>
      )}
    </div>
  );
}
export default App;

我们还需要在用户点击任何菜单项,或试图点击离开菜单时隐藏菜单。为此,我们将有一个条件检查:如果菜单被显示,将setShow 状态切换到false ,并隐藏菜单,否则不做任何事情。这个函数只有在show 状态通过useCallback 钩子改变时才会更新。

就像我们在contextmenu 事件中看到的那样,我们再次处理DOM,这次是用click 事件。

useEffect 钩子中添加这个,并在return 函数中删除事件监听器。

const handleClick = useCallback(() => (show ? setShow(false) : null), [show]);

useEffect(() => {
    document.addEventListener("click", handleClick);
    return () => {
      document.removeEventListener("click", handleClick);
    };
  });

这就是了!下面是完整的代码。

import "./styles.css";
import { useCallback, useEffect, useState } from "react";

function App() {
  const [anchorPoint, setAnchorPoint] = useState({ x: 0, y: 0 });
  const [show, setShow] = useState(false); // hide menu

  const handleContextMenu = useCallback(
    (event) => {
      event.preventDefault();
      setAnchorPoint({ x: event.pageX, y: event.pageY });
      setShow(true);
    },
    [setAnchorPoint]
  );

  const handleClick = useCallback(() => (show ? setShow(false) : null), [show]);

  useEffect(() => {
    document.addEventListener("click", handleClick);
    document.addEventListener("contextmenu", handleContextMenu);
    return () => {
      document.removeEventListener("click", handleClick);
      document.removeEventListener("contextmenu", handleContextMenu);
    };
  });

  return (
    <div className="app">
      <h1>Right click somewhere on the page..</h1>
      {show ? (
        <ul
          className="menu"
          style={{
            top: anchorPoint.y,
            left: anchorPoint.x
          }}
        >
          <li>Share to..</li>
          <li>Cut</li>
          <li>Copy</li>
          <li>Paste</li>
          <hr className="divider" />
          <li>Refresh</li>
          <li>Exit</li>
        </ul>
      ) : (
        <> </>
      )}
    </div>
  );
}
export default App;

创建一个自定义的上下文菜单钩子

到目前为止,我们已经把所有的代码放在了App.js 。然而,React是建立在组件之上的,这意味着我们可以保持我们的代码更加模块化。

让我们来创建一些组件。对于我们的自定义上下文菜单,我们将创建一个自定义钩子,并在Menu 组件中使用它。我将把这个自定义钩子命名为useContextMenu.js ,并从中返回showanchorPoint

我们的自定义钩子的最重要部分是return 语句。在这里,我们返回任何我们希望另一个组件能够访问的东西。我们可以返回一个数组或一个对象。

如果你返回一个数组,我们可以在文件外为返回的值命名任何我们想要的东西。我们不需要和我们返回的东西保持相同的名字。

// useContextMenu.js
import { useEffect, useCallback, useState } from "react";

const useContextMenu = () => {
  const [anchorPoint, setAnchorPoint] = useState({ x: 0, y: 0 });
  const [show, setShow] = useState(false);

  const handleContextMenu = useCallback(
    (event) => {
      event.preventDefault();
      setAnchorPoint({ x: event.pageX, y: event.pageY });
      setShow(true);
    },
    [setShow, setAnchorPoint]
  );

  const handleClick = useCallback(() => (show ? setShow(false) : null), [show]);

  useEffect(() => {
    document.addEventListener("click", handleClick);
    document.addEventListener("contextmenu", handleContextMenu);
    return () => {
      document.removeEventListener("click", handleClick);
      document.removeEventListener("contextmenu", handleContextMenu);
    };
  });
  return { anchorPoint, show };
};

export default useContextMenu;

我们将在Menu 组件内访问我们的自定义钩子,并在App.js 文件内传递给Menu 组件。

// Menu.js

import useContextMenu from "./useContextMenu";

const Menu = () => {
  const { anchorPoint, show } = useContextMenu();

  if (show) {
    return (
      <ul className="menu" style={{ top: anchorPoint.y, left: anchorPoint.x }}>
        <li>Share to..</li>
        <li>Cut</li>
        <li>Copy</li>
        <li>Paste</li>
        <hr />
        <li>Refresh</li>
        <li>Exit</li>
      </ul>
    );
  }
  return <></>;
};

export default Menu;

现在,我们的App.js 正在渲染一个Menu 组件,它看起来更简单了。

// App.js

import "./styles.css";
import Menu from "./Menu";

function App() {
  return (
    <div className="app">
      <h1>Right click somewhere on the page..</h1>
      <Menu />
    </div>
  );
}
export default App;

在菜单中添加Feather图标

最后,我使用了react-feather 包来为项目添加Feather图标。

你所需要的就是用npm install react-feather 来导入这个包。用import * as Icon from "react-feather"; 在你的项目顶部导入,然后像这样访问图标:
<Icon.Share size={20} />

结论和考虑

如果你不想自己实现自定义上下文菜单,还有其他选择。其中之一是Material UI。Material UI是一个React UI框架,它允许你创建不同类型的菜单以及上下文菜单。

另一个选择是react-menu包。它提供了无限级别的子菜单,支持单选和复选框菜单项,支持上下文菜单,并遵守WAI-ARIA创作实践

如果你正在创建你自己的自定义上下文菜单,一定要考虑到移动交互。如果用户使用的是移动电话,他们可能无法右键点击。这就是为什么你可能需要三思,为什么你真的需要一个自定义上下文菜单。如果用户只想看到默认的菜单,会造成一些不好的体验。

The postHow to create a context menu in Reactappeared first onLogRocket Blog.