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
179 lines
6.3 KiB
Plaintext
179 lines
6.3 KiB
Plaintext
---
|
|
title: Raccourcis clavier
|
|
---
|
|
|
|
## Introduction
|
|
|
|
Lorsque vous devez écouter une touche de raccourci, vous utilisez normalement l'événement `onKeyDown`.
|
|
|
|
Cependant, dans `twenty-front`, vous pourriez avoir des conflits entre les mêmes raccourcis utilisés dans différents composants, montés en même temps.
|
|
|
|
Par exemple, si vous avez une page qui écoute la touche Entrée, et une fenêtre modale qui écoute aussi la touche Entrée, avec un composant Select à l'intérieur de cette fenêtre qui écoute également la touche Entrée, vous risquez d'avoir un conflit lorsque tous sont montés en même temps.
|
|
|
|
## Le hook `useScopedHotkeys`
|
|
|
|
Pour résoudre ce problème, nous avons un hook personnalisé qui permet d'écouter les raccourcis sans aucun conflit.
|
|
|
|
Vous l'insérez dans un composant et il écoutera les raccourcis uniquement lorsque le composant est monté ET lorsque le **périmètre du raccourci** spécifié est actif.
|
|
|
|
## Comment écouter les raccourcis en pratique ?
|
|
|
|
Deux étapes sont nécessaires pour configurer l'écoute des raccourcis :
|
|
|
|
1. Définir le [périmètre du raccourci](#what-is-a-hotkey-scope-) qui écoutera les raccourcis
|
|
2. Utiliser le hook `useScopedHotkeys` pour écouter les raccourcis
|
|
|
|
La configuration des périmètres de raccourcis est nécessaire même sur des pages simples, car d'autres éléments de l'interface utilisateur comme le menu de gauche ou le menu de commandes pourraient également écouter les raccourcis.
|
|
|
|
## Cas d'utilisation des raccourcis
|
|
|
|
En général, vous aurez deux cas d'utilisation nécessitant des raccourcis :
|
|
|
|
1. Dans une page ou un composant monté dans une page
|
|
2. Dans un composant de type modal qui prend le focus à la suite d'une action utilisateur
|
|
|
|
Le deuxième cas d'utilisation peut se produire de manière récursive : un menu déroulant dans une fenêtre modale par exemple.
|
|
|
|
### Écouter les raccourcis dans une page
|
|
|
|
Exemple :
|
|
|
|
```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>;
|
|
};
|
|
```
|
|
|
|
### Écouter les raccourcis dans un composant de type modal
|
|
|
|
Pour cet exemple, nous allons utiliser un composant modal qui écoute la touche Échap pour informer son parent de le fermer.
|
|
|
|
Ici, l'interaction utilisateur change le périmètre.
|
|
|
|
```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>;
|
|
};
|
|
```
|
|
|
|
Ensuite, dans le composant modal :
|
|
|
|
```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>;
|
|
};
|
|
```
|
|
|
|
Il est important d'utiliser ce masque lorsque vous n'êtes pas sûr que le simple fait d'utiliser un useEffect avec mount/unmount sera suffisant pour éviter les conflits.
|
|
|
|
Ces conflits peuvent être difficiles à déboger, et cela peut se produire plus souvent avec useEffects.
|
|
|
|
## Qu'est-ce qu'un périmètre de raccourci ?
|
|
|
|
Un périmètre de raccourci est une chaîne de caractères qui représente un contexte dans lequel les raccourcis sont actifs. Il est généralement encodé sous forme d'enum.
|
|
|
|
Lorsque vous modifiez le périmètre de raccourci, les raccourcis qui écoutent ce périmètre seront activés et ceux qui écoutent d'autres périmètres seront désactivés.
|
|
|
|
Vous ne pouvez définir qu'un seul périmètre à la fois.
|
|
|
|
Par exemple, les périmètres de raccourcis pour chaque page sont définis dans l'`enum 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',
|
|
}
|
|
```
|
|
|
|
En interne, le périmètre actuellement sélectionné est stocké dans un atome Jotai qui est partagé dans toute l'application :
|
|
|
|
```tsx
|
|
export const currentHotkeyScopeState = createState<HotkeyScope>({
|
|
key: 'currentHotkeyScopeState',
|
|
defaultValue: INITIAL_HOTKEYS_SCOPE,
|
|
});
|
|
```
|
|
|
|
Mais cet atome ne doit jamais être manipulé manuellement ! Nous verrons comment l'utiliser dans la prochaine section.
|
|
|
|
## Comment cela fonctionne-t-il en interne ?
|
|
|
|
Nous avons créé un léger emballage au-dessus de [react-hotkeys-hook](https://react-hotkeys-hook.vercel.app/docs/intro) qui le rend plus performant et évite les rendus inutiles.
|
|
|
|
Nous créons également un atome Jotai pour gérer l'état du périmètre des raccourcis et le rendre disponible partout dans l'application.
|