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:
@@ -12,12 +12,12 @@ import {
|
||||
Text,
|
||||
View,
|
||||
type PressableStateCallbackType,
|
||||
type TextInput,
|
||||
type TextStyle,
|
||||
type ViewStyle,
|
||||
} from "react-native";
|
||||
import { StyleSheet } from "react-native-unistyles";
|
||||
import { AdaptiveTextInput, type AdaptiveTextInputProps } from "@/components/adaptive-modal-sheet";
|
||||
import type { EditingTextInputHandle } from "@/components/ui/text-input";
|
||||
import {
|
||||
createControlGeometry,
|
||||
resolveControlInteractionStyles,
|
||||
@@ -141,7 +141,10 @@ function stripUnistylesMetadata<TStyle extends object>(style: TStyle): TStyle {
|
||||
return cleanStyle as TStyle;
|
||||
}
|
||||
|
||||
function assignTextInputRef(forwardedRef: ForwardedRef<TextInput>, node: TextInput | null): void {
|
||||
function assignTextInputRef(
|
||||
forwardedRef: ForwardedRef<EditingTextInputHandle>,
|
||||
node: EditingTextInputHandle | null,
|
||||
): void {
|
||||
if (typeof forwardedRef === "function") {
|
||||
forwardedRef(node);
|
||||
return;
|
||||
@@ -151,74 +154,73 @@ function assignTextInputRef(forwardedRef: ForwardedRef<TextInput>, node: TextInp
|
||||
}
|
||||
}
|
||||
|
||||
export const FormTextInput = forwardRef<TextInput, FormTextInputProps>(function FormTextInput(
|
||||
{ size = "md", style, onFocus, onBlur, editable, ...props },
|
||||
ref,
|
||||
) {
|
||||
const [focused, setFocused] = useState(false);
|
||||
const isDisabled = editable === false;
|
||||
const chromeSizeStyle = size === "sm" ? formInputStyles.chromeSm : formInputStyles.chromeMd;
|
||||
const inputSizeStyle = size === "sm" ? formInputStyles.inputSm : formInputStyles.inputMd;
|
||||
const splitStyle = useMemo(() => splitFormTextInputStyle(style), [style]);
|
||||
const setInputRef = useCallback(
|
||||
(node: TextInput | null) => {
|
||||
assignTextInputRef(ref, node);
|
||||
},
|
||||
[ref],
|
||||
);
|
||||
const handleFocus = useCallback<NonNullable<AdaptiveTextInputProps["onFocus"]>>(
|
||||
(event) => {
|
||||
setFocused(true);
|
||||
onFocus?.(event);
|
||||
},
|
||||
[onFocus],
|
||||
);
|
||||
const handleBlur = useCallback<NonNullable<AdaptiveTextInputProps["onBlur"]>>(
|
||||
(event) => {
|
||||
setFocused(false);
|
||||
onBlur?.(event);
|
||||
},
|
||||
[onBlur],
|
||||
);
|
||||
const inputStyle = useMemo(
|
||||
() => [formInputStyles.input, inputSizeStyle, splitStyle.inputStyle],
|
||||
[inputSizeStyle, splitStyle.inputStyle],
|
||||
) as AdaptiveTextInputProps["style"];
|
||||
const chromeStyle = useCallback(
|
||||
({ hovered = false }: PressableStateCallbackType & { hovered?: boolean }) => [
|
||||
formInputStyles.chrome,
|
||||
chromeSizeStyle,
|
||||
resolveControlInteractionStyles(
|
||||
{
|
||||
controlRest: formInputStyles.controlRest,
|
||||
controlHover: formInputStyles.controlHover,
|
||||
controlActive: formInputStyles.controlActive,
|
||||
controlDisabled: formInputStyles.controlDisabled,
|
||||
},
|
||||
{
|
||||
hovered,
|
||||
focused,
|
||||
disabled: isDisabled,
|
||||
},
|
||||
),
|
||||
splitStyle.chromeStyle,
|
||||
],
|
||||
[chromeSizeStyle, focused, isDisabled, splitStyle.chromeStyle],
|
||||
);
|
||||
export const FormTextInput = forwardRef<EditingTextInputHandle, FormTextInputProps>(
|
||||
function FormTextInput({ size = "md", style, onFocus, onBlur, editable, ...props }, ref) {
|
||||
const [focused, setFocused] = useState(false);
|
||||
const isDisabled = editable === false;
|
||||
const chromeSizeStyle = size === "sm" ? formInputStyles.chromeSm : formInputStyles.chromeMd;
|
||||
const inputSizeStyle = size === "sm" ? formInputStyles.inputSm : formInputStyles.inputMd;
|
||||
const splitStyle = useMemo(() => splitFormTextInputStyle(style), [style]);
|
||||
const setInputRef = useCallback(
|
||||
(node: EditingTextInputHandle | null) => {
|
||||
assignTextInputRef(ref, node);
|
||||
},
|
||||
[ref],
|
||||
);
|
||||
const handleFocus = useCallback<NonNullable<AdaptiveTextInputProps["onFocus"]>>(
|
||||
(event) => {
|
||||
setFocused(true);
|
||||
onFocus?.(event);
|
||||
},
|
||||
[onFocus],
|
||||
);
|
||||
const handleBlur = useCallback<NonNullable<AdaptiveTextInputProps["onBlur"]>>(
|
||||
(event) => {
|
||||
setFocused(false);
|
||||
onBlur?.(event);
|
||||
},
|
||||
[onBlur],
|
||||
);
|
||||
const inputStyle = useMemo(
|
||||
() => [formInputStyles.input, inputSizeStyle, splitStyle.inputStyle],
|
||||
[inputSizeStyle, splitStyle.inputStyle],
|
||||
) as AdaptiveTextInputProps["style"];
|
||||
const chromeStyle = useCallback(
|
||||
({ hovered = false }: PressableStateCallbackType & { hovered?: boolean }) => [
|
||||
formInputStyles.chrome,
|
||||
chromeSizeStyle,
|
||||
resolveControlInteractionStyles(
|
||||
{
|
||||
controlRest: formInputStyles.controlRest,
|
||||
controlHover: formInputStyles.controlHover,
|
||||
controlActive: formInputStyles.controlActive,
|
||||
controlDisabled: formInputStyles.controlDisabled,
|
||||
},
|
||||
{
|
||||
hovered,
|
||||
focused,
|
||||
disabled: isDisabled,
|
||||
},
|
||||
),
|
||||
splitStyle.chromeStyle,
|
||||
],
|
||||
[chromeSizeStyle, focused, isDisabled, splitStyle.chromeStyle],
|
||||
);
|
||||
|
||||
return (
|
||||
<Pressable disabled={isDisabled} style={chromeStyle}>
|
||||
<AdaptiveTextInput
|
||||
ref={setInputRef}
|
||||
editable={editable}
|
||||
{...props}
|
||||
onBlur={handleBlur}
|
||||
onFocus={handleFocus}
|
||||
style={inputStyle}
|
||||
/>
|
||||
</Pressable>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<Pressable disabled={isDisabled} style={chromeStyle}>
|
||||
<AdaptiveTextInput
|
||||
ref={setInputRef}
|
||||
editable={editable}
|
||||
{...props}
|
||||
onBlur={handleBlur}
|
||||
onFocus={handleFocus}
|
||||
style={inputStyle}
|
||||
/>
|
||||
</Pressable>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const styles = StyleSheet.create((theme) => ({
|
||||
container: {
|
||||
|
||||
Reference in New Issue
Block a user