diff --git a/.babelrc b/.babelrc new file mode 100644 index 00000000..a9ce1369 --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["react-native"] +} diff --git a/.codeclimate.yml b/.codeclimate.yml index b12fee86..05e05cef 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -12,7 +12,7 @@ checks: plugins: eslint: enabled: true - channel: "eslint-5" + channel: "eslint-4" exclude_patterns: - "example/" - "**/test.js" diff --git a/.eslintrc b/.eslintrc index 0c229732..c5360064 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,14 +1,8 @@ { 'parser': 'babel-eslint', - 'env': { - 'es6': true, - 'react-native/react-native': true, - }, - 'plugins': [ 'react', - 'react-native' ], 'extends': [ @@ -16,12 +10,6 @@ 'plugin:react/recommended', ], - 'settings': { - 'react': { - 'version': '16.9', - }, - }, - 'rules': { 'no-unused-vars': ['error', { 'vars': 'all', @@ -29,183 +17,7 @@ 'ignoreRestSiblings': true, }], - 'no-console': ['error'], - - /* - * let foo = [1]; - * let bar = [ - * 1, - * ]; - */ - 'array-bracket-newline': ['error', 'consistent'], - - /* - * let foo = [1, 2, 3]; - */ - 'array-bracket-spacing': ['error', 'never'], - - /* - * if (foo) { bar() } - */ - 'block-spacing': ['error', 'always'], - - /* - * if (foo) { - * bar(); - * } else { - * baz(); - * } - */ - 'brace-style': ['error', '1tbs', { 'allowSingleLine': true }], - - /* - * let foo = [ - * 1, - * ]; - */ 'comma-dangle': ['error', 'always-multiline'], - - /* - * let foo = [1, 2]; - */ - 'comma-spacing': ['error', { 'before': false, 'after': true }], - - /* - * let foo, - * bar, - * baz; - */ - 'comma-style': ['error', 'last'], - - /* - * let foo = bar[bar]; - */ - 'computed-property-spacing': ['error', 'never'], - - /* - * let that = this; - */ - 'consistent-this': ['error', 'that'], - - /* - * call(); - */ - 'func-call-spacing': ['error', 'never'], - - /* - * function bar() { 1; } - * let bar = () => 1; - */ - 'func-style': ['error', 'declaration', { 'allowArrowFunctions': true }], - - /* - * foo(1); - * bar( - * 2, 3 - * ); - */ - 'function-paren-newline': ['error', 'consistent'], - - /* - * () => 1; - */ - 'implicit-arrow-linebreak': ['error', 'beside'], - - /* - * [ - * 1 - * ] - */ - 'indent': ['warn', 2, { 'SwitchCase': 1 }], - - /* - * - */ - 'jsx-quotes': ['error', 'prefer-single'], - - /* - * let foo = { bar: true }; - */ - 'key-spacing': ['error', { 'beforeColon': false, 'afterColon': true }], - - /* - * if (foo) { return; } - */ - 'keyword-spacing': ['error', { 'before': true, 'after': true }], - - 'linebreak-style': ['error', 'unix'], - - 'lines-between-class-members': ['error', 'always'], - - 'max-len': ['warn', { 'code': 100, 'ignoreTrailingComments': true }], - - /* - * foo(1, 2, 3, 4, 5); - */ - 'max-params': ['error', { 'max': 6 }], - - 'max-statements-per-line': ['error', { 'max': 1 }], - - 'new-parens': ['error'], - - 'no-trailing-spaces': ['error'], - - 'no-whitespace-before-property': ['error'], - - /* - * let foo = { bar, baz }; - */ - 'object-curly-newline': ['error', { 'consistent': true }], - 'object-curly-spacing': ['error', 'always'], - 'object-property-newline': ['error', { 'allowAllPropertiesOnSameLine': true }], - - /* - * let foo = 'bar' - * + 'baz'; - */ - 'operator-linebreak': ['error', 'before', { 'overrides': { '?': 'after', ':': 'after' } }], - - /* - * 'bar' - */ - 'quotes': ['warn', 'single'], - - /* - * 1; - */ - 'semi': ['warn', 'always'], - 'semi-spacing': ['error', { 'before': false, 'after': true }], - 'semi-style': ['error', 'last'], - - /* - * () => { - * }; - */ - 'space-before-blocks': ['error', 'always'], - - /* - * foo(1, 2, 3); - */ - 'space-before-function-paren': ['error', { - 'anonymous': 'always', - 'named': 'never', - 'asyncArrow': 'always', - }], - - /* - * foo('bar'); - */ - 'space-in-parens': ['error', 'never'], - - /* - * switch (a) { - * case 0: break; - * } - */ - 'switch-colon-spacing': ['error', { 'after': true, 'before': false }], - - 'react-native/no-unused-styles': ['warn'], - 'react-native/no-inline-styles': ['warn'], }, } diff --git a/.npmignore b/.npmignore new file mode 100644 index 00000000..44ce6534 --- /dev/null +++ b/.npmignore @@ -0,0 +1,2 @@ +example +coverage diff --git a/.travis.yml b/.travis.yml index 6953d40c..7a56d2a5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,3 @@ -os: osx language: node_js - node_js: - "stable" diff --git a/index.js b/index.js index 60c77f35..bb8fd6ee 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,3 @@ import TextField from './src/components/field'; -import FilledTextField from './src/components/field-filled'; -import OutlinedTextField from './src/components/field-outlined'; -export { TextField, FilledTextField, OutlinedTextField }; +export { TextField }; diff --git a/package.json b/package.json index 1e63b0fb..cc741a88 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-material-textfield", - "version": "0.16.1", + "version": "0.12.0", "license": "BSD-3-Clause", "author": "Alexander Nazarov ", @@ -21,7 +21,6 @@ ], "main": "index.js", - "files": ["index.js", "src/*", "license.txt", "readme.md", "changelog.md"], "repository": { "type": "git", @@ -33,28 +32,25 @@ }, "peerDependencies": { - "react": ">=16.3.0", - "react-native": ">=0.55.0" + "react": "*", + "react-native": "*" }, "devDependencies": { - "@babel/core": "^7.5.5", - "@babel/runtime": "^7.5.5", - "babel-eslint": "^10.0.0", - "babel-jest": "^24.9.0", - "eslint": "^6.5.0", - "eslint-plugin-react": "^7.16.0", - "eslint-plugin-react-native": "^3.7.0", - "jest": "^24.9.0", - "metro-react-native-babel-preset": "^0.56.0", - "react": "16.10.2", - "react-native": "0.61.2", - "react-test-renderer": "16.10.2" + "babel-eslint": "^8.0.0", + "babel-jest": "^22.0.0", + "babel-preset-react-native": "4.0.0", + "eslint": "^4.6.0", + "eslint-plugin-react": "^7.0.0", + "jest": "^22.0.0", + "react": "16.2.0", + "react-native": "0.52.0", + "react-test-renderer": "16.2.0" }, "scripts": { - "test": "eslint index.js src && jest --silent", - "lint": "eslint index.js src example/app.js", + "test": "npm run lint && npm run jest -- --silent", + "lint": "eslint src example/app.js", "jest": "jest" }, diff --git a/readme.md b/readme.md index 4083fb2f..55d215e1 100644 --- a/readme.md +++ b/readme.md @@ -28,12 +28,10 @@ Material texfield with consistent behaviour on iOS and Android * Animated state transitions (normal, focused and errored) * Customizable font size, colors and animation duration * Disabled state (with dotted underline) -* Outlined and filled fields -* Masked input support * Multiline text input * Character counter * Prefix and suffix -* Accessory views +* Accessory view * Helper text * RTL support * Pure javascript implementation @@ -48,33 +46,21 @@ npm install --save react-native-material-textfield ```javascript import React, { Component } from 'react'; -import { - TextField, - FilledTextField, - OutlinedTextField, -} from 'react-native-material-textfield'; +import { TextField } from 'react-native-material-textfield'; class Example extends Component { - fieldRef = React.createRef(); - - onSubmit = () => { - let { current: field } = this.fieldRef; - - console.log(field.value()); - }; - - formatText = (text) => { - return text.replace(/[^+\d]/g, ''); + state = { + phone: '', }; render() { + let { phone } = this.state; + return ( - this.setState({ phone }) } /> ); } @@ -87,7 +73,11 @@ class Example extends Component { :--------------------- |:------------------------------------------- | --------:|:------------------ textColor | Text input color | String | rgba(0, 0, 0, .87) fontSize | Text input font size | Number | 16 + titleFontSize | Text field title and error fontSize | Number | 12 labelFontSize | Text field label font size | Number | 12 + labelHeight | Text field label base height | Number | 32 + labelPadding | Text field label base padding | Number | 4 + inputContainerPadding | Text field input container base padding | Number | 8 lineWidth | Text field underline width | Number | 0.5 activeLineWidth | Text field active underline width | Number | 2 disabledLineWidth | Text field disabled underline width | Number | 1 @@ -99,62 +89,34 @@ class Example extends Component { suffix | Text field suffix text | String | - error | Text field error text | String | - errorColor | Text field color for errored state | String | rgb(213, 0, 0) - lineType | Text field line type | String | solid disabledLineType | Text field line type in disabled state | String | dotted animationDuration | Text field animation duration in ms | Number | 225 characterRestriction | Text field soft limit for character counter | Number | - disabled | Text field availability | Boolean | false editable | Text field text can be edited | Boolean | true multiline | Text filed multiline input | Boolean | false - contentInset | Layout configuration object | Object | [{...}](#content-inset) - labelOffset | Label position adjustment | Object | [{...}](#label-offset) inputContainerStyle | Style for input container view | Object | - containerStyle | Style for container view | Object | - labelTextStyle | Style for label inner Text component | Object | - titleTextStyle | Style for title inner Text component | Object | - affixTextStyle | Style for affix inner Text component | Object | - - formatText | Input mask callback | Function | - - renderLeftAccessory | Render left input accessory view | Function | - - renderRightAccessory | Render right input accessory view | Function | - + renderAccessory | Render input accessory view | Function | - onChangeText | Change text callback | Function | - onFocus | Focus callback | Function | - onBlur | Blur callback | Function | - -Other [TextInput][rn-textinput] properties will also work. - -### Content Inset - - name | description | Normal | Filled | Outlined -:----- |:--------------------------------- | ------:| ------:| --------: - top | Inset on the top side | 16 | 8 | 0 - left | Inset on the left side | 0 | 12 | 12 - right | Inset on the right side | 0 | 12 | 12 - label | Space between label and TextInput | 4 | 4 | 4 - input | Space between line and TextInput | 8 | 8 | 16 - -### Label Offset - - name | description | Normal | Filled | Outlined -:---- |:------------------------------------ | ------:| ------:| --------: - x0 | Horizontal offset for inactive state | 0 | 0 | 0 - y0 | Vertical offset for inactive state | 0 | -10 | 0 - x1 | Horizontal offset for active state | 0 | 0 | 0 - y1 | Vertical offset for active state | 0 | -2 | -10 +Other [TextInput][rn-textinput] properties will also work ## Methods - name | description | returns -:---------------------- |:----------------------------- | -------: - focus() | Acquire focus | - - blur() | Release focus | - - clear() | Clear text field | - - value() | Get current value | String - isFocused() | Get current focus state | Boolean - isErrored() | Get current error state | Boolean - isRestricted() | Get current restriction state | Boolean - isDefaultVisible() | Get default value visibility | Boolean - isPlaceholderVisible() | Get placeholder visibility | Boolean - setValue() | Set current value | - + name | description | returns +:-------------- |:----------------------------- | -------: + focus() | Acquire focus | - + blur() | Release focus | - + clear() | Clear text field | - + value() | Get current value | String + isFocused() | Get current focus state | Boolean + isRestricted() | Get current restriction state | Boolean ## Example @@ -169,4 +131,4 @@ npm run ios # or npm run android BSD License -Copyright 2017-2019 Alexander Nazarov. All rights reserved. +Copyright 2017 Alexander Nazarov. All rights reserved. diff --git a/src/components/affix/__snapshots__/test.js.snap b/src/components/affix/__snapshots__/test.js.snap index 4e065728..8ae91131 100644 --- a/src/components/affix/__snapshots__/test.js.snap +++ b/src/components/affix/__snapshots__/test.js.snap @@ -1,81 +1,29 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`renders inactive prefix 1`] = ` - - - a - - -`; - -exports[`renders inactive suffix 1`] = ` - - - z - - -`; - exports[`renders prefix 1`] = ` @@ -86,24 +34,28 @@ exports[`renders prefix 1`] = ` exports[`renders suffix 1`] = ` diff --git a/src/components/affix/index.js b/src/components/affix/index.js index 0f85022e..d7cfd41f 100644 --- a/src/components/affix/index.js +++ b/src/components/affix/index.js @@ -7,22 +7,24 @@ import styles from './styles'; export default class Affix extends PureComponent { static defaultProps = { numberOfLines: 1, + + active: false, + focused: false, }; static propTypes = { numberOfLines: PropTypes.number, - style: Animated.Text.propTypes.style, - color: PropTypes.string.isRequired, - fontSize: PropTypes.number.isRequired, + active: PropTypes.bool, + focused: PropTypes.bool, - type: PropTypes - .oneOf(['prefix', 'suffix']) - .isRequired, + type: PropTypes.oneOf(['prefix', 'suffix']).isRequired, + + fontSize: PropTypes.number.isRequired, + baseColor: PropTypes.string.isRequired, + animationDuration: PropTypes.number.isRequired, - labelAnimation: PropTypes - .instanceOf(Animated.Value) - .isRequired, + // style: Animated.Text.propTypes.style, children: PropTypes.oneOfType([ PropTypes.arrayOf(PropTypes.node), @@ -30,20 +32,42 @@ export default class Affix extends PureComponent { ]), }; + constructor(props) { + super(props); + + let { active, focused } = this.props; + + this.state = { + opacity: new Animated.Value((active || focused)? 1 : 0), + }; + } + + componentWillReceiveProps(props) { + let { opacity } = this.state; + let { active, focused, animationDuration } = this.props; + + if ((focused ^ props.focused) || (active ^ props.active)) { + Animated + .timing(opacity, { + toValue: (props.active || props.focused)? 1 : 0, + duration: animationDuration, + }) + .start(); + } + } + render() { - let { labelAnimation, style, children, type, fontSize, color } = this.props; + let { opacity } = this.state; + let { style, children, type, fontSize, baseColor: color } = this.props; let containerStyle = { height: fontSize * 1.5, - opacity: labelAnimation, + opacity, }; let textStyle = { - includeFontPadding: false, - textAlignVertical: 'top', - - fontSize, color, + fontSize, }; switch (type) { diff --git a/src/components/affix/styles.js b/src/components/affix/styles.js index 2ac28a6d..ca4e1fcd 100644 --- a/src/components/affix/styles.js +++ b/src/components/affix/styles.js @@ -3,6 +3,7 @@ import { StyleSheet } from 'react-native'; export default StyleSheet.create({ container: { top: 2, + alignSelf: 'flex-start', justifyContent: 'center', }, }); diff --git a/src/components/affix/test.js b/src/components/affix/test.js index 86d1aa0f..6bfa4430 100644 --- a/src/components/affix/test.js +++ b/src/components/affix/test.js @@ -1,6 +1,5 @@ import 'react-native'; import React from 'react'; -import { Animated } from 'react-native'; import renderer from 'react-test-renderer'; import Affix from '.'; @@ -8,10 +7,9 @@ import Affix from '.'; /* eslint-env jest */ const props = { - color: 'black', fontSize: 16, - - labelAnimation: new Animated.Value(1), + baseColor: 'blue', + animationDuration: 225, }; const prefix = 'a'; @@ -26,19 +24,6 @@ it('renders prefix', () => { .toMatchSnapshot(); }); -it('renders inactive prefix', () => { - let affix = renderer - .create( - - {prefix} - - ) - .toJSON(); - - expect(affix) - .toMatchSnapshot(); -}); - it('renders suffix', () => { let affix = renderer .create({suffix}) @@ -47,16 +32,3 @@ it('renders suffix', () => { expect(affix) .toMatchSnapshot(); }); - -it('renders inactive suffix', () => { - let affix = renderer - .create( - - {suffix} - - ) - .toJSON(); - - expect(affix) - .toMatchSnapshot(); -}); diff --git a/src/components/counter/__snapshots__/test.js.snap b/src/components/counter/__snapshots__/test.js.snap index 71ab773f..815c25cb 100644 --- a/src/components/counter/__snapshots__/test.js.snap +++ b/src/components/counter/__snapshots__/test.js.snap @@ -1,51 +1,69 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`renders when limit is exceeded 1`] = ` - - 2 - / - 1 - + + 2 + / + 1 + + `; exports[`renders when limit is set 1`] = ` - - 1 - / - 1 - + + 1 + / + 1 + + `; diff --git a/src/components/counter/index.js b/src/components/counter/index.js index 35d3264f..ad6e3133 100644 --- a/src/components/counter/index.js +++ b/src/components/counter/index.js @@ -1,6 +1,6 @@ import PropTypes from 'prop-types'; import React, { PureComponent } from 'react'; -import { Text } from 'react-native'; +import { View, Text } from 'react-native'; import styles from './styles'; @@ -9,6 +9,8 @@ export default class Counter extends PureComponent { count: PropTypes.number.isRequired, limit: PropTypes.number, + fontSize: PropTypes.number, + baseColor: PropTypes.string.isRequired, errorColor: PropTypes.string.isRequired, @@ -16,22 +18,23 @@ export default class Counter extends PureComponent { }; render() { - let { count, limit, baseColor, errorColor, style } = this.props; + let { count, limit, baseColor, errorColor, fontSize, style } = this.props; + + let textStyle = { + color: count > limit? errorColor : baseColor, + fontSize, + }; if (!limit) { return null; } - let textStyle = { - color: count > limit? - errorColor: - baseColor, - }; - return ( - - {count} / {limit} - + + + {count} / {limit} + + ); } } diff --git a/src/components/counter/styles.js b/src/components/counter/styles.js index 8eb9cdf4..5a3a718d 100644 --- a/src/components/counter/styles.js +++ b/src/components/counter/styles.js @@ -1,12 +1,13 @@ import { StyleSheet } from 'react-native'; export default StyleSheet.create({ + container: { + paddingVertical: 4, + paddingLeft: 4, + }, + text: { - fontSize: 12, - lineHeight: 16, textAlign: 'right', backgroundColor: 'transparent', - paddingVertical: 2, - marginLeft: 8, }, }); diff --git a/src/components/field/__snapshots__/test.js.snap b/src/components/field/__snapshots__/test.js.snap index 705e7bf3..caf36655 100644 --- a/src/components/field/__snapshots__/test.js.snap +++ b/src/components/field/__snapshots__/test.js.snap @@ -5,52 +5,98 @@ exports[`renders 1`] = ` onResponderRelease={[Function]} onStartShouldSetResponder={[Function]} pointerEvents="auto" + style={undefined} > - + test + + + + + + - test - + /> - - `; -exports[`renders counter 1`] = ` +exports[`renders accessory 1`] = ` - + test + + + + + + + + + - test - + /> - + +`; + +exports[`renders counter 1`] = ` + + + + test + + + + + + + - + + + + + + + + - 4 - / - 10 - + + 4 + / + 10 + + `; @@ -346,52 +540,98 @@ exports[`renders default value 1`] = ` onResponderRelease={[Function]} onStartShouldSetResponder={[Function]} pointerEvents="auto" + style={undefined} > - + test + + + + + + - test - + /> - - `; @@ -505,52 +704,116 @@ exports[`renders disabled value 1`] = ` onResponderRelease={[Function]} onStartShouldSetResponder={[Function]} pointerEvents="none" + style={undefined} > + - + test + + + + + + - test - + /> - - `; @@ -664,52 +886,99 @@ exports[`renders error 1`] = ` onResponderRelease={[Function]} onStartShouldSetResponder={[Function]} pointerEvents="auto" + style={undefined} > - + test + + + + + + - test + message - - - - message - - `; -exports[`renders left accessory 1`] = ` +exports[`renders multiline value 1`] = ` - + test + + + + - + + - test - + /> - - `; -exports[`renders multiline value 1`] = ` +exports[`renders prefix 1`] = ` - + > + test + - test + $ - - - + underlineColorAndroid="transparent" + value="text" + /> - -`; - -exports[`renders prefix 1`] = ` - - - - - - test - + /> - - - $ - - - - `; @@ -1349,52 +1413,98 @@ exports[`renders restriction 1`] = ` onResponderRelease={[Function]} onStartShouldSetResponder={[Function]} pointerEvents="auto" + style={undefined} > - + test + + + + + + - test - + /> - - - - - 4 - / - 2 - + + 4 + / + 2 + + `; -exports[`renders right accessory 1`] = ` +exports[`renders suffix 1`] = ` - + > + test + + - test + .com - - - - - -`; - -exports[`renders suffix 1`] = ` - - - - - - test - + /> - - - - .com - - + /> - `; @@ -1875,52 +1802,98 @@ exports[`renders title 1`] = ` onResponderRelease={[Function]} onStartShouldSetResponder={[Function]} pointerEvents="auto" + style={undefined} > - + test + + + + + + - test - + /> - + Object { + "backgroundColor": "transparent", + "color": "rgba(0, 0, 0, .38)", + "fontSize": 12, + "opacity": 1, + } + } + > + field + - - - field - - `; @@ -2051,52 +1968,98 @@ exports[`renders value 1`] = ` onResponderRelease={[Function]} onStartShouldSetResponder={[Function]} pointerEvents="auto" + style={undefined} > - + test + + + + + + - test - + /> - - `; diff --git a/src/components/field/index.js b/src/components/field/index.js index 281b7451..52c1868a 100644 --- a/src/components/field/index.js +++ b/src/components/field/index.js @@ -8,34 +8,18 @@ import { StyleSheet, Platform, ViewPropTypes, + I18nManager, } from 'react-native'; +import RN from 'react-native/package.json'; + import Line from '../line'; import Label from '../label'; import Affix from '../affix'; import Helper from '../helper'; import Counter from '../counter'; -import styles from './styles'; - -function startAnimation(animation, options, callback) { - Animated - .timing(animation, options) - .start(callback); -} - -function labelStateFromProps(props, state) { - let { placeholder, defaultValue } = props; - let { text, receivedFocus } = state; - - return !!(placeholder || text || (!receivedFocus && defaultValue)); -} - -function errorStateFromProps(props, state) { - let { error } = props; - - return !!error; -} +import styles from './styles.js'; export default class TextField extends PureComponent { static defaultProps = { @@ -47,7 +31,11 @@ export default class TextField extends PureComponent { animationDuration: 225, fontSize: 16, + titleFontSize: 12, labelFontSize: 12, + labelHeight: 32, + labelPadding: 4, + inputContainerPadding: 8, tintColor: 'rgb(0, 145, 234)', textColor: 'rgba(0, 0, 0, .87)', @@ -57,12 +45,10 @@ export default class TextField extends PureComponent { lineWidth: StyleSheet.hairlineWidth, activeLineWidth: 2, - disabledLineWidth: 1, - - lineType: 'solid', - disabledLineType: 'dotted', disabled: false, + disabledLineType: 'dotted', + disabledLineWidth: 1, }; static propTypes = { @@ -71,18 +57,11 @@ export default class TextField extends PureComponent { animationDuration: PropTypes.number, fontSize: PropTypes.number, + titleFontSize: PropTypes.number, labelFontSize: PropTypes.number, - - contentInset: PropTypes.shape({ - top: PropTypes.number, - label: PropTypes.number, - input: PropTypes.number, - left: PropTypes.number, - right: PropTypes.number, - bottom: PropTypes.number, - }), - - labelOffset: Label.propTypes.offset, + labelHeight: PropTypes.number, + labelPadding: PropTypes.number, + inputContainerPadding: PropTypes.number, labelTextStyle: Text.propTypes.style, titleTextStyle: Text.propTypes.style, @@ -92,7 +71,7 @@ export default class TextField extends PureComponent { textColor: PropTypes.string, baseColor: PropTypes.string, - label: PropTypes.string, + label: PropTypes.string.isRequired, title: PropTypes.string, characterRestriction: PropTypes.number, @@ -102,17 +81,12 @@ export default class TextField extends PureComponent { lineWidth: PropTypes.number, activeLineWidth: PropTypes.number, - disabledLineWidth: PropTypes.number, - - lineType: Line.propTypes.lineType, - disabledLineType: Line.propTypes.lineType, disabled: PropTypes.bool, + disabledLineType: Line.propTypes.type, + disabledLineWidth: PropTypes.number, - formatText: PropTypes.func, - - renderLeftAccessory: PropTypes.func, - renderRightAccessory: PropTypes.func, + renderAccessory: PropTypes.func, prefix: PropTypes.string, suffix: PropTypes.string, @@ -121,33 +95,6 @@ export default class TextField extends PureComponent { inputContainerStyle: (ViewPropTypes || View.propTypes).style, }; - static inputContainerStyle = styles.inputContainer; - - static contentInset = { - top: 16, - label: 4, - input: 8, - left: 0, - right: 0, - bottom: 8, - }; - - static labelOffset = { - x0: 0, - y0: 0, - x1: 0, - y1: 0, - }; - - static getDerivedStateFromProps({ error }, state) { - /* Keep last received error in state */ - if (error && error !== state.error) { - return { error }; - } - - return null; - } - constructor(props) { super(props); @@ -159,38 +106,39 @@ export default class TextField extends PureComponent { this.onContentSizeChange = this.onContentSizeChange.bind(this); this.onFocusAnimationEnd = this.onFocusAnimationEnd.bind(this); - this.createGetter('contentInset'); - this.createGetter('labelOffset'); + this.updateRef = this.updateRef.bind(this, 'input'); - this.inputRef = React.createRef(); - this.mounted = false; - this.focused = false; - - let { value: text, error, fontSize } = this.props; - - let labelState = labelStateFromProps(this.props, { text })? 1 : 0; - let focusState = errorStateFromProps(this.props)? -1 : 0; + let { value, error, fontSize } = this.props; + this.mounted = false; this.state = { - text, - error, - - focusAnimation: new Animated.Value(focusState), - labelAnimation: new Animated.Value(labelState), + text: value, + focus: new Animated.Value(this.focusState(error, false)), + focused: false, receivedFocus: false, + error: error, + errored: !!error, + height: fontSize * 1.5, }; } - createGetter(name) { - this[name] = () => { - let { [name]: value } = this.props; - let { [name]: defaultValue } = this.constructor; + componentWillReceiveProps(props) { + let { error } = this.state; - return { ...defaultValue, ...value }; - }; + if (null != props.value) { + this.setState({ text: props.value }); + } + + if (props.error && props.error !== error) { + this.setState({ error: props.error }); + } + + if (props.error !== this.props.error) { + this.setState({ errored: !!props.error }); + } } componentDidMount() { @@ -201,151 +149,68 @@ export default class TextField extends PureComponent { this.mounted = false; } - componentDidUpdate(prevProps, prevState) { - let errorState = errorStateFromProps(this.props); - let prevErrorState = errorStateFromProps(prevProps); + componentWillUpdate(props, state) { + let { error, animationDuration: duration } = this.props; + let { focus, focused } = this.state; - if (errorState ^ prevErrorState) { - this.startFocusAnimation(); - } + if (props.error !== error || focused ^ state.focused) { + let toValue = this.focusState(props.error, state.focused); - let labelState = labelStateFromProps(this.props, this.state); - let prevLabelState = labelStateFromProps(prevProps, prevState); - - if (labelState ^ prevLabelState) { - this.startLabelAnimation(); + Animated + .timing(focus, { toValue, duration }) + .start(this.onFocusAnimationEnd); } } - startFocusAnimation() { - let { focusAnimation } = this.state; - let { animationDuration: duration } = this.props; - - let options = { - toValue: this.focusState(), - duration, - }; - - startAnimation(focusAnimation, options, this.onFocusAnimationEnd); - } - - startLabelAnimation() { - let { labelAnimation } = this.state; - let { animationDuration: duration } = this.props; - - let options = { - toValue: this.labelState(), - useNativeDriver: true, - duration, - }; - - startAnimation(labelAnimation, options); - } - - setNativeProps(props) { - let { current: input } = this.inputRef; - - input.setNativeProps(props); + updateRef(name, ref) { + this[name] = ref; } - focusState() { - if (errorStateFromProps(this.props)) { - return -1; - } - - return this.focused? 1 : 0; - } - - labelState() { - if (labelStateFromProps(this.props, this.state)) { - return 1; - } - - return this.focused? 1 : 0; + focusState(error, focused) { + return error? -1 : (focused? 1 : 0); } focus() { let { disabled, editable } = this.props; - let { current: input } = this.inputRef; if (!disabled && editable) { - input.focus(); + this.input.focus(); } } blur() { - let { current: input } = this.inputRef; - - input.blur(); + this.input.blur(); } clear() { - let { current: input } = this.inputRef; - - input.clear(); + this.input.clear(); /* onChangeText is not triggered by .clear() */ this.onChangeText(''); } value() { - let { text } = this.state; - let { defaultValue } = this.props; - - let value = this.isDefaultVisible()? - defaultValue: - text; - - if (null == value) { - return ''; - } - - return 'string' === typeof value? - value: - String(value); - } + let { text, receivedFocus } = this.state; + let { value, defaultValue } = this.props; - setValue(text) { - this.setState({ text }); + return (receivedFocus || null != value || null == defaultValue)? + text: + defaultValue; } isFocused() { - let { current: input } = this.inputRef; - - return input.isFocused(); + return this.input.isFocused(); } isRestricted() { - let { characterRestriction: limit } = this.props; - let { length: count } = this.value(); - - return limit < count; - } + let { characterRestriction } = this.props; + let { text = '' } = this.state; - isErrored() { - return errorStateFromProps(this.props); - } - - isDefaultVisible() { - let { text, receivedFocus } = this.state; - let { defaultValue } = this.props; - - return !receivedFocus && null == text && null != defaultValue; - } - - isPlaceholderVisible() { - let { placeholder } = this.props; - - return placeholder && !this.focused && !this.value(); - } - - isLabelActive() { - return 1 === this.labelState(); + return characterRestriction < text.length; } onFocus(event) { let { onFocus, clearTextOnFocus } = this.props; - let { receivedFocus } = this.state; if ('function' === typeof onFocus) { onFocus(event); @@ -355,14 +220,7 @@ export default class TextField extends PureComponent { this.clear(); } - this.focused = true; - - this.startFocusAnimation(); - this.startLabelAnimation(); - - if (!receivedFocus) { - this.setState({ receivedFocus: true, text: this.value() }); - } + this.setState({ focused: true, receivedFocus: true }); } onBlur(event) { @@ -372,26 +230,26 @@ export default class TextField extends PureComponent { onBlur(event); } - this.focused = false; - - this.startFocusAnimation(); - this.startLabelAnimation(); + this.setState({ focused: false }); } onChange(event) { - let { onChange } = this.props; + let { onChange, multiline } = this.props; if ('function' === typeof onChange) { onChange(event); } + + /* XXX: onContentSizeChange is not called on RN 0.44 and 0.45 */ + if (multiline && 'android' === Platform.OS) { + if (/^0\.4[45]\./.test(RN.version)) { + this.onContentSizeChange(event); + } + } } onChangeText(text) { - let { onChangeText, formatText } = this.props; - - if ('function' === typeof formatText) { - text = formatText(text); - } + let { onChangeText } = this.props; this.setState({ text }); @@ -411,130 +269,38 @@ export default class TextField extends PureComponent { this.setState({ height: Math.max( fontSize * 1.5, - Math.ceil(height) + Platform.select({ ios: 4, android: 1 }) + Math.ceil(height) + Platform.select({ ios: 5, android: 1 }) ), }); } onFocusAnimationEnd() { - let { error } = this.props; - let { error: retainedError } = this.state; - - if (this.mounted && !error && retainedError) { - this.setState({ error: null }); + if (this.mounted) { + this.setState((state, { error }) => ({ error })); } } - inputHeight() { - let { height: computedHeight } = this.state; - let { multiline, fontSize, height = computedHeight } = this.props; - - return multiline? - height: - fontSize * 1.5; - } - - inputContainerHeight() { - let { labelFontSize, multiline } = this.props; - let contentInset = this.contentInset(); - - if ('web' === Platform.OS && multiline) { - return 'auto'; - } - - return contentInset.top - + labelFontSize - + contentInset.label - + this.inputHeight() - + contentInset.input; - } - - inputProps() { - let store = {}; - - for (let key in TextInput.propTypes) { - if ('defaultValue' === key) { - continue; - } + renderAccessory() { + let { renderAccessory } = this.props; - if (key in this.props) { - store[key] = this.props[key]; - } - } - - return store; - } - - inputStyle() { - let { fontSize, baseColor, textColor, disabled, multiline } = this.props; - - let color = disabled || this.isDefaultVisible()? - baseColor: - textColor; - - let style = { - fontSize, - color, - - height: this.inputHeight(), - }; - - if (multiline) { - let lineHeight = fontSize * 1.5; - let offset = 'ios' === Platform.OS? 2 : 0; - - style.height += lineHeight; - style.transform = [{ - translateY: lineHeight + offset, - }]; + if ('function' !== typeof renderAccessory) { + return null; } - return style; - } - - renderLabel(props) { - let offset = this.labelOffset(); - - let { - label, - fontSize, - labelFontSize, - labelTextStyle, - } = this.props; - return ( - `; diff --git a/src/components/helper/index.js b/src/components/helper/index.js index 6060f9f5..0d20d8fc 100644 --- a/src/components/helper/index.js +++ b/src/components/helper/index.js @@ -1,97 +1,31 @@ import PropTypes from 'prop-types'; import React, { PureComponent } from 'react'; -import { Animated } from 'react-native'; +import { View, Animated } from 'react-native'; import styles from './styles'; export default class Helper extends PureComponent { - static propTypes = { - title: PropTypes.string, - error: PropTypes.string, - - disabled: PropTypes.bool, - - style: Animated.Text.propTypes.style, - - baseColor: PropTypes.string, - errorColor: PropTypes.string, - - focusAnimation: PropTypes.instanceOf(Animated.Value), + static defaultProps = { + numberOfLines: 1, }; - constructor(props) { - super(props); - - let { error, focusAnimation } = this.props; - - let opacity = focusAnimation.interpolate({ - inputRange: [-1, -0.5, 0], - outputRange: [1, 0, 1], - extrapolate: 'clamp', - }); - - this.state = { - errored: !!error, - opacity, - }; - } - - componentDidMount() { - let { focusAnimation } = this.props; - - this.listener = focusAnimation - .addListener(this.onAnimation.bind(this)); - } - - componentWillUnmount() { - let { focusAnimation } = this.props; - - focusAnimation.removeListener(this.listener); - } - - onAnimation({ value }) { - if (this.animationValue > -0.5 && value <= -0.5) { - this.setState({ errored: true }); - } - - if (this.animationValue < -0.5 && value >= -0.5) { - this.setState({ errored: false }); - } - - this.animationValue = value; - } + static propTypes = { + // style: Animated.Text.propTypes.style, + children: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.node), + PropTypes.node, + ]), + }; render() { - let { errored, opacity } = this.state; - let { - style, - title, - error, - disabled, - baseColor, - errorColor, - } = this.props; - - let text = errored? - error: - title; - - if (null == text) { - return null; - } - - let textStyle = { - opacity, - - color: !disabled && errored? - errorColor: - baseColor, - }; + let { children, style, ...props } = this.props; return ( - - {text} - + + + {children} + + ); } } diff --git a/src/components/helper/styles.js b/src/components/helper/styles.js index b9dec92f..483a0997 100644 --- a/src/components/helper/styles.js +++ b/src/components/helper/styles.js @@ -1,12 +1,13 @@ import { StyleSheet } from 'react-native'; export default StyleSheet.create({ + container: { + ...StyleSheet.absoluteFillObject, + paddingVertical: 4, + alignItems: 'flex-start', + }, + text: { - flex: 1, - fontSize: 12, - lineHeight: 16, backgroundColor: 'transparent', - paddingVertical: 2, - textAlign: 'left', }, }); diff --git a/src/components/helper/test.js b/src/components/helper/test.js index ecf6c240..bac3fdbd 100644 --- a/src/components/helper/test.js +++ b/src/components/helper/test.js @@ -1,6 +1,5 @@ import 'react-native'; import React from 'react'; -import { Animated } from 'react-native'; import renderer from 'react-test-renderer'; import Helper from '.'; @@ -8,41 +7,10 @@ import Helper from '.'; /* eslint-env jest */ const text = 'helper'; -const props = { - title: text, - fontSize: 16, - - baseColor: 'black', - errorColor: 'red', - - focusAnimation: new Animated.Value(0), -}; it('renders helper', () => { let helper = renderer - .create() - .toJSON(); - - expect(helper) - .toMatchSnapshot(); -}); - -it('renders disabled helper', () => { - let helper = renderer - .create( - - ) - .toJSON(); - - expect(helper) - .toMatchSnapshot(); -}); - -it('renders helper with error', () => { - let helper = renderer - .create( - - ) + .create({text}) .toJSON(); expect(helper) diff --git a/src/components/label/__snapshots__/test.js.snap b/src/components/label/__snapshots__/test.js.snap index e7c6a190..729e0f72 100644 --- a/src/components/label/__snapshots__/test.js.snap +++ b/src/components/label/__snapshots__/test.js.snap @@ -1,120 +1,25 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`renders active errored label 1`] = ` - - - test - - -`; - -exports[`renders active focused label 1`] = ` - - - test - - -`; - exports[`renders active label 1`] = ` @@ -123,41 +28,26 @@ exports[`renders active label 1`] = ` `; -exports[`renders empty label 1`] = `null`; - exports[`renders errored label 1`] = ` @@ -166,39 +56,26 @@ exports[`renders errored label 1`] = ` `; -exports[`renders label 1`] = ` +exports[`renders focused label 1`] = ` @@ -207,39 +84,26 @@ exports[`renders label 1`] = ` `; -exports[`renders restricted label 1`] = ` +exports[`renders label 1`] = ` @@ -248,40 +112,26 @@ exports[`renders restricted label 1`] = ` `; -exports[`renders styled label 1`] = ` +exports[`renders restricted label 1`] = ` diff --git a/src/components/label/index.js b/src/components/label/index.js index 82eaf033..3d4a9a9f 100644 --- a/src/components/label/index.js +++ b/src/components/label/index.js @@ -2,117 +2,132 @@ import PropTypes from 'prop-types'; import React, { PureComponent } from 'react'; import { Animated } from 'react-native'; -import styles from './styles'; - export default class Label extends PureComponent { static defaultProps = { numberOfLines: 1, - disabled: false, + + active: false, + focused: false, + errored: false, restricted: false, }; static propTypes = { - numberOfLines: PropTypes.number, - - disabled: PropTypes.bool, + active: PropTypes.bool, + focused: PropTypes.bool, + errored: PropTypes.bool, restricted: PropTypes.bool, + baseSize: PropTypes.number.isRequired, fontSize: PropTypes.number.isRequired, activeFontSize: PropTypes.number.isRequired, + basePadding: PropTypes.number.isRequired, - baseColor: PropTypes.string.isRequired, tintColor: PropTypes.string.isRequired, + baseColor: PropTypes.string.isRequired, errorColor: PropTypes.string.isRequired, - focusAnimation: PropTypes - .instanceOf(Animated.Value) - .isRequired, + animationDuration: PropTypes.number.isRequired, - labelAnimation: PropTypes - .instanceOf(Animated.Value) - .isRequired, + // style: Animated.Text.propTypes.style, - contentInset: PropTypes.shape({ - label: PropTypes.number, - }), + children: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.node), + PropTypes.node, + ]), + }; - offset: PropTypes.shape({ - x0: PropTypes.number, - y0: PropTypes.number, - x1: PropTypes.number, - y1: PropTypes.number, - }), + constructor(props) { + super(props); - style: Animated.Text.propTypes.style, - label: PropTypes.string, - }; + this.state = { + input: new Animated.Value(this.inputState()), + focus: new Animated.Value(this.focusState()), + }; + } + + componentWillReceiveProps(props) { + let { focus, input } = this.state; + let { active, focused, errored, animationDuration: duration } = this.props; + + if (focused ^ props.focused || active ^ props.active) { + let toValue = this.inputState(props); + + Animated + .timing(input, { toValue, duration }) + .start(); + } + + if (focused ^ props.focused || errored ^ props.errored) { + let toValue = this.focusState(props); + + Animated + .timing(focus, { toValue, duration }) + .start(); + } + } + + inputState({ focused, active } = this.props) { + return active || focused? 1 : 0; + } + + focusState({ focused, errored } = this.props) { + return errored? -1 : (focused? 1 : 0); + } render() { + let { focus, input } = this.state; let { - label, - offset, - disabled, + children, restricted, fontSize, activeFontSize, - contentInset, errorColor, baseColor, tintColor, + baseSize, + basePadding, style, - focusAnimation, - labelAnimation, + errored, + active, + focused, + animationDuration, ...props } = this.props; - if (null == label) { - return null; - } - - let color = disabled? - baseColor: - restricted? - errorColor: - focusAnimation.interpolate({ - inputRange: [-1, 0, 1], - outputRange: [errorColor, baseColor, tintColor], - }); + let color = restricted? + errorColor: + focus.interpolate({ + inputRange: [-1, 0, 1], + outputRange: [errorColor, baseColor, tintColor], + }); + + let top = input.interpolate({ + inputRange: [0, 1], + outputRange: [ + baseSize + fontSize * 0.25, + baseSize - basePadding - activeFontSize, + ], + }); let textStyle = { - lineHeight: fontSize, - fontSize, + fontSize: input.interpolate({ + inputRange: [0, 1], + outputRange: [fontSize, activeFontSize], + }), + color, }; - let { x0, y0, x1, y1 } = offset; - - y0 += activeFontSize; - y0 += contentInset.label; - y0 += fontSize * 0.25; - let containerStyle = { - transform: [{ - scale: labelAnimation.interpolate({ - inputRange: [0, 1], - outputRange: [1, activeFontSize / fontSize], - }), - }, { - translateY: labelAnimation.interpolate({ - inputRange: [0, 1], - outputRange: [y0, y1], - }), - }, { - translateX: labelAnimation.interpolate({ - inputRange: [0, 1], - outputRange: [x0, x1], - }), - }], + position: 'absolute', + top, }; return ( - - - {label} + + + {children} ); diff --git a/src/components/label/test.js b/src/components/label/test.js index cdd7cb85..4441c575 100644 --- a/src/components/label/test.js +++ b/src/components/label/test.js @@ -1,40 +1,26 @@ import 'react-native'; import React from 'react'; -import { Animated } from 'react-native'; import renderer from 'react-test-renderer'; import Label from '.'; /* eslint-env jest */ +const text = 'test'; const props = { + baseSize: 32, + basePadding: 4, fontSize: 16, activeFontSize: 12, - - contentInset: { label: 4 }, - - baseColor: 'black', tintColor: 'blue', + baseColor: 'black', errorColor: 'red', - - offset: { x0: 0, y0: 0, x1: 0, y1: 0 }, - - focusAnimation: new Animated.Value(0), - labelAnimation: new Animated.Value(0), - label: 'test', + animationDuration: 225, }; it('renders label', () => { let label = renderer - .create(