├── .travis.yml ├── src ├── react-app-env.d.ts ├── .eslintrc ├── typings.d.ts ├── index.tsx └── index.test.tsx ├── .eslintignore ├── example ├── src │ ├── react-app-env.d.ts │ ├── index.tsx │ ├── setupTests.ts │ ├── App.test.tsx │ ├── index.css │ └── App.tsx ├── public │ ├── favicon.ico │ ├── manifest.json │ └── index.html ├── README.md ├── tsconfig.json └── package.json ├── tsconfig.test.json ├── .editorconfig ├── .prettierrc ├── dist ├── index.css ├── index.modern.js ├── index.js ├── index.js.map └── index.modern.js.map ├── .gitignore ├── .eslintrc ├── tsconfig.json ├── .github └── workflows │ └── main.yml ├── LICENSE ├── package.json └── README.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 12 4 | - 10 5 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | node_modules/ 4 | .snapshots/ 5 | *.min.js -------------------------------------------------------------------------------- /example/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /example/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drac94/react-auth-code-input/HEAD/example/public/favicon.ico -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs" 5 | } 6 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /example/src/index.tsx: -------------------------------------------------------------------------------- 1 | import './index.css'; 2 | 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import App from './App'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "jsxSingleQuote": true, 4 | "tabWidth": 2, 5 | "bracketSpacing": true, 6 | "jsxBracketSameLine": false, 7 | "arrowParens": "always", 8 | "trailingComma": "none" 9 | } 10 | -------------------------------------------------------------------------------- /dist/index.css: -------------------------------------------------------------------------------- 1 | /* add css module styles here (optional) */ 2 | 3 | ._styles-module__test__3ybTi { 4 | margin: 2em; 5 | padding: 0.5em; 6 | border: 2px solid #000; 7 | font-size: 2em; 8 | text-align: center; 9 | } 10 | -------------------------------------------------------------------------------- /example/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | This example was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | It is linked to the react-auth-code-input package in the parent directory for development purposes. 4 | 5 | You can run `yarn install` and then `yarn start` to test your package. 6 | -------------------------------------------------------------------------------- /example/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # See https://help.github.com/ignore-files/ for more about ignoring files. 3 | 4 | # dependencies 5 | node_modules 6 | 7 | # builds 8 | build 9 | dist 10 | .rpt2_cache 11 | 12 | # misc 13 | .DS_Store 14 | .env 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | -------------------------------------------------------------------------------- /example/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "react-auth-code-input", 3 | "name": "react-auth-code-input", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Default CSS definition for typescript, 3 | * will be overridden with file-specific definitions by rollup 4 | */ 5 | declare module '*.css' { 6 | const content: { [className: string]: string }; 7 | export default content; 8 | } 9 | 10 | interface SvgrComponent 11 | extends React.StatelessComponent> {} 12 | 13 | declare module '*.svg' { 14 | const svgUrl: string; 15 | const svgComponent: SvgrComponent; 16 | export default svgUrl; 17 | export { svgComponent as ReactComponent }; 18 | } 19 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": [ 4 | "standard", 5 | "standard-react", 6 | "plugin:prettier/recommended", 7 | "prettier/standard", 8 | "prettier/react", 9 | "plugin:@typescript-eslint/eslint-recommended" 10 | ], 11 | "env": { 12 | "node": true 13 | }, 14 | "parserOptions": { 15 | "ecmaVersion": 2020, 16 | "ecmaFeatures": { 17 | "legacyDecorators": true, 18 | "jsx": true 19 | } 20 | }, 21 | "settings": { 22 | "react": { 23 | "version": "16" 24 | } 25 | }, 26 | "rules": { 27 | "space-before-function-paren": 0, 28 | "react/prop-types": 0, 29 | "react/jsx-handler-names": 0, 30 | "react/jsx-fragments": 0, 31 | "react/no-unused-prop-types": 0, 32 | "import/export": 0 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "module": "esnext", 5 | "lib": ["dom", "esnext"], 6 | "moduleResolution": "node", 7 | "jsx": "react", 8 | "sourceMap": true, 9 | "declaration": true, 10 | "esModuleInterop": true, 11 | "noImplicitReturns": true, 12 | "noImplicitThis": true, 13 | "noImplicitAny": true, 14 | "strictNullChecks": true, 15 | "suppressImplicitAnyIndexErrors": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "allowSyntheticDefaultImports": true, 19 | "target": "es5", 20 | "allowJs": true, 21 | "skipLibCheck": true, 22 | "strict": true, 23 | "forceConsistentCasingInFileNames": true, 24 | "resolveJsonModule": true, 25 | "isolatedModules": true, 26 | "noEmit": true 27 | }, 28 | "include": ["src"], 29 | "exclude": ["node_modules", "dist", "example"] 30 | } 31 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "module": "esnext", 5 | "lib": [ 6 | "dom", 7 | "esnext" 8 | ], 9 | "moduleResolution": "node", 10 | "jsx": "react", 11 | "sourceMap": true, 12 | "declaration": true, 13 | "esModuleInterop": true, 14 | "noImplicitReturns": true, 15 | "noImplicitThis": true, 16 | "noImplicitAny": true, 17 | "strictNullChecks": true, 18 | "suppressImplicitAnyIndexErrors": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "allowSyntheticDefaultImports": true, 22 | "target": "es5", 23 | "allowJs": true, 24 | "skipLibCheck": true, 25 | "strict": true, 26 | "forceConsistentCasingInFileNames": true, 27 | "resolveJsonModule": true, 28 | "isolatedModules": true, 29 | "noEmit": true 30 | }, 31 | "include": [ 32 | "src" 33 | ], 34 | "exclude": [ 35 | "node_modules", 36 | "build" 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-auth-code-input-example", 3 | "homepage": "https://drac94.github.io/react-auth-code-input", 4 | "version": "0.0.0", 5 | "private": true, 6 | "dependencies": { 7 | "react": "link:../node_modules/react", 8 | "react-auth-code-input": "link:..", 9 | "react-dom": "link:../node_modules/react-dom", 10 | "react-scripts": "link:../node_modules/react-scripts" 11 | }, 12 | "scripts": { 13 | "start": "node ../node_modules/react-scripts/bin/react-scripts.js start", 14 | "build": "node ../node_modules/react-scripts/bin/react-scripts.js build", 15 | "test": "node ../node_modules/react-scripts/bin/react-scripts.js test", 16 | "eject": "node ../node_modules/react-scripts/bin/react-scripts.js eject" 17 | }, 18 | "eslintConfig": { 19 | "extends": "react-app" 20 | }, 21 | "browserslist": [ 22 | ">0.2%", 23 | "not dead", 24 | "not ie <= 11", 25 | "not op_mini all" 26 | ], 27 | "devDependencies": {} 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CI 4 | 5 | # Controls when the action will run. 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the master branch 8 | push: 9 | branches: [ master ] 10 | pull_request: 11 | branches: [ master ] 12 | 13 | # Allows you to run this workflow manually from the Actions tab 14 | workflow_dispatch: 15 | 16 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 17 | jobs: 18 | # This workflow contains a single job called "build" 19 | build: 20 | # The type of runner that the job will run on 21 | runs-on: ubuntu-latest 22 | 23 | # Steps represent a sequence of tasks that will be executed as part of the job 24 | steps: 25 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 26 | - uses: actions/checkout@v2 27 | 28 | # Rinstall dependencies 29 | - name: Install dependencies 30 | run: npm install 31 | 32 | # Runs tests 33 | - name: Run tests 34 | run: npm run test 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-present Luis Guerrero 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /example/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 5 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 6 | sans-serif; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | text-align: center; 10 | color: #494949; 11 | } 12 | 13 | .main { 14 | display: flex; 15 | flex-direction: column; 16 | align-items: center; 17 | } 18 | 19 | .badges > * { 20 | margin-right: 8px; 21 | } 22 | .container { 23 | padding: 16px; 24 | } 25 | 26 | .input { 27 | width: 45px; 28 | height: 45px; 29 | padding: 0; 30 | font-size: 24px; 31 | text-align: center; 32 | margin-right: 12px; 33 | text-transform: uppercase; 34 | color: #494949; 35 | font-family: SF Pro Text, SF Pro Icons, Helvetica Neue, Helvetica, Arial, 36 | sans-serif; 37 | border: 1px solid #d6d6d6; 38 | border-radius: 4px; 39 | background: #fff; 40 | background-clip: padding-box; 41 | } 42 | 43 | .input:focus { 44 | appearance: none; 45 | outline: 0; 46 | box-shadow: 0 0 0 3px rgb(131 192 253 / 50%); 47 | } 48 | 49 | button { 50 | appearance: none; 51 | background: #0071e3; 52 | border-radius: 980px; 53 | text-align: center; 54 | border: none; 55 | color: #fff; 56 | margin: 8px; 57 | padding: 4px 11px; 58 | } 59 | 60 | p { 61 | font-size: 17px; 62 | line-height: 1.47059; 63 | } 64 | 65 | .props { 66 | border: 1px solid #d6d6d6; 67 | padding: 8px 16px; 68 | } 69 | 70 | .options { 71 | display: flex; 72 | justify-content: space-between; 73 | align-items: center; 74 | } 75 | 76 | .allowed-characters { 77 | text-align: left; 78 | } 79 | -------------------------------------------------------------------------------- /example/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | 16 | 17 | 21 | 22 | 23 | 32 | react-auth-code-input 33 | 34 | 35 | 36 | 39 | 40 |
41 | 42 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-auth-code-input", 3 | "version": "3.2.1", 4 | "description": "One-time password (OTP) React input component, uncontrolled, zero dependencies, fully tested.", 5 | "author": "drac94", 6 | "license": "MIT", 7 | "repository": "drac94/react-auth-code-input", 8 | "main": "dist/index.js", 9 | "module": "dist/index.modern.js", 10 | "source": "src/index.tsx", 11 | "engines": { 12 | "node": ">=10" 13 | }, 14 | "scripts": { 15 | "build": "microbundle-crl --no-compress --format modern,cjs", 16 | "start": "microbundle-crl watch --no-compress --format modern,cjs", 17 | "prepublish": "run-s build", 18 | "test": "run-s test:unit test:lint test:build", 19 | "test:build": "run-s build", 20 | "test:lint": "eslint --max-warnings 0 --ext .js,.jsx,.ts,.tsx src/", 21 | "test:unit": "cross-env CI=1 react-scripts test --env=jest-environment-jsdom-sixteen", 22 | "test:watch": "react-scripts test --env=jest-environment-jsdom-sixteen", 23 | "predeploy": "cd example && yarn install && yarn run build", 24 | "deploy": "gh-pages -d example/build", 25 | "format": "prettier --write './**/*.{js,jsx,ts,tsx,css, md}'" 26 | }, 27 | "peerDependencies": { 28 | "react": ">=16.0.0" 29 | }, 30 | "devDependencies": { 31 | "@testing-library/jest-dom": "^5.11.10", 32 | "@testing-library/react": "^11.2.6", 33 | "@testing-library/user-event": "^13.1.1", 34 | "@types/jest": "^25.1.4", 35 | "@types/node": "^12.12.38", 36 | "@types/react": "^16.9.27", 37 | "@types/react-dom": "^16.9.7", 38 | "@typescript-eslint/eslint-plugin": "^2.26.0", 39 | "@typescript-eslint/parser": "^2.26.0", 40 | "babel-eslint": "^10.0.3", 41 | "cross-env": "^7.0.2", 42 | "eslint": "^6.8.0", 43 | "eslint-config-prettier": "^6.7.0", 44 | "eslint-config-standard": "^14.1.0", 45 | "eslint-config-standard-react": "^9.2.0", 46 | "eslint-plugin-import": "^2.18.2", 47 | "eslint-plugin-node": "^11.0.0", 48 | "eslint-plugin-prettier": "^3.1.1", 49 | "eslint-plugin-promise": "^4.2.1", 50 | "eslint-plugin-react": "^7.17.0", 51 | "eslint-plugin-standard": "^4.0.1", 52 | "gh-pages": "^2.2.0", 53 | "jest-environment-jsdom-sixteen": "^1.0.3", 54 | "microbundle-crl": "^0.13.10", 55 | "npm-run-all": "^4.1.5", 56 | "prettier": "^2.0.4", 57 | "react": "^16.13.1", 58 | "react-dom": "^16.13.1", 59 | "react-scripts": "^3.4.1", 60 | "typescript": "^3.7.5" 61 | }, 62 | "files": [ 63 | "dist" 64 | ], 65 | "keywords": [ 66 | "auth", 67 | "react", 68 | "2FA", 69 | "apple", 70 | "input" 71 | ], 72 | "dependencies": {} 73 | } 74 | -------------------------------------------------------------------------------- /example/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState } from 'react'; 2 | 3 | import AuthCode, { AuthCodeRef, AuthCodeProps } from 'react-auth-code-input'; 4 | import './index.css'; 5 | 6 | const allowedCharactersMap = [ 7 | { id: 'alphanumeric', name: 'Letters & Numbers' }, 8 | { id: 'alpha', name: 'Only Letters' }, 9 | { id: 'numeric', name: 'Only Numbers' } 10 | ]; 11 | 12 | const App = () => { 13 | const AuthInputRef = useRef(null); 14 | const [result, setResult] = useState(''); 15 | const [isPassword, setIsPassword] = useState(false); 16 | const [disabled, setDisabled] = useState(false); 17 | const [allowedCharacters, setAllowedCharacters] = useState< 18 | AuthCodeProps['allowedCharacters'] 19 | >('alphanumeric'); 20 | const handleOnChange = (res: string) => { 21 | setResult(res); 22 | }; 23 | 24 | const handleAllowedCharactersChange = ( 25 | e: React.ChangeEvent 26 | ) => { 27 | const { id } = e.currentTarget; 28 | // @ts-ignore 29 | setAllowedCharacters(id); 30 | setResult(''); 31 | }; 32 | 33 | return ( 34 |
35 |

React Auth Code Input

36 |

One-time password (OTP) React component.

37 |
38 | 42 | code style standard 46 | license MIT 50 | 54 | 58 |
59 |

60 | 61 | View documentation on GitHub 62 | 63 |

64 |
65 |
66 |

67 | Two-Factor 68 |
Authentication 69 |

70 | 80 |

81 | A message with a verification code has been sent to
82 | your devices. Enter the code to continue. 83 |

84 |

Code: {result}

85 |
86 |
87 |
88 | setIsPassword(e.target.checked)} 93 | /> 94 | 95 |
96 |
97 | setDisabled(e.target.checked)} 102 | /> 103 | 104 |
105 | 108 | 111 |
112 |
113 | {allowedCharactersMap.map((aC) => ( 114 |
115 | 122 | 123 |
124 | ))} 125 |
126 |
127 |
128 |
129 |
130 | ); 131 | }; 132 | 133 | export default App; 134 | -------------------------------------------------------------------------------- /dist/index.modern.js: -------------------------------------------------------------------------------- 1 | import React, { forwardRef, useRef, useImperativeHandle, useEffect } from 'react'; 2 | 3 | var allowedCharactersValues = ['alpha', 'numeric', 'alphanumeric']; 4 | var propsMap = { 5 | alpha: { 6 | type: 'text', 7 | inputMode: 'text', 8 | pattern: '[a-zA-Z]{1}' 9 | }, 10 | alphanumeric: { 11 | type: 'text', 12 | inputMode: 'text', 13 | pattern: '[a-zA-Z0-9]{1}' 14 | }, 15 | numeric: { 16 | type: 'tel', 17 | inputMode: 'numeric', 18 | pattern: '[0-9]{1}', 19 | min: '0', 20 | max: '9' 21 | } 22 | }; 23 | var AuthCode = forwardRef(function (_ref, ref) { 24 | var _ref$allowedCharacter = _ref.allowedCharacters, 25 | allowedCharacters = _ref$allowedCharacter === void 0 ? 'alphanumeric' : _ref$allowedCharacter, 26 | ariaLabel = _ref.ariaLabel, 27 | _ref$autoFocus = _ref.autoFocus, 28 | autoFocus = _ref$autoFocus === void 0 ? true : _ref$autoFocus, 29 | containerClassName = _ref.containerClassName, 30 | disabled = _ref.disabled, 31 | inputClassName = _ref.inputClassName, 32 | _ref$isPassword = _ref.isPassword, 33 | isPassword = _ref$isPassword === void 0 ? false : _ref$isPassword, 34 | _ref$length = _ref.length, 35 | length = _ref$length === void 0 ? 6 : _ref$length, 36 | placeholder = _ref.placeholder, 37 | onChange = _ref.onChange; 38 | 39 | if (isNaN(length) || length < 1) { 40 | throw new Error('Length should be a number and greater than 0'); 41 | } 42 | 43 | if (!allowedCharactersValues.some(function (value) { 44 | return value === allowedCharacters; 45 | })) { 46 | throw new Error('Invalid value for allowedCharacters. Use alpha, numeric, or alphanumeric'); 47 | } 48 | 49 | var inputsRef = useRef([]); 50 | var inputProps = propsMap[allowedCharacters]; 51 | useImperativeHandle(ref, function () { 52 | return { 53 | focus: function focus() { 54 | if (inputsRef.current) { 55 | inputsRef.current[0].focus(); 56 | } 57 | }, 58 | clear: function clear() { 59 | if (inputsRef.current) { 60 | for (var i = 0; i < inputsRef.current.length; i++) { 61 | inputsRef.current[i].value = ''; 62 | } 63 | 64 | inputsRef.current[0].focus(); 65 | } 66 | 67 | sendResult(); 68 | } 69 | }; 70 | }); 71 | useEffect(function () { 72 | if (autoFocus) { 73 | inputsRef.current[0].focus(); 74 | } 75 | }, []); 76 | 77 | var sendResult = function sendResult() { 78 | var res = inputsRef.current.map(function (input) { 79 | return input.value; 80 | }).join(''); 81 | onChange && onChange(res); 82 | }; 83 | 84 | var handleOnChange = function handleOnChange(e) { 85 | var _e$target = e.target, 86 | value = _e$target.value, 87 | nextElementSibling = _e$target.nextElementSibling; 88 | 89 | if (value.length > 1) { 90 | e.target.value = value.charAt(0); 91 | 92 | if (nextElementSibling !== null) { 93 | nextElementSibling.focus(); 94 | } 95 | } else { 96 | if (value.match(inputProps.pattern)) { 97 | if (nextElementSibling !== null) { 98 | nextElementSibling.focus(); 99 | } 100 | } else { 101 | e.target.value = ''; 102 | } 103 | } 104 | 105 | sendResult(); 106 | }; 107 | 108 | var handleOnKeyDown = function handleOnKeyDown(e) { 109 | var key = e.key; 110 | var target = e.target; 111 | 112 | if (key === 'Backspace') { 113 | if (target.value === '') { 114 | if (target.previousElementSibling !== null) { 115 | var t = target.previousElementSibling; 116 | t.value = ''; 117 | t.focus(); 118 | e.preventDefault(); 119 | } 120 | } else { 121 | target.value = ''; 122 | } 123 | 124 | sendResult(); 125 | } 126 | }; 127 | 128 | var handleOnFocus = function handleOnFocus(e) { 129 | e.target.select(); 130 | }; 131 | 132 | var handleOnPaste = function handleOnPaste(e) { 133 | var pastedValue = e.clipboardData.getData('Text'); 134 | var currentInput = 0; 135 | 136 | for (var i = 0; i < pastedValue.length; i++) { 137 | var pastedCharacter = pastedValue.charAt(i); 138 | var currentValue = inputsRef.current[currentInput].value; 139 | 140 | if (pastedCharacter.match(inputProps.pattern)) { 141 | if (!currentValue) { 142 | inputsRef.current[currentInput].value = pastedCharacter; 143 | 144 | if (inputsRef.current[currentInput].nextElementSibling !== null) { 145 | inputsRef.current[currentInput].nextElementSibling.focus(); 146 | currentInput++; 147 | } 148 | } 149 | } 150 | } 151 | 152 | sendResult(); 153 | e.preventDefault(); 154 | }; 155 | 156 | var inputs = []; 157 | 158 | var _loop = function _loop(i) { 159 | inputs.push(React.createElement("input", Object.assign({ 160 | key: i, 161 | onChange: handleOnChange, 162 | onKeyDown: handleOnKeyDown, 163 | onFocus: handleOnFocus, 164 | onPaste: handleOnPaste 165 | }, inputProps, { 166 | type: isPassword ? 'password' : inputProps.type, 167 | ref: function ref(el) { 168 | inputsRef.current[i] = el; 169 | }, 170 | maxLength: 1, 171 | className: inputClassName, 172 | autoComplete: i === 0 ? 'one-time-code' : 'off', 173 | "aria-label": ariaLabel ? ariaLabel + ". Character " + (i + 1) + "." : "Character " + (i + 1) + ".", 174 | disabled: disabled, 175 | placeholder: placeholder 176 | }))); 177 | }; 178 | 179 | for (var i = 0; i < length; i++) { 180 | _loop(i); 181 | } 182 | 183 | return React.createElement("div", { 184 | className: containerClassName 185 | }, inputs); 186 | }); 187 | 188 | export default AuthCode; 189 | //# sourceMappingURL=index.modern.js.map 190 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } 2 | 3 | var React = require('react'); 4 | var React__default = _interopDefault(React); 5 | 6 | var allowedCharactersValues = ['alpha', 'numeric', 'alphanumeric']; 7 | var propsMap = { 8 | alpha: { 9 | type: 'text', 10 | inputMode: 'text', 11 | pattern: '[a-zA-Z]{1}' 12 | }, 13 | alphanumeric: { 14 | type: 'text', 15 | inputMode: 'text', 16 | pattern: '[a-zA-Z0-9]{1}' 17 | }, 18 | numeric: { 19 | type: 'tel', 20 | inputMode: 'numeric', 21 | pattern: '[0-9]{1}', 22 | min: '0', 23 | max: '9' 24 | } 25 | }; 26 | var AuthCode = React.forwardRef(function (_ref, ref) { 27 | var _ref$allowedCharacter = _ref.allowedCharacters, 28 | allowedCharacters = _ref$allowedCharacter === void 0 ? 'alphanumeric' : _ref$allowedCharacter, 29 | ariaLabel = _ref.ariaLabel, 30 | _ref$autoFocus = _ref.autoFocus, 31 | autoFocus = _ref$autoFocus === void 0 ? true : _ref$autoFocus, 32 | containerClassName = _ref.containerClassName, 33 | disabled = _ref.disabled, 34 | inputClassName = _ref.inputClassName, 35 | _ref$isPassword = _ref.isPassword, 36 | isPassword = _ref$isPassword === void 0 ? false : _ref$isPassword, 37 | _ref$length = _ref.length, 38 | length = _ref$length === void 0 ? 6 : _ref$length, 39 | placeholder = _ref.placeholder, 40 | onChange = _ref.onChange; 41 | 42 | if (isNaN(length) || length < 1) { 43 | throw new Error('Length should be a number and greater than 0'); 44 | } 45 | 46 | if (!allowedCharactersValues.some(function (value) { 47 | return value === allowedCharacters; 48 | })) { 49 | throw new Error('Invalid value for allowedCharacters. Use alpha, numeric, or alphanumeric'); 50 | } 51 | 52 | var inputsRef = React.useRef([]); 53 | var inputProps = propsMap[allowedCharacters]; 54 | React.useImperativeHandle(ref, function () { 55 | return { 56 | focus: function focus() { 57 | if (inputsRef.current) { 58 | inputsRef.current[0].focus(); 59 | } 60 | }, 61 | clear: function clear() { 62 | if (inputsRef.current) { 63 | for (var i = 0; i < inputsRef.current.length; i++) { 64 | inputsRef.current[i].value = ''; 65 | } 66 | 67 | inputsRef.current[0].focus(); 68 | } 69 | 70 | sendResult(); 71 | } 72 | }; 73 | }); 74 | React.useEffect(function () { 75 | if (autoFocus) { 76 | inputsRef.current[0].focus(); 77 | } 78 | }, []); 79 | 80 | var sendResult = function sendResult() { 81 | var res = inputsRef.current.map(function (input) { 82 | return input.value; 83 | }).join(''); 84 | onChange && onChange(res); 85 | }; 86 | 87 | var handleOnChange = function handleOnChange(e) { 88 | var _e$target = e.target, 89 | value = _e$target.value, 90 | nextElementSibling = _e$target.nextElementSibling; 91 | 92 | if (value.length > 1) { 93 | e.target.value = value.charAt(0); 94 | 95 | if (nextElementSibling !== null) { 96 | nextElementSibling.focus(); 97 | } 98 | } else { 99 | if (value.match(inputProps.pattern)) { 100 | if (nextElementSibling !== null) { 101 | nextElementSibling.focus(); 102 | } 103 | } else { 104 | e.target.value = ''; 105 | } 106 | } 107 | 108 | sendResult(); 109 | }; 110 | 111 | var handleOnKeyDown = function handleOnKeyDown(e) { 112 | var key = e.key; 113 | var target = e.target; 114 | 115 | if (key === 'Backspace') { 116 | if (target.value === '') { 117 | if (target.previousElementSibling !== null) { 118 | var t = target.previousElementSibling; 119 | t.value = ''; 120 | t.focus(); 121 | e.preventDefault(); 122 | } 123 | } else { 124 | target.value = ''; 125 | } 126 | 127 | sendResult(); 128 | } 129 | }; 130 | 131 | var handleOnFocus = function handleOnFocus(e) { 132 | e.target.select(); 133 | }; 134 | 135 | var handleOnPaste = function handleOnPaste(e) { 136 | var pastedValue = e.clipboardData.getData('Text'); 137 | var currentInput = 0; 138 | 139 | for (var i = 0; i < pastedValue.length; i++) { 140 | var pastedCharacter = pastedValue.charAt(i); 141 | var currentValue = inputsRef.current[currentInput].value; 142 | 143 | if (pastedCharacter.match(inputProps.pattern)) { 144 | if (!currentValue) { 145 | inputsRef.current[currentInput].value = pastedCharacter; 146 | 147 | if (inputsRef.current[currentInput].nextElementSibling !== null) { 148 | inputsRef.current[currentInput].nextElementSibling.focus(); 149 | currentInput++; 150 | } 151 | } 152 | } 153 | } 154 | 155 | sendResult(); 156 | e.preventDefault(); 157 | }; 158 | 159 | var inputs = []; 160 | 161 | var _loop = function _loop(i) { 162 | inputs.push(React__default.createElement("input", Object.assign({ 163 | key: i, 164 | onChange: handleOnChange, 165 | onKeyDown: handleOnKeyDown, 166 | onFocus: handleOnFocus, 167 | onPaste: handleOnPaste 168 | }, inputProps, { 169 | type: isPassword ? 'password' : inputProps.type, 170 | ref: function ref(el) { 171 | inputsRef.current[i] = el; 172 | }, 173 | maxLength: 1, 174 | className: inputClassName, 175 | autoComplete: i === 0 ? 'one-time-code' : 'off', 176 | "aria-label": ariaLabel ? ariaLabel + ". Character " + (i + 1) + "." : "Character " + (i + 1) + ".", 177 | disabled: disabled, 178 | placeholder: placeholder 179 | }))); 180 | }; 181 | 182 | for (var i = 0; i < length; i++) { 183 | _loop(i); 184 | } 185 | 186 | return React__default.createElement("div", { 187 | className: containerClassName 188 | }, inputs); 189 | }); 190 | 191 | module.exports = AuthCode; 192 | //# sourceMappingURL=index.js.map 193 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | useRef, 3 | useEffect, 4 | useImperativeHandle, 5 | forwardRef 6 | } from 'react'; 7 | 8 | const allowedCharactersValues = ['alpha', 'numeric', 'alphanumeric'] as const; 9 | 10 | export type AuthCodeProps = { 11 | allowedCharacters?: typeof allowedCharactersValues[number]; 12 | ariaLabel?: string; 13 | autoFocus?: boolean; 14 | containerClassName?: string; 15 | disabled?: boolean; 16 | inputClassName?: string; 17 | isPassword?: boolean; 18 | length?: number; 19 | placeholder?: string; 20 | onChange: (res: string) => void; 21 | }; 22 | 23 | type InputMode = 'text' | 'numeric'; 24 | 25 | type InputType = 'text' | 'tel' | 'password'; 26 | 27 | type InputProps = { 28 | type: InputType; 29 | inputMode: InputMode; 30 | pattern: string; 31 | min?: string; 32 | max?: string; 33 | }; 34 | 35 | export type AuthCodeRef = { 36 | focus: () => void; 37 | clear: () => void; 38 | }; 39 | 40 | const propsMap: { [key: string]: InputProps } = { 41 | alpha: { 42 | type: 'text', 43 | inputMode: 'text', 44 | pattern: '[a-zA-Z]{1}' 45 | }, 46 | 47 | alphanumeric: { 48 | type: 'text', 49 | inputMode: 'text', 50 | pattern: '[a-zA-Z0-9]{1}' 51 | }, 52 | 53 | numeric: { 54 | type: 'tel', 55 | inputMode: 'numeric', 56 | pattern: '[0-9]{1}', 57 | min: '0', 58 | max: '9' 59 | } 60 | }; 61 | 62 | const AuthCode = forwardRef( 63 | ( 64 | { 65 | allowedCharacters = 'alphanumeric', 66 | ariaLabel, 67 | autoFocus = true, 68 | containerClassName, 69 | disabled, 70 | inputClassName, 71 | isPassword = false, 72 | length = 6, 73 | placeholder, 74 | onChange 75 | }, 76 | ref 77 | ) => { 78 | if (isNaN(length) || length < 1) { 79 | throw new Error('Length should be a number and greater than 0'); 80 | } 81 | 82 | if (!allowedCharactersValues.some((value) => value === allowedCharacters)) { 83 | throw new Error( 84 | 'Invalid value for allowedCharacters. Use alpha, numeric, or alphanumeric' 85 | ); 86 | } 87 | 88 | const inputsRef = useRef>([]); 89 | const inputProps = propsMap[allowedCharacters]; 90 | 91 | useImperativeHandle(ref, () => ({ 92 | focus: () => { 93 | if (inputsRef.current) { 94 | inputsRef.current[0].focus(); 95 | } 96 | }, 97 | clear: () => { 98 | if (inputsRef.current) { 99 | for (let i = 0; i < inputsRef.current.length; i++) { 100 | inputsRef.current[i].value = ''; 101 | } 102 | inputsRef.current[0].focus(); 103 | } 104 | sendResult(); 105 | } 106 | })); 107 | 108 | useEffect(() => { 109 | if (autoFocus) { 110 | inputsRef.current[0].focus(); 111 | } 112 | }, []); 113 | 114 | const sendResult = () => { 115 | const res = inputsRef.current.map((input) => input.value).join(''); 116 | onChange && onChange(res); 117 | }; 118 | 119 | const handleOnChange = (e: React.ChangeEvent) => { 120 | const { 121 | target: { value, nextElementSibling } 122 | } = e; 123 | if (value.length > 1) { 124 | e.target.value = value.charAt(0); 125 | if (nextElementSibling !== null) { 126 | (nextElementSibling as HTMLInputElement).focus(); 127 | } 128 | } else { 129 | if (value.match(inputProps.pattern)) { 130 | if (nextElementSibling !== null) { 131 | (nextElementSibling as HTMLInputElement).focus(); 132 | } 133 | } else { 134 | e.target.value = ''; 135 | } 136 | } 137 | sendResult(); 138 | }; 139 | 140 | const handleOnKeyDown = (e: React.KeyboardEvent) => { 141 | const { key } = e; 142 | const target = e.target as HTMLInputElement; 143 | if (key === 'Backspace') { 144 | if (target.value === '') { 145 | if (target.previousElementSibling !== null) { 146 | const t = target.previousElementSibling as HTMLInputElement; 147 | t.value = ''; 148 | t.focus(); 149 | e.preventDefault(); 150 | } 151 | } else { 152 | target.value = ''; 153 | } 154 | sendResult(); 155 | } 156 | }; 157 | 158 | const handleOnFocus = (e: React.FocusEvent) => { 159 | e.target.select(); 160 | }; 161 | 162 | const handleOnPaste = (e: React.ClipboardEvent) => { 163 | const pastedValue = e.clipboardData.getData('Text'); 164 | 165 | let currentInput = 0; 166 | 167 | for (let i = 0; i < pastedValue.length; i++) { 168 | const pastedCharacter = pastedValue.charAt(i); 169 | const currentValue = inputsRef.current[currentInput].value; 170 | if (pastedCharacter.match(inputProps.pattern)) { 171 | if (!currentValue) { 172 | inputsRef.current[currentInput].value = pastedCharacter; 173 | if (inputsRef.current[currentInput].nextElementSibling !== null) { 174 | (inputsRef.current[currentInput] 175 | .nextElementSibling as HTMLInputElement).focus(); 176 | currentInput++; 177 | } 178 | } 179 | } 180 | } 181 | sendResult(); 182 | 183 | e.preventDefault(); 184 | }; 185 | 186 | const inputs = []; 187 | for (let i = 0; i < length; i++) { 188 | inputs.push( 189 | { 198 | inputsRef.current[i] = el; 199 | }} 200 | maxLength={1} 201 | className={inputClassName} 202 | autoComplete={i === 0 ? 'one-time-code' : 'off'} 203 | aria-label={ 204 | ariaLabel 205 | ? `${ariaLabel}. Character ${i + 1}.` 206 | : `Character ${i + 1}.` 207 | } 208 | disabled={disabled} 209 | placeholder={placeholder} 210 | /> 211 | ); 212 | } 213 | 214 | return
{inputs}
; 215 | } 216 | ); 217 | 218 | export default AuthCode; 219 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | > [!IMPORTANT] 3 | > After much consideration, I've come to the difficult decision that I will no longer be able to maintain this project due to my current time constraints. I sincerely apologize for any inconvenience this may cause and appreciate the understanding and support from the community. It's been a rewarding journey, and I'm grateful for the contributions and engagement of all involved. Thank you for your understanding. 4 | 5 | ![image](https://user-images.githubusercontent.com/1719915/159784302-4bb83708-e993-4800-9bcf-091ecb709ef7.png) 6 | 7 | # React Auth Code Input 8 | 9 | > One-time password (OTP) React component, zero dependencies, fully tested. 10 | 11 | [![NPM](https://img.shields.io/npm/v/react-auth-code-input.svg)](https://www.npmjs.com/package/react-auth-code-input) [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com) 12 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE.md) 13 | [![npm](https://img.shields.io/npm/dt/react-auth-code-input.svg)](https://www.npmjs.com/package/react-auth-code-input) 14 | [![npm](https://img.shields.io/npm/dw/react-auth-code-input.svg)](https://www.npmjs.com/package/react-auth-code-input) 15 | ![GitHub actions state](https://img.shields.io/github/workflow/status/drac94/react-auth-code-input/CI) 16 | 17 | ## Demo 18 | 19 | [Demo](https://www.luisguerrero.me/react-auth-code-input/) 20 | 21 | ## Install 22 | 23 | ```bash 24 | npm install --save react-auth-code-input 25 | ``` 26 | 27 | or 28 | 29 | ```bash 30 | yarn add react-auth-code-input 31 | ``` 32 | 33 | # Version 3+ 34 | 35 | ## Basic Usage 36 | 37 | ```tsx 38 | import React, { useState } from 'react'; 39 | import AuthCode from 'react-auth-code-input'; 40 | 41 | const App = () => { 42 | const [result, setResult] = useState(); 43 | const handleOnChange = (res: string) => { 44 | setResult(res); 45 | }; 46 | 47 | return ; 48 | }; 49 | ``` 50 | 51 | ## Mode 52 | 53 | By default you can type numbers and letters in the inputs as the `allowedCharacters` prop is defaulted to `alphanumeric` but you can also choose between allowing only letters or only numbers by setting the prop to `alpha` or `numeric` respectively. 54 | 55 | ```tsx 56 | import React, { useState } from 'react'; 57 | import AuthCode from 'react-auth-code-input'; 58 | 59 | const App = () => { 60 | const [result, setResult] = useState(); 61 | const handleOnChange = (res: string) => { 62 | setResult(res); 63 | }; 64 | 65 | return ; 66 | }; 67 | ``` 68 | 69 | ## Focus 70 | 71 | By default the first input is focused when the component is mounted, you can opt-out from this by setting the `autoFocus` prop to `false`, and then you can handle the focus manually by passing a reference. 72 | 73 | ```tsx 74 | import React, { useRef, useState } from 'react'; 75 | import AuthCode, { AuthCodeRef } from 'react-auth-code-input'; 76 | 77 | const App = () => { 78 | const AuthInputRef = useRef(null); 79 | const [result, setResult] = useState(); 80 | const handleOnChange = (res: string) => { 81 | setResult(res); 82 | }; 83 | 84 | return ( 85 | <> 86 | 91 | 92 | 93 | ); 94 | }; 95 | ``` 96 | 97 | ## Clear 98 | 99 | You can clear all the inputs by passing a reference and then calling the `clear` method. 100 | 101 | ```tsx 102 | import React, { useRef, useState } from 'react'; 103 | import AuthCode, { AuthCodeRef } from 'react-auth-code-input'; 104 | 105 | const App = () => { 106 | const AuthInputRef = useRef(null); 107 | const [result, setResult] = useState(); 108 | const handleOnChange = (res: string) => { 109 | setResult(res); 110 | }; 111 | 112 | return ( 113 | <> 114 | 115 | 116 | 117 | ); 118 | }; 119 | ``` 120 | 121 | ## SMS Autofill 122 | 123 | This component supports autofill from SMS's received, tested on Safari and Chrome in iOS. 124 | 125 | ## Props 126 | 127 | | Prop | Type | Description | Default Value | Observations | 128 | | :------------------- | :---------------------- | :---------------------------------------------------------- | :------------- | :----------------------------------------------- | 129 | | `allowedCharacters` | String | Type of allowed characters for your code. | `alphanumeric` | Valid values: `alpha`, `alphanumeric`, `numeric` | 130 | | `ariaLabel` | String | Accessibility. | | | 131 | | `autoFocus` | Boolean | Wether the first input is focused on mount or not.. | true | Since version 3.1.0 | 132 | | `length` | Number | The number of inputs to display. | 6 | | 133 | | `containerClassName` | String | The styles to be applied to the container. | | | 134 | | `inputClassName` | String | The styles to be applied to each input. | | | 135 | | `onChange` | Function(value: String) | Callback function called every time an input value changes. | | Required | 136 | | `isPassword` | Boolean | Whether to display the inputs as passwords or not. | false | | 137 | | `disabled` | Boolean | Makes all the inputs disabled if true. | false | Since version 3.1.1 | 138 | | `placeholder` | String | Displays a placeholder in all the inputs. | | Since version 3.2.0 | 139 | 140 | ## Changelog 141 | 142 | ### 3.2.1 143 | 144 | - Fix allowing non-numeric characters being introduced in numeric mode on Safari and Firefox. 145 | 146 | ### 3.2.0 147 | 148 | - Add `placeholder` prop. 149 | - Export component props. 150 | 151 | ### 3.1.0 152 | 153 | - Add `disabled` prop to disable all the inputs. 154 | - Make the backspace delete the previous character if the current is empty. Previously it just moved the focus making the user hit twice the backspace to delete the value. 155 | 156 | ### 3.1.0 157 | 158 | - Add `autoFocus` prop set to true by default to not break current usages. 159 | - Expose a `focus` method using references to handle the focus of the first input manually. 160 | - Expose a `clear` method using references to clear the input programmatically. 161 | - Add validations for when not using typescript. 162 | - Update React peerDependency to use any version 16+. 163 | 164 | ### 3.0.0 165 | 166 | - Change the way the allowed characters are handled by using 3 predefined modes: alpha, alphanumeric, and numeric, allowing to have more control when validating the values introduced in the inputs. 167 | - Improved logic. 168 | - Improved tests. 169 | - Improved types. 170 | 171 | ## License 172 | 173 | Licensed under the MIT License, Copyright © 2020-present Luis Guerrero [drac94](https://github.com/drac94). 174 | 175 | See [LICENSE](./LICENSE) for more information. 176 | -------------------------------------------------------------------------------- /dist/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.js","sources":["../src/index.tsx"],"sourcesContent":["import React, {\n useRef,\n useEffect,\n useImperativeHandle,\n forwardRef\n} from 'react';\n\nconst allowedCharactersValues = ['alpha', 'numeric', 'alphanumeric'] as const;\n\nexport type AuthCodeProps = {\n allowedCharacters?: typeof allowedCharactersValues[number];\n ariaLabel?: string;\n autoFocus?: boolean;\n containerClassName?: string;\n disabled?: boolean;\n inputClassName?: string;\n isPassword?: boolean;\n length?: number;\n placeholder?: string;\n onChange: (res: string) => void;\n};\n\ntype InputMode = 'text' | 'numeric';\n\ntype InputType = 'text' | 'tel' | 'password';\n\ntype InputProps = {\n type: InputType;\n inputMode: InputMode;\n pattern: string;\n min?: string;\n max?: string;\n};\n\nexport type AuthCodeRef = {\n focus: () => void;\n clear: () => void;\n};\n\nconst propsMap: { [key: string]: InputProps } = {\n alpha: {\n type: 'text',\n inputMode: 'text',\n pattern: '[a-zA-Z]{1}'\n },\n\n alphanumeric: {\n type: 'text',\n inputMode: 'text',\n pattern: '[a-zA-Z0-9]{1}'\n },\n\n numeric: {\n type: 'tel',\n inputMode: 'numeric',\n pattern: '[0-9]{1}',\n min: '0',\n max: '9'\n }\n};\n\nconst AuthCode = forwardRef(\n (\n {\n allowedCharacters = 'alphanumeric',\n ariaLabel,\n autoFocus = true,\n containerClassName,\n disabled,\n inputClassName,\n isPassword = false,\n length = 6,\n placeholder,\n onChange\n },\n ref\n ) => {\n if (isNaN(length) || length < 1) {\n throw new Error('Length should be a number and greater than 0');\n }\n\n if (!allowedCharactersValues.some((value) => value === allowedCharacters)) {\n throw new Error(\n 'Invalid value for allowedCharacters. Use alpha, numeric, or alphanumeric'\n );\n }\n\n const inputsRef = useRef>([]);\n const inputProps = propsMap[allowedCharacters];\n\n useImperativeHandle(ref, () => ({\n focus: () => {\n if (inputsRef.current) {\n inputsRef.current[0].focus();\n }\n },\n clear: () => {\n if (inputsRef.current) {\n for (let i = 0; i < inputsRef.current.length; i++) {\n inputsRef.current[i].value = '';\n }\n inputsRef.current[0].focus();\n }\n sendResult();\n }\n }));\n\n useEffect(() => {\n if (autoFocus) {\n inputsRef.current[0].focus();\n }\n }, []);\n\n const sendResult = () => {\n const res = inputsRef.current.map((input) => input.value).join('');\n onChange && onChange(res);\n };\n\n const handleOnChange = (e: React.ChangeEvent) => {\n const {\n target: { value, nextElementSibling }\n } = e;\n if (value.length > 1) {\n e.target.value = value.charAt(0);\n if (nextElementSibling !== null) {\n (nextElementSibling as HTMLInputElement).focus();\n }\n } else {\n if (value.match(inputProps.pattern)) {\n if (nextElementSibling !== null) {\n (nextElementSibling as HTMLInputElement).focus();\n }\n } else {\n e.target.value = '';\n }\n }\n sendResult();\n };\n\n const handleOnKeyDown = (e: React.KeyboardEvent) => {\n const { key } = e;\n const target = e.target as HTMLInputElement;\n if (key === 'Backspace') {\n if (target.value === '') {\n if (target.previousElementSibling !== null) {\n const t = target.previousElementSibling as HTMLInputElement;\n t.value = '';\n t.focus();\n e.preventDefault();\n }\n } else {\n target.value = '';\n }\n sendResult();\n }\n };\n\n const handleOnFocus = (e: React.FocusEvent) => {\n e.target.select();\n };\n\n const handleOnPaste = (e: React.ClipboardEvent) => {\n const pastedValue = e.clipboardData.getData('Text');\n\n let currentInput = 0;\n\n for (let i = 0; i < pastedValue.length; i++) {\n const pastedCharacter = pastedValue.charAt(i);\n const currentValue = inputsRef.current[currentInput].value;\n if (pastedCharacter.match(inputProps.pattern)) {\n if (!currentValue) {\n inputsRef.current[currentInput].value = pastedCharacter;\n if (inputsRef.current[currentInput].nextElementSibling !== null) {\n (inputsRef.current[currentInput]\n .nextElementSibling as HTMLInputElement).focus();\n currentInput++;\n }\n }\n }\n }\n sendResult();\n\n e.preventDefault();\n };\n\n const inputs = [];\n for (let i = 0; i < length; i++) {\n inputs.push(\n {\n inputsRef.current[i] = el;\n }}\n maxLength={1}\n className={inputClassName}\n autoComplete={i === 0 ? 'one-time-code' : 'off'}\n aria-label={\n ariaLabel\n ? `${ariaLabel}. Character ${i + 1}.`\n : `Character ${i + 1}.`\n }\n disabled={disabled}\n placeholder={placeholder}\n />\n );\n }\n\n return
{inputs}
;\n }\n);\n\nexport default AuthCode;\n"],"names":["allowedCharactersValues","propsMap","alpha","type","inputMode","pattern","alphanumeric","numeric","min","max","AuthCode","forwardRef","ref","allowedCharacters","ariaLabel","autoFocus","containerClassName","disabled","inputClassName","isPassword","length","placeholder","onChange","isNaN","Error","some","value","inputsRef","useRef","inputProps","useImperativeHandle","focus","current","clear","i","sendResult","useEffect","res","map","input","join","handleOnChange","e","target","nextElementSibling","charAt","match","handleOnKeyDown","key","previousElementSibling","t","preventDefault","handleOnFocus","select","handleOnPaste","pastedValue","clipboardData","getData","currentInput","pastedCharacter","currentValue","inputs","push","React","onKeyDown","onFocus","onPaste","el","maxLength","className","autoComplete"],"mappings":"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"} -------------------------------------------------------------------------------- /dist/index.modern.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.modern.js","sources":["../src/index.tsx"],"sourcesContent":["import React, {\n useRef,\n useEffect,\n useImperativeHandle,\n forwardRef\n} from 'react';\n\nconst allowedCharactersValues = ['alpha', 'numeric', 'alphanumeric'] as const;\n\nexport type AuthCodeProps = {\n allowedCharacters?: typeof allowedCharactersValues[number];\n ariaLabel?: string;\n autoFocus?: boolean;\n containerClassName?: string;\n disabled?: boolean;\n inputClassName?: string;\n isPassword?: boolean;\n length?: number;\n placeholder?: string;\n onChange: (res: string) => void;\n};\n\ntype InputMode = 'text' | 'numeric';\n\ntype InputType = 'text' | 'tel' | 'password';\n\ntype InputProps = {\n type: InputType;\n inputMode: InputMode;\n pattern: string;\n min?: string;\n max?: string;\n};\n\nexport type AuthCodeRef = {\n focus: () => void;\n clear: () => void;\n};\n\nconst propsMap: { [key: string]: InputProps } = {\n alpha: {\n type: 'text',\n inputMode: 'text',\n pattern: '[a-zA-Z]{1}'\n },\n\n alphanumeric: {\n type: 'text',\n inputMode: 'text',\n pattern: '[a-zA-Z0-9]{1}'\n },\n\n numeric: {\n type: 'tel',\n inputMode: 'numeric',\n pattern: '[0-9]{1}',\n min: '0',\n max: '9'\n }\n};\n\nconst AuthCode = forwardRef(\n (\n {\n allowedCharacters = 'alphanumeric',\n ariaLabel,\n autoFocus = true,\n containerClassName,\n disabled,\n inputClassName,\n isPassword = false,\n length = 6,\n placeholder,\n onChange\n },\n ref\n ) => {\n if (isNaN(length) || length < 1) {\n throw new Error('Length should be a number and greater than 0');\n }\n\n if (!allowedCharactersValues.some((value) => value === allowedCharacters)) {\n throw new Error(\n 'Invalid value for allowedCharacters. Use alpha, numeric, or alphanumeric'\n );\n }\n\n const inputsRef = useRef>([]);\n const inputProps = propsMap[allowedCharacters];\n\n useImperativeHandle(ref, () => ({\n focus: () => {\n if (inputsRef.current) {\n inputsRef.current[0].focus();\n }\n },\n clear: () => {\n if (inputsRef.current) {\n for (let i = 0; i < inputsRef.current.length; i++) {\n inputsRef.current[i].value = '';\n }\n inputsRef.current[0].focus();\n }\n sendResult();\n }\n }));\n\n useEffect(() => {\n if (autoFocus) {\n inputsRef.current[0].focus();\n }\n }, []);\n\n const sendResult = () => {\n const res = inputsRef.current.map((input) => input.value).join('');\n onChange && onChange(res);\n };\n\n const handleOnChange = (e: React.ChangeEvent) => {\n const {\n target: { value, nextElementSibling }\n } = e;\n if (value.length > 1) {\n e.target.value = value.charAt(0);\n if (nextElementSibling !== null) {\n (nextElementSibling as HTMLInputElement).focus();\n }\n } else {\n if (value.match(inputProps.pattern)) {\n if (nextElementSibling !== null) {\n (nextElementSibling as HTMLInputElement).focus();\n }\n } else {\n e.target.value = '';\n }\n }\n sendResult();\n };\n\n const handleOnKeyDown = (e: React.KeyboardEvent) => {\n const { key } = e;\n const target = e.target as HTMLInputElement;\n if (key === 'Backspace') {\n if (target.value === '') {\n if (target.previousElementSibling !== null) {\n const t = target.previousElementSibling as HTMLInputElement;\n t.value = '';\n t.focus();\n e.preventDefault();\n }\n } else {\n target.value = '';\n }\n sendResult();\n }\n };\n\n const handleOnFocus = (e: React.FocusEvent) => {\n e.target.select();\n };\n\n const handleOnPaste = (e: React.ClipboardEvent) => {\n const pastedValue = e.clipboardData.getData('Text');\n\n let currentInput = 0;\n\n for (let i = 0; i < pastedValue.length; i++) {\n const pastedCharacter = pastedValue.charAt(i);\n const currentValue = inputsRef.current[currentInput].value;\n if (pastedCharacter.match(inputProps.pattern)) {\n if (!currentValue) {\n inputsRef.current[currentInput].value = pastedCharacter;\n if (inputsRef.current[currentInput].nextElementSibling !== null) {\n (inputsRef.current[currentInput]\n .nextElementSibling as HTMLInputElement).focus();\n currentInput++;\n }\n }\n }\n }\n sendResult();\n\n e.preventDefault();\n };\n\n const inputs = [];\n for (let i = 0; i < length; i++) {\n inputs.push(\n {\n inputsRef.current[i] = el;\n }}\n maxLength={1}\n className={inputClassName}\n autoComplete={i === 0 ? 'one-time-code' : 'off'}\n aria-label={\n ariaLabel\n ? `${ariaLabel}. Character ${i + 1}.`\n : `Character ${i + 1}.`\n }\n disabled={disabled}\n placeholder={placeholder}\n />\n );\n }\n\n return
{inputs}
;\n }\n);\n\nexport default AuthCode;\n"],"names":["allowedCharactersValues","propsMap","alpha","type","inputMode","pattern","alphanumeric","numeric","min","max","AuthCode","forwardRef","ref","allowedCharacters","ariaLabel","autoFocus","containerClassName","disabled","inputClassName","isPassword","length","placeholder","onChange","isNaN","Error","some","value","inputsRef","useRef","inputProps","useImperativeHandle","focus","current","clear","i","sendResult","useEffect","res","map","input","join","handleOnChange","e","target","nextElementSibling","charAt","match","handleOnKeyDown","key","previousElementSibling","t","preventDefault","handleOnFocus","select","handleOnPaste","pastedValue","clipboardData","getData","currentInput","pastedCharacter","currentValue","inputs","push","React","onKeyDown","onFocus","onPaste","el","maxLength","className","autoComplete"],"mappings":"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"} -------------------------------------------------------------------------------- /src/index.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import '@testing-library/jest-dom'; 4 | 5 | import { createEvent, fireEvent, render, screen } from '@testing-library/react'; 6 | import userEvent from '@testing-library/user-event'; 7 | 8 | import AuthCode from '.'; 9 | 10 | describe('AuthCode', () => { 11 | it('should render the default component', () => { 12 | render( null} />); 13 | const inputs = screen.getAllByRole('textbox'); 14 | expect(inputs).toHaveLength(6); 15 | expect(inputs[0]).toHaveFocus(); 16 | }); 17 | 18 | it('should display a placeholder if passed as prop', () => { 19 | render( null} placeholder='*' />); 20 | const inputs = screen.getAllByPlaceholderText('*'); 21 | expect(inputs).toHaveLength(6); 22 | }); 23 | 24 | it('should render the component but not focus the first input', () => { 25 | render( null} />); 26 | const inputs = screen.getAllByRole('textbox'); 27 | expect(inputs).toHaveLength(6); 28 | expect(inputs[0]).not.toHaveFocus(); 29 | }); 30 | 31 | it('should render n inputs', () => { 32 | render( null} length={4} />); 33 | expect(screen.getAllByRole('textbox')).toHaveLength(4); 34 | }); 35 | 36 | it('should call onChange function when typing any character', async () => { 37 | const onChangeFn = jest.fn(); 38 | render(); 39 | 40 | const input = screen.getAllByRole('textbox')[0] as HTMLInputElement; 41 | 42 | userEvent.type(input, 'A'); 43 | expect(input).toHaveValue('A'); 44 | expect(onChangeFn).toHaveBeenCalledTimes(1); 45 | }); 46 | 47 | it('should delete the previous value if the current input is empty', async () => { 48 | const onChangeFn = jest.fn(); 49 | render(); 50 | 51 | const input4 = screen.getAllByRole('textbox')[4] as HTMLInputElement; 52 | const input5 = screen.getAllByRole('textbox')[5] as HTMLInputElement; 53 | 54 | userEvent.type(input4, 'A'); 55 | expect(input4).toHaveValue('A'); 56 | 57 | userEvent.type(input5, 'B'); 58 | expect(input5).toHaveValue('B'); 59 | 60 | userEvent.keyboard('[Backspace]'); 61 | expect(input5).toHaveValue(''); 62 | expect(input5).toHaveFocus(); 63 | 64 | userEvent.keyboard('[Backspace]'); 65 | expect(input4).toHaveValue(''); 66 | expect(input4).toHaveFocus(); 67 | }); 68 | 69 | describe('Alphanumeric', () => { 70 | it('should not change the input value when typing a not allowed character', async () => { 71 | const onChangeFn = jest.fn(); 72 | render(); 73 | 74 | const input = screen.getAllByRole('textbox')[0] as HTMLInputElement; 75 | 76 | userEvent.type(input, ','); 77 | expect(input).toHaveValue(''); 78 | }); 79 | 80 | it('should allow only one character for input', async () => { 81 | const onChangeFn = jest.fn(); 82 | render(); 83 | 84 | const input = screen.getAllByRole('textbox')[0] as HTMLInputElement; 85 | 86 | userEvent.type(input, 'A'); 87 | userEvent.type(input, 'B'); 88 | expect(input).toHaveValue('A'); 89 | 90 | userEvent.type(input, '1'); 91 | expect(input).toHaveValue('A'); 92 | expect(onChangeFn).toHaveBeenCalledTimes(1); 93 | }); 94 | 95 | it('should paste all the characters', async () => { 96 | const onChangeFn = jest.fn(); 97 | render(); 98 | 99 | const firstInput = screen.getAllByRole('textbox')[0] as HTMLInputElement; 100 | const lastInput = screen.getAllByRole('textbox')[5] as HTMLInputElement; 101 | 102 | const paste = createEvent.paste(firstInput, { 103 | clipboardData: { 104 | getData: () => 'a12def' 105 | } 106 | }); 107 | 108 | fireEvent(firstInput, paste); 109 | 110 | expect(firstInput).toHaveValue('a'); 111 | expect(lastInput).toHaveValue('f'); 112 | expect(onChangeFn).toHaveBeenCalledTimes(1); 113 | }); 114 | 115 | it('should paste only the allowed the characters', async () => { 116 | const onChangeFn = jest.fn(); 117 | render(); 118 | 119 | const firstInput = screen.getAllByRole('textbox')[0] as HTMLInputElement; 120 | const lastInput = screen.getAllByRole('textbox')[4] as HTMLInputElement; 121 | 122 | const paste = createEvent.paste(firstInput, { 123 | clipboardData: { 124 | getData: () => '1,b456' 125 | } 126 | }); 127 | 128 | fireEvent(firstInput, paste); 129 | 130 | expect(firstInput).toHaveValue('1'); 131 | expect(lastInput).toHaveValue('6'); 132 | expect(onChangeFn).toHaveBeenCalledTimes(1); 133 | }); 134 | 135 | it('should take in account the number of characters when pasting', async () => { 136 | const onChangeFn = jest.fn(); 137 | render(); 138 | 139 | const firstInput = screen.getAllByRole('textbox')[0] as HTMLInputElement; 140 | const lastInput = screen.getAllByRole('textbox')[4] as HTMLInputElement; 141 | 142 | const paste = createEvent.paste(firstInput, { 143 | clipboardData: { 144 | getData: () => 'abcdef' 145 | } 146 | }); 147 | 148 | fireEvent(firstInput, paste); 149 | 150 | expect(firstInput).toHaveValue('a'); 151 | expect(lastInput).toHaveValue('e'); 152 | expect(onChangeFn).toHaveBeenCalledTimes(1); 153 | }); 154 | }); 155 | 156 | describe('Alpha', () => { 157 | it('should not change the input value when typing a not allowed character', async () => { 158 | const onChangeFn = jest.fn(); 159 | render(); 160 | 161 | const input = screen.getAllByRole('textbox')[0] as HTMLInputElement; 162 | 163 | userEvent.type(input, ','); 164 | expect(input).toHaveValue(''); 165 | }); 166 | 167 | it('should allow only one character for input', async () => { 168 | const onChangeFn = jest.fn(); 169 | render(); 170 | 171 | const input = screen.getAllByRole('textbox')[0] as HTMLInputElement; 172 | 173 | userEvent.type(input, 'A'); 174 | userEvent.type(input, 'B'); 175 | expect(input).toHaveValue('A'); 176 | 177 | userEvent.type(input, '1'); 178 | expect(input).toHaveValue('A'); 179 | expect(onChangeFn).toHaveBeenCalledTimes(1); 180 | }); 181 | 182 | it('should paste all the characters', async () => { 183 | const onChangeFn = jest.fn(); 184 | render(); 185 | 186 | const firstInput = screen.getAllByRole('textbox')[0] as HTMLInputElement; 187 | const lastInput = screen.getAllByRole('textbox')[5] as HTMLInputElement; 188 | 189 | const paste = createEvent.paste(firstInput, { 190 | clipboardData: { 191 | getData: () => 'abcdef' 192 | } 193 | }); 194 | 195 | fireEvent(firstInput, paste); 196 | 197 | expect(firstInput).toHaveValue('a'); 198 | expect(lastInput).toHaveValue('f'); 199 | expect(onChangeFn).toHaveBeenCalledTimes(1); 200 | }); 201 | 202 | it('should paste only the allowed the characters', async () => { 203 | const onChangeFn = jest.fn(); 204 | render(); 205 | 206 | const firstInput = screen.getAllByRole('textbox')[0] as HTMLInputElement; 207 | const lastInput = screen.getAllByRole('textbox')[4] as HTMLInputElement; 208 | 209 | const paste = createEvent.paste(firstInput, { 210 | clipboardData: { 211 | getData: () => 'a,bcde' 212 | } 213 | }); 214 | 215 | fireEvent(firstInput, paste); 216 | 217 | expect(firstInput).toHaveValue('a'); 218 | expect(lastInput).toHaveValue('e'); 219 | expect(onChangeFn).toHaveBeenCalledTimes(1); 220 | }); 221 | 222 | it('should take in account the number of characters when pasting', async () => { 223 | const onChangeFn = jest.fn(); 224 | render( 225 | 226 | ); 227 | 228 | const firstInput = screen.getAllByRole('textbox')[0] as HTMLInputElement; 229 | const lastInput = screen.getAllByRole('textbox')[4] as HTMLInputElement; 230 | 231 | const paste = createEvent.paste(firstInput, { 232 | clipboardData: { 233 | getData: () => 'abcdef' 234 | } 235 | }); 236 | 237 | fireEvent(firstInput, paste); 238 | 239 | expect(firstInput).toHaveValue('a'); 240 | expect(lastInput).toHaveValue('e'); 241 | expect(onChangeFn).toHaveBeenCalledTimes(1); 242 | }); 243 | }); 244 | 245 | describe('Numeric', () => { 246 | it('should not change the input value when typing a not allowed character', async () => { 247 | const onChangeFn = jest.fn(); 248 | render(); 249 | 250 | const input = screen.getAllByRole('textbox')[0] as HTMLInputElement; 251 | 252 | userEvent.type(input, 'a'); 253 | expect(input).toHaveValue(''); 254 | }); 255 | 256 | it('should allow only one character for input', async () => { 257 | const onChangeFn = jest.fn(); 258 | render(); 259 | 260 | const input = screen.getAllByRole('textbox')[0] as HTMLInputElement; 261 | 262 | userEvent.type(input, '1'); 263 | userEvent.type(input, '{arrowright}'); 264 | userEvent.type(input, '2'); 265 | expect(input).toHaveValue('1'); 266 | 267 | userEvent.type(input, '{arrowright}'); 268 | userEvent.type(input, 'B'); 269 | expect(input).toHaveValue('1'); 270 | expect(onChangeFn).toHaveBeenCalledTimes(3); 271 | }); 272 | 273 | it('should paste all the characters', async () => { 274 | const onChangeFn = jest.fn(); 275 | render(); 276 | 277 | const firstInput = screen.getAllByRole('textbox')[0] as HTMLInputElement; 278 | const lastInput = screen.getAllByRole('textbox')[5] as HTMLInputElement; 279 | 280 | const paste = createEvent.paste(firstInput, { 281 | clipboardData: { 282 | getData: () => '123456' 283 | } 284 | }); 285 | 286 | fireEvent(firstInput, paste); 287 | 288 | expect(firstInput).toHaveValue('1'); 289 | expect(lastInput).toHaveValue('6'); 290 | expect(onChangeFn).toHaveBeenCalledTimes(1); 291 | }); 292 | 293 | it('should paste only the allowed the characters', async () => { 294 | const onChangeFn = jest.fn(); 295 | render(); 296 | 297 | const firstInput = screen.getAllByRole('textbox')[0] as HTMLInputElement; 298 | const lastInput = screen.getAllByRole('textbox')[5] as HTMLInputElement; 299 | 300 | const paste = createEvent.paste(firstInput, { 301 | clipboardData: { 302 | getData: () => '1ab45678' 303 | } 304 | }); 305 | 306 | fireEvent(firstInput, paste); 307 | 308 | expect(firstInput).toHaveValue('1'); 309 | expect(lastInput).toHaveValue('8'); 310 | expect(onChangeFn).toHaveBeenCalledTimes(1); 311 | }); 312 | 313 | it('should take in account the number of characters when pasting', async () => { 314 | const onChangeFn = jest.fn(); 315 | render( 316 | 321 | ); 322 | 323 | const firstInput = screen.getAllByRole('textbox')[0] as HTMLInputElement; 324 | const lastInput = screen.getAllByRole('textbox')[4] as HTMLInputElement; 325 | 326 | const paste = createEvent.paste(firstInput, { 327 | clipboardData: { 328 | getData: () => '123456' 329 | } 330 | }); 331 | 332 | fireEvent(firstInput, paste); 333 | 334 | expect(firstInput).toHaveValue('1'); 335 | expect(lastInput).toHaveValue('5'); 336 | expect(onChangeFn).toHaveBeenCalledTimes(1); 337 | }); 338 | }); 339 | 340 | describe('Bad properties', () => { 341 | it('should throw an exception when the length prop is less than 1', () => { 342 | const err = console.error; 343 | console.error = jest.fn(); 344 | const onChangeFn = jest.fn(); 345 | const badRender = () => { 346 | render(); 347 | }; 348 | 349 | expect(badRender).toThrowError( 350 | 'Length should be a number and greater than 0' 351 | ); 352 | console.error = err; 353 | }); 354 | 355 | it('should throw an exception when the length prop is not a number', () => { 356 | const err = console.error; 357 | console.error = jest.fn(); 358 | const onChangeFn = jest.fn(); 359 | const badRender = () => { 360 | // @ts-ignore 361 | render(); 362 | }; 363 | 364 | expect(badRender).toThrowError( 365 | 'Length should be a number and greater than 0' 366 | ); 367 | console.error = err; 368 | }); 369 | 370 | it('should throw an exception when the allowedCharacters prop s not valid', () => { 371 | const err = console.error; 372 | console.error = jest.fn(); 373 | const onChangeFn = jest.fn(); 374 | const badRender = () => { 375 | // @ts-ignore 376 | render(); 377 | }; 378 | 379 | expect(badRender).toThrowError('Invalid value for allowedCharacters'); 380 | console.error = err; 381 | }); 382 | }); 383 | }); 384 | --------------------------------------------------------------------------------