Skip to content
This repository has been archived by the owner on Oct 6, 2020. It is now read-only.

Commit

Permalink
feat(Form): Add <PhoneInput /> and <DateInput />; Clean up <Select /> (
Browse files Browse the repository at this point in the history
…#34)

- Adds new `<PhoneInput />` and `<DateInput />` components for easy input masking
- Fixes some existing styling issues with `<Select />`
  • Loading branch information
kylealwyn authored Apr 1, 2019
1 parent ebfca7a commit 801047a
Show file tree
Hide file tree
Showing 14 changed files with 634 additions and 62 deletions.
44 changes: 42 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
"release": "standard-version"
},
"dependencies": {
"cleave.js": "^1.4.10",
"libphonenumber-js": "^1.7.13",
"lodash": "^4.17.11",
"mitt": "^1.1.3",
"polished": "^2.0.0",
Expand Down
96 changes: 96 additions & 0 deletions src/Form/DateInput.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import React, { useState, useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import DateFormatter from 'cleave.js/src/shortcuts/DateFormatter';
import Input from './Input';
import { createEasyInput } from './EasyInput';
import { getNextCursorPosition } from '../utils';

export const getRawMaxLength = pattern => {
const formatter = new DateFormatter(pattern);
const blocks = formatter.getBlocks();
return blocks.reduce((sum, block) => sum + block, 0);
};

const formatDate = (pattern, delimiter, dateString = '') => {
const formatter = new DateFormatter(pattern);

// Process our date string, bounding values between 1 and 31, and prepending 0s for
// for single digit blocks that can't have 2 numbers, e.g. 5
let tmpDate = formatter.getValidatedDate(`${dateString}`);

// Blocks look something like [2, 2, 4], telling us how long each chunk should be
return formatter.getBlocks().reduce((str, blockLength, index, blockArr) => {
const block = tmpDate.substring(0, blockLength);
if (!block) {
return str;
}

tmpDate = tmpDate.substring(blockLength);

// Append the delimiter if our block is complete and we're not at the last block
const shouldAppendDelimiter = block.length === blockLength && index < blockArr.length - 1;

return `${str}${block}${shouldAppendDelimiter ? delimiter : ''}`;
}, '');
};

function DateInput({ delimiter, pattern, forwardedRef, value: propValue, onKeyDown, onChange, ...inputProps }) {
const format = value => formatDate(pattern, delimiter, value);
const [currentValue, setValue] = useState(format(propValue));
const inputRef = forwardedRef || useRef();

useEffect(() => {
if (propValue !== currentValue) {
setValue(format(propValue));
}
}, [propValue]);

const handleKeyDown = event => {
const isLetterLike = /^\w{1}$/.test(event.key);
if (isLetterLike && currentValue.replace(/\D/g, '').length >= getRawMaxLength(pattern)) {
event.preventDefault();
event.stopPropagation();
}

if (onKeyDown) {
onKeyDown(event);
}
};

const handleChange = (name, newValue, event) => {
const nextValue = newValue.length < currentValue.length ? newValue.trim() : format(newValue);
const nextCursorPosition = getNextCursorPosition(event.target.selectionStart, currentValue, nextValue);

setValue(nextValue);
setTimeout(() => {
inputRef.current.setSelectionRange(nextCursorPosition, nextCursorPosition);
});

if (onChange) {
onChange(name, nextValue, event);
}
};

return (
<Input
forwardedRef={inputRef}
value={currentValue}
onKeyDown={handleKeyDown}
onChange={handleChange}
{...inputProps}
/>
);
}

DateInput.propTypes = {
...Input.propTypes,
pattern: PropTypes.arrayOf(PropTypes.string),
delimiter: PropTypes.string,
};

DateInput.defaultProps = {
pattern: ['m', 'd', 'Y'],
delimiter: '/',
};

export default createEasyInput(DateInput);
87 changes: 87 additions & 0 deletions src/Form/DateInput.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import React from 'react';
import { renderWithTheme, fireEvent, act } from '../../test/utils';
import DateInput, { getRawMaxLength } from './DateInput';
import ThemeProvider from '../ThemeProvider';

describe('<DateInput />', () => {
const renderInput = props => {
const utils = renderWithTheme(<DateInput placeholder="Input" {...props} />);
return {
...utils,
input: utils.getByPlaceholderText('Input'),
};
};

const updateInputValue = (input, value) => {
act(() => {
fireEvent.change(input, { target: { value } });
});
};

test('#getMaxDateLength', () => {
expect(getRawMaxLength(['m', 'd', 'Y'])).toEqual(8);
expect(getRawMaxLength(['m', 'd', 'y'])).toEqual(6);
});

test('snapshot', () => {
const { asFragment } = renderInput();
expect(asFragment()).toMatchSnapshot();
});

test('initally sets the value', () => {
const value = '12/05/1992';
const { input } = renderInput({ value });

expect(input.value).toEqual(value);
});

test("prefixes month and day block with a 0 when second number isn't possible", () => {
const { input } = renderInput();

updateInputValue(input, '4');
expect(input.value).toEqual('04/');

updateInputValue(input, '04/4');
expect(input.value).toEqual('04/04/');
});

test("doesn't prefix when second number is possible", () => {
const { input } = renderInput();

updateInputValue(input, '1');
expect(input.value).toEqual('1');

updateInputValue(input, '12/1');
expect(input.value).toEqual('12/1');
});

test('truncates a date string that is too long', () => {
const { input } = renderInput({ value: '12/05/19922222' });

expect(input.value).toEqual('12/05/1992');
});

test('can remove trailing slash when backspacing', () => {
const { input } = renderInput({ value: '12/05/' });

fireEvent.keyDown(input, { key: 'Backspace' });
updateInputValue(input, '12/05');
expect(input.value).toEqual('12/05');
});

test('takes custom delimter and pattern', () => {
const { input } = renderInput({ value: '2000-5', pattern: ['Y', 'm', 'd'], delimiter: '-' });

expect(input.value).toEqual('2000-05-');
});

test('updates internally when value prop changes', () => {
const { input, rerender } = renderInput({ value: '01/05' });
rerender(
<ThemeProvider>
<DateInput placeholder="input" value="01/09/1990" />
</ThemeProvider>
);
expect(input.value).toEqual('01/09/1990');
});
});
14 changes: 7 additions & 7 deletions src/Form/EasyInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,19 @@ function EasyInput({ name, Component, ...props }) {
const state = useContext(Context);

if (!state) {
return <PureInput name={name} Component={Component} {...props} />
return <PureInput name={name} Component={Component} {...props} />;
}

const value = state.values[name];
const defaultValue = Component.defaultProps && Component.defaultProps.defaultValue !== undefined
? Component.defaultProps.defaultValue
: '';
const defaultValue =
Component.defaultProps && Component.defaultProps.defaultValue !== undefined
? Component.defaultProps.defaultValue
: '';

return (
<PureInput
name={name}
value={value !== undefined ? value : defaultValue }
value={value !== undefined ? value : defaultValue}
error={state.errors[name]}
onChange={state.onChange}
onBlur={state.onBlur}
Expand All @@ -35,7 +36,6 @@ function EasyInput({ name, Component, ...props }) {
}

export const createEasyInput = Component =>
forwardRef((props, ref) =>
<EasyInput Component={Component} forwardedRef={ref} {...props} />);
forwardRef((props, ref) => <EasyInput Component={Component} forwardedRef={ref} {...props} />);

export default EasyInput;
28 changes: 8 additions & 20 deletions src/Form/Formbot.example.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import Select from './Select';
import Formbot, { Context } from './Formbot';
import Form from './Form';
import Button from '../Button';
import FormGroup from './FormGroup';
import Fieldset from './Fieldset';
import CheckboxGroup from './CheckboxGroup';
import RadioGroup from './RadioGroup';
import Switch from './Switch';
import PhoneInput from './PhoneInput';
import DateInput from './DateInput';

const selectValues = [{ id: 1, value: 'male', label: 'Male' }, { id: 1, value: 'female', label: 'Female' }];

Expand Down Expand Up @@ -42,10 +43,8 @@ const radioValues = [

const Values = () => {
const state = useContext(Context);
return (
<pre>{JSON.stringify(state.values, null, 2)}</pre>
)
}
return <pre>{JSON.stringify(state.values, null, 2)}</pre>;
};

export default class FormbotExample extends React.Component {
nameRef = createRef();
Expand Down Expand Up @@ -86,24 +85,13 @@ export default class FormbotExample extends React.Component {
<Fieldset legend="A Group of Inputs">
<Input name="name" placeholder="Name (should autofocus)" label="Name" ref={this.nameRef} />
<Input name="email" placeholder="Email" label="Email" />

<Select
name="gender"
placeholder="Select a Gender"
label="Gender"
options={selectValues}
/>
<PhoneInput name="phone" placeholder="Phone Number" label="Phone" />
<DateInput name="dob" placeholder="MM/DD/YYYY" label="Date of Birth" />
<Select name="gender" placeholder="Select a Gender" label="Gender" options={selectValues} />
</Fieldset>

<Fieldset legend="Another Group of Inputs">
<Input
name="message"
multiline
size="md"
autogrow
placeholder="Your Message"
label="Write a Message"
/>
<Input name="message" multiline size="md" autogrow placeholder="Your Message" label="Write a Message" />

<Input name="favorite_word" placeholder="Favorite Word" label="Favorite Word" />

Expand Down
2 changes: 1 addition & 1 deletion src/Form/Input.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ class Input extends Component {

onChange = e => {
this.setState({ value: e.target.value });
this.props.onChange(e.target.name, e.target.value);
this.props.onChange(e.target.name, e.target.value, e);
};

handleAutogrowRef = node => {
Expand Down
10 changes: 10 additions & 0 deletions src/Form/Input.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ name: Input

import { Playground, PropsTable } from 'docz'
import Input from './Input'
import PhoneInput from './PhoneInput';
import DateInput from './DateInput';
import Button from '../Button'
import Formbot from './Formbot';

Expand Down Expand Up @@ -93,3 +95,11 @@ Form input component. Different sizes are available.
<Input size="lg" placeholder="Example" label="Example" />
<Input size="xl" placeholder="Example" label="Example" />
</Playground>

## Formatted Inputs

### Phone Input
<PhoneInput placeholder="Example" label="Phone Number" value="4088675309" />

### Date Input
<DateInput placeholder="Date of Birth" label="Date of Birth" value="12/05/1992" />
Loading

0 comments on commit 801047a

Please sign in to comment.