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

feat(Form): Add <PhoneInput /> and <DateInput />; Clean up <Select /> #34

Merged
merged 16 commits into from
Apr 1, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) => {
cehsu marked this conversation as resolved.
Show resolved Hide resolved
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);
cehsu marked this conversation as resolved.
Show resolved Hide resolved
};

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