· 6 years ago · Apr 17, 2020, 04:08 PM
1/**
2 * Copyright (c) Facebook, Inc. and its affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @flow
8 * @format
9 */
10
11'use strict';
12
13const DeprecatedTextInputPropTypes = require('../../DeprecatedPropTypes/DeprecatedTextInputPropTypes');
14const Platform = require('../../Utilities/Platform');
15const React = require('react');
16const ReactNative = require('../../Renderer/shims/ReactNative');
17const StyleSheet = require('../../StyleSheet/StyleSheet');
18const Text = require('../../Text/Text');
19const TextAncestor = require('../../Text/TextAncestor');
20const TextInputState = require('./TextInputState');
21const TouchableWithoutFeedback = require('../Touchable/TouchableWithoutFeedback');
22
23const invariant = require('invariant');
24const nullthrows = require('nullthrows');
25const requireNativeComponent = require('../../ReactNative/requireNativeComponent');
26const setAndForwardRef = require('../../Utilities/setAndForwardRef');
27
28import type {TextStyleProp, ViewStyleProp} from '../../StyleSheet/StyleSheet';
29import type {ColorValue} from '../../StyleSheet/StyleSheetTypes';
30import type {ViewProps} from '../View/ViewPropTypes';
31import type {SyntheticEvent, ScrollEvent} from '../../Types/CoreEventTypes';
32import type {PressEvent} from '../../Types/CoreEventTypes';
33import type {HostComponent} from '../../Renderer/shims/ReactNativeTypes';
34
35const {useEffect, useRef, useState} = React;
36
37type ReactRefSetter<T> = {current: null | T, ...} | ((ref: null | T) => mixed);
38
39let AndroidTextInput;
40let RCTMultilineTextInputView;
41let RCTSinglelineTextInputView;
42
43if (Platform.OS === 'android') {
44 AndroidTextInput = require('./AndroidTextInputNativeComponent').default;
45} else if (Platform.OS === 'ios') {
46 RCTMultilineTextInputView = requireNativeComponent(
47 'RCTMultilineTextInputView',
48 );
49 RCTSinglelineTextInputView = requireNativeComponent(
50 'RCTSinglelineTextInputView',
51 );
52}
53
54export type ChangeEvent = SyntheticEvent<
55 $ReadOnly<{|
56 eventCount: number,
57 target: number,
58 text: string,
59 |}>,
60>;
61
62export type TextInputEvent = SyntheticEvent<
63 $ReadOnly<{|
64 eventCount: number,
65 previousText: string,
66 range: $ReadOnly<{|
67 start: number,
68 end: number,
69 |}>,
70 target: number,
71 text: string,
72 |}>,
73>;
74
75export type ContentSizeChangeEvent = SyntheticEvent<
76 $ReadOnly<{|
77 target: number,
78 contentSize: $ReadOnly<{|
79 width: number,
80 height: number,
81 |}>,
82 |}>,
83>;
84
85type TargetEvent = SyntheticEvent<
86 $ReadOnly<{|
87 target: number,
88 |}>,
89>;
90
91export type BlurEvent = TargetEvent;
92export type FocusEvent = TargetEvent;
93
94type Selection = $ReadOnly<{|
95 start: number,
96 end: number,
97|}>;
98
99export type SelectionChangeEvent = SyntheticEvent<
100 $ReadOnly<{|
101 selection: Selection,
102 target: number,
103 |}>,
104>;
105
106export type KeyPressEvent = SyntheticEvent<
107 $ReadOnly<{|
108 key: string,
109 target?: ?number,
110 eventCount?: ?number,
111 |}>,
112>;
113
114export type EditingEvent = SyntheticEvent<
115 $ReadOnly<{|
116 eventCount: number,
117 text: string,
118 target: number,
119 |}>,
120>;
121
122type DataDetectorTypesType =
123 | 'phoneNumber'
124 | 'link'
125 | 'address'
126 | 'calendarEvent'
127 | 'none'
128 | 'all';
129
130export type KeyboardType =
131 // Cross Platform
132 | 'default'
133 | 'email-address'
134 | 'numeric'
135 | 'phone-pad'
136 | 'number-pad'
137 | 'decimal-pad'
138 // iOS-only
139 | 'ascii-capable'
140 | 'numbers-and-punctuation'
141 | 'url'
142 | 'name-phone-pad'
143 | 'twitter'
144 | 'web-search'
145 // iOS 10+ only
146 | 'ascii-capable-number-pad'
147 // Android-only
148 | 'visible-password';
149
150export type ReturnKeyType =
151 // Cross Platform
152 | 'done'
153 | 'go'
154 | 'next'
155 | 'search'
156 | 'send'
157 // Android-only
158 | 'none'
159 | 'previous'
160 // iOS-only
161 | 'default'
162 | 'emergency-call'
163 | 'google'
164 | 'join'
165 | 'route'
166 | 'yahoo';
167
168export type AutoCapitalize = 'none' | 'sentences' | 'words' | 'characters';
169
170export type TextContentType =
171 | 'none'
172 | 'URL'
173 | 'addressCity'
174 | 'addressCityAndState'
175 | 'addressState'
176 | 'countryName'
177 | 'creditCardNumber'
178 | 'emailAddress'
179 | 'familyName'
180 | 'fullStreetAddress'
181 | 'givenName'
182 | 'jobTitle'
183 | 'location'
184 | 'middleName'
185 | 'name'
186 | 'namePrefix'
187 | 'nameSuffix'
188 | 'nickname'
189 | 'organizationName'
190 | 'postalCode'
191 | 'streetAddressLine1'
192 | 'streetAddressLine2'
193 | 'sublocality'
194 | 'telephoneNumber'
195 | 'username'
196 | 'password'
197 | 'newPassword'
198 | 'oneTimeCode';
199
200type PasswordRules = string;
201
202type IOSProps = $ReadOnly<{|
203 /**
204 * If `false`, disables spell-check style (i.e. red underlines).
205 * The default value is inherited from `autoCorrect`.
206 * @platform ios
207 */
208 spellCheck?: ?boolean,
209
210 /**
211 * Determines the color of the keyboard.
212 * @platform ios
213 */
214 keyboardAppearance?: ?('default' | 'light' | 'dark'),
215
216 /**
217 * If `true`, the keyboard disables the return key when there is no text and
218 * automatically enables it when there is text. The default value is `false`.
219 * @platform ios
220 */
221 enablesReturnKeyAutomatically?: ?boolean,
222
223 /**
224 * When the clear button should appear on the right side of the text view.
225 * This property is supported only for single-line TextInput component.
226 * @platform ios
227 */
228 clearButtonMode?: ?('never' | 'while-editing' | 'unless-editing' | 'always'),
229
230 /**
231 * If `true`, clears the text field automatically when editing begins.
232 * @platform ios
233 */
234 clearTextOnFocus?: ?boolean,
235
236 /**
237 * Determines the types of data converted to clickable URLs in the text input.
238 * Only valid if `multiline={true}` and `editable={false}`.
239 * By default no data types are detected.
240 *
241 * You can provide one type or an array of many types.
242 *
243 * Possible values for `dataDetectorTypes` are:
244 *
245 * - `'phoneNumber'`
246 * - `'link'`
247 * - `'address'`
248 * - `'calendarEvent'`
249 * - `'none'`
250 * - `'all'`
251 *
252 * @platform ios
253 */
254 dataDetectorTypes?:
255 | ?DataDetectorTypesType
256 | $ReadOnlyArray<DataDetectorTypesType>,
257
258 /**
259 * An optional identifier which links a custom InputAccessoryView to
260 * this text input. The InputAccessoryView is rendered above the
261 * keyboard when this text input is focused.
262 * @platform ios
263 */
264 inputAccessoryViewID?: ?string,
265
266 /**
267 * Give the keyboard and the system information about the
268 * expected semantic meaning for the content that users enter.
269 * @platform ios
270 */
271 textContentType?: ?TextContentType,
272
273 PasswordRules?: ?PasswordRules,
274
275 /*
276 * @platform ios
277 */
278 rejectResponderTermination?: ?boolean,
279
280 /**
281 * If `false`, scrolling of the text view will be disabled.
282 * The default value is `true`. Does only work with 'multiline={true}'.
283 * @platform ios
284 */
285 scrollEnabled?: ?boolean,
286|}>;
287
288type AndroidProps = $ReadOnly<{|
289 /**
290 * Determines which content to suggest on auto complete, e.g.`username`.
291 * To disable auto complete, use `off`.
292 *
293 * *Android Only*
294 *
295 * The following values work on Android only:
296 *
297 * - `username`
298 * - `password`
299 * - `email`
300 * - `name`
301 * - `tel`
302 * - `street-address`
303 * - `postal-code`
304 * - `cc-number`
305 * - `cc-csc`
306 * - `cc-exp`
307 * - `cc-exp-month`
308 * - `cc-exp-year`
309 * - `off`
310 *
311 * @platform android
312 */
313 autoCompleteType?: ?(
314 | 'cc-csc'
315 | 'cc-exp'
316 | 'cc-exp-month'
317 | 'cc-exp-year'
318 | 'cc-number'
319 | 'email'
320 | 'name'
321 | 'password'
322 | 'postal-code'
323 | 'street-address'
324 | 'tel'
325 | 'username'
326 | 'off'
327 ),
328
329 /**
330 * Sets the return key to the label. Use it instead of `returnKeyType`.
331 * @platform android
332 */
333 returnKeyLabel?: ?string,
334
335 /**
336 * Sets the number of lines for a `TextInput`. Use it with multiline set to
337 * `true` to be able to fill the lines.
338 * @platform android
339 */
340 numberOfLines?: ?number,
341
342 /**
343 * When `false`, if there is a small amount of space available around a text input
344 * (e.g. landscape orientation on a phone), the OS may choose to have the user edit
345 * the text inside of a full screen text input mode. When `true`, this feature is
346 * disabled and users will always edit the text directly inside of the text input.
347 * Defaults to `false`.
348 * @platform android
349 */
350 disableFullscreenUI?: ?boolean,
351
352 /**
353 * Set text break strategy on Android API Level 23+, possible values are `simple`, `highQuality`, `balanced`
354 * The default value is `simple`.
355 * @platform android
356 */
357 textBreakStrategy?: ?('simple' | 'highQuality' | 'balanced'),
358
359 /**
360 * The color of the `TextInput` underline.
361 * @platform android
362 */
363 underlineColorAndroid?: ?ColorValue,
364
365 /**
366 * If defined, the provided image resource will be rendered on the left.
367 * The image resource must be inside `/android/app/src/main/res/drawable` and referenced
368 * like
369 * ```
370 * <TextInput
371 * inlineImageLeft='search_icon'
372 * />
373 * ```
374 * @platform android
375 */
376 inlineImageLeft?: ?string,
377
378 /**
379 * Padding between the inline image, if any, and the text input itself.
380 * @platform android
381 */
382 inlineImagePadding?: ?number,
383
384 importantForAutofill?: ?(
385 | 'auto'
386 | 'no'
387 | 'noExcludeDescendants'
388 | 'yes'
389 | 'yesExcludeDescendants'
390 ),
391
392 /**
393 * When `false`, it will prevent the soft keyboard from showing when the field is focused.
394 * Defaults to `true`.
395 * @platform android
396 */
397 showSoftInputOnFocus?: ?boolean,
398|}>;
399
400export type Props = $ReadOnly<{|
401 ...$Diff<ViewProps, $ReadOnly<{|style: ?ViewStyleProp|}>>,
402 ...IOSProps,
403 ...AndroidProps,
404
405 /**
406 * Can tell `TextInput` to automatically capitalize certain characters.
407 *
408 * - `characters`: all characters.
409 * - `words`: first letter of each word.
410 * - `sentences`: first letter of each sentence (*default*).
411 * - `none`: don't auto capitalize anything.
412 */
413 autoCapitalize?: ?AutoCapitalize,
414
415 /**
416 * If `false`, disables auto-correct. The default value is `true`.
417 */
418 autoCorrect?: ?boolean,
419
420 /**
421 * If `true`, focuses the input on `componentDidMount`.
422 * The default value is `false`.
423 */
424 autoFocus?: ?boolean,
425
426 /**
427 * Specifies whether fonts should scale to respect Text Size accessibility settings. The
428 * default is `true`.
429 */
430 allowFontScaling?: ?boolean,
431
432 /**
433 * Specifies largest possible scale a font can reach when `allowFontScaling` is enabled.
434 * Possible values:
435 * `null/undefined` (default): inherit from the parent node or the global default (0)
436 * `0`: no max, ignore parent/global default
437 * `>= 1`: sets the maxFontSizeMultiplier of this node to this value
438 */
439 maxFontSizeMultiplier?: ?number,
440
441 /**
442 * If `false`, text is not editable. The default value is `true`.
443 */
444 editable?: ?boolean,
445
446 /**
447 * Determines which keyboard to open, e.g.`numeric`.
448 *
449 * The following values work across platforms:
450 *
451 * - `default`
452 * - `numeric`
453 * - `number-pad`
454 * - `decimal-pad`
455 * - `email-address`
456 * - `phone-pad`
457 *
458 * *iOS Only*
459 *
460 * The following values work on iOS only:
461 *
462 * - `ascii-capable`
463 * - `numbers-and-punctuation`
464 * - `url`
465 * - `name-phone-pad`
466 * - `twitter`
467 * - `web-search`
468 *
469 * *Android Only*
470 *
471 * The following values work on Android only:
472 *
473 * - `visible-password`
474 */
475 keyboardType?: ?KeyboardType,
476
477 /**
478 * Determines how the return key should look. On Android you can also use
479 * `returnKeyLabel`.
480 *
481 * *Cross platform*
482 *
483 * The following values work across platforms:
484 *
485 * - `done`
486 * - `go`
487 * - `next`
488 * - `search`
489 * - `send`
490 *
491 * *Android Only*
492 *
493 * The following values work on Android only:
494 *
495 * - `none`
496 * - `previous`
497 *
498 * *iOS Only*
499 *
500 * The following values work on iOS only:
501 *
502 * - `default`
503 * - `emergency-call`
504 * - `google`
505 * - `join`
506 * - `route`
507 * - `yahoo`
508 */
509 returnKeyType?: ?ReturnKeyType,
510
511 /**
512 * Limits the maximum number of characters that can be entered. Use this
513 * instead of implementing the logic in JS to avoid flicker.
514 */
515 maxLength?: ?number,
516
517 /**
518 * If `true`, the text input can be multiple lines.
519 * The default value is `false`.
520 */
521 multiline?: ?boolean,
522
523 /**
524 * Callback that is called when the text input is blurred.
525 */
526 onBlur?: ?(e: BlurEvent) => mixed,
527
528 /**
529 * Callback that is called when the text input is focused.
530 */
531 onFocus?: ?(e: FocusEvent) => mixed,
532
533 /**
534 * Callback that is called when the text input's text changes.
535 */
536 onChange?: ?(e: ChangeEvent) => mixed,
537
538 /**
539 * Callback that is called when the text input's text changes.
540 * Changed text is passed as an argument to the callback handler.
541 */
542 onChangeText?: ?(text: string) => mixed,
543
544 /**
545 * Callback that is called when the text input's content size changes.
546 * This will be called with
547 * `{ nativeEvent: { contentSize: { width, height } } }`.
548 *
549 * Only called for multiline text inputs.
550 */
551 onContentSizeChange?: ?(e: ContentSizeChangeEvent) => mixed,
552
553 /**
554 * Callback that is called when text input ends.
555 */
556 onEndEditing?: ?(e: EditingEvent) => mixed,
557
558 /**
559 * Callback that is called when the text input selection is changed.
560 * This will be called with
561 * `{ nativeEvent: { selection: { start, end } } }`.
562 */
563 onSelectionChange?: ?(e: SelectionChangeEvent) => mixed,
564
565 /**
566 * Callback that is called when the text input's submit button is pressed.
567 * Invalid if `multiline={true}` is specified.
568 */
569 onSubmitEditing?: ?(e: EditingEvent) => mixed,
570
571 /**
572 * Callback that is called when a key is pressed.
573 * This will be called with `{ nativeEvent: { key: keyValue } }`
574 * where `keyValue` is `'Enter'` or `'Backspace'` for respective keys and
575 * the typed-in character otherwise including `' '` for space.
576 * Fires before `onChange` callbacks.
577 */
578 onKeyPress?: ?(e: KeyPressEvent) => mixed,
579
580 /**
581 * Invoked on content scroll with `{ nativeEvent: { contentOffset: { x, y } } }`.
582 * May also contain other properties from ScrollEvent but on Android contentSize
583 * is not provided for performance reasons.
584 */
585 onScroll?: ?(e: ScrollEvent) => mixed,
586
587 /**
588 * The string that will be rendered before text input has been entered.
589 */
590 placeholder?: ?Stringish,
591
592 /**
593 * The text color of the placeholder string.
594 */
595 placeholderTextColor?: ?ColorValue,
596
597 /**
598 * If `true`, the text input obscures the text entered so that sensitive text
599 * like passwords stay secure. The default value is `false`. Does not work with 'multiline={true}'.
600 */
601 secureTextEntry?: ?boolean,
602
603 /**
604 * The highlight and cursor color of the text input.
605 */
606 selectionColor?: ?ColorValue,
607
608 /**
609 * The start and end of the text input's selection. Set start and end to
610 * the same value to position the cursor.
611 */
612 selection?: ?$ReadOnly<{|
613 start: number,
614 end?: ?number,
615 |}>,
616
617 /**
618 * The value to show for the text input. `TextInput` is a controlled
619 * component, which means the native value will be forced to match this
620 * value prop if provided. For most uses, this works great, but in some
621 * cases this may cause flickering - one common cause is preventing edits
622 * by keeping value the same. In addition to simply setting the same value,
623 * either set `editable={false}`, or set/update `maxLength` to prevent
624 * unwanted edits without flicker.
625 */
626 value?: ?Stringish,
627
628 /**
629 * Provides an initial value that will change when the user starts typing.
630 * Useful for simple use-cases where you do not want to deal with listening
631 * to events and updating the value prop to keep the controlled state in sync.
632 */
633 defaultValue?: ?Stringish,
634
635 /**
636 * If `true`, all text will automatically be selected on focus.
637 */
638 selectTextOnFocus?: ?boolean,
639
640 /**
641 * If `true`, the text field will blur when submitted.
642 * The default value is true for single-line fields and false for
643 * multiline fields. Note that for multiline fields, setting `blurOnSubmit`
644 * to `true` means that pressing return will blur the field and trigger the
645 * `onSubmitEditing` event instead of inserting a newline into the field.
646 */
647 blurOnSubmit?: ?boolean,
648
649 /**
650 * Note that not all Text styles are supported, an incomplete list of what is not supported includes:
651 *
652 * - `borderLeftWidth`
653 * - `borderTopWidth`
654 * - `borderRightWidth`
655 * - `borderBottomWidth`
656 * - `borderTopLeftRadius`
657 * - `borderTopRightRadius`
658 * - `borderBottomRightRadius`
659 * - `borderBottomLeftRadius`
660 *
661 * see [Issue#7070](https://github.com/facebook/react-native/issues/7070)
662 * for more detail.
663 *
664 * [Styles](docs/style.html)
665 */
666 style?: ?TextStyleProp,
667
668 /**
669 * If `true`, caret is hidden. The default value is `false`.
670 * This property is supported only for single-line TextInput component on iOS.
671 */
672 caretHidden?: ?boolean,
673
674 /*
675 * If `true`, contextMenuHidden is hidden. The default value is `false`.
676 */
677 contextMenuHidden?: ?boolean,
678
679 forwardedRef?: ?ReactRefSetter<
680 React.ElementRef<HostComponent<mixed>> & ImperativeMethods,
681 >,
682|}>;
683
684type ImperativeMethods = $ReadOnly<{|
685 clear: () => void,
686 isFocused: () => boolean,
687 getNativeRef: () => ?React.ElementRef<HostComponent<mixed>>,
688|}>;
689
690const emptyFunctionThatReturnsTrue = () => true;
691
692function useFocusOnMount(
693 initialAutoFocus: ?boolean,
694 inputRef: {|
695 current: null | React.ElementRef<HostComponent<mixed>>,
696 |},
697) {
698 const initialAutoFocusValue = useRef<?boolean>(initialAutoFocus);
699
700 useEffect(() => {
701 // We only want to autofocus on initial mount.
702 // Since initialAutoFocusValue and inputRef will never change
703 // this should match the expected behavior
704 if (initialAutoFocusValue.current) {
705 const focus = () => {
706 if (inputRef.current != null) {
707 inputRef.current.focus();
708 }
709 };
710
711 let rafId;
712 if (Platform.OS === 'android') {
713 // On Android this needs to be executed in a rAF callback
714 // otherwise the keyboard opens then closes immediately.
715 rafId = requestAnimationFrame(focus);
716 } else {
717 focus();
718 }
719
720 return () => {
721 if (rafId != null) {
722 cancelAnimationFrame(rafId);
723 }
724 };
725 }
726 }, [initialAutoFocusValue, inputRef]);
727}
728/**
729 * A foundational component for inputting text into the app via a
730 * keyboard. Props provide configurability for several features, such as
731 * auto-correction, auto-capitalization, placeholder text, and different keyboard
732 * types, such as a numeric keypad.
733 *
734 * The simplest use case is to plop down a `TextInput` and subscribe to the
735 * `onChangeText` events to read the user input. There are also other events,
736 * such as `onSubmitEditing` and `onFocus` that can be subscribed to. A simple
737 * example:
738 *
739 * ```ReactNativeWebPlayer
740 * import React, { Component } from 'react';
741 * import { AppRegistry, TextInput } from 'react-native';
742 *
743 * export default class UselessTextInput extends Component {
744 * constructor(props) {
745 * super(props);
746 * this.state = { text: 'Useless Placeholder' };
747 * }
748 *
749 * render() {
750 * return (
751 * <TextInput
752 * style={{height: 40, borderColor: 'gray', borderWidth: 1}}
753 * onChangeText={(text) => this.setState({text})}
754 * value={this.state.text}
755 * />
756 * );
757 * }
758 * }
759 *
760 * // skip this line if using Create React Native App
761 * AppRegistry.registerComponent('AwesomeProject', () => UselessTextInput);
762 * ```
763 *
764 * Two methods exposed via the native element are .focus() and .blur() that
765 * will focus or blur the TextInput programmatically.
766 *
767 * Note that some props are only available with `multiline={true/false}`.
768 * Additionally, border styles that apply to only one side of the element
769 * (e.g., `borderBottomColor`, `borderLeftWidth`, etc.) will not be applied if
770 * `multiline=false`. To achieve the same effect, you can wrap your `TextInput`
771 * in a `View`:
772 *
773 * ```ReactNativeWebPlayer
774 * import React, { Component } from 'react';
775 * import { AppRegistry, View, TextInput } from 'react-native';
776 *
777 * class UselessTextInput extends Component {
778 * render() {
779 * return (
780 * <TextInput
781 * {...this.props} // Inherit any props passed to it; e.g., multiline, numberOfLines below
782 * editable = {true}
783 * maxLength = {40}
784 * />
785 * );
786 * }
787 * }
788 *
789 * export default class UselessTextInputMultiline extends Component {
790 * constructor(props) {
791 * super(props);
792 * this.state = {
793 * text: 'Useless Multiline Placeholder',
794 * };
795 * }
796 *
797 * // If you type something in the text box that is a color, the background will change to that
798 * // color.
799 * render() {
800 * return (
801 * <View style={{
802 * backgroundColor: this.state.text,
803 * borderBottomColor: '#000000',
804 * borderBottomWidth: 1 }}
805 * >
806 * <UselessTextInput
807 * multiline = {true}
808 * numberOfLines = {4}
809 * onChangeText={(text) => this.setState({text})}
810 * value={this.state.text}
811 * />
812 * </View>
813 * );
814 * }
815 * }
816 *
817 * // skip these lines if using Create React Native App
818 * AppRegistry.registerComponent(
819 * 'AwesomeProject',
820 * () => UselessTextInputMultiline
821 * );
822 * ```
823 *
824 * `TextInput` has by default a border at the bottom of its view. This border
825 * has its padding set by the background image provided by the system, and it
826 * cannot be changed. Solutions to avoid this is to either not set height
827 * explicitly, case in which the system will take care of displaying the border
828 * in the correct position, or to not display the border by setting
829 * `underlineColorAndroid` to transparent.
830 *
831 * Note that on Android performing text selection in input can change
832 * app's activity `windowSoftInputMode` param to `adjustResize`.
833 * This may cause issues with components that have position: 'absolute'
834 * while keyboard is active. To avoid this behavior either specify `windowSoftInputMode`
835 * in AndroidManifest.xml ( https://developer.android.com/guide/topics/manifest/activity-element.html )
836 * or control this param programmatically with native code.
837 *
838 */
839function InternalTextInput(props: Props): React.Node {
840 const inputRef = useRef<null | React.ElementRef<HostComponent<mixed>>>(null);
841
842 const selection: ?Selection =
843 props.selection == null
844 ? null
845 : {
846 start: props.selection.start,
847 end: props.selection.end ?? props.selection.start,
848 };
849
850 const [lastNativeText, setLastNativeText] = useState<?Stringish>(props.value);
851 const [lastNativeSelection, setLastNativeSelection] = useState<?Selection>(
852 selection,
853 );
854
855 // This is necessary in case native updates the text and JS decides
856 // that the update should be ignored and we should stick with the value
857 // that we have in JS.
858 useEffect(() => {
859 const nativeUpdate = {};
860
861 if (lastNativeText !== props.value && typeof props.value === 'string') {
862 nativeUpdate.text = props.value;
863 setLastNativeText(props.value);
864 }
865
866 if (
867 selection &&
868 lastNativeSelection &&
869 (lastNativeSelection.start !== selection.start ||
870 lastNativeSelection.end !== selection.end)
871 ) {
872 nativeUpdate.selection = selection;
873 setLastNativeSelection(selection);
874 }
875
876 if (Object.keys(nativeUpdate).length > 0 && inputRef.current) {
877 inputRef.current.setNativeProps(nativeUpdate);
878 }
879 }, [inputRef, props.value, lastNativeText, selection, lastNativeSelection]);
880
881 useFocusOnMount(props.autoFocus, inputRef);
882
883 useEffect(() => {
884 const tag = ReactNative.findNodeHandle(inputRef.current);
885 if (tag != null) {
886 TextInputState.registerInput(tag);
887
888 return () => {
889 TextInputState.unregisterInput(tag);
890 };
891 }
892 }, [inputRef]);
893
894 useEffect(() => {
895 // When unmounting we need to blur the input
896 return () => {
897 if (isFocused()) {
898 nullthrows(inputRef.current).blur();
899 }
900 };
901 }, [inputRef]);
902
903 function clear(): void {
904 if (inputRef.current != null) {
905 inputRef.current.setNativeProps({text: ''});
906 }
907 }
908
909 // TODO: Fix this returning true on null === null, when no input is focused
910 function isFocused(): boolean {
911 return (
912 TextInputState.currentlyFocusedField() ===
913 ReactNative.findNodeHandle(inputRef.current)
914 );
915 }
916
917 function getNativeRef(): ?React.ElementRef<HostComponent<mixed>> {
918 return inputRef.current;
919 }
920
921 function _getText(): ?string {
922 return typeof props.value === 'string'
923 ? props.value
924 : typeof props.defaultValue === 'string'
925 ? props.defaultValue
926 : '';
927 }
928
929 const _setNativeRef = setAndForwardRef({
930 getForwardedRef: () => props.forwardedRef,
931 setLocalRef: ref => {
932 inputRef.current = ref;
933
934 /*
935 Hi reader from the future. I'm sorry for this.
936
937 This is a hack. Ideally we would forwardRef to the underlying
938 host component. However, since TextInput has it's own methods that can be
939 called as well, if we used the standard forwardRef then these
940 methods wouldn't be accessible and thus be a breaking change.
941
942 We have a couple of options of how to handle this:
943 - Return a new ref with everything we methods from both. This is problematic
944 because we need React to also know it is a host component which requires
945 internals of the class implementation of the ref.
946 - Break the API and have some other way to call one set of the methods or
947 the other. This is our long term approach as we want to eventually
948 get the methods on host components off the ref. So instead of calling
949 ref.measure() you might call ReactNative.measure(ref). This would hopefully
950 let the ref for TextInput then have the methods like `.clear`. Or we do it
951 the other way and make it TextInput.clear(textInputRef) which would be fine
952 too. Either way though is a breaking change that is longer term.
953 - Mutate this ref. :( Gross, but accomplishes what we need in the meantime
954 before we can get to the long term breaking change.
955 */
956 if (ref) {
957 ref.clear = clear;
958 ref.isFocused = isFocused;
959 ref.getNativeRef = getNativeRef;
960 }
961 },
962 });
963
964 const _onPress = (event: PressEvent) => {
965 if (props.editable || props.editable === undefined) {
966 nullthrows(inputRef.current).focus();
967 }
968 };
969
970 const _onChange = (event: ChangeEvent) => {
971 // Make sure to fire the mostRecentEventCount first so it is already set on
972 // native when the text value is set.
973 if (inputRef.current) {
974 inputRef.current.setNativeProps({
975 mostRecentEventCount: event.nativeEvent.eventCount,
976 });
977 }
978
979 const text = event.nativeEvent.text;
980 props.onChange && props.onChange(event);
981 props.onChangeText && props.onChangeText(text);
982
983 if (!inputRef.current) {
984 // calling `props.onChange` or `props.onChangeText`
985 // may clean up the input itself. Exits here.
986 return;
987 }
988
989 setLastNativeText(text);
990 };
991
992 const _onSelectionChange = (event: SelectionChangeEvent) => {
993 props.onSelectionChange && props.onSelectionChange(event);
994
995 if (!inputRef.current) {
996 // calling `props.onSelectionChange`
997 // may clean up the input itself. Exits here.
998 return;
999 }
1000
1001 setLastNativeSelection(event.nativeEvent.selection);
1002 };
1003
1004 const _onFocus = (event: FocusEvent) => {
1005 TextInputState.focusField(ReactNative.findNodeHandle(inputRef.current));
1006 if (props.onFocus) {
1007 props.onFocus(event);
1008 }
1009 };
1010
1011 const _onBlur = (event: BlurEvent) => {
1012 TextInputState.blurField(ReactNative.findNodeHandle(inputRef.current));
1013 if (props.onBlur) {
1014 props.onBlur(event);
1015 }
1016 };
1017
1018 const _onScroll = (event: ScrollEvent) => {
1019 props.onScroll && props.onScroll(event);
1020 };
1021
1022 let textInput = null;
1023 let additionalTouchableProps: {|
1024 rejectResponderTermination?: $PropertyType<
1025 Props,
1026 'rejectResponderTermination',
1027 >,
1028 // This is a hack to let Flow know we want an exact object
1029 |} = {...null};
1030
1031 if (Platform.OS === 'ios') {
1032 const RCTTextInputView = props.multiline
1033 ? RCTMultilineTextInputView
1034 : RCTSinglelineTextInputView;
1035
1036 const style = props.multiline
1037 ? [styles.multilineInput, props.style]
1038 : props.style;
1039
1040 additionalTouchableProps.rejectResponderTermination =
1041 props.rejectResponderTermination;
1042
1043 textInput = (
1044 <RCTTextInputView
1045 ref={_setNativeRef}
1046 {...props}
1047 dataDetectorTypes={props.dataDetectorTypes}
1048 onBlur={_onBlur}
1049 onChange={_onChange}
1050 onContentSizeChange={props.onContentSizeChange}
1051 onFocus={_onFocus}
1052 onScroll={_onScroll}
1053 onSelectionChange={_onSelectionChange}
1054 onSelectionChangeShouldSetResponder={emptyFunctionThatReturnsTrue}
1055 selection={selection}
1056 style={style}
1057 text={_getText()}
1058 />
1059 );
1060 } else if (Platform.OS === 'android') {
1061 const style = [props.style];
1062 const autoCapitalize = props.autoCapitalize || 'sentences';
1063 let children = props.children;
1064 let childCount = 0;
1065 React.Children.forEach(children, () => ++childCount);
1066 invariant(
1067 !(props.value && childCount),
1068 'Cannot specify both value and children.',
1069 );
1070 if (childCount > 1) {
1071 children = <Text>{children}</Text>;
1072 }
1073
1074 textInput = (
1075 /* $FlowFixMe the types for AndroidTextInput don't match up exactly with
1076 the props for TextInput. This will need to get fixed */
1077 <AndroidTextInput
1078 ref={_setNativeRef}
1079 {...props}
1080 autoCapitalize={autoCapitalize}
1081 children={children}
1082 disableFullscreenUI={props.disableFullscreenUI}
1083 mostRecentEventCount={0}
1084 onBlur={_onBlur}
1085 onChange={_onChange}
1086 onFocus={_onFocus}
1087 onScroll={_onScroll}
1088 onSelectionChange={_onSelectionChange}
1089 selection={selection}
1090 style={style}
1091 text={_getText()}
1092 textBreakStrategy={props.textBreakStrategy}
1093 />
1094 );
1095 }
1096 return (
1097 <TextAncestor.Provider value={true}>
1098 <TouchableWithoutFeedback
1099 onLayout={props.onLayout}
1100 onPress={_onPress}
1101 accessible={props.accessible}
1102 accessibilityLabel={props.accessibilityLabel}
1103 accessibilityRole={props.accessibilityRole}
1104 accessibilityState={props.accessibilityState}
1105 nativeID={props.nativeID}
1106 testID={props.testID}
1107 {...additionalTouchableProps}>
1108 {textInput}
1109 </TouchableWithoutFeedback>
1110 </TextAncestor.Provider>
1111 );
1112}
1113
1114const ExportedForwardRef: React.AbstractComponent<
1115 React.ElementConfig<typeof InternalTextInput>,
1116 React.ElementRef<HostComponent<mixed>> & ImperativeMethods,
1117> = React.forwardRef(function TextInput(
1118 props,
1119 forwardedRef: ReactRefSetter<
1120 React.ElementRef<HostComponent<mixed>> & ImperativeMethods,
1121 >,
1122) {
1123 return <InternalTextInput {...props} forwardedRef={forwardedRef} />;
1124});
1125
1126// $FlowFixMe
1127ExportedForwardRef.defaultProps = {
1128 allowFontScaling: true,
1129 rejectResponderTermination: true,
1130 underlineColorAndroid: 'transparent',
1131};
1132
1133// TODO: Deprecate this
1134// $FlowFixMe
1135ExportedForwardRef.propTypes = DeprecatedTextInputPropTypes;
1136
1137// $FlowFixMe
1138ExportedForwardRef.State = {
1139 currentlyFocusedField: TextInputState.currentlyFocusedField,
1140 focusTextInput: TextInputState.focusTextInput,
1141 blurTextInput: TextInputState.blurTextInput,
1142};
1143
1144type TextInputComponentStatics = $ReadOnly<{|
1145 State: $ReadOnly<{|
1146 currentlyFocusedField: typeof TextInputState.currentlyFocusedField,
1147 focusTextInput: typeof TextInputState.focusTextInput,
1148 blurTextInput: typeof TextInputState.blurTextInput,
1149 |}>,
1150 propTypes: typeof DeprecatedTextInputPropTypes,
1151|}>;
1152
1153const styles = StyleSheet.create({
1154 multilineInput: {
1155 // This default top inset makes RCTMultilineTextInputView seem as close as possible
1156 // to single-line RCTSinglelineTextInputView defaults, using the system defaults
1157 // of font size 17 and a height of 31 points.
1158 paddingTop: 5,
1159 },
1160});
1161
1162module.exports = ((ExportedForwardRef: any): React.AbstractComponent<
1163 React.ElementConfig<typeof InternalTextInput>,
1164 $ReadOnly<{|
1165 ...React.ElementRef<HostComponent<mixed>>,
1166 ...ImperativeMethods,
1167 |}>,
1168> &
1169 TextInputComponentStatics);