mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 08:59:40 +02:00
fix(mobile): keep keyboard focus taps non-activating
This commit is contained in:
@@ -650,6 +650,7 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
|
|
||||||
let didScroll = false; // track whether touchmove fired (tap vs scroll)
|
let didScroll = false; // track whether touchmove fired (tap vs scroll)
|
||||||
let touchStartY = 0;
|
let touchStartY = 0;
|
||||||
|
let tapCanActivateTerminal = false;
|
||||||
const TAP_THRESHOLD = 8; // px — ignore micro-drift to distinguish tap from scroll
|
const TAP_THRESHOLD = 8; // px — ignore micro-drift to distinguish tap from scroll
|
||||||
container.addEventListener(
|
container.addEventListener(
|
||||||
'touchstart',
|
'touchstart',
|
||||||
@@ -662,6 +663,7 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
pixelAccum = 0;
|
pixelAccum = 0;
|
||||||
isTouching = true;
|
isTouching = true;
|
||||||
didScroll = false;
|
didScroll = false;
|
||||||
|
tapCanActivateTerminal = this._shouldForwardMobileTapToApp();
|
||||||
lastTime = 0;
|
lastTime = 0;
|
||||||
if (scrollFrame) {
|
if (scrollFrame) {
|
||||||
cancelAnimationFrame(scrollFrame);
|
cancelAnimationFrame(scrollFrame);
|
||||||
@@ -737,9 +739,13 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
if (touch) {
|
if (touch) {
|
||||||
this._suppressTrustedTapMouseEvents();
|
this._suppressTrustedTapMouseEvents();
|
||||||
}
|
}
|
||||||
if (touch && mouseTrackingOn) {
|
if (touch && tapCanActivateTerminal && mouseTrackingOn) {
|
||||||
this._dispatchSyntheticTerminalClick(touch.clientX, touch.clientY);
|
this._dispatchSyntheticTerminalClick(touch.clientX, touch.clientY);
|
||||||
} else if (touch && this._sessionUsesServerMouseStrip()) {
|
} else if (
|
||||||
|
touch &&
|
||||||
|
tapCanActivateTerminal &&
|
||||||
|
this._sessionUsesServerMouseStrip()
|
||||||
|
) {
|
||||||
// The server strips mouse-tracking DECSETs from claude/codex/gemini
|
// The server strips mouse-tracking DECSETs from claude/codex/gemini
|
||||||
// output (isAltScreenStripMode, session.ts) so the wheel keeps
|
// output (isAltScreenStripMode, session.ts) so the wheel keeps
|
||||||
// scrolling scrollback — which leaves THIS xterm permanently at
|
// scrolling scrollback — which leaves THIS xterm permanently at
|
||||||
@@ -759,6 +765,7 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
this.terminal.focus();
|
this.terminal.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
tapCanActivateTerminal = false;
|
||||||
},
|
},
|
||||||
{ passive: true }
|
{ passive: true }
|
||||||
);
|
);
|
||||||
@@ -3363,6 +3370,26 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
} catch {}
|
} catch {}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A tap that opens the phone keyboard is focus-only. Forwarding that same
|
||||||
|
* gesture as a mouse click would activate the highlighted CLI menu option.
|
||||||
|
* Once the keyboard and terminal input were already active at touchstart,
|
||||||
|
* later taps may intentionally position the cursor or select a TUI row.
|
||||||
|
*/
|
||||||
|
_shouldForwardMobileTapToApp() {
|
||||||
|
const keyboardVisible =
|
||||||
|
(typeof KeyboardHandler !== 'undefined' && KeyboardHandler.keyboardVisible) ||
|
||||||
|
document.body?.classList?.contains('keyboard-visible');
|
||||||
|
if (!keyboardVisible) return false;
|
||||||
|
|
||||||
|
const active = document.activeElement;
|
||||||
|
return (
|
||||||
|
active === this.terminal?.textarea ||
|
||||||
|
active?.classList?.contains('xterm-helper-textarea') ||
|
||||||
|
active?.id === 'cjkInput'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════
|
||||||
// Synthetic tap → mouse report
|
// Synthetic tap → mouse report
|
||||||
// ═══════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|||||||
@@ -756,12 +756,16 @@ describe('Virtual Keyboard', () => {
|
|||||||
|
|
||||||
it('focuses the terminal helper textarea when the terminal is tapped', async () => {
|
it('focuses the terminal helper textarea when the terminal is tapped', async () => {
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
|
window.__sentInputs = [];
|
||||||
app.activeSessionId = 'mobile-focus-visible-input-test';
|
app.activeSessionId = 'mobile-focus-visible-input-test';
|
||||||
app.sessions.set('mobile-focus-visible-input-test', {
|
app.sessions.set('mobile-focus-visible-input-test', {
|
||||||
id: 'mobile-focus-visible-input-test',
|
id: 'mobile-focus-visible-input-test',
|
||||||
mode: 'codex',
|
mode: 'codex',
|
||||||
status: 'running',
|
status: 'running',
|
||||||
});
|
});
|
||||||
|
app._sendInputAsync = (_sessionId: string, input: string) => {
|
||||||
|
window.__sentInputs.push(input);
|
||||||
|
};
|
||||||
app.hideWelcome();
|
app.hideWelcome();
|
||||||
const settings = app.loadAppSettingsFromStorage();
|
const settings = app.loadAppSettingsFromStorage();
|
||||||
settings.cjkInputEnabled = false;
|
settings.cjkInputEnabled = false;
|
||||||
@@ -771,8 +775,12 @@ describe('Virtual Keyboard', () => {
|
|||||||
|
|
||||||
await page.locator('#terminalContainer').tap({ position: { x: 40, y: 40 } });
|
await page.locator('#terminalContainer').tap({ position: { x: 40, y: 40 } });
|
||||||
|
|
||||||
const activeClass = await page.evaluate(() => document.activeElement?.className);
|
const state = await page.evaluate(() => ({
|
||||||
expect(activeClass).toContain('xterm-helper-textarea');
|
activeClass: document.activeElement?.className,
|
||||||
|
sentInputs: window.__sentInputs,
|
||||||
|
}));
|
||||||
|
expect(state.activeClass).toContain('xterm-helper-textarea');
|
||||||
|
expect(state.sentInputs).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Regression guard for the phone-keyboard blocker reduced in #173 and re-hit
|
// Regression guard for the phone-keyboard blocker reduced in #173 and re-hit
|
||||||
|
|||||||
@@ -6,8 +6,16 @@ import { describe, expect, it, vi } from 'vitest';
|
|||||||
function loadTerminalUiHarness() {
|
function loadTerminalUiHarness() {
|
||||||
const CodemanApp = function CodemanApp(this: any) {};
|
const CodemanApp = function CodemanApp(this: any) {};
|
||||||
let now = 1_000;
|
let now = 1_000;
|
||||||
|
let keyboardVisible = false;
|
||||||
|
let activeElement: unknown = null;
|
||||||
const context = vm.createContext({
|
const context = vm.createContext({
|
||||||
window: {},
|
window: {},
|
||||||
|
document: {
|
||||||
|
body: { classList: { contains: () => false } },
|
||||||
|
get activeElement() {
|
||||||
|
return activeElement;
|
||||||
|
},
|
||||||
|
},
|
||||||
CodemanApp,
|
CodemanApp,
|
||||||
console: { warn: vi.fn(), log: vi.fn() },
|
console: { warn: vi.fn(), log: vi.fn() },
|
||||||
_crashDiag: { log: vi.fn() },
|
_crashDiag: { log: vi.fn() },
|
||||||
@@ -25,6 +33,11 @@ function loadTerminalUiHarness() {
|
|||||||
MobileDetection: {
|
MobileDetection: {
|
||||||
isTouchDevice: () => true,
|
isTouchDevice: () => true,
|
||||||
},
|
},
|
||||||
|
KeyboardHandler: {
|
||||||
|
get keyboardVisible() {
|
||||||
|
return keyboardVisible;
|
||||||
|
},
|
||||||
|
},
|
||||||
DEC_SYNC_STRIP_RE: /\x1b\[\?2026[hl]/g,
|
DEC_SYNC_STRIP_RE: /\x1b\[\?2026[hl]/g,
|
||||||
TERMINAL_CHUNK_SIZE: 32 * 1024,
|
TERMINAL_CHUNK_SIZE: 32 * 1024,
|
||||||
});
|
});
|
||||||
@@ -38,6 +51,12 @@ function loadTerminalUiHarness() {
|
|||||||
setNow: (value: number) => {
|
setNow: (value: number) => {
|
||||||
now = value;
|
now = value;
|
||||||
},
|
},
|
||||||
|
setKeyboardVisible: (visible: boolean) => {
|
||||||
|
keyboardVisible = visible;
|
||||||
|
},
|
||||||
|
setActiveElement: (element: unknown) => {
|
||||||
|
activeElement = element;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,6 +75,19 @@ function createElementHarness() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('terminal touch tap mouse guard', () => {
|
describe('terminal touch tap mouse guard', () => {
|
||||||
|
it('keeps the keyboard-opening tap focus-only', () => {
|
||||||
|
const { app, setActiveElement, setKeyboardVisible } = loadTerminalUiHarness();
|
||||||
|
const textarea = { classList: { contains: () => true } };
|
||||||
|
app.terminal = { textarea };
|
||||||
|
setActiveElement(textarea);
|
||||||
|
|
||||||
|
setKeyboardVisible(false);
|
||||||
|
expect(app._shouldForwardMobileTapToApp()).toBe(false);
|
||||||
|
|
||||||
|
setKeyboardVisible(true);
|
||||||
|
expect(app._shouldForwardMobileTapToApp()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it('suppresses browser trusted compatibility mouse events during the tap window', () => {
|
it('suppresses browser trusted compatibility mouse events during the tap window', () => {
|
||||||
const { app } = loadTerminalUiHarness();
|
const { app } = loadTerminalUiHarness();
|
||||||
const { element, dispatch } = createElementHarness();
|
const { element, dispatch } = createElementHarness();
|
||||||
|
|||||||
Reference in New Issue
Block a user