feat: touch swipe scrolling in terminal on mobile

- Intercepts touch events on the terminal container when isMobile=true
- Detects vertical swipe gestures (dominant over horizontal movement)
- Translates swipe distance to xterm.js scrollLines() calls
- Uses requestAnimationFrame for smooth scroll updates
- Threshold of 10px before scroll kicks in; 30px per line
- Touch listeners cleaned up on component unmount
This commit is contained in:
Alex Blank
2026-05-29 19:20:52 +02:00
parent c2740cd282
commit aa34314175
+56
View File
@@ -429,6 +429,56 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
};
document.addEventListener("visibilitychange", handleVisibilityChange);
// Mobile touch scrolling — translate vertical swipe to terminal scroll
let touchStartY = 0;
let touchStartX = 0;
let isTouchScrolling = false;
let touchScrollRaf: number | null = null;
const handleTouchStart = (e: TouchEvent) => {
if (e.touches.length === 1) {
touchStartY = e.touches[0].clientY;
touchStartX = e.touches[0].clientX;
isTouchScrolling = false;
}
};
const handleTouchMove = (e: TouchEvent) => {
if (e.touches.length !== 1 || !termRef.current) return;
const touch = e.touches[0];
const deltaY = touchStartY - touch.clientY;
const deltaX = Math.abs(touchStartX - touch.clientX);
// If vertical movement dominates and exceeds threshold, scroll terminal buffer
if (Math.abs(deltaY) > deltaX && Math.abs(deltaY) > 10) {
if (!isTouchScrolling) isTouchScrolling = true;
e.preventDefault();
if (touchScrollRaf) cancelAnimationFrame(touchScrollRaf);
touchScrollRaf = requestAnimationFrame(() => {
if (!termRef.current) return;
const lines = Math.round(deltaY / 30);
if (lines !== 0) {
termRef.current.scrollLines(lines);
touchStartY = touch.clientY;
}
touchScrollRaf = null;
});
}
};
const handleTouchEnd = () => {
isTouchScrolling = false;
if (touchScrollRaf) {
cancelAnimationFrame(touchScrollRaf);
touchScrollRaf = null;
}
};
if (isMobile) {
container.addEventListener("touchstart", handleTouchStart, { passive: true });
container.addEventListener("touchmove", handleTouchMove, { passive: false });
container.addEventListener("touchend", handleTouchEnd);
}
return () => {
isUnmountingRef.current = true;
clearTimeout(resizeTimeout);
@@ -440,6 +490,12 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
"visibilitychange",
handleVisibilityChange,
);
if (isMobile) {
container.removeEventListener("touchstart", handleTouchStart);
container.removeEventListener("touchmove", handleTouchMove);
container.removeEventListener("touchend", handleTouchEnd);
}
if (touchScrollRaf) cancelAnimationFrame(touchScrollRaf);
if (ws) {
ws.close(1000, "Component unmounting");
}