From d5bb7efb3efc8139d78ef573ef2790af13f9ca9f Mon Sep 17 00:00:00 2001 From: Robert Knight Date: Tue, 23 Apr 2024 15:18:09 +0100 Subject: [PATCH] Return focus to active element after send shortcut --- src/project/SaveButton.tsx | 6 +-- src/project/SendButton.tsx | 45 +++++++++++-------- src/project/project-actions.tsx | 24 +++++----- .../connect-dialogs/ConnectDialog.tsx | 3 +- .../connect-dialogs/FirmwareDialog.tsx | 3 +- .../connect-dialogs/NotFoundDialog.tsx | 3 +- .../connect-dialogs/WebUSBDialog.tsx | 3 +- 7 files changed, 50 insertions(+), 37 deletions(-) diff --git a/src/project/SaveButton.tsx b/src/project/SaveButton.tsx index d9b4c964a..eece5280e 100644 --- a/src/project/SaveButton.tsx +++ b/src/project/SaveButton.tsx @@ -32,10 +32,10 @@ const SaveButton = (props: SaveButtonProps) => { const actions = useProjectActions(); const intl = useIntl(); const menuButtonRef = useRef(null); - const ref = useRef(null); + const activeElementRef = useRef(null); const handleSave = useCallback(() => { - ref.current = document.activeElement as HTMLElement; - actions.save(ref); + activeElementRef.current = document.activeElement as HTMLElement; + actions.save(activeElementRef); }, [actions]); useHotkeys(keyboardShortcuts.saveProject, handleSave, globalShortcutConfig); return ( diff --git a/src/project/SendButton.tsx b/src/project/SendButton.tsx index 0b479c231..beff503f2 100644 --- a/src/project/SendButton.tsx +++ b/src/project/SendButton.tsx @@ -27,6 +27,7 @@ import { globalShortcutConfig, keyboardShortcuts, } from "../common/keyboard-shortcuts"; +import { FinalFocusRef } from "./project-actions"; interface SendButtonProps { size?: ThemeTypings["components"]["Button"]["sizes"]; @@ -53,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) => { const inProgress = flashing.current.flashing; @@ -84,9 +88,14 @@ const SendButton = React.forwardRef( [flashing] ); const menuButtonRef = useRef(null); + const activeElementRef = useRef(null); + const handleSendToMicrobitShortcut = useCallback(() => { + activeElementRef.current = document.activeElement as HTMLElement; + handleSendToMicrobit(activeElementRef); + }, [handleSendToMicrobit]); useHotkeys( keyboardShortcuts.sendToMicrobit, - handleSendToMicrobit, + handleSendToMicrobitShortcut, globalShortcutConfig ); return ( @@ -106,7 +115,7 @@ const SendButton = React.forwardRef( size={size} variant="solid" leftIcon={} - onClick={handleSendToMicrobit} + onClick={() => handleSendToMicrobit(sendButtonRef)} > diff --git a/src/project/project-actions.tsx b/src/project/project-actions.tsx index 6f7edc7a8..c794e27bd 100644 --- a/src/project/project-actions.tsx +++ b/src/project/project-actions.tsx @@ -76,7 +76,7 @@ import ProjectNameQuestion from "./ProjectNameQuestion"; */ export type LoadType = "drop-load" | "file-upload"; -export type FinalFocusRef = React.RefObject; +export type FinalFocusRef = React.RefObject | undefined; export interface MainScriptChoice { main: string | undefined; @@ -122,7 +122,7 @@ export class ProjectActions { connect = async ( forceConnectHelp: boolean, userAction: ConnectionAction, - finalFocusRef: React.RefObject + finalFocusRef: FinalFocusRef ): Promise => { this.logging.event({ type: "connect", @@ -149,7 +149,7 @@ export class ProjectActions { */ private async showConnectHelp( force: boolean, - finalFocusRef: React.RefObject + finalFocusRef: FinalFocusRef ): Promise { const showConnectHelpSetting = this.settings.values.showConnectHelp; if ( @@ -188,11 +188,11 @@ export class ProjectActions { private async connectInternal( options: ConnectOptions, userAction: ConnectionAction, - finalFocusRef: React.RefObject + finalFocusRef: FinalFocusRef ) { try { await this.device.connect(options); - finalFocusRef.current?.focus(); + finalFocusRef?.current?.focus(); return true; } catch (e) { this.handleWebUSBError(e, userAction, finalFocusRef); @@ -203,7 +203,7 @@ export class ProjectActions { /** * Disconnect from the device. */ - disconnect = async (finalFocusRef: React.RefObject) => { + disconnect = async (finalFocusRef: FinalFocusRef) => { this.logging.event({ type: "disconnect", }); @@ -490,7 +490,7 @@ export class ProjectActions { * Flash the device, reporting progress via a dialog. */ flash = async ( - finalFocusRef: React.RefObject, + finalFocusRef: FinalFocusRef, tryAgain?: boolean ): Promise => { this.logging.event({ @@ -787,7 +787,7 @@ export class ProjectActions { private handleConnectErrorChoice = ( choice: ConnectErrorChoice, userAction: ConnectionAction, - finalFocusRef: React.RefObject + finalFocusRef: FinalFocusRef ) => { if (choice !== ConnectErrorChoice.TRY_AGAIN) { return; @@ -801,7 +801,7 @@ export class ProjectActions { private async handleNotFound( userAction: ConnectionAction, - finalFocusRef: React.RefObject + finalFocusRef: FinalFocusRef ) { const choice = await this.dialogs.show((callback) => ( @@ -812,7 +812,7 @@ export class ProjectActions { private async handleFirmwareUpdate( _errorCode: WebUSBErrorCode, userAction: ConnectionAction, - finalFocusRef: React.RefObject + finalFocusRef: FinalFocusRef ) { this.device.clearDevice(); const choice = await this.dialogs.show((callback) => ( @@ -824,7 +824,7 @@ export class ProjectActions { private async handleWebUSBError( e: any, userAction: ConnectionAction, - finalFocusRef: React.RefObject + finalFocusRef: FinalFocusRef ) { if (e instanceof WebUSBError) { this.device.emit(EVENT_END_USB_SELECT); @@ -859,7 +859,7 @@ export class ProjectActions { } private async webusbNotSupportedError( - finalFocusRef: React.RefObject + finalFocusRef: FinalFocusRef ): Promise { if (this.sessionSettings.values.showWebUsbNotSupported) { await this.dialogs.show((callback) => ( diff --git a/src/workbench/connect-dialogs/ConnectDialog.tsx b/src/workbench/connect-dialogs/ConnectDialog.tsx index 612aabeea..2ae12ffe2 100644 --- a/src/workbench/connect-dialogs/ConnectDialog.tsx +++ b/src/workbench/connect-dialogs/ConnectDialog.tsx @@ -10,6 +10,7 @@ import { FormattedMessage } from "react-intl"; import { GenericDialog } from "../../common/GenericDialog"; import ConnectCableDialogBody from "./ConnectCableDialog"; import ConnectHelpDialogBody from "./ConnectHelpDialog"; +import { FinalFocusRef } from "../../project/project-actions"; export const enum ConnectHelpChoice { Next, @@ -21,7 +22,7 @@ interface ConnectHelpDialogProps { callback: (choice: ConnectHelpChoice) => void; dialogNormallyHidden: boolean; shownByRequest: boolean; - finalFocusRef: React.RefObject; + finalFocusRef: FinalFocusRef; } const enum Stage { diff --git a/src/workbench/connect-dialogs/FirmwareDialog.tsx b/src/workbench/connect-dialogs/FirmwareDialog.tsx index 21820abf5..a14194b51 100644 --- a/src/workbench/connect-dialogs/FirmwareDialog.tsx +++ b/src/workbench/connect-dialogs/FirmwareDialog.tsx @@ -11,6 +11,7 @@ import { RiExternalLinkLine } from "react-icons/ri"; import { FormattedMessage } from "react-intl"; import { GenericDialog } from "../../common/GenericDialog"; import firmwareUpgrade from "./firmware-upgrade.svg"; +import { FinalFocusRef } from "../../project/project-actions"; export const enum ConnectErrorChoice { TRY_AGAIN = "TRY_AGAIN", @@ -19,7 +20,7 @@ export const enum ConnectErrorChoice { interface FirmwareDialogProps { callback: (choice: ConnectErrorChoice) => void; - finalFocusRef: React.RefObject; + finalFocusRef: FinalFocusRef; } const FirmwareDialog = ({ callback, finalFocusRef }: FirmwareDialogProps) => { diff --git a/src/workbench/connect-dialogs/NotFoundDialog.tsx b/src/workbench/connect-dialogs/NotFoundDialog.tsx index ace1dd97e..0a25562df 100644 --- a/src/workbench/connect-dialogs/NotFoundDialog.tsx +++ b/src/workbench/connect-dialogs/NotFoundDialog.tsx @@ -13,10 +13,11 @@ import { GenericDialog } from "../../common/GenericDialog"; import SaveButton from "../../project/SaveButton"; import { ConnectErrorChoice } from "./FirmwareDialog"; import notFound from "./not-found.svg"; +import { FinalFocusRef } from "../../project/project-actions"; interface NotFoundDialogProps { callback: (value: ConnectErrorChoice) => void; - finalFocusRef: React.RefObject; + finalFocusRef: FinalFocusRef; } export const NotFoundDialog = ({ diff --git a/src/workbench/connect-dialogs/WebUSBDialog.tsx b/src/workbench/connect-dialogs/WebUSBDialog.tsx index 0ab33ce7d..21b1544ec 100644 --- a/src/workbench/connect-dialogs/WebUSBDialog.tsx +++ b/src/workbench/connect-dialogs/WebUSBDialog.tsx @@ -10,10 +10,11 @@ import { FormattedMessage } from "react-intl"; import { GenericDialog } from "../../common/GenericDialog"; import { isChromeOS105 } from "../../device/webusb"; import chromeOSErrorImage from "./chrome-os-105-error.png"; +import { FinalFocusRef } from "../../project/project-actions"; interface WebUSBDialogProps { callback: () => void; - finalFocusRef: React.RefObject; + finalFocusRef: FinalFocusRef; } export const WebUSBDialog = ({