Skip to content

Commit 46e51f3

Browse files
Add keyboard shortcuts (#553)
- Add action: ["ctrl+shift+enter", "meta+shift+enter"], - Save: ["ctrl+shift+s", "meta+shift+s"], - Settings: ["ctrl+shift+p", "meta+shift+p"], - Focus action below: ["down"], - Focus action above: ["up"], - Focus action name: ["left"], - Focus record button: ["right"], - Rename renameAction: ["F2"], - Connect: ["ctrl+shift+u", "meta+shift+u"], - Disconnect: ["ctrl+shift+k", "meta+shift+k"], - Edit in MakeCode: ["ctrl+shift+e", "meta+shift+e"],
1 parent 67ef7d3 commit 46e51f3

17 files changed

+464
-215
lines changed

package-lock.json

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
"ml4f": "git://github.com/microsoft/ml4f#v1.10.1",
7979
"react": "^18.3.1",
8080
"react-dom": "^18.3.1",
81+
"react-hotkeys-hook": "^4.6.1",
8182
"react-icons": "^4.12.0",
8283
"react-intl": "^6.6.8",
8384
"react-router": "^6.24.0",

src/components/ActionBar/ActionBarItemsRight.tsx

Lines changed: 30 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@
33
*
44
* SPDX-License-Identifier: MIT
55
*/
6-
import { HStack, MenuDivider, useDisclosure } from "@chakra-ui/react";
6+
import { HStack, MenuDivider } from "@chakra-ui/react";
77
import { ReactNode, useMemo } from "react";
88
import { useIntl } from "react-intl";
99
import { useLocation } from "react-router";
10+
import { keyboardShortcuts, useShortcut } from "../../keyboard-shortcut-hooks";
1011
import { useStore } from "../../store";
1112
import AboutDialog from "../AboutDialog";
1213
import ConnectFirstDialog from "../ConnectFirstDialog";
13-
import FeedbackForm from "../FeedbackForm";
1414
import HelpMenu from "../HelpMenu";
1515
import HelpMenuItems, { tourMap } from "../HelpMenuItems";
1616
import { LanguageDialog } from "../LanguageDialog";
@@ -27,11 +27,16 @@ interface ItemsRightProps {
2727

2828
const ItemsRight = ({ menuItems, toolbarItems }: ItemsRightProps) => {
2929
const intl = useIntl();
30-
const languageDisclosure = useDisclosure();
31-
const settingsDisclosure = useDisclosure();
32-
const aboutDialogDisclosure = useDisclosure();
33-
const feedbackDisclosure = useDisclosure();
34-
const connectFirstDisclosure = useDisclosure();
30+
const closeDialog = useStore((s) => s.closeDialog);
31+
const languageDialogOnOpen = useStore((s) => s.languageDialogOnOpen);
32+
const isLanguageDialogOpen = useStore((s) => s.isLanguageDialogOpen);
33+
const settingsDialogOnOpen = useStore((s) => s.settingsDialogOnOpen);
34+
const isSettingsDialogOpen = useStore((s) => s.isSettingsDialogOpen);
35+
const aboutDialogOnOpen = useStore((s) => s.aboutDialogOnOpen);
36+
const isAboutDialogOpen = useStore((s) => s.isAboutDialogOpen);
37+
const feedbackOnOpen = useStore((s) => s.feedbackFormOnOpen);
38+
const connectFirstDialogOnOpen = useStore((s) => s.connectFirstDialogOnOpen);
39+
const isConnectFirstDialogOpen = useStore((s) => s.isConnectFirstDialogOpen);
3540
const setPostConnectTourTrigger = useStore(
3641
(s) => s.setPostConnectTourTrigger
3742
);
@@ -52,43 +57,31 @@ const ItemsRight = ({ menuItems, toolbarItems }: ItemsRightProps) => {
5257
}
5358
}
5459
}, [tourTriggerName]);
60+
useShortcut(keyboardShortcuts.settings, settingsDialogOnOpen);
5561
return (
5662
<>
57-
<LanguageDialog
58-
isOpen={languageDisclosure.isOpen}
59-
onClose={languageDisclosure.onClose}
60-
/>
61-
<SettingsDialog
62-
isOpen={settingsDisclosure.isOpen}
63-
onClose={settingsDisclosure.onClose}
64-
/>
63+
<LanguageDialog isOpen={isLanguageDialogOpen} onClose={closeDialog} />
64+
<SettingsDialog isOpen={isSettingsDialogOpen} onClose={closeDialog} />
6565
<ConnectFirstDialog
66-
isOpen={connectFirstDisclosure.isOpen}
67-
onClose={connectFirstDisclosure.onClose}
66+
isOpen={isConnectFirstDialogOpen}
67+
onClose={closeDialog}
6868
onChooseConnect={() => setPostConnectTourTrigger(tourTrigger)}
6969
explanationTextId="connect-to-tour-body"
7070
options={{ postConnectTourTrigger: tourTrigger }}
7171
/>
72-
<AboutDialog
73-
isOpen={aboutDialogDisclosure.isOpen}
74-
onClose={aboutDialogDisclosure.onClose}
75-
/>
76-
<FeedbackForm
77-
isOpen={feedbackDisclosure.isOpen}
78-
onClose={feedbackDisclosure.onClose}
79-
/>
72+
<AboutDialog isOpen={isAboutDialogOpen} onClose={closeDialog} />
8073
<HStack spacing={3} display={{ base: "none", lg: "flex" }}>
8174
{toolbarItems}
8275
<SettingsMenu
83-
onLanguageDialogOpen={languageDisclosure.onOpen}
84-
onSettingsDialogOpen={settingsDisclosure.onOpen}
76+
onLanguageDialogOpen={languageDialogOnOpen}
77+
onSettingsDialogOpen={settingsDialogOnOpen}
8578
/>
8679
</HStack>
8780
<HelpMenu
8881
display={{ base: "none", md: "block", lg: "block" }}
89-
onAboutDialogOpen={aboutDialogDisclosure.onOpen}
90-
onConnectFirstDialogOpen={connectFirstDisclosure.onOpen}
91-
onFeedbackOpen={feedbackDisclosure.onOpen}
82+
onAboutDialogOpen={aboutDialogOnOpen}
83+
onConnectFirstDialogOpen={connectFirstDialogOnOpen}
84+
onFeedbackOpen={feedbackOnOpen}
9285
tourTrigger={tourTrigger}
9386
/>
9487
<ToolbarMenu
@@ -97,8 +90,8 @@ const ItemsRight = ({ menuItems, toolbarItems }: ItemsRightProps) => {
9790
label={intl.formatMessage({ id: "main-menu" })}
9891
>
9992
{menuItems}
100-
<LanguageMenuItem onOpen={languageDisclosure.onOpen} />
101-
<SettingsMenuItem onOpen={settingsDisclosure.onOpen} />
93+
<LanguageMenuItem onOpen={languageDialogOnOpen} />
94+
<SettingsMenuItem onOpen={settingsDialogOnOpen} />
10295
</ToolbarMenu>
10396
{/* Toolbar items when sm window size. */}
10497
<ToolbarMenu
@@ -107,13 +100,13 @@ const ItemsRight = ({ menuItems, toolbarItems }: ItemsRightProps) => {
107100
label={intl.formatMessage({ id: "main-menu" })}
108101
>
109102
{menuItems}
110-
<LanguageMenuItem onOpen={languageDisclosure.onOpen} />
111-
<SettingsMenuItem onOpen={settingsDisclosure.onOpen} />
103+
<LanguageMenuItem onOpen={languageDialogOnOpen} />
104+
<SettingsMenuItem onOpen={settingsDialogOnOpen} />
112105
<MenuDivider />
113106
<HelpMenuItems
114-
onAboutDialogOpen={aboutDialogDisclosure.onOpen}
115-
onConnectFirstDialogOpen={connectFirstDisclosure.onOpen}
116-
onFeedbackOpen={feedbackDisclosure.onOpen}
107+
onAboutDialogOpen={aboutDialogOnOpen}
108+
onConnectFirstDialogOpen={connectFirstDialogOnOpen}
109+
onFeedbackOpen={feedbackOnOpen}
117110
tourTrigger={tourTrigger}
118111
/>
119112
</ToolbarMenu>

src/components/ActionDataSamplesCard.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,9 @@ interface RecordingAreaProps extends BoxProps {
191191
onRecord: (recordingOptions: RecordingOptions) => void;
192192
}
193193

194+
export const recordButtonId = (action: ActionData) =>
195+
`record-button-${action.ID}`;
196+
194197
const RecordingArea = ({
195198
action,
196199
selected,
@@ -203,6 +206,7 @@ const RecordingArea = ({
203206
<Menu>
204207
<ButtonGroup isAttached>
205208
<Button
209+
id={recordButtonId(action)}
206210
pr={2}
207211
variant={selected ? "record" : "recordOutline"}
208212
borderRight="none"

src/components/ActionNameCard.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ interface ActionNameCardProps {
3333

3434
const actionNameMaxLength = 18;
3535

36+
export const actionNameInputId = (action: Action) =>
37+
`action-name-input-${action.ID}`;
38+
3639
const ActionNameCard = ({
3740
value,
3841
onDeleteAction,
@@ -55,9 +58,13 @@ const ActionNameCard = ({
5558

5659
const debouncedSetActionName = useMemo(
5760
() =>
58-
debounce((id: ActionData["ID"], name: string) => {
59-
setActionName(id, name);
60-
}, 400),
61+
debounce(
62+
(id: ActionData["ID"], name: string) => {
63+
setActionName(id, name);
64+
},
65+
400,
66+
{ leading: true }
67+
),
6168
[setActionName]
6269
);
6370

@@ -134,6 +141,7 @@ const ActionNameCard = ({
134141
)}
135142
</HStack>
136143
<Input
144+
id={actionNameInputId(value)}
137145
autoFocus={localName.length === 0}
138146
isTruncated
139147
readOnly={readOnly}

src/components/ConnectFirstDialog.tsx

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,7 @@ import {
1717
import { ComponentProps, useCallback, useEffect, useState } from "react";
1818
import { FormattedMessage } from "react-intl";
1919
import { ConnectionStatus } from "../connect-status-hooks";
20-
import {
21-
ConnectionFlowStep,
22-
useConnectionStage,
23-
} from "../connection-stage-hooks";
20+
import { useConnectionStage } from "../connection-stage-hooks";
2421
import { ConnectOptions } from "../store";
2522

2623
interface ConnectFirstDialogProps
@@ -35,12 +32,13 @@ const ConnectFirstDialog = ({
3532
options,
3633
onClose,
3734
onChooseConnect,
35+
isOpen,
3836
...rest
3937
}: ConnectFirstDialogProps) => {
4038
const {
4139
actions,
4240
status: connStatus,
43-
stage: connStage,
41+
isDialogOpen: isConnectionDialogOpen,
4442
} = useConnectionStage();
4543
const [isWaiting, setIsWaiting] = useState<boolean>(false);
4644

@@ -86,15 +84,23 @@ const ConnectFirstDialog = ({
8684

8785
useEffect(() => {
8886
if (
89-
connStage.flowStep !== ConnectionFlowStep.None ||
90-
(isWaiting && connStatus === ConnectionStatus.Connected)
87+
isOpen &&
88+
(isConnectionDialogOpen ||
89+
(isWaiting && connStatus === ConnectionStatus.Connected))
9190
) {
9291
// Close dialog if connection dialog is opened, or
9392
// once connected after waiting.
9493
handleOnClose();
9594
return;
9695
}
97-
}, [connStage.flowStep, connStatus, handleOnClose, isWaiting, onClose]);
96+
}, [
97+
connStatus,
98+
handleOnClose,
99+
isConnectionDialogOpen,
100+
isOpen,
101+
isWaiting,
102+
onClose,
103+
]);
98104

99105
return (
100106
<Modal
@@ -103,6 +109,7 @@ const ConnectFirstDialog = ({
103109
size="md"
104110
isCentered
105111
onClose={handleOnClose}
112+
isOpen={isOpen}
106113
{...rest}
107114
>
108115
<ModalOverlay>

src/components/DataSamplesMenu.tsx

Lines changed: 20 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import {
1313
MenuList,
1414
Portal,
1515
Text,
16-
useDisclosure,
1716
} from "@chakra-ui/react";
1817
import { useCallback } from "react";
1918
import { MdMoreVert } from "react-icons/md";
@@ -23,10 +22,6 @@ import {
2322
RiUpload2Line,
2423
} from "react-icons/ri";
2524
import { FormattedMessage, useIntl } from "react-intl";
26-
import {
27-
ConnectionFlowStep,
28-
useConnectionStage,
29-
} from "../connection-stage-hooks";
3025
import { useLogging } from "../logging/logging-hooks";
3126
import { useStore } from "../store";
3227
import { getTotalNumSamples } from "../utils/actions";
@@ -41,9 +36,15 @@ const DataSamplesMenu = () => {
4136
const logging = useLogging();
4237
const actions = useStore((s) => s.actions);
4338
const downloadDataset = useStore((s) => s.downloadDataset);
44-
const { stage } = useConnectionStage();
45-
const deleteConfirmDisclosure = useDisclosure();
46-
const nameProjectDialogDisclosure = useDisclosure();
39+
const isDeleteAllActionsDialogOpen = useStore(
40+
(s) => s.isDeleteAllActionsDialogOpen
41+
);
42+
const deleteAllActionsDialogOnOpen = useStore(
43+
(s) => s.deleteAllActionsDialogOnOpen
44+
);
45+
const closeDialog = useStore((s) => s.closeDialog);
46+
const isNameProjectDialogOpen = useStore((s) => s.isNameProjectDialogOpen);
47+
const nameProjectDialogOnOpen = useStore((s) => s.nameProjectDialogOnOpen);
4748
const isUntitled = useProjectIsUntitled();
4849
const setProjectName = useStore((s) => s.setProjectName);
4950

@@ -63,43 +64,37 @@ const DataSamplesMenu = () => {
6364
type: "dataset-delete",
6465
});
6566
deleteAllActions();
66-
deleteConfirmDisclosure.onClose();
67-
}, [deleteAllActions, deleteConfirmDisclosure, logging]);
67+
closeDialog();
68+
}, [closeDialog, deleteAllActions, logging]);
6869

6970
const handleSave = useCallback(
7071
(newName?: string) => {
7172
if (newName) {
7273
setProjectName(newName);
7374
}
7475
download();
75-
nameProjectDialogDisclosure.onClose();
76+
closeDialog();
7677
},
77-
[download, nameProjectDialogDisclosure, setProjectName]
78+
[closeDialog, download, setProjectName]
7879
);
7980

8081
const handleDownloadDataset = useCallback(() => {
8182
if (isUntitled) {
82-
nameProjectDialogDisclosure.onOpen();
83+
nameProjectDialogOnOpen();
8384
} else {
8485
download();
8586
}
86-
}, [download, isUntitled, nameProjectDialogDisclosure]);
87+
}, [download, isUntitled, nameProjectDialogOnOpen]);
8788

8889
return (
8990
<>
9091
<NameProjectDialog
91-
isOpen={
92-
stage.flowStep === ConnectionFlowStep.None &&
93-
nameProjectDialogDisclosure.isOpen
94-
}
95-
onClose={nameProjectDialogDisclosure.onClose}
92+
isOpen={isNameProjectDialogOpen}
93+
onClose={closeDialog}
9694
onSave={handleSave}
9795
/>
9896
<ConfirmDialog
99-
isOpen={
100-
deleteConfirmDisclosure.isOpen &&
101-
stage.flowStep === ConnectionFlowStep.None
102-
}
97+
isOpen={isDeleteAllActionsDialogOpen}
10398
heading={intl.formatMessage({
10499
id: "delete-data-samples-confirm-heading",
105100
})}
@@ -109,7 +104,7 @@ const DataSamplesMenu = () => {
109104
</Text>
110105
}
111106
onConfirm={handleDeleteAllActions}
112-
onCancel={deleteConfirmDisclosure.onClose}
107+
onCancel={closeDialog}
113108
/>
114109
<Menu>
115110
<MenuButton
@@ -135,7 +130,7 @@ const DataSamplesMenu = () => {
135130
</MenuItem>
136131
<MenuItem
137132
icon={<RiDeleteBin2Line />}
138-
onClick={deleteConfirmDisclosure.onOpen}
133+
onClick={deleteAllActionsDialogOnOpen}
139134
>
140135
<FormattedMessage id="delete-data-samples-action" />
141136
</MenuItem>

0 commit comments

Comments
 (0)