Skip to content

feat(rules): add prefer-keybind-library draft rule#743

Draft
NisargIO wants to merge 4 commits into
mainfrom
cursor/prefer-keybind-library-rule-b5e7
Draft

feat(rules): add prefer-keybind-library draft rule#743
NisargIO wants to merge 4 commits into
mainfrom
cursor/prefer-keybind-library-rule-b5e7

Conversation

@NisargIO

@NisargIO NisargIO commented Jun 8, 2026

Copy link
Copy Markdown
Member

Why

Catches a hand-rolled keyboard shortcut, a keydown/keyup/keypress addEventListener whose handler compares a KeyboardEvent key-identity property to decide which combination fired.

Hand-rolling shortcuts means re-implementing platform meta vs ctrl normalization, scoping, key sequences, text-input exclusion, and the add/remove listener lifecycle by hand, and it's easy to get those wrong. A dedicated library (react-hotkeys-hook and friends) owns all of that.

Before:

useEffect(() => {
  const onKeyDown = (event: KeyboardEvent) => {
    if ((event.metaKey || event.ctrlKey) && event.key === "k") {
      setOpen((value) => !value);
    }
  };
  window.addEventListener("keydown", onKeyDown);
  return () => window.removeEventListener("keydown", onKeyDown);
}, []);

After:

import { useHotkeys } from "react-hotkeys-hook";

useHotkeys("mod+k", () => setOpen((value) => !value));

What changed

  • Added prefer-keybind-library (bucket: architecture, severity warn).
  • Registered as a draft / opt-in rule (defaultEnabled: false), so it ships in the plugin but stays out of the recommended preset until teams opt in via severityControls.
  • Detects addEventListener("keydown" | "keyup" | "keypress", handler) where the handler compares a key-identity property (key, code, keyCode, which, charCode). Comparison forms supported: equality (event.key === "Escape"), switch (event.code), and membership/string tests (["j","k"].includes(event.key), event.code.startsWith("Arrow")). Resolves the handler inline, through one binding hop, and via destructured / renamed event params (({ key: pressedKey }) =>, const { key } = event).
  • Recommends a keybind library: react-hotkeys-hook by default, or one the file already imports (tinykeys, hotkeys-js, mousetrap, @mantine/hooks, @github/hotkey, react-hotkeys).
  • Stays quiet for the look-alikes a keybind library does not replace:
    • Input-modality detection (focus-visible polyfills) that reads only modifier flags (event.metaKey || event.altKey) with no key-identity comparison.
    • Tab focus trapping whose only key check is Tab (event.key === "Tab", === KEYS.TAB, keyCode === 9).
    • JSX onKeyDown handlers (element-level a11y), non-keyboard listeners, computed event names, unresolved/imported handlers, shadowed inner params, and .key access on unrelated objects.
  • Adds keyboard/keybind constants (constants/dom.ts, constants/library.ts), 31 co-located unit tests, and 5 end-to-end regression tests.

Eval results

Validated against 7 large open-source React codebases (scoped to files with keyboard addEventListener calls, rule force-enabled, output filtered to this rule):

Check Result
Repos scanned 7 (excalidraw, cal.com, outline, lobe-chat, tldraw, mui, supabase)
Target rule prefer-keybind-library
Diagnostics 44
False positives found 0 (after refinement; an earlier draft surfaced 4, all now exempted)

The earlier draft flagged 57; manual inspection found 4 false positives (MUI useIsFocusVisible modality detection, MUI FocusTrap, Excalidraw Dialog, and tldraw A11y Tab focus trapping). The detector was tightened to require a real key comparison and exempt Tab-only focus trapping, dropping those to 0 while keeping every true positive (Escape-to-close, Cmd+K palettes, single-key shortcuts, useKeyPress-style hooks).

Test plan

  • vp test run on prefer-keybind-library.test.ts (31 unit cases: positives, alias/destructure resolution, library suggestion, and false-positive traps).
  • vp test run on tests/regressions/prefer-keybind-library.test.ts (5 end-to-end cases through the real oxlint pipeline).
  • tsc --noEmit for oxlint-plugin-react-doctor (passes).
  • vp lint + vp fmt --check on all touched files (pass).
  • pnpm gen registry regeneration (committed).

Note: the repo's broader plugin test run has ~38 pre-existing failures in unrelated react-builtins rules (jsx-no-new-*, no-multi-comp, …) that also fail on a clean main checkout in this environment; they are not affected by this change.

Open in Web Open in Cursor 

cursoragent and others added 4 commits June 8, 2026 06:35
Co-authored-by: Nisarg Patel <NisargIO@users.noreply.github.com>
Flags hand-rolled keyboard shortcuts (addEventListener('keydown'|'keyup'|'keypress')
whose handler inspects a KeyboardEvent shortcut property) and recommends a dedicated
keybind library (react-hotkeys-hook by default, or one already imported). Registered
as defaultEnabled: false (opt-in draft).

Co-authored-by: Nisarg Patel <NisargIO@users.noreply.github.com>
Co-authored-by: Nisarg Patel <NisargIO@users.noreply.github.com>
…arison

Require the handler to COMPARE a KeyboardEvent key-identity property
(key/code/keyCode/which/charCode) rather than merely read one, and exempt
Tab-only focus trapping. Eliminates false positives found while validating
against open-source repos (MUI useIsFocusVisible modality detection and
FocusTrap/Excalidraw/tldraw Tab focus trapping). Adds regression tests and
a changeset.

Co-authored-by: Nisarg Patel <NisargIO@users.noreply.github.com>
@pkg-pr-new

pkg-pr-new Bot commented Jun 8, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/eslint-plugin-react-doctor@743
npm i https://pkg.pr.new/oxlint-plugin-react-doctor@743
npm i https://pkg.pr.new/react-doctor@743

commit: 4c53bcd

@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

No React Doctor issues found. 🎉

Reviewed by React Doctor for commit 4c53bcd.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants