From ef9ac76f06c12e16872610b3215a956a2be0a8ef Mon Sep 17 00:00:00 2001 From: Alex Blank Date: Fri, 29 May 2026 20:08:58 +0200 Subject: [PATCH] fix: remove all touch interception, let browser scroll xterm viewport natively - xterm.js has zero touch event handlers (verified: only 1 'touch' ref in entire library), so it wasn't intercepting anything - Our touch-action: none + preventDefault() combo was blocking the browser from scrolling the .xterm-viewport natively - Removed all custom touch event handlers from terminal.tsx - Removed touch-action: none from .terminal-container - Added touch-action: pan-y to .xterm-viewport so browser allows vertical pan - Body scroll lock (terminal-page-open) prevents page from scrolling --- apps/web/src/components/terminal.tsx | 78 ---------------------------- apps/web/src/styles.css | 6 ++- 2 files changed, 5 insertions(+), 79 deletions(-) diff --git a/apps/web/src/components/terminal.tsx b/apps/web/src/components/terminal.tsx index a62272b..117820d 100644 --- a/apps/web/src/components/terminal.tsx +++ b/apps/web/src/components/terminal.tsx @@ -429,74 +429,7 @@ export const TerminalComponent = React.forwardRef( }; document.addEventListener("visibilitychange", handleVisibilityChange); - // Mobile touch scroll forwarding. - // 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) 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); - - if (!isVerticalScroll) { - if (Math.abs(deltaY) > deltaX && Math.abs(deltaY) > 3) { - isVerticalScroll = true; - } - } - - if (isVerticalScroll) { - e.preventDefault(); - e.stopPropagation(); - accumulatedDeltaY += deltaY; - touchStartY = touch.clientY; - const lines = Math.round(accumulatedDeltaY / 20); - if (lines !== 0) { - termRef.current.scrollLines(-lines); - accumulatedDeltaY = 0; - } - } - }; - - const handleTouchEnd = () => { - isVerticalScroll = false; - accumulatedDeltaY = 0; - }; - - if (isMobile) { - document.addEventListener("touchstart", handleTouchStart, { - passive: true, - capture: true, - }); - document.addEventListener("touchmove", handleTouchMove, { - passive: false, - capture: true, - }); - document.addEventListener("touchend", handleTouchEnd, { - capture: true, - }); - } return () => { isUnmountingRef.current = true; @@ -509,17 +442,6 @@ export const TerminalComponent = React.forwardRef( "visibilitychange", handleVisibilityChange, ); - if (isMobile) { - document.removeEventListener("touchstart", handleTouchStart, { - capture: true, - }); - document.removeEventListener("touchmove", handleTouchMove, { - capture: true, - }); - document.removeEventListener("touchend", handleTouchEnd, { - capture: true, - }); - } if (ws) { ws.close(1000, "Component unmounting"); } diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index 66d6087..a77c97a 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -3707,13 +3707,17 @@ a.nav-item, padding: 0; overflow: hidden; position: relative; - touch-action: none; } /* xterm.js manages its own sizing */ /* xterm.js manages its own scrolling and viewport dimensions */ +/* Mobile: allow native vertical touch panning on xterm viewport */ +.terminal-wrapper.mobile .xterm .xterm-viewport { + touch-action: pan-y; +} + /* Special Keys Strip */ .special-keys-strip { flex-shrink: 0;