fix(ui): Android soft keyboard support in terminal touch input
- summon the IME via Capacitor Keyboard.show() on tap-to-focus, since Android WebView only raises the keyboard for native tap-focus - forward beforeinput text/backspace/enter to the terminal because Android IMEs deliver input as beforeinput with keyCode 229 keydowns, which ghostty-web ignores - blur the focused terminal input and hide the keyboard before disposal so tearing down a focused editable mid-composition no longer wedges WebView touch dispatch (app froze on terminal tab close)
This commit is contained in:
@@ -165,6 +165,20 @@ const TerminalViewport = React.forwardRef<TerminalController, Props>(({
|
||||
|
||||
return () => {
|
||||
disposed = true;
|
||||
// Removing a focused editable mid-IME-composition wedges Android
|
||||
// WebView's input dispatch (the whole app stops responding to touch).
|
||||
// Blur first so the IME detaches cleanly, and hide the soft keyboard
|
||||
// explicitly on Android before the terminal DOM is torn down.
|
||||
const active = document.activeElement;
|
||||
if (active instanceof HTMLElement && container.contains(active)) {
|
||||
active.blur();
|
||||
const capacitor = (window as typeof window & { Capacitor?: { getPlatform?: () => string } }).Capacitor;
|
||||
if (capacitor?.getPlatform?.() === 'android') {
|
||||
void import('@capacitor/keyboard')
|
||||
.then(({ Keyboard }) => Keyboard.hide())
|
||||
.catch(() => undefined);
|
||||
}
|
||||
}
|
||||
observer?.disconnect();
|
||||
if (resizeTimeout) clearTimeout(resizeTimeout);
|
||||
if (fitFrame !== null) cancelAnimationFrame(fitFrame);
|
||||
@@ -218,6 +232,36 @@ const TerminalViewport = React.forwardRef<TerminalController, Props>(({
|
||||
return () => cancelAnimationFrame(frame);
|
||||
}, [autoFocus, isVisible, ready, sessionKey]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (!enableTouchScroll || !container) return;
|
||||
// ghostty-web only reads keydown/composition events and preventDefaults
|
||||
// beforeinput without consuming it. Android IMEs deliver text via
|
||||
// beforeinput (their keydown arrives as keyCode 229, which ghostty
|
||||
// ignores), so forward those payloads to the terminal here. Composition
|
||||
// updates are skipped: ghostty commits them itself on compositionend.
|
||||
const handleBeforeInput = (event: Event) => {
|
||||
const input = event as InputEvent;
|
||||
if (input.isComposing) return;
|
||||
switch (input.inputType) {
|
||||
case 'insertText':
|
||||
if (input.data) inputRef.current(input.data);
|
||||
break;
|
||||
case 'insertLineBreak':
|
||||
case 'insertParagraph':
|
||||
inputRef.current('\r');
|
||||
break;
|
||||
case 'deleteContentBackward':
|
||||
inputRef.current('\x7f');
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
container.addEventListener('beforeinput', handleBeforeInput);
|
||||
return () => container.removeEventListener('beforeinput', handleBeforeInput);
|
||||
}, [enableTouchScroll, ready]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
const terminal = terminalRef.current;
|
||||
@@ -231,6 +275,16 @@ const TerminalViewport = React.forwardRef<TerminalController, Props>(({
|
||||
let remainder = 0;
|
||||
let selectionFocus: TerminalCellPosition | null = null;
|
||||
const lineHeight = Math.max(12, fontSize + 2);
|
||||
// Android WebView only raises the soft keyboard for a native tap-focus; the
|
||||
// pointer-captured, touch-action:none tap here focuses programmatically, so
|
||||
// the IME must be summoned explicitly via the Capacitor Keyboard plugin.
|
||||
const showAndroidSoftKeyboard = () => {
|
||||
const capacitor = (window as typeof window & { Capacitor?: { getPlatform?: () => string } }).Capacitor;
|
||||
if (capacitor?.getPlatform?.() !== 'android') return;
|
||||
void import('@capacitor/keyboard')
|
||||
.then(({ Keyboard }) => Keyboard.show())
|
||||
.catch(() => undefined);
|
||||
};
|
||||
const clearLongPress = () => {
|
||||
if (!longPressTimeout) return;
|
||||
clearTimeout(longPressTimeout);
|
||||
@@ -332,7 +386,10 @@ const TerminalViewport = React.forwardRef<TerminalController, Props>(({
|
||||
pointerId = null;
|
||||
gesture = 'idle';
|
||||
if (shouldFinishSelection) finishSelection();
|
||||
if (shouldFocus) terminal.focus();
|
||||
if (shouldFocus) {
|
||||
terminal.focus();
|
||||
showAndroidSoftKeyboard();
|
||||
}
|
||||
};
|
||||
const cancel = (event: PointerEvent) => {
|
||||
if (pointerId !== event.pointerId) return;
|
||||
|
||||
Reference in New Issue
Block a user