diff --git a/package-lock.json b/package-lock.json index 568b78c5..dcad3a87 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58,6 +58,7 @@ "rollup-plugin-uglify": "^6.0.4", "sass": "^1.26.11", "sass-loader": "^8.0.2", + "storybook-addon-state": "^1.0.3", "style-loader": "^1.2.1", "stylelint": "^13.12.0", "stylelint-config-recommended": "^3.0.0", @@ -28298,6 +28299,12 @@ "integrity": "sha512-7t+/wpKLanLzSnQPX8WAcuLCCeuSHoWdQuh9SB3xD0kNOM38DNf+0Oa+wmvxmYueRzkmh6IcdKFtvTa+ecgPDw==", "dev": true }, + "node_modules/storybook-addon-state": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/storybook-addon-state/-/storybook-addon-state-1.0.3.tgz", + "integrity": "sha512-0lpedSYu2xIlq4QK/8YH4Cuj6vt2p8iRCcJBb4JFxyLZ6RAMHFFkf072BKl4WrU1LfLq3/7SD4snvxRoTjvsvw==", + "dev": true + }, "node_modules/stream-browserify": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", @@ -55836,6 +55843,12 @@ "integrity": "sha512-7t+/wpKLanLzSnQPX8WAcuLCCeuSHoWdQuh9SB3xD0kNOM38DNf+0Oa+wmvxmYueRzkmh6IcdKFtvTa+ecgPDw==", "dev": true }, + "storybook-addon-state": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/storybook-addon-state/-/storybook-addon-state-1.0.3.tgz", + "integrity": "sha512-0lpedSYu2xIlq4QK/8YH4Cuj6vt2p8iRCcJBb4JFxyLZ6RAMHFFkf072BKl4WrU1LfLq3/7SD4snvxRoTjvsvw==", + "dev": true + }, "stream-browserify": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", diff --git a/package.json b/package.json index fb1c8e35..ddf9cb62 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,7 @@ "rollup-plugin-uglify": "^6.0.4", "sass": "^1.26.11", "sass-loader": "^8.0.2", + "storybook-addon-state": "^1.0.3", "style-loader": "^1.2.1", "stylelint": "^13.12.0", "stylelint-config-recommended": "^3.0.0", diff --git a/src/components/Input/indexStateless.jsx b/src/components/Input/indexStateless.jsx new file mode 100644 index 00000000..eb898a12 --- /dev/null +++ b/src/components/Input/indexStateless.jsx @@ -0,0 +1,84 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import uuid from 'uuid/v4'; +import { Search } from 'react-feather'; +import InputError from '../InputError'; + +const Input = ({ + ariaLabel, ariaLabelSearchButton, className, disabled, error, errorMessage, handleChange, id, label, negative, placeholder, searchField, submitCallback, type, value, onFocus, onBlur, size, +}) => { + const inputId = id || uuid(); + + const handleKeyDown = e => { + if (e.key === 'Enter') { + submitCallback(value); + } + }; + + return ( +
+ {label && } +
+ handleKeyDown(e) : undefined} + /> + {searchField && ( + + )} +
+ {error && (errorMessage && ( + + ))} +
+ ); +}; + +Input.defaultProps = { + className: '', + disabled: false, + error: false, + handleChange: () => {}, + onFocus: () => {}, + onBlur: () => {}, + negative: false, + searchField: false, + submitCallback: () => {}, + type: 'text', + ariaLabelSearchButton: 'search', + value: '', +}; + +Input.propTypes = { + ariaLabel: PropTypes.string, + ariaLabelSearchButton: PropTypes.string, + className: PropTypes.string, + disabled: PropTypes.bool, + error: PropTypes.bool, + errorMessage: PropTypes.string, + handleChange: PropTypes.func, + onFocus: PropTypes.func, + onBlur: PropTypes.func, + id: PropTypes.string, + label: PropTypes.string, + negative: PropTypes.bool, + placeholder: PropTypes.string, + searchField: PropTypes.bool, + size: PropTypes.string, + submitCallback: PropTypes.func, + type: PropTypes.string, + value: PropTypes.string, +}; + +export default Input; diff --git a/src/components/Input/indexUpdateOnValueChange.jsx b/src/components/Input/indexUpdateOnValueChange.jsx new file mode 100644 index 00000000..9951e257 --- /dev/null +++ b/src/components/Input/indexUpdateOnValueChange.jsx @@ -0,0 +1,94 @@ +import React, { useState, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import uuid from 'uuid/v4'; +import { Search } from 'react-feather'; +import InputError from '../InputError'; + +const Input = ({ + ariaLabel, ariaLabelSearchButton, className, disabled, error, errorMessage, handleChange, id, label, negative, placeholder, searchField, submitCallback, type, value, onFocus, onBlur, size, +}) => { + const [inputValue, setValue] = useState(value); + const inputId = id || uuid(); + + useEffect(() => { + setValue(value); + }, [value]); + + const handleInputChange = e => { + setValue(e.target.value); + handleChange(e); + }; + + const handleKeyDown = e => { + if (e.key === 'Enter') { + submitCallback(inputValue); + } + }; + + return ( +
+ {label && } +
+ handleInputChange(e)} + onFocus={onFocus} + onBlur={onBlur} + placeholder={placeholder} + aria-label={ariaLabel} + className={searchField || error ? ' with-icon' : ''} + onKeyDown={searchField ? e => handleKeyDown(e) : undefined} + /> + {searchField && ( + + )} +
+ {error && (errorMessage && ( + + ))} +
+ ); +}; + +Input.defaultProps = { + className: '', + disabled: false, + error: false, + handleChange: () => {}, + onFocus: () => {}, + onBlur: () => {}, + negative: false, + searchField: false, + submitCallback: () => {}, + type: 'text', + ariaLabelSearchButton: 'search', + value: '', +}; + +Input.propTypes = { + ariaLabel: PropTypes.string, + ariaLabelSearchButton: PropTypes.string, + className: PropTypes.string, + disabled: PropTypes.bool, + error: PropTypes.bool, + errorMessage: PropTypes.string, + handleChange: PropTypes.func, + onFocus: PropTypes.func, + onBlur: PropTypes.func, + id: PropTypes.string, + label: PropTypes.string, + negative: PropTypes.bool, + placeholder: PropTypes.string, + searchField: PropTypes.bool, + size: PropTypes.string, + submitCallback: PropTypes.func, + type: PropTypes.string, + value: PropTypes.string, +}; + +export default Input; diff --git a/src/components/Input/input.story.jsx b/src/components/Input/input.story.jsx index 04b080db..c01e556d 100644 --- a/src/components/Input/input.story.jsx +++ b/src/components/Input/input.story.jsx @@ -1,6 +1,7 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; import centered from '@storybook/addon-centered/react'; +import useState from 'storybook-addon-state'; import Input from './index'; let someValue = ''; @@ -13,6 +14,25 @@ const handleSubmit = e => { }; storiesOf('Input', module).addDecorator(centered) + .add('with state', () => { + /** + * Just an example to show Input component works fine when the app state + * (which supplies the prop value) changes. Here appValue is shared across + * two components and updating one reflects in the other + */ + const [appValue, setAppValue] = useState('appVal', ''); + + const updateValue = value => { + setAppValue(value); + }; + + return ( +
+ + +
+ ); + }) .add('Default', () => (
diff --git a/src/components/Input/inputStateless.story.jsx b/src/components/Input/inputStateless.story.jsx new file mode 100644 index 00000000..a80df653 --- /dev/null +++ b/src/components/Input/inputStateless.story.jsx @@ -0,0 +1,90 @@ +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import centered from '@storybook/addon-centered/react'; +import useState from 'storybook-addon-state'; +import Input from './index'; + +let someValue = ''; +const handleChange = e => { + someValue = e.target.value; +}; + +const handleSubmit = e => { + console.log(e.target.value); +}; + +storiesOf('Input', module).addDecorator(centered) + .add('with state', () => { + /** + * Just an example to show Input component works fine when the app state + * (which supplies the prop value) changes. Here appValue is shared across + * two components and updating one reflects in the other + */ + const [appValue, setAppValue] = useState('appVal', ''); + + const updateValue = e => { + setAppValue(e.target.value); + }; + + return ( +
+ + +
+ ); + }) + .add('Default', () => ( +
+ +
+ )) + .add('Search field', () => ( +
+ +
+ )) + .add('With value', () => ( +
+ +
+ )) + .add('Disabled', () => ( +
+ +
+ )) + .add('Error', () => ( +
+ +
+ )) + .add('Negative', () => ( +
+ + + +
+ )) + .add('Large input', () => ( +
+ +
+ )); diff --git a/src/components/Input/inputUpdateOnValueChange.story.jsx b/src/components/Input/inputUpdateOnValueChange.story.jsx new file mode 100644 index 00000000..5b94867f --- /dev/null +++ b/src/components/Input/inputUpdateOnValueChange.story.jsx @@ -0,0 +1,90 @@ +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import centered from '@storybook/addon-centered/react'; +import useState from 'storybook-addon-state'; +import Input from './indexUpdateOnValueChange'; + +let someValue = ''; +const handleChange = e => { + someValue = e.target.value; +}; + +const handleSubmit = e => { + console.log(e); +}; + +storiesOf('Input', module).addDecorator(centered) + .add('with state', () => { + /** + * Just an example to show Input component works fine when the app state + * (which supplies the prop value) changes. Here appValue is shared across + * two components and updating one reflects in the other + */ + const [appValue, setAppValue] = useState('appVal', ''); + + const updateValue = value => { + setAppValue(value); + }; + + return ( +
+ + +
+ ); + }) + .add('Default', () => ( +
+ +
+ )) + .add('Search field', () => ( +
+ +
+ )) + .add('With value', () => ( +
+ +
+ )) + .add('Disabled', () => ( +
+ +
+ )) + .add('Error', () => ( +
+ +
+ )) + .add('Negative', () => ( +
+ + + +
+ )) + .add('Large input', () => ( +
+ +
+ ));