fix(app): preserve IME composition across text fields (#3517)
* fix(app): preserve IME composition in form inputs Form model publication was replaying changing default text into the native or DOM editing surface. Keep the editing surface authoritative, defer web publication until composition commits, and reserve resetKey changes for explicit text replacement. * refactor(app): centralize text editing authority Route every app text field, including the composer and plugin runtime, through one platform-owned editing primitive. Its type and runtime reject controlled value replay, its command handle owns programmatic replacement, and lint prevents raw React Native TextInput imports outside the boundary. * fix(app): restore composer placeholder styling
This commit is contained in:
@@ -10,7 +10,11 @@ import {
|
||||
createElement,
|
||||
} from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { Pressable, Text, TextInput, View, type StyleProp, type ViewStyle } from "react-native";
|
||||
import { Pressable, Text, View, type StyleProp, type ViewStyle } from "react-native";
|
||||
import {
|
||||
EditingTextInput as TextInput,
|
||||
type EditingTextInputHandle,
|
||||
} from "@/components/ui/text-input";
|
||||
import {
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
@@ -91,10 +95,6 @@ type ElectronWebview = HTMLElement & {
|
||||
removeEventListener: (type: string, listener: EventListenerOrEventListenerObject) => void;
|
||||
};
|
||||
|
||||
type WebTextInput = TextInput & {
|
||||
getNativeRef?: () => unknown;
|
||||
};
|
||||
|
||||
interface BrowserElementAnnotation {
|
||||
comment: string;
|
||||
}
|
||||
@@ -403,8 +403,10 @@ function clearAnnotationMarkers(webview: ElectronWebview): void {
|
||||
).catch(ignoreWebviewJavaScriptError);
|
||||
}
|
||||
|
||||
function getTextInputNativeElement(current: WebTextInput | null): HTMLInputElement | null {
|
||||
const native = current?.getNativeRef?.() ?? current;
|
||||
function getTextInputNativeElement(
|
||||
current: EditingTextInputHandle | null,
|
||||
): HTMLInputElement | null {
|
||||
const native = current?.getNativeRef();
|
||||
return native instanceof HTMLInputElement ? native : null;
|
||||
}
|
||||
|
||||
@@ -606,7 +608,7 @@ export function BrowserPane({
|
||||
const webviewRef = useRef<ElectronWebview | null>(null);
|
||||
const webviewHostRef = useRef<HTMLDivElement | null>(null);
|
||||
const webviewClipRef = useRef<HTMLElement | null>(null);
|
||||
const urlInputRef = useRef<WebTextInput | null>(null);
|
||||
const urlInputRef = useRef<EditingTextInputHandle | null>(null);
|
||||
const initialUrlRef = useRef(browser?.url ?? "https://example.com");
|
||||
const browserIdRef = useRef(browserId);
|
||||
browserIdRef.current = browserId;
|
||||
@@ -674,6 +676,7 @@ export function BrowserPane({
|
||||
|
||||
useEffect(() => {
|
||||
const nextUrl = browser?.url ?? "https://example.com";
|
||||
urlInputRef.current?.replaceText(nextUrl);
|
||||
setDraftUrl((current) => (current === nextUrl ? current : nextUrl));
|
||||
}, [browser?.url]);
|
||||
|
||||
@@ -1473,7 +1476,7 @@ export function BrowserPane({
|
||||
placeholderTextColor={theme.colors.foregroundMuted}
|
||||
ref={urlInputRef}
|
||||
style={urlInputStyle}
|
||||
value={draftUrl}
|
||||
initialValue={draftUrl}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.chromeRight}>
|
||||
@@ -1629,7 +1632,7 @@ function BrowserElementAnnotationCard({
|
||||
placeholder={t("workspace.browser.annotate.placeholder")}
|
||||
style={styles.annotationInput}
|
||||
uniProps={annotationInputMapping}
|
||||
value={comment}
|
||||
initialValue={comment}
|
||||
/>
|
||||
<View style={styles.annotationActions}>
|
||||
<Button variant="ghost" size="sm" onPress={onCancel}>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { Text, TextInput, View } from "react-native";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Text, View } from "react-native";
|
||||
import * as Clipboard from "expo-clipboard";
|
||||
import * as QRCode from "qrcode";
|
||||
import { SvgXml } from "react-native-svg";
|
||||
@@ -16,6 +16,10 @@ import { daemonPairingOfferQueryKey } from "@/data/daemon-pairing";
|
||||
import { useDaemonConfig } from "@/hooks/use-daemon-config";
|
||||
import { useHostRuntimeClient, useHostRuntimeSnapshot } from "@/runtime/host-runtime";
|
||||
import type { Theme } from "@/styles/theme";
|
||||
import {
|
||||
EditingTextInput as TextInput,
|
||||
type EditingTextInputHandle,
|
||||
} from "@/components/ui/text-input";
|
||||
|
||||
const RELAY_DOCS_URL = "https://paseo.sh/docs/security";
|
||||
const FLEX_ONE_STYLE = { flex: 1 } as const;
|
||||
@@ -230,6 +234,8 @@ function RelayHeroBadge() {
|
||||
|
||||
function PairingOffer(props: PairDeviceBodyProps & { offer: { url: string } }) {
|
||||
const { t } = useTranslation();
|
||||
const inputRef = useRef<EditingTextInputHandle>(null);
|
||||
useEffect(() => inputRef.current?.replaceText(props.offer.url), [props.offer.url]);
|
||||
return (
|
||||
<View style={styles.offer}>
|
||||
<Text style={styles.offerHint}>{t("pairing.device.hint")}</Text>
|
||||
@@ -239,8 +245,9 @@ function PairingOffer(props: PairDeviceBodyProps & { offer: { url: string } }) {
|
||||
<View style={styles.linkRow}>
|
||||
<View style={styles.inputWrapper}>
|
||||
<TextInput
|
||||
ref={inputRef}
|
||||
style={styles.linkInput}
|
||||
value={props.offer.url}
|
||||
initialValue={props.offer.url}
|
||||
readOnly
|
||||
selectTextOnFocus
|
||||
accessibilityLabel={t("pairing.link.label")}
|
||||
|
||||
Reference in New Issue
Block a user