Как создать повторно используемый компонент прослушивателя сочетаний клавиш в React
Если вы похожи на меня и любите быстрые клавиши, вы знаете, как приятно нажать несколько клавиш и наблюдать, как происходит волшебство. Будь то привычные сочетания клавиш Ctrl+C - Ctrl + V, которые разработчики используют для заимствования кода" из LLMS и code pages, или персонализированные сочетания клавиш, которые мы настраиваем в наших любимых инструментах, сочетания клавиш экономят время и позволяют нам почувствовать себя компьютерными гениями.
Что ж, не бойтесь! Я взломал код для создания компонентов, которые запускаются и реагируют на сочетания клавиш. В этой статье я научу вас, как создавать их с помощью React, Tailwind CSS и Framer Motion.
Таблица содержания
Вот все, о чем мы расскажем:
-
Предпосылки
Что такое компонент прослушивателя сочетаний клавиш (KSL)?
Как создать компонент KSL
Как создать компонент отображения
Как запустить компонент с помощью сочетания клавиш
Как анимировать видимость компонента
Как оптимизировать ваш компонент KSL
Вывод
Предпосылки
-
Основы HTML, CSS и Tailwind CSS
Основы JavaScript, React и перехватчиков React.
Что такое компонент прослушивателя сочетаний клавиш (KSL)?
Компонент прослушивания сочетаний клавиш (KSLC) - это компонент, который прослушивает определенные комбинации клавиш и запускает действия в вашем приложении. Он предназначен для того, чтобы ваше приложение реагировало на сочетания клавиш, обеспечивая более плавный и эффективный пользовательский интерфейс.
Почему это так важно?
-
Специальные возможности: Компонент KSL упрощает пользователям, использующим клавиатуру, выполнение действий, делая ваше приложение более доступным и простым в использовании.
Удобство использования: Сочетания клавиш работают быстро и эффективно, позволяя пользователям выполнять работу за меньшее время. Больше не нужно возиться с мышью - просто нажмите клавишу (или две), и бум, начинается действие!
Возможность повторного использования: Как только вы настроите свой KSL, он сможет обрабатывать различные ярлыки в вашем приложении, что упростит его добавление без необходимости переписывать ту же логику.
Более чистый код: Вместо того, чтобы разбрасывать прослушиватели событий клавиатуры по всему миру, компонент KSL поддерживает порядок, централизуя логику. Ваш код остается чистым, организованным и его проще поддерживать.
Как создать компонент KSL
Я подготовил репозиторий на GitHub со стартовыми файлами, чтобы ускорить процесс. Просто клонируйте это репозиторий и устанавливайте зависимости.
Для этого проекта мы используем домашнюю страницу Tailwind в качестве основы и создаем функциональность KSL. После установки и запуска команды build, вот как должна выглядеть ваша страница.:
Как создать компонент Reveal (Раскрытие)
Компонент "Показать" - это компонент, который мы хотим показать при использовании ярлыка.
Для начала создайте файл с именем 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="фиксированный верхний-0 левый-0 ...">)
Это полноэкранное наложение, при котором фон затемняется.
Функция размытия фона (background-blur-sm) добавляет к фону едва заметное размытие, а функция bg-slate-900/50 создает полупрозрачное темное наложение.
Оболочка окна поиска (<div className="p-[15vh] ...">)
Содержимое размещается по центру с помощью утилит padding и 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 устанавливает для параметра isOpen значение true, отображая поле поиска.
Но, как вы видели, это было вызвано щелчком мыши, а не сочетанием клавиш. Давайте сделаем это дальше.
Как запустить компонент с помощью сочетания клавиш
Чтобы компонент reveal открывался и закрывался с помощью сочетания клавиш, мы будем использовать механизм useEffect для прослушивания определенных комбинаций клавиш и соответствующего обновления состояния компонента.
Шаг 1: Прослушивание событий с клавиатуры
Добавьте перехватчик useEffect в свой файл App.tsx для прослушивания нажатий клавиш:
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.ctrlKey && event.key === Key.K) {
event.preventDefault(); // Prevent default browser behavior
} };
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, []);
Что происходит в этом коде?
-
Настройка эффекта (useEffect)
useEffect гарантирует, что прослушиватель событий для нажатий клавиш добавляется при подключении компонента и очищается при отключении компонента, предотвращая утечки памяти.
Комбинация клавиш (event.ctrlKey && event.key === "k")
Event.ctrlKey проверяет, нажата ли управляющая клавиша.
Event.key === "k" гарантирует, что мы прослушиваем именно клавишу "K". В совокупности это проверяет, нажата ли комбинация Ctrl + K.
Предотвращение поведения по умолчанию (event.preventDefault())
В некоторых браузерах поведение по умолчанию может быть привязано к сочетаниям клавиш, таким как Ctrl + K (например, для фокусировки адресной строки браузера). Вызов preventDefault останавливает это поведение.
Очистка события (return () => ...)
Функция очистки удаляет прослушиватель событий, чтобы предотвратить добавление дублирующихся прослушивателей при повторном отображении компонента.
Шаг 2: Переключите видимость компонентов
Затем обновите функцию handleKeyDown, чтобы переключать видимость окна поиска при нажатии сочетания клавиш:
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
// Listen for Ctrl + K
if (event.ctrlKey && event.key === Key.K) {
event.preventDefault(); // Prevent default browser behavior
setIsOpen((prev) => !prev); // Toggle the search box
} else if (event.key === Key.Escape) {
setIsOpen(false); // Close the search box
}
};
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, []);
Что происходит в этом коде?
-
Переключение состояния (setIsOpen((prev) => !prev))
При нажатии Ctrl + K средство настройки состояния setIsOpen переключает видимость окна поиска.
Аргумент prev представляет предыдущее состояние. Использование параметра !prev изменяет его значение на другое:
значение true (открыто) становится значением false (закрыто).
значение false (закрыто) становится значением true (открыто).
Закрытие с помощью клавиши Escape (event.key === "Escape")
Когда нажата клавиша Escape, setIsOpen(false) явно устанавливает состояние в false, закрывая окно поиска.
Это приводит к следующему:
Как анимировать видимость компонента
На данный момент наш компонент работает, но ему не хватает некоторой изюминки, не так ли? Давайте это изменим.
Шаг 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: Добавьте анимацию в оверлей
Теперь давайте сделаем так, чтобы наложение появлялось и исчезало при помощи движения кадра. Обновите компонент 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>
);
}
Key animation props:
-
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. Это гарантирует, что движение фреймера будет отслеживаться при входе элементов в DOM и выходе из него.
<AnimatePresence>{isOpen && <SearchBox />}</AnimatePresence>
Это позволяет отслеживать движение кадра при входе элемента в DOM и выходе из него. В результате мы получаем следующий результат:
Ах, так гораздо лучше!
Как оптимизировать ваш компонент KSL
Если вы думали, что мы закончили, то не так быстро - нам еще нужно кое-что сделать.
Нам нужно провести оптимизацию с учетом доступности. Мы должны добавить возможность для пользователей закрывать компонент поиска с помощью мыши, поскольку доступность очень важна.
Чтобы сделать это, начните с создания хука useClickOutside. Этот хук использует ссылочный элемент, чтобы узнать, когда пользователь нажимает за пределами целевого элемента (окна поиска), что является очень популярным поведением для закрывающих модальностей и KSLC.
import { useEffect } from "react";
type ClickOutsideHandler = (event: Event) => void;
export const useClickOutside = (
ref: React.RefObject<HTMLElement>,
handler: ClickOutsideHandler
) => {
useEffect(() => {
const listener = (event: Event) => {
// Do nothing if clicking ref's element or descendant elements
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>
);
}
Затем передайте этот ref и функцию, которая будет вызываться при обнаружении щелчка за пределами этого элемента.
useClickOutside(searchboxRef, () => close(false));
Тестирование этого сейчас дает следующий результат:
Мы также можем еще немного оптимизировать код. Как и в случае с функцией специальных возможностей, мы можем сделать прослушиватель для обнаружения ярлыков намного чище и эффективнее, выполнив следующие действия.
Во-первых, создайте файл привязки useKeyBindings для обработки комбинаций нажатий клавиш.
Затем определите хук и интерфейс. Хук будет принимать массив привязок, где каждая привязка состоит из:
-
Массив клавиш, который определяет комбинацию клавиш (например, ["Control", "k"])
Функция обратного вызова, которая вызывается при нажатии соответствующих клавиш.
import { useEffect } from "react";
// Define the structure of a keybinding
interface KeyBinding {
keys: string[]; // Array of keys (e.g., ["Control", "k"])
callback: () => void; // Function to execute when the keys are pressed
}
export const useKeyBindings = (bindings: KeyBinding[]) => {
};
Затем создайте функцию handleKeyDown. Внутри перехватчика определите функцию, которая будет прослушивать события с клавиатуры. Эта функция будет проверять, соответствуют ли нажатые клавиши каким-либо определенным комбинациям клавиш.
Мы переведем клавиши в нижний регистр, чтобы сравнение было нечувствительным к регистру, и отследим, какие клавиши нажаты, проверив наличие ctrlKey, shiftKey, altKey, metaKey и нажатой клавиши (например, "k" для Ctrl + K).
const handleKeyDown = (event: KeyboardEvent) => {
// Track the keys that are pressed
const pressedKeys = new Set<string>();
// Check for modifier keys (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");
// Add the key that was pressed (e.g., "k" for Ctrl + K)
if (event.key) pressedKeys.add(event.key.toLowerCase());
};
Затем мы сравним нажатые клавиши с массивом клавиш из наших привязок, чтобы проверить, совпадают ли они. Если они совпадают, мы вызовем соответствующую функцию обратного вызова. Мы также следим за тем, чтобы количество нажатых клавиш совпадало с количеством клавиш, определенным в привязке.
// Loop through each keybinding
bindings.forEach(({ keys, callback }) => {
// Normalize the keys to lowercase for comparison
const normalizedKeys = keys.map((key) => key.toLowerCase());
// Check if the pressed keys match the keybinding
const isMatch =
pressedKeys.size === normalizedKeys.length &&
normalizedKeys.every((key) => pressedKeys.has(key));
// If the keys match, call the callback
if (isMatch) {
event.preventDefault(); // Prevent default browser behavior
callback(); // Execute the callback function
}
});
Наконец, настройте прослушиватели событий в объекте window для прослушивания событий нажатия клавиш. Эти прослушиватели будут запускать функцию handleKeyDown при каждом нажатии клавиши. Не забудьте добавить функцию очистки прослушивателей событий при отключении компонента.
useEffect(() => {
// Add event listeners for keydown
window.addEventListener("keydown", handleKeyDown);
// Cleanup the event listeners when the component unmounts
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [bindings]);
Теперь полный хук useKeyBindings, собранный вместе, выглядит следующим образом:
import { useEffect } from "react";
interface KeyBinding {
keys: string[]; // A combination of keys to trigger the callback (e.g., ["Control", "k"])
callback: () => void; // The function to execute when the keys are pressed
}
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>();
// Track modifier keys explicitly
if (event.ctrlKey) pressedKeys.add("control");
if (event.shiftKey) pressedKeys.add("shift");
if (event.altKey) pressedKeys.add("alt");
if (event.metaKey) pressedKeys.add("meta");
// Add the actual key pressed
if (event.key) pressedKeys.add(event.key.toLowerCase());
// Match exactly: pressed keys must match the defined keys
const isExactMatch =
pressedKeys.size === normalizedKeys.length &&
normalizedKeys.every((key) => pressedKeys.has(key));
if (isExactMatch) {
event.preventDefault(); // Prevent default behavior
callback(); // Execute the callback
}
});
};
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [bindings]);
}
Вот как вы можете использовать этот хук в своем приложении:
import { useKeyBindings } from "./hooks/useKeyBindings";
export default function App() {
const [isOpen, setIsOpen] = useState<boolean>(false);
useKeyBindings([
{
keys: ["Control", "k"], // Listen for "Ctrl + K"
callback: () => setIsOpen((prev) => !prev), // Toggle the search box
},
{
keys: ["Escape"], // Listen for "Escape"
callback: () => setIsOpen(false), // Close the search box
},
]);
Что дает следующий результат:
При таком подходе вы даже можете добавить несколько ярлыков для повышения видимости компонента поиска.
useKeyBindings([
{
keys: ["Control", "k"], // Listen for "Ctrl + K"
callback: () => setIsOpen((prev) => !prev), // Toggle the search box
},
{
keys: ["Control", "d"], // Listen for "Ctrl + D"
callback: () => setIsOpen((prev) => !prev), // Toggle the search box
},
{
keys: ["Escape"], // Listen for "Escape"
callback: () => setIsOpen(false), // Close the search box
},
]);
Вот ссылки на все ресурсы, которые могут вам понадобиться для этой статьи:
-
Исходные файлы
Окончательный
Вывод
Я надеюсь, что эта статья показалась вам своевременной и помогла вам разобраться в сути создания многоразовых компонентов сочетания клавиш. С помощью каждого нажатия клавиши и анимации теперь вы можете превратить обычные веб-приложения в необычные.
Я надеюсь, что ваши ярлыки помогут вам создавать приложения, которые понравятся вашим пользователям. В конце концов, лучшие путешествия часто начинаются с правильного сочетания клавиш.
Нравятся мои статьи?
Не стесняйтесь угостить меня здесь кофе, чтобы я не скучал и публиковал больше подобных статей.
Контактная информация
Хотите связаться со мной? Не стесняйтесь обращаться ко мне по следующим вопросам:
-
Twitter / X: @jajadavid8
В LinkedIn: Дэвид Джаджа
Электронная почта: Jajadavidjid@gmail.com