-
Notifications
You must be signed in to change notification settings - Fork 0
fix(web): reset shortcut modifiers after dictation paste #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a1f704c
33aa200
a6cb39c
315649a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| import { useEffect, useState } from "react"; | ||
| import { useEffect, useRef, useState } from "react"; | ||
|
|
||
| export interface ShortcutModifierState { | ||
| metaKey: boolean; | ||
|
|
@@ -28,12 +28,25 @@ export function areShortcutModifierStatesEqual( | |
|
|
||
| export function useShortcutModifierState(): ShortcutModifierState { | ||
| const [state, setState] = useState(EMPTY_SHORTCUT_MODIFIER_STATE); | ||
| const keyboardPastePendingRef = useRef(false); | ||
|
|
||
| useEffect(() => { | ||
| const onKeyboardEvent = (event: KeyboardEvent) => { | ||
| if (event.type === "keydown") { | ||
| keyboardPastePendingRef.current = | ||
| event.key.toLowerCase() === "v" && (event.metaKey || event.ctrlKey); | ||
| } else if (event.key.toLowerCase() === "v") { | ||
| keyboardPastePendingRef.current = false; | ||
| } | ||
| setState((current) => shortcutModifierStateAfterKeyboardEvent(current, event)); | ||
| }; | ||
| const onPaste = () => { | ||
| const wasKeyboardInitiated = keyboardPastePendingRef.current; | ||
| keyboardPastePendingRef.current = false; | ||
| setState((current) => shortcutModifierStateAfterPaste(current, wasKeyboardInitiated)); | ||
| }; | ||
| const onWindowBlur = () => { | ||
| keyboardPastePendingRef.current = false; | ||
| setState((current) => | ||
| areShortcutModifierStatesEqual(current, EMPTY_SHORTCUT_MODIFIER_STATE) | ||
| ? current | ||
|
|
@@ -43,18 +56,20 @@ export function useShortcutModifierState(): ShortcutModifierState { | |
|
|
||
| window.addEventListener("keydown", onKeyboardEvent, true); | ||
| window.addEventListener("keyup", onKeyboardEvent, true); | ||
| window.addEventListener("paste", onPaste, true); | ||
| window.addEventListener("blur", onWindowBlur); | ||
| return () => { | ||
| window.removeEventListener("keydown", onKeyboardEvent, true); | ||
| window.removeEventListener("keyup", onKeyboardEvent, true); | ||
| window.removeEventListener("paste", onPaste, true); | ||
| window.removeEventListener("blur", onWindowBlur); | ||
| }; | ||
| }, []); | ||
|
|
||
| return state; | ||
| } | ||
|
|
||
| function normalizeModifierKey(key: string): keyof ShortcutModifierState | null { | ||
| function normalizeModifierKey(key: string): keyof ShortcutModifierState | "altGraph" | null { | ||
| switch (key) { | ||
| case "Meta": | ||
| case "OS": | ||
|
|
@@ -65,6 +80,8 @@ function normalizeModifierKey(key: string): keyof ShortcutModifierState | null { | |
| case "Alt": | ||
| case "Option": | ||
| return "altKey"; | ||
| case "AltGraph": | ||
| return "altGraph"; | ||
| case "Shift": | ||
| return "shiftKey"; | ||
| default: | ||
|
|
@@ -78,19 +95,40 @@ export function shortcutModifierStateAfterKeyboardEvent( | |
| ): ShortcutModifierState { | ||
| const normalizedModifierKey = normalizeModifierKey(event.key); | ||
| let nextState: ShortcutModifierState; | ||
| if (normalizedModifierKey) { | ||
| if (normalizedModifierKey === "altGraph") { | ||
| nextState = { | ||
| ...currentState, | ||
| ctrlKey: event.type === "keydown", | ||
| altKey: event.type === "keydown", | ||
|
Comment on lines
+101
to
+102
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a user keeps a separate Control or Alt key held while releasing AltGraph, these assignments unconditionally clear both flags. Because the new non-modifier path can only clear state, a subsequent number keydown cannot restore the still-held modifier, so jump hints remain hidden until that modifier is released and pressed again. Preserve modifier flags that remain active independently of AltGraph. AGENTS.md reference: AGENTS.md:L142-L142 Useful? React with 👍 / 👎. |
||
| }; | ||
| } else if (normalizedModifierKey) { | ||
| nextState = { | ||
| ...currentState, | ||
| [normalizedModifierKey]: event.type === "keydown", | ||
| }; | ||
| } else { | ||
| // Non-modifier events can clear stale browser flags, but only a real | ||
| // modifier keydown may mark a modifier as held. | ||
| nextState = { | ||
| metaKey: event.metaKey, | ||
| ctrlKey: event.ctrlKey, | ||
| altKey: event.altKey, | ||
| shiftKey: event.shiftKey, | ||
| metaKey: currentState.metaKey && event.metaKey, | ||
| ctrlKey: currentState.ctrlKey && event.ctrlKey, | ||
| altKey: currentState.altKey && event.altKey, | ||
|
Comment on lines
+114
to
+115
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
On Windows/Linux layouts where AltGraph is emitted as a Control keydown followed by an Useful? React with 👍 / 👎. |
||
| shiftKey: currentState.shiftKey && event.shiftKey, | ||
| }; | ||
| } | ||
|
|
||
| return areShortcutModifierStatesEqual(currentState, nextState) ? currentState : nextState; | ||
| } | ||
|
|
||
| export function shortcutModifierStateAfterPaste( | ||
| currentState: ShortcutModifierState, | ||
| wasKeyboardInitiated: boolean, | ||
| ): ShortcutModifierState { | ||
| if ( | ||
| wasKeyboardInitiated || | ||
| areShortcutModifierStatesEqual(currentState, EMPTY_SHORTCUT_MODIFIER_STATE) | ||
| ) { | ||
| return currentState; | ||
| } | ||
| return EMPTY_SHORTCUT_MODIFIER_STATE; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the web/desktop shortcut-state hook, a dictation tool that injects Control/Meta keydown, V keydown, and paste but omits the matching keyup is classified as keyboard-initiated here. The paste handler then preserves the held modifier, leaving the thread-jump hints stuck—the failure this change is intended to fix. The paste reset must distinguish ordinary keyboard input from an injected sequence without relying only on the preceding V keydown.
AGENTS.md reference: AGENTS.md:L142-L142
Useful? React with 👍 / 👎.