mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-31 15:45:23 +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);
|
(hasKeyboardFocusRef.current || displayIsFocused);
|
||||||
|
|
||||||
if (!shouldCaptureInput) {
|
if (!shouldCaptureInput) {
|
||||||
|
keyboard.reset();
|
||||||
keyboard.onkeydown = null;
|
keyboard.onkeydown = null;
|
||||||
keyboard.onkeyup = null;
|
keyboard.onkeyup = null;
|
||||||
keyboard.reset();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,17 +366,8 @@ export const GuacamoleDisplay = forwardRef<
|
|||||||
};
|
};
|
||||||
|
|
||||||
keyboard.onkeyup = (keysym: number) => {
|
keyboard.onkeyup = (keysym: number) => {
|
||||||
if (!clientRef.current) return;
|
// Focus loss must still release keys already sent to the remote host.
|
||||||
if (!isVisible || !windowFocusedRef.current) return;
|
clientRef.current?.sendKeyEvent(0, keysym);
|
||||||
|
|
||||||
const activeDisplay = displayElementRef.current;
|
|
||||||
const stillFocused =
|
|
||||||
!!activeDisplay &&
|
|
||||||
typeof document !== "undefined" &&
|
|
||||||
document.activeElement === activeDisplay;
|
|
||||||
|
|
||||||
if (!hasKeyboardFocusRef.current && !stillFocused) return;
|
|
||||||
clientRef.current.sendKeyEvent(0, keysym);
|
|
||||||
};
|
};
|
||||||
}, [isVisible]);
|
}, [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