Skip to content

Commit

Permalink
feat(atoms): add mask feature to the input
Browse files Browse the repository at this point in the history
  • Loading branch information
ej9x committed May 29, 2018
1 parent a02fa91 commit 7e1c096
Show file tree
Hide file tree
Showing 9 changed files with 146 additions and 56 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
"final-form": "^4.6.1",
"prop-types": "^15.6.1",
"react-final-form": "^3.4.0",
"react-input-mask": "^2.0.1",
"react-onclickoutside": "^6.7.1",
"react-popper": "^0.10.1",
"react-portal": "^4.1.5",
Expand Down
72 changes: 49 additions & 23 deletions src/atoms/dataEntry/Input/Input.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// @flow

import React, { PureComponent } from 'react';
import { InputWrapperTag, InputTag, InputIndicatorTag, InputRightIconTag, InputLeftIconTag } from './Input.theme';
import fp from 'lodash/fp';
import { InputWrapperTag, InputTag, InputIndicatorTag, InputRightIconTag, InputLeftIconTag, InputMaskStyled } from './Input.theme';

type InputCommonProps = {
/** field placeholder */
Expand All @@ -11,24 +12,30 @@ type InputCommonProps = {
/** when true then stretch to the maximal width */
stretch?: boolean,
/** when true then don't show error indicator */
hideIndicator?: boolean,
hideErrorIndicator?: boolean,
/** left icon componen */
leftIcon?: React$Node,
/** right icon componen */
rightIcon?: React$Node,
/** max symbols in the input value*/
maxLength?: number,
}

type InputProps = {
/** input name */
name?: string,
/** input value */
value?: string,
value?: string | number,
/** possible input types */
type?: 'text' | 'number' | 'password' | 'email' | 'url',
/** then true when show error styles */
hasError?: boolean,
/** text of the error */
errorText?: string,
/** mask string in the react-input-mask format */
mask?: string,
/** set input width to the equal height */
square?: boolean,
/** callback to change input value */
onChange?: (value?: string | number, event?: SyntheticInputEvent<HTMLInputElement>) => void,
onFocus?: (?SyntheticFocusEvent<HTMLInputElement>) => void,
Expand All @@ -38,41 +45,60 @@ type InputProps = {
class Input extends PureComponent<InputProps> {
static defaultProps = {
type: 'text',
square: false,
stretch: true,
hideIndicator: false,
hideErrorIndicator: false,
autoComplete: false,
hasError: false,
}

onChange = (event: *) => {
const { onChange, type } = this.props;
const { onChange, type, maxLength } = this.props;
const { value } = event.target;
if (type === 'number') {
onChange && onChange(Number(value) || undefined, event);
}
else {
onChange && onChange(value, event);
const hasNotMaxLength = maxLength === undefined;

if (value.toString().length <= maxLength || hasNotMaxLength) {
if (type === 'number') {
onChange && onChange(Number(value) || undefined, event);
}
else {
onChange && onChange(value, event);
}
}
}

render() {
const { hasError, hideIndicator, autoComplete, stretch, errorText, leftIcon, rightIcon, ...rest } = this.props;
const { hasError, hideErrorIndicator, autoComplete, stretch, errorText, leftIcon, rightIcon, mask, square, value, ...rest } = this.props;
const hasLeftIcon = !!leftIcon;
const hasRightIcon = !!rightIcon;
const htmlAutoComplete = autoComplete ? 'on' : 'off';

const inputProps = {
value,
square,
onChange: this.onChange,
hasError,
hasLeftIcon,
hasRightIcon,
autoComplete: autoComplete ? 'on' : 'off',
};

return (
<InputWrapperTag stretch={ stretch } tagName="div">
<InputTag
{ ...rest }
hasError={ hasError }
hasLeftIcon={ hasLeftIcon }
hasRightIcon={ hasRightIcon }
onChange={ this.onChange }
autoComplete={ htmlAutoComplete }
tagName="input"
/>
<If condition={ !!hasError && !hideIndicator }>
<InputWrapperTag { ...fp.omit(['onChange'], rest) } stretch={ stretch } square={ square } tagName="div">
<Choose>
<When condition={ !mask }>
<InputTag
{ ...inputProps }
tagName="input"
/>
</When>
<Otherwise >
<InputMaskStyled
{ ...inputProps }
mask={ mask }
/>
</Otherwise>
</Choose>
<If condition={ !!hasError && !hideErrorIndicator }>
<InputIndicatorTag hasError={ hasError } tagName="div" />
</If>
<If condition={ hasLeftIcon }>
Expand Down
7 changes: 3 additions & 4 deletions src/atoms/dataEntry/Input/Input.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ export default (asStory) => {
.add('with error', () => (
<Input name="input" hasError onChange={ () => null } />
))
.add('with stretch', () => (
<Input name="input" stretch onChange={ () => null } />
.add('with stretch=false', () => (
<Input name="input" stretch={ false } onChange={ () => null } />
))
.add('with left icon', () => (
<Input name="input" leftIcon="i" />
Expand All @@ -25,11 +25,10 @@ export default (asStory) => {
<Input name="input" rightIcon="i" />
))
.add('with right icon and error', () => (
<Input name="input" rightIcon="i" hasError hideIndicator />
<Input name="input" rightIcon="i" hasError hideErrorIndicator />
))
.add('with html auto-complete', () => (
<Input name="input" autoComplete />
));
});
};

11 changes: 11 additions & 0 deletions src/atoms/dataEntry/Input/Input.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,15 @@ describe('<Input />', () => {
expect(onChange.mock.calls[1][0]).toBe(42);
expect(typeof onChange.mock.calls[1][0]).toBe('number');
});

it('should not call onCahnge with max value', () => {
const onChange = jest.fn();
const wrapper = mount(<Input onChange={ onChange } maxLength={ 2 } />);

wrapper.find('input').simulate('change', { target: { value: '123' }});
expect(onChange).not.toHaveBeenCalled();

wrapper.find('input').simulate('change', { target: { value: '12' }});
expect(onChange.mock.calls[0][0]).toBe('12');
});
});
62 changes: 37 additions & 25 deletions src/atoms/dataEntry/Input/Input.theme.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// @flow

import { createStyledTag, createTheme, getThemeStyle } from 'utils';
import styled from 'react-emotion';
import InputMask from 'react-input-mask';
import { createStyledTag, createTheme, getThemeStyle, getThemeStyleByCond } from 'utils';

const name = 'input';

Expand All @@ -20,6 +21,16 @@ const theme = createTheme(name, (colors: *): * => ({
},
},

inputSquare: {
width: '4rem',
textAlign: 'center',
padding: 0,
},

inputError: {
borderColor: `${colors.DANGER} !important`,
},

inputIndicator: {
right: '1rem',
top: '50%',
Expand All @@ -30,20 +41,14 @@ const theme = createTheme(name, (colors: *): * => ({
borderRadius: '50%',
},

modifiers: {
hasError: {
borderColor: `${colors.DANGER} !important`,
},
},
defaults: {
},
}));

const InputWrapperTag = createStyledTag(name, props => ({
display: 'inline-flex',
position: 'relative',

width: props.stretch ? '100%' : 'auto',
width: props.stretch && !props.square ? '100%' : 'auto',
}));

const InputIndicatorTag = createStyledTag(name, props => ({
Expand All @@ -52,21 +57,6 @@ const InputIndicatorTag = createStyledTag(name, props => ({
...getThemeStyle(props, name).inputIndicator,
}));

const InputTag = createStyledTag(name, props => ({
width: '100%',
outline: 'none',
paddingLeft: props.hasLeftIcon ? '3rem' : '1rem',
paddingRight: props.hasRightIcon ? '3rem' : '2rem',

...getThemeStyle(props, name).input,

'&::-webkit-outer-spin-button, &::-webkit-inner-spin-button': {
'-webkit-appearance': 'none',
margin: 0,
},
}));


const iconsStyles = {
position: 'absolute',
display: 'flex',
Expand All @@ -86,4 +76,26 @@ const InputRightIconTag = createStyledTag(name, {
right: 0,
});

export { InputWrapperTag, InputTag, InputIndicatorTag, InputRightIconTag, InputLeftIconTag, theme };
const getInputStyles = props => ({
width: '100%',
outline: 'none',
paddingLeft: props.hasLeftIcon ? '3rem' : '1rem',
paddingRight: props.hasRightIcon ? '3rem' : '2rem',

...getThemeStyle(props, name).input,
...getThemeStyleByCond(props, name, 'inputError', props.hasError),
...getThemeStyleByCond(props, name, 'inputSquare', props.square),

'&::-webkit-outer-spin-button, &::-webkit-inner-spin-button': {
'-webkit-appearance': 'none',
margin: 0,
},
});

const InputTag = createStyledTag(name, props => ({
...getInputStyles(props),
}));

const InputMaskStyled = styled(InputMask)(getInputStyles);

export { InputWrapperTag, InputTag, InputIndicatorTag, InputRightIconTag, InputLeftIconTag, InputMaskStyled, theme };
22 changes: 20 additions & 2 deletions src/atoms/dataEntry/InputField/InputField.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ type InputFieldProps = {|
label?: string,
/** when true then stretch to the maximal width */
stretch?: boolean,
/** direction of the input with label */
direction?: 'row' | 'column',
/** set input width to the equal height */
square?: boolean,
/** max symbols in the input value*/
maxLength?: number,
/** when true then don't show error label */
hideErrorLabel?: boolean,
/** when true then don't show error indicator */
hideErrorIndicator?: boolean,
/** form input object */
input?: InputType,
/** form meta object */
Expand All @@ -25,8 +35,13 @@ const theme = createTheme(name, {
});

const InputField = ({
square,
label,
stretch,
direction,
maxLength,
hideErrorLabel,
hideErrorIndicator,
input = {},
meta = {},
...rest
Expand All @@ -36,9 +51,12 @@ const InputField = ({
const hasError = !!error && !!touched;

return (
<FormField label={ label } stretch={ stretch } input={ input } meta={ meta }>
<FormField label={ label } stretch={ stretch } direction={ direction } hideErrorLabel={ hideErrorLabel } input={ input } meta={ meta }>
<Input
{ ...rest }
hideErrorIndicator={ hideErrorIndicator }
maxLength={ maxLength }
square={ square }
name={ name }
onChange={ onChange }
value={ value }
Expand All @@ -49,7 +67,7 @@ const InputField = ({
};

InputField.defaultProps = {
stretch: false,
stretch: true,
type: 'text',
input: {},
meta: {},
Expand Down
13 changes: 12 additions & 1 deletion src/atoms/dataEntry/InputField/InputField.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,18 @@ export default (asStory) => {
))
.add('without label', () => (
<InputField input={{ name: 'input', onChange }} />
))
.add('without error label and indicator', () => (
<InputField hideErrorLabel hideErrorIndicator input={{ name: 'input', onChange }} meta={{ error: 'Required', touched: true }} />
))
.add('with row direction', () => (
<InputField direction="row" label="Input" input={{ name: 'input', onChange }} />
))
.add('with row direction stretch=false', () => (
<InputField direction="row" stretch={ false } label="Input" input={{ name: 'input', onChange }} />
))
.add('with square input', () => (
<InputField direction="row" stretch={ false } square label="Input" input={{ name: 'input', onChange }} />
));
});
};

5 changes: 5 additions & 0 deletions src/utils/themeSelectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,8 @@ import fp from 'lodash/fp';
export const getThemeStyle = (props: Object, themeName: string) => fp.getOr({}, ['theme', themeName], props);

export const getThemeColors = (props: Object) => fp.getOr({}, ['theme', 'COLORS'], props);

export const getThemeStyleByCond = (props: Object, themeName: string, styleName: string, cond: boolean) =>
cond
? getThemeStyle(props, themeName)[styleName]
: {};
9 changes: 8 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4722,7 +4722,7 @@ interpret@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614"

invariant@^2.2.1, invariant@^2.2.2:
invariant@^2.2.1, invariant@^2.2.2, invariant@^2.2.4:
version "2.2.4"
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
dependencies:
Expand Down Expand Up @@ -7441,6 +7441,13 @@ react-icons@^2.2.7:
dependencies:
react-icon-base "2.1.0"

react-input-mask@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/react-input-mask/-/react-input-mask-2.0.1.tgz#fa577d24cd1e0c64d297904be618d0f4b893e773"
dependencies:
invariant "^2.2.4"
warning "^3.0.0"

react-inspector@^2.2.2:
version "2.3.0"
resolved "https://registry.yarnpkg.com/react-inspector/-/react-inspector-2.3.0.tgz#fc9c1d38ab687fc0d190dcaf133ae40158968fc8"
Expand Down

0 comments on commit 7e1c096

Please sign in to comment.