From 70726741110a688bb7caafcafca4b5213b14b281 Mon Sep 17 00:00:00 2001 From: Robert Dyer <rdyer@unl.edu> Date: Mon, 15 Jul 2024 10:06:09 -0500 Subject: [PATCH] Add help modal for keyboard shortcuts. (#3033) * Add help modal for keyboard shortcuts. * add release note * fix linter * fix typecheck * fix linter * use component syntax for GroupHeading * use component syntax for Shortcut * fix linter * use component syntax for KeyIcon * refactor to support different dialogs * show different help based on current page * fix linter * reword help * capitalize letters * show cmd on mac * stop event propagation * dont show if a modal is already open * remove unused import * rename modal * move where location check happens * dont stop event * allow typing '?' in inputs * better filter * extract function * fix linter * dont show if filter popover is visible * fix linter * fix wrong shortcut, support SHIFT * fix linter * fix conditional --- .../src/browser-preload.browser.js | 19 +- .../desktop-client/src/components/Modals.tsx | 4 + .../modals/KeyboardShortcutModal.tsx | 262 ++++++++++++++++++ .../loot-core/src/client/reducers/modals.ts | 10 + upcoming-release-notes/3033.md | 6 + 5 files changed, 296 insertions(+), 5 deletions(-) create mode 100644 packages/desktop-client/src/components/modals/KeyboardShortcutModal.tsx create mode 100644 upcoming-release-notes/3033.md diff --git a/packages/desktop-client/src/browser-preload.browser.js b/packages/desktop-client/src/browser-preload.browser.js index ca5bc118d..c7e0f0b35 100644 --- a/packages/desktop-client/src/browser-preload.browser.js +++ b/packages/desktop-client/src/browser-preload.browser.js @@ -135,6 +135,14 @@ global.Actual = { }, }; +function inputFocused(e) { + return ( + e.target.tagName === 'INPUT' || + e.target.tagName === 'TEXTAREA' || + e.target.isContentEditable + ); +} + document.addEventListener('keydown', e => { if (e.metaKey || e.ctrlKey) { // Cmd/Ctrl+o @@ -144,11 +152,7 @@ document.addEventListener('keydown', e => { } // Cmd/Ctrl+z else if (e.key.toLowerCase() === 'z') { - if ( - e.target.tagName === 'INPUT' || - e.target.tagName === 'TEXTAREA' || - e.target.isContentEditable - ) { + if (inputFocused(e)) { return; } e.preventDefault(); @@ -160,5 +164,10 @@ document.addEventListener('keydown', e => { window.__actionsForMenu.undo(); } } + } else if (e.key === '?') { + if (inputFocused(e)) { + return; + } + window.__actionsForMenu.pushModal('keyboard-shortcuts'); } }); diff --git a/packages/desktop-client/src/components/Modals.tsx b/packages/desktop-client/src/components/Modals.tsx index bbd54ad98..01f1505f4 100644 --- a/packages/desktop-client/src/components/Modals.tsx +++ b/packages/desktop-client/src/components/Modals.tsx @@ -35,6 +35,7 @@ import { GoCardlessExternalMsg } from './modals/GoCardlessExternalMsg'; import { GoCardlessInitialise } from './modals/GoCardlessInitialise'; import { HoldBufferModal } from './modals/HoldBufferModal'; import { ImportTransactions } from './modals/ImportTransactions'; +import { KeyboardShortcutModal } from './modals/KeyboardShortcutModal'; import { LoadBackup } from './modals/LoadBackup'; import { ManageRulesModal } from './modals/ManageRulesModal'; import { MergeUnusedPayees } from './modals/MergeUnusedPayees'; @@ -95,6 +96,9 @@ export function Modals() { }; switch (name) { + case 'keyboard-shortcuts': + return <KeyboardShortcutModal modalProps={modalProps} />; + case 'import-transactions': return ( <ImportTransactions diff --git a/packages/desktop-client/src/components/modals/KeyboardShortcutModal.tsx b/packages/desktop-client/src/components/modals/KeyboardShortcutModal.tsx new file mode 100644 index 000000000..0f018ac98 --- /dev/null +++ b/packages/desktop-client/src/components/modals/KeyboardShortcutModal.tsx @@ -0,0 +1,262 @@ +import { useLocation } from 'react-router-dom'; + +import * as Platform from 'loot-core/src/client/platform'; + +import { Modal, type ModalProps } from '../common/Modal'; +import { Text } from '../common/Text'; +import { View } from '../common/View'; + +type KeyboardShortcutsModalProps = { + modalProps?: Partial<ModalProps>; +}; + +type KeyIconProps = { + shortcut: string; +}; + +type GroupHeadingProps = { + group: string; +}; + +type ShortcutProps = { + shortcut: string; + description: string; + meta?: string; + shift?: boolean; +}; + +function KeyIcon({ shortcut }: KeyIconProps) { + return ( + <div + style={{ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + fontWeight: 'bold', + backgroundColor: '#fff', + color: '#000', + border: '1px solid #000', + borderRadius: 8, + minWidth: 35, + minHeight: 35, + filter: 'drop-shadow(1px 1px)', + padding: 5, + }} + > + {shortcut} + </div> + ); +} + +function GroupHeading({ group }: GroupHeadingProps) { + return ( + <Text + style={{ + fontWeight: 'bold', + fontSize: 16, + marginTop: 20, + marginBottom: 10, + }} + > + {group}: + </Text> + ); +} + +function Shortcut({ shortcut, description, meta, shift }: ShortcutProps) { + return ( + <div + style={{ + display: 'flex', + marginBottom: 10, + marginLeft: 20, + }} + > + <div + style={{ + display: 'flex', + flexDirection: 'column', + }} + > + <div + style={{ + display: 'flex', + flexDirection: 'row', + marginRight: 10, + }} + > + {meta && ( + <> + <KeyIcon shortcut={meta} /> + <Text + style={{ + display: 'flex', + alignItems: 'center', + textAlign: 'center', + fontSize: 16, + paddingLeft: 2, + paddingRight: 2, + }} + > + + + </Text> + </> + )} + {shift && ( + <> + <KeyIcon shortcut="Shift" /> + <Text + style={{ + display: 'flex', + alignItems: 'center', + textAlign: 'center', + fontSize: 16, + paddingLeft: 2, + paddingRight: 2, + }} + > + + + </Text> + </> + )} + <KeyIcon shortcut={shortcut} /> + </div> + <div + style={{ + display: 'flex', + flexDirection: 'row', + flex: 1, + }} + /> + </div> + <div + style={{ + display: 'flex', + alignItems: 'center', + maxWidth: 300, + }} + > + {description} + </div> + </div> + ); +} + +export function KeyboardShortcutModal({ + modalProps, +}: KeyboardShortcutsModalProps) { + const location = useLocation(); + const onAccounts = location.pathname.startsWith('/accounts'); + const ctrl = Platform.OS === 'mac' ? '⌘' : 'Ctrl'; + return ( + <Modal title="Keyboard Shortcuts" {...modalProps}> + <View + style={{ + flexDirection: 'row', + }} + > + <View> + <Shortcut + shortcut="O" + description="Close the current budget and open another" + meta={ctrl} + /> + <Shortcut shortcut="?" description="Show this help dialog" /> + {onAccounts && ( + <> + <Shortcut shortcut="Enter" description="Move down when editing" /> + <Shortcut shortcut="Tab" description="Move right when editing" /> + <GroupHeading group="Select a transaction, then" /> + <Shortcut + shortcut="J" + description="Move to the next transaction down" + /> + <Shortcut + shortcut="K" + description="Move to the next transaction up" + /> + <Shortcut + shortcut="↑" + description="Move to the next transaction down and scroll" + /> + <Shortcut + shortcut="↓" + description="Move to the next transaction up and scroll" + /> + <Shortcut + shortcut="Space" + description="Toggle selection of current transaction" + /> + <Shortcut + shortcut="Space" + description="Toggle all transactions between current and most recently selected transaction" + shift={true} + /> + </> + )} + </View> + <View + style={{ + marginLeft: 20, + marginRight: 20, + }} + > + <Shortcut + shortcut="Z" + description="Undo the last change" + meta={ctrl} + /> + <Shortcut + shortcut="Z" + description="Redo the last undone change" + shift={true} + meta={ctrl} + /> + {onAccounts && ( + <> + <Shortcut + shortcut="Enter" + description="Move up when editing" + shift={true} + /> + <Shortcut + shortcut="Tab" + description="Move left when editing" + shift={true} + /> + <GroupHeading group="With transaction(s) selected" /> + <Shortcut + shortcut="F" + description="Filter to the selected transactions" + /> + <Shortcut + shortcut="D" + description="Delete selected transactions" + /> + <Shortcut + shortcut="A" + description="Set account for selected transactions" + /> + <Shortcut + shortcut="P" + description="Set payee for selected transactions" + /> + <Shortcut + shortcut="N" + description="Set notes for selected transactions" + /> + <Shortcut + shortcut="C" + description="Set category for selected transactions" + /> + <Shortcut + shortcut="L" + description="Toggle cleared for current transaction" + /> + </> + )} + </View> + </View> + </Modal> + ); +} diff --git a/packages/loot-core/src/client/reducers/modals.ts b/packages/loot-core/src/client/reducers/modals.ts index a600b1867..d0e6840fb 100644 --- a/packages/loot-core/src/client/reducers/modals.ts +++ b/packages/loot-core/src/client/reducers/modals.ts @@ -10,6 +10,16 @@ const initialState: ModalsState = { export function update(state = initialState, action: Action): ModalsState { switch (action.type) { case constants.PUSH_MODAL: + // special case: don't show the keyboard shortcuts modal if there's already a modal open + if ( + action.modal.name.endsWith('keyboard-shortcuts') && + (state.modalStack.length > 0 || + window.document.querySelector( + 'div[data-testid="filters-menu-tooltip"]', + ) !== null) + ) { + return state; + } return { ...state, modalStack: [...state.modalStack, action.modal], diff --git a/upcoming-release-notes/3033.md b/upcoming-release-notes/3033.md new file mode 100644 index 000000000..0416374a7 --- /dev/null +++ b/upcoming-release-notes/3033.md @@ -0,0 +1,6 @@ +--- +category: Features +authors: [psybers] +--- + +Add help modal for keyboard shortcuts. -- GitLab