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 <>;