fix: document-level capture touch listeners for mobile terminal scroll
- Attach touch listeners to document with capture:true instead of container - Check if touch target is inside terminal container before handling - This runs before xterm.js internal handlers, giving us full control - Add touch-action: none to terminal container to prevent browser gestures - Lower threshold to 3px, 20px per line for responsive scrolling
This commit is contained in:
@@ -430,32 +430,36 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
|||||||
document.addEventListener("visibilitychange", handleVisibilityChange);
|
document.addEventListener("visibilitychange", handleVisibilityChange);
|
||||||
|
|
||||||
// Mobile touch scroll forwarding.
|
// Mobile touch scroll forwarding.
|
||||||
// xterm.js intercepts touch events for selection, which blocks the
|
// xterm.js intercepts touch events for selection, blocking native scroll.
|
||||||
// browser from scrolling the viewport. We listen on the container in
|
// We attach to *document* in capture phase so we run before xterm.js
|
||||||
// capture phase, decide early if the gesture is vertical, and then
|
// even on child elements. We only act when the touch target is inside
|
||||||
// preventDefault + stopPropagation so xterm.js never sees the event.
|
// this terminal container.
|
||||||
// The scroll is forwarded to term.scrollLines().
|
|
||||||
let touchStartY = 0;
|
let touchStartY = 0;
|
||||||
let touchStartX = 0;
|
let touchStartX = 0;
|
||||||
let isVerticalScroll = false;
|
let isVerticalScroll = false;
|
||||||
let accumulatedDeltaY = 0;
|
let accumulatedDeltaY = 0;
|
||||||
|
|
||||||
|
const isInThisTerminal = (target: EventTarget | null): boolean => {
|
||||||
|
if (!(target instanceof Node)) return false;
|
||||||
|
return container.contains(target);
|
||||||
|
};
|
||||||
|
|
||||||
const handleTouchStart = (e: TouchEvent) => {
|
const handleTouchStart = (e: TouchEvent) => {
|
||||||
if (e.touches.length === 1) {
|
if (e.touches.length !== 1) return;
|
||||||
touchStartY = e.touches[0].clientY;
|
if (!isInThisTerminal(e.target)) return;
|
||||||
touchStartX = e.touches[0].clientX;
|
touchStartY = e.touches[0].clientY;
|
||||||
isVerticalScroll = false;
|
touchStartX = e.touches[0].clientX;
|
||||||
accumulatedDeltaY = 0;
|
isVerticalScroll = false;
|
||||||
}
|
accumulatedDeltaY = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTouchMove = (e: TouchEvent) => {
|
const handleTouchMove = (e: TouchEvent) => {
|
||||||
if (e.touches.length !== 1 || !termRef.current) return;
|
if (e.touches.length !== 1 || !termRef.current) return;
|
||||||
|
if (!isInThisTerminal(e.target)) return;
|
||||||
const touch = e.touches[0];
|
const touch = e.touches[0];
|
||||||
const deltaY = touchStartY - touch.clientY;
|
const deltaY = touchStartY - touch.clientY;
|
||||||
const deltaX = Math.abs(touchStartX - touch.clientX);
|
const deltaX = Math.abs(touchStartX - touch.clientX);
|
||||||
|
|
||||||
// Decide scroll direction on the first meaningful movement (3px)
|
|
||||||
if (!isVerticalScroll) {
|
if (!isVerticalScroll) {
|
||||||
if (Math.abs(deltaY) > deltaX && Math.abs(deltaY) > 3) {
|
if (Math.abs(deltaY) > deltaX && Math.abs(deltaY) > 3) {
|
||||||
isVerticalScroll = true;
|
isVerticalScroll = true;
|
||||||
@@ -463,16 +467,12 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isVerticalScroll) {
|
if (isVerticalScroll) {
|
||||||
// preventDefault MUST be called on the first matching touchmove
|
|
||||||
// or the browser compositor will already have committed to page
|
|
||||||
// scroll before our JS runs.
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
accumulatedDeltaY += deltaY;
|
accumulatedDeltaY += deltaY;
|
||||||
touchStartY = touch.clientY;
|
touchStartY = touch.clientY;
|
||||||
const lines = Math.round(accumulatedDeltaY / 24);
|
const lines = Math.round(accumulatedDeltaY / 20);
|
||||||
if (lines !== 0) {
|
if (lines !== 0) {
|
||||||
// Negative = scroll up (show older buffer content)
|
|
||||||
termRef.current.scrollLines(-lines);
|
termRef.current.scrollLines(-lines);
|
||||||
accumulatedDeltaY = 0;
|
accumulatedDeltaY = 0;
|
||||||
}
|
}
|
||||||
@@ -485,15 +485,15 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
container.addEventListener("touchstart", handleTouchStart, {
|
document.addEventListener("touchstart", handleTouchStart, {
|
||||||
passive: true,
|
passive: true,
|
||||||
capture: true,
|
capture: true,
|
||||||
});
|
});
|
||||||
container.addEventListener("touchmove", handleTouchMove, {
|
document.addEventListener("touchmove", handleTouchMove, {
|
||||||
passive: false,
|
passive: false,
|
||||||
capture: true,
|
capture: true,
|
||||||
});
|
});
|
||||||
container.addEventListener("touchend", handleTouchEnd, {
|
document.addEventListener("touchend", handleTouchEnd, {
|
||||||
capture: true,
|
capture: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -510,13 +510,13 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
|||||||
handleVisibilityChange,
|
handleVisibilityChange,
|
||||||
);
|
);
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
container.removeEventListener("touchstart", handleTouchStart, {
|
document.removeEventListener("touchstart", handleTouchStart, {
|
||||||
capture: true,
|
capture: true,
|
||||||
});
|
});
|
||||||
container.removeEventListener("touchmove", handleTouchMove, {
|
document.removeEventListener("touchmove", handleTouchMove, {
|
||||||
capture: true,
|
capture: true,
|
||||||
});
|
});
|
||||||
container.removeEventListener("touchend", handleTouchEnd, {
|
document.removeEventListener("touchend", handleTouchEnd, {
|
||||||
capture: true,
|
capture: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3707,6 +3707,7 @@ 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