From 383b28c85e5249a5aba14d9ee3bcd07029a9d7b2 Mon Sep 17 00:00:00 2001 From: Brian Krabach Date: Sat, 28 Mar 2026 15:52:37 -0700 Subject: [PATCH] =?UTF-8?q?fix:=20sidebar=20touch=20scroll=20via=20scrollT?= =?UTF-8?q?op=20=E2=80=94=20bypasses=20broken=20CSS=20path?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit position:fixed + overflow:hidden on .session-sidebar in mobile overlay mode prevents Android Chrome from routing native touch-scroll to the child .sidebar-list (overflow-y:auto). Direct scrollTop manipulation always works. Mirrors the terminal WheelEvent approach: take JS control rather than relying on native CSS touch-scroll routing. --- muxplex/frontend/app.js | 28 ++++++++++++++++++++++++++++ muxplex/frontend/tests/test_app.mjs | 12 ++++++++++++ 2 files changed, 40 insertions(+) diff --git a/muxplex/frontend/app.js b/muxplex/frontend/app.js index ef7d752..c36b116 100644 --- a/muxplex/frontend/app.js +++ b/muxplex/frontend/app.js @@ -1050,6 +1050,34 @@ document.addEventListener('DOMContentLoaded', () => { document.addEventListener('keydown', trackInteraction); document.addEventListener('click', trackInteraction); document.addEventListener('touchstart', trackInteraction); + + // --------------------------------------------------------------------------- + // Sidebar touch scroll — manual scrollTop manipulation bypasses the CSS + // touch-scroll path that fails when position:fixed + overflow:hidden is on + // the parent (.session-sidebar) in mobile overlay mode. + // --------------------------------------------------------------------------- + ;(function initSidebarTouchScroll() { + var list = document.querySelector('.sidebar-list'); + if (!list || typeof list.addEventListener !== 'function') return; + + var _lastY = 0; + + list.addEventListener('touchstart', function (e) { + _lastY = e.touches[0].clientY; + }, { passive: true }); + + list.addEventListener('touchmove', function (e) { + e.preventDefault(); // block page scroll + var y = e.touches[0].clientY; + list.scrollTop += _lastY - y; // direct scrollTop — always works + _lastY = y; + }, { passive: false }); // passive:false required for preventDefault + + list.addEventListener('touchend', function () { + _lastY = 0; + }, { passive: true }); + })(); + restoreState() .then(() => { startPolling(); diff --git a/muxplex/frontend/tests/test_app.mjs b/muxplex/frontend/tests/test_app.mjs index 0933b98..b34e203 100644 --- a/muxplex/frontend/tests/test_app.mjs +++ b/muxplex/frontend/tests/test_app.mjs @@ -40,6 +40,7 @@ Object.defineProperty(globalThis, 'navigator', { configurable: true, }); +import fs from 'node:fs'; import { createRequire } from 'node:module'; import { test } from 'node:test'; import assert from 'node:assert/strict'; @@ -2024,3 +2025,14 @@ test('bindSidebarClickAway registers click listener on terminal-container', () = assert.strictEqual(addEventListenerCalledWith, 'click', 'bindSidebarClickAway should register a click listener on terminal-container'); globalThis.document.getElementById = origGetById; }); + +// --- Sidebar touch scroll --- + +test('app.js has sidebar touch scroll handler that uses scrollTop', () => { + const source = fs.readFileSync( + new URL('../app.js', import.meta.url), 'utf8' + ); + assert.ok(source.includes('sidebar-list'), 'must target .sidebar-list'); + assert.ok(source.includes('scrollTop'), 'must use scrollTop (not scrollLines or WheelEvent)'); + assert.ok(source.includes('passive: false'), 'touchmove must be non-passive to allow preventDefault'); +});