fix: mobile terminal touch scrolling direction and target
- Attach touch listeners to term.element (xterm root) instead of wrapper - Fix scroll direction: swipe up now scrolls up (shows older buffer) - Remove RAF indirection; scroll applied synchronously in touchmove - Accumulate delta between events for smoother scrolling - Lower threshold to 6px and px-per-line to 16 for better responsiveness - Add touch-action: none to terminal container on mobile
This commit is contained in:
@@ -430,16 +430,20 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
|||||||
document.addEventListener("visibilitychange", handleVisibilityChange);
|
document.addEventListener("visibilitychange", handleVisibilityChange);
|
||||||
|
|
||||||
// Mobile touch scrolling — translate vertical swipe to terminal scroll
|
// Mobile touch scrolling — translate vertical swipe to terminal scroll
|
||||||
|
// Attach to term.element (xterm root) rather than container wrapper,
|
||||||
|
// since xterm internals may intercept events before they bubble up.
|
||||||
|
const touchTarget = term.element || container;
|
||||||
let touchStartY = 0;
|
let touchStartY = 0;
|
||||||
let touchStartX = 0;
|
let touchStartX = 0;
|
||||||
let isTouchScrolling = false;
|
let isTouchScrolling = false;
|
||||||
let touchScrollRaf: number | null = null;
|
let accumulatedDeltaY = 0;
|
||||||
|
|
||||||
const handleTouchStart = (e: TouchEvent) => {
|
const handleTouchStart = (e: TouchEvent) => {
|
||||||
if (e.touches.length === 1) {
|
if (e.touches.length === 1) {
|
||||||
touchStartY = e.touches[0].clientY;
|
touchStartY = e.touches[0].clientY;
|
||||||
touchStartX = e.touches[0].clientX;
|
touchStartX = e.touches[0].clientX;
|
||||||
isTouchScrolling = false;
|
isTouchScrolling = false;
|
||||||
|
accumulatedDeltaY = 0;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -449,34 +453,34 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
|||||||
const deltaY = touchStartY - touch.clientY;
|
const deltaY = touchStartY - touch.clientY;
|
||||||
const deltaX = Math.abs(touchStartX - touch.clientX);
|
const deltaX = Math.abs(touchStartX - touch.clientX);
|
||||||
// If vertical movement dominates and exceeds threshold, scroll terminal buffer
|
// If vertical movement dominates and exceeds threshold, scroll terminal buffer
|
||||||
if (Math.abs(deltaY) > deltaX && Math.abs(deltaY) > 10) {
|
if (Math.abs(deltaY) > deltaX && Math.abs(deltaY) > 6) {
|
||||||
if (!isTouchScrolling) isTouchScrolling = true;
|
if (!isTouchScrolling) isTouchScrolling = true;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (touchScrollRaf) cancelAnimationFrame(touchScrollRaf);
|
accumulatedDeltaY += deltaY;
|
||||||
touchScrollRaf = requestAnimationFrame(() => {
|
touchStartY = touch.clientY;
|
||||||
if (!termRef.current) return;
|
const pxPerLine = 16;
|
||||||
const lines = Math.round(deltaY / 30);
|
const lines = Math.round(accumulatedDeltaY / pxPerLine);
|
||||||
if (lines !== 0) {
|
if (lines !== 0) {
|
||||||
termRef.current.scrollLines(lines);
|
// Negative scrollLines = scroll up (show older buffer content)
|
||||||
touchStartY = touch.clientY;
|
termRef.current.scrollLines(-lines);
|
||||||
}
|
accumulatedDeltaY = 0;
|
||||||
touchScrollRaf = null;
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTouchEnd = () => {
|
const handleTouchEnd = () => {
|
||||||
isTouchScrolling = false;
|
isTouchScrolling = false;
|
||||||
if (touchScrollRaf) {
|
accumulatedDeltaY = 0;
|
||||||
cancelAnimationFrame(touchScrollRaf);
|
|
||||||
touchScrollRaf = null;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
container.addEventListener("touchstart", handleTouchStart, { passive: true });
|
touchTarget.addEventListener("touchstart", handleTouchStart, {
|
||||||
container.addEventListener("touchmove", handleTouchMove, { passive: false });
|
passive: true,
|
||||||
container.addEventListener("touchend", handleTouchEnd);
|
});
|
||||||
|
touchTarget.addEventListener("touchmove", handleTouchMove, {
|
||||||
|
passive: false,
|
||||||
|
});
|
||||||
|
touchTarget.addEventListener("touchend", handleTouchEnd);
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -491,11 +495,10 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
|||||||
handleVisibilityChange,
|
handleVisibilityChange,
|
||||||
);
|
);
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
container.removeEventListener("touchstart", handleTouchStart);
|
touchTarget.removeEventListener("touchstart", handleTouchStart);
|
||||||
container.removeEventListener("touchmove", handleTouchMove);
|
touchTarget.removeEventListener("touchmove", handleTouchMove);
|
||||||
container.removeEventListener("touchend", handleTouchEnd);
|
touchTarget.removeEventListener("touchend", handleTouchEnd);
|
||||||
}
|
}
|
||||||
if (touchScrollRaf) cancelAnimationFrame(touchScrollRaf);
|
|
||||||
if (ws) {
|
if (ws) {
|
||||||
ws.close(1000, "Component unmounting");
|
ws.close(1000, "Component unmounting");
|
||||||
}
|
}
|
||||||
@@ -624,7 +627,9 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`terminal-wrapper ${isMobile ? "mobile" : ""} ${!showControls ? "no-controls" : ""}`}>
|
<div
|
||||||
|
className={`terminal-wrapper ${isMobile ? "mobile" : ""} ${!showControls ? "no-controls" : ""}`}
|
||||||
|
>
|
||||||
{showControls && (
|
{showControls && (
|
||||||
<div className="terminal-header">
|
<div className="terminal-header">
|
||||||
<div className="terminal-header-left">
|
<div className="terminal-header-left">
|
||||||
|
|||||||
@@ -3703,6 +3703,7 @@ a.nav-item,
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
touch-action: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* xterm.js manages its own sizing */
|
/* xterm.js manages its own sizing */
|
||||||
|
|||||||
Reference in New Issue
Block a user