Se sei come me e adori le scorciatoie, sai quanto sia soddisfacente premere alcuni tasti e vedere la magia accadere. Che si tratti del familiare Ctrl+C – Ctrl+V che gli sviluppatori usano per “prendere in prestito codice” 😉 da LLM e pagine di codice, o delle scorciatoie personalizzate che impostiamo nei nostri strumenti preferiti, le scorciatoie da tastiera risparmiano tempo e ci fanno sentire come dei maghi del computer.
Bene, non temere! Ho scoperto il segreto per creare componenti che attivano e rispondono alle scorciatoie da tastiera. In questo articolo, ti mostrerò come crearli con React, Tailwind CSS e Framer Motion.
Indice
Ecco tutto ciò che tratteremo:
Requisiti
-
Fondamenti di HTML, CSS e Tailwind CSS
-
Fondamenti di JavaScript, React e React Hooks.
Cosa È un Componente Listener per Combinazioni di Tasti (KSL)?
Un componente Listener per le Scorciatoie da Tastiera (KSLC) è un componente che ascolta combinazioni di tasti specifiche e attiva azioni nella tua app. È progettato per far sì che la tua app risponda alle scorciatoie da tastiera, consentendo un’esperienza utente più fluida ed efficiente.
Perché è importante?
-
Accessibilità: Il componente KSL semplifica per le persone che utilizzano una tastiera attivare azioni, rendendo la tua app più inclusiva e facile da usare.
-
Esperienza più Veloce: Le scorciatoie sono rapide ed efficienti, consentendo agli utenti di completare le attività in meno tempo. Niente più smanettamenti con il mouse—basta premere un tasto (o due) ed ecco, l’azione avviene!
-
Riutilizzabilità: Una volta configurato il tuo KSL, può gestire diverse scorciatoie nella tua app, rendendo facile aggiungerle senza riscrivere la stessa logica.
-
Codice più pulito: Invece di sparpagliare i gestori degli eventi della tastiera ovunque, il componente KSL mantiene le cose ordinate centralizzando la logica. Il tuo codice rimane pulito, organizzato e più facile da mantenere.
Come Costruire il Componente KSL
Ho preparato un repository GitHub con file di avvio per velocizzare le cose. Basta clonare questo repository e installare le dipendenze.
Per questo progetto, stiamo usando la home page di Tailwind come nostra musa e creando la funzionalità KSL. Dopo aver installato ed eseguito il comando di build, ecco come dovrebbe apparire la tua pagina:
Come Creare il Componente di Rivelazione
Il componente di rivelazione è il componente che vogliamo mostrare quando usiamo la scorciatoia.
Per iniziare, crea un file chiamato search-box.tsx
e incolla questo codice:
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>
);
}
Ok, quindi cosa succede in questo codice?
-
Overlay Principale (
<div className="fixed top-0 left-0 ...">
)-
Questo è l’overlay a schermo intero che oscura lo sfondo.
-
Il
backdrop-blur-sm
aggiunge un leggero sfocatura allo sfondo, ebg-slate-900/50
gli conferisce un’overlay scuro semi-trasparente.
-
-
Involucro della Casella di Ricerca (
<div className="p-[15vh] ...">
)-
Il contenuto è centrato utilizzando padding e utility flex.
-
Il
max-w-xl
assicura che la casella di ricerca rimanga all’interno di una larghezza ragionevole per la leggibilità.
-
Poi nel tuo App.tsx
, crea uno stato che mostra dinamicamente quel componente:
const [isOpen, setIsOpen] = useState<boolean>(false);
-
useState
: Questo hook inizializzaisOpen
afalse
, il che significa che la casella di ricerca è nascosta per impostazione predefinita. -
Quando
isOpen
è impostato atrue
, il componenteSearchBox
verrà visualizzato sullo schermo.
E renderizza il componente di ricerca:
{isOpen && <SearchBox />}
Per mostrare il componente di ricerca, aggiungi una funzione di toggle al pulsante di input:
<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’evento onClick
imposta isOpen
a true
, mostrando il SearchBox
.
Ma come hai visto, questo è stato attivato da un’azione di clic, non da un’azione di scorciatoia da tastiera. Facciamo questo dopo.
Come attivare il componente tramite scorciatoia da tastiera
Per far aprire e chiudere il componente di rivelazione utilizzando una scorciatoia da tastiera, utilizzeremo un hook useEffect
per ascoltare combinazioni di tasti specifiche e aggiornare lo stato del componente di conseguenza.
Passaggio 1: Ascolta gli eventi da tastiera
Aggiungi un hook useEffect
nel tuo file App.tsx
per ascoltare le pressioni di tasti:
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.ctrlKey && event.key === Key.K) {
event.preventDefault(); // Prevenire il comportamento predefinito del browser
} };
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, []);
Cosa sta succedendo in questo codice?
-
Impostazione dell’effetto (
useEffect
)useEffect
garantisce che il listener per le pressioni di tasti venga aggiunto quando il componente viene montato e rimosso quando il componente viene smontato, prevenendo perdite di memoria.
-
Combinazione di Tasti (
event.ctrlKey && event.key === "k"
)-
Il
event.ctrlKey
verifica se il tasto Control è premuto. -
Il
event.key === "k"
garantisce che stiamo ascoltando specificamente per il tasto “K”. Insieme, questo verifica se la combinazione Ctrl + K è premuta.
-
-
Prevenire il Comportamento Predefinito (
event.preventDefault()
)- Alcuni browser potrebbero avere comportamenti predefiniti legati a combinazioni di tasti come Ctrl + K (ad esempio, focalizzarsi sulla barra degli indirizzi del browser). Chiamare
preventDefault
ferma questo comportamento.
- Alcuni browser potrebbero avere comportamenti predefiniti legati a combinazioni di tasti come Ctrl + K (ad esempio, focalizzarsi sulla barra degli indirizzi del browser). Chiamare
-
Pulizia dell’Evento (
return () => ...
)- La funzione di pulizia rimuove l’ascoltatore di eventi per evitare che vengano aggiunti ascoltatori duplicati se il componente viene ri-renderizzato.
Passo 2: Attivare la Visibilità del Componente
Successivamente, aggiorna la funzione handleKeyDown
per attivare la visibilità del SearchBox
quando viene premuto il collegamento rapido:
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
// Ascolta per Ctrl + K
if (event.ctrlKey && event.key === Key.K) {
event.preventDefault(); // Impedire il comportamento predefinito del browser
setIsOpen((prev) => !prev); // Attiva la casella di ricerca
} else if (event.key === Key.Escape) {
setIsOpen(false); // Chiudi la casella di ricerca
}
};
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, []);
Cosa sta succedendo in questo codice?
-
Attivare/disattivare lo stato (
setIsOpen((prev) => !prev)
)-
Quando viene premuto Ctrl + K, il setter di stato
setIsOpen
attiva/disattiva la visibilità dellaSearchBox
. -
L’argomento
prev
rappresenta lo stato precedente. Utilizzando!prev
si inverte il suo valore:-
true
(aperto) diventafalse
(chiuso). -
false
(chiuso) diventatrue
(aperto).
-
-
-
Chiusura con il tasto Escape (
event.key === "Escape"
)- Quando il tasto Escape viene premuto,
setIsOpen(false)
imposta esplicitamente lo stato sufalse
, chiudendo ilSearchBox
.
- Quando il tasto Escape viene premuto,
Questo porta al seguente:
Come animare la visibilità del componente
Al momento, il nostro componente funziona, ma gli manca un po’ di stile, non credi? Cambiamo questo.
Passo 1: Crea il componente Overlay
Iniziamo creando un componente overlay, che funge da sfondo scuro e sfocato per la casella di ricerca. Ecco la versione 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>
);
}
Passo 2: Aggiungi animazioni all’Overlay
Ora, facciamo in modo che l’overlay svanisca dentro e fuori utilizzando Framer Motion. Aggiorna il componente OverlayWrapper
in questo modo:
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>
);
}
Proprietà di animazione chiave:
-
initial
: Imposta lo stato iniziale quando il componente è montato (completamente trasparente). -
animate
: Definisce lo stato verso cui animare (completamente opaco). -
exit
: Specifica l’animazione quando il componente viene smontato (dissolvenza).
Passo 3: Anima la Casella di Ricerca
Successivamente, aggiungi un po’ di movimento alla casella di ricerca stessa. La faremo scorrere e dissolversi quando appare e scorrere via quando scompare.
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>
);
}
Passo 4: Abilita il Tracciamento delle Animazioni con AnimatePresence
Infine, avvolgi la tua logica di rendering condizionale nel componente AnimatePresence
fornito da Framer Motion. Questo assicura che Framer Motion tracci quando gli elementi entrano ed escono dal DOM.
<AnimatePresence>{isOpen && <SearchBox />}</AnimatePresence>
Questo consente a Framer Motion di tracciare quando un elemento entra ed esce dal DOM. Con questo, otteniamo il seguente risultato:
Ah, molto meglio!
Come Ottimizzare il Tuo Componente KSL
Se pensavi che fossimo a posto, non così in fretta… Abbiamo ancora un po’ da fare.
Dobbiamo ottimizzare per l’accessibilità. Dobbiamo aggiungere un modo per gli utenti di chiudere il componente di ricerca con il mouse, poiché l’accessibilità è molto importante.
Per fare ciò, inizia creando un hook chiamato useClickOutside
. Questo hook utilizza un elemento di riferimento per sapere quando un utente clicca al di fuori dell’elemento target (casella di ricerca), che è un comportamento molto popolare per chiudere modali e KSLC.
import { useEffect } from "react";
type ClickOutsideHandler = (event: Event) => void;
export const useClickOutside = (
ref: React.RefObject<HTMLElement>,
handler: ClickOutsideHandler
) => {
useEffect(() => {
const listener = (event: Event) => {
// Non fare nulla se si clicca sull'elemento di riferimento o sugli elementi discendenti
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]);
};
Per utilizzare questo hook, passa la funzione responsabile dell’apertura e della chiusura del componente di ricerca:
<AnimatePresence> {isOpen && <SearchBox close={setIsOpen} />} </AnimatePresence>
Quindi ricevi la funzione nella ricerca con il suo tipo di proprietà corretto:
export default function SearchBox({
close,
}: {
close: React.Dispatch<React.SetStateAction<boolean>>;
}) {
Dopo di che, crea un riferimento (ref) all’elemento che desideri tracciare e contrassegna quell’elemento:
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>
);
}
Poi passa quel ref e la funzione da chiamare quando viene rilevato un clic al di fuori di quell’elemento.
useClickOutside(searchboxRef, () => close(false));
Testarlo ora dà il seguente risultato:
Possiamo anche ottimizzare un po’ di più il codice. Come abbiamo fatto con la funzionalità di accessibilità, possiamo rendere il nostro listener per rilevare le scorciatoie molto più pulito ed efficiente con i seguenti passaggi.
Prima di tutto, crea un file hook useKeyBindings
per gestire le combinazioni di tasti.
Poi definisci l’hook e l’interfaccia. L’hook accetterà un array di binding, dove ogni binding è composto da:
-
Un array di
keys
, che specifica la combinazione di tasti (ad esempio, [“Control”, “k”]) -
Una funzione di callback, che viene chiamata quando i tasti corrispondenti vengono premuti.
import { useEffect } from "react";
// Definisci la struttura di un keybinding
interface KeyBinding {
keys: string[]; // Array di tasti (es., ["Control", "k"])
callback: () => void; // Funzione da eseguire quando i tasti vengono premuti
}
export const useKeyBindings = (bindings: KeyBinding[]) => {
};
Successivamente, crea la funzione handleKeyDown
. All’interno del hook, definisci una funzione che ascolterà gli eventi della tastiera. Questa funzione verificherà se i tasti premuti corrispondono a combinazioni di tasti definite.
Normalizzeremo i tasti in minuscolo in modo che il confronto sia insensibile al maiuscolo e seguiremo quali tasti sono stati premuti controllando ctrlKey
, shiftKey
, altKey
, metaKey
e il tasto premuto (ad esempio, “k” per Ctrl + K).
const handleKeyDown = (event: KeyboardEvent) => {
// Tieni traccia dei tasti premuti
const pressedKeys = new Set<string>();
// Controlla i tasti modificatori (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");
// Aggiungi il tasto che è stato premuto (ad es., "k" per Ctrl + K)
if (event.key) pressedKeys.add(event.key.toLowerCase());
};
Successivamente, confronteremo i tasti premuti con l’array di tasti dalle nostre associazioni per verificare se corrispondono. Se lo fanno, chiameremo la funzione di callback associata. Ci assicuriamo inoltre che il numero di tasti premuti corrisponda al numero di tasti definiti nell’associazione.
// Scorri ogni associazione di tasti
bindings.forEach(({ keys, callback }) => {
// Normalizza i tasti in minuscolo per il confronto
const normalizedKeys = keys.map((key) => key.toLowerCase());
// Controlla se i tasti premuti corrispondono all'associazione di tasti
const isMatch =
pressedKeys.size === normalizedKeys.length &&
normalizedKeys.every((key) => pressedKeys.has(key));
// Se i tasti corrispondono, chiama la callback
if (isMatch) {
event.preventDefault(); // Impedisci il comportamento predefinito del browser
callback(); // Esegui la funzione di callback
}
});
Infine, imposta gli ascoltatori di eventi sull’oggetto finestra per ascoltare gli eventi di pressione dei tasti. Questi ascoltatori attiveranno la funzione handleKeyDown
ogni volta che un tasto viene premuto. Assicurati di pulire gli ascoltatori di eventi quando il componente viene smontato.
useEffect(() => {
// Aggiungi gli ascoltatori di eventi per la pressione dei tasti
window.addEventListener("keydown", handleKeyDown);
// Pulisci gli ascoltatori di eventi quando il componente viene smontato
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [bindings]);
Il completo useKeyBindings
hook ora assemblato appare così:
import { useEffect } from "react";
interface KeyBinding {
keys: string[]; // Una combinazione di tasti per attivare il callback (ad es., ["Control", "k"])
callback: () => void; // La funzione da eseguire quando i tasti vengono premuti
}
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>();
// Tieni traccia esplicita dei tasti modificatori
if (event.ctrlKey) pressedKeys.add("control");
if (event.shiftKey) pressedKeys.add("shift");
if (event.altKey) pressedKeys.add("alt");
if (event.metaKey) pressedKeys.add("meta");
// Aggiungi il tasto effettivamente premuto
if (event.key) pressedKeys.add(event.key.toLowerCase());
// Corrispondenza esatta: i tasti premuti devono corrispondere ai tasti definiti
const isExactMatch =
pressedKeys.size === normalizedKeys.length &&
normalizedKeys.every((key) => pressedKeys.has(key));
if (isExactMatch) {
event.preventDefault(); // Prevenire il comportamento predefinito
callback(); // Esegui il callback
}
});
};
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [bindings]);
}
Ecco come puoi utilizzare questo hook nella tua App
:
import { useKeyBindings } from "./hooks/useKeyBindings";
export default function App() {
const [isOpen, setIsOpen] = useState<boolean>(false);
useKeyBindings([
{
keys: ["Control", "k"], // Ascolta per "Ctrl + K"
callback: () => setIsOpen((prev) => !prev), // Attiva/disattiva la casella di ricerca
},
{
keys: ["Escape"], // Ascolta per "Escape"
callback: () => setIsOpen(false), // Chiudi la casella di ricerca
},
]);
Che dà il seguente risultato:
Con questo approccio, è possibile aggiungere anche più scorciatoie per attivare la visibilità del componente di ricerca.
useKeyBindings([
{
keys: ["Control", "k"], // Ascolta "Ctrl + K"
callback: () => setIsOpen((prev) => !prev), // Attiva/disattiva il riquadro di ricerca
},
{
keys: ["Control", "d"], // Ascolta "Ctrl + D"
callback: () => setIsOpen((prev) => !prev), // Attiva/disattiva il riquadro di ricerca
},
{
keys: ["Escape"], // Ascolta "Esc"
callback: () => setIsOpen(false), // Chiudi il riquadro di ricerca
},
]);
Ecco i link a tutte le risorse di cui potresti avere bisogno per questo articolo:
Conclusione
Spero che questo articolo sia risultato come una scorciatoia ben calibrata, portandoti al cuore della costruzione di componenti di scorciatoie da tastiera riutilizzabili. Con ogni pressione di un tasto e animazione, ora puoi trasformare esperienze web ordinarie in straordinarie.
Spero che le tue scorciatoie ti aiutino a creare app che si accordino con i tuoi utenti. Dopotutto, i migliori viaggi spesso iniziano con la combinazione giusta.
Ti piacciono i miei articoli?
Sentiti libero di offrirmi un caffè qui, per mantenere il mio cervello attivo e fornire più articoli come questo.
Informazioni di contatto
Vuoi connetterti o contattarmi? Sentiti libero di scrivermi sui seguenti:
-
Twitter / X: @jajadavid8
-
LinkedIn: David Jaja
-
Email: [email protected]