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;
|
||||
|
||||
@@ -3713,16 +3713,6 @@ a.nav-item,
|
||||
|
||||
/* xterm.js manages its own scrolling and viewport dimensions */
|
||||
|
||||
/* Mobile: allow native vertical touch panning on xterm viewport */
|
||||
.terminal-wrapper.mobile .xterm .xterm-viewport {
|
||||
touch-action: pan-y;
|
||||
overflow-y: scroll !important;
|
||||
-webkit-overflow-scrolling: touch !important;
|
||||
overscroll-behavior-y: contain;
|
||||
transform: translate3d(0, 0, 0);
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* Special Keys Strip */
|
||||
.special-keys-strip {
|
||||
flex-shrink: 0;
|
||||
|
||||
Reference in New Issue
Block a user