diff --git a/src/web/public/terminal-ui.js b/src/web/public/terminal-ui.js index ba97630b..a497b597 100644 --- a/src/web/public/terminal-ui.js +++ b/src/web/public/terminal-ui.js @@ -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 }); },