fix(mobile): hand a swipe-locked drag back to the scroll past the swipe's drift limit

A scroll whose first move past the tap slop leaned sideways (a thumb arc) locked as 'swipe' for the whole contact, and SwipeHandler drops a swipe at 50 px of vertical drift, so the gesture did nothing. Past that limit the drag is now a 'y' scroll. Also brings the mobile-handlers fileoverview to the current 500 ms / 50 px limits and notes in the Mobile Guide that a selection pauses the swipe and a wide terminal pans instead.
This commit is contained in:
JD committed 2026-10-22 13:53:48 -04:00
1 parent 63cada938d
commit e8fc08beb3
4 files changed
+30 -5

No files matched your search

+2 -1
View File
@@ -37,7 +37,8 @@ a bottom sheet rather than a dropdown. On a folding phone (iPhone Duo) dialogs s
the hinge, and opening or closing the device is treated as the device changing shape, never
as the keyboard appearing.
**Swipe left and right** on the terminal to switch sessions.
**Swipe left and right** on the terminal to switch sessions. A visible text selection pauses
the swipe, and a terminal wider than the screen pans sideways instead.
## The home screen
+1 -1
View File
@@ -9,7 +9,7 @@
* toolbar/accessory bar repositioning, terminal resize on keyboard open/close,
* and input scroll-into-view. Uses 100px threshold for iOS address bar drift.
* - SwipeHandler — Horizontal swipe detection on the terminal area for session switching.
* 80px minimum distance, 300ms maximum time, 100px max vertical drift.
* 80px minimum distance, 500ms maximum time, 50px max vertical drift.
*
* All three have init()/cleanup() lifecycle methods. They are re-initialized after SSE
* reconnect (in handleInit) to prevent stale closures.
+14 -2
View File
@@ -1200,7 +1200,8 @@ Object.assign(CodemanApp.prototype, {
// slop, so a diagonal drag cannot pan and scroll at the same time. 'swipe'
// is a sideways drag on a terminal that cannot pan: it is the session
// swipe's (SwipeHandler, mobile-handlers.js), so it neither scrolls nor
// ends as a tap on the session being left. Published per gesture as
// ends as a tap on the session being left, until it drifts past the
// swipe's vertical limit and becomes a 'y' scroll. Published per gesture as
// `this._terminalGestureAxis`, which SwipeHandler reads to stand aside
// for a scroll or a pan.
let panAxis = null;
@@ -1335,7 +1336,18 @@ Object.assign(CodemanApp.prototype, {
// fling, so a jittery tap would both position the cursor AND scroll.
if (!didScroll) return;
ev.preventDefault();
if (panAxis === 'swipe') return;
if (panAxis === 'swipe') {
// Past the swipe's own vertical drift limit the session swipe has
// given up, so the drag is a scroll after all (a thumb arc whose
// first move leaned sideways): hand it to 'y'.
const drift = typeof SwipeHandler !== 'undefined' ? SwipeHandler.maxVerticalDrift : 50;
if (Math.abs(touchY - touchStartY) <= drift) return;
panAxis = 'y';
this._terminalGestureAxis = 'y';
touchLastX = touchX;
touchLastY = touchY;
return;
}
if (panAxis === 'x') {
// Pan the container, and touch nothing the vertical path owns —
// no pixelAccum, no velocity, so touchend cannot turn a sideways
+13 -1
View File
@@ -147,6 +147,18 @@ describe('terminal touch handler hands sideways drags to the swipe', () => {
expect(TERMINAL_SOURCE).toContain("panAxis = travelX > travelY ? (canPanHorizontally() ? 'x' : 'swipe') : 'y';");
expect(TERMINAL_SOURCE).toContain('this._terminalGestureAxis = panAxis;');
// A 'swipe' is neither a scroll nor (didScroll is set) a tap.
expect(TERMINAL_SOURCE).toContain("if (panAxis === 'swipe') return;");
expect(TERMINAL_SOURCE).toContain("if (panAxis === 'swipe') {");
});
it("hands a 'swipe' drag back to the scroll once it passes the swipe's own drift limit", () => {
// A thumb arc whose first move leans sideways (+9 px across, +5 px down) locks
// as 'swipe'; SwipeHandler drops it past maxVerticalDrift, so without the
// handback the 300 px of downward travel that follows would do nothing.
const start = TERMINAL_SOURCE.indexOf("if (panAxis === 'swipe') {");
const block = TERMINAL_SOURCE.slice(start, TERMINAL_SOURCE.indexOf("if (panAxis === 'x') {", start));
expect(block).toContain('SwipeHandler.maxVerticalDrift');
expect(block).toContain('if (Math.abs(touchY - touchStartY) <= drift) return;');
expect(block).toContain("panAxis = 'y';");
expect(block).toContain("this._terminalGestureAxis = 'y';");
});
});