From 13b619227906edc6443a6a56cdad8aebad458943 Mon Sep 17 00:00:00 2001 From: Daniel Ayoub Date: Wed, 12 Sep 2018 17:40:35 -0400 Subject: [PATCH 01/16] WIP --- shared/common-adapters/plain-input.desktop.js | 5 +- shared/common-adapters/plain-input.js.flow | 1 + shared/common-adapters/plain-input.native.js | 41 ++++++++++- shared/common-adapters/plain-input.stories.js | 73 ++++++++++++++++++- 4 files changed, 113 insertions(+), 7 deletions(-) diff --git a/shared/common-adapters/plain-input.desktop.js b/shared/common-adapters/plain-input.desktop.js index aaae6d37134d..c8da2d113db6 100644 --- a/shared/common-adapters/plain-input.desktop.js +++ b/shared/common-adapters/plain-input.desktop.js @@ -2,6 +2,7 @@ import * as React from 'react' import {getStyle as getTextStyle} from './text.desktop' import {collapseStyles, globalColors, styleSheetCreate, platformStyles} from '../styles' +import {pick} from 'lodash-es' import type {_StylesDesktop} from '../styles/css' import type {InternalProps, TextInfo} from './plain-input' @@ -129,6 +130,7 @@ class PlainInput extends React.PureComponent { _getCommonProps = () => { let commonProps: any = { + ...pick(this.props, ['maxLength', 'value']), // Props we should only passthrough if supplied autoFocus: this.props.autoFocus, className: this.props.className, onBlur: this._onBlur, @@ -145,9 +147,6 @@ class PlainInput extends React.PureComponent { if (this.props.disabled) { commonProps.readOnly = 'readonly' } - if (this.props.maxLength) { - commonProps.maxlength = this.props.maxLength - } return commonProps } diff --git a/shared/common-adapters/plain-input.js.flow b/shared/common-adapters/plain-input.js.flow index 2345cb612702..00828c437f66 100644 --- a/shared/common-adapters/plain-input.js.flow +++ b/shared/common-adapters/plain-input.js.flow @@ -38,6 +38,7 @@ export type Props = { style?: StylesCrossPlatform, textType?: TextType, type?: 'password' | 'text' | 'number', + value?: string, // Makes this a controlled input when passed /* Platform discrepancies */ // Maps to onSubmitEditing on native diff --git a/shared/common-adapters/plain-input.native.js b/shared/common-adapters/plain-input.native.js index d773b3281666..b35ef019b57c 100644 --- a/shared/common-adapters/plain-input.native.js +++ b/shared/common-adapters/plain-input.native.js @@ -4,8 +4,10 @@ import {getStyle as getTextStyle} from './text' import {NativeTextInput} from './native-wrappers.native' import {collapseStyles, globalColors, styleSheetCreate} from '../styles' import {isIOS} from '../constants/platform' +import {checkTextInfo} from './input.shared' +import {pick} from 'lodash-es' -import type {InternalProps} from './plain-input' +import type {InternalProps, TextInfo} from './plain-input' type ContentSizeChangeEvent = {nativeEvent: {contentSize: {width: number, height: number}}} @@ -26,8 +28,10 @@ class PlainInput extends Component { focused: false, height: null, } - _input: ?NativeTextInput + _lastNativeText: ?string // sourced from onChangeText + _lastNativeSelection: ?{start: number, end: number} + _setInputRef = (ref: ?NativeTextInput) => { this._input = ref } @@ -38,6 +42,32 @@ class PlainInput extends Component { this._input && this._input.setNativeProps(nativeProps) } + transformText = (fn: TextInfo => TextInfo) => { + const currentTextInfo = { + text: this._lastNativeText || '', + selection: this._lastNativeSelection || {start: 0, end: 0}, + } + const newTextInfo = fn(currentTextInfo) + checkTextInfo(newTextInfo) + this.setNativeProps({text: newTextInfo.text, selection: newTextInfo.selection}) + this._lastNativeText = newTextInfo.text + this._lastNativeSelection = newTextInfo.selection + } + + _onChangeText = (t: string) => { + this._lastNativeText = t + this.props.onChangeText && this.props.onChangeText(t) + } + + _onSelectionChange = (event: {nativeEvent: {selection: {start: number, end: number}}}) => { + let {start: _start, end: _end} = event.nativeEvent.selection + // Work around Android bug which sometimes puts end before start: + // https://github.com/facebook/react-native/issues/18579 . + const start = Math.min(_start, _end) + const end = Math.max(_start, _end) + this._lastNativeSelection = {start, end} + } + _onContentSizeChange = (event: ContentSizeChangeEvent) => { if (this.props.multiline) { let height = event.nativeEvent.contentSize.height @@ -116,6 +146,7 @@ class PlainInput extends Component { _getProps = () => { const common: any = { + ...pick(this.props, ['maxLength', 'value']), // Props we should only passthrough if supplied autoCapitalize: this.props.autoCapitalize || 'none', autoCorrect: !!this.props.autoCorrect, autoFocus: this.props.autoFocus, @@ -123,9 +154,10 @@ class PlainInput extends Component { keyboardType: this.props.keyboardType, multiline: false, onBlur: this._onBlur, - onChangeText: this.props.onChangeText, + onChangeText: this._onChangeText, onEndEditing: this.props.onEndEditing, onFocus: this._onFocus, + onSelectionChange: this._onSelectionChange, onSubmitEditing: this.props.onEnterKeyDown, placeholder: this.props.placeholder, placeholderTextColor: this.props.placeholderColor || globalColors.black_40, @@ -151,6 +183,9 @@ class PlainInput extends Component { render = () => { const props = this._getProps() + if (props.value) { + this._lastNativeText = props.value + } return } } diff --git a/shared/common-adapters/plain-input.stories.js b/shared/common-adapters/plain-input.stories.js index e0185a6392bf..24a5fb52aa78 100644 --- a/shared/common-adapters/plain-input.stories.js +++ b/shared/common-adapters/plain-input.stories.js @@ -2,9 +2,11 @@ import * as React from 'react' import PlainInput from './plain-input' import Box, {Box2} from './box' +import Button from './button' +import ButtonBar from './button-bar' import Text from './text' import {action, storiesOf} from '../stories/storybook' -import {globalColors} from '../styles' +import {globalColors, globalMargins} from '../styles' const commonProps = { onBlur: action('onBlur'), @@ -17,6 +19,72 @@ const commonProps = { style: {borderWidth: 1, borderStyle: 'solid', borderColor: globalColors.black_10}, } +type ControlledInputState = {[key: string]: string} +class ControlledInputPlayground extends React.Component<{}, ControlledInputState> { + state = {} + mutationTarget = React.createRef() + _onChangeText = (valueKey: string) => (t: string) => this.setState({[valueKey]: t}) + _onChangeSelection = () => { + if (this.mutationTarget.current) { + const input = this.mutationTarget.current + input.transformText(ti => ({text: ti.text, selection: {start: 2, end: 5}})) + input.focus() + } + } + _onTestCrossSelection = () => { + if (this.mutationTarget.current) { + const input = this.mutationTarget.current + input.transformText(ti => ({text: '5char', selection: {start: 0, end: 0}})) + input.transformText(ti => ({text: 'a lot more than 5 characters', selection: {start: 3, end: 5}})) + } + } + render() { + return ( + + Basic controlled inputs + + + + + Live mutations + + +