diff --git a/Libraries/Components/TextInput/TextInput.js b/Libraries/Components/TextInput/TextInput.js index 883ec8fc0..2ed2f3b89 100644 --- a/Libraries/Components/TextInput/TextInput.js +++ b/Libraries/Components/TextInput/TextInput.js @@ -9,11 +9,11 @@ */ 'use strict'; -const DeprecatedViewPropTypes = require('DeprecatedViewPropTypes'); const DeprecatedColorPropType = require('DeprecatedColorPropType'); -const DeprecatedStyleSheetPropType = require('DeprecatedStyleSheetPropType'); +const DeprecatedViewPropTypes = require('DeprecatedViewPropTypes'); const DocumentSelectionState = require('DocumentSelectionState'); -const TextStylePropTypes = require('TextStylePropTypes'); +const EventEmitter = require('EventEmitter'); +const NativeMethodsMixin = require('NativeMethodsMixin'); const Platform = require('Platform'); const PropTypes = require('prop-types'); const React = require('React'); @@ -25,32 +25,193 @@ const TextInputState = require('TextInputState'); const TouchableWithoutFeedback = require('TouchableWithoutFeedback'); const UIManager = require('UIManager'); -const { - AndroidTextInput, - RCTMultilineTextInputView, - RCTSinglelineTextInputView, -} = require('TextInputNativeComponent'); - +const createReactClass = require('create-react-class'); const emptyFunction = require('fbjs/lib/emptyFunction'); const invariant = require('fbjs/lib/invariant'); +const requireNativeComponent = require('requireNativeComponent'); const warning = require('fbjs/lib/warning'); -const nullthrows = require('nullthrows'); -import type {Props, Selection, Event} from 'TextInputTypes'; -import type {EventEmitter} from 'EventEmitter'; -import type {TextInputType} from 'TextInputNativeComponent'; -import type {PressEvent} from 'CoreEventTypes'; -import type { - MeasureOnSuccessCallback, - MeasureLayoutOnSuccessCallback, - MeasureInWindowOnSuccessCallback, -} from 'ReactNativeTypes'; +import type {TextStyleProp, ViewStyleProp} from 'StyleSheet'; +import type {ColorValue} from 'StyleSheetTypes'; +import type {ViewProps} from 'ViewPropTypes'; + +let AndroidTextInput; +let RCTMultilineTextInputView; +let RCTSinglelineTextInputView; + +if (Platform.OS === 'android') { + AndroidTextInput = requireNativeComponent('AndroidTextInput'); +} else if (Platform.OS === 'ios') { + RCTMultilineTextInputView = requireNativeComponent( + 'RCTMultilineTextInputView', + ); + RCTSinglelineTextInputView = requireNativeComponent( + 'RCTSinglelineTextInputView', + ); +} const onlyMultiline = { onTextInput: true, children: true, }; +type Event = Object; +type Selection = { + start: number, + end?: number, +}; + +const DataDetectorTypes = [ + 'phoneNumber', + 'link', + 'address', + 'calendarEvent', + 'none', + 'all', +]; + +type DataDetectorTypesType = + | 'phoneNumber' + | 'link' + | 'address' + | 'calendarEvent' + | 'none' + | 'all'; + +export type KeyboardType = + // Cross Platform + | 'default' + | 'email-address' + | 'numeric' + | 'phone-pad' + | 'number-pad' + | 'decimal-pad' + // iOS-only + | 'ascii-capable' + | 'numbers-and-punctuation' + | 'url' + | 'name-phone-pad' + | 'twitter' + | 'web-search' + // Android-only + | 'visible-password'; + +export type ReturnKeyType = + // Cross Platform + | 'done' + | 'go' + | 'next' + | 'search' + | 'send' + // Android-only + | 'none' + | 'previous' + // iOS-only + | 'default' + | 'emergency-call' + | 'google' + | 'join' + | 'route' + | 'yahoo'; + +export type AutoCapitalize = 'none' | 'sentences' | 'words' | 'characters'; + +type IOSProps = $ReadOnly<{| + spellCheck?: ?boolean, + keyboardAppearance?: ?('default' | 'light' | 'dark'), + enablesReturnKeyAutomatically?: ?boolean, + selectionState?: ?DocumentSelectionState, + clearButtonMode?: ?('never' | 'while-editing' | 'unless-editing' | 'always'), + clearTextOnFocus?: ?boolean, + dataDetectorTypes?: + | ?DataDetectorTypesType + | $ReadOnlyArray, + inputAccessoryViewID?: ?string, + textContentType?: ?( + | 'none' + | 'URL' + | 'addressCity' + | 'addressCityAndState' + | 'addressState' + | 'countryName' + | 'creditCardNumber' + | 'emailAddress' + | 'familyName' + | 'fullStreetAddress' + | 'givenName' + | 'jobTitle' + | 'location' + | 'middleName' + | 'name' + | 'namePrefix' + | 'nameSuffix' + | 'nickname' + | 'organizationName' + | 'postalCode' + | 'streetAddressLine1' + | 'streetAddressLine2' + | 'sublocality' + | 'telephoneNumber' + | 'username' + | 'password' + | 'newPassword' + | 'oneTimeCode' + ), + scrollEnabled?: ?boolean, +|}>; + +type AndroidProps = $ReadOnly<{| + returnKeyLabel?: ?string, + numberOfLines?: ?number, + disableFullscreenUI?: ?boolean, + textBreakStrategy?: ?('simple' | 'highQuality' | 'balanced'), + underlineColorAndroid?: ?ColorValue, + inlineImageLeft?: ?string, + inlineImagePadding?: ?number, +|}>; + +type Props = $ReadOnly<{| + ...$Diff>, + ...IOSProps, + ...AndroidProps, + autoCapitalize?: ?AutoCapitalize, + autoCorrect?: ?boolean, + autoFocus?: ?boolean, + allowFontScaling?: ?boolean, + maxFontSizeMultiplier?: ?number, + editable?: ?boolean, + keyboardType?: ?KeyboardType, + returnKeyType?: ?ReturnKeyType, + maxLength?: ?number, + multiline?: ?boolean, + onBlur?: ?Function, + onFocus?: ?Function, + onChange?: ?Function, + onChangeText?: ?Function, + onContentSizeChange?: ?Function, + onTextInput?: ?Function, + onEndEditing?: ?Function, + onSelectionChange?: ?Function, + onSubmitEditing?: ?Function, + onKeyPress?: ?Function, + onScroll?: ?Function, + placeholder?: ?Stringish, + placeholderTextColor?: ?ColorValue, + secureTextEntry?: ?boolean, + selectionColor?: ?ColorValue, + selection?: ?$ReadOnly<{| + start: number, + end?: ?number, + |}>, + value?: ?Stringish, + defaultValue?: ?Stringish, + selectTextOnFocus?: ?boolean, + blurOnSubmit?: ?boolean, + style?: ?TextStyleProp, + caretHidden?: ?boolean, + contextMenuHidden?: ?boolean, +|}>; + /** * A foundational component for inputting text into the app via a * keyboard. Props provide configurability for several features, such as @@ -162,111 +323,536 @@ const onlyMultiline = { * or control this param programmatically with native code. * */ -class TextInput extends React.Component { - static defaultProps = { - allowFontScaling: true, - underlineColorAndroid: 'transparent', - }; - _inputRef: ?React.ElementRef> = null; - _lastNativeText: ?Stringish = null; - _lastNativeSelection: ?Selection = null; - _rafId: ?AnimationFrameID = null; +const TextInput = createReactClass({ + displayName: 'TextInput', + statics: { + State: { + currentlyFocusedField: TextInputState.currentlyFocusedField, + focusTextInput: TextInputState.focusTextInput, + blurTextInput: TextInputState.blurTextInput, + }, + }, + propTypes: { + ...DeprecatedViewPropTypes, + /** + * Can tell `TextInput` to automatically capitalize certain characters. + * + * - `characters`: all characters. + * - `words`: first letter of each word. + * - `sentences`: first letter of each sentence (*default*). + * - `none`: don't auto capitalize anything. + */ + autoCapitalize: PropTypes.oneOf([ + 'none', + 'sentences', + 'words', + 'characters', + ]), + /** + * If `false`, disables auto-correct. The default value is `true`. + */ + autoCorrect: PropTypes.bool, + /** + * If `false`, disables spell-check style (i.e. red underlines). + * The default value is inherited from `autoCorrect`. + * @platform ios + */ + spellCheck: PropTypes.bool, + /** + * If `true`, focuses the input on `componentDidMount`. + * The default value is `false`. + */ + autoFocus: PropTypes.bool, + /** + * Specifies whether fonts should scale to respect Text Size accessibility settings. The + * default is `true`. + */ + allowFontScaling: PropTypes.bool, + /** + * Specifies largest possible scale a font can reach when `allowFontScaling` is enabled. + * Possible values: + * `null/undefined` (default): inherit from the parent node or the global default (0) + * `0`: no max, ignore parent/global default + * `>= 1`: sets the maxFontSizeMultiplier of this node to this value + */ + maxFontSizeMultiplier: PropTypes.number, + /** + * If `false`, text is not editable. The default value is `true`. + */ + editable: PropTypes.bool, + /** + * Determines which keyboard to open, e.g.`numeric`. + * + * The following values work across platforms: + * + * - `default` + * - `numeric` + * - `number-pad` + * - `decimal-pad` + * - `email-address` + * - `phone-pad` + * + * *iOS Only* + * + * The following values work on iOS only: + * + * - `ascii-capable` + * - `numbers-and-punctuation` + * - `url` + * - `name-phone-pad` + * - `twitter` + * - `web-search` + * + * *Android Only* + * + * The following values work on Android only: + * + * - `visible-password` + */ + keyboardType: PropTypes.oneOf([ + // Cross-platform + 'default', + 'email-address', + 'numeric', + 'phone-pad', + 'number-pad', + // iOS-only + 'ascii-capable', + 'numbers-and-punctuation', + 'url', + 'name-phone-pad', + 'decimal-pad', + 'twitter', + 'web-search', + // Android-only + 'visible-password', + ]), + /** + * Determines the color of the keyboard. + * @platform ios + */ + keyboardAppearance: PropTypes.oneOf(['default', 'light', 'dark']), + /** + * Determines how the return key should look. On Android you can also use + * `returnKeyLabel`. + * + * *Cross platform* + * + * The following values work across platforms: + * + * - `done` + * - `go` + * - `next` + * - `search` + * - `send` + * + * *Android Only* + * + * The following values work on Android only: + * + * - `none` + * - `previous` + * + * *iOS Only* + * + * The following values work on iOS only: + * + * - `default` + * - `emergency-call` + * - `google` + * - `join` + * - `route` + * - `yahoo` + */ + returnKeyType: PropTypes.oneOf([ + // Cross-platform + 'done', + 'go', + 'next', + 'search', + 'send', + // Android-only + 'none', + 'previous', + // iOS-only + 'default', + 'emergency-call', + 'google', + 'join', + 'route', + 'yahoo', + ]), + /** + * Sets the return key to the label. Use it instead of `returnKeyType`. + * @platform android + */ + returnKeyLabel: PropTypes.string, + /** + * Limits the maximum number of characters that can be entered. Use this + * instead of implementing the logic in JS to avoid flicker. + */ + maxLength: PropTypes.number, + /** + * Sets the number of lines for a `TextInput`. Use it with multiline set to + * `true` to be able to fill the lines. + * @platform android + */ + numberOfLines: PropTypes.number, + /** + * When `false`, if there is a small amount of space available around a text input + * (e.g. landscape orientation on a phone), the OS may choose to have the user edit + * the text inside of a full screen text input mode. When `true`, this feature is + * disabled and users will always edit the text directly inside of the text input. + * Defaults to `false`. + * @platform android + */ + disableFullscreenUI: PropTypes.bool, + /** + * If `true`, the keyboard disables the return key when there is no text and + * automatically enables it when there is text. The default value is `false`. + * @platform ios + */ + enablesReturnKeyAutomatically: PropTypes.bool, + /** + * If `true`, the text input can be multiple lines. + * The default value is `false`. + */ + multiline: PropTypes.bool, + /** + * Set text break strategy on Android API Level 23+, possible values are `simple`, `highQuality`, `balanced` + * The default value is `simple`. + * @platform android + */ + textBreakStrategy: PropTypes.oneOf(['simple', 'highQuality', 'balanced']), + /** + * Callback that is called when the text input is blurred. + */ + onBlur: PropTypes.func, + /** + * Callback that is called when the text input is focused. + */ + onFocus: PropTypes.func, + /** + * Callback that is called when the text input's text changes. + */ + onChange: PropTypes.func, + /** + * Callback that is called when the text input's text changes. + * Changed text is passed as an argument to the callback handler. + */ + onChangeText: PropTypes.func, + /** + * Callback that is called when the text input's content size changes. + * This will be called with + * `{ nativeEvent: { contentSize: { width, height } } }`. + * + * Only called for multiline text inputs. + */ + onContentSizeChange: PropTypes.func, + onTextInput: PropTypes.func, + /** + * Callback that is called when text input ends. + */ + onEndEditing: PropTypes.func, + /** + * Callback that is called when the text input selection is changed. + * This will be called with + * `{ nativeEvent: { selection: { start, end } } }`. + */ + onSelectionChange: PropTypes.func, + /** + * Callback that is called when the text input's submit button is pressed. + * Invalid if `multiline={true}` is specified. + */ + onSubmitEditing: PropTypes.func, + /** + * Callback that is called when a key is pressed. + * This will be called with `{ nativeEvent: { key: keyValue } }` + * where `keyValue` is `'Enter'` or `'Backspace'` for respective keys and + * the typed-in character otherwise including `' '` for space. + * Fires before `onChange` callbacks. + */ + onKeyPress: PropTypes.func, + /** + * Invoked on mount and layout changes with `{x, y, width, height}`. + */ + onLayout: PropTypes.func, + /** + * Invoked on content scroll with `{ nativeEvent: { contentOffset: { x, y } } }`. + * May also contain other properties from ScrollEvent but on Android contentSize + * is not provided for performance reasons. + */ + onScroll: PropTypes.func, + /** + * The string that will be rendered before text input has been entered. + */ + placeholder: PropTypes.string, + /** + * The text color of the placeholder string. + */ + placeholderTextColor: DeprecatedColorPropType, + /** + * If `false`, scrolling of the text view will be disabled. + * The default value is `true`. Does only work with 'multiline={true}'. + * @platform ios + */ + scrollEnabled: PropTypes.bool, + /** + * If `true`, the text input obscures the text entered so that sensitive text + * like passwords stay secure. The default value is `false`. Does not work with 'multiline={true}'. + */ + secureTextEntry: PropTypes.bool, + /** + * The highlight and cursor color of the text input. + */ + selectionColor: DeprecatedColorPropType, + /** + * An instance of `DocumentSelectionState`, this is some state that is responsible for + * maintaining selection information for a document. + * + * Some functionality that can be performed with this instance is: + * + * - `blur()` + * - `focus()` + * - `update()` + * + * > You can reference `DocumentSelectionState` in + * > [`vendor/document/selection/DocumentSelectionState.js`](https://github.com/facebook/react-native/blob/master/Libraries/vendor/document/selection/DocumentSelectionState.js) + * + * @platform ios + */ + selectionState: PropTypes.instanceOf(DocumentSelectionState), + /** + * The start and end of the text input's selection. Set start and end to + * the same value to position the cursor. + */ + selection: PropTypes.shape({ + start: PropTypes.number.isRequired, + end: PropTypes.number, + }), + /** + * The value to show for the text input. `TextInput` is a controlled + * component, which means the native value will be forced to match this + * value prop if provided. For most uses, this works great, but in some + * cases this may cause flickering - one common cause is preventing edits + * by keeping value the same. In addition to simply setting the same value, + * either set `editable={false}`, or set/update `maxLength` to prevent + * unwanted edits without flicker. + */ + value: PropTypes.string, + /** + * Provides an initial value that will change when the user starts typing. + * Useful for simple use-cases where you do not want to deal with listening + * to events and updating the value prop to keep the controlled state in sync. + */ + defaultValue: PropTypes.string, + /** + * When the clear button should appear on the right side of the text view. + * This property is supported only for single-line TextInput component. + * @platform ios + */ + clearButtonMode: PropTypes.oneOf([ + 'never', + 'while-editing', + 'unless-editing', + 'always', + ]), + /** + * If `true`, clears the text field automatically when editing begins. + * @platform ios + */ + clearTextOnFocus: PropTypes.bool, + /** + * If `true`, all text will automatically be selected on focus. + */ + selectTextOnFocus: PropTypes.bool, + /** + * If `true`, the text field will blur when submitted. + * The default value is true for single-line fields and false for + * multiline fields. Note that for multiline fields, setting `blurOnSubmit` + * to `true` means that pressing return will blur the field and trigger the + * `onSubmitEditing` event instead of inserting a newline into the field. + */ + blurOnSubmit: PropTypes.bool, + /** + * Note that not all Text styles are supported, an incomplete list of what is not supported includes: + * + * - `borderLeftWidth` + * - `borderTopWidth` + * - `borderRightWidth` + * - `borderBottomWidth` + * - `borderTopLeftRadius` + * - `borderTopRightRadius` + * - `borderBottomRightRadius` + * - `borderBottomLeftRadius` + * + * see [Issue#7070](https://github.com/facebook/react-native/issues/7070) + * for more detail. + * + * [Styles](docs/style.html) + */ + style: Text.propTypes.style, + /** + * The color of the `TextInput` underline. + * @platform android + */ + underlineColorAndroid: DeprecatedColorPropType, - context: { - focusEmitter?: ?EventEmitter, - onFocusRequested?: ?(component: React.Component) => mixed, - }; - _focusSubscription: ?Function = null; + /** + * If defined, the provided image resource will be rendered on the left. + * The image resource must be inside `/android/app/src/main/res/drawable` and referenced + * like + * ``` + * + * ``` + * @platform android + */ + inlineImageLeft: PropTypes.string, - _setNativeRef = ref => { - this._inputRef = ref; - }; + /** + * Padding between the inline image, if any, and the text input itself. + * @platform android + */ + inlineImagePadding: PropTypes.number, - focus(): void { - nullthrows(this._inputRef).focus(); - } - - setNativeProps(props: Object): void { - nullthrows(this._inputRef).setNativeProps(props); - } - - blur(): void { - nullthrows(this._inputRef).blur(); - } + /** + * Determines the types of data converted to clickable URLs in the text input. + * Only valid if `multiline={true}` and `editable={false}`. + * By default no data types are detected. + * + * You can provide one type or an array of many types. + * + * Possible values for `dataDetectorTypes` are: + * + * - `'phoneNumber'` + * - `'link'` + * - `'address'` + * - `'calendarEvent'` + * - `'none'` + * - `'all'` + * + * @platform ios + */ + dataDetectorTypes: PropTypes.oneOfType([ + PropTypes.oneOf(DataDetectorTypes), + PropTypes.arrayOf(PropTypes.oneOf(DataDetectorTypes)), + ]), + /** + * If `true`, caret is hidden. The default value is `false`. + * This property is supported only for single-line TextInput component on iOS. + */ + caretHidden: PropTypes.bool, + /* + * If `true`, contextMenuHidden is hidden. The default value is `false`. + */ + contextMenuHidden: PropTypes.bool, + /** + * An optional identifier which links a custom InputAccessoryView to + * this text input. The InputAccessoryView is rendered above the + * keyboard when this text input is focused. + * @platform ios + */ + inputAccessoryViewID: PropTypes.string, + /** + * Give the keyboard and the system information about the + * expected semantic meaning for the content that users enter. + * @platform ios + */ + textContentType: PropTypes.oneOf([ + 'none', + 'URL', + 'addressCity', + 'addressCityAndState', + 'addressState', + 'countryName', + 'creditCardNumber', + 'emailAddress', + 'familyName', + 'fullStreetAddress', + 'givenName', + 'jobTitle', + 'location', + 'middleName', + 'name', + 'namePrefix', + 'nameSuffix', + 'nickname', + 'organizationName', + 'postalCode', + 'streetAddressLine1', + 'streetAddressLine2', + 'sublocality', + 'telephoneNumber', + 'username', + 'password', + 'newPassword', + 'oneTimeCode', + ]), + }, + getDefaultProps(): Object { + return { + allowFontScaling: true, + underlineColorAndroid: 'transparent', + }; + }, + /** + * `NativeMethodsMixin` will look for this when invoking `setNativeProps`. We + * make `this` look like an actual native component class. + */ + mixins: [NativeMethodsMixin], /** * Returns `true` if the input is currently focused; `false` otherwise. */ - isFocused(): boolean { + isFocused: function(): boolean { return ( TextInputState.currentlyFocusedField() === ReactNative.findNodeHandle(this._inputRef) ); - } + }, - measure(callback: MeasureOnSuccessCallback): void { - nullthrows(this._inputRef).measure(callback); - } + _inputRef: (undefined: any), + _focusSubscription: (undefined: ?Function), + _lastNativeText: (undefined: ?string), + _lastNativeSelection: (undefined: ?Selection), + _rafId: (null: ?AnimationFrameID), - measureLayout( - relativeToNativeNode: number, - onSuccess: MeasureLayoutOnSuccessCallback, - onFail: () => void, - ): void { - nullthrows(this._inputRef).measureLayout( - relativeToNativeNode, - onSuccess, - onFail, - ); - } - - measureInWindow(callback: MeasureInWindowOnSuccessCallback): void { - nullthrows(this._inputRef).measureInWindow(callback); - } - - componentDidMount() { + componentDidMount: function() { this._lastNativeText = this.props.value; const tag = ReactNative.findNodeHandle(this._inputRef); if (tag != null) { // tag is null only in unit tests TextInputState.registerInput(tag); } - const safeCallbackDoFocus = () => { - // Checks needed to prevent jest tests from crashing - if (this._inputRef && this._inputRef.focus) { - this._inputRef.focus(); - } - }; + if (this.context.focusEmitter) { this._focusSubscription = this.context.focusEmitter.addListener( 'focus', el => { if (this === el) { - this._rafId = requestAnimationFrame(safeCallbackDoFocus); - } else if ( - this.isFocused() && - this._inputRef && - this._inputRef.blur - ) { - this._inputRef.blur(); + this._rafId = requestAnimationFrame(this.focus); + } else if (this.isFocused()) { + this.blur(); } }, ); - if (this.props.autoFocus && this.context.onFocusRequested) { + if (this.props.autoFocus) { this.context.onFocusRequested(this); } } else { if (this.props.autoFocus) { - this._rafId = requestAnimationFrame(safeCallbackDoFocus); + this._rafId = requestAnimationFrame(this.focus); } } - } + }, - componentWillUnmount() { + componentWillUnmount: function() { this._focusSubscription && this._focusSubscription.remove(); - if (this.isFocused() && this._inputRef && this._inputRef.blur) { - this._inputRef.blur(); + if (this.isFocused()) { + this.blur(); } const tag = ReactNative.findNodeHandle(this._inputRef); if (tag != null) { @@ -275,16 +861,21 @@ class TextInput extends React.Component { if (this._rafId != null) { cancelAnimationFrame(this._rafId); } - } + }, + + contextTypes: { + onFocusRequested: PropTypes.func, + focusEmitter: PropTypes.instanceOf(EventEmitter), + }, /** * Removes all text from the `TextInput`. */ - clear() { - nullthrows(this._inputRef).setNativeProps({text: ''}); - } + clear: function() { + this.setNativeProps({text: ''}); + }, - render() { + render: function() { let textInput; if (Platform.OS === 'ios') { textInput = UIManager.getViewManagerConfig('RCTVirtualText') @@ -296,21 +887,25 @@ class TextInput extends React.Component { return ( {textInput} ); - } + }, - _getText(): ?string { + _getText: function(): ?string { return typeof this.props.value === 'string' ? this.props.value : typeof this.props.defaultValue === 'string' ? this.props.defaultValue : ''; - } + }, - _renderIOSLegacy() { + _setNativeRef: function(ref: any) { + this._inputRef = ref; + }, + + _renderIOSLegacy: function() { let textContainer; const props = Object.assign({}, this.props); - const style = [this.props.style]; + props.style = [this.props.style]; if (props.selection && props.selection.end == null) { props.selection = { @@ -355,7 +950,7 @@ class TextInput extends React.Component { if (childCount >= 1) { children = ( {children} @@ -365,7 +960,7 @@ class TextInput extends React.Component { if (props.inputView) { children = [children, props.inputView]; } - style.unshift(styles.multilineInput); + props.style.unshift(styles.multilineInput); textContainer = ( { {textContainer} ); - } + }, - _renderIOS() { + _renderIOS: function() { const props = Object.assign({}, this.props); props.style = [this.props.style]; @@ -451,14 +1046,17 @@ class TextInput extends React.Component { {textContainer} ); - } + }, - _renderAndroid() { + _renderAndroid: function() { const props = Object.assign({}, this.props); props.style = [this.props.style]; props.autoCapitalize = UIManager.getViewManagerConfig( 'AndroidTextInput', ).Constants.AutoCapitalizationType[props.autoCapitalize || 'sentences']; + /* $FlowFixMe(>=0.53.0 site=react_native_fb,react_native_oss) This comment + * suppresses an error when upgrading Flow's support for React. To see the + * error delete this comment and run Flow. */ let children = this.props.children; let childCount = 0; React.Children.forEach(children, () => ++childCount); @@ -508,9 +1106,9 @@ class TextInput extends React.Component { {textContainer} ); - } + }, - _onFocus = (event: Event) => { + _onFocus: function(event: Event) { if (this.props.onFocus) { this.props.onFocus(event); } @@ -518,15 +1116,15 @@ class TextInput extends React.Component { if (this.props.selectionState) { this.props.selectionState.focus(); } - }; + }, - _onPress = (event: PressEvent) => { + _onPress: function(event: Event) { if (this.props.editable || this.props.editable === undefined) { - nullthrows(this._inputRef).focus(); + this.focus(); } - }; + }, - _onChange = (event: Event) => { + _onChange: function(event: Event) { // Make sure to fire the mostRecentEventCount first so it is already set on // native when the text value is set. if (this._inputRef && this._inputRef.setNativeProps) { @@ -547,9 +1145,9 @@ class TextInput extends React.Component { this._lastNativeText = text; this.forceUpdate(); - }; + }, - _onSelectionChange = (event: Event) => { + _onSelectionChange: function(event: Event) { this.props.onSelectionChange && this.props.onSelectionChange(event); if (!this._inputRef) { @@ -563,9 +1161,9 @@ class TextInput extends React.Component { if (this.props.selection || this.props.selectionState) { this.forceUpdate(); } - }; + }, - componentDidUpdate() { + componentDidUpdate: function() { // 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. @@ -601,10 +1199,10 @@ class TextInput extends React.Component { if (this.props.selectionState && selection) { this.props.selectionState.update(selection.start, selection.end); } - } + }, - _onBlur = (event: Event) => { - nullthrows(this._inputRef).blur(); + _onBlur: function(event: Event) { + this.blur(); if (this.props.onBlur) { this.props.onBlur(event); } @@ -612,154 +1210,25 @@ class TextInput extends React.Component { if (this.props.selectionState) { this.props.selectionState.blur(); } - }; + }, - _onTextInput = (event: Event) => { + _onTextInput: function(event: Event) { this.props.onTextInput && this.props.onTextInput(event); - }; + }, - _onScroll = (event: Event) => { + _onScroll: function(event: Event) { this.props.onScroll && this.props.onScroll(event); - }; + }, +}); + +class InternalTextInputType extends ReactNative.NativeComponent { + clear() {} + + // $FlowFixMe + isFocused(): boolean {} } -const DataDetectorTypes = [ - 'phoneNumber', - 'link', - 'address', - 'calendarEvent', - 'none', - 'all', -]; - -TextInput.propTypes = { - ...DeprecatedViewPropTypes, - autoCapitalize: PropTypes.oneOf(['none', 'sentences', 'words', 'characters']), - autoCorrect: PropTypes.bool, - spellCheck: PropTypes.bool, - autoFocus: PropTypes.bool, - allowFontScaling: PropTypes.bool, - maxFontSizeMultiplier: PropTypes.number, - editable: PropTypes.bool, - keyboardType: PropTypes.oneOf([ - // Cross-platform - 'default', - 'email-address', - 'numeric', - 'phone-pad', - 'number-pad', - // iOS-only - 'ascii-capable', - 'numbers-and-punctuation', - 'url', - 'name-phone-pad', - 'decimal-pad', - 'twitter', - 'web-search', - // Android-only - 'visible-password', - ]), - keyboardAppearance: PropTypes.oneOf(['default', 'light', 'dark']), - returnKeyType: PropTypes.oneOf([ - // Cross-platform - 'done', - 'go', - 'next', - 'search', - 'send', - // Android-only - 'none', - 'previous', - // iOS-only - 'default', - 'emergency-call', - 'google', - 'join', - 'route', - 'yahoo', - ]), - returnKeyLabel: PropTypes.string, - maxLength: PropTypes.number, - numberOfLines: PropTypes.number, - disableFullscreenUI: PropTypes.bool, - enablesReturnKeyAutomatically: PropTypes.bool, - multiline: PropTypes.bool, - textBreakStrategy: PropTypes.oneOf(['simple', 'highQuality', 'balanced']), - onBlur: PropTypes.func, - onFocus: PropTypes.func, - onChange: PropTypes.func, - onChangeText: PropTypes.func, - onContentSizeChange: PropTypes.func, - onTextInput: PropTypes.func, - onEndEditing: PropTypes.func, - onSelectionChange: PropTypes.func, - onSubmitEditing: PropTypes.func, - onKeyPress: PropTypes.func, - onLayout: PropTypes.func, - onScroll: PropTypes.func, - placeholder: PropTypes.string, - placeholderTextColor: DeprecatedColorPropType, - scrollEnabled: PropTypes.bool, - secureTextEntry: PropTypes.bool, - selectionColor: DeprecatedColorPropType, - selectionState: PropTypes.instanceOf(DocumentSelectionState), - selection: PropTypes.shape({ - start: PropTypes.number.isRequired, - end: PropTypes.number, - }), - value: PropTypes.string, - defaultValue: PropTypes.string, - clearButtonMode: PropTypes.oneOf([ - 'never', - 'while-editing', - 'unless-editing', - 'always', - ]), - clearTextOnFocus: PropTypes.bool, - selectTextOnFocus: PropTypes.bool, - blurOnSubmit: PropTypes.bool, - style: DeprecatedStyleSheetPropType(TextStylePropTypes), - underlineColorAndroid: DeprecatedColorPropType, - inlineImageLeft: PropTypes.string, - inlineImagePadding: PropTypes.number, - dataDetectorTypes: PropTypes.oneOfType([ - PropTypes.oneOf(DataDetectorTypes), - PropTypes.arrayOf(PropTypes.oneOf(DataDetectorTypes)), - ]), - caretHidden: PropTypes.bool, - contextMenuHidden: PropTypes.bool, - inputAccessoryViewID: PropTypes.string, - textContentType: PropTypes.oneOf([ - 'none', - 'URL', - 'addressCity', - 'addressCityAndState', - 'addressState', - 'countryName', - 'creditCardNumber', - 'emailAddress', - 'familyName', - 'fullStreetAddress', - 'givenName', - 'jobTitle', - 'location', - 'middleName', - 'name', - 'namePrefix', - 'nameSuffix', - 'nickname', - 'organizationName', - 'postalCode', - 'streetAddressLine1', - 'streetAddressLine2', - 'sublocality', - 'telephoneNumber', - 'username', - 'password', - 'newPassword', - 'oneTimeCode', - ]), -}; +const TypedTextInput = ((TextInput: any): Class); const styles = StyleSheet.create({ multilineInput: { @@ -770,4 +1239,4 @@ const styles = StyleSheet.create({ }, }); -module.exports = TextInput; +module.exports = TypedTextInput; diff --git a/Libraries/Components/TextInput/TextInputNativeComponent.js b/Libraries/Components/TextInput/TextInputNativeComponent.js deleted file mode 100644 index 9c9cdf408..000000000 --- a/Libraries/Components/TextInput/TextInputNativeComponent.js +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - * @format - */ - -'use strict'; - -const Platform = require('Platform'); -const ReactNative = require('ReactNative'); // eslint-disable-line no-unused-vars - -const requireNativeComponent = require('requireNativeComponent'); - -import type {Props} from 'TextInputTypes'; - -let AndroidTextInput = null; -let RCTMultilineTextInputView = null; -let RCTSinglelineTextInputView = null; - -if (Platform.OS === 'android') { - AndroidTextInput = requireNativeComponent('AndroidTextInput'); -} else if (Platform.OS === 'ios') { - RCTMultilineTextInputView = requireNativeComponent( - 'RCTMultilineTextInputView', - ); - RCTSinglelineTextInputView = requireNativeComponent( - 'RCTSinglelineTextInputView', - ); -} - -type NativeProps = $ReadOnly<{| - ...Props, - text?: ?string, - onSelectionChangeShouldSetResponder?: ?() => boolean, - mostRecentEventCount?: ?number, -|}>; - -declare class TextInputType extends ReactNative.NativeComponent { - /** - * Removes all text from the `TextInput`. - */ - clear(): mixed; - - /** - * Returns `true` if the input is currently focused; `false` otherwise. - */ - isFocused(): boolean; -} - -export type {TextInputType}; - -module.exports = { - AndroidTextInput: ((AndroidTextInput: any): Class), - RCTMultilineTextInputView: ((RCTMultilineTextInputView: any): Class< - TextInputType, - >), - RCTSinglelineTextInputView: ((RCTSinglelineTextInputView: any): Class< - TextInputType, - >), -}; diff --git a/Libraries/Components/TextInput/TextInputTypes.js b/Libraries/Components/TextInput/TextInputTypes.js deleted file mode 100644 index dd4618f0b..000000000 --- a/Libraries/Components/TextInput/TextInputTypes.js +++ /dev/null @@ -1,532 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - * @format - */ - -'use strict'; - -import type React from 'React'; -import type {SyntheticEvent} from 'CoreEventTypes'; -import type {TextStyleProp, ViewStyleProp} from 'StyleSheet'; -import type {ColorValue} from 'StyleSheetTypes'; -import type {ViewProps} from 'ViewPropTypes'; -import type {TextInputType} from 'TextInputNativeComponent'; -import type DocumentSelectionState from 'DocumentSelectionState'; - -export type Event = Object; - -export type Selection = $ReadOnly<{| - start: number, - end?: number, -|}>; - -type DataDetectorTypes = - | 'phoneNumber' - | 'link' - | 'address' - | 'calendarEvent' - | 'none' - | 'all'; - -export type KeyboardType = - // Cross Platform - | 'default' - | 'email-address' - | 'numeric' - | 'phone-pad' - | 'number-pad' - | 'decimal-pad' - // iOS-only - | 'ascii-capable' - | 'numbers-and-punctuation' - | 'url' - | 'name-phone-pad' - | 'twitter' - | 'web-search' - // Android-only - | 'visible-password'; - -export type ReturnKeyType = - // Cross Platform - | 'done' - | 'go' - | 'next' - | 'search' - | 'send' - // Android-only - | 'none' - | 'previous' - // iOS-only - | 'default' - | 'emergency-call' - | 'google' - | 'join' - | 'route' - | 'yahoo'; - -export type AutoCapitalize = 'none' | 'sentences' | 'words' | 'characters'; - -type IOSProps = $ReadOnly<{| - /** - * If `false`, disables spell-check style (i.e. red underlines). - * The default value is inherited from `autoCorrect`. - * @platform ios - */ - spellCheck?: ?boolean, - - /** - * Determines the color of the keyboard. - * @platform ios - */ - keyboardAppearance?: ?('default' | 'light' | 'dark'), - - /** - * If `true`, the keyboard disables the return key when there is no text and - * automatically enables it when there is text. The default value is `false`. - * @platform ios - */ - enablesReturnKeyAutomatically?: ?boolean, - - /** - * An instance of `DocumentSelectionState`, this is some state that is responsible for - * maintaining selection information for a document. - * - * Some functionality that can be performed with this instance is: - * - * - `blur()` - * - `focus()` - * - `update()` - * - * > You can reference `DocumentSelectionState` in - * > [`vendor/document/selection/DocumentSelectionState.js`](https://github.com/facebook/react-native/blob/master/Libraries/vendor/document/selection/DocumentSelectionState.js) - * - * @platform ios - */ - selectionState?: ?DocumentSelectionState, - - /** - * When the clear button should appear on the right side of the text view. - * This property is supported only for single-line TextInput component. - * @platform ios - */ - clearButtonMode?: ?('never' | 'while-editing' | 'unless-editing' | 'always'), - - /** - * If `true`, clears the text field automatically when editing begins. - * @platform ios - */ - clearTextOnFocus?: ?boolean, - - /** - * Determines the types of data converted to clickable URLs in the text input. - * Only valid if `multiline={true}` and `editable={false}`. - * By default no data types are detected. - * - * You can provide one type or an array of many types. - * - * Possible values for `dataDetectorTypes` are: - * - * - `'phoneNumber'` - * - `'link'` - * - `'address'` - * - `'calendarEvent'` - * - `'none'` - * - `'all'` - * - * @platform ios - */ - dataDetectorTypes?: ?DataDetectorTypes | $ReadOnlyArray, - - /** - * An optional identifier which links a custom InputAccessoryView to - * this text input. The InputAccessoryView is rendered above the - * keyboard when this text input is focused. - * @platform ios - */ - inputAccessoryViewID?: ?string, - - /** - * Give the keyboard and the system information about the - * expected semantic meaning for the content that users enter. - * @platform ios - */ - textContentType?: ?( - | 'none' - | 'URL' - | 'addressCity' - | 'addressCityAndState' - | 'addressState' - | 'countryName' - | 'creditCardNumber' - | 'emailAddress' - | 'familyName' - | 'fullStreetAddress' - | 'givenName' - | 'jobTitle' - | 'location' - | 'middleName' - | 'name' - | 'namePrefix' - | 'nameSuffix' - | 'nickname' - | 'organizationName' - | 'postalCode' - | 'streetAddressLine1' - | 'streetAddressLine2' - | 'sublocality' - | 'telephoneNumber' - | 'username' - | 'password' - | 'newPassword' - | 'oneTimeCode' - ), - - /** - * If `false`, scrolling of the text view will be disabled. - * The default value is `true`. Does only work with 'multiline={true}'. - * @platform ios - */ - scrollEnabled?: ?boolean, -|}>; - -type AndroidProps = $ReadOnly<{| - /** - * Sets the return key to the label. Use it instead of `returnKeyType`. - * @platform android - */ - returnKeyLabel?: ?string, - - /** - * Sets the number of lines for a `TextInput`. Use it with multiline set to - * `true` to be able to fill the lines. - * @platform android - */ - numberOfLines?: ?number, - - /** - * When `false`, if there is a small amount of space available around a text input - * (e.g. landscape orientation on a phone), the OS may choose to have the user edit - * the text inside of a full screen text input mode. When `true`, this feature is - * disabled and users will always edit the text directly inside of the text input. - * Defaults to `false`. - * @platform android - */ - disableFullscreenUI?: ?boolean, - - /** - * Set text break strategy on Android API Level 23+, possible values are `simple`, `highQuality`, `balanced` - * The default value is `simple`. - * @platform android - */ - textBreakStrategy?: ?('simple' | 'highQuality' | 'balanced'), - - /** - * The color of the `TextInput` underline. - * @platform android - */ - underlineColorAndroid?: ?ColorValue, - - /** - * If defined, the provided image resource will be rendered on the left. - * The image resource must be inside `/android/app/src/main/res/drawable` and referenced - * like - * ``` - * - * ``` - * @platform android - */ - inlineImageLeft?: ?string, - - /** - * Padding between the inline image, if any, and the text input itself. - * @platform android - */ - inlineImagePadding?: ?number, -|}>; - -export type Props = $ReadOnly<{| - ...$Diff>, - ...IOSProps, - ...AndroidProps, - - /** - * Can tell `TextInput` to automatically capitalize certain characters. - * - * - `characters`: all characters. - * - `words`: first letter of each word. - * - `sentences`: first letter of each sentence (*default*). - * - `none`: don't auto capitalize anything. - */ - autoCapitalize?: ?AutoCapitalize, - - /** - * If `false`, disables auto-correct. The default value is `true`. - */ - autoCorrect?: ?boolean, - - /** - * If `true`, focuses the input on `componentDidMount`. - * The default value is `false`. - */ - autoFocus?: ?boolean, - - /** - * Specifies whether fonts should scale to respect Text Size accessibility settings. The - * default is `true`. - */ - allowFontScaling?: ?boolean, - - /** - * Specifies largest possible scale a font can reach when `allowFontScaling` is enabled. - * Possible values: - * `null/undefined` (default): inherit from the parent node or the global default (0) - * `0`: no max, ignore parent/global default - * `>= 1`: sets the maxFontSizeMultiplier of this node to this value - */ - maxFontSizeMultiplier?: ?number, - - /** - * If `false`, text is not editable. The default value is `true`. - */ - editable?: ?boolean, - - /** - * Determines which keyboard to open, e.g.`numeric`. - * - * The following values work across platforms: - * - * - `default` - * - `numeric` - * - `number-pad` - * - `decimal-pad` - * - `email-address` - * - `phone-pad` - * - * *iOS Only* - * - * The following values work on iOS only: - * - * - `ascii-capable` - * - `numbers-and-punctuation` - * - `url` - * - `name-phone-pad` - * - `twitter` - * - `web-search` - * - * *Android Only* - * - * The following values work on Android only: - * - * - `visible-password` - */ - keyboardType?: ?KeyboardType, - - /** - * Determines how the return key should look. On Android you can also use - * `returnKeyLabel`. - * - * *Cross platform* - * - * The following values work across platforms: - * - * - `done` - * - `go` - * - `next` - * - `search` - * - `send` - * - * *Android Only* - * - * The following values work on Android only: - * - * - `none` - * - `previous` - * - * *iOS Only* - * - * The following values work on iOS only: - * - * - `default` - * - `emergency-call` - * - `google` - * - `join` - * - `route` - * - `yahoo` - */ - returnKeyType?: ?ReturnKeyType, - - /** - * Limits the maximum number of characters that can be entered. Use this - * instead of implementing the logic in JS to avoid flicker. - */ - maxLength?: ?number, - - /** - * If `true`, the text input can be multiple lines. - * The default value is `false`. - */ - multiline?: ?boolean, - - /** - * Callback that is called when the text input is blurred. - */ - onBlur?: ?Function, - - /** - * Callback that is called when the text input is focused. - */ - onFocus?: ?Function, - - /** - * Callback that is called when the text input's text changes. - */ - onChange?: ?Function, - - /** - * Callback that is called when the text input's text changes. - * Changed text is passed as an argument to the callback handler. - */ - onChangeText?: ?Function, - - /** - * Callback that is called when the text input's content size changes. - * This will be called with - * `{ nativeEvent: { contentSize: { width, height } } }`. - * - * Only called for multiline text inputs. - */ - onContentSizeChange?: ?Function, - - onTextInput?: ?Function, - - /** - * Callback that is called when text input ends. - */ - onEndEditing?: ?Function, - - /** - * Callback that is called when the text input selection is changed. - * This will be called with - * `{ nativeEvent: { selection: { start, end } } }`. - */ - onSelectionChange?: ?Function, - - /** - * Callback that is called when the text input's submit button is pressed. - * Invalid if `multiline={true}` is specified. - */ - onSubmitEditing?: ?Function, - - /** - * Callback that is called when a key is pressed. - * This will be called with `{ nativeEvent: { key: keyValue } }` - * where `keyValue` is `'Enter'` or `'Backspace'` for respective keys and - * the typed-in character otherwise including `' '` for space. - * Fires before `onChange` callbacks. - */ - onKeyPress?: ?Function, - - /** - * Invoked on content scroll with `{ nativeEvent: { contentOffset: { x, y } } }`. - * May also contain other properties from ScrollEvent but on Android contentSize - * is not provided for performance reasons. - */ - onScroll?: ?Function, - - /** - * The string that will be rendered before text input has been entered. - */ - placeholder?: ?Stringish, - - /** - * The text color of the placeholder string. - */ - placeholderTextColor?: ?ColorValue, - - /** - * If `true`, the text input obscures the text entered so that sensitive text - * like passwords stay secure. The default value is `false`. Does not work with 'multiline={true}'. - */ - secureTextEntry?: ?boolean, - - /** - * The highlight and cursor color of the text input. - */ - selectionColor?: ?ColorValue, - - /** - * The start and end of the text input's selection. Set start and end to - * the same value to position the cursor. - */ - selection?: ?Selection, - - /** - * The value to show for the text input. `TextInput` is a controlled - * component, which means the native value will be forced to match this - * value prop if provided. For most uses, this works great, but in some - * cases this may cause flickering - one common cause is preventing edits - * by keeping value the same. In addition to simply setting the same value, - * either set `editable={false}`, or set/update `maxLength` to prevent - * unwanted edits without flicker. - */ - value?: ?Stringish, - - /** - * Provides an initial value that will change when the user starts typing. - * Useful for simple use-cases where you do not want to deal with listening - * to events and updating the value prop to keep the controlled state in sync. - */ - defaultValue?: ?Stringish, - - /** - * If `true`, all text will automatically be selected on focus. - */ - selectTextOnFocus?: ?boolean, - - /** - * If `true`, the text field will blur when submitted. - * The default value is true for single-line fields and false for - * multiline fields. Note that for multiline fields, setting `blurOnSubmit` - * to `true` means that pressing return will blur the field and trigger the - * `onSubmitEditing` event instead of inserting a newline into the field. - */ - blurOnSubmit?: ?boolean, - - /** - * If `true`, caret is hidden. The default value is `false`. - * This property is supported only for single-line TextInput component on iOS. - */ - caretHidden?: ?boolean, - - /* - * If `true`, contextMenuHidden is hidden. The default value is `false`. - */ - contextMenuHidden?: ?boolean, - - /** - * Note that not all Text styles are supported, an incomplete list of what is not supported includes: - * - * - `borderLeftWidth` - * - `borderTopWidth` - * - `borderRightWidth` - * - `borderBottomWidth` - * - `borderTopLeftRadius` - * - `borderTopRightRadius` - * - `borderBottomRightRadius` - * - `borderBottomLeftRadius` - * - * see [Issue#7070](https://github.com/facebook/react-native/issues/7070) - * for more detail. - * - * [Styles](docs/style.html) - */ - style?: ?TextStyleProp, - forwardedRef?: ?React.Ref>, -|}>; diff --git a/Libraries/Components/TextInput/__tests__/TextInput-test.js b/Libraries/Components/TextInput/__tests__/TextInput-test.js index 27dba9b07..78d6884db 100644 --- a/Libraries/Components/TextInput/__tests__/TextInput-test.js +++ b/Libraries/Components/TextInput/__tests__/TextInput-test.js @@ -12,11 +12,12 @@ 'use strict'; const React = require('React'); +const ReactTestRenderer = require('react-test-renderer'); const TextInput = require('TextInput'); import Component from '@reactions/component'; -const {enter, renderWithStrictMode} = require('ReactNativeTestTools'); +const {enter} = require('ReactNativeTestTools'); jest.unmock('TextInput'); @@ -28,7 +29,7 @@ describe('TextInput tests', () => { beforeEach(() => { onChangeListener = jest.fn(); onChangeTextListener = jest.fn(); - const renderTree = renderWithStrictMode( + const renderTree = ReactTestRenderer.create( {({setState, state}) => ( {}) .mock('Image', () => mockComponent('Image')) .mock('Text', () => mockComponent('Text', MockNativeMethods)) - .mock('TextInput', () => mockComponent('TextInput', MockNativeMethods)) + .mock('TextInput', () => mockComponent('TextInput')) .mock('Modal', () => mockComponent('Modal')) .mock('View', () => mockComponent('View', MockNativeMethods)) .mock('RefreshControl', () => jest.requireMock('RefreshControlMock'))