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 () => {
|
return () => {
|
||||||
disposed = true;
|
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();
|
observer?.disconnect();
|
||||||
if (resizeTimeout) clearTimeout(resizeTimeout);
|
if (resizeTimeout) clearTimeout(resizeTimeout);
|
||||||
if (fitFrame !== null) cancelAnimationFrame(fitFrame);
|
if (fitFrame !== null) cancelAnimationFrame(fitFrame);
|
||||||
@@ -218,6 +232,36 @@ const TerminalViewport = React.forwardRef<TerminalController, Props>(({
|
|||||||
return () => cancelAnimationFrame(frame);
|
return () => cancelAnimationFrame(frame);
|
||||||
}, [autoFocus, isVisible, ready, sessionKey]);
|
}, [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(() => {
|
React.useEffect(() => {
|
||||||
const container = containerRef.current;
|
const container = containerRef.current;
|
||||||
const terminal = terminalRef.current;
|
const terminal = terminalRef.current;
|
||||||
@@ -231,6 +275,16 @@ const TerminalViewport = React.forwardRef<TerminalController, Props>(({
|
|||||||
let remainder = 0;
|
let remainder = 0;
|
||||||
let selectionFocus: TerminalCellPosition | null = null;
|
let selectionFocus: TerminalCellPosition | null = null;
|
||||||
const lineHeight = Math.max(12, fontSize + 2);
|
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 = () => {
|
const clearLongPress = () => {
|
||||||
if (!longPressTimeout) return;
|
if (!longPressTimeout) return;
|
||||||
clearTimeout(longPressTimeout);
|
clearTimeout(longPressTimeout);
|
||||||
@@ -332,7 +386,10 @@ const TerminalViewport = React.forwardRef<TerminalController, Props>(({
|
|||||||
pointerId = null;
|
pointerId = null;
|
||||||
gesture = 'idle';
|
gesture = 'idle';
|
||||||
if (shouldFinishSelection) finishSelection();
|
if (shouldFinishSelection) finishSelection();
|
||||||
if (shouldFocus) terminal.focus();
|
if (shouldFocus) {
|
||||||
|
terminal.focus();
|
||||||
|
showAndroidSoftKeyboard();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
const cancel = (event: PointerEvent) => {
|
const cancel = (event: PointerEvent) => {
|
||||||
if (pointerId !== event.pointerId) return;
|
if (pointerId !== event.pointerId) return;
|
||||||
|
|||||||
Reference in New Issue
Block a user