Если вы, как и я, обожаете яркие сокращения, вы знаете, насколько удовлетворительно нажать несколько клавиш и наблюдать, как волшебство происходит. Будь то привычное Ctrl+C – Ctrl+V, которое разработчики используют для “одолжения кода” 😉 из LLM и кодовых страниц, или персонализированные ярлыки, которые мы настраиваем в наших любимых инструментах, клавиатурные ярлыки экономят время и заставляют нас чувствовать себя компьютерными волшебниками.

Ну, не бойтесь! Я разгадал код создания компонентов, которые запускают и реагируют на клавиатурные ярлыки. В этой статье я научу вас создавать их с помощью React, Tailwind CSS и Framer Motion.

Содержание

Вот все, о чем мы поговорим:

Предварительные требования

  • Основы HTML, CSS и Tailwind CSS

  • Основы JavaScript, React и React Hooks

Что такое компонент прослушивания клавиш быстрых сочетаний (KSL)?

Компонент Слушатель комбинаций клавиатуры (KSLC) — это компонент, который отслеживает определенные комбинации клавиш и выполняет действия в вашем приложении. Он предназначен для того, чтобы ваше приложение реагировало на комбинации клавиш, обеспечивая более плавный и эффективный пользовательский опыт.

Почему это важно?

  • Доступность: Компонент KSL упрощает пользователям, использующим клавиатуру, выполнение действий, делая ваше приложение более инклюзивным и простым в использовании.

  • Быстрота работы: Комбинации клавиш быстры и эффективны, позволяя пользователям выполнять задачи быстрее. Больше не нужно искать мышь — просто нажмите клавишу (или две), и действие выполнится!

  • Повторное использование: Как только вы настроите свой KSL, он сможет обрабатывать разные комбинации клавиш в вашем приложении, что упрощает добавление без переписывания одной и той же логики.

  • Чистый код: Вместо того чтобы разбросать слушатели событий клавиатуры повсюду, компонент KSL поддерживает порядок, централизуя логику. Ваш код остается чистым, организованным и проще в обслуживании.

Как создать компонент KSL

Я подготовил репозиторий на GitHub с начальными файлами, чтобы ускорить процесс. Просто клонируйте этот репозиторий и установите зависимости.

Для этого проекта мы используем главную страницу Tailwind в качестве вдохновения и создаем функциональность KSL. После установки и запуска команды сборки, вот как должна выглядеть ваша страница:

Как создать компонент Reveal

Компонент 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>
  );
}

Итак, что происходит в этом коде?

  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 устанавливает isOpen в true, отображая SearchBox.

Но, как вы видели, это было вызвано действием клика, а не сочетанием клавиш. Давайте сделаем это следующим шагом.

Как вызвать компонент с помощью сочетания клавиш

Чтобы сделать так, чтобы компонент открывался и закрывался с помощью сочетания клавиш, мы используем хук useEffect, чтобы следить за определенными комбинациями клавиш и обновлять состояние компонента соответственно.

Шаг 1: Слушайте события клавиатуры

Добавьте хук useEffect в ваш файл App.tsx, чтобы слушать нажатия клавиш:

  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. Закрытие с помощью клавиши Escape (event.key === "Escape")

    • Когда нажата клавиша Escape, 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. Этот хук использует ссылку на элемент, чтобы знать, когда пользователь щелкает за пределами целевого элемента (поля поиска), что является очень популярным поведением для закрытия модальных окон и KSLC.


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(); // Выполнение функции обратного вызова
  }
});

Наконец, настройте слушатели событий на объекте window для отслеживания событий нажатия клавиш. Эти слушатели будут запускать функцию handleKeyDown каждый раз, когда будет нажата клавиша. Убедитесь, что добавили очистку слушателей событий при размонтировании компонента.

useEffect(() => {
  // Добавить слушатели событий для событий нажатия клавиш
  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), // Закрываем поле поиска
    },
  ]);

Вот ссылки на все ресурсы, которые вам могут понадобиться для этой статьи:

Заключение

Надеюсь, что эта статья покажется вам как удачный ярлык, помогающий вам в создании повторно используемых компонентов сочетаний клавиш. С каждым нажатием клавиши и анимацией, вы теперь можете превратить обычные веб-приложения в удивительные.

Надеюсь, ваши ярлыки помогут вам создавать приложения, которые будут понятны вашим пользователям. В конце концов, лучшие путешествия часто начинаются с правильной комбинации.

Нравятся мои статьи?

Не стесняйтесь подарить мне кофе здесь, чтобы мой мозг продолжал работать и появлялись новые статьи.

Контактная информация

Хотите связаться со мной? Не стесняйтесь написать мне по следующим контактам: