Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
268 changes: 201 additions & 67 deletions packages/react-native/Libraries/Components/TextInput/TextInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type {
import type {ViewProps} from '../View/ViewPropTypes';
import type {TextInputType} from './TextInput.flow';

import * as ReactNativeFeatureFlags from '../../../src/private/featureflags/ReactNativeFeatureFlags';
import usePressability from '../../Pressability/usePressability';
import flattenStyle from '../../StyleSheet/flattenStyle';
import StyleSheet, {
Expand Down Expand Up @@ -957,8 +958,188 @@ export type Props = $ReadOnly<{|
value?: ?Stringish,
|}>;

type ViewCommands = $NonMaybeType<
| typeof AndroidTextInputCommands
| typeof RCTMultilineTextInputNativeCommands
| typeof RCTSinglelineTextInputNativeCommands,
>;

type LastNativeSelection = {|
selection: Selection,
mostRecentEventCount: number,
|};

const emptyFunctionThatReturnsTrue = () => true;

/**
* This hook handles the synchronization between the state of the text input
* in native and in JavaScript. This is necessary due to the asynchronous nature
* of text input events.
*/
function useTextInputStateSynchronization_STATE({
props,
mostRecentEventCount,
selection,
inputRef,
text,
viewCommands,
}: {
props: Props,
mostRecentEventCount: number,
selection: ?Selection,
inputRef: React.RefObject<null | React.ElementRef<HostComponent<mixed>>>,
text: string,
viewCommands: ViewCommands,
}): {
setLastNativeText: string => void,
setLastNativeSelection: LastNativeSelection => void,
} {
const [lastNativeText, setLastNativeText] = useState<?Stringish>(props.value);
const [lastNativeSelectionState, setLastNativeSelection] =
useState<LastNativeSelection>({
selection: {start: -1, end: -1},
mostRecentEventCount: mostRecentEventCount,
});

const lastNativeSelection = lastNativeSelectionState.selection;

// This is necessary in case native updates the text and JS decides
// that the update should be ignored and we should stick with the value
// that we have in JS.
useLayoutEffect(() => {
const nativeUpdate: {text?: string, selection?: Selection} = {};

if (lastNativeText !== props.value && typeof props.value === 'string') {
nativeUpdate.text = props.value;
setLastNativeText(props.value);
}

if (
selection &&
lastNativeSelection &&
(lastNativeSelection.start !== selection.start ||
lastNativeSelection.end !== selection.end)
) {
nativeUpdate.selection = selection;
setLastNativeSelection({selection, mostRecentEventCount});
}

if (Object.keys(nativeUpdate).length === 0) {
return;
}

if (inputRef.current != null) {
viewCommands.setTextAndSelection(
inputRef.current,
mostRecentEventCount,
text,
selection?.start ?? -1,
selection?.end ?? -1,
);
}
}, [
mostRecentEventCount,
inputRef,
props.value,
props.defaultValue,
lastNativeText,
selection,
lastNativeSelection,
text,
viewCommands,
]);

return {setLastNativeText, setLastNativeSelection};
}

/**
* This hook handles the synchronization between the state of the text input
* in native and in JavaScript. This is necessary due to the asynchronous nature
* of text input events.
*/
function useTextInputStateSynchronization_REFS({
props,
mostRecentEventCount,
selection,
inputRef,
text,
viewCommands,
}: {
props: Props,
mostRecentEventCount: number,
selection: ?Selection,
inputRef: React.RefObject<null | React.ElementRef<HostComponent<mixed>>>,
text: string,
viewCommands: ViewCommands,
}): {
setLastNativeText: string => void,
setLastNativeSelection: LastNativeSelection => void,
} {
const lastNativeTextRef = useRef<?Stringish>(props.value);
const lastNativeSelectionRef = useRef<LastNativeSelection>({
selection: {start: -1, end: -1},
mostRecentEventCount: mostRecentEventCount,
});

// This is necessary in case native updates the text and JS decides
// that the update should be ignored and we should stick with the value
// that we have in JS.
useLayoutEffect(() => {
const nativeUpdate: {text?: string, selection?: Selection} = {};

const lastNativeSelection = lastNativeSelectionRef.current.selection;

if (
lastNativeTextRef.current !== props.value &&
typeof props.value === 'string'
) {
nativeUpdate.text = props.value;
lastNativeTextRef.current = props.value;
}

if (
selection &&
lastNativeSelection &&
(lastNativeSelection.start !== selection.start ||
lastNativeSelection.end !== selection.end)
) {
nativeUpdate.selection = selection;
lastNativeSelectionRef.current = {selection, mostRecentEventCount};
}

if (Object.keys(nativeUpdate).length === 0) {
return;
}

if (inputRef.current != null) {
viewCommands.setTextAndSelection(
inputRef.current,
mostRecentEventCount,
text,
selection?.start ?? -1,
selection?.end ?? -1,
);
}
}, [
mostRecentEventCount,
inputRef,
props.value,
props.defaultValue,
selection,
text,
viewCommands,
]);

return {
setLastNativeText: lastNativeText => {
lastNativeTextRef.current = lastNativeText;
},
setLastNativeSelection: lastNativeSelection => {
lastNativeSelectionRef.current = lastNativeSelection;
},
};
}

/**
* A foundational component for inputting text into the app via a
* keyboard. Props provide configurability for several features, such as
Expand Down Expand Up @@ -1098,80 +1279,33 @@ function InternalTextInput(props: Props): React.Node {
end: propsSelection.end ?? propsSelection.start,
};

const [mostRecentEventCount, setMostRecentEventCount] = useState<number>(0);
const [lastNativeText, setLastNativeText] = useState<?Stringish>(props.value);
const [lastNativeSelectionState, setLastNativeSelection] = useState<{|
selection: Selection,
mostRecentEventCount: number,
|}>({
selection: {start: -1, end: -1},
mostRecentEventCount: mostRecentEventCount,
});

const lastNativeSelection = lastNativeSelectionState.selection;

let viewCommands;
if (AndroidTextInputCommands) {
viewCommands = AndroidTextInputCommands;
} else {
viewCommands =
props.multiline === true
? RCTMultilineTextInputNativeCommands
: RCTSinglelineTextInputNativeCommands;
}

const text =
typeof props.value === 'string'
? props.value
: typeof props.defaultValue === 'string'
? props.defaultValue
: '';

// This is necessary in case native updates the text and JS decides
// that the update should be ignored and we should stick with the value
// that we have in JS.
useLayoutEffect(() => {
const nativeUpdate: {text?: string, selection?: Selection} = {};

if (lastNativeText !== props.value && typeof props.value === 'string') {
nativeUpdate.text = props.value;
setLastNativeText(props.value);
}

if (
selection &&
lastNativeSelection &&
(lastNativeSelection.start !== selection.start ||
lastNativeSelection.end !== selection.end)
) {
nativeUpdate.selection = selection;
setLastNativeSelection({selection, mostRecentEventCount});
}

if (Object.keys(nativeUpdate).length === 0) {
return;
}
const viewCommands =
AndroidTextInputCommands ||
(props.multiline === true
? RCTMultilineTextInputNativeCommands
: RCTSinglelineTextInputNativeCommands);

if (inputRef.current != null) {
viewCommands.setTextAndSelection(
inputRef.current,
mostRecentEventCount,
text,
selection?.start ?? -1,
selection?.end ?? -1,
);
}
}, [
mostRecentEventCount,
inputRef,
props.value,
props.defaultValue,
lastNativeText,
selection,
lastNativeSelection,
text,
viewCommands,
]);
const [mostRecentEventCount, setMostRecentEventCount] = useState<number>(0);
const useTextInputStateSynchronization =
ReactNativeFeatureFlags.useRefsForTextInputState()
? useTextInputStateSynchronization_REFS
: useTextInputStateSynchronization_STATE;
const {setLastNativeText, setLastNativeSelection} =
useTextInputStateSynchronization({
props,
inputRef,
mostRecentEventCount,
selection,
text,
viewCommands,
});

useLayoutEffect(() => {
const inputRefValue = inputRef.current;
Expand All @@ -1187,7 +1321,7 @@ function InternalTextInput(props: Props): React.Node {
}
};
}
}, [inputRef]);
}, []);

const setLocalRef = useCallback(
(instance: TextInputInstance | null) => {
Expand Down
Loading