fix(FN-000): restore iOS terminal input by full-covering helper textarea

On iOS Safari/PWA, tapping the terminal opened the keyboard but typed
keys produced no output. Focus landed on a 1×1 opacity-0.01 helper
textarea overlapped by the xterm canvas, and capture-phase gesture
handlers re-focused on every tap — a combination iOS silently drops
input events on.

- On touch-primary devices, expand .xterm-helper-textarea to cover the
  entire terminal surface (100% × 100%, opacity 0, z-index 2) via
  @media (hover: none) and (pointer: coarse). Taps land on the
  textarea directly, so iOS grants keyboard + input events natively.
- Desktop keeps the 1×1 rule so xterm's canvas-level drag-to-select
  and mouse tracking continue to work.
- Revert onPointerDownCapture/onTouchStartCapture/onClickCapture back
  to bubble-phase onPointerDown/onTouchStart; capture phase was
  confusing iOS's focus attribution and is no longer needed now that
  taps reach the textarea directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-04-20 10:42:44 -07:00
parent c19ac60d10
commit c7266b7fbd
3 changed files with 23 additions and 10 deletions

View File

@@ -989,9 +989,8 @@ export function TerminalModal({ isOpen, onClose, initialCommand, projectId }: Te
ref={terminalRef}
className="terminal-xterm"
data-testid="terminal-xterm"
onPointerDownCapture={handleTerminalGestureFocus}
onTouchStartCapture={handleTerminalGestureFocus}
onClickCapture={handleTerminalGestureFocus}
onPointerDown={handleTerminalGestureFocus}
onTouchStart={handleTerminalGestureFocus}
/>
</div>

View File

@@ -3603,7 +3603,7 @@ describe("TerminalModal — xterm focus initialization (FN-1602)", () => {
});
});
it("focuses xterm helper textarea on user pointer gesture (capture phase)", async () => {
it("focuses xterm helper textarea on user pointer gesture", async () => {
render(<TerminalModal isOpen={true} onClose={mockOnClose} />);
await waitFor(() => {
@@ -3617,12 +3617,7 @@ describe("TerminalModal — xterm focus initialization (FN-1602)", () => {
const setSelectionRangeSpy = vi.spyOn(helperTextarea, "setSelectionRange");
terminalDiv.appendChild(helperTextarea);
// Simulate xterm child event handlers stopping bubble propagation.
const child = document.createElement("div");
child.addEventListener("pointerdown", (event) => event.stopPropagation());
terminalDiv.appendChild(child);
fireEvent.pointerDown(child);
fireEvent.pointerDown(terminalDiv);
expect(mockTerminalInstance.focus).toHaveBeenCalled();
expect(focusSpy).toHaveBeenCalled();

View File

@@ -9995,6 +9995,25 @@ input[type="range"]:focus-visible {
z-index: 1 !important;
}
/*
* On touch-primary devices (mobile/tablet), expand the helper textarea to
* cover the entire terminal surface so taps land directly on it. iOS Safari
* reliably opens the soft keyboard and routes input events only when focus
* originates from a tap on the focused element itself — programmatic focus()
* from a parent's touch handler is often silently ignored. The textarea
* remains visually invisible via opacity:0 and sits above the canvas so
* tap/drag events land on it. Desktop (mouse) keeps the 1×1 rule so xterm
* can still handle drag-to-select and mouse-tracking on the canvas.
*/
@media (hover: none) and (pointer: coarse) {
.terminal-xterm .xterm .xterm-helper-textarea {
width: 100% !important;
height: 100% !important;
opacity: 0 !important;
z-index: 2 !important;
}
}
.terminal-loading {
display: flex;
flex-direction: column;