feat: touch scroll for terminal and sidebar on mobile
Terminal: add touchstart/touchmove/touchend IIFE on #terminal-container. Vertical swipe delta translated to _term.scrollLines(n). touchmove is non-passive (required for e.preventDefault to block page scroll while swiping inside terminal). mouse wheel scroll unaffected (separate wheel event path in xterm.js). Sidebar: add touch-action:pan-y + overscroll-behavior:contain to .sidebar-list. Tells browser explicitly that vertical panning is allowed and prevents scroll chaining when reaching list ends.
This commit is contained in:
@@ -427,6 +427,8 @@ body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
touch-action: pan-y; /* tell browser vertical panning is allowed on mobile */
|
||||
overscroll-behavior: contain; /* prevent scroll chaining to non-scrollable parent */
|
||||
}
|
||||
|
||||
.sidebar-collapse-btn {
|
||||
|
||||
@@ -246,3 +246,42 @@ function closeTerminal() {
|
||||
// ─── Expose to app.js ─────────────────────────────────────────────────────────
|
||||
window._openTerminal = openTerminal;
|
||||
window._closeTerminal = closeTerminal;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Touch scroll — translates vertical swipe into xterm.js scrollback
|
||||
// ---------------------------------------------------------------------------
|
||||
// Runs once after DOM is ready. Mouse wheel scroll is unaffected (separate
|
||||
// `wheel` event path handled natively by xterm.js).
|
||||
;(function initTerminalTouchScroll() {
|
||||
var container = document.getElementById('terminal-container');
|
||||
if (!container || typeof container.addEventListener !== 'function') return;
|
||||
|
||||
var _touchLastY = 0;
|
||||
|
||||
container.addEventListener('touchstart', function (e) {
|
||||
_touchLastY = e.touches[0].clientY;
|
||||
}, { passive: true });
|
||||
|
||||
container.addEventListener('touchmove', function (e) {
|
||||
if (!_term) return;
|
||||
e.preventDefault(); // prevent page scroll while swiping inside terminal
|
||||
|
||||
var y = e.touches[0].clientY;
|
||||
var deltaY = _touchLastY - y; // positive = swipe up = scroll toward newer content
|
||||
_touchLastY = y;
|
||||
|
||||
// Convert pixels to lines. xterm.js fontSize is set per device (12 mobile, 14 desktop).
|
||||
// lineHeight ratio is 1.2 — gives ~14-17px per line.
|
||||
var pxPerLine = (_term.options.fontSize || 14) * 1.2;
|
||||
var lines = deltaY / pxPerLine;
|
||||
|
||||
// Only fire if movement is meaningful (avoids micro-jitter)
|
||||
if (Math.abs(lines) >= 0.3) {
|
||||
_term.scrollLines(Math.round(lines));
|
||||
}
|
||||
}, { passive: false }); // passive: false required to allow e.preventDefault()
|
||||
|
||||
container.addEventListener('touchend', function () {
|
||||
_touchLastY = 0;
|
||||
}, { passive: true });
|
||||
})();
|
||||
|
||||
@@ -5,6 +5,7 @@ import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join } from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
@@ -717,3 +718,31 @@ test('terminal is auto-focused when WebSocket opens', () => {
|
||||
assert.strictEqual(t.focusCallCount, 1,
|
||||
'_term.focus() should be called exactly once when the WebSocket open event fires');
|
||||
});
|
||||
|
||||
// --- Touch scroll source-inspection tests ---
|
||||
|
||||
const terminalSrc = fs.readFileSync(
|
||||
new URL('../terminal.js', import.meta.url), 'utf8'
|
||||
);
|
||||
|
||||
test('terminal.js registers touchmove handler on terminal-container', () => {
|
||||
// Source inspection: verify the touch scroll IIFE is present and uses
|
||||
// e.preventDefault() (passive: false is required for prevent default)
|
||||
assert.ok(terminalSrc.includes('touchmove'),
|
||||
'touchmove listener must be present');
|
||||
assert.ok(terminalSrc.includes('e.preventDefault'),
|
||||
'must call preventDefault to prevent page scroll while swiping inside terminal');
|
||||
assert.ok(terminalSrc.includes('scrollLines'),
|
||||
'must call _term.scrollLines to scroll terminal');
|
||||
assert.ok(terminalSrc.includes('passive: false'),
|
||||
'touchmove must be non-passive to allow preventDefault');
|
||||
});
|
||||
|
||||
test('terminal.js touchstart and touchend are passive', () => {
|
||||
// touchstart and touchend should be passive: true for performance
|
||||
// (they don't need preventDefault)
|
||||
const touchstartPassive = terminalSrc.match(/touchstart[\s\S]*?passive:\s*(true|false)/);
|
||||
const touchendPassive = terminalSrc.match(/touchend[\s\S]*?passive:\s*(true|false)/);
|
||||
assert.ok(touchstartPassive, 'touchstart listener must declare passive');
|
||||
assert.ok(touchendPassive, 'touchend listener must declare passive');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user