fix: lock body scroll and re-add programmatic terminal touch scroll
- Add body.terminal-page-open { overflow: hidden } to prevent page scroll
- TerminalPage adds/removes 'terminal-page-open' class on body when mounted
- Re-add capture-phase touch listeners in terminal.tsx with low 3px threshold
- Call e.preventDefault() immediately when vertical gesture is detected,
before browser compositor commits to page scroll
- Remove CSS touch-action overrides on xterm viewport (now handled in JS)
- Scroll forwarded via term.scrollLines() with 24px per line sensitivity
This commit is contained in:
@@ -429,18 +429,73 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
|||||||
};
|
};
|
||||||
document.addEventListener("visibilitychange", handleVisibilityChange);
|
document.addEventListener("visibilitychange", handleVisibilityChange);
|
||||||
|
|
||||||
// Enable native touch scrolling on xterm.js viewport for mobile.
|
// Mobile touch scroll forwarding.
|
||||||
// xterm.js creates an internal .xterm-viewport div that has overflow-y
|
// xterm.js intercepts touch events for selection, which blocks the
|
||||||
// scroll but may disable touch-action. We override it so the browser
|
// browser from scrolling the viewport. We listen on the container in
|
||||||
// handles vertical touch panning natively.
|
// capture phase, decide early if the gesture is vertical, and then
|
||||||
if (isMobile) {
|
// preventDefault + stopPropagation so xterm.js never sees the event.
|
||||||
const viewport = container.querySelector(
|
// The scroll is forwarded to term.scrollLines().
|
||||||
".xterm-viewport",
|
let touchStartY = 0;
|
||||||
) as HTMLElement | null;
|
let touchStartX = 0;
|
||||||
if (viewport) {
|
let isVerticalScroll = false;
|
||||||
viewport.style.touchAction = "pan-y";
|
let accumulatedDeltaY = 0;
|
||||||
viewport.style.overscrollBehavior = "contain";
|
|
||||||
|
const handleTouchStart = (e: TouchEvent) => {
|
||||||
|
if (e.touches.length === 1) {
|
||||||
|
touchStartY = e.touches[0].clientY;
|
||||||
|
touchStartX = e.touches[0].clientX;
|
||||||
|
isVerticalScroll = false;
|
||||||
|
accumulatedDeltaY = 0;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTouchMove = (e: TouchEvent) => {
|
||||||
|
if (e.touches.length !== 1 || !termRef.current) return;
|
||||||
|
const touch = e.touches[0];
|
||||||
|
const deltaY = touchStartY - touch.clientY;
|
||||||
|
const deltaX = Math.abs(touchStartX - touch.clientX);
|
||||||
|
|
||||||
|
// Decide scroll direction on the first meaningful movement (3px)
|
||||||
|
if (!isVerticalScroll) {
|
||||||
|
if (Math.abs(deltaY) > deltaX && Math.abs(deltaY) > 3) {
|
||||||
|
isVerticalScroll = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.stopPropagation();
|
||||||
|
accumulatedDeltaY += deltaY;
|
||||||
|
touchStartY = touch.clientY;
|
||||||
|
const lines = Math.round(accumulatedDeltaY / 24);
|
||||||
|
if (lines !== 0) {
|
||||||
|
// Negative = scroll up (show older buffer content)
|
||||||
|
termRef.current.scrollLines(-lines);
|
||||||
|
accumulatedDeltaY = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTouchEnd = () => {
|
||||||
|
isVerticalScroll = false;
|
||||||
|
accumulatedDeltaY = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isMobile) {
|
||||||
|
container.addEventListener("touchstart", handleTouchStart, {
|
||||||
|
passive: true,
|
||||||
|
capture: true,
|
||||||
|
});
|
||||||
|
container.addEventListener("touchmove", handleTouchMove, {
|
||||||
|
passive: false,
|
||||||
|
capture: true,
|
||||||
|
});
|
||||||
|
container.addEventListener("touchend", handleTouchEnd, {
|
||||||
|
capture: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -454,6 +509,17 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
|||||||
"visibilitychange",
|
"visibilitychange",
|
||||||
handleVisibilityChange,
|
handleVisibilityChange,
|
||||||
);
|
);
|
||||||
|
if (isMobile) {
|
||||||
|
container.removeEventListener("touchstart", handleTouchStart, {
|
||||||
|
capture: true,
|
||||||
|
});
|
||||||
|
container.removeEventListener("touchmove", handleTouchMove, {
|
||||||
|
capture: true,
|
||||||
|
});
|
||||||
|
container.removeEventListener("touchend", handleTouchEnd, {
|
||||||
|
capture: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
if (ws) {
|
if (ws) {
|
||||||
ws.close(1000, "Component unmounting");
|
ws.close(1000, "Component unmounting");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -204,6 +204,16 @@ export const TerminalPage: React.FC = () => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Lock body scroll on mobile terminal so swipes scroll the terminal buffer,
|
||||||
|
// not the page.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isMobile) return;
|
||||||
|
document.body.classList.add("terminal-page-open");
|
||||||
|
return () => {
|
||||||
|
document.body.classList.remove("terminal-page-open");
|
||||||
|
};
|
||||||
|
}, [isMobile]);
|
||||||
|
|
||||||
// Click outside terminal content/header to exit fullscreen
|
// Click outside terminal content/header to exit fullscreen
|
||||||
const handleFullscreenClick = useCallback(
|
const handleFullscreenClick = useCallback(
|
||||||
(e: React.MouseEvent<HTMLElement>) => {
|
(e: React.MouseEvent<HTMLElement>) => {
|
||||||
|
|||||||
@@ -77,6 +77,10 @@ body {
|
|||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.terminal-page-open {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
[data-theme="dark"] body {
|
[data-theme="dark"] body {
|
||||||
background: radial-gradient(circle at top right, #2a2520, var(--bg));
|
background: radial-gradient(circle at top right, #2a2520, var(--bg));
|
||||||
}
|
}
|
||||||
@@ -3709,14 +3713,6 @@ a.nav-item,
|
|||||||
|
|
||||||
/* xterm.js manages its own scrolling and viewport dimensions */
|
/* xterm.js manages its own scrolling and viewport dimensions */
|
||||||
|
|
||||||
/* Mobile: enable native touch scrolling on xterm viewport */
|
|
||||||
.terminal-wrapper.mobile .xterm .xterm-viewport {
|
|
||||||
touch-action: pan-y !important;
|
|
||||||
-webkit-overflow-scrolling: touch !important;
|
|
||||||
overflow-y: auto !important;
|
|
||||||
scrollbar-width: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Special Keys Strip */
|
/* Special Keys Strip */
|
||||||
.special-keys-strip {
|
.special-keys-strip {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user