|
1 | 1 | import * as React from 'react'; |
2 | | -import styles from './styles.module.css'; |
| 2 | +import './index.css'; |
3 | 3 |
|
4 | | -interface Props { |
5 | | - text: string; |
6 | | -} |
| 4 | +const KEY_CODE = { |
| 5 | + BACKSPACE: 8, |
| 6 | + ARROW_LEFT: 37, |
| 7 | + ARROW_RIGHT: 39, |
| 8 | + DELETE: 46, |
| 9 | +}; |
| 10 | + |
| 11 | +export default ({ length = 4, placeholder = '·' }) => { |
| 12 | + const [activeIndex, setActiveIndex] = React.useState<number>(-1); |
| 13 | + const [value, setValue] = React.useState<string[]>( |
| 14 | + new Array(length).fill(placeholder) |
| 15 | + ); |
| 16 | + |
| 17 | + const codeInputRef = React.createRef<HTMLInputElement>(); |
| 18 | + const itemsRef = React.useMemo( |
| 19 | + () => [...new Array(length)].map(() => React.createRef<HTMLDivElement>()), |
| 20 | + [length] |
| 21 | + ); |
| 22 | + |
| 23 | + const isCodeRegex = new RegExp(`^[0-9]{${length}}$`); |
| 24 | + |
| 25 | + const getItem = (index: number) => itemsRef[index].current!; |
| 26 | + const focusItem = (index: number): void => getItem(index).focus(); |
| 27 | + const blurItem = (index: number): void => getItem(index).blur(); |
| 28 | + |
| 29 | + const onItemFocus = (index: number) => () => { |
| 30 | + setActiveIndex(index); |
| 31 | + if (codeInputRef.current) codeInputRef.current.focus(); |
| 32 | + }; |
| 33 | + |
| 34 | + const onItemKeyUp = ({ key, keyCode }: React.KeyboardEvent) => { |
| 35 | + const newValue = [...value]; |
| 36 | + const nextIndex = activeIndex + 1; |
| 37 | + const prevIndex = activeIndex - 1; |
| 38 | + |
| 39 | + const isLast = nextIndex === length; |
| 40 | + const isDeleting = |
| 41 | + keyCode === KEY_CODE.DELETE || keyCode === KEY_CODE.BACKSPACE; |
| 42 | + |
| 43 | + // keep items focus in sync |
| 44 | + onItemFocus(activeIndex); |
| 45 | + |
| 46 | + // on delete, replace the current value |
| 47 | + // and focus on the previous item |
| 48 | + if (isDeleting) { |
| 49 | + newValue[activeIndex] = placeholder; |
| 50 | + setValue(newValue); |
| 51 | + |
| 52 | + if (activeIndex > 0) { |
| 53 | + setActiveIndex(prevIndex); |
| 54 | + focusItem(prevIndex); |
| 55 | + } |
| 56 | + |
| 57 | + return; |
| 58 | + } |
| 59 | + |
| 60 | + // if the key pressed is not a number |
| 61 | + // don't do anything |
| 62 | + if (Number.isNaN(+key)) return; |
| 63 | + |
| 64 | + // reset the current value |
| 65 | + // and set the new one |
| 66 | + if (codeInputRef.current) codeInputRef.current.value = ''; |
| 67 | + newValue[activeIndex] = key; |
| 68 | + setValue(newValue); |
| 69 | + |
| 70 | + if (!isLast) { |
| 71 | + setActiveIndex(nextIndex); |
| 72 | + focusItem(nextIndex); |
| 73 | + return; |
| 74 | + } |
| 75 | + |
| 76 | + if (codeInputRef.current) codeInputRef.current.blur(); |
| 77 | + getItem(activeIndex).blur(); |
| 78 | + setActiveIndex(-1); |
| 79 | + }; |
| 80 | + |
| 81 | + const onItemChange = (e: React.ChangeEvent<HTMLInputElement>) => { |
| 82 | + const { value: changeValue } = e.target; |
| 83 | + const isCode = isCodeRegex.test(changeValue); |
| 84 | + |
| 85 | + if (!isCode) return; |
| 86 | + setValue(changeValue.split('')); |
| 87 | + blurItem(activeIndex); |
| 88 | + }; |
| 89 | + |
| 90 | + return ( |
| 91 | + <div |
| 92 | + className='ReactVerificationCode__container' |
| 93 | + style={ |
| 94 | + { |
| 95 | + '--activeIndex': activeIndex, |
| 96 | + '--itemsCount': length, |
| 97 | + '--itemWidth': '4.5rem', |
| 98 | + '--itemHeight': '5rem', |
| 99 | + '--itemSpacing': '1rem', |
| 100 | + } as React.CSSProperties |
| 101 | + } |
| 102 | + > |
| 103 | + <input |
| 104 | + ref={codeInputRef} |
| 105 | + className='ReactVerificationCode__input' |
| 106 | + autoComplete='one-time-code' |
| 107 | + type='text' |
| 108 | + inputMode='decimal' |
| 109 | + id='one-time-code' |
| 110 | + // use onKeyUp rather than onChange for a better control |
| 111 | + // onChange is still needed to handle the autocompletion |
| 112 | + // when receiving a code by SMS |
| 113 | + onChange={onItemChange} |
| 114 | + onKeyUp={onItemKeyUp} |
| 115 | + /> |
7 | 116 |
|
8 | | -export const ExampleComponent = ({ text }: Props) => { |
9 | | - return <div className={styles.test}>Example Component: {text}</div>; |
| 117 | + {itemsRef.map((ref, i) => ( |
| 118 | + <div |
| 119 | + key={i} |
| 120 | + ref={ref} |
| 121 | + role='button' |
| 122 | + tabIndex={0} |
| 123 | + className={`ReactVerificationCode__item ${ |
| 124 | + activeIndex === i ? 'is-active' : '' |
| 125 | + }`} |
| 126 | + onFocus={onItemFocus(i)} |
| 127 | + > |
| 128 | + {value[i] || placeholder} |
| 129 | + </div> |
| 130 | + ))} |
| 131 | + </div> |
| 132 | + ); |
10 | 133 | }; |
0 commit comments