fix: container-level capture touch with direct viewport.scrollTop manipulation
- Attach capture-phase touch listeners to .terminal-container (parent of xterm) - On vertical swipe: e.preventDefault() blocks page scroll, then directly adjust .xterm-viewport.scrollTop by the swipe delta - This bypasses term.scrollLines() API and directly manipulates the DOM element that xterm.js watches via its internal scroll handler - Remove all CSS touch-action overrides — container handles it in JS
This commit is contained in:
@@ -311,38 +311,75 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
||||
// Open xterm first (must happen before fit)
|
||||
term.open(container);
|
||||
term.focus();
|
||||
const ws = connectWebSocket();
|
||||
|
||||
// Mobile: allow native touch scroll on xterm viewport.
|
||||
// xterm.js 5.x+ uses SmoothScrollableElement which can block native
|
||||
// touch scrolling. We add passive touch listeners that call
|
||||
// stopPropagation (not preventDefault) so the browser handles the
|
||||
// gesture natively while xterm.js internal handlers don't fire.
|
||||
// Mobile: xterm.js 5.x+ viewport is behind the canvas in z-order, so
|
||||
// native browser touch scrolling never reaches it. We attach capture-
|
||||
// phase listeners to the wrapper container (parent of xterm) so we
|
||||
// intercept touches before they bubble up to the page. Vertical swipes
|
||||
// adjust .xterm-viewport.scrollTop directly, which fires scroll events
|
||||
// that xterm.js handles internally.
|
||||
let touchCleanup: (() => void) | undefined;
|
||||
if (isMobile) {
|
||||
const viewport = container.querySelector(
|
||||
".xterm-viewport",
|
||||
) as HTMLElement | null;
|
||||
if (viewport) {
|
||||
const onTouchStart = (e: TouchEvent) => {
|
||||
e.stopPropagation();
|
||||
};
|
||||
const onTouchMove = (e: TouchEvent) => {
|
||||
e.stopPropagation();
|
||||
};
|
||||
viewport.addEventListener("touchstart", onTouchStart, {
|
||||
passive: true,
|
||||
});
|
||||
viewport.addEventListener("touchmove", onTouchMove, {
|
||||
passive: true,
|
||||
});
|
||||
touchCleanup = () => {
|
||||
viewport.removeEventListener("touchstart", onTouchStart);
|
||||
viewport.removeEventListener("touchmove", onTouchMove);
|
||||
};
|
||||
}
|
||||
}
|
||||
let startY = 0;
|
||||
let startX = 0;
|
||||
let isScrolling = false;
|
||||
|
||||
const ws = connectWebSocket();
|
||||
const onTouchStart = (e: TouchEvent) => {
|
||||
if (e.touches.length === 1) {
|
||||
startY = e.touches[0].clientY;
|
||||
startX = e.touches[0].clientX;
|
||||
isScrolling = false;
|
||||
}
|
||||
};
|
||||
const onTouchMove = (e: TouchEvent) => {
|
||||
if (e.touches.length !== 1) return;
|
||||
const touch = e.touches[0];
|
||||
const deltaY = startY - touch.clientY;
|
||||
const deltaX = Math.abs(startX - touch.clientX);
|
||||
if (!isScrolling) {
|
||||
if (Math.abs(deltaY) > deltaX && Math.abs(deltaY) > 4) {
|
||||
isScrolling = true;
|
||||
}
|
||||
}
|
||||
if (isScrolling) {
|
||||
e.preventDefault();
|
||||
const viewport = container.querySelector(
|
||||
".xterm-viewport",
|
||||
) as HTMLElement | null;
|
||||
if (viewport) {
|
||||
viewport.scrollTop += deltaY;
|
||||
}
|
||||
startY = touch.clientY;
|
||||
}
|
||||
};
|
||||
const onTouchEnd = () => {
|
||||
isScrolling = false;
|
||||
};
|
||||
|
||||
container.addEventListener("touchstart", onTouchStart, {
|
||||
passive: true,
|
||||
capture: true,
|
||||
});
|
||||
container.addEventListener("touchmove", onTouchMove, {
|
||||
passive: false,
|
||||
capture: true,
|
||||
});
|
||||
container.addEventListener("touchend", onTouchEnd, {
|
||||
capture: true,
|
||||
});
|
||||
touchCleanup = () => {
|
||||
container.removeEventListener("touchstart", onTouchStart, {
|
||||
capture: true,
|
||||
});
|
||||
container.removeEventListener("touchmove", onTouchMove, {
|
||||
capture: true,
|
||||
});
|
||||
container.removeEventListener("touchend", onTouchEnd, {
|
||||
capture: true,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// Initial fit after layout settles (terminal must be opened first)
|
||||
let fitAttempts = 0;
|
||||
|
||||
Reference in New Issue
Block a user