概要
/ キーや Ctrl+K で検索入力欄へフォーカスを移動するショートカットを実装する。useEffect でグローバルな keydown イベントをリッスンし、input や textarea に入力中のときは誤作動しないよう制御する。
インストール
# 追加インストールは不要
実装
ショートカットフック
// hooks/useSearchShortcut.ts
import { useEffect } from "react";
type Options = {
/** フォーカス先の input の ref */
inputRef: React.RefObject<HTMLInputElement | null>;
/** "/" キーを有効にするか(デフォルト true) */
enableSlash?: boolean;
/** Ctrl+K / Cmd+K を有効にするか(デフォルト true) */
enableCtrlK?: boolean;
};
export function useSearchShortcut({
inputRef,
enableSlash = true,
enableCtrlK = true,
}: Options) {
useEffect(() => {
const handler = (e: KeyboardEvent) => {
const target = e.target as HTMLElement;
// input / textarea / contenteditable に入力中は無視
if (
target.tagName === "INPUT" ||
target.tagName === "TEXTAREA" ||
target.isContentEditable
) {
return;
}
const isSlash = enableSlash && e.key === "/";
const isCtrlK = enableCtrlK && (e.ctrlKey || e.metaKey) && e.key === "k";
if (isSlash || isCtrlK) {
e.preventDefault();
inputRef.current?.focus();
inputRef.current?.select();
}
};
document.addEventListener("keydown", handler);
return () => document.removeEventListener("keydown", handler);
}, [inputRef, enableSlash, enableCtrlK]);
}
検索インプットへの適用
// components/SearchInput.tsx
"use client";
import { useRef } from "react";
import { useSearchShortcut } from "@/hooks/useSearchShortcut";
type Props = {
value: string;
onChange: (v: string) => void;
placeholder?: string;
};
export function SearchInput({ value, onChange, placeholder = "検索…" }: Props) {
const inputRef = useRef<HTMLInputElement>(null);
useSearchShortcut({ inputRef });
return (
<div className="relative">
<input
ref={inputRef}
type="search"
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
className="w-full rounded border border-gray-200 py-2 pl-3 pr-20 text-sm focus:outline-none focus:ring-1 focus:ring-blue-400"
aria-label="サンプルを検索"
/>
{/* ショートカットヒント */}
<span
className="pointer-events-none absolute inset-y-0 right-2 flex items-center gap-1 text-xs text-gray-400"
aria-hidden="true"
>
<kbd className="rounded border border-gray-200 px-1">/</kbd>
</span>
</div>
);
}
Ctrl+K のみ有効にする場合
// Ctrl+K / Cmd+K のみ("/" は無効)
useSearchShortcut({ inputRef, enableSlash: false, enableCtrlK: true });
フォーカス後にモーダルを開くパターン
// hooks/useCommandPaletteShortcut.ts
import { useEffect } from "react";
export function useCommandPaletteShortcut(onOpen: () => void) {
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === "k") {
e.preventDefault();
onOpen();
}
};
document.addEventListener("keydown", handler);
return () => document.removeEventListener("keydown", handler);
}, [onOpen]);
}
ポイント
target.tagName === "INPUT"チェックで、ユーザーが別の入力欄を使っているときに誤フォーカスしないよう防ぐ。contenteditableも合わせてチェックするe.preventDefault()を呼ぶことで/キーが入力欄に入力されるのを防ぐ。フォーカス移動のみを行うinputRef.current?.select()でフォーカスと同時に既存の入力値を全選択する。上書き入力がしやすくなるuseEffectのクリーンアップでremoveEventListenerを確実に呼ぶ。コンポーネントのアンマウント後にハンドラが残るメモリリークを防ぐenableSlash/enableCtrlKのオプションで、プロジェクトの UX 方針に合わせてショートカットを選べる設計にするCtrl+Kは macOS ではCmd+KとしてmetaKeyになるため、e.ctrlKey || e.metaKeyで両方チェックする