پرتوپرتو

پروایدر کلیدمیانبر (HotkeyProvider)

registry سراسری برای ترکیب‌های keyboard — یک listener سراسری، cheatsheet readback، disable سراسری

معرفی

HotkeyProvider یک registry سراسری برای ترکیب‌های keyboard است. یک‌بار wrap کنید، descendantها با useHotkey() ترکیب‌ها را register می‌کنند، و هر UI (CommandPalette، "?" cheatsheet، صفحه‌ی help) لیست فعال را با useHotkeyRegistry() می‌خواند.

چه زمانی استفاده کنیم:

  • اپ شما بیش از ۲-۳ ترکیب keyboard دارد (Cmd+K برای palette، Esc برای modal، /? برای cheatsheet…)
  • می‌خواهید یک صفحه help که همه‌ی شورتکات‌های فعال را لیست می‌کند داشته باشید
  • می‌خواهید disable سراسری هنگام modal باز ی full-screen tutorial داشته باشید
  • می‌خواهید جلوی هم‌پوشانی شورتکات را با dedupe-by-id بگیرید

چه زمانی استفاده نکنیم:

  • اگر اپ شما فقط mod+k برای CommandPalette دارد → از useHotkeys معمولی استفاده کنید (registry overhead لازم نیست)
  • اگر فقط شورتکات scoped درون یک panel/modal دارید → useHotkeys با target ref scoped می‌کند

اختیاری است — useHotkey() خارج از provider silently no-op می‌کند، پس می‌توانید provider را به‌صورت تدریجی اضافه کنید بدون refactor sweep.

استفاده پایه

import { HotkeyProvider, useHotkey, useHotkeyRegistry } from '@parto-system-design/ui'

function App() {
  return (
    <HotkeyProvider>
      <Layout />
    </HotkeyProvider>
  )
}

function GlobalShortcuts({ onOpenPalette, onSearchFocus }) {
  useHotkey('palette', 'mod+k', onOpenPalette, {
    description: 'باز کردن پالت دستور',
    group: 'سراسری',
  })
  useHotkey('search', '/', onSearchFocus, {
    description: 'فوکوس جستجو',
    group: 'سراسری',
  })
  return null
}

نمایش cheatsheet

import { useHotkeyRegistry } from '@parto-system-design/ui'

function ShortcutsCheatsheet() {
  const { hotkeys } = useHotkeyRegistry()
  // Group by `group` field for nicer rendering
  const groups = hotkeys.reduce<Record<string, typeof hotkeys>>((acc, h) => {
    const key = h.group ?? 'سایر'
    ;(acc[key] ??= []).push(h)
    return acc
  }, {})

  return Object.entries(groups).map(([group, list]) => (
    <section key={group}>
      <h3>{group}</h3>
      <dl>
        {list.map((h) => (
          <React.Fragment key={h.id}>
            <dt>{Array.isArray(h.combo) ? h.combo[0] : h.combo}</dt>
            <dd>{h.description}</dd>
          </React.Fragment>
        ))}
      </dl>
    </section>
  ))
}

useHotkeyRegistry() خارج از provider لیست خالی می‌دهد — پس Cheatsheet هرکجا باشد crash نمی‌کند.

Disable سراسری

const [tutorialOpen, setTutorialOpen] = React.useState(true)

// In tutorial mode، همه‌ی hotkeys را disable کن تا با کلاس آموزش تداخل نداشته باشند
<HotkeyProvider enabled={!tutorialOpen}>
  <App />
</HotkeyProvider>

enabled={false} listener‌ها را متوقف می‌کند ولی registry list دست‌نخورده می‌ماند (cheatsheet کار می‌کند).

ویژگی‌های کلیدی پیاده‌سازی

  • Two-context split داخلی: register fn برای مادام‌العمر provider stable است، entries list reactive. این از infinite-loop trap که consumer effects را در یک Context واحد می‌کشد جلوگیری می‌کند (test-discovered).
  • Latest-handler ref: می‌توانید inline arrow هر render پاس بدهید — با handlerRef بدون re-register هر تغییر متوجه می‌شود.
  • Dedupe by id: useHotkey('palette', ...) در یک کامپوننت دیگر با همان id، entry قبلی را replace می‌کند نه duplicate.
  • یک useHotkeys per entry: parsing key، ignore-when-typing، و OR-array logic در hook موجود می‌ماند — این provider صرفاً orchestrate می‌کند.

جدول ویژگی‌ها

HotkeyProvider

Prop

Type

useHotkey(id, combo, handler, options?)

Prop

Type

useHotkeyRegistry(){ hotkeys: RegisteredHotkey[] }

برمی‌گرداند آرایه‌ی فعال entries در ترتیب register-time. خارج از provider لیست خالی برمی‌گرداند.

دسترسی‌پذیری

  • ترکیب‌های ثبت‌شده باید همگی discoverable باشند — یک cheatsheet (? یا منوی Help) که از useHotkeyRegistry() می‌خواند، شرط استفاده صحیح است؛ keyboard-only userها بدون کشف، شورتکات‌ها را پیدا نمی‌کنند.
  • در نمایش shortcut از <Kbd> + formatHotkey('mod+k') استفاده کنید تا روی macOS «⌘K» و روی Windows/Linux «Ctrl+K» مناسب رندر شود.
  • قاعده‌ی ignore-when-typing در useHotkeys پیاده‌سازی شده — ترکیب‌ها وقتی فوکوس روی input/textarea/contentEditable است fire نمی‌شوند تا تایپ کاربر را قطع نکنند.
  • هنگام باز شدن modal یا full-screen tutorial از enabled={false} استفاده کنید تا shortcutهای سراسری با focus-trap modal تداخل نکنند.
  • handler هیچ ARIA live region یا announcement خودکار ارائه نمی‌کند؛ اگر یک shortcut state قابل‌توجهی را تغییر می‌دهد (مثل toggling sidebar)، خود consumer باید aria-live مناسب اضافه کند.

راهنمای استفاده

بکنید

  • برای هر app یک HotkeyProvider در ریشه‌ی Layout قرار دهید — provider light است (هیچ DOM رندر نمی‌کند) - id ها را به‌صورت feature-action نام‌گذاری کنید (palette، search، nav-back) — در replace + dedupe کمک می‌کند - برای modal-scoped shortcuts از useHotkeys (نه useHotkey) استفاده کنید با target={modalRef} — registry سراسری برای global shortcuts است نه local

نکنید

  • id ها را dynamic نسازید (مثل palette-${counter}) — هر بار register می‌کند نه replace - hook را در render شرطی نگذارید — قاعده‌ی hooks - dispatcher را override نکنید مگر دلیل خاص — useHotkeys logic در یک جا تست‌شده است

کامپوننت‌های مرتبط

  • اگر فقط یک hotkey globaluseHotkeys (بدون registry overhead)
  • برای palette commandCommandPalette می‌تواند useHotkeyRegistry() را برای item list استفاده کند
  • توالی keyboard interaction در یک scopeuseHotkeys({ target: ref })
  • نمایش shortcut در buttonKbd + formatHotkey('mod+k')