diff --git a/front/src/modules/keyboard-shortcut-menu/components/KeyboardShortcutMenu.tsx b/front/src/modules/keyboard-shortcut-menu/components/KeyboardShortcutMenu.tsx
new file mode 100644
index 000000000..0ae8bb2fb
--- /dev/null
+++ b/front/src/modules/keyboard-shortcut-menu/components/KeyboardShortcutMenu.tsx
@@ -0,0 +1,47 @@
+import { useRecoilValue } from 'recoil';
+
+import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
+import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
+
+import {
+ keyboardShortcutsGeneral,
+ keyboardShortcutsTable,
+} from '../constants/keyboardShortcuts';
+import { useKeyboardShortcutMenu } from '../hooks/useKeyboardShortcutMenu';
+import { isKeyboardShortcutMenuOpenedState } from '../states/isKeyboardShortcutMenuOpenedState';
+
+import { KeyboardMenuDialog } from './KeyboardShortcutMenuDialog';
+import { KeyboardMenuGroup } from './KeyboardShortcutMenuGroup';
+import { KeyboardMenuItem } from './KeyboardShortcutMenuItem';
+
+export const KeyboardShortcutMenu = () => {
+ const { toggleKeyboardShortcutMenu } = useKeyboardShortcutMenu();
+ const isKeyboardShortcutMenuOpened = useRecoilValue(
+ isKeyboardShortcutMenuOpenedState,
+ );
+ useScopedHotkeys(
+ 'shift+?,meta+?,esc',
+ () => {
+ toggleKeyboardShortcutMenu();
+ },
+ AppHotkeyScope.KeyboardShortcutMenu,
+ [toggleKeyboardShortcutMenu],
+ );
+
+ return (
+ isKeyboardShortcutMenuOpened && (
+
+
+ {keyboardShortcutsTable.map((TableShortcut) => (
+
+ ))}
+
+
+ {keyboardShortcutsGeneral.map((GeneralShortcut) => (
+
+ ))}
+
+
+ )
+ );
+};
diff --git a/front/src/modules/keyboard-shortcut-menu/components/KeyboardShortcutMenuDialog.tsx b/front/src/modules/keyboard-shortcut-menu/components/KeyboardShortcutMenuDialog.tsx
new file mode 100644
index 000000000..c185d7c8e
--- /dev/null
+++ b/front/src/modules/keyboard-shortcut-menu/components/KeyboardShortcutMenuDialog.tsx
@@ -0,0 +1,28 @@
+import { IconX } from '@/ui/display/icon';
+import { IconButton } from '@/ui/input/button/components/IconButton';
+
+import {
+ StyledContainer,
+ StyledDialog,
+ StyledHeading,
+} from './KeyboardShortcutMenuStyles';
+
+type KeyboardMenuDialogProps = {
+ onClose: () => void;
+ children: React.ReactNode | React.ReactNode[];
+};
+
+export const KeyboardMenuDialog = ({
+ onClose,
+ children,
+}: KeyboardMenuDialogProps) => {
+ return (
+
+
+ Keyboard shortcuts
+
+
+ {children}
+
+ );
+};
diff --git a/front/src/modules/keyboard-shortcut-menu/components/KeyboardShortcutMenuGroup.tsx b/front/src/modules/keyboard-shortcut-menu/components/KeyboardShortcutMenuGroup.tsx
new file mode 100644
index 000000000..b18e5f5ab
--- /dev/null
+++ b/front/src/modules/keyboard-shortcut-menu/components/KeyboardShortcutMenuGroup.tsx
@@ -0,0 +1,18 @@
+import { StyledGroup, StyledGroupHeading } from './KeyboardShortcutMenuStyles';
+
+type KeyboardMenuGroupProps = {
+ heading: string;
+ children: React.ReactNode | React.ReactNode[];
+};
+
+export const KeyboardMenuGroup = ({
+ heading,
+ children,
+}: KeyboardMenuGroupProps) => {
+ return (
+
+ {heading}
+ {children}
+
+ );
+};
diff --git a/front/src/modules/keyboard-shortcut-menu/components/KeyboardShortcutMenuItem.tsx b/front/src/modules/keyboard-shortcut-menu/components/KeyboardShortcutMenuItem.tsx
new file mode 100644
index 000000000..8bcf3f081
--- /dev/null
+++ b/front/src/modules/keyboard-shortcut-menu/components/KeyboardShortcutMenuItem.tsx
@@ -0,0 +1,34 @@
+import {
+ StyledItem,
+ StyledShortcutKey,
+ StyledShortcutKeyContainer,
+} from '@/keyboard-shortcut-menu/components/KeyboardShortcutMenuStyles';
+import { Shortcut } from '@/keyboard-shortcut-menu/types/Shortcut';
+
+type KeyboardMenuItemProps = {
+ shortcut: Shortcut;
+};
+
+export const KeyboardMenuItem = ({ shortcut }: KeyboardMenuItemProps) => {
+ return (
+
+ {shortcut.label}
+ {shortcut.secondHotKey ? (
+ shortcut.areSimultaneous ? (
+
+ {shortcut.firstHotKey}
+ {shortcut.secondHotKey}
+
+ ) : (
+
+ {shortcut.firstHotKey}
+ then
+ {shortcut.secondHotKey}
+
+ )
+ ) : (
+ {shortcut.firstHotKey}
+ )}
+
+ );
+};
diff --git a/front/src/modules/keyboard-shortcut-menu/components/KeyboardShortcutMenuStyles.tsx b/front/src/modules/keyboard-shortcut-menu/components/KeyboardShortcutMenuStyles.tsx
new file mode 100644
index 000000000..0e5c1d94f
--- /dev/null
+++ b/front/src/modules/keyboard-shortcut-menu/components/KeyboardShortcutMenuStyles.tsx
@@ -0,0 +1,88 @@
+import styled from '@emotion/styled';
+
+import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
+
+export const StyledDialog = styled.div`
+ background: ${({ theme }) => theme.background.primary};
+ border-radius: ${({ theme }) => theme.border.radius.md};
+ box-shadow: ${({ theme }) => theme.boxShadow.strong};
+ font-family: ${({ theme }) => theme.font.family};
+ left: 50%;
+ max-width: 400px;
+ overflow: hidden;
+ padding: 0;
+ padding: ${({ theme }) => theme.spacing(1)};
+ position: fixed;
+ top: 30%;
+ transform: ${() =>
+ useIsMobile() ? 'translateX(-49.5%)' : 'translateX(-50%)'};
+ width: ${() => (useIsMobile() ? 'calc(100% - 40px)' : '100%')};
+ z-index: 1000;
+`;
+
+export const StyledHeading = styled.div`
+ align-items: center;
+ border-bottom: 1px solid ${({ theme }) => theme.border.color.medium};
+ color: ${({ theme }) => theme.font.color.primary};
+ display: flex;
+ flex-direction: row;
+ font-weight: ${({ theme }) => theme.font.weight.semiBold};
+ justify-content: space-between;
+ padding: ${({ theme }) => theme.spacing(3)};
+`;
+
+export const StyledContainer = styled.div`
+ gap: ${({ theme }) => theme.spacing(2)};
+ padding-bottom: ${({ theme }) => theme.spacing(4)};
+ padding-left: ${({ theme }) => theme.spacing(4)};
+ padding-right: ${({ theme }) => theme.spacing(4)};
+ padding-top: ${({ theme }) => theme.spacing(1)};
+`;
+
+export const StyledGroupHeading = styled.label`
+ color: ${({ theme }) => theme.color.gray50};
+ padding-bottom: ${({ theme }) => theme.spacing(1)};
+ padding-top: ${({ theme }) => theme.spacing(4)};
+`;
+
+export const StyledGroup = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: ${({ theme }) => theme.spacing(2)};
+`;
+
+export const StyledItem = styled.div`
+ align-items: center;
+ color: ${({ theme }) => theme.font.color.primary};
+ display: flex;
+ flex-direction: row;
+ font-weight: ${({ theme }) => theme.font.weight.regular};
+ height: 24px;
+ justify-content: space-between;
+`;
+
+export const StyledShortcutKey = styled.div`
+ align-items: center;
+ background-color: ${({ theme }) => theme.background.secondary};
+ border: 1px solid ${({ theme }) => theme.border.color.strong};
+ border-radius: ${({ theme }) => theme.border.radius.sm};
+ box-shadow: ${({ theme }) => theme.boxShadow.underline};
+ color: ${({ theme }) => theme.font.color.tertiary};
+ display: flex;
+ flex-direction: column;
+ font-size: ${({ theme }) => theme.font.size.md};
+ font-weight: ${({ theme }) => theme.font.weight.regular};
+ height: 20px;
+ justify-content: center;
+ padding-left: ${({ theme }) => theme.spacing(1)};
+ padding-right: ${({ theme }) => theme.spacing(1)};
+ text-align: center;
+`;
+
+export const StyledShortcutKeyContainer = styled.div`
+ align-items: center;
+ color: ${({ theme }) => theme.font.color.tertiary};
+ display: flex;
+ flex-direction: row;
+ gap: ${({ theme }) => theme.spacing(1)};
+`;
diff --git a/front/src/modules/keyboard-shortcut-menu/constants/keyboardShortcuts.ts b/front/src/modules/keyboard-shortcut-menu/constants/keyboardShortcuts.ts
new file mode 100644
index 000000000..36f4239d1
--- /dev/null
+++ b/front/src/modules/keyboard-shortcut-menu/constants/keyboardShortcuts.ts
@@ -0,0 +1,39 @@
+import { Shortcut, ShortcutType } from '../types/Shortcut';
+
+export const keyboardShortcutsTable: Shortcut[] = [
+ {
+ label: 'Move right',
+ type: ShortcutType.Table,
+ firstHotKey: '→',
+ areSimultaneous: true,
+ },
+ {
+ label: 'Move left',
+ type: ShortcutType.Table,
+ firstHotKey: '←',
+ areSimultaneous: true,
+ },
+ {
+ label: 'Clear selection',
+ type: ShortcutType.Table,
+ firstHotKey: 'esc',
+ areSimultaneous: true,
+ },
+];
+
+export const keyboardShortcutsGeneral: Shortcut[] = [
+ {
+ label: 'Open search',
+ type: ShortcutType.General,
+ firstHotKey: '⌘',
+ secondHotKey: 'K',
+ areSimultaneous: false,
+ },
+ {
+ label: 'Mark as favourite',
+ type: ShortcutType.General,
+ firstHotKey: '⇧',
+ secondHotKey: 'F',
+ areSimultaneous: false,
+ },
+];
diff --git a/front/src/modules/keyboard-shortcut-menu/hooks/useKeyboardShortcutMenu.ts b/front/src/modules/keyboard-shortcut-menu/hooks/useKeyboardShortcutMenu.ts
new file mode 100644
index 000000000..347e40db0
--- /dev/null
+++ b/front/src/modules/keyboard-shortcut-menu/hooks/useKeyboardShortcutMenu.ts
@@ -0,0 +1,47 @@
+import { useRecoilState, useRecoilValue } from 'recoil';
+
+import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
+import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
+
+import { isKeyboardShortcutMenuOpenedState } from '../states/isKeyboardShortcutMenuOpenedState';
+
+export const useKeyboardShortcutMenu = () => {
+ const [, setIsKeyboardShortcutMenuOpened] = useRecoilState(
+ isKeyboardShortcutMenuOpenedState,
+ );
+ const isKeyboardShortcutMenuOpened = useRecoilValue(
+ isKeyboardShortcutMenuOpenedState,
+ );
+ const {
+ setHotkeyScopeAndMemorizePreviousScope,
+ goBackToPreviousHotkeyScope,
+ } = usePreviousHotkeyScope();
+
+ const toggleKeyboardShortcutMenu = () => {
+ if (isKeyboardShortcutMenuOpened === false) {
+ setIsKeyboardShortcutMenuOpened(true);
+ setHotkeyScopeAndMemorizePreviousScope(
+ AppHotkeyScope.KeyboardShortcutMenu,
+ );
+ } else {
+ setIsKeyboardShortcutMenuOpened(false);
+ goBackToPreviousHotkeyScope();
+ }
+ };
+
+ const openKeyboardShortcutMenu = () => {
+ setIsKeyboardShortcutMenuOpened(true);
+ setHotkeyScopeAndMemorizePreviousScope(AppHotkeyScope.KeyboardShortcutMenu);
+ };
+
+ const closeKeyboardShortcutMenu = () => {
+ setIsKeyboardShortcutMenuOpened(false);
+ goBackToPreviousHotkeyScope();
+ };
+
+ return {
+ toggleKeyboardShortcutMenu,
+ openKeyboardShortcutMenu,
+ closeKeyboardShortcutMenu,
+ };
+};
diff --git a/front/src/modules/keyboard-shortcut-menu/states/isKeyboardShortcutMenuOpenedState.ts b/front/src/modules/keyboard-shortcut-menu/states/isKeyboardShortcutMenuOpenedState.ts
new file mode 100644
index 000000000..443155fbe
--- /dev/null
+++ b/front/src/modules/keyboard-shortcut-menu/states/isKeyboardShortcutMenuOpenedState.ts
@@ -0,0 +1,6 @@
+import { atom } from 'recoil';
+
+export const isKeyboardShortcutMenuOpenedState = atom({
+ key: 'keyboard-shortcut-menu/isKeyboardShortcutMenuOpenedState',
+ default: false,
+});
diff --git a/front/src/modules/keyboard-shortcut-menu/types/Shortcut.ts b/front/src/modules/keyboard-shortcut-menu/types/Shortcut.ts
new file mode 100644
index 000000000..baa7cf1f6
--- /dev/null
+++ b/front/src/modules/keyboard-shortcut-menu/types/Shortcut.ts
@@ -0,0 +1,12 @@
+export enum ShortcutType {
+ Table = 'Table',
+ General = 'General',
+}
+
+export type Shortcut = {
+ label: string;
+ type: ShortcutType.Table | ShortcutType.General;
+ firstHotKey?: string;
+ secondHotKey?: string;
+ areSimultaneous: boolean;
+};
diff --git a/front/src/modules/ui/layout/page/DefaultLayout.tsx b/front/src/modules/ui/layout/page/DefaultLayout.tsx
index b24268fac..80755a03f 100644
--- a/front/src/modules/ui/layout/page/DefaultLayout.tsx
+++ b/front/src/modules/ui/layout/page/DefaultLayout.tsx
@@ -6,6 +6,7 @@ import { AuthModal } from '@/auth/components/Modal';
import { useOnboardingStatus } from '@/auth/hooks/useOnboardingStatus';
import { OnboardingStatus } from '@/auth/utils/getOnboardingStatus';
import { CommandMenu } from '@/command-menu/components/CommandMenu';
+import { KeyboardShortcutMenu } from '@/keyboard-shortcut-menu/components/KeyboardShortcutMenu';
import { NavbarAnimatedContainer } from '@/ui/navigation/navbar/components/NavbarAnimatedContainer';
import { MOBILE_VIEWPORT } from '@/ui/theme/constants/theme';
import { AppNavbar } from '~/AppNavbar';
@@ -58,6 +59,7 @@ export const DefaultLayout = ({ children }: DefaultLayoutProps) => {
return (
+
diff --git a/front/src/modules/ui/utilities/hotkey/constants/index.ts b/front/src/modules/ui/utilities/hotkey/constants/index.ts
index 6633c375a..4b152d6c5 100644
--- a/front/src/modules/ui/utilities/hotkey/constants/index.ts
+++ b/front/src/modules/ui/utilities/hotkey/constants/index.ts
@@ -5,6 +5,7 @@ import { HotkeyScope } from '../types/HotkeyScope';
export const DEFAULT_HOTKEYS_SCOPE_CUSTOM_SCOPES: CustomHotkeyScopes = {
commandMenu: true,
goto: false,
+ keyboardShortcutMenu: true,
};
export const INITIAL_HOTKEYS_SCOPE: HotkeyScope = {
@@ -12,5 +13,6 @@ export const INITIAL_HOTKEYS_SCOPE: HotkeyScope = {
customScopes: {
commandMenu: true,
goto: true,
+ keyboardShortcutMenu: true,
},
};
diff --git a/front/src/modules/ui/utilities/hotkey/hooks/useSetHotkeyScope.ts b/front/src/modules/ui/utilities/hotkey/hooks/useSetHotkeyScope.ts
index 6d8c8e0ba..b378f2b9e 100644
--- a/front/src/modules/ui/utilities/hotkey/hooks/useSetHotkeyScope.ts
+++ b/front/src/modules/ui/utilities/hotkey/hooks/useSetHotkeyScope.ts
@@ -15,7 +15,8 @@ const isCustomScopesEqual = (
) => {
return (
customScopesA?.commandMenu === customScopesB?.commandMenu &&
- customScopesA?.goto === customScopesB?.goto
+ customScopesA?.goto === customScopesB?.goto &&
+ customScopesA?.keyboardShortcutMenu === customScopesB?.keyboardShortcutMenu
);
};
@@ -54,6 +55,7 @@ export const useSetHotkeyScope = () =>
customScopes: {
commandMenu: customScopes?.commandMenu ?? true,
goto: customScopes?.goto ?? false,
+ keyboardShortcutMenu: customScopes?.keyboardShortcutMenu ?? true,
},
};
@@ -67,6 +69,10 @@ export const useSetHotkeyScope = () =>
scopesToSet.push(AppHotkeyScope.Goto);
}
+ if (newHotkeyScope?.customScopes?.keyboardShortcutMenu) {
+ scopesToSet.push(AppHotkeyScope.KeyboardShortcutMenu);
+ }
+
scopesToSet.push(newHotkeyScope.scope);
set(internalHotkeysEnabledScopesState, scopesToSet);
set(currentHotkeyScopeState, newHotkeyScope);
diff --git a/front/src/modules/ui/utilities/hotkey/types/AppHotkeyScope.ts b/front/src/modules/ui/utilities/hotkey/types/AppHotkeyScope.ts
index f460708af..29a2f3255 100644
--- a/front/src/modules/ui/utilities/hotkey/types/AppHotkeyScope.ts
+++ b/front/src/modules/ui/utilities/hotkey/types/AppHotkeyScope.ts
@@ -2,4 +2,5 @@ export enum AppHotkeyScope {
App = 'app',
Goto = 'goto',
CommandMenu = 'command-menu',
+ KeyboardShortcutMenu = 'keyboard-shortcut-menu',
}
diff --git a/front/src/modules/ui/utilities/hotkey/types/CustomHotkeyScope.ts b/front/src/modules/ui/utilities/hotkey/types/CustomHotkeyScope.ts
index a6917626f..0630b0710 100644
--- a/front/src/modules/ui/utilities/hotkey/types/CustomHotkeyScope.ts
+++ b/front/src/modules/ui/utilities/hotkey/types/CustomHotkeyScope.ts
@@ -1,4 +1,5 @@
export type CustomHotkeyScopes = {
goto?: boolean;
commandMenu?: boolean;
+ keyboardShortcutMenu?: boolean;
};
diff --git a/front/src/testing/InitializeHotkeyStorybookHook.tsx b/front/src/testing/InitializeHotkeyStorybookHook.tsx
index cdc765d16..4dc315628 100644
--- a/front/src/testing/InitializeHotkeyStorybookHook.tsx
+++ b/front/src/testing/InitializeHotkeyStorybookHook.tsx
@@ -7,7 +7,11 @@ export const InitializeHotkeyStorybookHookEffect = () => {
const setHotkeyScope = useSetHotkeyScope();
useEffect(() => {
- setHotkeyScope(AppHotkeyScope.App, { commandMenu: true, goto: false });
+ setHotkeyScope(AppHotkeyScope.App, {
+ commandMenu: true,
+ goto: false,
+ keyboardShortcutMenu: true,
+ });
}, [setHotkeyScope]);
return <>>;