in vnext/src/Libraries/Components/TextInput/TextInput.windows.js [959:1431]
function InternalTextInput(props: Props): React.Node {
const inputRef = useRef<null | React.ElementRef<HostComponent<mixed>>>(null);
// Android sends a "onTextChanged" event followed by a "onSelectionChanged" event, for
// the same "most recent event count".
// For controlled selection, that means that immediately after text is updated,
// a controlled component will pass in the *previous* selection, even if the controlled
// component didn't mean to modify the selection at all.
// Therefore, we ignore selections and pass them through until the selection event has
// been sent.
// Note that this mitigation is NOT needed for Fabric.
// discovered when upgrading react-hooks
// eslint-disable-next-line react-hooks/exhaustive-deps
let selection: ?Selection =
props.selection == null
? null
: {
start: props.selection.start,
end: props.selection.end ?? props.selection.start,
};
const [mostRecentEventCount, setMostRecentEventCount] = useState<number>(0);
const [lastNativeText, setLastNativeText] = useState<?Stringish>(props.value);
const [lastNativeSelectionState, setLastNativeSelection] = useState<{|
selection: ?Selection,
mostRecentEventCount: number,
|}>({selection, mostRecentEventCount});
const lastNativeSelection = lastNativeSelectionState.selection;
const lastNativeSelectionEventCount =
lastNativeSelectionState.mostRecentEventCount;
if (lastNativeSelectionEventCount < mostRecentEventCount) {
selection = null;
}
let viewCommands;
if (AndroidTextInputCommands) {
viewCommands = AndroidTextInputCommands;
}
// [Windows
else if (WindowsTextInputCommands) {
viewCommands = WindowsTextInputCommands;
}
// Windows]
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 = {};
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,
]);
useLayoutEffect(() => {
const inputRefValue = inputRef.current;
if (inputRefValue != null) {
TextInputState.registerInput(inputRefValue);
return () => {
TextInputState.unregisterInput(inputRefValue);
if (TextInputState.currentlyFocusedInput() === inputRefValue) {
nullthrows(inputRefValue).blur();
}
};
}
}, [inputRef]);
function clear(): void {
if (inputRef.current != null) {
viewCommands.setTextAndSelection(
inputRef.current,
mostRecentEventCount,
'',
0,
0,
);
}
}
function setSelection(start: number, end: number): void {
if (inputRef.current != null) {
viewCommands.setTextAndSelection(
inputRef.current,
mostRecentEventCount,
null,
start,
end,
);
}
}
// TODO: Fix this returning true on null === null, when no input is focused
function isFocused(): boolean {
return TextInputState.currentlyFocusedInput() === inputRef.current;
}
function getNativeRef(): ?React.ElementRef<HostComponent<mixed>> {
return inputRef.current;
}
const _setNativeRef = setAndForwardRef({
getForwardedRef: () => props.forwardedRef,
setLocalRef: (ref) => {
inputRef.current = ref;
/*
Hi reader from the future. I'm sorry for this.
This is a hack. Ideally we would forwardRef to the underlying
host component. However, since TextInput has it's own methods that can be
called as well, if we used the standard forwardRef then these
methods wouldn't be accessible and thus be a breaking change.
We have a couple of options of how to handle this:
- Return a new ref with everything we methods from both. This is problematic
because we need React to also know it is a host component which requires
internals of the class implementation of the ref.
- Break the API and have some other way to call one set of the methods or
the other. This is our long term approach as we want to eventually
get the methods on host components off the ref. So instead of calling
ref.measure() you might call ReactNative.measure(ref). This would hopefully
let the ref for TextInput then have the methods like `.clear`. Or we do it
the other way and make it TextInput.clear(textInputRef) which would be fine
too. Either way though is a breaking change that is longer term.
- Mutate this ref. :( Gross, but accomplishes what we need in the meantime
before we can get to the long term breaking change.
*/
if (ref) {
ref.clear = clear;
ref.isFocused = isFocused;
ref.getNativeRef = getNativeRef;
ref.setSelection = setSelection;
}
},
});
const _onChange = (event: ChangeEvent) => {
const currentText = event.nativeEvent.text;
props.onChange && props.onChange(event);
props.onChangeText && props.onChangeText(currentText);
if (inputRef.current == null) {
// calling `props.onChange` or `props.onChangeText`
// may clean up the input itself. Exits here.
return;
}
setLastNativeText(currentText);
// This must happen last, after we call setLastNativeText.
// Different ordering can cause bugs when editing AndroidTextInputs
// with multiple Fragments.
// We must update this so that controlled input updates work.
setMostRecentEventCount(event.nativeEvent.eventCount);
};
const _onChangeSync = (event: ChangeEvent) => {
const currentText = event.nativeEvent.text;
props.unstable_onChangeSync && props.unstable_onChangeSync(event);
props.unstable_onChangeTextSync &&
props.unstable_onChangeTextSync(currentText);
if (inputRef.current == null) {
// calling `props.onChange` or `props.onChangeText`
// may clean up the input itself. Exits here.
return;
}
setLastNativeText(currentText);
// This must happen last, after we call setLastNativeText.
// Different ordering can cause bugs when editing AndroidTextInputs
// with multiple Fragments.
// We must update this so that controlled input updates work.
setMostRecentEventCount(event.nativeEvent.eventCount);
};
const _onSelectionChange = (event: SelectionChangeEvent) => {
props.onSelectionChange && props.onSelectionChange(event);
if (inputRef.current == null) {
// calling `props.onSelectionChange`
// may clean up the input itself. Exits here.
return;
}
setLastNativeSelection({
selection: event.nativeEvent.selection,
mostRecentEventCount,
});
};
const _onFocus = (event: FocusEvent) => {
TextInputState.focusInput(inputRef.current);
if (props.onFocus) {
props.onFocus(event);
}
};
const _onBlur = (event: BlurEvent) => {
TextInputState.blurInput(inputRef.current);
if (props.onBlur) {
props.onBlur(event);
}
};
const _onScroll = (event: ScrollEvent) => {
props.onScroll && props.onScroll(event);
};
let textInput = null;
// The default value for `blurOnSubmit` is true for single-line fields and
// false for multi-line fields.
const blurOnSubmit = props.blurOnSubmit ?? !props.multiline;
const accessible = props.accessible !== false;
const focusable = props.focusable !== false;
const config = React.useMemo(
() => ({
onPress: (event: PressEvent) => {
if (props.editable !== false) {
if (inputRef.current != null) {
inputRef.current.focus();
}
}
},
onPressIn: props.onPressIn,
onPressOut: props.onPressOut,
cancelable:
Platform.OS === 'ios' ? !props.rejectResponderTermination : null,
}),
[
props.editable,
props.onPressIn,
props.onPressOut,
props.rejectResponderTermination,
],
);
// Hide caret during test runs due to a flashing caret
// makes screenshot tests flakey
let caretHidden = props.caretHidden;
if (Platform.isTesting) {
caretHidden = true;
}
// TextInput handles onBlur and onFocus events
// so omitting onBlur and onFocus pressability handlers here.
const {onBlur, onFocus, ...eventHandlers} = usePressability(config) || {};
const eventPhase = Object.freeze({Capturing: 1, Bubbling: 3});
const _keyDown = (event: KeyEvent) => {
if (props.keyDownEvents && event.isPropagationStopped() !== true) {
for (const el of props.keyDownEvents) {
if (
event.nativeEvent.code == el.code &&
el.handledEventPhase == eventPhase.Bubbling
) {
event.stopPropagation();
}
}
}
props.onKeyDown && props.onKeyDown(event);
};
const _keyUp = (event: KeyEvent) => {
if (props.keyUpEvents && event.isPropagationStopped() !== true) {
for (const el of props.keyUpEvents) {
if (event.nativeEvent.code == el.code && el.handledEventPhase == 3) {
event.stopPropagation();
}
}
}
props.onKeyUp && props.onKeyUp(event);
};
const _keyDownCapture = (event: KeyEvent) => {
if (props.keyDownEvents && event.isPropagationStopped() !== true) {
for (const el of props.keyDownEvents) {
if (event.nativeEvent.code == el.code && el.handledEventPhase == 1) {
event.stopPropagation();
}
}
}
props.onKeyDownCapture && props.onKeyDownCapture(event);
};
const _keyUpCapture = (event: KeyEvent) => {
if (props.keyUpEvents && event.isPropagationStopped() !== true) {
for (const el of props.keyUpEvents) {
if (event.nativeEvent.code == el.code && el.handledEventPhase == 1) {
event.stopPropagation();
}
}
}
props.onKeyUpCapture && props.onKeyUpCapture(event);
};
if (Platform.OS === 'ios') {
const RCTTextInputView =
props.multiline === true
? RCTMultilineTextInputView
: RCTSinglelineTextInputView;
const style =
props.multiline === true
? [styles.multilineInput, props.style]
: props.style;
const useOnChangeSync =
(props.unstable_onChangeSync || props.unstable_onChangeTextSync) &&
!(props.onChange || props.onChangeText);
textInput = (
<RCTTextInputView
ref={_setNativeRef}
{...props}
{...eventHandlers}
accessible={accessible}
blurOnSubmit={blurOnSubmit}
caretHidden={caretHidden}
dataDetectorTypes={props.dataDetectorTypes}
focusable={focusable}
mostRecentEventCount={mostRecentEventCount}
onBlur={_onBlur}
onKeyPressSync={props.unstable_onKeyPressSync}
onChange={_onChange}
onChangeSync={useOnChangeSync === true ? _onChangeSync : null}
onContentSizeChange={props.onContentSizeChange}
onFocus={_onFocus}
onScroll={_onScroll}
onSelectionChange={_onSelectionChange}
onSelectionChangeShouldSetResponder={emptyFunctionThatReturnsTrue}
selection={selection}
style={style}
text={text}
/>
);
} else if (Platform.OS === 'android') {
const style = [props.style];
const autoCapitalize = props.autoCapitalize || 'sentences';
const placeholder = props.placeholder ?? '';
let children = props.children;
const childCount = React.Children.count(children);
invariant(
!(props.value != null && childCount),
'Cannot specify both value and children.',
);
if (childCount > 1) {
children = <Text>{children}</Text>;
}
textInput = (
/* $FlowFixMe[prop-missing] the types for AndroidTextInput don't match up
* exactly with the props for TextInput. This will need to get fixed */
/* $FlowFixMe[incompatible-type] the types for AndroidTextInput don't
* match up exactly with the props for TextInput. This will need to get
* fixed */
/* $FlowFixMe[incompatible-type-arg] the types for AndroidTextInput don't
* match up exactly with the props for TextInput. This will need to get
* fixed */
<AndroidTextInput
ref={_setNativeRef}
{...props}
{...eventHandlers}
accessible={accessible}
autoCapitalize={autoCapitalize}
blurOnSubmit={blurOnSubmit}
caretHidden={caretHidden}
children={children}
disableFullscreenUI={props.disableFullscreenUI}
focusable={focusable}
mostRecentEventCount={mostRecentEventCount}
onBlur={_onBlur}
onChange={_onChange}
onFocus={_onFocus}
/* $FlowFixMe[prop-missing] the types for AndroidTextInput don't match
* up exactly with the props for TextInput. This will need to get fixed
*/
/* $FlowFixMe[incompatible-type-arg] the types for AndroidTextInput
* don't match up exactly with the props for TextInput. This will need
* to get fixed */
onScroll={_onScroll}
onSelectionChange={_onSelectionChange}
placeholder={placeholder}
selection={selection}
style={style}
text={text}
textBreakStrategy={props.textBreakStrategy}
/>
);
} // [Windows
else if (Platform.OS === 'windows') {
textInput = (
<WindowsTextInput
ref={_setNativeRef}
{...props}
dataDetectorTypes={props.dataDetectorTypes}
mostRecentEventCount={mostRecentEventCount}
onBlur={_onBlur}
onChange={_onChange}
onContentSizeChange={props.onContentSizeChange}
onFocus={_onFocus}
onScroll={_onScroll}
onSelectionChange={_onSelectionChange}
onSelectionChangeShouldSetResponder={emptyFunctionThatReturnsTrue}
selection={selection}
text={text}
onKeyDown={_keyDown}
onKeyDownCapture={_keyDownCapture}
onKeyUp={_keyUp}
onKeyUpCapture={_keyUpCapture}
/>
);
} // Windows]
return (
<TextAncestor.Provider value={true}>{textInput}</TextAncestor.Provider>
);
}