fix(mobile): do not dismiss the keyboard when a scroll ends

Regression from the dismiss handler in #279: it fired on any touchend,
and a scroll ends in touchend too. Scrolling to read something while composing
closed the keyboard and dropped the composer — worse than the bug it fixed.

Track finger travel from touchstart and only treat a near-stationary gesture as
a tap, using the same 8px TAP_THRESHOLD the terminal's own touch handling uses
so both agree on tap-vs-scroll. Multi-touch is never a dismissing tap.

All three listeners stay passive; nothing calls preventDefault.

Measured on a Pixel-class viewport with a Firefox UA:
  tap                -> dismissed
  scroll (120px)     -> keyboard kept
  micro-drift (4px)  -> dismissed, so an imprecise tap still works

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
lior
2026-08-10 18:21:05 +03:00
co-authored by Claude Opus 5
parent a6a572e635
commit 3a7be356ae
+36 -2
View File
@@ -36,6 +36,9 @@
// (_installMobileKeyboardDismiss). Two groups: anything that is about to take
// focus itself, and the accessory bar, which is built to be used while the
// keyboard is open.
// Finger travel (px) still counted as a tap for keyboard dismissal. Matches
// the terminal's own TAP_THRESHOLD so both agree on tap-vs-scroll.
const MOBILE_KEYBOARD_DISMISS_TAP_SLOP = 8;
const MOBILE_KEYBOARD_DISMISS_EXEMPT_SELECTOR = [
'input',
'textarea',
@@ -199,6 +202,7 @@
PAGE_KEY_MAX_PER_BATCH,
TUI_PROMPT_DEFAULT_ROWS_FROM_BOTTOM,
MOBILE_KEYBOARD_DISMISS_EXEMPT_SELECTOR,
MOBILE_KEYBOARD_DISMISS_TAP_SLOP,
};
global.CODEMAN_XTERM_THEMES = CODEMAN_XTERM_THEMES;
global.codemanCurrentXtermTheme = currentXtermTheme;
@@ -3534,7 +3538,35 @@ Object.assign(CodemanApp.prototype, {
*/
_installMobileKeyboardDismiss() {
if (this._mobileKeyboardDismissHandler) return;
// A SCROLL also ends in touchend, and dismissing there is wrong: scrolling
// to read something while composing must not close the keyboard and lose
// the composer. Track how far the finger travelled and only treat a
// near-stationary gesture as a tap — the same TAP_THRESHOLD the terminal's
// own touch handling uses, so both agree on what a tap is.
let startX = 0;
let startY = 0;
let moved = false;
this._mobileKeyboardDismissStart = (ev) => {
if (ev.touches.length !== 1) {
moved = true; // a multi-touch gesture is never a dismissing tap
return;
}
startX = ev.touches[0].clientX;
startY = ev.touches[0].clientY;
moved = false;
};
this._mobileKeyboardDismissMove = (ev) => {
if (moved || !ev.touches.length) return;
const dx = ev.touches[0].clientX - startX;
const dy = ev.touches[0].clientY - startY;
const slop = window.CodemanTerminalInput.MOBILE_KEYBOARD_DISMISS_TAP_SLOP;
if (Math.abs(dx) > slop || Math.abs(dy) > slop) {
moved = true;
}
};
this._mobileKeyboardDismissHandler = (ev) => {
if (moved) return;
if (!this._isMobileTerminalInputFocused()) return;
const target = ev.target;
if (!target || typeof target.closest !== 'function') return;
@@ -3542,8 +3574,10 @@ Object.assign(CodemanApp.prototype, {
if (target.closest(window.CodemanTerminalInput.MOBILE_KEYBOARD_DISMISS_EXEMPT_SELECTOR)) return;
this._blurMobileTerminalInput();
};
// Passive: this never calls preventDefault, so it must not make the page
// feel less responsive to scrolling.
// Passive throughout: this never calls preventDefault, so it must not make
// the page feel less responsive to scrolling.
document.addEventListener('touchstart', this._mobileKeyboardDismissStart, { passive: true });
document.addEventListener('touchmove', this._mobileKeyboardDismissMove, { passive: true });
document.addEventListener('touchend', this._mobileKeyboardDismissHandler, { passive: true });
},