From 301532b51fe33cd08152c7dde2b15d57105332dd Mon Sep 17 00:00:00 2001 From: Olivier Bouillet Date: Mon, 24 Feb 2025 08:23:30 -0800 Subject: [PATCH] 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 --- .../Components/TextInput/TextInput.js | 6 +- .../TextInput/__tests__/TextInput-test.js | 88 ++++++++++--- .../__snapshots__/TextInput-test.js.snap | 120 ++++++++++++++++-- 3 files changed, 187 insertions(+), 27 deletions(-) diff --git a/packages/react-native/Libraries/Components/TextInput/TextInput.js b/packages/react-native/Libraries/Components/TextInput/TextInput.js index 48a3c99609a..db199daa8b6 100644 --- a/packages/react-native/Libraries/Components/TextInput/TextInput.js +++ b/packages/react-native/Libraries/Components/TextInput/TextInput.js @@ -1019,7 +1019,7 @@ function useTextInputStateSynchronization_STATE({ mostRecentEventCount: number, selection: ?Selection, inputRef: React.RefObject, - text: string, + text?: string, viewCommands: ViewCommands, }): { setLastNativeText: string => void, @@ -1100,7 +1100,7 @@ function useTextInputStateSynchronization_REFS({ mostRecentEventCount: number, selection: ?Selection, inputRef: React.RefObject, - 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 || diff --git a/packages/react-native/Libraries/Components/TextInput/__tests__/TextInput-test.js b/packages/react-native/Libraries/Components/TextInput/__tests__/TextInput-test.js index 5173b8975ac..45bd8d3aa61 100644 --- a/packages/react-native/Libraries/Components/TextInput/__tests__/TextInput-test.js +++ b/packages/react-native/Libraries/Components/TextInput/__tests__/TextInput-test.js @@ -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 ( { onChangeTextListener(text); setState({text}); }} onChange={event => { onChangeListener(event); - }} - /> + }}> + {useTextChildren ? state.text : undefined} + ); } const renderTree = await create(); @@ -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( - , + + {useTextChildren ? 'value1' : undefined} + , ); 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( + + {useTextChildren ? defaultValue : undefined} + , + ); + 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, + + {useTextChildren ? defaultValue : undefined} + , + ); + 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( <> - - + + {useTextChildren ? 'value1' : undefined} + + + {useTextChildren ? 'value2' : undefined} + , ); 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" /> `); diff --git a/packages/react-native/Libraries/Components/TextInput/__tests__/__snapshots__/TextInput-test.js.snap b/packages/react-native/Libraries/Components/TextInput/__tests__/__snapshots__/TextInput-test.js.snap index 6f672ac96c5..ba3c5771de9 100644 --- a/packages/react-native/Libraries/Components/TextInput/__tests__/__snapshots__/TextInput-test.js.snap +++ b/packages/react-native/Libraries/Components/TextInput/__tests__/__snapshots__/TextInput-test.js.snap @@ -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`] = ` `; -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`] = ` `; -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`] = ` `; -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`] = ` + +`; + +exports[`TextInput tests (useRefsForTextInputState = true) useTextChildren = false should render as expected: should deep render when mocked (please verify output manually) 1`] = ` + +`; + +exports[`TextInput tests (useRefsForTextInputState = true) useTextChildren = false should render as expected: should deep render when not mocked (please verify output manually) 1`] = ` + +`; + +exports[`TextInput tests (useRefsForTextInputState = true) useTextChildren = true should render as expected: should deep render when mocked (please verify output manually) 1`] = ` + +`; + +exports[`TextInput tests (useRefsForTextInputState = true) useTextChildren = true should render as expected: should deep render when not mocked (please verify output manually) 1`] = ` `;