From e020d693f6835df03ba9fc70fc833722f18924a7 Mon Sep 17 00:00:00 2001 From: lulutia Date: Thu, 16 Mar 2017 11:52:23 +0800 Subject: [PATCH] feat(Input): Add Real Input --- components/Input/.npmignore | 9 + components/Input/Input.js | 248 ++++++++++++++++++ components/Input/InputStyle.js | 90 +++++++ components/Input/README.md | 29 ++ components/Input/__tests__/Input.test.js | 203 ++++++++++++++ .../__snapshots__/Input.test.js.snap | 71 +++++ .../__snapshots__/index.test.js.snap | 71 +++++ components/Input/example/LICENSE | 21 ++ components/Input/example/index.android.js | 82 ++++++ components/Input/package.json | 24 ++ 10 files changed, 848 insertions(+) create mode 100644 components/Input/.npmignore create mode 100644 components/Input/Input.js create mode 100644 components/Input/InputStyle.js create mode 100644 components/Input/README.md create mode 100644 components/Input/__tests__/Input.test.js create mode 100644 components/Input/__tests__/__snapshots__/Input.test.js.snap create mode 100644 components/Input/__tests__/__snapshots__/index.test.js.snap create mode 100644 components/Input/example/LICENSE create mode 100644 components/Input/example/index.android.js create mode 100644 components/Input/package.json diff --git a/components/Input/.npmignore b/components/Input/.npmignore new file mode 100644 index 0000000..4a28fd0 --- /dev/null +++ b/components/Input/.npmignore @@ -0,0 +1,9 @@ +example/ +__tests__/ +.* +components/ +coverage/ +index.android.js +index.ios.js +android/ +ios/ diff --git a/components/Input/Input.js b/components/Input/Input.js new file mode 100644 index 0000000..6285184 --- /dev/null +++ b/components/Input/Input.js @@ -0,0 +1,248 @@ +import React, {Component} from 'react'; +import { + View, + TextInput, + Text, + Animated, + Platform, + TouchableWithoutFeedback +} from 'react-native'; +import Style from './InputStyle'; + +import StyleSheet from 'react-native-stylesheet-xg'; + +/** RichInput + * @example + * {}}}/> + * @props + * @extends TextInput + * label + * tips + * error + */ + +const ANIMATION_TIME = 300; +// 末尾留余高度 +const PLUS_HEIGHT = StyleSheet.r(20); +// 最小高度,末尾留余高度,防抖动 +const MULTI_MIN_HEIGHT = StyleSheet.r(40) + PLUS_HEIGHT; +// 校正 +const REVISES = { + ios: StyleSheet.r(20), + android: 0 +}; + +class Input extends Component { + + // 构造 + constructor(props) { + super(props); + // 初始状态 + this.state = { + isFocus: false, + multiHeight: MULTI_MIN_HEIGHT + REVISES[Platform.OS], + animatedFactor: new Animated.Value(0.0001) + }; + + this.lineAnimated = { + transform: [ + {scaleX: this.state.animatedFactor} + ] + }; + + this.focusInit = false; + + this.renderLabel = this.renderLabel.bind(this); + this.onFocus = this.onFocus.bind(this); + this.onBlur = this.onBlur.bind(this); + this.onChange = this.onChange.bind(this); + this.onPressLabel = this.onPressLabel.bind(this); + this.getContentSize = this.getContentSize.bind(this); + this.onContentSizeChange = this.onContentSizeChange.bind(this); + } + + onFocus() { + const {onFocus} = this.props; + + this.focusInit = true; + this.setState({ + isFocus: true + }); + + Animated.timing( + this.state.animatedFactor, + { + duration: ANIMATION_TIME, + toValue: 1 + } + ).start(); + + if (typeof onFocus === 'function') { + onFocus(); + } + } + + onBlur() { + const {onBlur} = this.props; + + this.setState({ + isFocus: false + }); + + Animated.timing( + this.state.animatedFactor, + { + duration: ANIMATION_TIME, + toValue: 0.0001 + } + ).start(); + + if (typeof onBlur === 'function') { + onBlur(); + } + } + + getContentSize(event) { + if (this.props.multiline) { + const newHeight = Math.max( + MULTI_MIN_HEIGHT, + event.nativeEvent.contentSize.height + PLUS_HEIGHT + REVISES[Platform.OS] + ); + + + if (newHeight !== this.state.multiHeight) { + this.setState({multiHeight: newHeight}); + } + } + } + + onChange(event) { + const {onChange} = this.props; + + this.getContentSize(event); + + if (typeof onChange === 'function') { + onChange(event); + } + } + + onContentSizeChange(event) { + const {onContentSizeChange} = this.props; + + this.getContentSize(event); + + if (typeof onContentSizeChange === 'function') { + onContentSizeChange(event); + } + } + + onPressLabel() { + const {editable, readOnly} = this.props; + + if (editable && !readOnly) { + this.input.focus(); + } + } + + renderLabel() { + const {label, labelStyle, multiline, tips, required} = this.props; + const isColumnMode = multiline || !!tips || (label ? label.length > 5 : false); + + if (typeof label === 'undefined') { + return; + } + + return ( + + + + {required && *}{label} + + {!!tips && {tips}} + + + ); + } + + render() { + const { + editable, + multiline, + error, + wrapperStyle, + focusStyle, + disabledStyle, + errorStyle, + initJudge, + readOnly, + label, + tips + } = this.props; + const {isFocus} = this.state; + const isColumnMode = multiline || !!tips || (label ? label.length > 5 : false); + + return ( + + {this.renderLabel()} + this.input = input} + style={[ + Style.input, isColumnMode && Style.columnModeInput, + this.props.style, + multiline && Style.multiInput, + multiline && {height: this.state.multiHeight} + ]} + onContentSizeChange={this.onContentSizeChange} + onChange={this.onChange} + onFocus={this.onFocus} + onBlur={this.onBlur} + /> + + {error && } + + ); + } +} + +Input.defaultProps = { + editable: true, + multiline: false, + error: false, + initJudge: true, + readOnly: false +}; + +Input.propTypes = { + ...TextInput.propTypes, + label: React.PropTypes.string, + defaultValue: React.PropTypes.string, + editable: React.PropTypes.bool, + multiline: React.PropTypes.bool, + error: React.PropTypes.bool, + required: React.PropTypes.bool, + tips: React.PropTypes.string, + wrapperStyle: React.PropTypes.object, + focusStyle: React.PropTypes.object, + disabledStyle: React.PropTypes.object, + errorStyle: React.PropTypes.object, + labelStyle: React.PropTypes.object, + initJudge: React.PropTypes.bool, + readOnly: React.PropTypes.bool +}; + + +export default Input; diff --git a/components/Input/InputStyle.js b/components/Input/InputStyle.js new file mode 100644 index 0000000..facc05b --- /dev/null +++ b/components/Input/InputStyle.js @@ -0,0 +1,90 @@ +import StyleSheet from 'react-native-stylesheet-xg'; + +const Style = StyleSheet.create({ + inputCon: { + flexDirection: 'row', + alignItems: 'center', + height: 45, + paddingLeft: 10, + paddingRight: 10, + borderBottomWidth: 1, + borderBottomColor: '#e5e5e5' + }, + multiInputCon: { + height: null, + flexDirection: 'column', + alignSelf: 'stretch' + }, + focus: { + borderColor: '#ff9900' + }, + error: { + borderColor: '#ec5330' + }, + disabled: { + backgroundColor: '#e5e5e5' + }, + input: { + flexGrow: 1, + height: 44, + padding: 0, + color: '#333', + fontSize: 14 + }, + columnModeInput: { + marginTop: -10, + marginBottom: -5, + alignSelf: 'stretch' + }, + multiInput: { + marginBottom: -20, + textAlignVertical: 'top', + alignSelf: 'stretch', + android: { + paddingTop: 8 + }, + ios: { + marginTop: -3, + marginBottom: -28 + } + }, + labelCon: { + flexDirection: 'row', + width: 90, + height: 44, + alignItems: 'center', + justifyContent: 'flex-start' + }, + columnModeLabelCon: { + width: null, + height: 35, + alignSelf: 'stretch' + }, + label: { + fontSize: 14, + color: '#666' + }, + lineBottom: { + position: 'absolute', + height: 2, + bottom: -1, + left: 0, + right: 0, + backgroundColor: '#ff9900' + }, + lineError: { + backgroundColor: '#ec5330' + }, + tips: { + marginTop: 1, + marginLeft: 5, + fontSize: 12, + color: '#06c1ae' + }, + required: { + fontSize: 14, + color: '#ec5330' + } +}); + +export default Style; diff --git a/components/Input/README.md b/components/Input/README.md new file mode 100644 index 0000000..a25db1b --- /dev/null +++ b/components/Input/README.md @@ -0,0 +1,29 @@ +### The react-native-input-xg +* react native Input component for both Android and iOS based on pure JavaScript + +### Main +* This component provide some more functions besides the basic input RN provided + +### Properties +* Besides the basic properties RN provided, we also provide belows: + +![image](https://raw.githubusercontent.com/lulutia/images/master/react-native-components/Screen-Capture-39.gif) + +### Properties + +| Prop | Default | Type | Description | +| :------------ |:---------------:| :---------------:| :-----| +| label | undefined | `string` | Specify the label of the input | +| defaultValue | undefined | `string` | Specify the default value of the input | +| editable | true | `bool` | if you can edit the input| +| multiline | false | `bool` | if the input support the multiline| +| error | false | `bool` | for you to judge if the content is wrong | +| required| false | `bool` | give you an indicate to show this one is a must | +| tips | undefined | `string` | give some more explanation | +| wrapperStyle | - | `style` | Specify the wrapper style | +| focusStyle | - | `style` | Specify the style when focus | +| disabledStyle | - | `style` | Specify the style when disabled| +| errorStyle | - | `style` | Specify the style when there's some error| +| labelStyle | - | `style` | Specify the label style| +| initJudge | true | `bool` | if judge error when init| +| readOnly | false | `bool` | you can only see but can not operate| diff --git a/components/Input/__tests__/Input.test.js b/components/Input/__tests__/Input.test.js new file mode 100644 index 0000000..5fbfccb --- /dev/null +++ b/components/Input/__tests__/Input.test.js @@ -0,0 +1,203 @@ +import {jsdom} from 'jsdom'; +global.document = jsdom(''); +global.window = document.defaultView; +import React, {Component, propTypes} from 'react'; +import { + Platform, + View, + Text, + TextInput, + Animated, + TouchableWithoutFeedback +} from 'react-native'; +import renderer from 'react-test-renderer'; +import {shallow, mount} from 'enzyme'; +import Input from '../Input'; + +describe('test render snapshot', () => { + const tree = renderer.create( + + ).toJSON(); + expect(tree).toMatchSnapshot(); +}); + +describe('test node', () => { + it('check node number', () => { + const wrapper = mount(); + expect(wrapper.find(Animated.View).length).toEqual(1); + expect(wrapper.find(TextInput).length).toEqual(1); + }); +}); + +describe('test props', () => { + it('check default props', () => { + const wrapper = mount( + ); + expect(wrapper.prop('multiline')).toEqual(false); + expect(wrapper.prop('editable')).toEqual(true); + expect(wrapper.prop('initJudge')).toEqual(true); + expect(wrapper.prop('error')).toEqual(false); + expect(wrapper.prop('readOnly')).toEqual(false); + }); + + it('check props', () => { + const wrapper1 = mount( + ); + expect(wrapper1.prop('label')).toEqual('age'); + expect(wrapper1.prop('tips')).toEqual("can't fix"); + expect(wrapper1.prop('defaultValue')).toEqual('12'); + }); +}); + +describe('test branch', () => { + it('check focuse', () => { + const wrapper = mount( + ); + + expect(wrapper.find(TextInput).length).toEqual(1); + wrapper.find(TextInput).simulate('focus'); + expect(wrapper.state('isFocus')).toEqual(true); + }); + + it('check blur', () => { + const wrapper6 = mount( + + ); + wrapper6.find(TextInput).simulate('blur'); + expect(wrapper6.state('isFocus')).toEqual(false); + }); + it('check require', () => { + const wrapper1 = mount( + + ); + const wrapper2 = mount( + + ); + expect(wrapper1.find(Text).length).toEqual(1); + expect(wrapper2.find(Text).length).toEqual(2); + }); + + it('check tips', () => { + const wrapper3 = mount( + + ); + const wrapper4 = mount( + + ); + expect(wrapper3.find(Text).length).toEqual(1); + expect(wrapper4.find(Text).length).toEqual(2); + }); + + it('check error', () => { + const wrapper5 = mount( + + ); + expect(wrapper5.find(View).length).toEqual(4); + }); +}); + +describe('check function', () => { + it('check onFocus', () => { + let errorT = false; + const wrapper = mount( + {errorT = true;}} + /> + ); + wrapper.find(TextInput).simulate('focus'); + expect(errorT).toEqual(true); + }); + + it('check onBlur', () => { + let temp = false; + const wrapper1 = mount( + {temp = true;}} + /> + ); + wrapper1.find(TextInput).simulate('blur'); + expect(temp).toEqual(true); + }); + + it('check onChange', () => { + let test = ''; + const wrapper2 = mount( + {test = 'finish';}} + /> + ); + wrapper2.find(TextInput).simulate('change'); + expect(test).toEqual('finish'); + }); + + it('check onContentSizeChange', () => { + let temp = false; + const wrapper3 = shallow( + {temp = true;}} + /> + ); + let instance = wrapper3.instance(); + instance.onContentSizeChange(); + expect(temp).toEqual(true); + }); + + it('check onPressLabel', () => { + const wrapper4 = shallow( + + ); + const wrapper5 = shallow( + + ); + let instance = wrapper4.instance(); + instance.onPressLabel(); + expect(wrapper4.state('isFocus')).toEqual(false); + let instance1 = wrapper5.instance(); + instance1.onPressLabel(); + expect(wrapper5.state('isFocus')).toEqual(false); + }); + + it('check getContentSize', () => { + const wrapper6 = shallow( + + ); + let instance = wrapper6.instance(); + instance.setState({multiHeight: 10}); + instance.getContentSize({nativeEvent: {contentSize: {height: 20}}}); + expect(wrapper6.state('multiHeight')).toBeGreaterThan(10); + }); +}); diff --git a/components/Input/__tests__/__snapshots__/Input.test.js.snap b/components/Input/__tests__/__snapshots__/Input.test.js.snap new file mode 100644 index 0000000..b87fcb6 --- /dev/null +++ b/components/Input/__tests__/__snapshots__/Input.test.js.snap @@ -0,0 +1,71 @@ +exports[`undefined 1`] = ` + + + + +`; diff --git a/components/Input/__tests__/__snapshots__/index.test.js.snap b/components/Input/__tests__/__snapshots__/index.test.js.snap new file mode 100644 index 0000000..b87fcb6 --- /dev/null +++ b/components/Input/__tests__/__snapshots__/index.test.js.snap @@ -0,0 +1,71 @@ +exports[`undefined 1`] = ` + + + + +`; diff --git a/components/Input/example/LICENSE b/components/Input/example/LICENSE new file mode 100644 index 0000000..ba73a3b --- /dev/null +++ b/components/Input/example/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2017 鲜果FE + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/components/Input/example/index.android.js b/components/Input/example/index.android.js new file mode 100644 index 0000000..7aad44f --- /dev/null +++ b/components/Input/example/index.android.js @@ -0,0 +1,82 @@ +/** + * Sample React Native App + * https://github.com/facebook/react-native + * @flow + */ + +import React, {Component} from 'react'; +import { + AppRegistry, + StyleSheet, + Text, + View +} from 'react-native'; +import Input from '../Input'; + +export default class input extends Component { + + constructor(props) { + super(props); + this.state = { + test1: '', + test2: '12', + test3: '', + test4: '', + test5: '', + test6: '', + test7: '' + }; + } + render() { + return ( + + { + console.debug('trsy'); + this.setState({test1: text}); + }}/> + + + { + this.setState({test3: text}); + }}/> + { + this.setState({test4: text}); + }}/> + { + this.setState({test5: text}); + }}/> + { + this.setState({test6: text}); + }}/> + { + this.setState({test7: text}); + }}/> + + ); + } +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: '#F5FCFF' + }, + welcome: { + fontSize: 20, + textAlign: 'center', + margin: 10 + }, + instructions: { + textAlign: 'center', + color: '#333333', + marginBottom: 5 + } +}); + +AppRegistry.registerComponent('input', () => input); diff --git a/components/Input/package.json b/components/Input/package.json new file mode 100644 index 0000000..5cbc2f2 --- /dev/null +++ b/components/Input/package.json @@ -0,0 +1,24 @@ +{ + "name": "react-native-input-xg", + "version": "0.0.1", + "description": "react native input component for both Android and IOS, useing pure JavaScript", + "main": "Datepicker.js", + "repository": { + "type": "git", + "url": "git+https://github.com/xgfe/react-native-ui-xg.git" + }, + "keywords": [ + "react-native", + "input", + "react-native-input" + ], + "author": "xgfe", + "license": "MIT", + "bugs": { + "url": "https://github.com/xgfe/react-native-ui-xg/issues" + }, + "homepage": "https://github.com/xgfe/react-native-ui-xg/components/Input#readme", + "dependencies": { + "react-native-stylesheet-xg": "^1.1.0" + } +}