如果你和我一樣喜歡快捷鍵,你就知道按幾個鍵看到魔法發生是多麼令人滿足。無論是開發者用來“借用代碼”的熟悉 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] ...">

    • 使用內邊距和 flex 工具使內容居中。

    • 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 事件設置 isOpentrue 時,顯示 SearchBox

但正如你所見,這是由點擊動作觸發的,而不是鍵盤快捷鍵動作。接下來讓我們來做這個。

如何透過鍵盤快捷鍵觸發元件

為了使顯示元件能夠使用鍵盤快捷鍵開啟和關閉,我們將使用 useEffect Hook 來監聽特定的鍵組合並相應地更新元件的狀態。

第 1 步:監聽鍵盤事件

在你的 App.tsx 文件中添加一個 useEffect Hook 以監聽鍵按下事件:

  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 檢查 控制 鍵是否被按下。

    • 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 的 hook。這個 hook 使用一個參考元素來知道用戶何時在目標元素(搜索框)外部點擊,這是一種非常流行的關閉模態和 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]);
};

要使用這個 hook,請傳入負責開啟和關閉搜索元件的函數:

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

然後根據其正確的prop類型在搜索中接收該函數:

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>
  );
}

然後將該ref和當檢測到單擊該元素之外時要調用的函數傳入。

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

現在測試它將得到以下結果:

我們還可以對代碼進行進一步優化。就像我們使用輔助功能時所做的那樣,我們可以通過以下步驟使我們用於檢測快捷方式的監聽器更加清潔和高效。

首先,為處理鍵盤組合按鍵創建一個useKeyBindings hook文件。

然後定義hook和接口。 hook將接受一個綁定數組,其中每個綁定包括:

  • 一個keys數組,指定鍵組合(例如,[“Control”,”k”])

  • 一個回調函數,當按下相應的鍵時調用。

import { useEffect } from "react";

// 定義按鍵綁定的結構
interface KeyBinding {
  keys: string[]; // 鍵組數組(例如,["Control","k"])
  callback: () => void; // 當按下鍵時執行的函數
}

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

};

接下來,創建 handleKeyDown 函數。在這個鉤子內,定義一個將會監聽鍵盤事件的函數。這個函數將檢查按下的鍵是否符合任何定義的鍵組合。

我們將鍵名標準化為小寫,以便進行不區分大小寫的比較,並通過檢查 ctrlKeyshiftKeyaltKeymetaKey 和按下的鍵(例如,對於 Ctrl + K 為 “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");

  // 添加被按下的鍵(例如,對於 Ctrl + K 為 "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(); // 執行回調函數
  }
});

最後,在 window 物件上設置事件監聽器,以監聽 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), // 關閉搜索框
    },
  ]);

以下是您可能需要的所有資源的鏈接:

結論

我希望這篇文章能讓您感受到如同一個恰到好處的快捷方式,讓您深入了解如何構建可重用的鍵盤快捷鍵組件。隨著每一次按鍵和動畫,您現在可以將普通的網絡體驗變成非凡的體驗。

我希望您的快捷鍵能幫助您創建與用戶心靈相通的應用程式。畢竟,最佳的旅程往往始於恰到好處的組合。

喜歡我的文章嗎?

隨時可以在這裡請我喝杯咖啡,讓我的大腦保持運轉,提供更多像這樣的文章。

聯絡資訊

想要聯繫或聯絡我嗎?隨時可以透過以下方式聯繫我: