Skip to content

Commit

Permalink
Centralize keyboard shortcuts
Browse files Browse the repository at this point in the history
  • Loading branch information
microbit-robert committed Apr 23, 2024
1 parent 027bb24 commit 7c1f212
Show file tree
Hide file tree
Showing 13 changed files with 95 additions and 71 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
24 changes: 8 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,7 @@ const CodeEmbed = ({
_focusVisible={{
outline: "none",
}}
onKeyDown={handleKeyDown}
// 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 ref = useRef<HTMLElement | null>(null);
const handleSave = useCallback(() => {
ref.current = document.activeElement as HTMLElement;
actions.save(ref);
}, [actions]);
useHotkeys(keyboardShortcuts.saveProject, handleSave, globalShortcutConfig);
return (
<Tooltip
hasArrow
Expand Down
10 changes: 10 additions & 0 deletions src/project/SendButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ 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";

interface SendButtonProps {
size?: ThemeTypings["components"]["Button"]["sizes"];
Expand Down Expand Up @@ -79,6 +84,11 @@ const SendButton = React.forwardRef(
[flashing]
);
const menuButtonRef = useRef<HTMLButtonElement>(null);
useHotkeys(
keyboardShortcuts.sendToMicrobit,
handleSendToMicrobit,
globalShortcutConfig
);
return (
<HStack>
<Menu>
Expand Down
14 changes: 7 additions & 7 deletions src/project/project-actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ import ProjectNameQuestion from "./ProjectNameQuestion";
*/
export type LoadType = "drop-load" | "file-upload";

export type FinalFocusRef = React.RefObject<HTMLElement>;

export interface MainScriptChoice {
main: string | undefined;
}
Expand Down Expand Up @@ -547,7 +549,7 @@ export class ProjectActions {
* Trigger a browser download with a universal hex file.
*/
save = async (
finalFocusRef: React.RefObject<HTMLButtonElement>,
finalFocusRef: FinalFocusRef,
saveViaWebUsbNotSupported?: boolean
) => {
this.logging.event({
Expand Down Expand Up @@ -728,7 +730,7 @@ export class ProjectActions {
isDefaultProjectName = (): boolean => this.fs.project.name === undefined;

ensureProjectName = async (
finalFocusRef: React.RefObject<HTMLButtonElement>
finalFocusRef: FinalFocusRef
): Promise<boolean | undefined> => {
if (this.isDefaultProjectName()) {
return await this.editProjectName(true, finalFocusRef);
Expand All @@ -738,7 +740,7 @@ export class ProjectActions {

editProjectName = async (
isSave: boolean = false,
finalFocusRef?: React.RefObject<HTMLButtonElement>
finalFocusRef?: FinalFocusRef
) => {
const name = await this.dialogs.show<string | undefined>((callback) => (
<InputDialog
Expand Down Expand Up @@ -939,9 +941,7 @@ export class ProjectActions {
}
}

private async handlePostSaveDialog(
finalFocusRef: React.RefObject<HTMLButtonElement>
) {
private async handlePostSaveDialog(finalFocusRef: FinalFocusRef) {
const showPostSaveHelpSetting = this.settings.values.showPostSaveHelp;
if (!showPostSaveHelpSetting) {
return;
Expand All @@ -966,7 +966,7 @@ export class ProjectActions {

private async handleTransferHexDialog(
forceTransferHexHelp: boolean,
finalFocusRef: React.RefObject<HTMLButtonElement>
finalFocusRef: FinalFocusRef
) {
const showTransferHexHelpSetting = this.settings.values.showTransferHexHelp;
if (!forceTransferHexHelp && !showTransferHexHelpSetting) {
Expand Down
47 changes: 19 additions & 28 deletions src/workbench/SideBarHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ import SearchDialog from "../documentation/search/SearchDialog";
import { useLogging } from "../logging/logging-hooks";
import { RouterState, useRouterState } from "../router-hooks";
import { useSettings } from "../settings/settings";
import { useHotkeys } from "react-hotkeys-hook";
import {
globalShortcutConfig,
keyboardShortcuts,
} from "../common/keyboard-shortcuts";

interface SideBarHeaderProps {
sidebarShown: boolean;
Expand Down Expand Up @@ -72,35 +77,21 @@ const SideBarHeader = ({

const [{ languageId }] = useSettings();
const searchAvailable = supportedSearchLanguages.includes(languageId);
// When we add more keyboard shortcuts, we should pull this up and have a CM-like model of the
// available actions and their shortcuts, with a hook used here to register a handler for the action.
useEffect(() => {
const isMac = /Mac/.test(navigator.platform);
const keydown = (e: KeyboardEvent) => {
if (
(e.key === "F" || e.key === "f") &&
(isMac ? e.metaKey : e.ctrlKey) &&
e.shiftKey &&
!e.repeat &&
searchAvailable
) {
handleModalOpened();
if (!sidebarShown) {
onSidebarToggled();
}

const handleSearchShortcut = useCallback(() => {
if (searchAvailable) {
handleModalOpened();
if (!sidebarShown) {
onSidebarToggled();
}
};
document.addEventListener("keydown", keydown);
return () => {
document.removeEventListener("keydown", keydown);
};
}, [
onSidebarToggled,
searchModal,
sidebarShown,
handleModalOpened,
searchAvailable,
]);
}
}, [handleModalOpened, onSidebarToggled, searchAvailable, sidebarShown]);

useHotkeys(
keyboardShortcuts.search,
handleSearchShortcut,
globalShortcutConfig
);

const handleQueryChange: React.ChangeEventHandler<HTMLInputElement> =
useCallback(
Expand Down
3 changes: 2 additions & 1 deletion src/workbench/connect-dialogs/TransferHexDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { FormattedMessage } from "react-intl";
import { GenericDialog, GenericDialogFooter } from "../../common/GenericDialog";
import transferHexMac from "./transfer-hex-mac.gif";
import transferHexWin from "./transfer-hex-win.gif";
import { FinalFocusRef } from "../../project/project-actions";

export const enum TransferHexChoice {
CloseDontShowAgain,
Expand All @@ -20,7 +21,7 @@ interface TransferHexDialogProps {
callback: (value: TransferHexChoice) => void;
dialogNormallyHidden: boolean;
shownByRequest: boolean;
finalFocusRef: React.RefObject<HTMLButtonElement>;
finalFocusRef: FinalFocusRef;
}

export const TransferHexDialog = ({
Expand Down

0 comments on commit 7c1f212

Please sign in to comment.