From b443cb63e21b696f8cd246ef9c5e57c1dbb90514 Mon Sep 17 00:00:00 2001 From: Ankit Aggarwal Date: Fri, 19 Jan 2018 14:34:31 +0530 Subject: [PATCH] feat(TextInput): add a native TextInput component --- src/components/Spacer/native/Spacer.js | 6 +- src/components/TextInput/native/TextInput.js | 232 +++++++++++++++++++ src/components/TextInput/native/index.js | 1 + src/native.js | 2 + storybook/native/stories.js | 116 +++++++++- 5 files changed, 344 insertions(+), 13 deletions(-) create mode 100644 src/components/TextInput/native/TextInput.js create mode 100644 src/components/TextInput/native/index.js diff --git a/src/components/Spacer/native/Spacer.js b/src/components/Spacer/native/Spacer.js index 768f51a..efe3aba 100644 --- a/src/components/Spacer/native/Spacer.js +++ b/src/components/Spacer/native/Spacer.js @@ -1,7 +1,11 @@ +import React from 'react'; import PropTypes from 'prop-types'; import styled from 'styled-components/native'; -const Spacer = styled.View` +const Spacer = styled(({ + children, + ...props +}) => React.cloneElement(children, props))` ${(p) => p.margin || p.margin === 0 ? `margin: ${p.theme.px(p.margin)} !important;` : ''} ${(p) => p.padding || p.padding === 0 ? `padding: ${p.theme.px(p.padding)} !important;` : ''} ${(p) => p.maxWidth ? `max-width: ${p.maxWidth} !important;` : ''} diff --git a/src/components/TextInput/native/TextInput.js b/src/components/TextInput/native/TextInput.js new file mode 100644 index 0000000..1f7bc9f --- /dev/null +++ b/src/components/TextInput/native/TextInput.js @@ -0,0 +1,232 @@ +import React, { Component } from 'react'; +import { Animated } from 'react-native'; +import PropTypes from 'prop-types'; +import styled, { withTheme } from 'styled-components/native'; +import Text from '../../Text/native'; +import Spacer from '../../Spacer/native'; + +const labelAnimationDuration = 300; + +const modifierColor = (focused = 'green', valid = 'greyLight', invalid = 'red', disabled = 'grey') => (props) => { + if (props.isFocused) return props.theme.color[focused]; + if (props.disabled) return props.theme.color[disabled]; + return props.error ? props.theme.color[invalid] : props.theme.color[valid]; +}; + +const labelColor = (props) => { + if (props.isFocused) return props.theme.color.green; + return props.error && !props.isEmptyInput ? props.theme.color.red : props.theme.color.grey; +}; + +const InputContainerWrapper = styled.View` + padding: ${(p) => p.theme.px([2, 0])}; +`; + +// TODO: create and use position component to create label component +const Label = Text.extend` + position: absolute; + top: ${(p) => p.theme.px(1.3)}; + left: ${(p) => p.theme.px(1.5)}; + font-size: ${(p) => p.theme.fontSize.xxs}; + color: ${labelColor}; + background-color: ${(p) => p.theme.color.transparent}; +`; + +const AnimatedLabel = Animated.createAnimatedComponent(Label); + +const InputContainer = styled.View` + position: relative; + flex-direction: row; + align-items: center; + background: ${modifierColor('transparent', 'transparent', 'redLighter', 'transparent')}; + border-color: ${modifierColor()}; + border-width: 1; + border-radius: ${(p) => p.theme.borderRadius}; + height: ${(p) => p.theme.px(6.75)}; +`; + +const RelativeFlexView = styled.View` + position: relative; + flex: 1; +`; + +const Input = styled.TextInput` + flex: 1; + padding: ${(p) => p.theme.px([2.1, 0, 0, 1.5])}; + font-size: ${(p) => p.theme.fontSize.s}; + color: ${(p) => p.theme.color.greyDarker}; + height: ${(p) => p.theme.px(6.75)}; +`; + +class TextInput extends Component { + constructor(props, { formik }) { + super(props); + const { name, theme } = this.props; + this.state = { + labelTranslateValue: formik && formik.values[name] ? + new Animated.Value(0) : new Animated.Value(theme.pxScale), + isFocused: false, + }; + } + + onFocus = () => { + const { labelTranslateValue, isFocused } = this.state; + Animated.timing(labelTranslateValue, { + toValue: 0, + duration: labelAnimationDuration, + }).start(); + if (!isFocused) { + this.setState({ isFocused: true }); + } + }; + + getIconComponent = (icon, isFocused) => { + if (icon === null) { + return null; + } + if (React.isValidElement(icon)) { + return icon; + } + return icon.showOnlyOnFocus && !isFocused ? null : icon.component; + }; + + handleTextChange = (value) => { + const { name } = this.props; + const { formik } = this.context; + if (formik) { + formik.setFieldValue(name, value); + } + }; + + handleBlur = () => { + const { name, theme } = this.props; + const { formik } = this.context; + const { labelTranslateValue, isFocused } = this.state; + let value = ''; + if (formik) { + formik.setFieldTouched(name, true); + value = formik.values[name]; + } + if (!value) { + Animated.timing(labelTranslateValue, { + toValue: theme.pxScale, + duration: labelAnimationDuration, + }).start(); + } + if (isFocused) { + this.setState({ isFocused: false }); + } + }; + + render() { + const { + name, + label, + placeholder, + error: errorMessage, + theme, + leftIcon, + rightIcon, + ...props + } = this.props; + const { formik } = this.context; + const { labelTranslateValue, isFocused } = this.state; + const inputProps = { + name, + onChangeText: this.handleTextChange, + onBlur: this.handleBlur, + onFocus: this.onFocus, + }; + let error = errorMessage; + if (formik) { + inputProps.value = formik.values[name]; + error = formik.touched[name] && formik.errors[name]; + } + const labelFontSize = labelTranslateValue.interpolate({ + inputRange: [0, theme.pxScale], + outputRange: [parseInt(theme.fontSize.xxs, 10), parseInt(theme.fontSize.s, 10)], + extrapolate: 'clamp', + }); + + return ( + + + {this.getIconComponent(leftIcon, isFocused)} + + + {label} + + + + {this.getIconComponent(rightIcon, isFocused)} + + { + error ? ( + + {error} + + ) : null + } + + ); + } +} + +TextInput.propTypes = { + ...Input.propTypes, + name: PropTypes.string, + label: PropTypes.string, + placeholder: PropTypes.string, + error: PropTypes.string, + theme: PropTypes.object, + leftIcon: PropTypes.oneOfType([ + PropTypes.node, + PropTypes.shape({ + component: PropTypes.node, + showOnlyOnFocus: PropTypes.bool, + }), + ]), + rightIcon: PropTypes.oneOfType([ + PropTypes.node, + PropTypes.shape({ + component: PropTypes.node, + showOnlyOnFocus: PropTypes.bool, + }), + ]), +}; + +TextInput.defaultProps = { + name: '', + label: '', + placeholder: '', + error: '', + leftIcon: null, + rightIcon: null, +}; + +TextInput.contextTypes = { + formik: PropTypes.object, +}; + +export default withTheme(TextInput); diff --git a/src/components/TextInput/native/index.js b/src/components/TextInput/native/index.js new file mode 100644 index 0000000..8a006c7 --- /dev/null +++ b/src/components/TextInput/native/index.js @@ -0,0 +1 @@ +export default from './TextInput'; diff --git a/src/native.js b/src/native.js index b5580e1..76443cc 100644 --- a/src/native.js +++ b/src/native.js @@ -1,3 +1,5 @@ export Button from './components/Button/native'; export Text from './components/Text/native'; +export Form from './components/Form'; +export TextInput from './components/TextInput/native'; export theme from './theme'; diff --git a/storybook/native/stories.js b/storybook/native/stories.js index ba3b9d2..11a4c70 100644 --- a/storybook/native/stories.js +++ b/storybook/native/stories.js @@ -1,18 +1,25 @@ import React from 'react'; import { storiesOf } from '@storybook/react-native'; +import styled from 'styled-components/native'; +import theme from '../../src/theme'; import Button from '../../src/components/Button/native'; import Text from '../../src/components/Text/native'; -import theme from '../../src/theme'; -import Spacer from '../../src/components/Spacer/native/Spacer'; +import Form from '../../src/components/Form'; +import TextInput from '../../src/components/TextInput/native'; +import Spacer from '../../src/components/Spacer/native'; + +const View = styled.View` + margin: 0; +`; storiesOf('Button', module) - .add('default', () => ( + .add('with text', () => ( )) .add('kinds', () => ( - + - + )) .add('shapes', () => ( - + - + )) .add('sizes', () => ( - + - + )) .add('block', () => (