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
This commit is contained in:
Alex Blank
2026-05-29 20:08:58 +02:00
parent ca9db195de
commit ef9ac76f06
2 changed files with 5 additions and 79 deletions
-78
View File
@@ -429,74 +429,7 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
};
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<TerminalRef, TerminalProps>(
"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");
}