2311 embed keyboard shortcuts (#2507)
* 2311-feat(front): AppHotKeyScope and CustomHotKeyScopes configured * 2311-feat(front): Groups and Items added * 2311-fix: pr requested changes --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -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 && (
|
||||||
|
<KeyboardMenuDialog onClose={toggleKeyboardShortcutMenu}>
|
||||||
|
<KeyboardMenuGroup heading="Table">
|
||||||
|
{keyboardShortcutsTable.map((TableShortcut) => (
|
||||||
|
<KeyboardMenuItem shortcut={TableShortcut} />
|
||||||
|
))}
|
||||||
|
</KeyboardMenuGroup>
|
||||||
|
<KeyboardMenuGroup heading="General">
|
||||||
|
{keyboardShortcutsGeneral.map((GeneralShortcut) => (
|
||||||
|
<KeyboardMenuItem shortcut={GeneralShortcut} />
|
||||||
|
))}
|
||||||
|
</KeyboardMenuGroup>
|
||||||
|
</KeyboardMenuDialog>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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 (
|
||||||
|
<StyledDialog>
|
||||||
|
<StyledHeading>
|
||||||
|
Keyboard shortcuts
|
||||||
|
<IconButton variant="tertiary" Icon={IconX} onClick={onClose} />
|
||||||
|
</StyledHeading>
|
||||||
|
<StyledContainer>{children}</StyledContainer>
|
||||||
|
</StyledDialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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 (
|
||||||
|
<StyledGroup>
|
||||||
|
<StyledGroupHeading>{heading}</StyledGroupHeading>
|
||||||
|
{children}
|
||||||
|
</StyledGroup>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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 (
|
||||||
|
<StyledItem>
|
||||||
|
{shortcut.label}
|
||||||
|
{shortcut.secondHotKey ? (
|
||||||
|
shortcut.areSimultaneous ? (
|
||||||
|
<StyledShortcutKeyContainer>
|
||||||
|
<StyledShortcutKey>{shortcut.firstHotKey}</StyledShortcutKey>
|
||||||
|
<StyledShortcutKey>{shortcut.secondHotKey}</StyledShortcutKey>
|
||||||
|
</StyledShortcutKeyContainer>
|
||||||
|
) : (
|
||||||
|
<StyledShortcutKeyContainer>
|
||||||
|
<StyledShortcutKey>{shortcut.firstHotKey}</StyledShortcutKey>
|
||||||
|
then
|
||||||
|
<StyledShortcutKey>{shortcut.secondHotKey}</StyledShortcutKey>
|
||||||
|
</StyledShortcutKeyContainer>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<StyledShortcutKey>{shortcut.firstHotKey}</StyledShortcutKey>
|
||||||
|
)}
|
||||||
|
</StyledItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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)};
|
||||||
|
`;
|
||||||
@ -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,
|
||||||
|
},
|
||||||
|
];
|
||||||
@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
import { atom } from 'recoil';
|
||||||
|
|
||||||
|
export const isKeyboardShortcutMenuOpenedState = atom({
|
||||||
|
key: 'keyboard-shortcut-menu/isKeyboardShortcutMenuOpenedState',
|
||||||
|
default: false,
|
||||||
|
});
|
||||||
12
front/src/modules/keyboard-shortcut-menu/types/Shortcut.ts
Normal file
12
front/src/modules/keyboard-shortcut-menu/types/Shortcut.ts
Normal file
@ -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;
|
||||||
|
};
|
||||||
@ -6,6 +6,7 @@ import { AuthModal } from '@/auth/components/Modal';
|
|||||||
import { useOnboardingStatus } from '@/auth/hooks/useOnboardingStatus';
|
import { useOnboardingStatus } from '@/auth/hooks/useOnboardingStatus';
|
||||||
import { OnboardingStatus } from '@/auth/utils/getOnboardingStatus';
|
import { OnboardingStatus } from '@/auth/utils/getOnboardingStatus';
|
||||||
import { CommandMenu } from '@/command-menu/components/CommandMenu';
|
import { CommandMenu } from '@/command-menu/components/CommandMenu';
|
||||||
|
import { KeyboardShortcutMenu } from '@/keyboard-shortcut-menu/components/KeyboardShortcutMenu';
|
||||||
import { NavbarAnimatedContainer } from '@/ui/navigation/navbar/components/NavbarAnimatedContainer';
|
import { NavbarAnimatedContainer } from '@/ui/navigation/navbar/components/NavbarAnimatedContainer';
|
||||||
import { MOBILE_VIEWPORT } from '@/ui/theme/constants/theme';
|
import { MOBILE_VIEWPORT } from '@/ui/theme/constants/theme';
|
||||||
import { AppNavbar } from '~/AppNavbar';
|
import { AppNavbar } from '~/AppNavbar';
|
||||||
@ -58,6 +59,7 @@ export const DefaultLayout = ({ children }: DefaultLayoutProps) => {
|
|||||||
return (
|
return (
|
||||||
<StyledLayout>
|
<StyledLayout>
|
||||||
<CommandMenu />
|
<CommandMenu />
|
||||||
|
<KeyboardShortcutMenu />
|
||||||
<NavbarAnimatedContainer>
|
<NavbarAnimatedContainer>
|
||||||
<AppNavbar />
|
<AppNavbar />
|
||||||
</NavbarAnimatedContainer>
|
</NavbarAnimatedContainer>
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { HotkeyScope } from '../types/HotkeyScope';
|
|||||||
export const DEFAULT_HOTKEYS_SCOPE_CUSTOM_SCOPES: CustomHotkeyScopes = {
|
export const DEFAULT_HOTKEYS_SCOPE_CUSTOM_SCOPES: CustomHotkeyScopes = {
|
||||||
commandMenu: true,
|
commandMenu: true,
|
||||||
goto: false,
|
goto: false,
|
||||||
|
keyboardShortcutMenu: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const INITIAL_HOTKEYS_SCOPE: HotkeyScope = {
|
export const INITIAL_HOTKEYS_SCOPE: HotkeyScope = {
|
||||||
@ -12,5 +13,6 @@ export const INITIAL_HOTKEYS_SCOPE: HotkeyScope = {
|
|||||||
customScopes: {
|
customScopes: {
|
||||||
commandMenu: true,
|
commandMenu: true,
|
||||||
goto: true,
|
goto: true,
|
||||||
|
keyboardShortcutMenu: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -15,7 +15,8 @@ const isCustomScopesEqual = (
|
|||||||
) => {
|
) => {
|
||||||
return (
|
return (
|
||||||
customScopesA?.commandMenu === customScopesB?.commandMenu &&
|
customScopesA?.commandMenu === customScopesB?.commandMenu &&
|
||||||
customScopesA?.goto === customScopesB?.goto
|
customScopesA?.goto === customScopesB?.goto &&
|
||||||
|
customScopesA?.keyboardShortcutMenu === customScopesB?.keyboardShortcutMenu
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -54,6 +55,7 @@ export const useSetHotkeyScope = () =>
|
|||||||
customScopes: {
|
customScopes: {
|
||||||
commandMenu: customScopes?.commandMenu ?? true,
|
commandMenu: customScopes?.commandMenu ?? true,
|
||||||
goto: customScopes?.goto ?? false,
|
goto: customScopes?.goto ?? false,
|
||||||
|
keyboardShortcutMenu: customScopes?.keyboardShortcutMenu ?? true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -67,6 +69,10 @@ export const useSetHotkeyScope = () =>
|
|||||||
scopesToSet.push(AppHotkeyScope.Goto);
|
scopesToSet.push(AppHotkeyScope.Goto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (newHotkeyScope?.customScopes?.keyboardShortcutMenu) {
|
||||||
|
scopesToSet.push(AppHotkeyScope.KeyboardShortcutMenu);
|
||||||
|
}
|
||||||
|
|
||||||
scopesToSet.push(newHotkeyScope.scope);
|
scopesToSet.push(newHotkeyScope.scope);
|
||||||
set(internalHotkeysEnabledScopesState, scopesToSet);
|
set(internalHotkeysEnabledScopesState, scopesToSet);
|
||||||
set(currentHotkeyScopeState, newHotkeyScope);
|
set(currentHotkeyScopeState, newHotkeyScope);
|
||||||
|
|||||||
@ -2,4 +2,5 @@ export enum AppHotkeyScope {
|
|||||||
App = 'app',
|
App = 'app',
|
||||||
Goto = 'goto',
|
Goto = 'goto',
|
||||||
CommandMenu = 'command-menu',
|
CommandMenu = 'command-menu',
|
||||||
|
KeyboardShortcutMenu = 'keyboard-shortcut-menu',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
export type CustomHotkeyScopes = {
|
export type CustomHotkeyScopes = {
|
||||||
goto?: boolean;
|
goto?: boolean;
|
||||||
commandMenu?: boolean;
|
commandMenu?: boolean;
|
||||||
|
keyboardShortcutMenu?: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -7,7 +7,11 @@ export const InitializeHotkeyStorybookHookEffect = () => {
|
|||||||
const setHotkeyScope = useSetHotkeyScope();
|
const setHotkeyScope = useSetHotkeyScope();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setHotkeyScope(AppHotkeyScope.App, { commandMenu: true, goto: false });
|
setHotkeyScope(AppHotkeyScope.App, {
|
||||||
|
commandMenu: true,
|
||||||
|
goto: false,
|
||||||
|
keyboardShortcutMenu: true,
|
||||||
|
});
|
||||||
}, [setHotkeyScope]);
|
}, [setHotkeyScope]);
|
||||||
|
|
||||||
return <></>;
|
return <></>;
|
||||||
|
|||||||
Reference in New Issue
Block a user