Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions web/packages/shared/components/DesktopSession/InputHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,5 +188,141 @@ describe('InputHandler', () => {
);
expect(shiftCalls).toHaveLength(0);
});

it('Control and Alt are not synchronized when AltGr pressed', () => {
// First, set Control and Alt to DOWN to mimick the what happens when a
// user presses AltGraph in a browser.
const ctrlDownEvent = new KeyboardEvent('keydown', {
code: 'ControlLeft',
});
inputHandler.handleInputEvent({
e: ctrlDownEvent,
state: ButtonState.DOWN,
cli: mockTdpClient,
});

const altDownEvent = new KeyboardEvent('keydown', { code: 'AltRight' });
inputHandler.handleInputEvent({
e: altDownEvent,
state: ButtonState.DOWN,
cli: mockTdpClient,
});

// Clear these events so they're not counted below
mockTdpClient.sendKeyboardInput.mockClear();

const altGrEvent = new KeyboardEvent('keydown', {
code: 'KeyQ',
ctrlKey: false,
altKey: false,
});

// There isn't a key code for AltGraph, so instead mock the
// getModifierState to return true for AltGraph
Object.defineProperty(altGrEvent, 'getModifierState', {
value: (key: string) => key === 'AltGraph',
});

inputHandler.handleInputEvent({
e: altGrEvent,
state: ButtonState.DOWN,
cli: mockTdpClient,
});

// Check that synchronizeModifierState doesn't try to set the remote state
// (down) to that of the local state (up)
const controlCalls = mockTdpClient.sendKeyboardInput.mock.calls.filter(
call => call[0].includes('Control')
);
const altCalls = mockTdpClient.sendKeyboardInput.mock.calls.filter(call =>
call[0].includes('Alt')
);

expect(controlCalls).toHaveLength(0);
expect(altCalls).toHaveLength(0);
});

it('Shift and Meta are still synchronized when AltGr is active', () => {
const shiftDownEvent = new KeyboardEvent('keydown', {
code: 'ShiftLeft',
});
inputHandler.handleInputEvent({
e: shiftDownEvent,
state: ButtonState.DOWN,
cli: mockTdpClient,
});

const metaDownEvent = new KeyboardEvent('keydown', { code: 'MetaLeft' });
inputHandler.handleInputEvent({
e: metaDownEvent,
state: ButtonState.DOWN,
cli: mockTdpClient,
});

mockTdpClient.sendKeyboardInput.mockClear();

// Press AltGr key with Shift and Meta released
const altGrEvent = new KeyboardEvent('keydown', {
code: 'KeyA',
shiftKey: false,
ctrlKey: false,
altKey: false,
metaKey: false,
});

Object.defineProperty(altGrEvent, 'getModifierState', {
value: (key: string) => key === 'AltGraph',
});

inputHandler.handleInputEvent({
e: altGrEvent,
state: ButtonState.DOWN,
cli: mockTdpClient,
});

// Shift & Meta should still be synchronized to UP even with AltGr active
expect(mockTdpClient.sendKeyboardInput).toHaveBeenCalledWith(
'ShiftLeft',
ButtonState.UP
);
expect(mockTdpClient.sendKeyboardInput).toHaveBeenCalledWith(
'MetaLeft',
ButtonState.UP
);
});

it('handles AltGr + Shift combination correctly', () => {
const altGrShiftEvent = new KeyboardEvent('keydown', {
code: 'KeyA',
shiftKey: true,
ctrlKey: false,
altKey: false,
});

Object.defineProperty(altGrShiftEvent, 'getModifierState', {
value: (key: string) => key === 'AltGraph' || key === 'Shift',
});

inputHandler.handleInputEvent({
e: altGrShiftEvent,
state: ButtonState.DOWN,
cli: mockTdpClient,
});

// Shift should be synchronized, but not Control/Alt
const shiftCalls = mockTdpClient.sendKeyboardInput.mock.calls.filter(
call => call[0].includes('Shift')
);
const controlCalls = mockTdpClient.sendKeyboardInput.mock.calls.filter(
call => call[0].includes('Control')
);
const altCalls = mockTdpClient.sendKeyboardInput.mock.calls.filter(call =>
call[0].includes('Alt')
);

expect(shiftCalls.length).toBeGreaterThan(0);
expect(controlCalls).toHaveLength(0);
expect(altCalls).toHaveLength(0);
});
});
});
12 changes: 12 additions & 0 deletions web/packages/shared/components/DesktopSession/InputHandler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,18 @@ export class InputHandler {
return;
}

// Check if AltGraph is being pressed
const isAltGraphActive = e.getModifierState('AltGraph');

this.remoteModifierState.forEach((state, modifier) => {
// When AltGr is pressed, browsers synthesize ControlLeft+AltRight, but
// getModifierState('Control') and getModifierState('Alt') return false
// because the physical keys aren't pressed. Skip sync to avoid sending
// incorrect UP events while AltGr is active.
if (isAltGraphActive && (modifier === 'Control' || modifier === 'Alt')) {
return;
}

const localState = e.getModifierState(modifier)
? ButtonState.DOWN
: ButtonState.UP;
Expand All @@ -200,6 +211,7 @@ export class InputHandler {
// If the local state is different from the remote state, send the updates.
cli.sendKeyboardInput(modifier + 'Left', localState);
cli.sendKeyboardInput(modifier + 'Right', localState);

// Update the remote state to match the local state.
this.remoteModifierState.set(modifier, localState);
}
Expand Down
Loading