feat(FN-2075): merge fusion/fn-2075
This commit is contained in:
@@ -679,6 +679,34 @@ export function TerminalModal({ isOpen, onClose, initialCommand, projectId }: Te
|
||||
}
|
||||
}, [connectionStatus]);
|
||||
|
||||
/**
|
||||
* On mobile browsers, opening the soft keyboard requires focus to happen
|
||||
* within a real user gesture. Programmatic focus in async effects is often
|
||||
* ignored even though xterm stays connected and receives output.
|
||||
*/
|
||||
const handleTerminalGestureFocus = useCallback(() => {
|
||||
if (!terminalRef.current) return;
|
||||
|
||||
// Ensure xterm updates its own focus state first.
|
||||
xtermRef.current?.focus();
|
||||
|
||||
const helperTextarea = terminalRef.current.querySelector(
|
||||
".xterm-helper-textarea",
|
||||
) as HTMLTextAreaElement | undefined;
|
||||
|
||||
if (!helperTextarea) return;
|
||||
|
||||
try {
|
||||
helperTextarea.focus({ preventScroll: true });
|
||||
} catch {
|
||||
helperTextarea.focus();
|
||||
}
|
||||
|
||||
// Keep caret at end so subsequent key presses append naturally.
|
||||
const caretPos = helperTextarea.value.length;
|
||||
helperTextarea.setSelectionRange(caretPos, caretPos);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Auto-recover when the server reports the session is invalid (code 4004).
|
||||
*
|
||||
@@ -953,6 +981,8 @@ export function TerminalModal({ isOpen, onClose, initialCommand, projectId }: Te
|
||||
ref={terminalRef}
|
||||
className="terminal-xterm"
|
||||
data-testid="terminal-xterm"
|
||||
onPointerDown={handleTerminalGestureFocus}
|
||||
onTouchStart={handleTerminalGestureFocus}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3602,6 +3602,45 @@ describe("TerminalModal — xterm focus initialization (FN-1602)", () => {
|
||||
expect(screen.getByTestId("terminal-modal")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it("focuses xterm helper textarea on user pointer gesture", async () => {
|
||||
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);
|
||||
|
||||
fireEvent.pointerDown(terminalDiv);
|
||||
|
||||
expect(mockTerminalInstance.focus).toHaveBeenCalled();
|
||||
expect(focusSpy).toHaveBeenCalled();
|
||||
expect(setSelectionRangeSpy).toHaveBeenCalledWith(0, 0);
|
||||
});
|
||||
|
||||
it("focuses xterm helper textarea on touch gesture", async () => {
|
||||
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");
|
||||
terminalDiv.appendChild(helperTextarea);
|
||||
|
||||
fireEvent.touchStart(terminalDiv);
|
||||
|
||||
expect(focusSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// --- FN-1765: Project-context propagation ---
|
||||
|
||||
Reference in New Issue
Block a user