mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-28 04:03:05 +00:00
* 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.
96 lines
3.4 KiB
TypeScript
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}
|
|
/>
|
|
);
|
|
},
|
|
);
|