fix: enable native touch scrolling on xterm.js viewport for mobile
- Remove all custom touch event interception code from terminal.tsx - After term.open(), find the internal .xterm-viewport element and set touchAction=pan-y and overscrollBehavior=contain via inline styles - Add CSS targeting .xterm-viewport on mobile with touch-action: pan-y, -webkit-overflow-scrolling: touch, and overflow-y: auto - Let the browser handle vertical touch panning natively instead of trying to intercept and manually forward events
This commit is contained in:
@@ -429,84 +429,18 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
|||||||
};
|
};
|
||||||
document.addEventListener("visibilitychange", handleVisibilityChange);
|
document.addEventListener("visibilitychange", handleVisibilityChange);
|
||||||
|
|
||||||
// Mobile touch scrolling — translate vertical swipe to terminal scroll.
|
// Enable native touch scrolling on xterm.js viewport for mobile.
|
||||||
// We attach to the container (wrapper) in CAPTURE phase so we run before
|
// xterm.js creates an internal .xterm-viewport div that has overflow-y
|
||||||
// xterm.js internals stop propagation. We also listen for wheel events
|
// scroll but may disable touch-action. We override it so the browser
|
||||||
// so that mobile browsers that translate touch-pan into synthetic wheel
|
// handles vertical touch panning natively.
|
||||||
// events will still scroll the buffer.
|
|
||||||
let touchStartY = 0;
|
|
||||||
let touchStartX = 0;
|
|
||||||
let isTouchScrolling = false;
|
|
||||||
let accumulatedDeltaY = 0;
|
|
||||||
|
|
||||||
const handleTouchStart = (e: TouchEvent) => {
|
|
||||||
if (e.touches.length === 1) {
|
|
||||||
touchStartY = e.touches[0].clientY;
|
|
||||||
touchStartX = e.touches[0].clientX;
|
|
||||||
isTouchScrolling = false;
|
|
||||||
accumulatedDeltaY = 0;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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) > 6) {
|
|
||||||
if (!isTouchScrolling) isTouchScrolling = true;
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
accumulatedDeltaY += deltaY;
|
|
||||||
touchStartY = touch.clientY;
|
|
||||||
const pxPerLine = 16;
|
|
||||||
const lines = Math.round(accumulatedDeltaY / pxPerLine);
|
|
||||||
if (lines !== 0) {
|
|
||||||
// Negative scrollLines = scroll up (show older buffer content)
|
|
||||||
termRef.current.scrollLines(-lines);
|
|
||||||
accumulatedDeltaY = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTouchEnd = () => {
|
|
||||||
isTouchScrolling = false;
|
|
||||||
accumulatedDeltaY = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleWheel = (e: WheelEvent) => {
|
|
||||||
if (!termRef.current) return;
|
|
||||||
// On mobile some browsers translate vertical pan into wheel events.
|
|
||||||
// xterm.js already handles wheel natively on desktop, but on mobile
|
|
||||||
// the synthetic wheel may not reach xterm because of our layout.
|
|
||||||
// We manually forward vertical wheel deltas to scrollLines.
|
|
||||||
if (Math.abs(e.deltaY) > Math.abs(e.deltaX)) {
|
|
||||||
e.preventDefault();
|
|
||||||
const lines = Math.round(e.deltaY / 16);
|
|
||||||
if (lines !== 0) {
|
|
||||||
termRef.current.scrollLines(lines);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
// Capture phase runs before xterm.js handlers on the child elements
|
const viewport = container.querySelector(
|
||||||
container.addEventListener("touchstart", handleTouchStart, {
|
".xterm-viewport",
|
||||||
passive: true,
|
) as HTMLElement | null;
|
||||||
capture: true,
|
if (viewport) {
|
||||||
});
|
viewport.style.touchAction = "pan-y";
|
||||||
container.addEventListener("touchmove", handleTouchMove, {
|
viewport.style.overscrollBehavior = "contain";
|
||||||
passive: false,
|
}
|
||||||
capture: true,
|
|
||||||
});
|
|
||||||
container.addEventListener("touchend", handleTouchEnd, {
|
|
||||||
capture: true,
|
|
||||||
});
|
|
||||||
container.addEventListener("wheel", handleWheel, {
|
|
||||||
passive: false,
|
|
||||||
capture: true,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -520,20 +454,6 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
|||||||
"visibilitychange",
|
"visibilitychange",
|
||||||
handleVisibilityChange,
|
handleVisibilityChange,
|
||||||
);
|
);
|
||||||
if (isMobile) {
|
|
||||||
container.removeEventListener("touchstart", handleTouchStart, {
|
|
||||||
capture: true,
|
|
||||||
});
|
|
||||||
container.removeEventListener("touchmove", handleTouchMove, {
|
|
||||||
capture: true,
|
|
||||||
});
|
|
||||||
container.removeEventListener("touchend", handleTouchEnd, {
|
|
||||||
capture: true,
|
|
||||||
});
|
|
||||||
container.removeEventListener("wheel", handleWheel, {
|
|
||||||
capture: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (ws) {
|
if (ws) {
|
||||||
ws.close(1000, "Component unmounting");
|
ws.close(1000, "Component unmounting");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3709,6 +3709,14 @@ a.nav-item,
|
|||||||
|
|
||||||
/* xterm.js manages its own scrolling and viewport dimensions */
|
/* xterm.js manages its own scrolling and viewport dimensions */
|
||||||
|
|
||||||
|
/* Mobile: enable native touch scrolling on xterm viewport */
|
||||||
|
.terminal-wrapper.mobile .xterm .xterm-viewport {
|
||||||
|
touch-action: pan-y !important;
|
||||||
|
-webkit-overflow-scrolling: touch !important;
|
||||||
|
overflow-y: auto !important;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* Special Keys Strip */
|
/* Special Keys Strip */
|
||||||
.special-keys-strip {
|
.special-keys-strip {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user