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
2 changes: 1 addition & 1 deletion docs/specs/mobile-terminal-ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ Source of truth: `MobileTerminalUi` and `Composer` in `lib/src/components/Mobile

## Physical keyboard

**While the composer is closed and the terminal is showing, a physical keyboard types straight into the terminal.** **Must leave to the page** a key aimed at a focused field or control, a Command or Option chord, and IME composition. A soft keyboard opens only for a focused field, so it never reaches this path.
**While the composer is closed, no gesture confirmation is open, and the terminal is showing, a physical keyboard types straight into the terminal.** **Must leave to the page** a key aimed at a focused field or control, a Command or Option chord, and IME composition. A soft keyboard opens only for a focused field, so it never reaches this path.

Source of truth: `hardwareKeySequence` and `isHardwareKeyTarget` in `lib/src/components/MobileTerminalUi.tsx`.

Expand Down
17 changes: 17 additions & 0 deletions lib/src/components/MobileTerminalUi.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { setNativeFieldValue } from '../lib/dom';
import { pointerEvent, PortalAnchoredButton } from './wall/wall-test-utils';
import * as registry from '../lib/terminal-registry';
import * as activity from '../lib/session-activity-store';
import * as gestureMenu from '../lib/mobile-gesture-menu';
import { beginClipboardAttempt, dismissClipboardFailure, reportClipboardFailure } from '../lib/clipboard-failure';
import type { Terminal } from '@xterm/xterm';

Expand Down Expand Up @@ -316,6 +317,22 @@ describe('MobileTerminalUi physical keyboard', () => {
keydown(document.body, { key: 'a' });
expect(onSendInput).not.toHaveBeenCalled();
});

it('types nothing while a gesture\'s confirmation is open', () => {
const onSendInput = vi.fn();
const { container, terminal } = renderUi({ onSendInput });
vi.spyOn(gestureMenu, 'finishMobileGesture').mockReturnValue({
state: gestureMenu.MOBILE_GESTURE_IDLE_STATE,
action: { kind: 'confirm', confirmation: 'ctrlC', action: { kind: 'input', input: 'ctrlC' } },
});
act(() => {
terminal.dispatchEvent(pointerEvent('pointerdown'));
terminal.dispatchEvent(pointerEvent('pointerup'));
});
expect(container.querySelector('[data-mobile-gesture-dialog]')).not.toBeNull();
expect(keydown(document.body, { key: 'Enter' }).defaultPrevented).toBe(false);
expect(onSendInput).not.toHaveBeenCalled();
});
});

describe('MobileTerminalUi controls inside the host', () => {
Expand Down
10 changes: 6 additions & 4 deletions lib/src/components/MobileTerminalUi.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -824,10 +824,12 @@ export function MobileTerminalUi({
}, [closeComposer, interactive, onPaste, sendInput]);

// A physical keyboard types straight into the terminal while the composer is
// closed. No soft keyboard reaches here: one opens only for a focused field.
// Its Command+V comes back from the page as a `paste` on the same targets.
// closed and no gesture confirmation is asking. No soft keyboard reaches
// here: one opens only for a focused field. Its Command+V comes back from
// the page as a `paste` on the same targets.
const confirming = pendingGestureConfirmation !== null;
useEffect(() => {
if (!terminalInteractive || composing || view !== 'terminal') return;
if (!terminalInteractive || composing || confirming || view !== 'terminal') return;
const onKeyDown = (event: globalThis.KeyboardEvent) => {
const root = rootRef.current;
if (!root || event.defaultPrevented || isComposingKey(event)) return;
Expand All @@ -851,7 +853,7 @@ export function MobileTerminalUi({
window.removeEventListener('keydown', onKeyDown);
window.removeEventListener('paste', onNativePaste);
};
}, [composing, onPaste, sendInput, terminalInteractive, view]);
}, [composing, confirming, onPaste, sendInput, terminalInteractive, view]);

useEffect(() => {
if (!terminalInteractive || view !== 'terminal') closeComposer();
Expand Down
Loading