fix: mobile terminal scroll via capture-phase touch listeners
- Attach touch listeners to container wrapper in CAPTURE phase so they run before xterm.js internals stop propagation - Add e.stopPropagation() in touchmove after handling scroll to prevent xterm.js from conflicting with our scroll - Add wheel event fallback for mobile browsers that synthesize wheel from touch - Remove touch-action: none CSS which was blocking native xterm viewport scroll
This commit is contained in:
@@ -429,10 +429,11 @@ 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,
|
// We attach to the container (wrapper) in CAPTURE phase so we run before
|
||||||
// since xterm internals may intercept events before they bubble up.
|
// xterm.js internals stop propagation. We also listen for wheel events
|
||||||
const touchTarget = term.element || container;
|
// so that mobile browsers that translate touch-pan into synthetic wheel
|
||||||
|
// events will still scroll the buffer.
|
||||||
let touchStartY = 0;
|
let touchStartY = 0;
|
||||||
let touchStartX = 0;
|
let touchStartX = 0;
|
||||||
let isTouchScrolling = false;
|
let isTouchScrolling = false;
|
||||||
@@ -456,6 +457,7 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
|||||||
if (Math.abs(deltaY) > deltaX && Math.abs(deltaY) > 6) {
|
if (Math.abs(deltaY) > deltaX && Math.abs(deltaY) > 6) {
|
||||||
if (!isTouchScrolling) isTouchScrolling = true;
|
if (!isTouchScrolling) isTouchScrolling = true;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
accumulatedDeltaY += deltaY;
|
accumulatedDeltaY += deltaY;
|
||||||
touchStartY = touch.clientY;
|
touchStartY = touch.clientY;
|
||||||
const pxPerLine = 16;
|
const pxPerLine = 16;
|
||||||
@@ -473,14 +475,38 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
|||||||
accumulatedDeltaY = 0;
|
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) {
|
||||||
touchTarget.addEventListener("touchstart", handleTouchStart, {
|
// Capture phase runs before xterm.js handlers on the child elements
|
||||||
|
container.addEventListener("touchstart", handleTouchStart, {
|
||||||
passive: true,
|
passive: true,
|
||||||
|
capture: true,
|
||||||
});
|
});
|
||||||
touchTarget.addEventListener("touchmove", handleTouchMove, {
|
container.addEventListener("touchmove", handleTouchMove, {
|
||||||
passive: false,
|
passive: false,
|
||||||
|
capture: true,
|
||||||
|
});
|
||||||
|
container.addEventListener("touchend", handleTouchEnd, {
|
||||||
|
capture: true,
|
||||||
|
});
|
||||||
|
container.addEventListener("wheel", handleWheel, {
|
||||||
|
passive: false,
|
||||||
|
capture: true,
|
||||||
});
|
});
|
||||||
touchTarget.addEventListener("touchend", handleTouchEnd);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -495,9 +521,18 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
|||||||
handleVisibilityChange,
|
handleVisibilityChange,
|
||||||
);
|
);
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
touchTarget.removeEventListener("touchstart", handleTouchStart);
|
container.removeEventListener("touchstart", handleTouchStart, {
|
||||||
touchTarget.removeEventListener("touchmove", handleTouchMove);
|
capture: true,
|
||||||
touchTarget.removeEventListener("touchend", handleTouchEnd);
|
});
|
||||||
|
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");
|
||||||
|
|||||||
@@ -3703,7 +3703,6 @@ 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