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:
Kanav Arora 2023-11-22 04:06:23 +05:30 committed by GitHub
parent a67199e0c3
commit ff42526a09
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 337 additions and 2 deletions

View File

@ -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>
)
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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)};
`;

View File

@ -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,
},
];

View File

@ -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,
};
};

View File

@ -0,0 +1,6 @@
import { atom } from 'recoil';
export const isKeyboardShortcutMenuOpenedState = atom({
key: 'keyboard-shortcut-menu/isKeyboardShortcutMenuOpenedState',
default: false,
});

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

View File

@ -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 (
<StyledLayout>
<CommandMenu />
<KeyboardShortcutMenu />
<NavbarAnimatedContainer>
<AppNavbar />
</NavbarAnimatedContainer>

View File

@ -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,
},
};

View File

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

View File

@ -2,4 +2,5 @@ export enum AppHotkeyScope {
App = 'app',
Goto = 'goto',
CommandMenu = 'command-menu',
KeyboardShortcutMenu = 'keyboard-shortcut-menu',
}

View File

@ -1,4 +1,5 @@
export type CustomHotkeyScopes = {
goto?: boolean;
commandMenu?: boolean;
keyboardShortcutMenu?: boolean;
};

View File

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