fix: xterm.js mobile touch scrolling via viewport CSS and stopPropagation
- Add full mobile viewport CSS: overflow-y scroll, -webkit-overflow-scrolling touch, overscroll-behavior-y contain, translate3d hardware accel, scroll-behavior smooth, touch-action pan-y - After term.open(), find .xterm-viewport and add passive touch listeners that call stopPropagation() (not preventDefault) — this lets the browser handle native touch scrolling while preventing xterm.js internal handlers from interfering - Based on xterm.js known issue #5489 and SCROLLING_FIX.md approach
This commit is contained in:
@@ -311,6 +311,37 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
|||||||
// Open xterm first (must happen before fit)
|
// Open xterm first (must happen before fit)
|
||||||
term.open(container);
|
term.open(container);
|
||||||
term.focus();
|
term.focus();
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const ws = connectWebSocket();
|
const ws = connectWebSocket();
|
||||||
|
|
||||||
// Initial fit after layout settles (terminal must be opened first)
|
// Initial fit after layout settles (terminal must be opened first)
|
||||||
@@ -429,8 +460,6 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
|||||||
};
|
};
|
||||||
document.addEventListener("visibilitychange", handleVisibilityChange);
|
document.addEventListener("visibilitychange", handleVisibilityChange);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
isUnmountingRef.current = true;
|
isUnmountingRef.current = true;
|
||||||
clearTimeout(resizeTimeout);
|
clearTimeout(resizeTimeout);
|
||||||
@@ -442,6 +471,7 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
|||||||
"visibilitychange",
|
"visibilitychange",
|
||||||
handleVisibilityChange,
|
handleVisibilityChange,
|
||||||
);
|
);
|
||||||
|
if (touchCleanup) touchCleanup();
|
||||||
if (ws) {
|
if (ws) {
|
||||||
ws.close(1000, "Component unmounting");
|
ws.close(1000, "Component unmounting");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3716,6 +3716,11 @@ a.nav-item,
|
|||||||
/* Mobile: allow native vertical touch panning on xterm viewport */
|
/* Mobile: allow native vertical touch panning on xterm viewport */
|
||||||
.terminal-wrapper.mobile .xterm .xterm-viewport {
|
.terminal-wrapper.mobile .xterm .xterm-viewport {
|
||||||
touch-action: pan-y;
|
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 */
|
||||||
|
|||||||
Reference in New Issue
Block a user