Skip to content

Instantly share code, notes, and snippets.

@olecksamdr
Created October 23, 2018 07:37
Show Gist options
  • Save olecksamdr/b2e15fdef8b02642f6a3e5349e15251f to your computer and use it in GitHub Desktop.
Save olecksamdr/b2e15fdef8b02642f6a3e5349e15251f to your computer and use it in GitHub Desktop.
Pin Input Component
import React, { PureComponent } from 'react';
import { number, func, bool } from 'prop-types';
import { PinItem } from './style';
const BACKSPACE_KEY = 8;
const LEFT_ARROW_KEY = 37;
const UP_ARROW_KEY = 38;
const RIGHT_ARROW_KEY = 39;
const DOWN_ARROW_KEY = 40;
class PinInput extends PureComponent {
constructor(props) {
super(props);
this.state = ({
values: new Array(props.length).fill(''),
});
this.elements = new Array(props.length);
this.maxIndex = props.length - 1;
}
onChange = ({ target: { value } }, currentIndex) => {
const { length, onComplete, onChange } = this.props;
const val = value.replace(/[^\d]/g, '');
const values = this.update(currentIndex, val);
this.setState({ values });
if (val.length === 1) {
this.focusNext(currentIndex);
}
// Notify the parent
const fullValue = values.join('');
onChange(fullValue);
if (fullValue.length === length) {
onComplete(fullValue);
}
};
onKeyDown = (event, currentIndex) => {
switch (event.keyCode) {
case BACKSPACE_KEY:
event.preventDefault();
const values = this.update(currentIndex, '');
this.props.onChange(values.join(''));
this.setState({ values });
this.focusPrev(currentIndex);
break;
case LEFT_ARROW_KEY:
event.preventDefault();
this.focusPrev(currentIndex);
break;
case RIGHT_ARROW_KEY:
event.preventDefault();
this.focusNext(currentIndex);
break;
case UP_ARROW_KEY:
case DOWN_ARROW_KEY:
event.preventDefault();
}
};
update(currentIndex, value) {
const values = this.state.values.slice();
if (value.length > 1) {
value.split('').forEach((char, charIndex) => {
const valueIndex = currentIndex + charIndex;
if (valueIndex <= this.maxIndex) {
values[valueIndex] = char;
this.elements[valueIndex].focus();
}
});
} else {
values[currentIndex] = value;
}
return values;
}
focusPrev(currentIndex) {
if (currentIndex > 0) {
this.elements[currentIndex - 1].focus();
}
}
focusNext(currentIndex) {
if (currentIndex < this.maxIndex) {
this.elements[currentIndex + 1].focus();
}
}
render() {
const { onChange, onKeyDown } = this;
const { disable, invalid } = this.props;
return (
<div>
{this.state.values.map((value, index) => (
<PinItem
// eslint-disable-next-line react/no-array-index-key
key={index}
autoFocus={index === 0}
innerRef={(ref) => {
this.elements[index] = ref;
}}
onFocus={event => event.target.select()}
onChange={event => onChange(event, index)}
onKeyDown={event => onKeyDown(event, index)}
{...{ value, disable, invalid }}
/>
))}
</div>
);
}
}
PinInput.propTypes = {
disable: bool,
invalid: bool,
length: number,
onComplete: func.isRequired,
onChange: func,
};
PinInput.defaultProps = {
length: 4,
disable: false,
invalid: false,
onChange: () => {},
};
export default PinInput;
import styled from 'styled-components';
import { getThemeColor, getThemeFont } from 'utils/theme';
import { invalidStyles } from 'components/global/Input';
export const PinItem = styled.input.attrs({
autoComplete: 'off',
inputMode: 'numeric',
placeholder: '-',
})`
width: 78px;
padding: 20px 26px;
display: inline-block;
position: relative;
border-radius: 4px;
font-size: 2.5rem;
line-height: 1.2;
border: 1px solid ${getThemeColor(['cloudDark'])};
font-weight: ${getThemeFont(['semibold'])};
&:not(:last-child) {
margin-right: 8px;
}
::placeholder {
text-align: center;
color: ${getThemeColor(['cloudDark'])};
}
${invalidStyles};
`;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment