如何在 React 中实现键盘快捷键管理器以提升用户体验

88次阅读
没有评论

你可能见过一些专业的 Web 应用,例如 Google Sheet、Figma 或其他功能丰富的应用。键盘交互可以提升这些应用的用户体验。你也可以在你的 React 应用中添加这些键盘交互。在本文中,我将尝试解释如何在你的应用中构建一个更简洁、开发者友好且符合最佳实践的键盘交互。

方法:中央快捷方式管理器

假设你的应用有一个智能的快捷键管理器,可以管理所有键盘快捷键。它的工作很简单:

  1. 聆听应用程序中任意位置按下的每个按键
  2. 了解所有可用的快捷键及其作用
  3. 忽略在文本(或可编辑)字段中按下的键(关键细节!)
  4. 立即触发正确的操作

这就是核心思想。你不再需要将数十个独立的按键监听器分散在各个组件中,而是拥有一个集中的大脑,可以集中管理快捷键。这让一切变得简单:避免冲突、管理内存,并保持代码简洁。

实施概览

此实现将包含两个主要部分或文件:

  1. 上下文提供程序 (ShortcutProvider):此组件包装了整个应用程序。它成为保存所有快捷方式及其功能的中央注册表。
  2. 自定义钩子 (useShortcuts):这是任何组件与提供程序通信的方式。它是一个简单的 API,包含两个函数:注册和注销。简单来说,就像这样:“嘿,管理员,请帮我注册这个快捷方式,这是我触发此快捷方式时要执行的功能”或“我已完成此快捷方式的使用,请注销它”。

这是使用此模式的美妙之处:

// Deep inside any component, you can easily add a shortcut.
function SaveButton() {const { register} = useShortcuts();

  // Register 'Ctrl+S' to save the document
  useEffect(() => {const handleSave = () => {/* Save logic here */};
    register({key: 's', modifiers: ['Ctrl'] }, handleSave);
    return () => unregister({ key: 's', modifiers: ['Meta']  }); // Cleanup!
  }, []);

简单至极,组件无需了解全局事件监听器,你还可以在“ShortcutProvider”中添加更严谨的冲突逻辑。它只需请求用户组件注册其意图即可。其余部分由中央管理器处理。

为什么这是一个更好的方法

  • 清除代码
  • 使用 最佳实践,结合上下文来响应钩子模式
  • 团队友好,因为它为团队中的每个人创建了一种标准、简单的方法来添加和删除快捷方式,而不会互相干扰。

以下是完整代码:

ShortcutsProvider.tsx

"use client";
import React, {createContext, useEffect, useRef} from "react";
import {
  Modifier,
  Shortcut,
  ShortcutHandler,
  ShortcutRegistry,
  ShortcutsContextType,
} from "./types";

export const ShortcutsContext = createContext<ShortcutsContextType>({register: () => {},
  unregister: () => {},
});

const normalizeShortcut = (shortcut: Shortcut): string => {const mods = shortcut.modifiers?.slice().sort() || []; // Sort alphabetically
  const key = shortcut.key.toUpperCase(); // Normalize case
  return [...mods, key].join("+");
};

const ShortcutProvider = ({children}: {children: React.ReactNode}) => {
  // Registry for shortcut with key as shortcut combination and value as the handler
  const ShortcutRegisteryRef = useRef<ShortcutRegistry>(new Map());

  const register = (
    shortcut: Shortcut,
    handler: ShortcutHandler,
    override = false
  ) => {
    const ShortcutRegistery = ShortcutRegisteryRef.current;
    // before proceeding with logic let normalized the key
    // first we sort the modifiers from shortcut alphabetically
    // then make key uppercase for consistency

    const modifiers = shortcut.modifiers
      ? shortcut.modifiers.slice().sort()
      : [];
    const key = shortcut.key.toUpperCase();

    const normalizedKey = [...modifiers, key].join("+");

    // here checking for conflicts
    if (ShortcutRegistery.has(normalizedKey) && !override) {
      console.warn(`Conflict: "${normalizedKey}" is already registered for shortcut. Use override=true to replace or handle conflict.`
      );
      return;
    }

    ShortcutRegistery.set(normalizedKey, handler);
  };

  const unregister = (shortcut: Shortcut) => {
    // again normaizing the key, we are repeating this code so better to make function out of this
    const modifiers = shortcut.modifiers
      ? shortcut.modifiers.slice().sort()
      : [];
    const key = shortcut.key.toUpperCase();

    const normalizedKey = [...modifiers, key].join("+");
    ShortcutRegisteryRef.current.delete(normalizedKey);
  };

  const handleKeyDown = (event: KeyboardEvent) => {
    const target = event.target as HTMLElement;
    // this check is important as without this we wont be able to write in these inputs
    if (
      target.tagName === "INPUT" ||
      target.tagName === "TEXTAREA" ||
      target.isContentEditable
    ) {return;}

    const modifiers: Modifier[] = [];

    if (event.ctrlKey) modifiers.push("Ctrl");
    if (event.altKey) modifiers.push("Alt");
    if (event.shiftKey) modifiers.push("Shift");
    if (event.metaKey) modifiers.push("Meta");

    const key = event.key.toUpperCase();
    const normalizedKey = [...modifiers.sort(), key].join("+");

    const handler = ShortcutRegisteryRef.current.get(normalizedKey);

    if (handler) {event.preventDefault();
      handler(event);
    }
  };

  useEffect(() => {window.addEventListener("keydown", handleKeyDown);
    return () => window.removeEventListener("keydown", handleKeyDown);
  }, []);

  return (<ShortcutsContext.Provider value={{ register, unregister}}>
      {children}
    </ShortcutsContext.Provider>
  );
};

export default ShortcutProvider;

useShortcuts.tsx

import {useContext} from "react";
import {ShortcutsContext} from "./ShortcutsProvider";

const useShortcuts = () => {const shortcutContext = useContext(ShortcutsContext);

  if (!shortcutContext) {console.error("Shortcut context must be wrapped inside Shortcut provider");
  }

  return shortcutContext;
};

export default useShortcuts;

types.ts

export type Modifier = string;
export type Key = string;

export interface Shortcut {
  key: Key;
  modifiers?: string[];
}

export type ShortcutHandler = (e: KeyboardEvent) => void;
export interface ShortcutsContextType {register: (shortcut: Shortcut, handler: ShortcutHandler) => void;
  unregister: (shortcut: Shortcut) => void;
}

export type ShortcutRegistry = Map<string, ShortcutHandler>;

主应用程序组件(此处使用 Next js)

export default function RootLayout({children,}: Readonly<{children: React.ReactNode;}>) {
  return (
    <html lang="en">
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        <ShortcutProvider>{children}</ShortcutProvider>
      </body>
    </html>
  );
}

您可以在 GitHub 上找到带有可行示例的完整解决方案。

正文完
 0
Pa2sw0rd
版权声明:本站原创文章,由 Pa2sw0rd 于2025-08-30发表,共计4552字。
转载说明:Unless otherwise specified, all articles are published by cc-4.0 protocol. Please indicate the source of reprint.
评论(没有评论)