Files
upstream-mirror e7da97194a
Some checks failed
App Prod-Parity E2E Dispatch / dispatch (push) Failing after 10s
CD Storybook UI / build (push) Failing after 3m25s
CD Storybook UI / dispatch (push) Has been skipped
CD deploy main / website-changed-files (push) Successful in 18m8s
CI Agent Skills / changed-files-check (push) Successful in 18m9s
CI Create App E2E minimal / changed-files-check (push) Successful in 24s
CI Create App / changed-files-check (push) Successful in 26s
CI Docs / changed-files-check (push) Successful in 26s
CI Emails / changed-files-check (push) Successful in 28s
CI Example App Hello World / changed-files-check (push) Successful in 23s
CI Front / changed-files-check (push) Has been skipped
CI Example App Postcard / changed-files-check (push) Successful in 27s
CI UI / changed-files-check (push) Has been skipped
CI Utils Test / changed-files-check (push) Successful in 45s
CI Twenty Apps / discover (push) Successful in 1m3s
CD deploy main / deploy-main (push) Failing after 3s
CI Agent Skills / agent-skills-validate (push) Has been skipped
CI Create App / create-app-test (lint) (push) Has been skipped
CI Emails / emails-test (push) Has been skipped
CI Front / front-task (lingui:compile) (push) Has been skipped
CI Front / front-task (lint) (push) Has been skipped
CI Example App Postcard / example-app-postcard (push) Has been skipped
CI UI / ui-task (lint) (push) Has been skipped
CI UI / ui-task (pack-size) (push) Has been skipped
CI UI / ui-task (size) (push) Has been skipped
CI UI / ui-task (test) (push) Has been skipped
CI UI / ui-task (typecheck) (push) Has been skipped
CI Utils Test / utils-test (push) Has been skipped
CI Twenty Apps / ${{ matrix.app.name }} (push) Has been skipped
CI Emails / ci-emails-status-check (push) Successful in 2s
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Successful in 2s
CI Example App Postcard / ci-example-app-postcard-status-check (push) Successful in 3s
CI UI / ui-sb-test (push) Has been skipped
CI Utils Test / ci-utils-test-status-check (push) Successful in 2s
CI Twenty Apps / ci-twenty-apps-status-check (push) Successful in 2s
CI UI / ci-ui-status-check (push) Successful in 2s
CI Desktop / desktop (push) Failing after 1m8s
Push docs to Crowdin / Push documentation to Crowdin (push) Failing after 55s
Push translations to Crowdin / Extract and upload translations (push) Failing after 5s
Push website translations to Crowdin / Extract and upload website translations (push) Failing after 5s
CI Create App / create-app-test (test) (push) Has been skipped
CI Create App / create-app-test (typecheck) (push) Has been skipped
CI Docs / docs-lint (push) Has been skipped
CI Example App Hello World / example-app-hello-world (push) Has been skipped
CI Front / front-task (lingui:extract) (push) Has been skipped
CI Front / front-task (test) (push) Has been skipped
CI Front / front-task (typecheck) (push) Has been skipped
CI UI / ui-sb-build (push) Failing after 1m32s
CI Twenty Apps / ${{ matrix.app.name }} (${{ matrix.server-source }}) (push) Has been skipped
CI Agent Skills / agent-skills-publish (push) Has been skipped
CI Create App / ci-create-app-status-check (push) Successful in 2s
CI Agent Skills / agent-skills-remote-source (push) Has been skipped
CI Create App E2E minimal / create-app-e2e-minimal (push) Has been cancelled
CI E2E Main / e2e-test (push) Has been cancelled
CI Front / front-sb-build (push) Has been cancelled
CI Front / front-build (push) Has been cancelled
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Has been cancelled
CI E2E Main / ci-e2e-main-status-check (push) Has been cancelled
CI E2E Main / notify-main-ci-failure (push) Has been cancelled
CI E2E Main / qa-scout-comment (push) Has been cancelled
CI Front / front-sb-test (push) Has been cancelled
CI Front / front-sb-screenshots (push) Has been cancelled
CI Front / ci-front-status-check (push) Has been cancelled
DPA Sub-Processor Sync / sync-subprocessors (push) Failing after 3s
AI Catalog Sync / sync-catalog (push) Failing after 2s
snapshot of https://github.com/twentyhq/twenty.git (main, 2026-09-17; blobs > 131072B omitted)
2026-09-17 23:28:13 +05:00

179 lines
7.1 KiB
Plaintext

---
title: مفاتيح الاختصار
---
## مقدمة
عندما تحتاج إلى الاستماع إلى مفتاح اختصار، فإنه عادةً ما تستخدم مستمع الحدث `onKeyDown`.
في `twenty-front`، قد تواجه تعارضات بين نفس مفاتيح الاختصار المستخدمة في مكونات مختلفة، مركبة في الوقت نفسه.
على سبيل المثال، إذا كان لديك صفحة تستمع لمفتاح Enter ومودال يستمع لمفتاح Enter، ولكن به مكون Select يستمع لمفتاح Enter، فقد تواجه تعارضًا عند تركيب الجميع في الوقت نفسه.
## الخطاف `useScopedHotkeys`
لمعالجة هذه المشكلة، لدينا خطاف مخصص يمكن من الاستماع لمفاتيح الاختصار دون أي تعارض.
تضعه داخل مكوّن، ولن يستمع إلى مفاتيح الاختصار إلا عندما يكون المكوّن مُركّبًا وعندما يكون **نطاق مفاتيح الاختصار** المحدّد نشطًا.
## كيف تستمع إلى مفاتيح الاختصار عمليًا؟
هناك خطوتان متضمنتان في إعداد الاستماع لمفاتيح الاختصار:
1. تعيين [نطاق المفتاح](#what-is-a-hotkey-scope-) الذي سيستمع لمفاتيح الاختصار
2. استخدام الخطاف `useScopedHotkeys` للاستماع لمفاتيح الاختصار
إعداد نطاقات مفاتيح الاختصار ضروري حتى في الصفحات البسيطة، لأن عناصر أخرى في واجهة المستخدم مثل القائمة اليسرى أو قائمة الأوامر قد تستمع أيضًا لمفاتيح الاختصار.
## حالات الاستخدام لمفاتيح الاختصار
بشكل عام، سيكون لديك حالتا استخدام تتطلبان مفاتيح الاختصار:
1. في صفحة أو مكون مركب في صفحة
2. في مكون من نوع مودال يتخذ التركيز بسبب إجراء من المستخدم
يمكن حدوث الحالة الثانية بشكل متكرر: على سبيل المثال، في قائمة منسدلة في مودال.
### الاستماع لمفاتيح الاختصار في صفحة
مثال:
```tsx
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 ليطلب من المكوّن الأب إغلاقه.
هنا يتمثّل تفاعل المستخدم في تغيير النطاق.
```tsx
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>;
};
```
ثم في مكون المودال:
```tsx
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>;
};
```
من المهم استخدام هذا النمط عندما لست متأكدا من أن استخدام تأثير الاستخدام مع التركيب/إلغاء التثبيت سيكون كافيا لتجنب الصراعات.
ويمكن أن يكون من الصعب تصحيح تلك الصراعات، وقد يحدث ذلك في كثير من الأحيان بآثار استعمالية.
## ما هو نطاق المفتاح؟
نطاق المفتاح هو سلسلة تمثل السياق الذي تكون فيه مفاتيح الاختصار نشطة. عادة ما تكون مشفرة كنوع مفصل.
عندما تقوم بتغيير نطاق المفتاح، سيتم تمكين مفاتيح الاختصار المستمعة لهذا النطاق وتعطيل المفاتيح المستمعة لنطاقات أخرى.
يمكنك تعيين نطاق واحد فقط في كل مرة.
على سبيل المثال، يتم تعريف نطاقات مفاتيح الاختصار لكل صفحة في نوع "PageHotkeyScope" المفصل:
```tsx
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',
}
```
داخليًا، يتم تخزين النطاق المحدد حاليًا في ذرة Jotai مشتركة عبر التطبيق:
```tsx
export const currentHotkeyScopeState = createState<HotkeyScope>({
key: 'currentHotkeyScopeState',
defaultValue: INITIAL_HOTKEYS_SCOPE,
});
```
لكن لا يجب التعامل مع هذه الذرة يدويًا! سنرى كيف يمكن استخدامها في القسم التالي.
## كيف يعمل داخليًا؟
قمنا بإنشاء غلاف رقيق فوق [react-hotkeys-hook](https://react-hotkeys-hook.vercel.app/docs/intro) والذي يجعله أكثر كفاءة ويتجنب عمليات إعادة التقديم غير الضرورية.
ونقوم أيضًا بإنشاء ذرة Jotai للتعامل مع حالة نطاق مفاتيح الاختصار وجعلها متاحة في جميع أنحاء التطبيق.