mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 06:05:24 +00:00
fix(remote-desktop): release held keys on focus loss (#1498)
This commit is contained in:
1 parent
a4a4d4f27b
commit
fb28cd1f21
2 files changed
+108
-12
No files matched your search
@@ -345,9 +345,9 @@ export const GuacamoleDisplay = forwardRef<
|
||||
(hasKeyboardFocusRef.current || displayIsFocused);
|
||||
|
||||
if (!shouldCaptureInput) {
|
||||
keyboard.reset();
|
||||
keyboard.onkeydown = null;
|
||||
keyboard.onkeyup = null;
|
||||
keyboard.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -366,17 +366,8 @@ export const GuacamoleDisplay = forwardRef<
|
||||
};
|
||||
|
||||
keyboard.onkeyup = (keysym: number) => {
|
||||
if (!clientRef.current) return;
|
||||
if (!isVisible || !windowFocusedRef.current) return;
|
||||
|
||||
const activeDisplay = displayElementRef.current;
|
||||
const stillFocused =
|
||||
!!activeDisplay &&
|
||||
typeof document !== "undefined" &&
|
||||
document.activeElement === activeDisplay;
|
||||
|
||||
if (!hasKeyboardFocusRef.current && !stillFocused) return;
|
||||
clientRef.current.sendKeyEvent(0, keysym);
|
||||
// Focus loss must still release keys already sent to the remote host.
|
||||
clientRef.current?.sendKeyEvent(0, keysym);
|
||||
};
|
||||
}, [isVisible]);
|
||||
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import ts from "typescript";
|
||||
import Guacamole from "guacamole-common-js";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import displaySource from "../../src/frontend/GuacamoleDisplay.tsx?raw";
|
||||
|
||||
// Exercise the display's actual focus callback with Guacamole's real keyboard.
|
||||
const source = ts.createSourceFile(
|
||||
"GuacamoleDisplay.tsx",
|
||||
displaySource,
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
ts.ScriptKind.TSX,
|
||||
);
|
||||
let callback: string | undefined;
|
||||
function visit(node: ts.Node) {
|
||||
if (
|
||||
ts.isVariableDeclaration(node) &&
|
||||
node.name.getText(source) === "refreshKeyboardHandlers" &&
|
||||
node.initializer &&
|
||||
ts.isCallExpression(node.initializer)
|
||||
) {
|
||||
callback = node.initializer.arguments[0].getText(source);
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
}
|
||||
visit(source);
|
||||
if (!callback) throw new Error("Missing refreshKeyboardHandlers callback");
|
||||
const runRefresh = new Function(
|
||||
"state",
|
||||
ts.transpileModule(
|
||||
`const { keyboardRef, clientRef, displayElementRef, isVisible, windowFocusedRef, hasKeyboardFocusRef } = state;
|
||||
(${callback})();`,
|
||||
{ compilerOptions: { target: ts.ScriptTarget.ES2022 } },
|
||||
).outputText,
|
||||
) as (state: Record<string, unknown>) => void;
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
document.body.replaceChildren();
|
||||
});
|
||||
|
||||
describe("remote keyboard focus loss", () => {
|
||||
it.each(["window", "display", "hidden", "tab"])(
|
||||
"releases held modifiers when losing %s focus",
|
||||
(reason) => {
|
||||
const display = document.createElement("div");
|
||||
display.tabIndex = 0;
|
||||
document.body.append(display);
|
||||
display.focus();
|
||||
vi.spyOn(document, "visibilityState", "get").mockReturnValue("visible");
|
||||
const keyboard = new Guacamole.Keyboard(display) as Guacamole.Keyboard & {
|
||||
press(keysym: number): boolean;
|
||||
release(keysym: number): void;
|
||||
pressed: Record<number, boolean>;
|
||||
};
|
||||
const sendKeyEvent = vi.fn();
|
||||
const state = {
|
||||
keyboardRef: { current: keyboard },
|
||||
clientRef: { current: { sendKeyEvent } },
|
||||
displayElementRef: { current: display },
|
||||
isVisible: true,
|
||||
windowFocusedRef: { current: true },
|
||||
hasKeyboardFocusRef: { current: true },
|
||||
};
|
||||
const refresh = () => runRefresh(state);
|
||||
refresh();
|
||||
const keys = [0xffeb, 0xffe3, 0xffe9]; // Windows, Ctrl, Alt
|
||||
keys.forEach((key) => keyboard.press(key));
|
||||
expect(sendKeyEvent.mock.calls).toEqual(keys.map((key) => [1, key]));
|
||||
|
||||
if (reason === "window") state.windowFocusedRef.current = false;
|
||||
if (reason === "tab") state.isVisible = false;
|
||||
if (reason === "hidden") {
|
||||
vi.spyOn(document, "visibilityState", "get").mockReturnValue("hidden");
|
||||
}
|
||||
if (reason === "display") {
|
||||
state.hasKeyboardFocusRef.current = false;
|
||||
display.blur();
|
||||
}
|
||||
refresh();
|
||||
expect(sendKeyEvent.mock.calls.slice(3)).toEqual(
|
||||
[...keys].sort((a, b) => a - b).map((key) => [0, key]),
|
||||
);
|
||||
expect(Object.keys(keyboard.pressed)).toEqual([]);
|
||||
expect(keyboard.onkeydown).toBeNull();
|
||||
expect(keyboard.onkeyup).toBeNull();
|
||||
keyboard.press(0x6c);
|
||||
keyboard.reset();
|
||||
expect(sendKeyEvent).toHaveBeenCalledTimes(6);
|
||||
|
||||
state.isVisible = true;
|
||||
state.windowFocusedRef.current = true;
|
||||
state.hasKeyboardFocusRef.current = true;
|
||||
vi.spyOn(document, "visibilityState", "get").mockReturnValue("visible");
|
||||
display.focus();
|
||||
refresh();
|
||||
keyboard.press(0x6c);
|
||||
keyboard.release(0x6c);
|
||||
expect(sendKeyEvent.mock.calls.slice(6)).toEqual([
|
||||
[1, 0x6c],
|
||||
[0, 0x6c],
|
||||
]);
|
||||
},
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user