├── .circleci └── config.yml ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── LICENSE ├── README.md ├── jest.config.js ├── package-lock.json ├── package.json ├── src └── index.tsx ├── test ├── __snapshots__ │ └── index.test.tsx.snap └── index.test.tsx ├── tsconfig.json ├── tsconfig.test.json └── tslint.json /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | defaults: &defaults 4 | working_directory: ~/repo 5 | docker: 6 | - image: circleci/node:10.13.0 7 | 8 | jobs: 9 | install: 10 | <<: *defaults 11 | steps: 12 | - checkout 13 | 14 | - restore_cache: 15 | keys: 16 | - v1-dependencies-{{ checksum "package.json" }} 17 | # fallback to using the latest cache if no exact match is found 18 | - v1-dependencies- 19 | 20 | - run: npm install 21 | - save_cache: 22 | paths: 23 | - node_modules 24 | key: v1-dependencies-{{ checksum "package.json" }} 25 | 26 | - persist_to_workspace: 27 | root: ~/repo 28 | paths: . 29 | lint: 30 | <<: *defaults 31 | steps: 32 | - attach_workspace: 33 | at: ~/repo 34 | 35 | - run: 36 | name: Run lint 37 | command: npm run lint 38 | test: 39 | <<: *defaults 40 | steps: 41 | - attach_workspace: 42 | at: ~/repo 43 | 44 | - run: 45 | name: Run tests 46 | command: npm test 47 | build: 48 | <<: *defaults 49 | steps: 50 | - attach_workspace: 51 | at: ~/repo 52 | 53 | - run: 54 | name: Build project 55 | command: npm run build 56 | publish: 57 | # Only runs when the branch is tagged (using git tag v...) 58 | <<: *defaults 59 | steps: 60 | - attach_workspace: 61 | at: ~/repo 62 | - run: 63 | name: Build project 64 | command: npm run build 65 | - run: 66 | name: Authenticate with registry 67 | command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc 68 | - run: 69 | name: Publish package 70 | command: npm publish 71 | 72 | workflows: 73 | version: 2 74 | lint_test_publish: 75 | jobs: 76 | - install 77 | - lint: 78 | requires: 79 | - install 80 | - test: 81 | requires: 82 | - install 83 | - build: 84 | requires: 85 | - install 86 | - publish: 87 | requires: 88 | - test 89 | filters: 90 | tags: 91 | only: /^v.*/ 92 | branches: 93 | ignore: /.*/ -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | lib/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Fabian Schliski 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![npm](https://img.shields.io/npm/v/antd-password-input-strength.svg)](https://npmjs.org/package/antd-password-input-strength) 2 | [![GitHub issues](https://img.shields.io/github/issues/Kombustor/antd-password-input-strength.svg)](https://github.com/Kombustor/antd-password-input-strength/issues) 3 | [![GitHub license](https://img.shields.io/github/license/Kombustor/antd-password-input-strength.svg)](https://github.com/Kombustor/antd-password-input-strength/blob/master/LICENSE) 4 | [![Twitter](https://img.shields.io/twitter/url/https/github.com/Kombustor/antd-password-input-strength.svg?style=social)](https://twitter.com/intent/tweet?text=Wow:&url=https%3A%2F%2Fgithub.com%2FKombustor%2Fantd-password-input-strength) [![Greenkeeper badge](https://badges.greenkeeper.io/Kombustor/antd-password-input-strength.svg)](https://greenkeeper.io/) 5 | 6 | # antd-password-input-strength 7 | 8 | > Antd Input Component with password-strength indicator. 9 | 10 | ![Preview GIF](https://i.imgur.com/V7Z1Yyr.gif) 11 | 12 | ## Features 13 | 14 | - Drop-in replacement for antd's Input component 15 | - Customizable 16 | - Uses [tai-password-strength](https://www.npmjs.com/package/tai-password-strength) for password strength estimation 17 | 18 | _Note: tai-password-strength is a rather large library. Use code splitting to only load the library when necessary._ 19 | 20 | ## Install 21 | 22 | ```bash 23 | npm install --save antd-password-input-strength 24 | ``` 25 | 26 | or 27 | 28 | ```bash 29 | yarn add --save antd-password-input-strength 30 | ``` 31 | 32 | _Note: antd and react/react-dom are peer dependencies. You should only use this library in a React/AntD project._ 33 | 34 | ## Usage 35 | 36 | Use as a drop-in replacement for antd's [Input](https://ant.design/components/input/): 37 | 38 | ```tsx 39 |
40 | 41 | 42 | 43 |
44 | ``` 45 | 46 | With ```Form.create()```: 47 | 48 | ```tsx 49 |
50 | 51 | {this.props.form.getFieldDecorator("password", { 52 | rules: [{ 53 | required: true, 54 | message: "Please enter your password" 55 | }] 56 | })()} 57 | 58 |
59 | ``` 60 | 61 | With custom settings: 62 | 63 | ```tsx 64 |
65 | 66 | console.log("Changed")} 73 | size="large" 74 | /> 75 | 76 |
77 | ``` 78 | 79 | With validation: 80 | 81 | ```tsx 82 | function ValidationExample() { 83 | const [level, setLevel] = useState(0) 84 | 85 | const minLevel = 1; 86 | const errorMessage = 'Password is too weak'; 87 | 88 | return ( 89 |
90 | { 94 | return level >= minLevel ? Promise.resolve() : Promise.reject(errorMessage); 95 | }, 96 | message: errorMessage 97 | }]} 98 | > 99 | 100 | 101 |
102 | ); 103 | } 104 | ``` 105 | 106 | ## API 107 | 108 | ### PasswordInput 109 | 110 | | props | type | description | 111 | | -- | -- | -- | 112 | | settings | PasswordInputSettings | Strength indicator display settings | 113 | | onLevelChange | (newLevel: 0 | 1 | 2 | 3 | 4) => void | Called when the input level changes | 114 | | ...props | [InputProps](https://ant.design/components/input/#Input) | Pass additional properties to the underlying [Input](https://ant.design/components/input/) component 115 | 116 | ### PasswordInputSettings 117 | 118 | | props | type | description | 119 | | -- | -- | -- | 120 | | colorScheme | ColorScheme | Modify the indicator's color scheme | 121 | | height | number | Change indicator bar height (in px) | 122 | | alwaysVisible | boolean | If false, the bar only appears if the input field isn't empty | 123 | 124 | > Default: 125 | 126 | ```jsx 127 | { 128 | colorScheme: [...], 129 | height: 3, 130 | alwaysVisible: false 131 | } 132 | ``` 133 | 134 | ### ColorScheme 135 | 136 | | props | type | description | 137 | | -- | -- | -- | 138 | | levels | string[] | Array of CSS color codes for the different strength levels:
`levels[0] = weakest`, `levels[4] = strongest` | 139 | | noLevel| string | CSS color code for non-colored strength indicator bars. | 140 | 141 | > Default: 142 | 143 | ```jsx 144 | { 145 | levels: ["#ff4033", "#fe940d", "#ffd908", "#cbe11d", "#6ecc3a"], 146 | noLevel: "lightgrey" 147 | } 148 | ``` 149 | 150 | ## License 151 | 152 | MIT -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'jsdom', 4 | "globals": { 5 | "ts-jest": { 6 | "tsconfig": "./tsconfig.test.json" 7 | } 8 | } 9 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "antd-password-input-strength", 3 | "author": "Fabian Schliski", 4 | "license": "MIT", 5 | "version": "2.0.1", 6 | "description": "antd Input component with password-strength indicator.", 7 | "main": "lib/index.js", 8 | "scripts": { 9 | "test": "jest", 10 | "coverage": "npm test -- --coverage", 11 | "lint": "tslint \"src/**/*.(ts|tsx)\" --project tsconfig.json --project tsconfig.json", 12 | "build": "npm run clean && tsc", 13 | "clean": "rimraf lib" 14 | }, 15 | "peerDependencies": { 16 | "antd": ">=4", 17 | "react": ">=16" 18 | }, 19 | "files": [ 20 | "lib", 21 | "LICENSE" 22 | ], 23 | "types": "lib/index.d.ts", 24 | "keywords": [ 25 | "antd", 26 | "ant-design", 27 | "css", 28 | "password", 29 | "strength", 30 | "input" 31 | ], 32 | "bugs": { 33 | "url": "https://github.com/Kombustor/antd-password-input-strength/issues" 34 | }, 35 | "homepage": "https://github.com/Kombustor/antd-password-input-strength#readme", 36 | "repository": { 37 | "type": "git", 38 | "url": "git+https://github.com/Kombustor/antd-password-input-strength.git" 39 | }, 40 | "dependencies": { 41 | "tai-password-strength": "^1.1.3" 42 | }, 43 | "devDependencies": { 44 | "@types/jest": "^24.0.15", 45 | "@types/node": "^12.6.6", 46 | "@types/react": "^16.8.23", 47 | "@types/react-test-renderer": "^16.8.2", 48 | "@types/zxcvbn": "^4.4.0", 49 | "antd": "4.19.5", 50 | "jest": "^27.5.1", 51 | "react": "^16.8.6", 52 | "react-dom": "^16.8.6", 53 | "react-test-renderer": "^16.8.6", 54 | "rimraf": "^2.6.3", 55 | "ts-jest": "^27.1.4", 56 | "tslint": "^5.18.0", 57 | "typescript": "^4.6.3" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import { Input, InputProps, InputRef } from "antd"; 2 | import React, { useEffect, useState } from "react"; 3 | import { PasswordStrength } from 'tai-password-strength'; 4 | 5 | const PASSWORD_STRENGTH = new PasswordStrength(); 6 | const PASSWORD_STRENGTH_CODE = ['VERY_WEAK', 'WEAK', 'REASONABLE', 'STRONG', 'VERY_STRONG']; 7 | export const defaultSettings = { 8 | colorScheme: { 9 | levels: ["#ff4033", "#fe940d", "#ffd908", "#cbe11d", "#6ecc3a"], 10 | noLevel: "lightgrey" 11 | }, 12 | height: 3, 13 | alwaysVisible: false 14 | } 15 | 16 | interface LevelChangeProps { 17 | onLevelChange?: (newLevel: number) => void; 18 | } 19 | 20 | export interface PasswordInputProps extends LevelChangeProps { 21 | settings?: PasswordInputSettings; 22 | } 23 | 24 | export const PasswordInput = React.forwardRef(({ 25 | settings = defaultSettings, 26 | onLevelChange, 27 | ...props 28 | }: PasswordInputProps & Partial, ref: React.Ref) => { 29 | const [input, setInput] = useState('') 30 | 31 | return ( 32 | <> 33 | { setInput(e.target.value); props?.onChange?.(e) }} 38 | /> 39 | 44 | 45 | ); 46 | }) 47 | 48 | interface PasswordStrengthIndicatorProps extends LevelChangeProps { 49 | input: string; 50 | settings: PasswordInputSettings; 51 | } 52 | 53 | export const PasswordStrengthIndicator = ({ input, settings, onLevelChange }: PasswordStrengthIndicatorProps) => { 54 | // Calculate level 55 | const level = React.useMemo(() => { 56 | return input.length == 0 ? -1 : PASSWORD_STRENGTH_CODE.indexOf(PASSWORD_STRENGTH.check(input).strengthCode); 57 | }, [input]) 58 | 59 | // Calculate indicators 60 | const indicators: React.ReactElement[] = React.useMemo(() => { 61 | const ind = []; 62 | for (let i = 0; i < 5; i++) { 63 | const color = 64 | i <= level 65 | ? settings.colorScheme.levels[level] 66 | : settings.colorScheme.noLevel; 67 | ind.push(
); 68 | } 69 | 70 | return ind; 71 | }, [level, settings]) 72 | 73 | useEffect(() => { 74 | onLevelChange?.(level); 75 | }, [level]) 76 | 77 | if (!settings.alwaysVisible && level < 0) { 78 | return null; 79 | } 80 | 81 | return
{indicators}
; 82 | }; 83 | 84 | function getWrapperStyle(height: number) { 85 | return { 86 | lineHeight: height + "px" 87 | }; 88 | } 89 | 90 | function getIndicatorStyle(color: string, height: number) { 91 | return { 92 | display: "inline-block", 93 | width: "20%", 94 | backgroundColor: color, 95 | height: height + "px", 96 | borderRadius: "2px" 97 | }; 98 | } 99 | 100 | export interface PasswordInputSettings { 101 | colorScheme: ColorScheme; 102 | height: number; 103 | alwaysVisible: boolean; 104 | } 105 | 106 | export interface ColorScheme { 107 | levels: string[], 108 | noLevel: string; 109 | } 110 | -------------------------------------------------------------------------------- /test/__snapshots__/index.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`PasswordInput should render correctly 1`] = ` 4 | 8 | 20 | 23 | 32 | 48 | 49 | 50 | 51 | `; 52 | -------------------------------------------------------------------------------- /test/index.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as renderer from 'react-test-renderer' 3 | import { PasswordInput, PasswordInputSettings } from '../src' 4 | 5 | describe('PasswordInput', () => { 6 | 7 | /** Snapshot testing */ 8 | it('should render correctly', () => { 9 | const settings: PasswordInputSettings = { 10 | colorScheme: { 11 | levels: ["#ff4033", "#fe940d", "#ffd908", "#cbe11d", "#6ecc3a"], 12 | noLevel: "lightgrey" 13 | }, 14 | height: 3, 15 | alwaysVisible: false 16 | }; 17 | const component = renderer.create( 18 | 21 | ); 22 | let tree = component.toJSON(); 23 | 24 | expect(tree).toMatchSnapshot() 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "jsx": "react", 6 | "moduleResolution": "node", 7 | "esModuleInterop": true, 8 | "declaration": true, 9 | "outDir": "lib", 10 | "lib": ["es6", "es7", "dom"], 11 | "baseUrl": "src", 12 | "skipLibCheck": true 13 | }, 14 | "include": [ 15 | "src" 16 | ], 17 | "exclude": ["lib","node_modules"] 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "esModuleInterop": true 5 | } 6 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ "tslint-react" ] 3 | } 4 | --------------------------------------------------------------------------------