Files
paseo/packages/app/src/components/ui/text-input/text-input.web.tsx
T
Mohamed Boudra 1622da40d9 Keep the mobile terminal keyboard open while typing (#4469)
* fix(app): keep terminal keyboard focus stable

Clearing the terminal buffer remounted the shared native input, triggering an IME hide/show cycle. Keep text replacement in place and make the composer explicitly reset its editor to preserve multiline layout clearing.

* test(app): verify terminal keyboard continuity on Android

Replace the new JSDOM regression with a real Android harness. It detects focused native input replacement and IME hide/show requests through terminal toolbar controls, and fails with the previous remount behavior.
2026-09-08 09:48:48 +02:00

96 lines
3.4 KiB
TypeScript

import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useRef } from "react";
import { TextInput } from "react-native";
import type { EditingTextInputHandle, EditingTextInputProps } from "./types";
interface WebTextInputElement extends TextInput {
value?: string;
setSelectionRange?: (start: number, end: number) => void;
addEventListener(type: "compositionstart" | "compositionend", listener: EventListener): void;
removeEventListener(type: "compositionstart" | "compositionend", listener: EventListener): void;
}
export const EditingTextInput = forwardRef<EditingTextInputHandle, EditingTextInputProps>(
function EditingTextInputWeb(allProps, ref) {
const {
initialValue = "",
onChangeText,
onPasteImages: _,
onPasteError: __,
variant: ___,
value: ____,
defaultValue: _____,
...props
} = allProps as EditingTextInputProps & { value?: unknown; defaultValue?: unknown };
const inputRef = useRef<TextInput | null>(null);
const initialTextRef = useRef(initialValue);
const textRef = useRef(initialTextRef.current);
const isComposingRef = useRef(false);
const onChangeTextRef = useRef(onChangeText);
onChangeTextRef.current = onChangeText;
useEffect(() => {
const input = inputRef.current as WebTextInputElement | null;
if (!input) return;
const startComposition = () => {
isComposingRef.current = true;
};
const endComposition = () => {
isComposingRef.current = false;
const nextText = input.value ?? "";
if (nextText === textRef.current) return;
textRef.current = nextText;
onChangeTextRef.current?.(nextText);
};
input.addEventListener("compositionstart", startComposition);
input.addEventListener("compositionend", endComposition);
return () => {
input.removeEventListener("compositionstart", startComposition);
input.removeEventListener("compositionend", endComposition);
};
}, []);
const handleChangeText = useCallback((nextText: string) => {
if (isComposingRef.current || nextText === textRef.current) return;
textRef.current = nextText;
onChangeTextRef.current?.(nextText);
}, []);
useImperativeHandle(ref, () => ({
focus: () => inputRef.current?.focus(),
blur: () => inputRef.current?.blur(),
isFocused: () => document.activeElement === inputRef.current,
getText: () => {
const input = inputRef.current as WebTextInputElement | null;
const nextText = input?.value ?? textRef.current;
textRef.current = nextText;
return nextText;
},
replaceText: (nextText, selection) => {
textRef.current = nextText;
const input = inputRef.current as WebTextInputElement | null;
if (input && "value" in input) input.value = nextText;
if (selection && typeof input?.setSelectionRange === "function") {
input.setSelectionRange(selection.start, selection.end);
}
},
reset: () => {
textRef.current = "";
const input = inputRef.current as WebTextInputElement | null;
if (input && "value" in input) input.value = "";
},
getNativeRef: () => inputRef.current,
}));
return (
<TextInput
{...props}
ref={inputRef}
defaultValue={initialTextRef.current}
onChangeText={handleChangeText}
/>
);
},
);