From 9766551ae3d26106cf1b89ae0a4a7551e96f8c07 Mon Sep 17 00:00:00 2001 From: MaxGorshkov Date: Mon, 18 May 2020 10:35:44 +0500 Subject: [PATCH 01/44] change description --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 1e63b0fb..423e7180 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "react-native-material-textfield", + "name": "@softmedialab/react-native-material-textfield", "version": "0.16.1", "license": "BSD-3-Clause", "author": "Alexander Nazarov ", @@ -25,7 +25,7 @@ "repository": { "type": "git", - "url": "git://github.com/n4kz/react-native-material-textfield.git" + "url": "git://github.com/SoftMediaLab/react-native-material-textfield.git" }, "dependencies": { From 6a5c4fb5a36e3cf54800eb12674d94a0d1468116 Mon Sep 17 00:00:00 2001 From: ruslandjent Date: Mon, 18 May 2020 10:51:24 +0500 Subject: [PATCH 02/44] lib fix --- src/components/affix/index.js | 4 ++-- src/components/helper/index.js | 4 ++-- src/components/label/index.js | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/affix/index.js b/src/components/affix/index.js index 0f85022e..8d7cd742 100644 --- a/src/components/affix/index.js +++ b/src/components/affix/index.js @@ -1,6 +1,6 @@ import PropTypes from 'prop-types'; import React, { PureComponent } from 'react'; -import { Animated } from 'react-native'; +import { Animated, Text } from 'react-native'; import styles from './styles'; @@ -11,7 +11,7 @@ export default class Affix extends PureComponent { static propTypes = { numberOfLines: PropTypes.number, - style: Animated.Text.propTypes.style, + style: Text.propTypes.style, color: PropTypes.string.isRequired, fontSize: PropTypes.number.isRequired, diff --git a/src/components/helper/index.js b/src/components/helper/index.js index 6060f9f5..76eb54d7 100644 --- a/src/components/helper/index.js +++ b/src/components/helper/index.js @@ -1,6 +1,6 @@ import PropTypes from 'prop-types'; import React, { PureComponent } from 'react'; -import { Animated } from 'react-native'; +import { Animated, Text } from 'react-native'; import styles from './styles'; @@ -11,7 +11,7 @@ export default class Helper extends PureComponent { disabled: PropTypes.bool, - style: Animated.Text.propTypes.style, + style: Text.propTypes.style, baseColor: PropTypes.string, errorColor: PropTypes.string, diff --git a/src/components/label/index.js b/src/components/label/index.js index 82eaf033..4eb5f8b6 100644 --- a/src/components/label/index.js +++ b/src/components/label/index.js @@ -1,6 +1,6 @@ import PropTypes from 'prop-types'; import React, { PureComponent } from 'react'; -import { Animated } from 'react-native'; +import { Animated, Text } from 'react-native'; import styles from './styles'; @@ -43,7 +43,7 @@ export default class Label extends PureComponent { y1: PropTypes.number, }), - style: Animated.Text.propTypes.style, + style: Text.propTypes.style, label: PropTypes.string, }; From f31d0bb0b8f6f77572bd977407e089616172f551 Mon Sep 17 00:00:00 2001 From: MaxGorshkov Date: Mon, 18 May 2020 10:55:54 +0500 Subject: [PATCH 03/44] 0.16.2 --- package.json | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 423e7180..4d317617 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,8 @@ { "name": "@softmedialab/react-native-material-textfield", - "version": "0.16.1", + "version": "0.16.2", "license": "BSD-3-Clause", "author": "Alexander Nazarov ", - "description": "Material textfield", "keywords": [ "react", @@ -19,24 +18,25 @@ "floating", "label" ], - "main": "index.js", - "files": ["index.js", "src/*", "license.txt", "readme.md", "changelog.md"], - + "files": [ + "index.js", + "src/*", + "license.txt", + "readme.md", + "changelog.md" + ], "repository": { "type": "git", "url": "git://github.com/SoftMediaLab/react-native-material-textfield.git" }, - "dependencies": { "prop-types": "^15.5.9" }, - "peerDependencies": { "react": ">=16.3.0", "react-native": ">=0.55.0" }, - "devDependencies": { "@babel/core": "^7.5.5", "@babel/runtime": "^7.5.5", @@ -51,15 +51,15 @@ "react-native": "0.61.2", "react-test-renderer": "16.10.2" }, - "scripts": { "test": "eslint index.js src && jest --silent", "lint": "eslint index.js src example/app.js", "jest": "jest" }, - "jest": { "preset": "react-native", - "modulePathIgnorePatterns": ["/example"] + "modulePathIgnorePatterns": [ + "/example" + ] } } From 2cdd07c43631da6c3385f14a66c07c50ab5d15b4 Mon Sep 17 00:00:00 2001 From: ruslandjent Date: Mon, 18 May 2020 15:09:01 +0500 Subject: [PATCH 04/44] fixed --- .../affix/__snapshots__/test.js.snap | 80 +- src/components/affix/index.js | 54 +- src/components/affix/styles.js | 1 + src/components/affix/test.js | 32 +- .../counter/__snapshots__/test.js.snap | 94 +- src/components/counter/index.js | 25 +- src/components/counter/styles.js | 9 +- .../field-filled/__snapshots__/test.js.snap | 1014 ------ src/components/field-filled/index.js | 24 - src/components/field-filled/styles.js | 10 - src/components/field-filled/test.js | 69 - .../field-outlined/__snapshots__/test.js.snap | 1730 ----------- src/components/field-outlined/index.js | 61 - src/components/field-outlined/test.js | 69 - .../field/__snapshots__/test.js.snap | 2762 ++++++++--------- src/components/field/index.js | 709 ++--- src/components/field/styles.js | 30 +- src/components/field/test.js | 21 +- .../helper/__snapshots__/test.js.snap | 70 +- src/components/helper/index.js | 96 +- src/components/helper/styles.js | 11 +- src/components/helper/test.js | 34 +- .../label/__snapshots__/test.js.snap | 226 +- src/components/label/index.js | 155 +- src/components/label/styles.js | 17 - src/components/label/test.js | 77 +- .../line/__snapshots__/test.js.snap | 126 +- src/components/line/index.js | 112 +- src/components/line/styles.js | 7 - src/components/line/test.js | 62 +- .../outline/__snapshots__/test.js.snap | 613 ---- src/components/outline/index.js | 136 - src/components/outline/styles.js | 83 - src/components/outline/test.js | 63 - 34 files changed, 1977 insertions(+), 6705 deletions(-) delete mode 100644 src/components/field-filled/__snapshots__/test.js.snap delete mode 100644 src/components/field-filled/index.js delete mode 100644 src/components/field-filled/styles.js delete mode 100644 src/components/field-filled/test.js delete mode 100644 src/components/field-outlined/__snapshots__/test.js.snap delete mode 100644 src/components/field-outlined/index.js delete mode 100644 src/components/field-outlined/test.js delete mode 100644 src/components/label/styles.js delete mode 100644 src/components/outline/__snapshots__/test.js.snap delete mode 100644 src/components/outline/index.js delete mode 100644 src/components/outline/styles.js delete mode 100644 src/components/outline/test.js 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 8d7cd742..fe8eb0a1 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: 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, - labelAnimation: PropTypes - .instanceOf(Animated.Value) - .isRequired, + fontSize: PropTypes.number.isRequired, + baseColor: PropTypes.string.isRequired, + animationDuration: PropTypes.number.isRequired, + + style: 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-filled/__snapshots__/test.js.snap b/src/components/field-filled/__snapshots__/test.js.snap deleted file mode 100644 index 485b017f..00000000 --- a/src/components/field-filled/__snapshots__/test.js.snap +++ /dev/null @@ -1,1014 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders 1`] = ` - - - - - - - - - test - - - - - - - - - -`; - -exports[`renders accessory 1`] = ` - - - - - - - - - - test - - - - - - - - - -`; - -exports[`renders counter 1`] = ` - - - - - - - - - test - - - - - - - - - - 4 - / - 10 - - - -`; - -exports[`renders disabled value 1`] = ` - - - - - - - - - test - - - - - - - - - -`; - -exports[`renders title 1`] = ` - - - - - - - - - test - - - - - - - - - - field - - - -`; - -exports[`renders value 1`] = ` - - - - - - - - - test - - - - - - - - - -`; diff --git a/src/components/field-filled/index.js b/src/components/field-filled/index.js deleted file mode 100644 index 3367229b..00000000 --- a/src/components/field-filled/index.js +++ /dev/null @@ -1,24 +0,0 @@ -import TextField from '../field'; -import styles from './styles'; - -export default class FilledTextField extends TextField { - static contentInset = { - ...TextField.contentInset, - - top: 8, - left: 12, - right: 12, - }; - - static labelOffset = { - ...TextField.labelOffset, - - y0: -10, - y1: -2, - }; - - static inputContainerStyle = [ - TextField.inputContainerStyle, - styles.inputContainer, - ]; -} diff --git a/src/components/field-filled/styles.js b/src/components/field-filled/styles.js deleted file mode 100644 index 5003cf85..00000000 --- a/src/components/field-filled/styles.js +++ /dev/null @@ -1,10 +0,0 @@ -import { StyleSheet } from 'react-native'; - -export default StyleSheet.create({ - inputContainer: { - borderTopLeftRadius: 4, - borderTopRightRadius: 4, - - backgroundColor: 'rgb(204, 204, 204)', - }, -}); diff --git a/src/components/field-filled/test.js b/src/components/field-filled/test.js deleted file mode 100644 index c779fe40..00000000 --- a/src/components/field-filled/test.js +++ /dev/null @@ -1,69 +0,0 @@ -import { Image } from 'react-native'; -import React from 'react'; -import renderer from 'react-test-renderer'; - -import FilledTextField from '.'; - -const props = { - label: 'test', -}; - -/* eslint-env jest */ - -it('renders', () => { - let field = renderer - .create() - .toJSON(); - - expect(field) - .toMatchSnapshot(); -}); - -it('renders value', () => { - let field = renderer - .create() - .toJSON(); - - expect(field) - .toMatchSnapshot(); -}); - -it('renders disabled value', () => { - let field = renderer - .create() - .toJSON(); - - expect(field) - .toMatchSnapshot(); -}); - -it('renders title', () => { - let field = renderer - .create() - .toJSON(); - - expect(field) - .toMatchSnapshot(); -}); - -it('renders counter', () => { - let field = renderer - .create() - .toJSON(); - - expect(field) - .toMatchSnapshot(); -}); - -it('renders accessory', () => { - let render = () => ( - - ); - - let field = renderer - .create() - .toJSON(); - - expect(field) - .toMatchSnapshot(); -}); diff --git a/src/components/field-outlined/__snapshots__/test.js.snap b/src/components/field-outlined/__snapshots__/test.js.snap deleted file mode 100644 index 011e0330..00000000 --- a/src/components/field-outlined/__snapshots__/test.js.snap +++ /dev/null @@ -1,1730 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders 1`] = ` - - - - - - - - - - - - - - - - - - - - test - - - - - - - - - -`; - -exports[`renders accessory 1`] = ` - - - - - - - - - - - - - - - - - - - - - test - - - - - - - - - -`; - -exports[`renders counter 1`] = ` - - - - - - - - - - - - - - - - - - - - test - - - - - - - - - - 4 - / - 10 - - - -`; - -exports[`renders disabled value 1`] = ` - - - - - - - - - - - - - - - - - - - - test - - - - - - - - - -`; - -exports[`renders title 1`] = ` - - - - - - - - - - - - - - - - - - - - test - - - - - - - - - - field - - - -`; - -exports[`renders value 1`] = ` - - - - - - - - - - - - - - - - - - - - test - - - - - - - - - -`; diff --git a/src/components/field-outlined/index.js b/src/components/field-outlined/index.js deleted file mode 100644 index 7005ddf0..00000000 --- a/src/components/field-outlined/index.js +++ /dev/null @@ -1,61 +0,0 @@ -import React from 'react'; -import { Animated, StyleSheet } from 'react-native'; - -import TextField from '../field'; -import Outline from '../outline'; - -export default class OutlinedTextField extends TextField { - static contentInset = { - ...TextField.contentInset, - - input: 16, - - top: 0, - left: 12, - right: 12, - }; - - static labelOffset = { - ...TextField.labelOffset, - - y0: 0, - y1: -10, - }; - - static defaultProps = { - ...TextField.defaultProps, - - lineWidth: 1, - disabledLineWidth: StyleSheet.hairlineWidth, - }; - - constructor(props) { - super(props); - - this.onTextLayout = this.onTextLayout.bind(this); - this.state.labelWidth = new Animated.Value(0); - } - - onTextLayout({ nativeEvent: { lines } }) { - let { fontSize, labelFontSize } = this.props; - let { labelWidth } = this.state; - - let scale = labelFontSize / fontSize; - - labelWidth.setValue(lines[0].width * scale); - } - - renderLabel(props) { - let { onTextLayout } = this; - - return super.renderLabel({ ...props, onTextLayout }); - } - - renderLine(props) { - let { labelWidth } = this.state; - - return ( - - ); - } -} diff --git a/src/components/field-outlined/test.js b/src/components/field-outlined/test.js deleted file mode 100644 index 831b70bd..00000000 --- a/src/components/field-outlined/test.js +++ /dev/null @@ -1,69 +0,0 @@ -import { Image } from 'react-native'; -import React from 'react'; -import renderer from 'react-test-renderer'; - -import OutlinedTextField from '.'; - -const props = { - label: 'test', -}; - -/* eslint-env jest */ - -it('renders', () => { - let field = renderer - .create() - .toJSON(); - - expect(field) - .toMatchSnapshot(); -}); - -it('renders value', () => { - let field = renderer - .create() - .toJSON(); - - expect(field) - .toMatchSnapshot(); -}); - -it('renders disabled value', () => { - let field = renderer - .create() - .toJSON(); - - expect(field) - .toMatchSnapshot(); -}); - -it('renders title', () => { - let field = renderer - .create() - .toJSON(); - - expect(field) - .toMatchSnapshot(); -}); - -it('renders counter', () => { - let field = renderer - .create() - .toJSON(); - - expect(field) - .toMatchSnapshot(); -}); - -it('renders accessory', () => { - let render = () => ( - - ); - - let field = renderer - .create() - .toJSON(); - - expect(field) - .toMatchSnapshot(); -}); 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 76eb54d7..84e6e3a9 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, Text } from 'react-native'; +import { View, Animated, Text } from 'react-native'; import styles from './styles'; export default class Helper extends PureComponent { - static propTypes = { - title: PropTypes.string, - error: PropTypes.string, - - disabled: PropTypes.bool, + static defaultProps = { + numberOfLines: 1, + }; + static propTypes = { style: Text.propTypes.style, - - baseColor: PropTypes.string, - errorColor: PropTypes.string, - - focusAnimation: PropTypes.instanceOf(Animated.Value), + children: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.node), + PropTypes.node, + ]), }; - 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; - } - 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 4eb5f8b6..bf551e3e 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, Text } 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: 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: 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/styles.js b/src/components/label/styles.js deleted file mode 100644 index af560f33..00000000 --- a/src/components/label/styles.js +++ /dev/null @@ -1,17 +0,0 @@ -import { StyleSheet } from 'react-native'; - -export default StyleSheet.create({ - container: { - position: 'absolute', - top: 0, - left: '-100%', - width: '200%', - paddingLeft: '50%', - }, - - text: { - textAlign: 'left', - includeFontPadding: false, - textAlignVertical: 'top', - }, -}); 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( `; diff --git a/src/components/counter/index.js b/src/components/counter/index.js index ad6e3133..35d3264f 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 { View, Text } from 'react-native'; +import { Text } from 'react-native'; import styles from './styles'; @@ -9,8 +9,6 @@ export default class Counter extends PureComponent { count: PropTypes.number.isRequired, limit: PropTypes.number, - fontSize: PropTypes.number, - baseColor: PropTypes.string.isRequired, errorColor: PropTypes.string.isRequired, @@ -18,23 +16,22 @@ export default class Counter extends PureComponent { }; render() { - let { count, limit, baseColor, errorColor, fontSize, style } = this.props; - - let textStyle = { - color: count > limit? errorColor : baseColor, - fontSize, - }; + let { count, limit, baseColor, errorColor, style } = this.props; 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 5a3a718d..8eb9cdf4 100644 --- a/src/components/counter/styles.js +++ b/src/components/counter/styles.js @@ -1,13 +1,12 @@ 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-filled/__snapshots__/test.js.snap b/src/components/field-filled/__snapshots__/test.js.snap new file mode 100644 index 00000000..b55ee33b --- /dev/null +++ b/src/components/field-filled/__snapshots__/test.js.snap @@ -0,0 +1,1014 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders 1`] = ` + + + + + + + + + test + + + + + + + + + +`; + +exports[`renders accessory 1`] = ` + + + + + + + + + + test + + + + + + + + + +`; + +exports[`renders counter 1`] = ` + + + + + + + + + test + + + + + + + + + + 4 + / + 10 + + + +`; + +exports[`renders disabled value 1`] = ` + + + + + + + + + test + + + + + + + + + +`; + +exports[`renders title 1`] = ` + + + + + + + + + test + + + + + + + + + + field + + + +`; + +exports[`renders value 1`] = ` + + + + + + + + + test + + + + + + + + + +`; diff --git a/src/components/field-filled/index.js b/src/components/field-filled/index.js new file mode 100644 index 00000000..3367229b --- /dev/null +++ b/src/components/field-filled/index.js @@ -0,0 +1,24 @@ +import TextField from '../field'; +import styles from './styles'; + +export default class FilledTextField extends TextField { + static contentInset = { + ...TextField.contentInset, + + top: 8, + left: 12, + right: 12, + }; + + static labelOffset = { + ...TextField.labelOffset, + + y0: -10, + y1: -2, + }; + + static inputContainerStyle = [ + TextField.inputContainerStyle, + styles.inputContainer, + ]; +} diff --git a/src/components/field-filled/styles.js b/src/components/field-filled/styles.js new file mode 100644 index 00000000..5003cf85 --- /dev/null +++ b/src/components/field-filled/styles.js @@ -0,0 +1,10 @@ +import { StyleSheet } from 'react-native'; + +export default StyleSheet.create({ + inputContainer: { + borderTopLeftRadius: 4, + borderTopRightRadius: 4, + + backgroundColor: 'rgb(204, 204, 204)', + }, +}); diff --git a/src/components/field-filled/test.js b/src/components/field-filled/test.js new file mode 100644 index 00000000..c779fe40 --- /dev/null +++ b/src/components/field-filled/test.js @@ -0,0 +1,69 @@ +import { Image } from 'react-native'; +import React from 'react'; +import renderer from 'react-test-renderer'; + +import FilledTextField from '.'; + +const props = { + label: 'test', +}; + +/* eslint-env jest */ + +it('renders', () => { + let field = renderer + .create() + .toJSON(); + + expect(field) + .toMatchSnapshot(); +}); + +it('renders value', () => { + let field = renderer + .create() + .toJSON(); + + expect(field) + .toMatchSnapshot(); +}); + +it('renders disabled value', () => { + let field = renderer + .create() + .toJSON(); + + expect(field) + .toMatchSnapshot(); +}); + +it('renders title', () => { + let field = renderer + .create() + .toJSON(); + + expect(field) + .toMatchSnapshot(); +}); + +it('renders counter', () => { + let field = renderer + .create() + .toJSON(); + + expect(field) + .toMatchSnapshot(); +}); + +it('renders accessory', () => { + let render = () => ( + + ); + + let field = renderer + .create() + .toJSON(); + + expect(field) + .toMatchSnapshot(); +}); diff --git a/src/components/field-outlined/__snapshots__/test.js.snap b/src/components/field-outlined/__snapshots__/test.js.snap new file mode 100644 index 00000000..df4605b0 --- /dev/null +++ b/src/components/field-outlined/__snapshots__/test.js.snap @@ -0,0 +1,1730 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders 1`] = ` + + + + + + + + + + + + + + + + + + + + test + + + + + + + + + +`; + +exports[`renders accessory 1`] = ` + + + + + + + + + + + + + + + + + + + + + test + + + + + + + + + +`; + +exports[`renders counter 1`] = ` + + + + + + + + + + + + + + + + + + + + test + + + + + + + + + + 4 + / + 10 + + + +`; + +exports[`renders disabled value 1`] = ` + + + + + + + + + + + + + + + + + + + + test + + + + + + + + + +`; + +exports[`renders title 1`] = ` + + + + + + + + + + + + + + + + + + + + test + + + + + + + + + + field + + + +`; + +exports[`renders value 1`] = ` + + + + + + + + + + + + + + + + + + + + test + + + + + + + + + +`; diff --git a/src/components/field-outlined/index.js b/src/components/field-outlined/index.js new file mode 100644 index 00000000..7005ddf0 --- /dev/null +++ b/src/components/field-outlined/index.js @@ -0,0 +1,61 @@ +import React from 'react'; +import { Animated, StyleSheet } from 'react-native'; + +import TextField from '../field'; +import Outline from '../outline'; + +export default class OutlinedTextField extends TextField { + static contentInset = { + ...TextField.contentInset, + + input: 16, + + top: 0, + left: 12, + right: 12, + }; + + static labelOffset = { + ...TextField.labelOffset, + + y0: 0, + y1: -10, + }; + + static defaultProps = { + ...TextField.defaultProps, + + lineWidth: 1, + disabledLineWidth: StyleSheet.hairlineWidth, + }; + + constructor(props) { + super(props); + + this.onTextLayout = this.onTextLayout.bind(this); + this.state.labelWidth = new Animated.Value(0); + } + + onTextLayout({ nativeEvent: { lines } }) { + let { fontSize, labelFontSize } = this.props; + let { labelWidth } = this.state; + + let scale = labelFontSize / fontSize; + + labelWidth.setValue(lines[0].width * scale); + } + + renderLabel(props) { + let { onTextLayout } = this; + + return super.renderLabel({ ...props, onTextLayout }); + } + + renderLine(props) { + let { labelWidth } = this.state; + + return ( + + ); + } +} diff --git a/src/components/field-outlined/test.js b/src/components/field-outlined/test.js new file mode 100644 index 00000000..831b70bd --- /dev/null +++ b/src/components/field-outlined/test.js @@ -0,0 +1,69 @@ +import { Image } from 'react-native'; +import React from 'react'; +import renderer from 'react-test-renderer'; + +import OutlinedTextField from '.'; + +const props = { + label: 'test', +}; + +/* eslint-env jest */ + +it('renders', () => { + let field = renderer + .create() + .toJSON(); + + expect(field) + .toMatchSnapshot(); +}); + +it('renders value', () => { + let field = renderer + .create() + .toJSON(); + + expect(field) + .toMatchSnapshot(); +}); + +it('renders disabled value', () => { + let field = renderer + .create() + .toJSON(); + + expect(field) + .toMatchSnapshot(); +}); + +it('renders title', () => { + let field = renderer + .create() + .toJSON(); + + expect(field) + .toMatchSnapshot(); +}); + +it('renders counter', () => { + let field = renderer + .create() + .toJSON(); + + expect(field) + .toMatchSnapshot(); +}); + +it('renders accessory', () => { + let render = () => ( + + ); + + let field = renderer + .create() + .toJSON(); + + expect(field) + .toMatchSnapshot(); +}); diff --git a/src/components/field/__snapshots__/test.js.snap b/src/components/field/__snapshots__/test.js.snap index caf36655..2f89f6ff 100644 --- a/src/components/field/__snapshots__/test.js.snap +++ b/src/components/field/__snapshots__/test.js.snap @@ -5,98 +5,52 @@ exports[`renders 1`] = ` onResponderRelease={[Function]} onStartShouldSetResponder={[Function]} pointerEvents="auto" - style={undefined} > - - test - - - - - - + > + test + - + `; -exports[`renders accessory 1`] = ` +exports[`renders counter 1`] = ` - - test - - - - - - + /> - - + > + test + - - -`; - -exports[`renders counter 1`] = ` - - - - test - - - - - - - - - - - - - - - - - - 4 - / - 10 - - + 4 + / + 10 + `; @@ -540,98 +346,52 @@ exports[`renders default value 1`] = ` onResponderRelease={[Function]} onStartShouldSetResponder={[Function]} pointerEvents="auto" - style={undefined} > - - test - - - - - - + > + test + - + `; @@ -704,116 +505,52 @@ exports[`renders disabled value 1`] = ` onResponderRelease={[Function]} onStartShouldSetResponder={[Function]} pointerEvents="none" - style={undefined} > - - - test - - - - - - + > + test + - + `; @@ -886,99 +664,52 @@ exports[`renders error 1`] = ` onResponderRelease={[Function]} onStartShouldSetResponder={[Function]} pointerEvents="auto" - style={undefined} > - - test - - - - - - - message + test - + + + message + + `; -exports[`renders multiline value 1`] = ` +exports[`renders left accessory 1`] = ` - - test - - - - - - + + > + test + - + `; -exports[`renders prefix 1`] = ` +exports[`renders multiline value 1`] = ` - - test - + /> - $ + test - + > + + + +`; + +exports[`renders prefix 1`] = ` + + + + + + > + test + - + + $ + + + + `; @@ -1413,98 +1349,52 @@ exports[`renders restriction 1`] = ` onResponderRelease={[Function]} onStartShouldSetResponder={[Function]} pointerEvents="auto" - style={undefined} > - - test - - - - - - + > + test + - - + + - - 4 - / - 2 - - + 4 + / + 2 + `; -exports[`renders suffix 1`] = ` +exports[`renders right accessory 1`] = ` - - test - + /> - - .com + test + + + + + +`; + +exports[`renders suffix 1`] = ` + + + + + + > + test + - + + > + + .com + + + `; @@ -1802,98 +1875,52 @@ exports[`renders title 1`] = ` onResponderRelease={[Function]} onStartShouldSetResponder={[Function]} pointerEvents="auto" - style={undefined} > - - test - - - - - - + > + test + - - field - + Array [ + Object { + "flex": 1, + "includeFontPadding": false, + "margin": 0, + "padding": 0, + "paddingTop": 0, + "textAlign": "left", + "textAlignVertical": "top", + "top": 2, + }, + Object { + "color": "rgba(0, 0, 0, .87)", + "fontSize": 16, + "height": 24, + }, + undefined, + ] + } + underlineColorAndroid="transparent" + value="" + /> + + + field + + `; @@ -1968,98 +2051,52 @@ 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 52c1868a..494bbaaa 100644 --- a/src/components/field/index.js +++ b/src/components/field/index.js @@ -8,18 +8,34 @@ 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.js'; +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; +} export default class TextField extends PureComponent { static defaultProps = { @@ -31,11 +47,7 @@ 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)', @@ -45,10 +57,12 @@ export default class TextField extends PureComponent { lineWidth: StyleSheet.hairlineWidth, activeLineWidth: 2, + disabledLineWidth: 1, - disabled: false, + lineType: 'solid', disabledLineType: 'dotted', - disabledLineWidth: 1, + + disabled: false, }; static propTypes = { @@ -57,11 +71,17 @@ export default class TextField extends PureComponent { animationDuration: PropTypes.number, fontSize: PropTypes.number, - titleFontSize: PropTypes.number, labelFontSize: PropTypes.number, - labelHeight: PropTypes.number, - labelPadding: PropTypes.number, - inputContainerPadding: PropTypes.number, + + contentInset: PropTypes.shape({ + top: PropTypes.number, + label: PropTypes.number, + input: PropTypes.number, + left: PropTypes.number, + right: PropTypes.number, + }), + + labelOffset: Label.propTypes.offset, labelTextStyle: Text.propTypes.style, titleTextStyle: Text.propTypes.style, @@ -71,7 +91,7 @@ export default class TextField extends PureComponent { textColor: PropTypes.string, baseColor: PropTypes.string, - label: PropTypes.string.isRequired, + label: PropTypes.string, title: PropTypes.string, characterRestriction: PropTypes.number, @@ -81,12 +101,17 @@ 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, - renderAccessory: PropTypes.func, + formatText: PropTypes.func, + + renderLeftAccessory: PropTypes.func, + renderRightAccessory: PropTypes.func, prefix: PropTypes.string, suffix: PropTypes.string, @@ -95,6 +120,32 @@ 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, + }; + + 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); @@ -106,39 +157,38 @@ export default class TextField extends PureComponent { this.onContentSizeChange = this.onContentSizeChange.bind(this); this.onFocusAnimationEnd = this.onFocusAnimationEnd.bind(this); - this.updateRef = this.updateRef.bind(this, 'input'); - - let { value, error, fontSize } = this.props; + this.createGetter('contentInset'); + this.createGetter('labelOffset'); + 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; + this.state = { - text: value, + text, + error, - focus: new Animated.Value(this.focusState(error, false)), - focused: false, - receivedFocus: false, + focusAnimation: new Animated.Value(focusState), + labelAnimation: new Animated.Value(labelState), - error: error, - errored: !!error, + receivedFocus: false, height: fontSize * 1.5, }; } - componentWillReceiveProps(props) { - let { error } = this.state; - - if (null != props.value) { - this.setState({ text: props.value }); - } + createGetter(name) { + this[name] = () => { + let { [name]: value } = this.props; + let { [name]: defaultValue } = this.constructor; - if (props.error && props.error !== error) { - this.setState({ error: props.error }); - } - - if (props.error !== this.props.error) { - this.setState({ errored: !!props.error }); - } + return { ...defaultValue, ...value }; + }; } componentDidMount() { @@ -149,68 +199,151 @@ export default class TextField extends PureComponent { this.mounted = false; } - componentWillUpdate(props, state) { - let { error, animationDuration: duration } = this.props; - let { focus, focused } = this.state; + componentDidUpdate(prevProps, prevState) { + let errorState = errorStateFromProps(this.props); + let prevErrorState = errorStateFromProps(prevProps); - if (props.error !== error || focused ^ state.focused) { - let toValue = this.focusState(props.error, state.focused); + if (errorState ^ prevErrorState) { + this.startFocusAnimation(); + } - Animated - .timing(focus, { toValue, duration }) - .start(this.onFocusAnimationEnd); + let labelState = labelStateFromProps(this.props, this.state); + let prevLabelState = labelStateFromProps(prevProps, prevState); + + if (labelState ^ prevLabelState) { + this.startLabelAnimation(); } } - updateRef(name, ref) { - this[name] = ref; + 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); } - focusState(error, focused) { - return error? -1 : (focused? 1 : 0); + 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; } focus() { let { disabled, editable } = this.props; + let { current: input } = this.inputRef; if (!disabled && editable) { - this.input.focus(); + input.focus(); } } blur() { - this.input.blur(); + let { current: input } = this.inputRef; + + input.blur(); } clear() { - this.input.clear(); + let { current: input } = this.inputRef; + + input.clear(); /* onChangeText is not triggered by .clear() */ this.onChangeText(''); } value() { - let { text, receivedFocus } = this.state; - let { value, defaultValue } = this.props; + let { text } = this.state; + let { defaultValue } = this.props; - return (receivedFocus || null != value || null == defaultValue)? - text: - defaultValue; + let value = this.isDefaultVisible()? + defaultValue: + text; + + if (null == value) { + return ''; + } + + return 'string' === typeof value? + value: + String(value); + } + + setValue(text) { + this.setState({ text }); } isFocused() { - return this.input.isFocused(); + let { current: input } = this.inputRef; + + return input.isFocused(); } isRestricted() { - let { characterRestriction } = this.props; - let { text = '' } = this.state; + let { characterRestriction: limit } = this.props; + let { length: count } = this.value(); + + return limit < count; + } - return characterRestriction < text.length; + 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(); } onFocus(event) { let { onFocus, clearTextOnFocus } = this.props; + let { receivedFocus } = this.state; if ('function' === typeof onFocus) { onFocus(event); @@ -220,7 +353,14 @@ export default class TextField extends PureComponent { this.clear(); } - this.setState({ focused: true, receivedFocus: true }); + this.focused = true; + + this.startFocusAnimation(); + this.startLabelAnimation(); + + if (!receivedFocus) { + this.setState({ receivedFocus: true, text: this.value() }); + } } onBlur(event) { @@ -230,26 +370,26 @@ export default class TextField extends PureComponent { onBlur(event); } - this.setState({ focused: false }); + this.focused = false; + + this.startFocusAnimation(); + this.startLabelAnimation(); } onChange(event) { - let { onChange, multiline } = this.props; + let { onChange } = 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 } = this.props; + let { onChangeText, formatText } = this.props; + + if ('function' === typeof formatText) { + text = formatText(text); + } this.setState({ text }); @@ -269,38 +409,130 @@ export default class TextField extends PureComponent { this.setState({ height: Math.max( fontSize * 1.5, - Math.ceil(height) + Platform.select({ ios: 5, android: 1 }) + Math.ceil(height) + Platform.select({ ios: 4, android: 1 }) ), }); } onFocusAnimationEnd() { - if (this.mounted) { - this.setState((state, { error }) => ({ error })); + let { error } = this.props; + let { error: retainedError } = this.state; + + if (this.mounted && !error && retainedError) { + this.setState({ error: null }); } } - renderAccessory() { - let { renderAccessory } = this.props; + inputHeight() { + let { height: computedHeight } = this.state; + let { multiline, fontSize, height = computedHeight } = this.props; - if ('function' !== typeof renderAccessory) { - return null; + 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; + } + + 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, + }]; + } + + return style; + } + + renderLabel(props) { + let offset = this.labelOffset(); + + let { + label, + fontSize, + labelFontSize, + labelTextStyle, + } = this.props; + return ( - - {renderAccessory()} - +