HextaUI

Hotkey

Analise, rotule, anuncie e compare atalhos de teclado, com ⌘ nas plataformas Apple e Ctrl em todas as outras.

Apple
⇧⌘K
Windows and Linux
Shift+Ctrl+K
Screen readers
Shift Command K
parseHotkey
[["shift","mod","k"]]

Click outside the field and press Shift Command Kmatched 0×

"use client"

import * as React from "react"

import { Input } from "@/components/ui/input"
import { Kbd } from "@/components/ui/kbd"
import {
  formatHotkey,
  matchesHotkey,
  parseHotkey,
  spokenKey,
  useIsApple,
} from "@/lib/hotkey"

export function HotkeyDemo() {
  const [hotkey, setHotkey] = React.useState("mod+shift+k")
  const [pressed, setPressed] = React.useState(0)
  const apple = useIsApple()
  const chord = parseHotkey(hotkey)[0] ?? []

  React.useEffect(() => {
    const onKeyDown = (event: KeyboardEvent) => {
      if (
        event.target instanceof HTMLInputElement ||
        !matchesHotkey(event, hotkey)
      ) {
        return
      }
      event.preventDefault()
      setPressed((count) => count + 1)
    }
    window.addEventListener("keydown", onKeyDown)
    return () => window.removeEventListener("keydown", onKeyDown)
  }, [hotkey])

  const rows = [
    ["Apple", formatHotkey(hotkey, true)],
    ["Windows and Linux", formatHotkey(hotkey, false)],
    ["Screen readers", chord.map((key) => spokenKey(key, apple)).join(" ")],
    ["parseHotkey", JSON.stringify(parseHotkey(hotkey))],
  ]

  return (
    <div className="flex w-full max-w-sm flex-col gap-4">
      <Input
        aria-label="Hotkey"
        value={hotkey}
        onChange={(event) => setHotkey(event.target.value)}
        spellCheck={false}
        autoCapitalize="off"
      />
      <dl className="grid grid-cols-[auto_minmax(0,1fr)] gap-x-6 gap-y-2 text-sm">
        {rows.map(([label, value]) => (
          <React.Fragment key={label}>
            <dt className="text-muted-foreground">{label}</dt>
            <dd className="truncate font-mono">{value}</dd>
          </React.Fragment>
        ))}
      </dl>
      <p className="flex items-center gap-2 text-sm text-muted-foreground">
        Click outside the field and press <Kbd keys={hotkey} />
        <span className="ms-auto font-mono tabular-nums">
          matched {pressed}×
        </span>
      </p>
    </div>
  )
}
pnpm dlx shadcn@latest add https://hextaui.com/r/hotkey.json

Adiciona o utilitário e tudo de que ele depende ao seu projeto.

import {
  formatHotkey,
  matchesHotkey,
  parseHotkey,
  useIsApple,
} from "@/lib/hotkey"
function SaveShortcut({ onSave }: { onSave: () => void }) {
  const apple = useIsApple()

  React.useEffect(() => {
    const onKeyDown = (event: KeyboardEvent) => {
      if (matchesHotkey(event, "mod+s")) {
        event.preventDefault()
        onSave()
      }
    }
    window.addEventListener("keydown", onKeyDown)
    return () => window.removeEventListener("keydown", onKeyDown)
  }, [onSave])

  return <span>Save {formatHotkey("mod+s", apple)}</span>
}

Escreva cada atalho uma vez, como uma string, e use a mesma string para exibi-lo, anunciá-lo e compará-lo. mod significa ⌘ nas plataformas Apple e Ctrl em todas as outras. É quase sempre o que você quer, já que Ctrl+K em um Mac e ⌘K no Windows soam errados.

"mod+k"            ⌘K on Apple platforms, Ctrl+K elsewhere
"shift+mod+p"      modifiers in any order
"cmd+option+esc"   aliases: cmd, command, option, opt, control, esc, return
"alt+up"           up, down, left and right for the arrow keys
"mod++"            a literal plus key
"g g"              a sequence of two chords, separated by a space

As teclas são separadas por + e não diferenciam maiúsculas de minúsculas. Teclas nomeadas usam o valor KeyboardEvent.key em minúsculas, como enter, tab, pageup ou f5. Use space para a barra de espaço.

parseHotkey("mod+shift+k")  // [["shift", "mod", "k"]]
parseHotkey("cmd+opt+esc")  // [["alt", "meta", "escape"]]
parseHotkey("g i")          // [["g"], ["i"]]

parseHotkey retorna um array por acorde, com os aliases resolvidos e os modificadores ordenados na ordem da Apple: Control, Option, Shift, Command. Todo rótulo construído a partir dele é lido na ordem que as pessoas esperam ver.

formatHotkey("mod+shift+k", true)   // "⇧⌘K"
formatHotkey("mod+shift+k", false)  // "Shift+Ctrl+K"
keyLabel("enter", true)             // "↵"
spokenKey("mod", true)              // "Command"
spokenKey("mod", false)             // "Control"

As plataformas Apple usam símbolos sem separador, como os menus os mostram. Windows e Linux usam palavras unidas por +. Símbolos são difíceis de ler em voz alta, então spokenKey fornece o nome que um leitor de tela deve anunciar. <Kbd keys> mostra o símbolo e coloca o nome falado em texto visualmente oculto.

useIsApple() escolhe a plataforma. Retorna true no servidor e durante a hidratação, e depois a resposta real, então um visitante de Windows vê brevemente ⌘ antes de Ctrl em vez de receber um erro de hidratação.

Listener de atalhos

matchesHotkey verifica um evento keydown contra um hotkey. Os modificadores precisam corresponder exatamente, então mod+b não dispara para mod+shift+b.

"use client"

import * as React from "react"

import { Kbd } from "@/components/ui/kbd"
import { matchesHotkey } from "@/lib/hotkey"

const shortcuts = [
  { hotkey: "mod+b", label: "Bold" },
  { hotkey: "mod+i", label: "Italic" },
  { hotkey: "mod+shift+x", label: "Strikethrough" },
]

export function HotkeyListener() {
  const [last, setLast] = React.useState<string>()

  React.useEffect(() => {
    const onKeyDown = (event: KeyboardEvent) => {
      const match = shortcuts.find((shortcut) =>
        matchesHotkey(event, shortcut.hotkey)
      )
      if (match) {
        event.preventDefault()
        setLast(match.label)
      }
    }
    window.addEventListener("keydown", onKeyDown)
    return () => window.removeEventListener("keydown", onKeyDown)
  }, [])

  return (
    <div className="flex w-full max-w-xs flex-col gap-3">
      <ul className="flex flex-col gap-2 text-sm">
        {shortcuts.map((shortcut) => (
          <li
            key={shortcut.hotkey}
            data-active={last === shortcut.label ? "" : undefined}
            className="flex items-center justify-between rounded-md px-2 py-1 transition-colors duration-150 data-active:bg-muted"
          >
            {shortcut.label}
            <Kbd keys={shortcut.hotkey} />
          </li>
        ))}
      </ul>
      <p role="status" className="text-sm text-muted-foreground">
        {last ? `Matched ${last}` : "Press a shortcut"}
      </p>
    </div>
  )
}
  • Letras e dígitos também correspondem pela tecla física, então alt+k funciona em um Mac, onde Option+K digita ˚.
  • Letras com Shift correspondem: shift+k corresponde ao K que o Shift produz.
  • Ele compara um único acorde. Para sequências como g i, acompanhe você mesmo o acorde anterior.
  • Ignora atalhos sem modificadores enquanto o foco está em um campo de texto, para que digitar uma letra nunca dispare um comando.
ExportaçãoDescrição
parseHotkey(hotkey)string[][]: um array por acorde, com os aliases resolvidos e os modificadores ordenados.
formatHotkey(hotkey, apple)O rótulo de um acorde, como ⇧⌘K ou Shift+Ctrl+K.
keyLabel(key, apple)O rótulo visível de um nome de tecla.
spokenKey(key, apple)O nome que um leitor de tela deve anunciar para uma tecla.
matchesHotkey(event, hotkey)Se um KeyboardEvent corresponde a um acorde, com modificadores exatos.
isApplePlatform()Lê navigator.platform. true no servidor.
useIsApple()isApplePlatform como um hook seguro para hidratação.

Kbd e Command.