メインコンテンツへスキップ
Header

イントロダクション

ホットキーをリッスンする必要がある場合、通常は onKeyDown イベントリスナーを使用します。 しかし、twenty-front では、同時にマウントされている異なるコンポーネントで使用される同じホットキーの間で競合が生じることがあります。 例えば、Enterキーをリッスンするページと、Enterキーをリッスンするモーダル、さらにそのモーダル内のSelectコンポーネントもEnterキーをリッスンしている場合、全てが同時にマウントされると競合が生じる可能性があります。

useScopedHotkeys フック

この問題を解決するために、どのような競合もなくホットキーをリッスンすることを可能にするカスタムフックがあります。 コンポーネント内に配置すると、コンポーネントがマウントされ、指定されたホットキースコープがアクティブなときだけホットキーをリッスンします。

実際にホットキーをリッスンする方法は?

ホットキーをリッスンするための設定には2つのステップがあります:
  1. ホットキーをリッスンするホットキースコープを設定します
  2. ホットキーをリッスンするために useScopedHotkeys フックを使用します
他のUI要素(例:左側のメニューやコマンドメニュー)もホットキーをリッスンする可能性があるため、ホットキースコープの設定は単純なページでも必要です。

ホットキーのユースケース

一般的に、ホットキーが必要となる動作は2つあります:
  1. ページにマウントされたコンポーネントで
  2. ユーザーのアクションでフォーカスをとるモーダルタイプのコンポーネントで
2番目のユースケースは再帰的に発生します:例えばモーダル内のドロップダウン。

ページ内でホットキーをリッスン

例:
const PageListeningEnter = () => {
  const {
    setHotkeyScopeAndMemorizePreviousScope,
    goBackToPreviousHotkeyScope,
  } = usePreviousHotkeyScope();

  // 1. Set the hotkey scope in a useEffect
  useEffect(() => {
    setHotkeyScopeAndMemorizePreviousScope(
      ExampleHotkeyScopes.ExampleEnterPage,
    );

    // Revert to the previous hotkey scope when the component is unmounted
    return () => {
      goBackToPreviousHotkeyScope();
    };
  }, [goBackToPreviousHotkeyScope, setHotkeyScopeAndMemorizePreviousScope]);

  // 2. Use the useScopedHotkeys hook
  useScopedHotkeys(
    Key.Enter,
    () => {
      // Some logic executed on this page when the user presses Enter
      // ...
    },
    ExampleHotkeyScopes.ExampleEnterPage,
  );

  return <div>My page that listens for Enter</div>;
};

モーダルタイプのコンポーネントでホットキーをリッスン

この例では、親にモーダルを閉じるように指示するためにEscapeキーをリッスンするモーダルコンポーネントを使用します。 ここで、ユーザーの操作がスコープを変更します。
const ExamplePageWithModal = () => {
  const [showModal, setShowModal] = useState(false);

  const {
    setHotkeyScopeAndMemorizePreviousScope,
    goBackToPreviousHotkeyScope,
  } = usePreviousHotkeyScope();

  const handleOpenModalClick = () => {
    // 1. Set the hotkey scope when user opens the modal
    setShowModal(true);
    setHotkeyScopeAndMemorizePreviousScope(
      ExampleHotkeyScopes.ExampleModal,
    );
  };

  const handleModalClose = () => {
    // 1. Revert to the previous hotkey scope when the modal is closed
    setShowModal(false);
    goBackToPreviousHotkeyScope();
  };

  return <div>
    <h1>My page with a modal</h1>
    <button onClick={handleOpenModalClick}>Open modal</button>
    {showModal && <MyModalComponent onClose={handleModalClose} />}
  </div>;
};
その後、モーダルコンポーネント内で:
const MyDropdownComponent = ({ onClose }: { onClose: () => void }) => {
  // 2. Use the useScopedHotkeys hook to listen for Escape.
  // Note that escape is a common hotkey that could be used by many other components
  // So it's important to use a hotkey scope to avoid conflicts
  useScopedHotkeys(
    Key.Escape,
    () => {
      onClose()
    },
    ExampleHotkeyScopes.ExampleModal,
  );

  return <div>My modal component</div>;
};
モント/アンマウントでのuseEffectだけで競合を避けるには不十分なことがある場合にこのパターンを使用することが重要です。 これらの競合はデバッグが困難で、useEffectsでよく発生することがあります。

ホットキースコープとは?

ホットキースコープは、ホットキーがアクティブなコンテキストを表す文字列です。 通常、enumとしてエンコードされます。 通常、enumとしてエンコードされます。 ホットキースコープを変更すると、このスコープをリッスンしているホットキーが有効になり、他のスコープをリッスンしているホットキーが無効になります。 一度に1つのスコープしか設定できません。 例として、各ページのホットキースコープはPageHotkeyScope enumで定義されています:
export enum PageHotkeyScope {
  Settings = 'settings',
  CreateWorkspace = 'create-workspace',
  SignInUp = 'sign-in-up',
  CreateProfile = 'create-profile',
  PlanRequired = 'plan-required',
  ShowPage = 'show-page',
  PersonShowPage = 'person-show-page',
  CompanyShowPage = 'company-show-page',
  CompaniesPage = 'companies-page',
  PeoplePage = 'people-page',
  OpportunitiesPage = 'opportunities-page',
  ProfilePage = 'profile-page',
  WorkspaceMemberPage = 'workspace-member-page',
  TaskPage = 'task-page',
}
内部的には、現在選択されているスコープはアプリケーション全体で共有されるRecoilステートに格納されています:
export const currentHotkeyScopeState = createState<HotkeyScope>({
  key: 'currentHotkeyScopeState',
  defaultValue: INITIAL_HOTKEYS_SCOPE,
});
しかし、このRecoilステートは手動で処理しないでください! 次のセクションでその使用方法を見ていきます。 次のセクションでその使用方法を見ていきます。

内部的にはどう機能しているのか?

react-hotkeys-hookの上に薄いラッパーを作成し、より効率的にし、不必要な再レンダリングを避けます。 また、ホットキースコープの状態を処理し、アプリケーション全体で利用できるRecoilステートを作成しました。