fix: set text position should not reset component text (#49450)

Summary:
fix: https://github.com/facebook/react-native/issues/49368
description is provided inside the ticket.
When we use TextInput on ios and manage selection with the selection prop, TextInput is reset when we change selection.

## Changelog:

[IOS] [FIXED] - Fix selection makes TextInput clear its content when using children

Pull Request resolved: https://github.com/facebook/react-native/pull/49450

Test Plan:
Tested with sample provided in ticket.
I also test it with my app on both android and ios, but I cannot share video

Reviewed By: sammy-SC

Differential Revision: D69984616

Pulled By: cipolleschi

fbshipit-source-id: a17169608f9df0ea1cb579e6038345f8e48bbc27
This commit is contained in:
Olivier Bouillet
2025-04-01 09:20:18 +00:00
committed by React Native Bot
parent 23c9dbc563
commit 301532b51f
3 changed files with 187 additions and 27 deletions
@@ -1019,7 +1019,7 @@ function useTextInputStateSynchronization_STATE({
mostRecentEventCount: number,
selection: ?Selection,
inputRef: React.RefObject<null | HostInstance>,
text: string,
text?: string,
viewCommands: ViewCommands,
}): {
setLastNativeText: string => void,
@@ -1100,7 +1100,7 @@ function useTextInputStateSynchronization_REFS({
mostRecentEventCount: number,
selection: ?Selection,
inputRef: React.RefObject<null | HostInstance>,
text: string,
text?: string,
viewCommands: ViewCommands,
}): {
setLastNativeText: string => void,
@@ -1314,7 +1314,7 @@ function InternalTextInput(props: Props): React.Node {
? props.value
: typeof props.defaultValue === 'string'
? props.defaultValue
: '';
: undefined;
const viewCommands =
AndroidTextInputCommands ||
@@ -7,7 +7,7 @@
* @format
*/
const {create} = require('../../../../jest/renderer');
const {create, update} = require('../../../../jest/renderer');
const ReactNativeFeatureFlags = require('../../../../src/private/featureflags/ReactNativeFeatureFlags');
const ReactNative = require('../../../ReactNative/RendererProxy');
const {
@@ -20,8 +20,14 @@ const ReactTestRenderer = require('react-test-renderer');
jest.unmock('../TextInput');
[true, false].forEach(useRefsForTextInputState => {
describe(`TextInput tests (useRefsForTextInputState = ${useRefsForTextInputState}`, () => {
[
{useRefsForTextInputState: true, useTextChildren: true},
{useRefsForTextInputState: false, useTextChildren: true},
{useRefsForTextInputState: true, useTextChildren: false},
{useRefsForTextInputState: false, useTextChildren: false},
].forEach(testCase => {
const {useRefsForTextInputState, useTextChildren} = testCase;
describe(`TextInput tests (useRefsForTextInputState = ${useRefsForTextInputState}) useTextChildren = ${useTextChildren}`, () => {
let input;
let inputRef;
let onChangeListener;
@@ -43,15 +49,16 @@ jest.unmock('../TextInput');
return (
<TextInput
ref={inputRef}
value={state.text}
value={useTextChildren ? undefined : state.text}
onChangeText={text => {
onChangeTextListener(text);
setState({text});
}}
onChange={event => {
onChangeListener(event);
}}
/>
}}>
{useTextChildren ? state.text : undefined}
</TextInput>
);
}
const renderTree = await create(<TextInputWrapper />);
@@ -75,12 +82,20 @@ jest.unmock('../TextInput');
);
});
it('calls onChange callbacks', () => {
expect(input.props.value).toBe(initialValue);
if (!useTextChildren) {
expect(input.props.value).toBe(initialValue);
} else {
expect(input.props.children).toBe(initialValue);
}
const message = 'This is a test message';
ReactTestRenderer.act(() => {
enter(input, message);
});
expect(input.props.value).toBe(message);
if (!useTextChildren) {
expect(input.props.value).toBe(message);
} else {
expect(input.props.children).toBe(message);
}
expect(onChangeTextListener).toHaveBeenCalledWith(message);
expect(onChangeListener).toHaveBeenCalledWith({
nativeEvent: {text: message},
@@ -90,7 +105,12 @@ jest.unmock('../TextInput');
async function createTextInput(extraProps) {
const textInputRef = React.createRef(null);
await create(
<TextInput ref={textInputRef} value="value1" {...extraProps} />,
<TextInput
ref={textInputRef}
value={useTextChildren ? undefined : 'value1'}
{...extraProps}>
{useTextChildren ? 'value1' : undefined}
</TextInput>,
);
return textInputRef;
}
@@ -134,14 +154,55 @@ jest.unmock('../TextInput');
expect(TextInput.State.currentlyFocusedInput()).toBe(null);
});
it('change selection keeps content', async () => {
const defaultValue = 'value1';
// create content
let renderTree = await create(
<TextInput
value={useTextChildren ? undefined : defaultValue}
position={{start: 1, end: 1}}>
{useTextChildren ? defaultValue : undefined}
</TextInput>,
);
input = renderTree.root.findByType(TextInput);
expect(
useTextChildren ? input.children[0].props.children : input.props.value,
).toBe(defaultValue);
expect(input.props.position.start).toBe(1);
expect(input.props.position.end).toBe(1);
// update position
renderTree = await update(
renderTree,
<TextInput
value={useTextChildren ? undefined : defaultValue}
position={{start: 2, end: 2}}>
{useTextChildren ? defaultValue : undefined}
</TextInput>,
);
expect(
useTextChildren ? input.children[0].props.children : input.props.value,
).toBe(defaultValue);
expect(input.props.position.start).toBe(2);
expect(input.props.position.end).toBe(2);
});
it('should unfocus when other TextInput is focused', async () => {
const textInputRe1 = React.createRef(null);
const textInputRe2 = React.createRef(null);
await create(
<>
<TextInput ref={textInputRe1} value="value1" />
<TextInput ref={textInputRe2} value="value2" />
<TextInput
ref={textInputRe1}
value={useTextChildren ? undefined : 'value1'}>
{useTextChildren ? 'value1' : undefined}
</TextInput>
<TextInput
ref={textInputRe2}
value={useTextChildren ? undefined : 'value2'}>
{useTextChildren ? 'value2' : undefined}
</TextInput>
</>,
);
ReactNative.findNodeHandle = jest.fn().mockImplementation(ref => {
@@ -210,7 +271,6 @@ jest.unmock('../TextInput');
rejectResponderTermination={true}
selection={null}
submitBehavior="blurAndSubmit"
text=""
textContentType="emailAddress"
underlineColorAndroid="transparent"
/>
@@ -255,7 +315,6 @@ jest.unmock('../TextInput');
rejectResponderTermination={true}
selection={null}
submitBehavior="blurAndSubmit"
text=""
underlineColorAndroid="transparent"
/>
`);
@@ -301,7 +360,6 @@ jest.unmock('../TextInput');
selection={null}
submitBehavior="blurAndSubmit"
testID="testID"
text=""
underlineColorAndroid="transparent"
/>
`);
@@ -432,7 +490,6 @@ jest.unmock('../TextInput');
role="main"
selection={null}
submitBehavior="blurAndSubmit"
text=""
underlineColorAndroid="transparent"
/>
`);
@@ -489,7 +546,6 @@ jest.unmock('../TextInput');
]
}
submitBehavior="blurAndSubmit"
text=""
underlineColorAndroid="transparent"
/>
`);
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TextInput tests (useRefsForTextInputState = false should render as expected: should deep render when mocked (please verify output manually) 1`] = `
exports[`TextInput tests (useRefsForTextInputState = false) useTextChildren = false should render as expected: should deep render when mocked (please verify output manually) 1`] = `
<RCTSinglelineTextInputView
accessible={true}
allowFontScaling={true}
@@ -23,12 +23,11 @@ exports[`TextInput tests (useRefsForTextInputState = false should render as expe
rejectResponderTermination={true}
selection={null}
submitBehavior="blurAndSubmit"
text=""
underlineColorAndroid="transparent"
/>
`;
exports[`TextInput tests (useRefsForTextInputState = false should render as expected: should deep render when not mocked (please verify output manually) 1`] = `
exports[`TextInput tests (useRefsForTextInputState = false) useTextChildren = false should render as expected: should deep render when not mocked (please verify output manually) 1`] = `
<RCTSinglelineTextInputView
accessible={true}
allowFontScaling={true}
@@ -51,12 +50,11 @@ exports[`TextInput tests (useRefsForTextInputState = false should render as expe
rejectResponderTermination={true}
selection={null}
submitBehavior="blurAndSubmit"
text=""
underlineColorAndroid="transparent"
/>
`;
exports[`TextInput tests (useRefsForTextInputState = true should render as expected: should deep render when mocked (please verify output manually) 1`] = `
exports[`TextInput tests (useRefsForTextInputState = false) useTextChildren = true should render as expected: should deep render when mocked (please verify output manually) 1`] = `
<RCTSinglelineTextInputView
accessible={true}
allowFontScaling={true}
@@ -79,12 +77,119 @@ exports[`TextInput tests (useRefsForTextInputState = true should render as expec
rejectResponderTermination={true}
selection={null}
submitBehavior="blurAndSubmit"
text=""
underlineColorAndroid="transparent"
/>
`;
exports[`TextInput tests (useRefsForTextInputState = true should render as expected: should deep render when not mocked (please verify output manually) 1`] = `
exports[`TextInput tests (useRefsForTextInputState = false) useTextChildren = true should render as expected: should deep render when not mocked (please verify output manually) 1`] = `
<RCTSinglelineTextInputView
accessible={true}
allowFontScaling={true}
focusable={true}
forwardedRef={null}
mostRecentEventCount={0}
onBlur={[Function]}
onChange={[Function]}
onClick={[Function]}
onFocus={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onScroll={[Function]}
onSelectionChange={[Function]}
onSelectionChangeShouldSetResponder={[Function]}
onStartShouldSetResponder={[Function]}
rejectResponderTermination={true}
selection={null}
submitBehavior="blurAndSubmit"
underlineColorAndroid="transparent"
/>
`;
exports[`TextInput tests (useRefsForTextInputState = true) useTextChildren = false should render as expected: should deep render when mocked (please verify output manually) 1`] = `
<RCTSinglelineTextInputView
accessible={true}
allowFontScaling={true}
focusable={true}
forwardedRef={null}
mostRecentEventCount={0}
onBlur={[Function]}
onChange={[Function]}
onClick={[Function]}
onFocus={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onScroll={[Function]}
onSelectionChange={[Function]}
onSelectionChangeShouldSetResponder={[Function]}
onStartShouldSetResponder={[Function]}
rejectResponderTermination={true}
selection={null}
submitBehavior="blurAndSubmit"
underlineColorAndroid="transparent"
/>
`;
exports[`TextInput tests (useRefsForTextInputState = true) useTextChildren = false should render as expected: should deep render when not mocked (please verify output manually) 1`] = `
<RCTSinglelineTextInputView
accessible={true}
allowFontScaling={true}
focusable={true}
forwardedRef={null}
mostRecentEventCount={0}
onBlur={[Function]}
onChange={[Function]}
onClick={[Function]}
onFocus={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onScroll={[Function]}
onSelectionChange={[Function]}
onSelectionChangeShouldSetResponder={[Function]}
onStartShouldSetResponder={[Function]}
rejectResponderTermination={true}
selection={null}
submitBehavior="blurAndSubmit"
underlineColorAndroid="transparent"
/>
`;
exports[`TextInput tests (useRefsForTextInputState = true) useTextChildren = true should render as expected: should deep render when mocked (please verify output manually) 1`] = `
<RCTSinglelineTextInputView
accessible={true}
allowFontScaling={true}
focusable={true}
forwardedRef={null}
mostRecentEventCount={0}
onBlur={[Function]}
onChange={[Function]}
onClick={[Function]}
onFocus={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onScroll={[Function]}
onSelectionChange={[Function]}
onSelectionChangeShouldSetResponder={[Function]}
onStartShouldSetResponder={[Function]}
rejectResponderTermination={true}
selection={null}
submitBehavior="blurAndSubmit"
underlineColorAndroid="transparent"
/>
`;
exports[`TextInput tests (useRefsForTextInputState = true) useTextChildren = true should render as expected: should deep render when not mocked (please verify output manually) 1`] = `
<RCTSinglelineTextInputView
accessible={true}
allowFontScaling={true}
@@ -107,7 +212,6 @@ exports[`TextInput tests (useRefsForTextInputState = true should render as expec
rejectResponderTermination={true}
selection={null}
submitBehavior="blurAndSubmit"
text=""
underlineColorAndroid="transparent"
/>
`;