diff --git a/apps/web/src/components/terminal.tsx b/apps/web/src/components/terminal.tsx index ceceb81..a62272b 100644 --- a/apps/web/src/components/terminal.tsx +++ b/apps/web/src/components/terminal.tsx @@ -430,32 +430,36 @@ export const TerminalComponent = React.forwardRef( document.addEventListener("visibilitychange", handleVisibilityChange); // Mobile touch scroll forwarding. - // xterm.js intercepts touch events for selection, which blocks the - // browser from scrolling the viewport. We listen on the container in - // capture phase, decide early if the gesture is vertical, and then - // preventDefault + stopPropagation so xterm.js never sees the event. - // The scroll is forwarded to term.scrollLines(). + // xterm.js intercepts touch events for selection, blocking native scroll. + // We attach to *document* in capture phase so we run before xterm.js + // even on child elements. We only act when the touch target is inside + // this terminal container. let touchStartY = 0; let touchStartX = 0; let isVerticalScroll = false; let accumulatedDeltaY = 0; + const isInThisTerminal = (target: EventTarget | null): boolean => { + if (!(target instanceof Node)) return false; + return container.contains(target); + }; + const handleTouchStart = (e: TouchEvent) => { - if (e.touches.length === 1) { - touchStartY = e.touches[0].clientY; - touchStartX = e.touches[0].clientX; - isVerticalScroll = false; - accumulatedDeltaY = 0; - } + if (e.touches.length !== 1) return; + if (!isInThisTerminal(e.target)) return; + touchStartY = e.touches[0].clientY; + touchStartX = e.touches[0].clientX; + isVerticalScroll = false; + accumulatedDeltaY = 0; }; const handleTouchMove = (e: TouchEvent) => { if (e.touches.length !== 1 || !termRef.current) return; + if (!isInThisTerminal(e.target)) return; const touch = e.touches[0]; const deltaY = touchStartY - touch.clientY; const deltaX = Math.abs(touchStartX - touch.clientX); - // Decide scroll direction on the first meaningful movement (3px) if (!isVerticalScroll) { if (Math.abs(deltaY) > deltaX && Math.abs(deltaY) > 3) { isVerticalScroll = true; @@ -463,16 +467,12 @@ export const TerminalComponent = React.forwardRef( } if (isVerticalScroll) { - // preventDefault MUST be called on the first matching touchmove - // or the browser compositor will already have committed to page - // scroll before our JS runs. e.preventDefault(); e.stopPropagation(); accumulatedDeltaY += deltaY; touchStartY = touch.clientY; - const lines = Math.round(accumulatedDeltaY / 24); + const lines = Math.round(accumulatedDeltaY / 20); if (lines !== 0) { - // Negative = scroll up (show older buffer content) termRef.current.scrollLines(-lines); accumulatedDeltaY = 0; } @@ -485,15 +485,15 @@ export const TerminalComponent = React.forwardRef( }; if (isMobile) { - container.addEventListener("touchstart", handleTouchStart, { + document.addEventListener("touchstart", handleTouchStart, { passive: true, capture: true, }); - container.addEventListener("touchmove", handleTouchMove, { + document.addEventListener("touchmove", handleTouchMove, { passive: false, capture: true, }); - container.addEventListener("touchend", handleTouchEnd, { + document.addEventListener("touchend", handleTouchEnd, { capture: true, }); } @@ -510,13 +510,13 @@ export const TerminalComponent = React.forwardRef( handleVisibilityChange, ); if (isMobile) { - container.removeEventListener("touchstart", handleTouchStart, { + document.removeEventListener("touchstart", handleTouchStart, { capture: true, }); - container.removeEventListener("touchmove", handleTouchMove, { + document.removeEventListener("touchmove", handleTouchMove, { capture: true, }); - container.removeEventListener("touchend", handleTouchEnd, { + document.removeEventListener("touchend", handleTouchEnd, { capture: true, }); } diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index e2872a9..66d6087 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -3707,6 +3707,7 @@ a.nav-item, padding: 0; overflow: hidden; position: relative; + touch-action: none; } /* xterm.js manages its own sizing */