Skip to content

Commit

Permalink
Centralize keyboard shortcuts (#1175)
Browse files Browse the repository at this point in the history
This also adds two new keyboard shortcuts:
Save project hex: (Windows) Ctrl+Shift+S; (Mac) Cmd+Shift+S
Send to micro:bit: (Windows) Ctrl+Shift+E; (Mac) Cmd+Shift+E
  • Loading branch information
microbit-robert authored May 1, 2024
1 parent 027bb24 commit bcedfb3
Show file tree
Hide file tree
Showing 17 changed files with 139 additions and 103 deletions.
10 changes: 10 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"mobile-drag-drop": "^2.3.0-rc.2",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-hotkeys-hook": "^4.5.0",
"react-icons": "^4.8.0",
"react-intl": "^6.2.10",
"vite": "^5.1.5",
Expand Down
3 changes: 2 additions & 1 deletion src/common/GenericDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { ThemingProps } from "@chakra-ui/styled-system";
import { ReactNode } from "react";
import { FormattedMessage } from "react-intl";
import ModalCloseButton from "./ModalCloseButton";
import { FinalFocusRef } from "../project/project-actions";

export interface GenericDialogProps {
header?: ReactNode;
Expand All @@ -24,7 +25,7 @@ export interface GenericDialogProps {
size?: ThemingProps<"Button">["size"];
onClose: () => void;
returnFocusOnClose?: boolean;
finalFocusRef?: React.RefObject<HTMLButtonElement>;
finalFocusRef?: FinalFocusRef;
}

export const GenericDialog = ({
Expand Down
3 changes: 2 additions & 1 deletion src/common/InputDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
import { ThemeTypings } from "@chakra-ui/styled-system";
import { ReactNode, useCallback, useState } from "react";
import { FormattedMessage } from "react-intl";
import { FinalFocusRef } from "../project/project-actions";

export interface InputValidationResult {
ok: boolean;
Expand All @@ -39,7 +40,7 @@ export interface InputDialogProps<T> {
actionLabel: string;
size?: ThemeTypings["components"]["Modal"]["sizes"];
validate?: (input: T) => InputValidationResult;
finalFocusRef?: React.RefObject<HTMLButtonElement>;
finalFocusRef?: FinalFocusRef;
callback: (value: ValueOrCancelled<T>) => void;
}

Expand Down
3 changes: 2 additions & 1 deletion src/common/PostSaveDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { ReactNode, useCallback } from "react";
import { FormattedMessage } from "react-intl";
import { GenericDialog, GenericDialogFooter } from "../common/GenericDialog";
import { useProject } from "../project/project-hooks";
import { FinalFocusRef } from "../project/project-actions";

export const enum PostSaveChoice {
ShowTransferHexHelp,
Expand All @@ -18,7 +19,7 @@ export const enum PostSaveChoice {
interface PostSaveDialogProps {
callback: (value: PostSaveChoice) => void;
dialogNormallyHidden: boolean;
finalFocusRef: React.RefObject<HTMLButtonElement>;
finalFocusRef: FinalFocusRef;
}

export const PostSaveDialog = ({
Expand Down
14 changes: 14 additions & 0 deletions src/common/keyboard-shortcuts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Shortcuts are global unless noted otherwise.
export const keyboardShortcuts = {
// This is scoped by keyboard focus.
copyCode: ["ctrl+c", "meta+c", "enter"],
search: ["ctrl+shift+f", "meta+shift+f"],
sendToMicrobit: ["ctrl+shift+e", "meta+shift+e"],
saveProject: ["ctrl+shift+s", "meta+shift+s"],
};

export const globalShortcutConfig = {
preventDefault: true,
enableOnContentEditable: true,
enableOnFormTags: true,
};
21 changes: 6 additions & 15 deletions src/documentation/api/ApiNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ import ShowMoreButton from "../common/ShowMoreButton";
import { allowWrapAtPeriods } from "../common/wrap";
import { useCodeDragImage } from "../documentation-hooks";
import Highlight from "../reference/Highlight";
import { useHotkeys } from "react-hotkeys-hook";
import { keyboardShortcuts } from "../../common/keyboard-shortcuts";

const kindToFontSize: Record<string, any> = {
module: "2xl",
Expand Down Expand Up @@ -422,20 +424,9 @@ const DraggableSignature = ({
onCopy();
await actions?.copyCode(code, codeWithImports, type, id);
}, [actions, code, codeWithImports, onCopy, type, id]);
const isMac = /Mac/.test(navigator.platform);
const handleKeyDown = useCallback(
async (e: React.KeyboardEvent<HTMLDivElement>) => {
if (e.key === "Enter") {
e.preventDefault();
handleCopyCode();
}
if ((e.key === "c" || e.key === "C") && (isMac ? e.metaKey : e.ctrlKey)) {
e.preventDefault();
handleCopyCode();
}
},
[handleCopyCode, isMac]
);
const hotKeysRef = useHotkeys(keyboardShortcuts.copyCode, handleCopyCode, {
preventDefault: true,
});
const intl = useIntl();
const [{ dragDropSuccess }] = useSessionSettings();
return (
Expand All @@ -448,6 +439,7 @@ const DraggableSignature = ({
isDisabled={dragDropSuccess}
>
<HStack
ref={hotKeysRef}
draggable
spacing={0}
onClick={copyCodeButton.onToggle}
Expand All @@ -467,7 +459,6 @@ const DraggableSignature = ({
boxShadow: "var(--chakra-shadows-outline);",
outline: "none",
}}
onKeyDown={handleKeyDown}
{...props}
cursor="grab"
>
Expand Down
23 changes: 7 additions & 16 deletions src/documentation/common/CodeEmbed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from "@chakra-ui/react";
import { forwardRef } from "@chakra-ui/system";
import React, {
LegacyRef,
Ref,
useCallback,
useEffect,
Expand All @@ -33,6 +34,8 @@ import { useSessionSettings } from "../../settings/session-settings";
import DragHandle from "../common/DragHandle";
import { useCodeDragImage } from "../documentation-hooks";
import CodeActionButton from "./CodeActionButton";
import { useHotkeys } from "react-hotkeys-hook";
import { keyboardShortcuts } from "../../common/keyboard-shortcuts";

interface CodeEmbedProps {
code: string;
Expand Down Expand Up @@ -132,20 +135,9 @@ const CodeEmbed = ({
const textHeight = lineCount * 1.375 + "em";
const codeHeight = `calc(${textHeight} + var(--chakra-space-2) + var(--chakra-space-2))`;
const codePopUpHeight = `calc(${codeHeight} + 2px)`; // Account for border.
const isMac = /Mac/.test(navigator.platform);
const handleKeyDown = useCallback(
async (e: React.KeyboardEvent<HTMLDivElement>) => {
if (e.key === "Enter") {
e.preventDefault();
handleCopyCode();
}
if ((e.key === "c" || e.key === "C") && (isMac ? e.metaKey : e.ctrlKey)) {
e.preventDefault();
handleCopyCode();
}
},
[handleCopyCode, isMac]
);
const hotKeysRef = useHotkeys(keyboardShortcuts.copyCode, handleCopyCode, {
preventDefault: true,
}) as LegacyRef<HTMLDivElement>;
const determineBackground = () => {
if (
(toolkitType === "ideas" && state === "highlighted") ||
Expand All @@ -157,7 +149,7 @@ const CodeEmbed = ({
};
return (
<Box position="relative">
<Box height={codeHeight} fontSize="md">
<Box height={codeHeight} fontSize="md" ref={hotKeysRef} tabIndex={-1}>
<Code
onMouseEnter={toRaised}
onMouseLeave={handleMouseLeave}
Expand All @@ -180,7 +172,6 @@ const CodeEmbed = ({
_focusVisible={{
outline: "none",
}}
onKeyDown={handleKeyDown}
zIndex={zIndexCode}
/>
{state === "raised" && (
Expand Down
13 changes: 12 additions & 1 deletion src/project/SaveButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,18 @@
* SPDX-License-Identifier: MIT
*/
import { Tooltip } from "@chakra-ui/react";
import { useRef } from "react";
import { useCallback, useRef } from "react";
import { RiDownload2Line } from "react-icons/ri";
import { useIntl } from "react-intl";
import CollapsibleButton, {
CollapsibleButtonProps,
} from "../common/CollapsibleButton";
import { useProjectActions } from "./project-hooks";
import { useHotkeys } from "react-hotkeys-hook";
import {
globalShortcutConfig,
keyboardShortcuts,
} from "../common/keyboard-shortcuts";

interface SaveButtonProps
extends Omit<CollapsibleButtonProps, "onClick" | "text" | "icon"> {}
Expand All @@ -27,6 +32,12 @@ const SaveButton = (props: SaveButtonProps) => {
const actions = useProjectActions();
const intl = useIntl();
const menuButtonRef = useRef<HTMLButtonElement>(null);
const activeElementRef = useRef<HTMLElement | null>(null);
const handleSave = useCallback(() => {
activeElementRef.current = document.activeElement as HTMLElement;
actions.save(activeElementRef);
}, [actions]);
useHotkeys(keyboardShortcuts.saveProject, handleSave, globalShortcutConfig);
return (
<Tooltip
hasArrow
Expand Down
53 changes: 36 additions & 17 deletions src/project/SendButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ import { ConnectionAction, ConnectionStatus } from "../device/device";
import { useConnectionStatus } from "../device/device-hooks";
import MoreMenuButton from "./MoreMenuButton";
import { useProjectActions } from "./project-hooks";
import { useHotkeys } from "react-hotkeys-hook";
import {
globalShortcutConfig,
keyboardShortcuts,
} from "../common/keyboard-shortcuts";
import { FinalFocusRef } from "./project-actions";

interface SendButtonProps {
size?: ThemeTypings["components"]["Button"]["sizes"];
Expand All @@ -48,24 +54,27 @@ const SendButton = React.forwardRef(
flashing: false,
lastCompleteFlash: 0,
});
const handleSendToMicrobit = useCallback(async () => {
if (flashing.current.flashing) {
// Ignore repeated clicks.
return;
}
flashing.current = {
flashing: true,
lastCompleteFlash: flashing.current.lastCompleteFlash,
};
try {
await actions.flash(sendButtonRef);
} finally {
const handleSendToMicrobit = useCallback(
async (finalFocusRef: FinalFocusRef) => {
if (flashing.current.flashing) {
// Ignore repeated clicks.
return;
}
flashing.current = {
flashing: false,
lastCompleteFlash: new Date().getTime(),
flashing: true,
lastCompleteFlash: flashing.current.lastCompleteFlash,
};
}
}, [flashing, actions, sendButtonRef]);
try {
await actions.flash(finalFocusRef);
} finally {
flashing.current = {
flashing: false,
lastCompleteFlash: new Date().getTime(),
};
}
},
[flashing, actions]
);
const handleFocus = useCallback(
(e: FocusEvent<unknown>) => {
const inProgress = flashing.current.flashing;
Expand All @@ -79,6 +88,16 @@ const SendButton = React.forwardRef(
[flashing]
);
const menuButtonRef = useRef<HTMLButtonElement>(null);
const activeElementRef = useRef<HTMLElement | null>(null);
const handleSendToMicrobitShortcut = useCallback(() => {
activeElementRef.current = document.activeElement as HTMLElement;
handleSendToMicrobit(activeElementRef);
}, [handleSendToMicrobit]);
useHotkeys(
keyboardShortcuts.sendToMicrobit,
handleSendToMicrobitShortcut,
globalShortcutConfig
);
return (
<HStack>
<Menu>
Expand All @@ -96,7 +115,7 @@ const SendButton = React.forwardRef(
size={size}
variant="solid"
leftIcon={<RiUsbLine />}
onClick={handleSendToMicrobit}
onClick={() => handleSendToMicrobit(sendButtonRef)}
>
<FormattedMessage id="send-action" />
</Button>
Expand Down
Loading

0 comments on commit bcedfb3

Please sign in to comment.