fix(FN-000): skip iOS gesture focus handler so typed keys reach xterm
On iOS Safari/PWA, tapping the terminal opened the on-screen keyboard but
keystrokes were silently dropped. After the earlier CSS fix (c7266b7f) the
helper textarea now covers the terminal surface and iOS focuses it natively
on tap — but the bubble-phase onPointerDown/onTouchStart handler kept
re-focusing xterm + the textarea and calling setSelectionRange during the
touch gesture. That is the same class of re-focus-mid-gesture that the
prior commit identified as disrupting iOS input attribution; moving from
capture to bubble phase wasn't enough.
- Early-return from handleTerminalGestureFocus on
(hover: none) and (pointer: coarse), so iOS handles focus with no JS
interference.
- Desktop (fine pointer) keeps the existing behavior because the textarea
stays 1x1 off-screen and still needs programmatic focus on canvas click.
- Add a regression test covering the no-op path with the media query mocked.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
5
.changeset/fix-ios-terminal-input.md
Normal file
5
.changeset/fix-ios-terminal-input.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix iOS terminal typing in the dashboard. On touch-primary devices, tapping the terminal opened the on-screen keyboard but keystrokes were silently dropped because the bubble-phase `handleTerminalGestureFocus` handler re-focused the helper textarea and reset its selection during touchstart/pointerdown, disrupting iOS's input-event attribution. The CSS fix in commit c7266b7f already positions the textarea to receive taps natively, so the JS handler is now a no-op on `(hover: none) and (pointer: coarse)` devices and desktop retains click-to-focus.
|
||||||
@@ -683,10 +683,25 @@ export function TerminalModal({ isOpen, onClose, initialCommand, projectId }: Te
|
|||||||
* On mobile browsers, opening the soft keyboard requires focus to happen
|
* On mobile browsers, opening the soft keyboard requires focus to happen
|
||||||
* within a real user gesture. Programmatic focus in async effects is often
|
* within a real user gesture. Programmatic focus in async effects is often
|
||||||
* ignored even though xterm stays connected and receives output.
|
* ignored even though xterm stays connected and receives output.
|
||||||
|
*
|
||||||
|
* On touch-primary devices, the CSS sizes `.xterm-helper-textarea` to cover
|
||||||
|
* the whole terminal surface (see styles.css @media (hover: none) and
|
||||||
|
* (pointer: coarse)), so iOS focuses it natively on tap. Re-focusing and
|
||||||
|
* calling setSelectionRange inside the touchstart/pointerdown handler
|
||||||
|
* disrupts iOS's input-event attribution (same class of bug the prior
|
||||||
|
* capture-phase handlers caused — see commit c7266b7f), and subsequent
|
||||||
|
* keystrokes are silently dropped. Early-return on touch-primary so iOS
|
||||||
|
* handles focus with no JS interference.
|
||||||
*/
|
*/
|
||||||
const handleTerminalGestureFocus = useCallback(() => {
|
const handleTerminalGestureFocus = useCallback(() => {
|
||||||
if (!terminalRef.current) return;
|
if (!terminalRef.current) return;
|
||||||
|
|
||||||
|
const isTouchPrimary =
|
||||||
|
typeof window !== "undefined" &&
|
||||||
|
typeof window.matchMedia === "function" &&
|
||||||
|
window.matchMedia("(hover: none) and (pointer: coarse)")?.matches === true;
|
||||||
|
if (isTouchPrimary) return;
|
||||||
|
|
||||||
// Ensure xterm updates its own focus state first.
|
// Ensure xterm updates its own focus state first.
|
||||||
xtermRef.current?.focus();
|
xtermRef.current?.focus();
|
||||||
|
|
||||||
|
|||||||
@@ -3646,6 +3646,50 @@ describe("TerminalModal — xterm focus initialization (FN-1602)", () => {
|
|||||||
expect(helperTextarea.autocorrect).toBe("off");
|
expect(helperTextarea.autocorrect).toBe("off");
|
||||||
expect(helperTextarea.spellcheck).toBe(false);
|
expect(helperTextarea.spellcheck).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// On touch-primary devices (iOS, Android), the CSS sizes the helper textarea
|
||||||
|
// to cover the whole terminal so iOS focuses it natively on tap. Re-focusing
|
||||||
|
// in the bubble-phase gesture handler disrupts iOS input-event attribution
|
||||||
|
// and causes typed keys to be silently dropped. The handler must be a no-op
|
||||||
|
// in that environment — see commit c7266b7f for prior iOS input fix context.
|
||||||
|
it("no-ops gesture focus handler on touch-primary devices", async () => {
|
||||||
|
const matchMediaSpy = vi
|
||||||
|
.spyOn(window, "matchMedia")
|
||||||
|
.mockImplementation((query: string) => ({
|
||||||
|
matches: query === "(hover: none) and (pointer: coarse)",
|
||||||
|
media: query,
|
||||||
|
onchange: null,
|
||||||
|
addListener: vi.fn(),
|
||||||
|
removeListener: vi.fn(),
|
||||||
|
addEventListener: vi.fn(),
|
||||||
|
removeEventListener: vi.fn(),
|
||||||
|
dispatchEvent: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
try {
|
||||||
|
render(<TerminalModal isOpen={true} onClose={mockOnClose} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockTerminalInstance.open).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
const terminalDiv = screen.getByTestId("terminal-xterm");
|
||||||
|
const helperTextarea = document.createElement("textarea");
|
||||||
|
helperTextarea.className = "xterm-helper-textarea";
|
||||||
|
const focusSpy = vi.spyOn(helperTextarea, "focus");
|
||||||
|
const setSelectionRangeSpy = vi.spyOn(helperTextarea, "setSelectionRange");
|
||||||
|
terminalDiv.appendChild(helperTextarea);
|
||||||
|
|
||||||
|
mockTerminalInstance.focus.mockClear();
|
||||||
|
fireEvent.touchStart(terminalDiv);
|
||||||
|
|
||||||
|
expect(mockTerminalInstance.focus).not.toHaveBeenCalled();
|
||||||
|
expect(focusSpy).not.toHaveBeenCalled();
|
||||||
|
expect(setSelectionRangeSpy).not.toHaveBeenCalled();
|
||||||
|
} finally {
|
||||||
|
matchMediaSpy.mockRestore();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- FN-1765: Project-context propagation ---
|
// --- FN-1765: Project-context propagation ---
|
||||||
|
|||||||
Reference in New Issue
Block a user