Zenn
Gemcook Tech Blog
⌨️

ブラウザでキーボードショートカットを実装する

2025/01/28に公開
2

はじめに

ショートカットとは、アプリケーションの操作を特定のキーボード入力で素早く実行するための効率的なインターフェースのことで、皆さんもブラウザやWebアプリで実装されているショートカットを使う機会も多いのではないでしょうか?
今回は、Reactを使ったブラウザでのショートカットの実装方法についてみていきたいと思います。

ブラウザでの入力と挙動について

実装にあたり、事前に知っておいた方が良い前提知識について少し見ていきたいと思います。

装飾キーと通常キー

ショートカットキーは通常、Command(修飾キー) + C(通常キー) のように、修飾キーと通常キーの組み合わせで動作します。
Javascriptのイベントにおいて、修飾キー(CtrlAltShift など)は通常キーと異なり特別な役割を持っています。ここでは何か違いがあるんだな〜ということに留意してください。

OS間のキーボードの違い

MacとWindowsでは一部、キーボードの名称が異なりますが、MacのOptionはWindowsのAlt、WindowsのWindowsキーはMacのcommandに対応する形で、ブラウザは次のように差分を吸収してくれています。

// Windowsでは Alt キー、Macでは Option キーを押した場合
document.addEventListener('keydown', (event) => {
  if (event.altKey) console.log('Alt/Optionキーが押されました!');
});

// Windowsでは Windowsキー、Macでは commandキーを押した場合
document.addEventListener('keydown', (event) => {
  if (event.metaKey) console.log('Windows/Commandキーが押されました!');
});

キーボード配列について

本記事では、QWERTY配列を使用していることを前提に進めていますが、特殊なキーボード配列を使用している場合、JavaScriptの一部のイベントプロパティ(KeyboardEvent.code)を扱う際に、ユーザーが実際に入力したキーと異なる値が取得される可能性があります。それぞれの要件にもよると思いますが、注意が必要です。(詳しく知りたい方は以下のドキュメントをお読みください。)

https://developer.mozilla.org/ja/docs/Web/API/Element/keyup_event#イベントプロパティ:~:text=を返します。-,KeyboardEvent.code,-読取専用

JavascriptでのKeyboardEvent

JavaScriptでキーボード操作を扱う際に発火するイベントオブジェクト(keydownEvent)を確認すると、押されたキーに関する情報(通常キー)に付随して、修飾キーが押されているかどうかを示すプロパティも同時に含まれています。この仕組みを利用することで、ショートカットキーの組み合わせを簡単に検出することができます。

c だけを入力した場合

document.addEventListener('keydown', (event) => {
  console.log('通常キー:', event.key); // c
  console.log('command:', event.metaKey); // false: commandキーは押されていない
});

command + c の入力の場合

document.addEventListener('keydown', (event) => {
  console.log('通常キー:', event.key); // c
  console.log('command:', event.metaKey); // true: commandキーも押されている
});

これらの事から分かる通り、ショートカットが押されたかを判断するにはcが入力されたイベントで、同時にcommandも押されているか?を確認するだけで実装することが可能です。

実装

KeyboardEventの項目の項目で既にショートカットの発火までのイメージは掴んでいただけたかと思いますが、その後の挙動も含め、全体の実装を進めていきます。

実際の運用では、カスタム Hook を作成することが多いと思いますので、useKeyboardShortcuts.ts を作成し、ショートカットの実装はそちらにまとめたいと思います。

ショートカットの実行

今回はcommand + c の場合(event.metaKey && event.key === "c")にショートカットが実行される。という想定で実装を行なっていきます。

Page.tsx
export const Page = () => {
  useKeyboardShortcut({
    handleShortcut: () => console.log('実行されました!'),
  });

  return <div>{/* 省略 */}</div>;
};
hooks/useKeyboardShortcut.ts
type Params = {
    handleShortcut: () => void
}

export const useKeyboardShortcut = ({ handleShortcut }: Params) => {
  useEffect(() => {
    const keydownListener = (event: KeyboardEvent) => {
      if (event.metaKey && event.key === 'c') {
        event.preventDefault(); // デフォルトのショートカットをブロック
        handleShortcut();
      }
    };
    window.addEventListener('keydown', keydownListener);
    return () => {
      window.removeEventListener('keydown', keydownListener);
    };
  }, []);
};

現状の問題点

command + c を入力すると、キーを離さない限り何度もショートカットが実行されてしまいます。ショートカットを実行した後はキーが離されるまで再実行されないように実装を改善しましょう。

完成系

ショートカットが実行されたら、isShortcutActivetrueにして、その後のkeyboardEventは早期リターンを返して、連続入力が起こらないように実装を修正しました。
その後にcommand + cから手が離されたら(keyupListener)でisShortcutActivefalseにして、再度ショートカットの処理ができるように修正して実装完了です!

hooks/useKeyboardShortcut.ts
type Params = {
  handleShortcut: () => void;
};

export const useKeyboardShortcut = ({ handleShortcut }: Params) => {
  const isShortcutActive = useRef(false); // ショートカットが実行されたかのフラグ

  useEffect(() => {
    const keydownListener = (event: KeyboardEvent) => {
      // ショートカットが実行されてから、まだ手がキーボードから離れていない場合はリターン
      if (isShortcutActive.current) return event.preventDefault();
      if (event.metaKey && event.key === 'c') {
        event.preventDefault(); // デフォルトのショートカットをブロック
        isShortcutActive.current = true; // 実行されたことを記録 & 連続実行を無効化
        handleShortcut(); // ショートカットの実行
      }
    };

    const keyupListener = () => {
      // 一度実行されたら、キーがupになるまで、新たにshortcutsが実行されないようにする。
      isShortcutActive.current = false;
    };

    window.addEventListener('keydown', keydownListener);
    window.addEventListener('keyup', keyupListener);

    return () => {
      window.removeEventListener('keydown', keydownListener);
      window.removeEventListener('keyup', keyupListener);
    };
  }, [handleShortcut]);
};

まとめ

いかがだったでしょうか?個人的にはもっと難しい実装になるのかと思っていたのですが、蓋を開けると意外に簡単に実装できました。ただ、複雑な要件だったり、装飾キーを使わない場合だともっと難しくなるのかもしれません。

余談ですが…Figmaのブログがおもしろかったです。グローバルなアプリではショートカットの実装も色々大変みたいですね。
https://www.figma.com/blog/behind-the-scenes-international-keyboard-shortcuts/

2
Gemcook Tech Blog
Gemcook Tech Blog

Discussion

ログインするとコメントできます