如果你和我一样热爱快捷键,你就会知道按下几个键并看着魔法发生是多么令人满意。无论是开发者用来“借用代码”😉 的熟悉 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>
);
}
那么,这段代码里发生了什么呢?
-
主覆盖层(
<div className="fixed top-0 left-0 ...">
)-
这是全屏覆盖层,能够使背景变暗。
-
backdrop-blur-sm
为背景添加了细微的模糊效果,而bg-slate-900/50
则给它一个半透明的深色覆盖层。
-
-
搜索框包装器 (
<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);
};
}, []);
这段代码中发生了什么?
-
效果设置(
useEffect
)useEffect
确保在组件挂载时添加按键事件监听器,并在组件卸载时进行清理,以防止内存泄漏。
-
键盘组合 (
event.ctrlKey && event.key === "k"
)-
event.ctrlKey
检查 Control 键是否被按下。 -
event.key === "k"
确保我们专门监听 “K” 键。结合这两个条件,可以检查是否按下 Ctrl + K 组合键。
-
-
防止默认行为 (
event.preventDefault()
)- 一些浏览器可能会将默认行为与像 Ctrl + K 的组合键关联(例如,聚焦浏览器地址栏)。调用
preventDefault
可以停止这种行为。
- 一些浏览器可能会将默认行为与像 Ctrl + K 的组合键关联(例如,聚焦浏览器地址栏)。调用
-
事件清理(
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);
};
}, []);
这段代码中发生了什么?
-
切换状态 (
setIsOpen((prev) => !prev)
)-
当 Ctrl + K 被按下时,
setIsOpen
状态设置器会切换SearchBox
的可见性。 -
参数
prev
代表上一个状态。使用!prev
反转其值:-
true
(打开) 变为false
(关闭)。 -
false
(关闭) 变为true
(打开)。
-
-
-
使用 Esc 键关闭 (
event.key === "Escape"
)- 当按下 Esc 键时,
setIsOpen(false)
明确将状态设置为false
,关闭SearchBox
。
- 当按下 Esc 键时,
这将导致以下结果:
如何动画化组件的可见性
目前,我们的组件可以正常工作,但缺少一些风格,是不是?让我们来改变这一点。
步骤 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
:指定组件卸载时的动画(淡出)。
步骤 3:为搜索框添加动画
接下来,为搜索框本身添加一些动效。我们会让它在出现时滑入并淡入,在消失时滑出。
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
函数。在钩子内部,定义一个函数来监听键盘事件。该函数将检查按下的键是否与任何定义的键组合匹配。
我们将键规范化为小写,以便比较时不区分大小写,并通过检查 ctrlKey
、shiftKey
、altKey
、metaKey
和按下的键(例如,”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), // 关闭搜索框
},
]);
以下是您可能需要的所有资源的链接:
结论
我希望这篇文章就像一个恰到好处的快捷方式,让您深入了解构建可重用键盘快捷键组件的核心。通过每次按键和动画,您现在可以将普通的网络体验变得非凡。
我希望您的快捷键能帮助您创建与用户产生共鸣的应用程序。毕竟,最好的旅程往往始于恰到好处的组合。
喜欢我的文章吗?
随意在这里给我买杯咖啡,让我的大脑保持活力,创作更多这样的文章。
联系信息
想要联系我吗?随时可以通过以下方式与我取得联系:
-
Twitter / X: @jajadavid8
-
LinkedIn: David Jaja
-
电子邮件: [email protected]