Si vous êtes comme moi et que vous adorez les raccourcis, vous savez à quel point il est satisfaisant d’appuyer sur quelques touches et de voir la magie opérer. Que ce soit le célèbre Ctrl+C – Ctrl+V que les développeurs utilisent pour « emprunter du code » 😉 des LLM et des pages de code, ou les raccourcis personnalisés que nous mettons en place dans nos outils préférés, les raccourcis clavier font gagner du temps et nous font sentir comme des experts en informatique.

Eh bien, n’ayez crainte ! J’ai déchiffré le code pour créer des composants qui déclenchent et répondent aux raccourcis clavier. Dans cet article, je vais vous apprendre à les créer avec React, Tailwind CSS et Framer Motion.

Table des matières

Voici tout ce que nous allons aborder :

Prérequis

  • Fondamentaux de HTML, CSS et Tailwind CSS

  • Fondamentaux de JavaScript, React et React Hooks.

Qu’est-ce qu’un composant d’écouteur de raccourci clavier (KSL) ?

Un composant d’écouteur de raccourcis clavier (KSLC) est un composant qui écoute des combinaisons de touches spécifiques et déclenche des actions dans votre application. Il est conçu pour faire en sorte que votre application réagisse aux raccourcis clavier, permettant ainsi une expérience utilisateur plus fluide et plus efficace.

Pourquoi est-ce important ?

  • Accessibilité : Le composant KSL permet aux personnes utilisant un clavier de déclencher des actions facilement, rendant votre application plus inclusive et facile à utiliser.

  • Expérience plus réactive : Les raccourcis sont rapides et efficaces, permettant aux utilisateurs d’accomplir des tâches en moins de temps. Plus besoin de chercher la souris—il suffit d’appuyer sur une touche (ou deux) et hop, l’action se déclenche !

  • Réutilisabilité : Une fois que vous avez configuré votre KSL, il peut gérer différents raccourcis dans votre application, ce qui facilite leur ajout sans avoir à réécrire la même logique.

  • Code plus propre: Au lieu de disperser les écouteurs d’événements clavier partout, le composant KSL garde les choses en ordre en centralisant la logique. Votre code reste propre, organisé et plus facile à maintenir.

Comment construire le composant KSL

J’ai préparé un dépôt GitHub avec les fichiers de démarrage pour accélérer les choses. Il suffit de cloner ce dépôt et d’installer les dépendances.

Pour ce projet, nous utilisons la page d’accueil de Tailwind comme source d’inspiration et nous créons la fonctionnalité KSL. Après avoir installé et exécuté la commande de construction, voici à quoi devrait ressembler votre page :

Comment créer le composant Révéler

Le composant révéler est le composant que nous voulons afficher lorsque nous utilisons le raccourci.

Pour commencer, créez un fichier appelé search-box.tsx et collez-y ce code :

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

Alors, que se passe-t-il dans ce code ?

  1. Superposition principale (<div className="fixed top-0 left-0 ...">)

    • Il s’agit de la superposition plein écran qui assombrit l’arrière-plan.

    • La classe backdrop-blur-sm ajoute un léger flou à l’arrière-plan, et bg-slate-900/50 lui donne une superposition sombre semi-transparente.

  2. Wrapper de la zone de recherche (<div className="p-[15vh] ...">)

    • Le contenu est centré en utilisant des utilitaires de rembourrage et de flexibilité.

    • Le max-w-xl s’assure que la zone de recherche reste dans une largeur raisonnable pour la lisibilité.

Ensuite dans votre App.tsx, créez un état qui affiche dynamiquement ce composant:

const [isOpen, setIsOpen] = useState<boolean>(false);
  • useState: Ce crochet initialise isOpen à false, ce qui signifie que la zone de recherche est initialement masquée.

  • Lorsque isOpen est défini sur true, le composant SearchBox sera rendu à l’écran.

Et rendre le composant de recherche:

  {isOpen && <SearchBox />}

Pour afficher le composant de recherche, ajoutez une fonction de basculement au bouton d’entrée:

<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>

L’événement onClick définit isOpen sur true, affichant le SearchBox.

Mais comme vous l’avez vu, cela a été déclenché par un clic, et non par une action de raccourci clavier. Faisons cela ensuite.

Comment Déclencher le Composant via un Raccourci Clavier

Pour faire en sorte que le composant de révélation s’ouvre et se ferme en utilisant un raccourci clavier, nous utiliserons un hook useEffect pour écouter des combinaisons de touches spécifiques et mettre à jour l’état du composant en conséquence.

Étape 1: Écouter les Événements Clavier

Ajoutez un hook useEffect dans votre fichier App.tsx pour écouter les pressions de touches:

  useEffect(() => {
    const handleKeyDown = (event: KeyboardEvent) => {
      if (event.ctrlKey && event.key === Key.K) {
        event.preventDefault(); // Empêcher le comportement par défaut du navigateur

      }    };

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

Que se passe-t-il dans ce code?

  1. Mise en Place de l’Effet (useEffect)

    • useEffect garantit que l’écouteur d’événements pour les pressions de touches est ajouté lorsque le composant est monté et nettoyé lorsque le composant est démonté, évitant les fuites de mémoire.
  2. Combinaison de touches (event.ctrlKey && event.key === "k")

    • Le event.ctrlKey vérifie si la touche Control est enfoncée.

    • Le event.key === "k" assure que nous écoutons spécifiquement la touche « K ». Ensemble, cela vérifie si la combinaison Ctrl + K est pressée.

  3. Empêcher le comportement par défaut (event.preventDefault())

    • Certains navigateurs peuvent avoir des comportements par défaut liés aux combinaisons de touches comme Ctrl + K (par exemple, mettre le focus sur la barre d’adresse du navigateur). Appeler preventDefault arrête ce comportement.
  4. Nettoyage des Événements (return () => ...)

    • La fonction de nettoyage supprime l’écouteur d’événements pour éviter que des écouteurs en double ne soient ajoutés si le composant se re-rend.

Étape 2 : Basculer la Visibilité du Composant

Ensuite, mettez à jour la fonction handleKeyDown pour basculer la visibilité de SearchBox lorsque le raccourci est pressé :

useEffect(() => {
    const handleKeyDown = (event: KeyboardEvent) => {
      // Écouter Ctrl + K
      if (event.ctrlKey && event.key === Key.K) {
        event.preventDefault(); // Prévenir le comportement par défaut du navigateur
        setIsOpen((prev) => !prev); // Basculer la boîte de recherche
      } else if (event.key === Key.Escape) {
        setIsOpen(false); // Fermer la boîte de recherche
      }
    };

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

Que se passe-t-il dans ce code ?

  1. Changement d’état (setIsOpen((prev) => !prev))

    • Lorsque Ctrl + K est pressé, le setter d’état setIsOpen change la visibilité de la SearchBox.

    • L’argument prev représente l’état précédent. L’utilisation de !prev inverse sa valeur :

      • true (ouvert) devient false (fermé).

      • false (fermé) devient true (ouvert).

  2. Fermeture avec la touche Échap (event.key === "Escape")

    • Lorsque la touche Échap est pressée, setIsOpen(false) définit explicitement l’état sur false, fermant le SearchBox.

Cela donne le résultat suivant :

Comment animer la visibilité du composant

Pour l’instant, notre composant fonctionne, mais il manque un peu de charme, n’est-ce pas ? Changeons cela.

Étape 1 : Créer le composant de superposition

Nous allons commencer par créer un composant de superposition, qui sert de fond sombre et flou pour la zone de recherche. Voici la version de base :

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

Étape 2 : Ajouter des animations à la superposition

Maintenant, faisons en sorte que la superposition apparaisse et disparaisse en utilisant Framer Motion. Mettez à jour le composant OverlayWrapper comme ceci :

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>
  );
}
Propriétés d’animation clés :
  • initial : Définit l’état de départ lorsque le composant est monté (entièrement transparent).

  • animate : Définit l’état vers lequel animer (entièrement opaque).

  • exit : Spécifie l’animation lorsque le composant est démonté (fondu en sortie).

Ensuite, ajoutez un peu de mouvement à la boîte de recherche elle-même. Nous allons la faire glisser et apparaître en fondu lorsqu’elle apparaît et glisser hors de vue lorsqu’elle disparaît.

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

Étape 4 : Activer le suivi d’animation avec AnimatePresence

Enfin, enveloppez votre logique de rendu conditionnel dans le composant AnimatePresence fourni par Framer Motion. Cela garantit que Framer Motion suit quand les éléments entrent et sortent du DOM.

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

Cela permet à Framer Motion de suivre quand un élément entre et sort du DOM. Avec cela, nous obtenons le résultat suivant :

Ah, beaucoup mieux !

Comment optimiser votre composant KSL

Si vous pensiez que nous avions terminé, pas si vite… Nous avons encore un peu plus à faire.

Nous devons optimiser pour l’accessibilité. Nous devrions ajouter un moyen pour les utilisateurs de fermer le composant de recherche avec une souris, car l’accessibilité est très importante.

Pour ce faire, commencez par créer un hook appelé useClickOutside. Ce hook utilise un élément de référence pour savoir quand un utilisateur clique en dehors de l’élément cible (boîte de recherche), ce qui est un comportement très populaire pour fermer des modaux et des KSLC.


import { useEffect } from "react";

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

export const useClickOutside = (
  ref: React.RefObject<HTMLElement>,
  handler: ClickOutsideHandler
) => {
  useEffect(() => {
    const listener = (event: Event) => {
      // Ne rien faire si l'on clique sur l'élément de référence ou sur ses éléments descendants
      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]);
};

Pour utiliser ce hook, passez la fonction responsable de l’ouverture et de la fermeture du composant de recherche :

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

Ensuite, recevez la fonction dans la recherche avec son type de prop approprié :

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

Après cela, créez une référence (ref) à l’élément que vous souhaitez suivre et marquez cet élément :

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

Ensuite, passez cette ref et la fonction à appeler lorsque un clic en dehors de cet élément est détecté.

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

Tester maintenant donne le résultat suivant :

Nous pouvons également optimiser le code un peu plus. Comme nous l’avons fait avec la fonctionnalité d’accessibilité, nous pouvons rendre notre écouteur pour détecter les raccourcis beaucoup plus propre et efficace avec les étapes suivantes.

Tout d’abord, créez un fichier de hook useKeyBindings pour gérer les combinaisons de touches.

Ensuite, définissez le hook et l’interface. Le hook acceptera un tableau de liaisons, où chaque liaison se compose de :

  • Un tableau keys, qui spécifie la combinaison de touches (par exemple, [« Control », « k »])

  • Une fonction de rappel, qui est appelée lorsque les touches correspondantes sont enfoncées.

import { useEffect } from "react";

// Définir la structure d'une liaison de touches
interface KeyBinding {
  keys: string[]; // Tableau de touches (par exemple, ["Control", "k"])
  callback: () => void; // Fonction à exécuter lorsque les touches sont enfoncées
}

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

};

Ensuite, créez la fonction handleKeyDown. À l’intérieur du hook, définissez une fonction qui écoutera les événements du clavier. Cette fonction vérifiera si les touches enfoncées correspondent à des combinaisons de touches définies.

Nous allons normaliser les touches en minuscules afin que la comparaison soit insensible à la casse et suivre quelles touches sont enfoncées en vérifiant ctrlKey, shiftKey, altKey, metaKey, et la touche enfoncée (par exemple, « k » pour Ctrl + K).

const handleKeyDown = (event: KeyboardEvent) => {
  // Suivre les touches qui sont enfoncées
  const pressedKeys = new Set<string>();

  // Vérifier les touches de modification (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");

  // Ajouter la touche qui a été enfoncée (par exemple, "k" pour Ctrl + K)
  if (event.key) pressedKeys.add(event.key.toLowerCase());
};

Ensuite, nous comparerons les touches enfoncées avec le tableau de touches de nos liaisons pour vérifier si elles correspondent. Si c’est le cas, nous appellerons la fonction de rappel associée. Nous veillons également à ce que le nombre de touches enfoncées corresponde au nombre de touches définies dans la liaison.

// Parcourir chaque liaison de touches
bindings.forEach(({ keys, callback }) => {
  // Normaliser les touches en minuscules pour la comparaison
  const normalizedKeys = keys.map((key) => key.toLowerCase());

  // Vérifier si les touches enfoncées correspondent à la liaison de touches
  const isMatch =
    pressedKeys.size === normalizedKeys.length &&
    normalizedKeys.every((key) => pressedKeys.has(key));

  // Si les touches correspondent, appeler le rappel
  if (isMatch) {
    event.preventDefault(); // Empêcher le comportement par défaut du navigateur
    callback(); // Exécuter la fonction de rappel
  }
});

Enfin, configurez des écouteurs d’événements sur l’objet window pour écouter les événements de type keydown. Ces écouteurs déclencheront la fonction handleKeyDown à chaque fois qu’une touche est pressée. Assurez-vous d’ajouter la suppression des écouteurs d’événements lorsque le composant est démonté.

useEffect(() => {
  // Ajouter des écouteurs d'événements pour keydown
  window.addEventListener("keydown", handleKeyDown);

  // Nettoyer les écouteurs d'événements lorsque le composant est démonté
  return () => {
    window.removeEventListener("keydown", handleKeyDown);
  };
}, [bindings]);

Le hook complet useKeyBindings mis ensemble ressemble maintenant à ceci :

import { useEffect } from "react";

interface KeyBinding {
  keys: string[]; // Une combinaison de touches pour déclencher le rappel (par exemple, ["Contrôle", "k"])
  callback: () => void; // La fonction à exécuter lorsque les touches sont pressées
}

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

        // Suivre explicitement les touches de modification
        if (event.ctrlKey) pressedKeys.add("control");
        if (event.shiftKey) pressedKeys.add("shift");
        if (event.altKey) pressedKeys.add("alt");
        if (event.metaKey) pressedKeys.add("meta");

        // Ajouter la touche réellement pressée
        if (event.key) pressedKeys.add(event.key.toLowerCase());

        // Correspondance exacte : les touches pressées doivent correspondre aux touches définies
        const isExactMatch =
          pressedKeys.size === normalizedKeys.length &&
          normalizedKeys.every((key) => pressedKeys.has(key));

        if (isExactMatch) {
          event.preventDefault(); // Empêcher le comportement par défaut
          callback(); // Exécuter le rappel
        }
      });
    };

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

Voici comment vous pouvez utiliser ce hook dans votre App :

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

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

  useKeyBindings([
    {
      keys: ["Control", "k"], // Écouter "Ctrl + K"
      callback: () => setIsOpen((prev) => !prev), // Basculer la boîte de recherche
    },
    {
      keys: ["Escape"], // Écouter "Échap"
      callback: () => setIsOpen(false), // Fermer la boîte de recherche
    },
  ]);

Ce qui donne le résultat suivant :

Avec cette approche, vous pouvez même ajouter plusieurs raccourcis pour déclencher la visibilité du composant de recherche.

useKeyBindings([
    {
      keys: ["Control", "k"], // Écoutez "Ctrl + K"
      callback: () => setIsOpen((prev) => !prev), // Alternez la boîte de recherche
    },
    {
      keys: ["Control", "d"], // Écoutez "Ctrl + D"
      callback: () => setIsOpen((prev) => !prev), // Alternez la boîte de recherche
    },
    {
      keys: ["Escape"], // Écoutez "Échap"
      callback: () => setIsOpen(false), // Fermez la boîte de recherche
    },
  ]);

Voici des liens vers toutes les ressources dont vous pourriez avoir besoin pour cet article :

Conclusion

J’espère que cet article vous a semblé être un raccourci bien chronométré, vous amenant au cœur de la création de composants de raccourcis clavier réutilisables. À chaque pression de touche et animation, vous pouvez maintenant transformer des expériences web ordinaires en expériences extraordinaires.

J’espère que vos raccourcis vous aideront à créer des applications qui plaisent à vos utilisateurs. Après tout, les meilleurs voyages commencent souvent par la bonne combinaison.

Aimez-vous mes articles ?

N’hésitez pas à m’acheter un café ici, pour garder mon esprit en marche et fournir plus d’articles comme celui-ci.

Informations de contact

Vous souhaitez vous connecter ou me contacter ? N’hésitez pas à me contacter sur les plateformes suivantes :