From 9ea129517922d8a23a5c81128e4dcf97f04a5129 Mon Sep 17 00:00:00 2001 From: Spencer Ahrens Date: Thu, 15 Nov 2018 20:42:47 -0800 Subject: [PATCH] reapply TextInput es6 conversion with fixes, attemps to fix Summary: D10515754 reapplied by backing out D12989604 and then fixed by manually forwarding the instance methods to the host function instead of using `forwardRef`. This also removes the need for the $flowFixMe. Reviewed By: TheSavior Differential Revision: D13048482 fbshipit-source-id: ff2447aff123e0960eddaef645f7dc976a426e14 --- Libraries/Components/TextInput/TextInput.js | 995 +++++------------- .../TextInput/TextInputNativeComponent.js | 64 ++ .../Components/TextInput/TextInputTypes.js | 532 ++++++++++ .../TextInput/__tests__/TextInput-test.js | 5 +- Libraries/Utilities/ReactNativeTestTools.js | 5 +- jest/setup.js | 2 +- 6 files changed, 866 insertions(+), 737 deletions(-) create mode 100644 Libraries/Components/TextInput/TextInputNativeComponent.js create mode 100644 Libraries/Components/TextInput/TextInputTypes.js diff --git a/Libraries/Components/TextInput/TextInput.js b/Libraries/Components/TextInput/TextInput.js index 2ed2f3b8905f67..883ec8fc06de46 100644 --- a/Libraries/Components/TextInput/TextInput.js +++ b/Libraries/Components/TextInput/TextInput.js @@ -9,11 +9,11 @@ */ 'use strict'; -const DeprecatedColorPropType = require('DeprecatedColorPropType'); const DeprecatedViewPropTypes = require('DeprecatedViewPropTypes'); +const DeprecatedColorPropType = require('DeprecatedColorPropType'); +const DeprecatedStyleSheetPropType = require('DeprecatedStyleSheetPropType'); const DocumentSelectionState = require('DocumentSelectionState'); -const EventEmitter = require('EventEmitter'); -const NativeMethodsMixin = require('NativeMethodsMixin'); +const TextStylePropTypes = require('TextStylePropTypes'); const Platform = require('Platform'); const PropTypes = require('prop-types'); const React = require('React'); @@ -25,193 +25,32 @@ const TextInputState = require('TextInputState'); const TouchableWithoutFeedback = require('TouchableWithoutFeedback'); const UIManager = require('UIManager'); -const createReactClass = require('create-react-class'); +const { + AndroidTextInput, + RCTMultilineTextInputView, + RCTSinglelineTextInputView, +} = require('TextInputNativeComponent'); + const emptyFunction = require('fbjs/lib/emptyFunction'); const invariant = require('fbjs/lib/invariant'); -const requireNativeComponent = require('requireNativeComponent'); const warning = require('fbjs/lib/warning'); - -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 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'; 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 @@ -323,536 +162,111 @@ type Props = $ReadOnly<{| * 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; + + context: { + focusEmitter?: ?EventEmitter, + onFocusRequested?: ?(component: React.Component) => mixed, + }; + _focusSubscription: ?Function = null; + + _setNativeRef = ref => { + this._inputRef = ref; + }; -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, + focus(): void { + nullthrows(this._inputRef).focus(); + } - /** - * 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, + setNativeProps(props: Object): void { + nullthrows(this._inputRef).setNativeProps(props); + } - /** - * Padding between the inline image, if any, and the text input itself. - * @platform android - */ - inlineImagePadding: PropTypes.number, - - /** - * 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], + blur(): void { + nullthrows(this._inputRef).blur(); + } /** * Returns `true` if the input is currently focused; `false` otherwise. */ - isFocused: function(): boolean { + isFocused(): boolean { return ( TextInputState.currentlyFocusedField() === ReactNative.findNodeHandle(this._inputRef) ); - }, + } + + measure(callback: MeasureOnSuccessCallback): void { + nullthrows(this._inputRef).measure(callback); + } + + measureLayout( + relativeToNativeNode: number, + onSuccess: MeasureLayoutOnSuccessCallback, + onFail: () => void, + ): void { + nullthrows(this._inputRef).measureLayout( + relativeToNativeNode, + onSuccess, + onFail, + ); + } - _inputRef: (undefined: any), - _focusSubscription: (undefined: ?Function), - _lastNativeText: (undefined: ?string), - _lastNativeSelection: (undefined: ?Selection), - _rafId: (null: ?AnimationFrameID), + measureInWindow(callback: MeasureInWindowOnSuccessCallback): void { + nullthrows(this._inputRef).measureInWindow(callback); + } - componentDidMount: function() { + componentDidMount() { 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(this.focus); - } else if (this.isFocused()) { - this.blur(); + this._rafId = requestAnimationFrame(safeCallbackDoFocus); + } else if ( + this.isFocused() && + this._inputRef && + this._inputRef.blur + ) { + this._inputRef.blur(); } }, ); - if (this.props.autoFocus) { + if (this.props.autoFocus && this.context.onFocusRequested) { this.context.onFocusRequested(this); } } else { if (this.props.autoFocus) { - this._rafId = requestAnimationFrame(this.focus); + this._rafId = requestAnimationFrame(safeCallbackDoFocus); } } - }, + } - componentWillUnmount: function() { + componentWillUnmount() { this._focusSubscription && this._focusSubscription.remove(); - if (this.isFocused()) { - this.blur(); + if (this.isFocused() && this._inputRef && this._inputRef.blur) { + this._inputRef.blur(); } const tag = ReactNative.findNodeHandle(this._inputRef); if (tag != null) { @@ -861,21 +275,16 @@ const TextInput = createReactClass({ if (this._rafId != null) { cancelAnimationFrame(this._rafId); } - }, - - contextTypes: { - onFocusRequested: PropTypes.func, - focusEmitter: PropTypes.instanceOf(EventEmitter), - }, + } /** * Removes all text from the `TextInput`. */ - clear: function() { - this.setNativeProps({text: ''}); - }, + clear() { + nullthrows(this._inputRef).setNativeProps({text: ''}); + } - render: function() { + render() { let textInput; if (Platform.OS === 'ios') { textInput = UIManager.getViewManagerConfig('RCTVirtualText') @@ -887,25 +296,21 @@ const TextInput = createReactClass({ return ( {textInput} ); - }, + } - _getText: function(): ?string { + _getText(): ?string { return typeof this.props.value === 'string' ? this.props.value : typeof this.props.defaultValue === 'string' ? this.props.defaultValue : ''; - }, + } - _setNativeRef: function(ref: any) { - this._inputRef = ref; - }, - - _renderIOSLegacy: function() { + _renderIOSLegacy() { let textContainer; const props = Object.assign({}, this.props); - props.style = [this.props.style]; + const style = [this.props.style]; if (props.selection && props.selection.end == null) { props.selection = { @@ -950,7 +355,7 @@ const TextInput = createReactClass({ if (childCount >= 1) { children = ( {children} @@ -960,7 +365,7 @@ const TextInput = createReactClass({ if (props.inputView) { children = [children, props.inputView]; } - props.style.unshift(styles.multilineInput); + style.unshift(styles.multilineInput); textContainer = ( ); - }, + } - _renderIOS: function() { + _renderIOS() { const props = Object.assign({}, this.props); props.style = [this.props.style]; @@ -1046,17 +451,14 @@ const TextInput = createReactClass({ {textContainer} ); - }, + } - _renderAndroid: function() { + _renderAndroid() { 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); @@ -1106,9 +508,9 @@ const TextInput = createReactClass({ {textContainer} ); - }, + } - _onFocus: function(event: Event) { + _onFocus = (event: Event) => { if (this.props.onFocus) { this.props.onFocus(event); } @@ -1116,15 +518,15 @@ const TextInput = createReactClass({ if (this.props.selectionState) { this.props.selectionState.focus(); } - }, + }; - _onPress: function(event: Event) { + _onPress = (event: PressEvent) => { if (this.props.editable || this.props.editable === undefined) { - this.focus(); + nullthrows(this._inputRef).focus(); } - }, + }; - _onChange: function(event: Event) { + _onChange = (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) { @@ -1145,9 +547,9 @@ const TextInput = createReactClass({ this._lastNativeText = text; this.forceUpdate(); - }, + }; - _onSelectionChange: function(event: Event) { + _onSelectionChange = (event: Event) => { this.props.onSelectionChange && this.props.onSelectionChange(event); if (!this._inputRef) { @@ -1161,9 +563,9 @@ const TextInput = createReactClass({ if (this.props.selection || this.props.selectionState) { this.forceUpdate(); } - }, + }; - componentDidUpdate: function() { + componentDidUpdate() { // 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. @@ -1199,10 +601,10 @@ const TextInput = createReactClass({ if (this.props.selectionState && selection) { this.props.selectionState.update(selection.start, selection.end); } - }, + } - _onBlur: function(event: Event) { - this.blur(); + _onBlur = (event: Event) => { + nullthrows(this._inputRef).blur(); if (this.props.onBlur) { this.props.onBlur(event); } @@ -1210,25 +612,154 @@ const TextInput = createReactClass({ if (this.props.selectionState) { this.props.selectionState.blur(); } - }, + }; - _onTextInput: function(event: Event) { + _onTextInput = (event: Event) => { this.props.onTextInput && this.props.onTextInput(event); - }, + }; - _onScroll: function(event: Event) { + _onScroll = (event: Event) => { this.props.onScroll && this.props.onScroll(event); - }, -}); - -class InternalTextInputType extends ReactNative.NativeComponent { - clear() {} - - // $FlowFixMe - isFocused(): boolean {} + }; } -const TypedTextInput = ((TextInput: any): Class); +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 styles = StyleSheet.create({ multilineInput: { @@ -1239,4 +770,4 @@ const styles = StyleSheet.create({ }, }); -module.exports = TypedTextInput; +module.exports = TextInput; diff --git a/Libraries/Components/TextInput/TextInputNativeComponent.js b/Libraries/Components/TextInput/TextInputNativeComponent.js new file mode 100644 index 00000000000000..9c9cdf4088cef1 --- /dev/null +++ b/Libraries/Components/TextInput/TextInputNativeComponent.js @@ -0,0 +1,64 @@ +/** + * 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 new file mode 100644 index 00000000000000..dd4618f0b541e0 --- /dev/null +++ b/Libraries/Components/TextInput/TextInputTypes.js @@ -0,0 +1,532 @@ +/** + * 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 78d6884db03395..27dba9b073f4e5 100644 --- a/Libraries/Components/TextInput/__tests__/TextInput-test.js +++ b/Libraries/Components/TextInput/__tests__/TextInput-test.js @@ -12,12 +12,11 @@ 'use strict'; const React = require('React'); -const ReactTestRenderer = require('react-test-renderer'); const TextInput = require('TextInput'); import Component from '@reactions/component'; -const {enter} = require('ReactNativeTestTools'); +const {enter, renderWithStrictMode} = require('ReactNativeTestTools'); jest.unmock('TextInput'); @@ -29,7 +28,7 @@ describe('TextInput tests', () => { beforeEach(() => { onChangeListener = jest.fn(); onChangeTextListener = jest.fn(); - const renderTree = ReactTestRenderer.create( + const renderTree = renderWithStrictMode( {({setState, state}) => ( {}) .mock('Image', () => mockComponent('Image')) .mock('Text', () => mockComponent('Text', MockNativeMethods)) - .mock('TextInput', () => mockComponent('TextInput')) + .mock('TextInput', () => mockComponent('TextInput', MockNativeMethods)) .mock('Modal', () => mockComponent('Modal')) .mock('View', () => mockComponent('View', MockNativeMethods)) .mock('RefreshControl', () => jest.requireMock('RefreshControlMock'))