fix: mobile terminal scroll in both normal mode and tmux
- Dual-mode touch scroll:
- Normal mode: scroll .xterm-viewport directly when scrollHeight > clientHeight
- Alternate screen (tmux/vim): send SGR 1006 mouse-wheel protocol data
using cursor position so tmux knows which pane to scroll
- Add touch-action: none to .terminal-container to prevent browser gestures
- Lock both html and body overflow when terminal page is open on mobile
- Remove synthetic WheelEvent approach (xterm.js SmoothScrollableElement
doesn't reliably handle synthetic events)
This commit is contained in:
@@ -313,12 +313,12 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
|||||||
term.focus();
|
term.focus();
|
||||||
const ws = connectWebSocket();
|
const ws = connectWebSocket();
|
||||||
|
|
||||||
// Mobile: xterm.js 5.x+ viewport is behind the canvas in z-order, so
|
// Mobile touch scroll.
|
||||||
// native browser touch scrolling never reaches it. We attach capture-
|
// In normal mode xterm.js has a scrollable viewport; in alternate
|
||||||
// phase listeners to the wrapper container (parent of xterm) so we
|
// screen (tmux/vim) there is no scrollback and the only way to
|
||||||
// intercept touches before they bubble up to the page. Vertical swipes
|
// scroll is to send mouse-wheel protocol sequences to the
|
||||||
// adjust .xterm-viewport.scrollTop directly, which fires scroll events
|
// application. We detect which situation we're in by checking
|
||||||
// that xterm.js handles internally.
|
// whether the viewport has scrollable height.
|
||||||
let touchCleanup: (() => void) | undefined;
|
let touchCleanup: (() => void) | undefined;
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
let startY = 0;
|
let startY = 0;
|
||||||
@@ -347,8 +347,30 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
|||||||
const viewport = container.querySelector(
|
const viewport = container.querySelector(
|
||||||
".xterm-viewport",
|
".xterm-viewport",
|
||||||
) as HTMLElement | null;
|
) as HTMLElement | null;
|
||||||
if (viewport) {
|
if (!viewport) return;
|
||||||
|
|
||||||
|
// If the viewport is scrollable, scroll it directly.
|
||||||
|
// Otherwise we are in alternate screen (tmux/vim) and must
|
||||||
|
// send SGR 1006 mouse-wheel protocol data.
|
||||||
|
const hasScrollback =
|
||||||
|
viewport.scrollHeight > viewport.clientHeight;
|
||||||
|
if (hasScrollback) {
|
||||||
viewport.scrollTop += deltaY;
|
viewport.scrollTop += deltaY;
|
||||||
|
} else {
|
||||||
|
const ws = wsRef.current;
|
||||||
|
if (
|
||||||
|
ws?.readyState === WebSocket.OPEN &&
|
||||||
|
termRef.current
|
||||||
|
) {
|
||||||
|
// Use the cursor position as the wheel location so
|
||||||
|
// tmux knows which pane to scroll.
|
||||||
|
const buf = termRef.current.buffer.active;
|
||||||
|
const col = buf.cursorX + 1;
|
||||||
|
const row = buf.cursorY + 1;
|
||||||
|
// SGR 1006: 64 = wheel-up, 65 = wheel-down
|
||||||
|
const btn = deltaY > 0 ? 64 : 65;
|
||||||
|
ws.send(`\x1b[<${btn};${col};${row}M`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
startY = touch.clientY;
|
startY = touch.clientY;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -204,12 +204,14 @@ export const TerminalPage: React.FC = () => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Lock body scroll on mobile terminal so swipes scroll the terminal buffer,
|
// Lock page scroll on mobile terminal so swipes scroll the terminal buffer,
|
||||||
// not the page.
|
// not the page.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isMobile) return;
|
if (!isMobile) return;
|
||||||
|
document.documentElement.classList.add("terminal-page-open");
|
||||||
document.body.classList.add("terminal-page-open");
|
document.body.classList.add("terminal-page-open");
|
||||||
return () => {
|
return () => {
|
||||||
|
document.documentElement.classList.remove("terminal-page-open");
|
||||||
document.body.classList.remove("terminal-page-open");
|
document.body.classList.remove("terminal-page-open");
|
||||||
};
|
};
|
||||||
}, [isMobile]);
|
}, [isMobile]);
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ body {
|
|||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html.terminal-page-open,
|
||||||
body.terminal-page-open {
|
body.terminal-page-open {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
@@ -3707,6 +3708,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