diff --git a/frontend/src/ts/input/hotkeys/quickrestart.ts b/frontend/src/ts/input/hotkeys/quickrestart.ts index 1b25dbc22179..310b3bf39fb3 100644 --- a/frontend/src/ts/input/hotkeys/quickrestart.ts +++ b/frontend/src/ts/input/hotkeys/quickrestart.ts @@ -4,9 +4,10 @@ import { navigate } from "../../controllers/route-controller"; import { restartTestEvent } from "../../events/test"; import { getActivePage } from "../../states/core"; import { hotkeys, quickRestartHotkeyMap } from "../../states/hotkeys"; -import { createHotkey } from "./utils"; +import { createHotkeys } from "./utils"; import { getConfig } from "../../config/store"; import { isLongTest, wordsHaveNewline, wordsHaveTab } from "../../states/test"; +import { untrack } from "solid-js"; function quickRestart(e: KeyboardEvent): void { if (isAnyPopupVisible()) { @@ -22,25 +23,42 @@ function quickRestart(e: KeyboardEvent): void { } } -// Disable restart when we're in long test and quick restart key is enter, because `shift + enter, shift + -// enter` is already reserved for bail out keybind. -createHotkey( - () => hotkeys.quickRestart, - quickRestart, - () => ({ enabled: !isLongTest() || getConfig.quickRestart !== "enter" }), -); - -// We also want to have a hotkey for quick restart key without shift, so when the -// test is considered long (which means that we can't quick restart), we show a -// notification when the user tries to press the quick restart key without shift, -// and we'll restart when it's pressed with shift. -createHotkey( - () => quickRestartHotkeyMap[getConfig.quickRestart], - quickRestart, - () => ({ - enabled: - isLongTest() && - !(wordsHaveTab() && getConfig.quickRestart === "tab") && - !(wordsHaveNewline() && getConfig.quickRestart === "enter"), - }), -); +// Notes about the following two hotkeys: +// +// - The order of the hotkeys in the array passed to `createHotkeys` is important. If +// the order was reversed, then the secondary hotkey will override the primary hotkey +// when the test isn't long, which would cause the quick restart hotkey to be disabled. +// - Both hotkeys only rerun when `hotkeys.quickRestart` changes. All other signals are +// accessed inside an `untrack` block. + +createHotkeys(() => [ + // Secondary hotkey used in long tests. + + // We want to have a hotkey for quick restart key without shift, so when the + // test is considered long (which means that we can't quick restart) and the user + // tries to press the quick restart key without shift, we'll show a notification, and + // when the user presses the quick restart key with shift, we'll restart. + untrack(() => ({ + hotkey: quickRestartHotkeyMap[getConfig.quickRestart], + callback: quickRestart, + options: { + enabled: + isLongTest() && + !(wordsHaveTab() && getConfig.quickRestart === "tab") && + !(wordsHaveNewline() && getConfig.quickRestart === "enter"), + }, + })), + + // Primary hotkey for quick restart. + { + hotkey: hotkeys.quickRestart, + callback: quickRestart, + options: { + enabled: untrack( + // Disable quick restart when we're in a long test and quick restart key is enter, because `shift + enter, shift + + // enter` is already reserved for bail out keybind. + () => !isLongTest() || getConfig.quickRestart !== "enter", + ), + }, + }, +]); diff --git a/frontend/src/ts/input/hotkeys/utils.ts b/frontend/src/ts/input/hotkeys/utils.ts index 5093d103f0da..68cb96e7bd46 100644 --- a/frontend/src/ts/input/hotkeys/utils.ts +++ b/frontend/src/ts/input/hotkeys/utils.ts @@ -1,9 +1,11 @@ import { CreateHotkeyOptions, + CreateHotkeyDefinition, Hotkey, HotkeyCallback, HotkeyCallbackContext, createHotkey as registerHotkey, + createHotkeys as registerHotkeys, } from "@tanstack/solid-hotkeys"; import { isAnyPopupVisible } from "../../utils/misc"; import { isInputElementFocused } from "../input-element"; @@ -11,6 +13,23 @@ import * as CompositionState from "../../legacy-states/composition"; export const NoKey = "" as Hotkey; +const defaultOptions: CreateHotkeyOptions = { + ignoreInputs: false, //hotkeys are active on the words input, but not on other interactive elements + stopPropagation: false, //we set stopPropagation in the callback if the hotkey executes + preventDefault: false, //we set preventDefault in the callback if the hotkey executes + requireReset: true, + conflictBehavior: "replace", +}; + +function attachBeforeCallback(callback: HotkeyCallback): HotkeyCallback { + return (e, context) => { + if (handleHotkeyOnInteractiveElement(e, context)) return; + e.stopPropagation(); + e.preventDefault(); + callback(e, context); + }; +} + export function createHotkey( hotkey: Hotkey | (() => Hotkey), callback: HotkeyCallback, @@ -21,24 +40,35 @@ export function createHotkey( > > = () => ({}), ): void { - registerHotkey( - hotkey, - (e, context) => { - if (handleHotkeyOnInteractiveElement(e, context)) return; - e.stopPropagation(); - e.preventDefault(); - callback(e, context); - }, - () => ({ - ignoreInputs: false, //hotkeys are active on the words input, but not on other interactive elements - stopPropagation: false, //we set stopPropagation in the callback if the hotkey executes - preventDefault: false, //we set preventDefault in the callback if the hotkey executes - requireReset: true, - conflictBehavior: "replace", - enabled: (typeof hotkey === "function" ? hotkey() : hotkey) !== NoKey, - ...options(), - }), - ); + registerHotkey(hotkey, attachBeforeCallback(callback), () => ({ + ...defaultOptions, + enabled: (typeof hotkey === "function" ? hotkey() : hotkey) !== NoKey, + ...options(), + })); +} + +export function createHotkeys( + hotkeys: CreateHotkeyDefinition[] | (() => CreateHotkeyDefinition[]), + commonOptions: () => Partial< + Omit< + CreateHotkeyOptions, + "ignoreInputs" | "stopPropagation" | "preventDefault" + > + > = () => ({}), +): void { + const modifiedHotkeys = (): CreateHotkeyDefinition[] => { + const resolvedHotkeys = typeof hotkeys === "function" ? hotkeys() : hotkeys; + resolvedHotkeys.forEach((hotkey) => { + hotkey.callback = attachBeforeCallback(hotkey.callback); + hotkey.options ??= {}; + hotkey.options.enabled ??= hotkey.hotkey !== NoKey; + }); + return resolvedHotkeys; + }; + registerHotkeys(modifiedHotkeys, () => ({ + ...defaultOptions, + ...commonOptions(), + })); } function isInteractiveElementFocused(): boolean {