Skip to content

Commit

Permalink
feat(codefield): add CodeField component
Browse files Browse the repository at this point in the history
  • Loading branch information
dackmin committed May 30, 2020
1 parent 1a34b4c commit 4884efb
Show file tree
Hide file tree
Showing 5 changed files with 417 additions and 0 deletions.
181 changes: 181 additions & 0 deletions lib/CodeField/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import React, {
useReducer,
useEffect,
useRef,
useImperativeHandle,
forwardRef,
} from 'react';
import PropTypes from 'prop-types';

import { classNames, mockState } from '../utils';

const CodeField = forwardRef(({
className,
value,
valid,
autoFocus = false,
disabled = false,
required = false,
size = 6,
validate = val => !!val || !required,
onChange = () => {},
...rest
}, ref) => {
const innerRef = useRef();
const inputsRef = useRef([]);
const [state, dispatch] = useReducer(mockState, {
valid: valid ?? false,
values: value?.split('').slice(0, size) || [],
dirty: false,
});

useEffect(() => {
if (value) {
dispatch({
values: value.split('').slice(0, size),
valid: validate(value),
});
}
}, [value]);

useImperativeHandle(ref, () => ({
innerRef,
inputsRef,
internalValue: state.values?.join(''),
dirty: state.dirty,
valid: state.valid,
focus,
blur,
reset,
}));

const focus = (index = 0) => {
inputsRef.current[index]?.focus();
};

const blur = (index) => {
inputsRef.current[index]?.blur();
};

const reset = () => {
dispatch({
dirty: false,
values: value?.split('').slice(0, size) || [],
valid: valid ?? false,
});
};

const onChange_ = (index, e) => {
if (disabled) {
return;
}

state.values[index] = e?.target?.value || '';
state.valid = validate?.(state.values.join(''),
{ dirty: state.dirty }) || false;
dispatch({ values: state.values, dirty: true, valid: state.valid });
onChange({ value: state.values.join(''), valid: state.valid });

if (state.values[index]) {
focus(index + 1);
}
};

const onKeyDown_ = (index, e) => {
if (disabled) {
return;
}

const current = inputsRef.current?.[index];
const prev = inputsRef.current?.[index - 1];
const next = inputsRef.current?.[index + 1];

switch (e.key) {
case 'Backspace':
/* istanbul ignore if: cannot test selections in jest/enzyme */
if (
current.selectionStart !== current.selectionEnd ||
current.selectionStart === 1 ||
index === 0
) {
return;
}

onChange_(index - 1, { target: { value: '' } });
prev?.focus();
break;

case 'ArrowLeft':
/* istanbul ignore if: cannot test selections in jest/enzyme */
if (current.selectionStart !== current.selectionEnd || index === 0) {
return;
}

prev.selectionStart = current.selectionStart;
prev.selectionEnd = current.selectionStart;
prev.focus();
break;

case 'ArrowRight':
/* istanbul ignore if: cannot test selections in jest/enzyme */
if (
current.selectionStart !== current.selectionEnd ||
index === size - 1
) {
return;
}

next.selectionStart = current.selectionStart;
next.selectionEnd = current.selectionStart;
next.focus();
break;
}
};

return (
<div
{ ...rest }
className={classNames(
'junipero',
'field',
'code',
{
dirty: state.dirty,
invalid: !state.valid && state.dirty,
}
)}
ref={innerRef}
>
<div className="wrapper">
{ Array.from({ length: size }).map((item, index) => (
<input
ref={ref => { inputsRef.current[index] = ref; }}
disabled={disabled}
size={1}
maxLength={1}
autoFocus={index === 0 && autoFocus}
type="tel"
key={index}
value={state.values[index] || ''}
required={required}
onChange={onChange_.bind(null, index)}
onKeyDown={onKeyDown_.bind(null, index)}
/>
)) }
</div>
</div>
);
});

CodeField.propTypes = {
autoFocus: PropTypes.bool,
disabled: PropTypes.bool,
required: PropTypes.bool,
size: PropTypes.number,
valid: PropTypes.bool,
value: PropTypes.string,
validate: PropTypes.func,
onChange: PropTypes.func,
};

export default CodeField;
26 changes: 26 additions & 0 deletions lib/CodeField/index.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from 'react';
import { action } from '@storybook/addon-actions';

import CodeField from './index';

export default { title: 'CodeField' };

export const basic = () => (
<CodeField onChange={action('change')} />
);

export const autoFocused = () => (
<CodeField autoFocus onChange={action('change')} />
);

export const withValue = () => (
<CodeField value="253453" onChange={action('change')} />
);

export const disabled = () => (
<CodeField disabled value="253453" />
);

export const withValidation = () => (
<CodeField validate={val => /^[0-9]+$/g.test(val)} />
);
34 changes: 34 additions & 0 deletions lib/CodeField/index.styl
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
@require "../theme/colors"

.junipero.code
display: inline-block
min-width: 250px

.wrapper
display: flex
align-items: center

input
outline: none
padding: 9px
border-radius: 2px
background: $color-black-squeeze
border: none
margin: 5px
text-align: center
flex: 1 1 auto
font-size: 16px
transition: box-shadow .1s ease-in-out

&:focus
box-shadow: 0 0 0 2px rgba($color-eastern-blue, .5)

&:disabled
opacity: .5

&.invalid.dirty
input
background: $color-lavender-blush

&:focus
box-shadow: 0 0 0 2px rgba($color-monza, .5)
Loading

0 comments on commit 4884efb

Please sign in to comment.