如果你和我一样热爱快捷键,你就会知道按下几个键并看着魔法发生是多么令人满意。无论是开发者用来“借用代码”😉 的熟悉 Ctrl+C 和 Ctrl+V,还是我们在最喜欢的工具中设置的个性化快捷键,键盘快捷键都能节省时间,让我们感觉像个计算机高手。

别担心!我已经破解了构建触发和响应键盘快捷键的组件的代码。在本文中,我将教你如何使用 React、Tailwind CSS 和 Framer Motion 创建它们。

目录

下面是我们将要覆盖的内容:

前提条件

  • HTML、CSS 和 Tailwind CSS 的基础知识

  • JavaScript、React 和 React Hooks 的基础知识。

什么是键盘快捷键监听器(KSL)组件?

一个键盘快捷键监听器组件(KSLC)是一个监听特定键组合并触发应用程序中动作的组件。它旨在使您的应用程序能够响应键盘快捷键,从而提供更流畅、更高效的用户体验。

为什么这很重要?

  • 无障碍性:KSL组件让使用键盘的人能够轻松触发动作,使您的应用程序更加包容且易于使用。

  • 更迅速的体验:快捷键快速且高效,允许用户在更短的时间内完成任务。无需再笨拙地寻找鼠标——只需按下一个(或两个)键,动作就会发生!

  • 可重用性:一旦您设置了KSL,它可以处理应用程序中的不同快捷键,使添加变得简单,而无需重写相同的逻辑。

  • 更干净的代码:与其到处散布键盘事件监听器,不如让 KSL 组件通过集中逻辑保持整洁。您的代码保持干净、有序,更易于维护。

如何构建 KSL 组件

我准备了一个 GitHub 仓库,里面有 起始文件 来加快进程。只需克隆此仓库并安装依赖项。

在这个项目中,我们将使用 Tailwind 的主页作为灵感,并创建 KSL 功能。安装并运行构建命令后,您的页面应该如下所示:

如何创建揭示组件

揭示组件是我们在使用快捷键时想要显示的组件。

首先,创建一个名为 search-box.tsx 的文件,并粘贴以下代码:

export default function SearchBox() {
  return (
    <div className="fixed top-0 left-0 w-full h-full backdrop-blur-sm bg-slate-900/50 ">
      {" "}
      <div className=" p-[15vh] text-[#939AA7] h-full">
        <div className="max-w-xl mx-auto divide-y divide-[#939AA7] bg-[#1e293b] rounded-md">
          <div className="relative flex justify-between px-4 py-2 text-sm ">
            <div className="flex items-center w-full gap-2 text-white">
              <BiSearch size={20} />
              <input
                type="text"
                className="w-full h-full p-2 bg-transparent focus-within:outline-none"
                placeholder="Search Documentation"
              />
            </div>
            <div className="absolute -translate-y-1/2 right-4 top-1/2 ">
              <kbd className="p-1 text-xs rounded-[4px] bg-[#475569] font-sans font-semibold text-slate-400">
                <abbr title="Escape" className="no-underline ">
                  Esc{" "}
                </abbr>{" "}
              </kbd>
            </div>
          </div>
          <div className="flex items-center justify-center p-10 text-center ">
            <h2 className="text-xl">
              How many licks does it take to get to the center of a Tootsie pop?
            </h2>
          </div>
        </div>
      </div>
    </div>
  );
}

那么,这段代码里发生了什么呢?

  1. 主覆盖层(<div className="fixed top-0 left-0 ...">

    • 这是全屏覆盖层,能够使背景变暗。

    • backdrop-blur-sm 为背景添加了细微的模糊效果,而 bg-slate-900/50 则给它一个半透明的深色覆盖层。

  2. 搜索框包装器 (<div className="p-[15vh] ...">)

    • 内容使用内边距和弹性布局工具居中。

    • max-w-xl 确保搜索框保持在合理的宽度内,以便于阅读。

然后在你的 App.tsx 中,创建一个状态动态显示该组件:

const [isOpen, setIsOpen] = useState<boolean>(false);
  • useState:这个钩子将 isOpen 初始化为 false,意味着搜索框默认是隐藏的。

  • 当将isOpen设置为true时,SearchBox组件将在屏幕上呈现。

并呈现搜索组件:

  {isOpen && <SearchBox />}

要显示搜索组件,请向输入按钮添加切换功能:

<button
  type="button"
  className="items-center hidden h-12 px-4 space-x-3 text-left rounded-lg shadow-sm sm:flex w-72 ring-slate-900/10 focus:outline-none hover:ring-2 hover:ring-sky-500 focus:ring-2 focus:ring-sky-500 bg-slate-800 ring-0 text-slate-300 highlight-white/5 hover:bg-slate-700"
  onClick={() => setIsOpen(true)}>
  <BiSearch size={20} />
  <span className="flex-auto">Quick search...</span>
   <kbd className="font-sans font-semibold text-slate-500">
   <abbr title="Control" className="no-underline text-slate-500">
    Ctrl{" "}
    </abbr>{" "}
    K
   </kbd>
</button>

onClick事件将isOpen设置为true,显示SearchBox

但正如您所见,这是由点击操作触发的,而不是键盘快捷键操作。让我们接着做。

如何通过键盘快捷键触发组件

为了使揭示组件可以使用键盘快捷键打开和关闭,我们将使用useEffect钩子来监听特定按键组合,并相应地更新组件的状态。

第1步:监听键盘事件

在您的App.tsx文件中添加一个useEffect钩子来监听按键:

  useEffect(() => {
    const handleKeyDown = (event: KeyboardEvent) => {
      if (event.ctrlKey && event.key === Key.K) {
        event.preventDefault(); // 阻止默认浏览器行为

      }    };

    window.addEventListener("keydown", handleKeyDown);
    return () => {
      window.removeEventListener("keydown", handleKeyDown);
    };
  }, []);

这段代码中发生了什么?

  1. 效果设置(useEffect

    • useEffect确保在组件挂载时添加按键事件监听器,并在组件卸载时进行清理,以防止内存泄漏。
  2. 键盘组合 (event.ctrlKey && event.key === "k")

    • event.ctrlKey 检查 Control 键是否被按下。

    • event.key === "k" 确保我们专门监听 “K” 键。结合这两个条件,可以检查是否按下 Ctrl + K 组合键。

  3. 防止默认行为 (event.preventDefault())

    • 一些浏览器可能会将默认行为与像 Ctrl + K 的组合键关联(例如,聚焦浏览器地址栏)。调用 preventDefault 可以停止这种行为。
  4. 事件清理(return () => ...

    • 清理函数会移除事件监听器,以防止在组件重新渲染时添加重复的监听器。

步骤2:切换组件可见性

然后,更新handleKeyDown函数以在按下快捷键时切换SearchBox的可见性:

useEffect(() => {
    const handleKeyDown = (event: KeyboardEvent) => {
      // 监听 Ctrl + K
      if (event.ctrlKey && event.key === Key.K) {
        event.preventDefault(); // 阻止默认浏览器行为
        setIsOpen((prev) => !prev); // 切换搜索框
      } else if (event.key === Key.Escape) {
        setIsOpen(false); // 关闭搜索框
      }
    };

    window.addEventListener("keydown", handleKeyDown);
    return () => {
      window.removeEventListener("keydown", handleKeyDown);
    };
  }, []);

这段代码中发生了什么?

  1. 切换状态 (setIsOpen((prev) => !prev))

    • Ctrl + K 被按下时,setIsOpen 状态设置器会切换 SearchBox 的可见性。

    • 参数 prev 代表上一个状态。使用 !prev 反转其值:

      • true (打开) 变为 false (关闭)。

      • false (关闭) 变为 true (打开)。

  2. 使用 Esc 键关闭 (event.key === "Escape")

    • 当按下 Esc 键时,setIsOpen(false) 明确将状态设置为 false,关闭 SearchBox

这将导致以下结果:

如何动画化组件的可见性

目前,我们的组件可以正常工作,但缺少一些风格,是不是?让我们来改变这一点。

步骤 1:创建覆盖组件

我们将首先创建一个 覆盖组件,它作为搜索框的黑暗模糊背景。以下是基础版本:

import { ReactNode } from "react";

export default function OverlayWrapper({ children }: { children: ReactNode }) {
  return (
    <div
      className="fixed top-0 left-0 w-full h-full backdrop-blur-sm bg-slate-900/50 ">
      {children}
    </div>
  );
}

步骤 2:为覆盖添加动画

现在,让我们使用 Framer Motion 让覆盖淡入淡出。像这样更新 OverlayWrapper 组件:

import { motion } from "framer-motion";
import { ReactNode } from "react";

export default function OverlayWrapper({ children }: { children: ReactNode }) {
  return (
    <motion.div
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      exit={{ opacity: 0 }}
      className="fixed top-0 left-0 w-full h-full backdrop-blur-sm bg-slate-900/50 ">
      {children}
    </motion.div>
  );
}
关键动画属性:
  • initial:当组件挂载时,设置起始状态(完全透明)。

  • animate:定义要动画化到的状态(完全不透明)。

  • exit:指定组件卸载时的动画(淡出)。

接下来,为搜索框本身添加一些动效。我们会让它在出现时滑入并淡入,在消失时滑出。

import { motion } from "framer-motion";
import { BiSearch } from "react-icons/bi";
import OverlayWrapper from "./overlay";

export default function SearchBox() {
  return (
    <OverlayWrapper>
      <motion.div
        initial={{ y: "-10%", opacity: 0 }}
        animate={{ y: "0%", opacity: 1 }}
        exit={{ y: "-5%", opacity: 0 }}
        className=" p-[15vh] text-[#939AA7] h-full">
        <div
          className="max-w-xl mx-auto divide-y divide-[#939AA7] bg-[#1e293b] rounded-md"
        >
          <div className="relative flex justify-between px-4 py-2 text-sm ">
            <div className="flex items-center w-full gap-2 text-white">
              <BiSearch size={20} />
              <input
                type="text"
                className="w-full h-full p-2 bg-transparent focus-within:outline-none"
                placeholder="Search Documentation"
              />
            </div>
            <div className="absolute -translate-y-1/2 right-4 top-1/2 ">
              <kbd className="p-1 text-xs rounded-[4px] bg-[#475569] font-sans font-semibold text-slate-400">
                <abbr title="Escape" className="no-underline ">
                  Esc{" "}
                </abbr>{" "}
              </kbd>
            </div>
          </div>
          <div className="flex items-center justify-center p-10 text-center ">
            <h2 className="text-xl">
              How many licks does it take to get to the center of a Tootsie pop?
            </h2>
          </div>
        </div>
      </motion.div>
    </OverlayWrapper>
  );
}

步骤 4:使用 AnimatePresence 启用动画跟踪

最后,将你的条件渲染逻辑包装在 AnimatePresence 组件中,Framer Motion 提供了这个组件。这确保了 Framer Motion 跟踪元素何时进入和离开 DOM。

<AnimatePresence>{isOpen && <SearchBox />}</AnimatePresence>

这使 Framer Motion 能够跟踪元素何时进入和离开 DOM。这样,我们得到以下结果:

啊,好多了!

如何优化你的 KSL 组件

如果你认为我们完成了,那可不行……我们还有一些事情要做。

我们需要优化可访问性。我们应该为用户提供一种使用鼠标关闭搜索组件的方法,因为可访问性非常重要。

为此,首先创建一个名为 useClickOutside 的钩子。这个钩子使用一个引用元素来判断用户何时点击目标元素(搜索框)之外的地方,这是一种关闭模态框和 KSL 组件非常流行的行为。


import { useEffect } from "react";

type ClickOutsideHandler = (event: Event) => void;

export const useClickOutside = (
  ref: React.RefObject<HTMLElement>,
  handler: ClickOutsideHandler
) => {
  useEffect(() => {
    const listener = (event: Event) => {
      // 如果点击的是引用元素或其子元素,则不做任何操作
      if (!ref.current || ref.current.contains(event.target as Node)) return;

      handler(event);
    };

    document.addEventListener("mousedown", listener);
    document.addEventListener("touchstart", listener);

    return () => {
      document.removeEventListener("mousedown", listener);
      document.removeEventListener("touchstart", listener);
    };
  }, [ref, handler]);
};

要使用这个钩子,传入负责打开和关闭搜索组件的函数:

<AnimatePresence> {isOpen && <SearchBox close={setIsOpen} />} </AnimatePresence>

然后使用适当的属性类型在搜索中接收该函数:

export default function SearchBox({
  close,
}: {
  close: React.Dispatch<React.SetStateAction<boolean>>;
}) {

之后,创建一个引用(ref)以跟踪要标记的项目并标记该元素:

import { motion } from "framer-motion";
import { useRef } from "react";
import { BiSearch } from "react-icons/bi";
import { useClickOutside } from "../hooks/useClickOutside";
import OverlayWrapper from "./overlay";

export default function SearchBox({
  close,
}: {
  close: React.Dispatch<React.SetStateAction<boolean>>;
}) {
  const searchboxRef = useRef<HTMLDivElement>(null);
  return (
    <OverlayWrapper>
      <motion.div
        initial={{ y: "-10%", opacity: 0 }}
        animate={{ y: "0%", opacity: 1 }}
        exit={{ y: "-5%", opacity: 0 }}
        className=" p-[15vh] text-[#939AA7] h-full">
        <div
          className="max-w-xl mx-auto divide-y divide-[#939AA7] bg-[#1e293b] rounded-md"
          ref={searchboxRef}>
          <div className="relative flex justify-between px-4 py-2 text-sm ">
            <div className="flex items-center w-full gap-2 text-white">
              <BiSearch size={20} />
              <input
                type="text"
                className="w-full h-full p-2 bg-transparent focus-within:outline-none"
                placeholder="Search Documentation"
              />
            </div>
            <div className="absolute -translate-y-1/2 right-4 top-1/2 ">
              <kbd className="p-1 text-xs rounded-[4px] bg-[#475569] font-sans font-semibold text-slate-400">
                <abbr title="Escape" className="no-underline ">
                  Esc{" "}
                </abbr>{" "}
              </kbd>
            </div>
          </div>
          <div className="flex items-center justify-center p-10 text-center ">
            <h2 className="text-xl">
              How many licks does it take to get to the center of a Tootsie pop?
            </h2>
          </div>
        </div>
      </motion.div>
    </OverlayWrapper>
  );
}

然后传入该引用和当检测到点击元素外部时要调用的函数。

useClickOutside(searchboxRef, () => close(false));

现在测试它会得到以下结果:

我们还可以进一步优化代码。就像我们为可访问性功能所做的那样,我们可以通过以下步骤使检测快捷键的监听器更清晰和高效。

首先,创建一个用于处理按键组合的useKeyBindings钩子文件。

然后定义钩子和接口。该钩子将接受一个绑定数组,其中每个绑定包括:

  • 一个keys数组,指定键组合(例如,[“Control”, “k”])

  • 一个回调函数,当按下相应的键时调用。

import { useEffect } from "react";

// 定义按键绑定的结构
interface KeyBinding {
  keys: string[]; // 按键数组(例如,["Control", "k"])
  callback: () => void; // 按下按键时执行的函数
}

export const useKeyBindings = (bindings: KeyBinding[]) => {

};

接下来,创建 handleKeyDown 函数。在钩子内部,定义一个函数来监听键盘事件。该函数将检查按下的键是否与任何定义的键组合匹配。

我们将键规范化为小写,以便比较时不区分大小写,并通过检查 ctrlKeyshiftKeyaltKeymetaKey 和按下的键(例如,”k” 表示 Ctrl + K)来跟踪按下的键。

const handleKeyDown = (event: KeyboardEvent) => {
  // 跟踪按下的键
  const pressedKeys = new Set<string>();

  // 检查修饰键(Ctrl、Shift、Alt、Meta)
  if (event.ctrlKey) pressedKeys.add("control");
  if (event.shiftKey) pressedKeys.add("shift");
  if (event.altKey) pressedKeys.add("alt");
  if (event.metaKey) pressedKeys.add("meta");

  // 添加按下的键(例如,"k" 表示 Ctrl + K)
  if (event.key) pressedKeys.add(event.key.toLowerCase());
};

接下来,我们将按下的键与绑定中的键数组进行比较,以检查是否匹配。如果匹配,我们将调用相关的回调函数。我们还确保按下的键数量与绑定中定义的键数量相匹配。

// 遍历每个键绑定
bindings.forEach(({ keys, callback }) => {
  // 将键规范化为小写以进行比较
  const normalizedKeys = keys.map((key) => key.toLowerCase());

  // 检查按下的键是否与键绑定匹配
  const isMatch =
    pressedKeys.size === normalizedKeys.length &&
    normalizedKeys.every((key) => pressedKeys.has(key));

  // 如果键匹配,调用回调
  if (isMatch) {
    event.preventDefault(); // 阻止默认的浏览器行为
    callback(); // 执行回调函数
  }
});

最后,在窗口对象上设置事件监听器以监听 keydown 事件。这些监听器将在按下键时触发 handleKeyDown 函数。确保在组件卸载时清理事件监听器。

useEffect(() => {
  // 添加 keydown 事件监听器
  window.addEventListener("keydown", handleKeyDown);

  // 在组件卸载时清理事件监听器
  return () => {
    window.removeEventListener("keydown", handleKeyDown);
  };
}, [bindings]);

完整的 useKeyBindings 钩子现在组合起来看起来是这样的:

import { useEffect } from "react";

interface KeyBinding {
  keys: string[]; // 一组触发回调的组合键(例如,["Control", "k"])
  callback: () => void; // 当按下这些键时执行的函数
}

export function useKeyBindings(bindings: KeyBinding[]) {
  useEffect(() => {
    const handleKeyDown = (event: KeyboardEvent) => {
      bindings.forEach(({ keys, callback }) => {
        const normalizedKeys = keys.map((key) => key.toLowerCase());
        const pressedKeys = new Set<string>();

        // 明确跟踪修饰键
        if (event.ctrlKey) pressedKeys.add("control");
        if (event.shiftKey) pressedKeys.add("shift");
        if (event.altKey) pressedKeys.add("alt");
        if (event.metaKey) pressedKeys.add("meta");

        // 添加实际按下的键
        if (event.key) pressedKeys.add(event.key.toLowerCase());

        // 精确匹配:按下的键必须与定义的键匹配
        const isExactMatch =
          pressedKeys.size === normalizedKeys.length &&
          normalizedKeys.every((key) => pressedKeys.has(key));

        if (isExactMatch) {
          event.preventDefault(); // 阻止默认行为
          callback(); // 执行回调
        }
      });
    };

    window.addEventListener("keydown", handleKeyDown);
    return () => {
      window.removeEventListener("keydown", handleKeyDown);
    };
  }, [bindings]);
}

下面是如何在你的 App 中使用这个钩子:

import { useKeyBindings } from "./hooks/useKeyBindings";

export default function App() {
  const [isOpen, setIsOpen] = useState<boolean>(false);

  useKeyBindings([
    {
      keys: ["Control", "k"], // 监听 "Ctrl + K"
      callback: () => setIsOpen((prev) => !prev), // 切换搜索框
    },
    {
      keys: ["Escape"], // 监听 "Escape"
      callback: () => setIsOpen(false), // 关闭搜索框
    },
  ]);

这将产生以下结果:

通过这种方法,您甚至可以添加多个快捷键来触发搜索组件的可见性。

useKeyBindings([
    {
      keys: ["Control", "k"], // 监听 "Ctrl + K"
      callback: () => setIsOpen((prev) => !prev), // 切换搜索框
    },
    {
      keys: ["Control", "d"], // 监听 "Ctrl + D"
      callback: () => setIsOpen((prev) => !prev), // 切换搜索框
    },
    {
      keys: ["Escape"], // 监听 "Escape"
      callback: () => setIsOpen(false), // 关闭搜索框
    },
  ]);

以下是您可能需要的所有资源的链接:

结论

我希望这篇文章就像一个恰到好处的快捷方式,让您深入了解构建可重用键盘快捷键组件的核心。通过每次按键和动画,您现在可以将普通的网络体验变得非凡。

我希望您的快捷键能帮助您创建与用户产生共鸣的应用程序。毕竟,最好的旅程往往始于恰到好处的组合。

喜欢我的文章吗?

随意在这里给我买杯咖啡,让我的大脑保持活力,创作更多这样的文章。

联系信息

想要联系我吗?随时可以通过以下方式与我取得联系: