├── .babelrc ├── .eslintrc.js ├── .gitignore ├── .size-snapshot.json ├── .travis.yml ├── LICENSE.md ├── README.md ├── dev ├── index.html └── index.js ├── index.js ├── karma.conf.js ├── package-lock.json ├── package.json ├── rollup.config.js ├── rollup.proto-to-assign.plugin.js ├── src ├── children-wrapper.js ├── constants.js ├── hooks.js ├── index.js ├── utils │ ├── defer.js │ ├── helpers.js │ ├── input.js │ ├── mask.js │ └── parse-mask.js └── validate-props.js ├── tests ├── build │ └── build.js ├── input │ └── input.js └── server-render │ └── server-render.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { "modules": false, "loose": true }], 4 | "@babel/preset-react" 5 | ], 6 | "plugins": [ 7 | ["@babel/plugin-proposal-class-properties", { "loose": true }], 8 | ], 9 | "env": { 10 | "test": { 11 | "plugins": [ 12 | "@babel/plugin-transform-proto-to-assign", 13 | "@babel/transform-modules-commonjs" 14 | ] 15 | }, 16 | "rollup": { 17 | "plugins": [ 18 | "babel-plugin-dev-expression" 19 | ] 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const OFF = 0; 2 | const WARN = 1; 3 | const ERROR = 2; 4 | 5 | module.exports = { 6 | parser: "babel-eslint", 7 | parserOptions: { 8 | ecmaVersion: 2018 9 | }, 10 | extends: ["airbnb", "prettier"], 11 | plugins: ["prettier", "react-hooks"], 12 | env: { 13 | browser: true 14 | }, 15 | rules: { 16 | "react/jsx-filename-extension": OFF, 17 | "react/jsx-props-no-spreading": OFF, 18 | "react/require-default-props": OFF, 19 | 'react/no-find-dom-node': OFF, 20 | "react/prop-types": [ERROR, { ignore: ["value", "defaultValue"] }], 21 | "react-hooks/rules-of-hooks": ERROR, 22 | "react-hooks/exhaustive-deps": ERROR, 23 | "no-shadow": OFF, 24 | "no-param-reassign": OFF, 25 | "no-plusplus": OFF, 26 | "global-require": OFF, 27 | "consistent-return": OFF, 28 | "prefer-const": [ERROR, { 29 | "destructuring": "all" 30 | }], 31 | "prettier/prettier": ERROR, 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | dist 4 | npm-debug.log 5 | browserStack.json -------------------------------------------------------------------------------- /.size-snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "dist/react-input-mask.js": { 3 | "bundled": 79669, 4 | "minified": 26008, 5 | "gzipped": 8342 6 | }, 7 | "lib/react-input-mask.development.js": { 8 | "bundled": 30278, 9 | "minified": 12895, 10 | "gzipped": 4267 11 | }, 12 | "dist/react-input-mask.min.js": { 13 | "bundled": 44160, 14 | "minified": 15279, 15 | "gzipped": 5298 16 | }, 17 | "lib/react-input-mask.production.min.js": { 18 | "bundled": 28947, 19 | "minified": 11818, 20 | "gzipped": 3931 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 12 4 | install: 5 | - npm install 6 | env: 7 | - DISABLE_CHROMIUM_SANDBOX=true 8 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Nikita Lobachev 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 | # react-input-mask 2 | 3 | [![Build Status](https://img.shields.io/travis/sanniassin/react-input-mask/master.svg?style=flat)](https://travis-ci.org/sanniassin/react-input-mask) [![npm version](https://img.shields.io/npm/v/react-input-mask.svg?style=flat)](https://www.npmjs.com/package/react-input-mask) [![npm downloads](https://img.shields.io/npm/dm/react-input-mask.svg?style=flat)](https://www.npmjs.com/package/react-input-mask) 4 | 5 | Input masking component for React. Made with attention to UX. 6 | 7 | **This is a development branch for version 3.0. For the latest stable version [see v2 branch](https://github.com/sanniassin/react-input-mask/tree/v2).** 8 | 9 | #### [Demo](http://sanniassin.github.io/react-input-mask/demo.html) 10 | 11 | # Table of Contents 12 | * [Installation](#installation) 13 | * [Usage](#usage) 14 | * [Properties](#properties) 15 | * [Known Issues](#known-issues) 16 | 17 | # Installation 18 | ```npm install react-input-mask@next --save``` 19 | 20 | react-input-mask requires **React 16.8.0 or later.** If you need support for older versions, use [version 2](https://github.com/sanniassin/react-input-mask/tree/v2). 21 | 22 | # Usage 23 | ```jsx 24 | import React from "react" 25 | import InputMask from "react-input-mask"; 26 | 27 | function DateInput(props) { 28 | return ; 29 | } 30 | ``` 31 | 32 | # Properties 33 | | Name | Type | Default | Description | 34 | | :-----------------------------------------: | :-------------------------: | :-----: | :--------------------------------------------------------------------- | 35 | | **[`mask`](#mask)** | `{String\|Array}` | | Mask format | 36 | | **[`maskPlaceholder`](#maskplaceholder)** | `{String}` | `_` | Placeholder to cover unfilled parts of the mask | 37 | | **[`alwaysShowMask`](#alwaysshowmask)** | `{Boolean}` | `false` | Whether mask prefix and placeholder should be displayed when input is empty and has no focus | 38 | | **[`beforeMaskedStateChange`](#beforemaskedstatechange)** | `{Function}` | | Function to modify value and selection before applying mask | 39 | | **[`children`](#children)** | `{ReactElement}` | | Custom render function for integration with other input components | 40 | 41 | 42 | ### `mask` 43 | 44 | Mask format. Can be either a string or array of characters and regular expressions.

45 | 46 | 47 | ```jsx 48 | 49 | ``` 50 | Simple masks can be defined as strings. The following characters will define mask format: 51 | 52 | | Character | Allowed input | 53 | | :-------: | :-----------: | 54 | | 9 | 0-9 | 55 | | a | a-z, A-Z | 56 | | * | 0-9, a-z, A-Z | 57 | 58 | Any format character can be escaped with a backslash.

59 | 60 | 61 | More complex masks can be defined as an array of regular expressions and constant characters. 62 | ```jsx 63 | // Canadian postal code mask 64 | const firstLetter = /(?!.*[DFIOQU])[A-VXY]/i; 65 | const letter = /(?!.*[DFIOQU])[A-Z]/i; 66 | const digit = /[0-9]/; 67 | const mask = [firstLetter, digit, letter, " ", digit, letter, digit]; 68 | return ; 69 | ``` 70 | 71 | 72 | ### `maskPlaceholder` 73 | ```jsx 74 | // Will be rendered as 12/--/-- 75 | 76 | 77 | // Will be rendered as 12/mm/yy 78 | 79 | 80 | // Will be rendered as 12/ 81 | 82 | ``` 83 | Character or string to cover unfilled parts of the mask. Default character is "\_". If set to `null` or empty string, unfilled parts will be empty as in a regular input. 84 | 85 | 86 | ### `alwaysShowMask` 87 | 88 | If enabled, mask prefix and placeholder will be displayed even when input is empty and has no focus. 89 | 90 | 91 | ### `beforeMaskedStateChange` 92 | In case you need to customize masking behavior, you can provide `beforeMaskedStateChange` function to change masked value and cursor position before it's applied to the input. 93 | 94 | It receieves an object with `previousState`, `currentState` and `nextState` properties. Each state is an object with `value` and `selection` properites where `value` is a string and selection is an object containing `start` and `end` positions of the selection. 95 | 1. **previousState:** Input state before change. Only defined on `change` event. 96 | 2. **currentState:** Current raw input state. Not defined during component render. 97 | 3. **nextState:** Input state with applied mask. Contains `value` and `selection` fields. 98 | 99 | Selection positions will be `null` if input isn't focused and during rendering. 100 | 101 | `beforeMaskedStateChange` must return a new state with `value` and `selection`. 102 | 103 | ```jsx 104 | // Trim trailing slashes 105 | function beforeMaskedStateChange({ nextState }) { 106 | let { value } = nextState; 107 | if (value.endsWith("/")) { 108 | value = value.slice(0, -1); 109 | } 110 | 111 | return { 112 | ...nextState, 113 | value 114 | }; 115 | } 116 | 117 | return ; 118 | ``` 119 | 120 | Please note that `beforeMaskedStateChange` executes more often than `onChange` and must be pure. 121 | 122 | 123 | ### `children` 124 | To use another component instead of regular `` provide it as children. The following properties, if used, should always be defined on the `InputMask` component itself: `onChange`, `onMouseDown`, `onFocus`, `onBlur`, `value`, `disabled`, `readOnly`. 125 | ```jsx 126 | import React from 'react'; 127 | import InputMask from 'react-input-mask'; 128 | import MaterialInput from '@material-ui/core/Input'; 129 | 130 | // Will work fine 131 | function Input(props) { 132 | return ( 133 | 134 | 135 | 136 | ); 137 | } 138 | 139 | // Will throw an error because InputMask's and children's onChange props aren't the same 140 | function InvalidInput(props) { 141 | return ( 142 | 143 | 144 | 145 | ); 146 | } 147 | ``` 148 | 149 | # Known Issues 150 | ### Autofill 151 | Browser's autofill requires either empty value in input or value which exactly matches beginning of the autofilled value. I.e. autofilled value "+1 (555) 123-4567" will work with "+1" or "+1 (5", but won't work with "+1 (\_\_\_) \_\_\_-\_\_\_\_" or "1 (555)". There are several possible solutions: 152 | 1. Set `maskChar` to null and trim space after "+1" with `beforeMaskedStateChange` if no more digits are entered. 153 | 2. Apply mask only if value is not empty. In general, this is the most reliable solution because we can't be sure about formatting in autofilled value. 154 | 3. Use less formatting in the mask. 155 | 156 | Please note that it might lead to worse user experience (should I enter +1 if input is empty?). You should choose what's more important to your users — smooth typing experience or autofill. Phone and ZIP code inputs are very likely to be autofilled and it's a good idea to care about it, while security confirmation code in two-factor authorization shouldn't care about autofill at all. 157 | 158 | ### Cypress tests 159 | The following sequence could fail 160 | ```js 161 | cy.get("input") 162 | .focus() 163 | .type("12345") 164 | .should("have.value", "12/34/5___"); // expected to have value 12/34/5___, but the value was 23/45/____ 165 | ```` 166 | 167 | Since [focus is not an action command](https://docs.cypress.io/api/commands/focus.html#Focus-is-not-an-action-command), it behaves differently than the real user interaction and, therefore, less reliable. 168 | 169 | There is a few possible workarounds 170 | ```js 171 | // Start typing without calling focus() explicitly. 172 | // type() is an action command and focuses input anyway 173 | cy.get("input") 174 | .type("12345") 175 | .should("have.value", "12/34/5___"); 176 | 177 | // Use click() instead of focus() 178 | cy.get("input") 179 | .click() 180 | .type("12345") 181 | .should("have.value", "12/34/5___"); 182 | 183 | // Or wait a little after focus() 184 | cy.get("input") 185 | .focus() 186 | .wait(50) 187 | .type("12345") 188 | .should("have.value", "12/34/5___"); 189 | ```` 190 | 191 | # Thanks 192 | Thanks to [BrowserStack](https://www.browserstack.com/) for the help with testing on real devices 193 | -------------------------------------------------------------------------------- /dev/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 |
10 | 11 | -------------------------------------------------------------------------------- /dev/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import React, { useState } from "react"; 3 | import ReactDOM from "react-dom"; 4 | import InputMask from "../src"; 5 | 6 | function Input() { 7 | const [value, setValue] = useState(""); 8 | 9 | function onChange(event) { 10 | setValue(event.target.value); 11 | } 12 | 13 | return ; 14 | } 15 | 16 | function escapeHtml(unsafe) { 17 | return `${unsafe}` 18 | .replace(/&/g, "&") 19 | .replace(//g, ">") 21 | .replace(/"/g, """) 22 | .replace(/'/g, "'"); 23 | } 24 | 25 | const consoleDiv = document.getElementById("console"); 26 | const { log } = console; 27 | console.log = (text, ...rest) => { 28 | log.apply(console, [text, ...rest]); 29 | consoleDiv.innerHTML = `${escapeHtml(text)}
${consoleDiv.innerHTML}`; 30 | }; 31 | 32 | ReactDOM.render(, document.getElementById("root")); 33 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-unresolved */ 2 | 3 | if (process.env.NODE_ENV === "production") { 4 | module.exports = require("./lib/react-input-mask.production.min.js"); 5 | } else { 6 | module.exports = require("./lib/react-input-mask.development.js"); 7 | } 8 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = config => { 2 | config.set({ 3 | // define browsers 4 | customLaunchers: { 5 | ChromeHeadlessNoSandbox: { 6 | base: "ChromeHeadless", 7 | flags: ["--no-sandbox"] 8 | } 9 | }, 10 | 11 | // start these browsers 12 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 13 | browsers: ["ChromeHeadlessNoSandbox"], 14 | 15 | // base path that will be used to resolve all patterns (eg. files, exclude) 16 | basePath: "./", 17 | 18 | // frameworks to use 19 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 20 | frameworks: ["mocha"], 21 | 22 | // list of files / patterns to load in the browser 23 | files: [ 24 | "node_modules/@babel/polyfill/dist/polyfill.min.js", 25 | "node_modules/console-polyfill/index.js", 26 | "tests/input/*.js" 27 | ], 28 | 29 | // list of files to exclude 30 | exclude: ["karma.conf.js"], 31 | 32 | // preprocess matching files before serving them to the browser 33 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 34 | preprocessors: { 35 | "tests/input/*.js": ["webpack"] 36 | }, 37 | 38 | // test results reporter to use 39 | // possible values: 'dots', 'progress' 40 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 41 | reporters: ["progress"], 42 | 43 | // web server port 44 | port: 9876, 45 | 46 | // enable / disable colors in the output (reporters and logs) 47 | colors: true, 48 | 49 | // level of logging 50 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 51 | logLevel: config.LOG_INFO, 52 | 53 | // enable / disable watching file and executing tests whenever any file changes 54 | autoWatch: false, 55 | 56 | // Continuous Integration mode 57 | // if true, Karma captures browsers, runs the tests and exits 58 | singleRun: true, 59 | 60 | webpack: { 61 | devtool: false, 62 | performance: { 63 | hints: false 64 | }, 65 | mode: "development", 66 | output: { 67 | filename: "[name].js" 68 | }, 69 | resolve: { 70 | modules: ["node_modules", "."] 71 | }, 72 | module: require("./webpack.config").module 73 | } 74 | }); 75 | }; 76 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-input-mask", 3 | "description": "Masked input component for React", 4 | "version": "3.0.0-alpha.2", 5 | "homepage": "https://github.com/sanniassin/react-input-mask", 6 | "license": "MIT", 7 | "author": "Nikita Lobachev ", 8 | "keywords": [ 9 | "react", 10 | "input", 11 | "mask", 12 | "masked", 13 | "react-component" 14 | ], 15 | "peerDependencies": { 16 | "react": ">=16.8", 17 | "react-dom": ">=16.8" 18 | }, 19 | "devDependencies": { 20 | "@babel/cli": "^7.8.3", 21 | "@babel/core": "^7.8.3", 22 | "@babel/plugin-proposal-class-properties": "^7.8.3", 23 | "@babel/plugin-transform-modules-commonjs": "^7.8.3", 24 | "@babel/plugin-transform-proto-to-assign": "^7.8.3", 25 | "@babel/polyfill": "^7.8.3", 26 | "@babel/preset-env": "^7.8.3", 27 | "@babel/preset-react": "^7.8.3", 28 | "@babel/register": "^7.8.3", 29 | "babel-eslint": "^10.0.3", 30 | "babel-loader": "^8.0.6", 31 | "babel-plugin-dev-expression": "^0.2.2", 32 | "babel-plugin-minify-dead-code-elimination": "^0.5.1", 33 | "babel-plugin-transform-react-remove-prop-types": "^0.4.24", 34 | "chai": "^4.2.0", 35 | "console-polyfill": "^0.3.0", 36 | "cross-env": "^5.2.1", 37 | "eslint": "^6.8.0", 38 | "eslint-config-airbnb": "^18.0.1", 39 | "eslint-config-prettier": "^6.9.0", 40 | "eslint-plugin-import": "^2.20.0", 41 | "eslint-plugin-jsx-a11y": "^6.2.3", 42 | "eslint-plugin-prettier": "^3.1.2", 43 | "eslint-plugin-react": "^7.18.0", 44 | "eslint-plugin-react-hooks": "^1.7.0", 45 | "html-webpack-plugin": "^3.2.0", 46 | "husky": "^3.1.0", 47 | "karma": "^4.4.1", 48 | "karma-chrome-launcher": "^3.1.0", 49 | "karma-mocha": "^1.3.0", 50 | "karma-webpack": "^4.0.2", 51 | "lint-staged": "^9.5.0", 52 | "mocha": "^6.2.2", 53 | "prettier": "^1.19.1", 54 | "puppeteer": "^1.20.0", 55 | "react": "^16.12.0", 56 | "react-dom": "^16.12.0", 57 | "rollup": "^1.29.0", 58 | "rollup-plugin-babel": "^4.3.3", 59 | "rollup-plugin-commonjs": "^10.1.0", 60 | "rollup-plugin-node-resolve": "^5.2.0", 61 | "rollup-plugin-replace": "^2.2.0", 62 | "rollup-plugin-size-snapshot": "^0.10.0", 63 | "rollup-plugin-terser": "^5.2.0", 64 | "webpack": "^4.41.5", 65 | "webpack-cli": "^3.3.10", 66 | "webpack-dev-server": "^3.10.1" 67 | }, 68 | "main": "index.js", 69 | "files": [ 70 | "lib", 71 | "dist" 72 | ], 73 | "lint-staged": { 74 | "*.js": "eslint" 75 | }, 76 | "husky": { 77 | "hooks": { 78 | "pre-commit": "lint-staged" 79 | } 80 | }, 81 | "scripts": { 82 | "clean": "rimraf lib dist", 83 | "build": "cross-env BABEL_ENV=rollup rollup -c", 84 | "start": "cross-env BABEL_ENV=test NODE_ENV=development webpack-dev-server", 85 | "prepare": "npm run lint && npm test && npm run clean && npm run build", 86 | "lint": "eslint ./src ./tests ./*.js", 87 | "lint-fix": "eslint --fix ./src ./tests ./*.js", 88 | "test": "npm run build && npm run test:input && npm run test:server-render && npm run test:build", 89 | "test:input": "cross-env NODE_ENV=test BABEL_ENV=test karma start", 90 | "test:server-render": "cross-env BABEL_ENV=test mocha --require @babel/register ./tests/server-render", 91 | "test:build": "cross-env BABEL_ENV=test mocha --require @babel/register ./tests/build" 92 | }, 93 | "repository": { 94 | "type": "git", 95 | "url": "https://github.com/sanniassin/react-input-mask.git" 96 | }, 97 | "dependencies": { 98 | "invariant": "^2.2.4", 99 | "prop-types": "^15.7.2", 100 | "warning": "^4.0.3" 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from "rollup-plugin-babel"; 2 | import { terser } from "rollup-plugin-terser"; 3 | import resolve from "rollup-plugin-node-resolve"; 4 | import commonjs from "rollup-plugin-commonjs"; 5 | import replace from "rollup-plugin-replace"; 6 | import { sizeSnapshot } from "rollup-plugin-size-snapshot"; 7 | import protoToAssign from "./rollup.proto-to-assign.plugin"; 8 | 9 | const input = "./src/index.js"; 10 | 11 | // Treat as externals all not relative and not absolute paths 12 | // e.g. 'react' to prevent duplications in user bundle. 13 | const isExternal = id => 14 | !id.startsWith("\0") && !id.startsWith(".") && !id.startsWith("/"); 15 | 16 | const external = ["react", "react-dom"]; 17 | const plugins = [ 18 | babel(), 19 | resolve(), 20 | commonjs(), 21 | protoToAssign(), 22 | sizeSnapshot() 23 | ]; 24 | const minifiedPlugins = [ 25 | ...plugins, 26 | replace({ 27 | "process.env.NODE_ENV": '"production"' 28 | }), 29 | babel({ 30 | babelrc: false, 31 | plugins: [ 32 | "babel-plugin-minify-dead-code-elimination", 33 | "babel-plugin-transform-react-remove-prop-types" 34 | ] 35 | }), 36 | terser({ 37 | compress: { warnings: false } 38 | }) 39 | ]; 40 | 41 | export default [ 42 | { 43 | input, 44 | output: { 45 | file: "dist/react-input-mask.js", 46 | format: "umd", 47 | name: "ReactInputMask", 48 | globals: { react: "React", "react-dom": "ReactDOM" } 49 | }, 50 | external, 51 | plugins: [ 52 | ...plugins, 53 | replace({ 54 | "process.env.NODE_ENV": '"development"' 55 | }) 56 | ] 57 | }, 58 | 59 | { 60 | input, 61 | output: { 62 | file: "dist/react-input-mask.min.js", 63 | format: "umd", 64 | name: "ReactInputMask", 65 | globals: { react: "React", "react-dom": "ReactDOM" } 66 | }, 67 | external, 68 | plugins: minifiedPlugins 69 | }, 70 | 71 | { 72 | input, 73 | output: { file: "lib/react-input-mask.development.js", format: "cjs" }, 74 | external: isExternal, 75 | plugins 76 | }, 77 | 78 | { 79 | input, 80 | output: { file: "lib/react-input-mask.production.min.js", format: "cjs" }, 81 | external: isExternal, 82 | plugins: minifiedPlugins 83 | } 84 | ]; 85 | -------------------------------------------------------------------------------- /rollup.proto-to-assign.plugin.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-extraneous-dependencies 2 | import { transform } from "@babel/core"; 3 | 4 | export default function protoToAssignTransform(options = {}) { 5 | return { 6 | transform(code) { 7 | const transformed = transform(code, { 8 | babelrc: false, 9 | plugins: ["@babel/plugin-transform-proto-to-assign"] 10 | }); 11 | 12 | const result = { code: transformed.code }; 13 | 14 | if (options.sourceMap !== false || options.sourcemap !== false) { 15 | result.map = transformed.map; 16 | } 17 | 18 | return result; 19 | } 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/children-wrapper.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default class InputMaskChildrenWrapper extends React.Component { 4 | render() { 5 | // eslint-disable-next-line react/prop-types 6 | const { children, ...props } = this.props; 7 | return React.cloneElement(children, props); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | export const CONTROLLED_PROPS = [ 2 | "disabled", 3 | "onBlur", 4 | "onChange", 5 | "onFocus", 6 | "onMouseDown", 7 | "readOnly", 8 | "value" 9 | ]; 10 | 11 | export const defaultFormatChars = { 12 | "9": /[0-9]/, 13 | a: /[A-Za-z]/, 14 | "*": /[A-Za-z0-9]/ 15 | }; 16 | -------------------------------------------------------------------------------- /src/hooks.js: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useLayoutEffect, useRef } from "react"; 2 | 3 | import { defer, cancelDefer } from "./utils/defer"; 4 | import { 5 | setInputSelection, 6 | getInputSelection, 7 | isInputFocused 8 | } from "./utils/input"; 9 | import { isDOMElement } from "./utils/helpers"; 10 | 11 | export function useInputElement(inputRef) { 12 | return useCallback(() => { 13 | let input = inputRef.current; 14 | const isDOMNode = typeof window !== "undefined" && isDOMElement(input); 15 | 16 | // workaround for react-test-renderer 17 | // https://github.com/sanniassin/react-input-mask/issues/147 18 | if (!input || !isDOMNode) { 19 | return null; 20 | } 21 | 22 | if (input.nodeName !== "INPUT") { 23 | input = input.querySelector("input"); 24 | } 25 | 26 | if (!input) { 27 | throw new Error( 28 | "react-input-mask: inputComponent doesn't contain input node" 29 | ); 30 | } 31 | 32 | return input; 33 | }, [inputRef]); 34 | } 35 | 36 | function useDeferLoop(callback) { 37 | const deferIdRef = useRef(null); 38 | 39 | const runLoop = useCallback(() => { 40 | // If there are simulated focus events, runLoop could be 41 | // called multiple times without blur or re-render 42 | if (deferIdRef.current !== null) { 43 | return; 44 | } 45 | 46 | function loop() { 47 | callback(); 48 | deferIdRef.current = defer(loop); 49 | } 50 | 51 | loop(); 52 | }, [callback]); 53 | 54 | const stopLoop = useCallback(() => { 55 | cancelDefer(deferIdRef.current); 56 | deferIdRef.current = null; 57 | }, []); 58 | 59 | useEffect(() => { 60 | if (deferIdRef.current) { 61 | stopLoop(); 62 | runLoop(); 63 | } 64 | }, [runLoop, stopLoop]); 65 | 66 | useEffect(cancelDefer, []); 67 | 68 | return [runLoop, stopLoop]; 69 | } 70 | 71 | function useSelection(inputRef, isMasked) { 72 | const selectionRef = useRef({ start: null, end: null }); 73 | const getInputElement = useInputElement(inputRef); 74 | 75 | const getSelection = useCallback(() => { 76 | const input = getInputElement(); 77 | return getInputSelection(input); 78 | }, [getInputElement]); 79 | 80 | const getLastSelection = useCallback(() => { 81 | return selectionRef.current; 82 | }, []); 83 | 84 | const setSelection = useCallback( 85 | selection => { 86 | const input = getInputElement(); 87 | 88 | // Don't change selection on unfocused input 89 | // because Safari sets focus on selection change (#154) 90 | if (!input || !isInputFocused(input)) { 91 | return; 92 | } 93 | 94 | setInputSelection(input, selection.start, selection.end); 95 | 96 | // Use actual selection in case the requested one was out of range 97 | selectionRef.current = getSelection(); 98 | }, 99 | [getInputElement, getSelection] 100 | ); 101 | 102 | const selectionLoop = useCallback(() => { 103 | selectionRef.current = getSelection(); 104 | }, [getSelection]); 105 | const [runSelectionLoop, stopSelectionLoop] = useDeferLoop(selectionLoop); 106 | 107 | useLayoutEffect(() => { 108 | if (!isMasked) { 109 | return; 110 | } 111 | 112 | const input = getInputElement(); 113 | input.addEventListener("focus", runSelectionLoop); 114 | input.addEventListener("blur", stopSelectionLoop); 115 | 116 | if (isInputFocused(input)) { 117 | runSelectionLoop(); 118 | } 119 | 120 | return () => { 121 | input.removeEventListener("focus", runSelectionLoop); 122 | input.removeEventListener("blur", stopSelectionLoop); 123 | 124 | stopSelectionLoop(); 125 | }; 126 | }); 127 | 128 | return { getSelection, getLastSelection, setSelection }; 129 | } 130 | 131 | function useValue(inputRef, initialValue) { 132 | const getInputElement = useInputElement(inputRef); 133 | const valueRef = useRef(initialValue); 134 | 135 | const getValue = useCallback(() => { 136 | const input = getInputElement(); 137 | return input.value; 138 | }, [getInputElement]); 139 | 140 | const getLastValue = useCallback(() => { 141 | return valueRef.current; 142 | }, []); 143 | 144 | const setValue = useCallback( 145 | newValue => { 146 | valueRef.current = newValue; 147 | 148 | const input = getInputElement(); 149 | if (input) { 150 | input.value = newValue; 151 | } 152 | }, 153 | [getInputElement] 154 | ); 155 | 156 | return { 157 | getValue, 158 | getLastValue, 159 | setValue 160 | }; 161 | } 162 | 163 | export function useInputState(initialValue, isMasked) { 164 | const inputRef = useRef(); 165 | const { getSelection, getLastSelection, setSelection } = useSelection( 166 | inputRef, 167 | isMasked 168 | ); 169 | const { getValue, getLastValue, setValue } = useValue(inputRef, initialValue); 170 | 171 | function getLastInputState() { 172 | return { 173 | value: getLastValue(), 174 | selection: getLastSelection() 175 | }; 176 | } 177 | 178 | function getInputState() { 179 | return { 180 | value: getValue(), 181 | selection: getSelection() 182 | }; 183 | } 184 | 185 | function setInputState({ value, selection }) { 186 | setValue(value); 187 | setSelection(selection); 188 | } 189 | 190 | return { 191 | inputRef, 192 | getInputState, 193 | getLastInputState, 194 | setInputState 195 | }; 196 | } 197 | 198 | export function usePrevious(value) { 199 | const ref = useRef(); 200 | useEffect(() => { 201 | ref.current = value; 202 | }); 203 | return ref.current; 204 | } 205 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React, { useLayoutEffect, forwardRef } from "react"; 2 | import { findDOMNode } from "react-dom"; 3 | import PropTypes from "prop-types"; 4 | 5 | import { useInputState, useInputElement, usePrevious } from "./hooks"; 6 | import { 7 | validateMaxLength, 8 | validateChildren, 9 | validateMaskPlaceholder 10 | } from "./validate-props"; 11 | 12 | import { defer } from "./utils/defer"; 13 | import { isInputFocused } from "./utils/input"; 14 | import { isFunction, toString, getElementDocument } from "./utils/helpers"; 15 | import MaskUtils from "./utils/mask"; 16 | import ChildrenWrapper from "./children-wrapper"; 17 | 18 | const InputMask = forwardRef(function InputMask(props, forwardedRef) { 19 | const { 20 | alwaysShowMask, 21 | children, 22 | mask, 23 | maskPlaceholder, 24 | beforeMaskedStateChange, 25 | ...restProps 26 | } = props; 27 | 28 | validateMaxLength(props); 29 | validateMaskPlaceholder(props); 30 | 31 | const maskUtils = new MaskUtils({ mask, maskPlaceholder }); 32 | 33 | const isMasked = !!mask; 34 | const isEditable = !restProps.disabled && !restProps.readOnly; 35 | const isControlled = props.value !== null && props.value !== undefined; 36 | const previousIsMasked = usePrevious(isMasked); 37 | const initialValue = toString( 38 | (isControlled ? props.value : props.defaultValue) || "" 39 | ); 40 | 41 | const { 42 | inputRef, 43 | getInputState, 44 | setInputState, 45 | getLastInputState 46 | } = useInputState(initialValue, isMasked); 47 | const getInputElement = useInputElement(inputRef); 48 | 49 | function onChange(event) { 50 | const currentState = getInputState(); 51 | const previousState = getLastInputState(); 52 | let newInputState = maskUtils.processChange(currentState, previousState); 53 | 54 | if (beforeMaskedStateChange) { 55 | newInputState = beforeMaskedStateChange({ 56 | currentState, 57 | previousState, 58 | nextState: newInputState 59 | }); 60 | } 61 | 62 | setInputState(newInputState); 63 | 64 | if (props.onChange) { 65 | props.onChange(event); 66 | } 67 | } 68 | 69 | function onFocus(event) { 70 | // If autoFocus property is set, focus event fires before the ref handler gets called 71 | inputRef.current = event.target; 72 | 73 | const currentValue = getInputState().value; 74 | 75 | if (isMasked && !maskUtils.isValueFilled(currentValue)) { 76 | let newValue = maskUtils.formatValue(currentValue); 77 | let newSelection = maskUtils.getDefaultSelectionForValue(newValue); 78 | let newInputState = { 79 | value: newValue, 80 | selection: newSelection 81 | }; 82 | 83 | if (beforeMaskedStateChange) { 84 | newInputState = beforeMaskedStateChange({ 85 | currentState: getInputState(), 86 | nextState: newInputState 87 | }); 88 | newValue = newInputState.value; 89 | newSelection = newInputState.selection; 90 | } 91 | 92 | setInputState(newInputState); 93 | 94 | if (newValue !== currentValue && props.onChange) { 95 | props.onChange(event); 96 | } 97 | 98 | // Chrome resets selection after focus event, 99 | // so we want to restore it later 100 | defer(() => { 101 | setInputState(getLastInputState()); 102 | }); 103 | } 104 | 105 | if (props.onFocus) { 106 | props.onFocus(event); 107 | } 108 | } 109 | 110 | function onBlur(event) { 111 | const currentValue = getInputState().value; 112 | const lastValue = getLastInputState().value; 113 | 114 | if (isMasked && !alwaysShowMask && maskUtils.isValueEmpty(lastValue)) { 115 | let newValue = ""; 116 | let newInputState = { 117 | value: newValue, 118 | selection: { start: null, end: null } 119 | }; 120 | 121 | if (beforeMaskedStateChange) { 122 | newInputState = beforeMaskedStateChange({ 123 | currentState: getInputState(), 124 | nextState: newInputState 125 | }); 126 | newValue = newInputState.value; 127 | } 128 | 129 | setInputState(newInputState); 130 | 131 | if (newValue !== currentValue && props.onChange) { 132 | props.onChange(event); 133 | } 134 | } 135 | 136 | if (props.onBlur) { 137 | props.onBlur(event); 138 | } 139 | } 140 | 141 | // Tiny unintentional mouse movements can break cursor 142 | // position on focus, so we have to restore it in that case 143 | // 144 | // https://github.com/sanniassin/react-input-mask/issues/108 145 | function onMouseDown(event) { 146 | const input = getInputElement(); 147 | const { value } = getInputState(); 148 | const inputDocument = getElementDocument(input); 149 | 150 | if (!isInputFocused(input) && !maskUtils.isValueFilled(value)) { 151 | const mouseDownX = event.clientX; 152 | const mouseDownY = event.clientY; 153 | const mouseDownTime = new Date().getTime(); 154 | 155 | const mouseUpHandler = mouseUpEvent => { 156 | inputDocument.removeEventListener("mouseup", mouseUpHandler); 157 | 158 | if (!isInputFocused(input)) { 159 | return; 160 | } 161 | 162 | const deltaX = Math.abs(mouseUpEvent.clientX - mouseDownX); 163 | const deltaY = Math.abs(mouseUpEvent.clientY - mouseDownY); 164 | const axisDelta = Math.max(deltaX, deltaY); 165 | const timeDelta = new Date().getTime() - mouseDownTime; 166 | 167 | if ( 168 | (axisDelta <= 10 && timeDelta <= 200) || 169 | (axisDelta <= 5 && timeDelta <= 300) 170 | ) { 171 | const lastState = getLastInputState(); 172 | const newSelection = maskUtils.getDefaultSelectionForValue( 173 | lastState.value 174 | ); 175 | const newState = { 176 | ...lastState, 177 | selection: newSelection 178 | }; 179 | setInputState(newState); 180 | } 181 | }; 182 | 183 | inputDocument.addEventListener("mouseup", mouseUpHandler); 184 | } 185 | 186 | if (props.onMouseDown) { 187 | props.onMouseDown(event); 188 | } 189 | } 190 | 191 | // For controlled inputs we want to provide properly formatted 192 | // value prop 193 | if (isMasked && isControlled) { 194 | const input = getInputElement(); 195 | const isFocused = input && isInputFocused(input); 196 | let newValue = 197 | isFocused || alwaysShowMask || props.value 198 | ? maskUtils.formatValue(props.value) 199 | : props.value; 200 | 201 | if (beforeMaskedStateChange) { 202 | newValue = beforeMaskedStateChange({ 203 | nextState: { value: newValue, selection: { start: null, end: null } } 204 | }).value; 205 | } 206 | 207 | setInputState({ 208 | ...getLastInputState(), 209 | value: newValue 210 | }); 211 | } 212 | 213 | const lastState = getLastInputState(); 214 | const lastSelection = lastState.selection; 215 | const lastValue = lastState.value; 216 | 217 | useLayoutEffect(() => { 218 | if (!isMasked) { 219 | return; 220 | } 221 | 222 | const input = getInputElement(); 223 | const isFocused = isInputFocused(input); 224 | const previousSelection = lastSelection; 225 | const currentState = getInputState(); 226 | let newInputState = { ...currentState }; 227 | 228 | // Update value for uncontrolled inputs to make sure 229 | // it's always in sync with mask props 230 | if (!isControlled) { 231 | const currentValue = currentState.value; 232 | const formattedValue = maskUtils.formatValue(currentValue); 233 | const isValueEmpty = maskUtils.isValueEmpty(formattedValue); 234 | const shouldFormatValue = !isValueEmpty || isFocused || alwaysShowMask; 235 | if (shouldFormatValue) { 236 | newInputState.value = formattedValue; 237 | } else if (isValueEmpty && !isFocused) { 238 | newInputState.value = ""; 239 | } 240 | } 241 | 242 | if (isFocused && !previousIsMasked) { 243 | // Adjust selection if input got masked while being focused 244 | newInputState.selection = maskUtils.getDefaultSelectionForValue( 245 | newInputState.value 246 | ); 247 | } else if (isControlled && isFocused && previousSelection) { 248 | // Restore cursor position if value has changed outside change event 249 | if (previousSelection.start !== null && previousSelection.end !== null) { 250 | newInputState.selection = previousSelection; 251 | } 252 | } 253 | 254 | if (beforeMaskedStateChange) { 255 | newInputState = beforeMaskedStateChange({ 256 | currentState, 257 | nextState: newInputState 258 | }); 259 | } 260 | 261 | setInputState(newInputState); 262 | }); 263 | 264 | const inputProps = { 265 | ...restProps, 266 | onFocus, 267 | onBlur, 268 | onChange: isMasked && isEditable ? onChange : props.onChange, 269 | onMouseDown: isMasked && isEditable ? onMouseDown : props.onMouseDown, 270 | ref: ref => { 271 | inputRef.current = findDOMNode(ref); 272 | 273 | if (isFunction(forwardedRef)) { 274 | forwardedRef(ref); 275 | } else if (forwardedRef !== null && typeof forwardedRef === "object") { 276 | forwardedRef.current = ref; 277 | } 278 | }, 279 | value: isMasked && isControlled ? lastValue : props.value 280 | }; 281 | 282 | if (children) { 283 | validateChildren(props, children); 284 | 285 | // We wrap children into a class component to be able to find 286 | // their input element using findDOMNode 287 | return {children}; 288 | } 289 | 290 | return ; 291 | }); 292 | 293 | InputMask.displayName = "InputMask"; 294 | 295 | InputMask.defaultProps = { 296 | alwaysShowMask: false, 297 | maskPlaceholder: "_" 298 | }; 299 | 300 | InputMask.propTypes = { 301 | alwaysShowMask: PropTypes.bool, 302 | beforeMaskedStateChange: PropTypes.func, 303 | children: PropTypes.element, 304 | mask: PropTypes.oneOfType([ 305 | PropTypes.string, 306 | PropTypes.arrayOf( 307 | PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(RegExp)]) 308 | ) 309 | ]), 310 | maskPlaceholder: PropTypes.string, 311 | onFocus: PropTypes.func, 312 | onBlur: PropTypes.func, 313 | onChange: PropTypes.func, 314 | onMouseDown: PropTypes.func 315 | }; 316 | 317 | export default InputMask; 318 | -------------------------------------------------------------------------------- /src/utils/defer.js: -------------------------------------------------------------------------------- 1 | export function defer(fn) { 2 | return requestAnimationFrame(fn); 3 | } 4 | 5 | export function cancelDefer(deferId) { 6 | cancelAnimationFrame(deferId); 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/helpers.js: -------------------------------------------------------------------------------- 1 | // Element's window may differ from the one within React instance 2 | // if element rendered within iframe. 3 | // See https://github.com/sanniassin/react-input-mask/issues/182 4 | export function getElementDocument(element) { 5 | return element?.ownerDocument; 6 | } 7 | 8 | export function getElementWindow(element) { 9 | return getElementDocument(element)?.defaultView; 10 | } 11 | 12 | export function isDOMElement(element) { 13 | const elementWindow = getElementWindow(element); 14 | return !!elementWindow && element instanceof elementWindow.HTMLElement; 15 | } 16 | 17 | export function isFunction(value) { 18 | return typeof value === "function"; 19 | } 20 | 21 | export function findLastIndex(array, predicate) { 22 | for (let i = array.length - 1; i >= 0; i--) { 23 | const x = array[i]; 24 | if (predicate(x, i)) { 25 | return i; 26 | } 27 | } 28 | return -1; 29 | } 30 | 31 | export function repeat(string, n = 1) { 32 | let result = ""; 33 | for (let i = 0; i < n; i++) { 34 | result += string; 35 | } 36 | return result; 37 | } 38 | 39 | export function toString(value) { 40 | return `${value}`; 41 | } 42 | -------------------------------------------------------------------------------- /src/utils/input.js: -------------------------------------------------------------------------------- 1 | export function setInputSelection(input, start, end) { 2 | if (end === undefined) { 3 | end = start; 4 | } 5 | input.setSelectionRange(start, end); 6 | } 7 | 8 | export function getInputSelection(input) { 9 | const start = input.selectionStart; 10 | const end = input.selectionEnd; 11 | 12 | return { 13 | start, 14 | end, 15 | length: end - start 16 | }; 17 | } 18 | 19 | export function isInputFocused(input) { 20 | const inputDocument = input.ownerDocument; 21 | return inputDocument.hasFocus() && inputDocument.activeElement === input; 22 | } 23 | -------------------------------------------------------------------------------- /src/utils/mask.js: -------------------------------------------------------------------------------- 1 | /* eslint no-use-before-define: ["error", { functions: false }] */ 2 | import { findLastIndex, repeat } from "./helpers"; 3 | import parseMask from "./parse-mask"; 4 | 5 | export default class MaskUtils { 6 | constructor(options) { 7 | this.maskOptions = parseMask(options); 8 | } 9 | 10 | isCharacterAllowedAtPosition = (character, position) => { 11 | const { maskPlaceholder } = this.maskOptions; 12 | 13 | if (this.isCharacterFillingPosition(character, position)) { 14 | return true; 15 | } 16 | 17 | if (!maskPlaceholder) { 18 | return false; 19 | } 20 | 21 | return maskPlaceholder[position] === character; 22 | }; 23 | 24 | isCharacterFillingPosition = (character, position) => { 25 | const { mask } = this.maskOptions; 26 | 27 | if (!character || position >= mask.length) { 28 | return false; 29 | } 30 | 31 | if (!this.isPositionEditable(position)) { 32 | return mask[position] === character; 33 | } 34 | 35 | const charRule = mask[position]; 36 | return new RegExp(charRule).test(character); 37 | }; 38 | 39 | isPositionEditable = position => { 40 | const { mask, permanents } = this.maskOptions; 41 | return position < mask.length && permanents.indexOf(position) === -1; 42 | }; 43 | 44 | isValueEmpty = value => { 45 | return value.split("").every((character, position) => { 46 | return ( 47 | !this.isPositionEditable(position) || 48 | !this.isCharacterFillingPosition(character, position) 49 | ); 50 | }); 51 | }; 52 | 53 | isValueFilled = value => { 54 | return ( 55 | this.getFilledLength(value) === this.maskOptions.lastEditablePosition + 1 56 | ); 57 | }; 58 | 59 | getDefaultSelectionForValue = value => { 60 | const filledLength = this.getFilledLength(value); 61 | const cursorPosition = this.getRightEditablePosition(filledLength); 62 | return { start: cursorPosition, end: cursorPosition }; 63 | }; 64 | 65 | getFilledLength = value => { 66 | const characters = value.split(""); 67 | const lastFilledIndex = findLastIndex(characters, (character, position) => { 68 | return ( 69 | this.isPositionEditable(position) && 70 | this.isCharacterFillingPosition(character, position) 71 | ); 72 | }); 73 | return lastFilledIndex + 1; 74 | }; 75 | 76 | getStringFillingLengthAtPosition = (string, position) => { 77 | const characters = string.split(""); 78 | const insertedValue = characters.reduce((value, character) => { 79 | return this.insertCharacterAtPosition(value, character, value.length); 80 | }, repeat(" ", position)); 81 | 82 | return insertedValue.length - position; 83 | }; 84 | 85 | getLeftEditablePosition = position => { 86 | for (let i = position; i >= 0; i--) { 87 | if (this.isPositionEditable(i)) { 88 | return i; 89 | } 90 | } 91 | return null; 92 | }; 93 | 94 | getRightEditablePosition = position => { 95 | const { mask } = this.maskOptions; 96 | for (let i = position; i < mask.length; i++) { 97 | if (this.isPositionEditable(i)) { 98 | return i; 99 | } 100 | } 101 | return null; 102 | }; 103 | 104 | formatValue = value => { 105 | const { maskPlaceholder, mask } = this.maskOptions; 106 | 107 | if (!maskPlaceholder) { 108 | value = this.insertStringAtPosition("", value, 0); 109 | 110 | while ( 111 | value.length < mask.length && 112 | !this.isPositionEditable(value.length) 113 | ) { 114 | value += mask[value.length]; 115 | } 116 | 117 | return value; 118 | } 119 | 120 | return this.insertStringAtPosition(maskPlaceholder, value, 0); 121 | }; 122 | 123 | clearRange = (value, start, len) => { 124 | if (!len) { 125 | return value; 126 | } 127 | 128 | const end = start + len; 129 | const { maskPlaceholder, mask } = this.maskOptions; 130 | 131 | const clearedValue = value 132 | .split("") 133 | .map((character, i) => { 134 | const isEditable = this.isPositionEditable(i); 135 | 136 | if (!maskPlaceholder && i >= end && !isEditable) { 137 | return ""; 138 | } 139 | if (i < start || i >= end) { 140 | return character; 141 | } 142 | if (!isEditable) { 143 | return mask[i]; 144 | } 145 | if (maskPlaceholder) { 146 | return maskPlaceholder[i]; 147 | } 148 | return ""; 149 | }) 150 | .join(""); 151 | 152 | return this.formatValue(clearedValue); 153 | }; 154 | 155 | insertCharacterAtPosition = (value, character, position) => { 156 | const { mask, maskPlaceholder } = this.maskOptions; 157 | if (position >= mask.length) { 158 | return value; 159 | } 160 | 161 | const isAllowed = this.isCharacterAllowedAtPosition(character, position); 162 | const isEditable = this.isPositionEditable(position); 163 | const nextEditablePosition = this.getRightEditablePosition(position); 164 | const isNextPlaceholder = 165 | maskPlaceholder && nextEditablePosition 166 | ? character === maskPlaceholder[nextEditablePosition] 167 | : null; 168 | const valueBefore = value.slice(0, position); 169 | 170 | if (isAllowed || !isEditable) { 171 | const insertedCharacter = isAllowed ? character : mask[position]; 172 | value = valueBefore + insertedCharacter; 173 | } 174 | 175 | if (!isAllowed && !isEditable && !isNextPlaceholder) { 176 | value = this.insertCharacterAtPosition(value, character, position + 1); 177 | } 178 | 179 | return value; 180 | }; 181 | 182 | insertStringAtPosition = (value, string, position) => { 183 | const { mask, maskPlaceholder } = this.maskOptions; 184 | if (!string || position >= mask.length) { 185 | return value; 186 | } 187 | 188 | const characters = string.split(""); 189 | const isFixedLength = this.isValueFilled(value) || !!maskPlaceholder; 190 | const valueAfter = value.slice(position); 191 | 192 | value = characters.reduce((value, character) => { 193 | return this.insertCharacterAtPosition(value, character, value.length); 194 | }, value.slice(0, position)); 195 | 196 | if (isFixedLength) { 197 | value += valueAfter.slice(value.length - position); 198 | } else if (this.isValueFilled(value)) { 199 | value += mask.slice(value.length).join(""); 200 | } else { 201 | const editableCharactersAfter = valueAfter 202 | .split("") 203 | .filter((character, i) => { 204 | return this.isPositionEditable(position + i); 205 | }); 206 | value = editableCharactersAfter.reduce((value, character) => { 207 | const nextEditablePosition = this.getRightEditablePosition( 208 | value.length 209 | ); 210 | if (nextEditablePosition === null) { 211 | return value; 212 | } 213 | 214 | if (!this.isPositionEditable(value.length)) { 215 | value += mask.slice(value.length, nextEditablePosition).join(""); 216 | } 217 | 218 | return this.insertCharacterAtPosition(value, character, value.length); 219 | }, value); 220 | } 221 | 222 | return value; 223 | }; 224 | 225 | processChange = (currentState, previousState) => { 226 | const { mask, prefix, lastEditablePosition } = this.maskOptions; 227 | const { value, selection } = currentState; 228 | const previousValue = previousState.value; 229 | const previousSelection = previousState.selection; 230 | let newValue = value; 231 | let enteredString = ""; 232 | let formattedEnteredStringLength = 0; 233 | let removedLength = 0; 234 | let cursorPosition = Math.min(previousSelection.start, selection.start); 235 | 236 | if (selection.end > previousSelection.start) { 237 | enteredString = newValue.slice(previousSelection.start, selection.end); 238 | formattedEnteredStringLength = this.getStringFillingLengthAtPosition( 239 | enteredString, 240 | cursorPosition 241 | ); 242 | if (!formattedEnteredStringLength) { 243 | removedLength = 0; 244 | } else { 245 | removedLength = previousSelection.length; 246 | } 247 | } else if (newValue.length < previousValue.length) { 248 | removedLength = previousValue.length - newValue.length; 249 | } 250 | 251 | newValue = previousValue; 252 | 253 | if (removedLength) { 254 | if (removedLength === 1 && !previousSelection.length) { 255 | const deleteFromRight = previousSelection.start === selection.start; 256 | cursorPosition = deleteFromRight 257 | ? this.getRightEditablePosition(selection.start) 258 | : this.getLeftEditablePosition(selection.start); 259 | } 260 | newValue = this.clearRange(newValue, cursorPosition, removedLength); 261 | } 262 | 263 | newValue = this.insertStringAtPosition( 264 | newValue, 265 | enteredString, 266 | cursorPosition 267 | ); 268 | 269 | cursorPosition += formattedEnteredStringLength; 270 | if (cursorPosition >= mask.length) { 271 | cursorPosition = mask.length; 272 | } else if ( 273 | cursorPosition < prefix.length && 274 | !formattedEnteredStringLength 275 | ) { 276 | cursorPosition = prefix.length; 277 | } else if ( 278 | cursorPosition >= prefix.length && 279 | cursorPosition < lastEditablePosition && 280 | formattedEnteredStringLength 281 | ) { 282 | cursorPosition = this.getRightEditablePosition(cursorPosition); 283 | } 284 | 285 | newValue = this.formatValue(newValue); 286 | 287 | return { 288 | value: newValue, 289 | enteredString, 290 | selection: { start: cursorPosition, end: cursorPosition } 291 | }; 292 | }; 293 | } 294 | -------------------------------------------------------------------------------- /src/utils/parse-mask.js: -------------------------------------------------------------------------------- 1 | import { defaultFormatChars } from "../constants"; 2 | 3 | export default function({ mask, maskPlaceholder }) { 4 | const permanents = []; 5 | 6 | if (!mask) { 7 | return { 8 | maskPlaceholder: null, 9 | mask: null, 10 | prefix: null, 11 | lastEditablePosition: null, 12 | permanents: [] 13 | }; 14 | } 15 | 16 | if (typeof mask === "string") { 17 | let isPermanent = false; 18 | let parsedMaskString = ""; 19 | mask.split("").forEach(character => { 20 | if (!isPermanent && character === "\\") { 21 | isPermanent = true; 22 | } else { 23 | if (isPermanent || !defaultFormatChars[character]) { 24 | permanents.push(parsedMaskString.length); 25 | } 26 | parsedMaskString += character; 27 | isPermanent = false; 28 | } 29 | }); 30 | 31 | mask = parsedMaskString.split("").map((character, index) => { 32 | if (permanents.indexOf(index) === -1) { 33 | return defaultFormatChars[character]; 34 | } 35 | return character; 36 | }); 37 | } else { 38 | mask.forEach((character, index) => { 39 | if (typeof character === "string") { 40 | permanents.push(index); 41 | } 42 | }); 43 | } 44 | 45 | if (maskPlaceholder) { 46 | if (maskPlaceholder.length === 1) { 47 | maskPlaceholder = mask.map((character, index) => { 48 | if (permanents.indexOf(index) !== -1) { 49 | return character; 50 | } 51 | return maskPlaceholder; 52 | }); 53 | } else { 54 | maskPlaceholder = maskPlaceholder.split(""); 55 | } 56 | 57 | permanents.forEach(position => { 58 | maskPlaceholder[position] = mask[position]; 59 | }); 60 | 61 | maskPlaceholder = maskPlaceholder.join(""); 62 | } 63 | 64 | const prefix = permanents 65 | .filter((position, index) => position === index) 66 | .map(position => mask[position]) 67 | .join(""); 68 | 69 | let lastEditablePosition = mask.length - 1; 70 | while (permanents.indexOf(lastEditablePosition) !== -1) { 71 | lastEditablePosition--; 72 | } 73 | 74 | return { 75 | maskPlaceholder, 76 | prefix, 77 | mask, 78 | lastEditablePosition, 79 | permanents 80 | }; 81 | } 82 | -------------------------------------------------------------------------------- /src/validate-props.js: -------------------------------------------------------------------------------- 1 | import invariant from "invariant"; 2 | import warning from "warning"; 3 | 4 | import { CONTROLLED_PROPS } from "./constants"; 5 | 6 | export function validateMaxLength(props) { 7 | warning( 8 | !props.maxLength || !props.mask, 9 | "react-input-mask: maxLength property shouldn't be passed to the masked input. It breaks masking and unnecessary because length is limited by the mask length." 10 | ); 11 | } 12 | 13 | export function validateMaskPlaceholder(props) { 14 | const { mask, maskPlaceholder } = props; 15 | 16 | invariant( 17 | !mask || 18 | !maskPlaceholder || 19 | maskPlaceholder.length === 1 || 20 | maskPlaceholder.length === mask.length, 21 | "react-input-mask: maskPlaceholder should either be a single character or have the same length as the mask:\n" + 22 | `mask: ${mask}\n` + 23 | `maskPlaceholder: ${maskPlaceholder}` 24 | ); 25 | } 26 | 27 | export function validateChildren(props, inputElement) { 28 | const conflictProps = CONTROLLED_PROPS.filter( 29 | propId => 30 | inputElement.props[propId] != null && 31 | inputElement.props[propId] !== props[propId] 32 | ); 33 | 34 | invariant( 35 | !conflictProps.length, 36 | `react-input-mask: the following props should be passed to the InputMask component, not to children: ${conflictProps.join( 37 | "," 38 | )}` 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /tests/build/build.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-dynamic-require */ 2 | /* global describe, it */ 3 | 4 | import path from "path"; 5 | import React from "react"; 6 | import ReactDOMServer from "react-dom/server"; 7 | import { expect } from "chai"; // eslint-disable-line import/no-extraneous-dependencies 8 | 9 | const rootDir = path.resolve(__dirname, "../.."); 10 | 11 | describe("CommonJS build", () => { 12 | const libPath = path.resolve( 13 | rootDir, 14 | "lib/react-input-mask.production.min.js" 15 | ); 16 | const InputElement = require(libPath); 17 | 18 | it("should return a string", () => { 19 | const result = ReactDOMServer.renderToString( 20 | 21 | ); 22 | expect(typeof result).to.equal("string"); 23 | }); 24 | }); 25 | 26 | describe("UMD build", () => { 27 | const libPath = path.resolve(rootDir, "dist/react-input-mask.min.js"); 28 | const InputElement = require(libPath); 29 | 30 | it("should return a string", () => { 31 | const result = ReactDOMServer.renderToString( 32 | 33 | ); 34 | expect(typeof result).to.equal("string"); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /tests/input/input.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, afterEach */ 2 | 3 | import React from "react"; 4 | import ReactDOM from "react-dom"; 5 | import TestUtils from "react-dom/test-utils"; 6 | import { expect } from "chai"; // eslint-disable-line import/no-extraneous-dependencies 7 | import * as deferUtils from "../../src/utils/defer"; 8 | import Input from "../../src"; 9 | import { getInputSelection } from "../../src/utils/input"; 10 | import { isDOMElement } from "../../src/utils/helpers"; 11 | 12 | document.body.innerHTML = '
'; 13 | const container = document.getElementById("container"); 14 | 15 | async function delay(duration) { 16 | await new Promise(resolve => setTimeout(resolve, duration)); 17 | } 18 | 19 | async function defer() { 20 | await new Promise(resolve => deferUtils.defer(resolve)); 21 | } 22 | 23 | async function setSelection(input, start, length) { 24 | input.setSelectionRange(start, start + length); 25 | await defer(); 26 | } 27 | 28 | async function setCursorPosition(input, start) { 29 | await setSelection(input, start, 0); 30 | } 31 | 32 | async function waitForPendingSelection() { 33 | await defer(); 34 | } 35 | 36 | function getInputDOMNode(input) { 37 | if (!isDOMElement(input)) { 38 | input = ReactDOM.findDOMNode(input); 39 | } 40 | 41 | if (input.nodeName !== "INPUT") { 42 | input = input.querySelector("input"); 43 | } 44 | 45 | if (!input) { 46 | throw new Error("inputComponent doesn't contain input node"); 47 | } 48 | 49 | return input; 50 | } 51 | 52 | function createInput(component) { 53 | const originalRef = component.ref; 54 | let { props } = component; 55 | let input; 56 | component = React.cloneElement(component, { 57 | ref: ref => { 58 | input = ref; 59 | 60 | if (typeof originalRef === "function") { 61 | originalRef(ref); 62 | } else if (originalRef !== null && typeof originalRef === "object") { 63 | originalRef.current = ref; 64 | } 65 | } 66 | }); 67 | 68 | function setProps(newProps) { 69 | props = { 70 | ...props, 71 | ...newProps 72 | }; 73 | 74 | ReactDOM.render(React.createElement(Input, props), container); 75 | } 76 | 77 | ReactDOM.render(component, container); 78 | 79 | return { input, setProps }; 80 | } 81 | 82 | async function simulateFocus(input) { 83 | input.focus(); 84 | TestUtils.Simulate.focus(input); 85 | await defer(); 86 | } 87 | 88 | async function simulateBlur(input) { 89 | input.blur(); 90 | TestUtils.Simulate.blur(input); 91 | } 92 | 93 | async function simulateInput(input, string) { 94 | const selection = getInputSelection(input); 95 | const { value } = input; 96 | const valueBefore = value.slice(0, selection.start); 97 | const valueAfter = value.slice(selection.end); 98 | 99 | input.value = valueBefore + string + valueAfter; 100 | 101 | setCursorPosition(input, selection.start + string.length); 102 | 103 | TestUtils.Simulate.change(input); 104 | } 105 | 106 | async function simulateInputPaste(input, string) { 107 | TestUtils.Simulate.paste(input); 108 | await simulateInput(input, string); 109 | } 110 | 111 | async function simulateBackspacePress(input) { 112 | const selection = getInputSelection(input); 113 | const { value } = input; 114 | 115 | if (selection.length) { 116 | input.value = value.slice(0, selection.start) + value.slice(selection.end); 117 | setSelection(input, selection.start, 0); 118 | } else if (selection.start) { 119 | input.value = 120 | value.slice(0, selection.start - 1) + value.slice(selection.end); 121 | setSelection(input, selection.start - 1, 0); 122 | } 123 | 124 | TestUtils.Simulate.change(input); 125 | } 126 | 127 | async function simulateDeletePress(input) { 128 | const selection = getInputSelection(input); 129 | const removedLength = selection.end - selection.start || 1; 130 | const { value } = input; 131 | const valueBefore = value.slice(0, selection.start); 132 | const valueAfter = value.slice(selection.start + removedLength); 133 | 134 | input.value = valueBefore + valueAfter; 135 | 136 | setCursorPosition(input, selection.start); 137 | 138 | TestUtils.Simulate.change(input); 139 | } 140 | 141 | // eslint-disable-next-line react/prefer-stateless-function 142 | class ClassInputComponent extends React.Component { 143 | render() { 144 | return ( 145 |
146 | 147 |
148 | ); 149 | } 150 | } 151 | 152 | const FunctionalInputComponent = React.forwardRef((props, ref) => { 153 | return ( 154 |
155 |
156 | 157 |
158 |
159 | ); 160 | }); 161 | 162 | describe("react-input-mask", () => { 163 | afterEach(() => { 164 | ReactDOM.unmountComponentAtNode(container); 165 | }); 166 | 167 | it("should format value on mount", async () => { 168 | const { input } = createInput( 169 | 170 | ); 171 | expect(input.value).to.equal("+7 (495) 315 64 54"); 172 | }); 173 | 174 | it("should format value with invalid characters on mount", async () => { 175 | const { input } = createInput( 176 | 177 | ); 178 | expect(input.value).to.equal("+7 (4b6) 454 __ __"); 179 | }); 180 | 181 | it("should handle array mask", async () => { 182 | const letter = /[АВЕКМНОРСТУХ]/i; 183 | const digit = /[0-9]/; 184 | const mask = [letter, digit, digit, digit, letter, letter]; 185 | const { input } = createInput( 186 | 187 | ); 188 | expect(input.value).to.equal("А784КТ"); 189 | 190 | await simulateFocus(input); 191 | await simulateBackspacePress(input); 192 | expect(input.value).to.equal("А784К_"); 193 | 194 | await simulateInput(input, "Б"); 195 | expect(getInputSelection(input).start).to.equal(5); 196 | expect(getInputSelection(input).end).to.equal(5); 197 | 198 | await simulateInput(input, "Х"); 199 | expect(getInputSelection(input).start).to.equal(6); 200 | expect(getInputSelection(input).end).to.equal(6); 201 | }); 202 | 203 | it("should handle full length maskPlaceholder", async () => { 204 | const { input } = createInput( 205 | 206 | ); 207 | expect(input.value).to.equal("12/mm/yyyy"); 208 | 209 | await simulateFocus(input); 210 | expect(getInputSelection(input).start).to.equal(3); 211 | expect(getInputSelection(input).end).to.equal(3); 212 | 213 | await simulateBackspacePress(input); 214 | expect(input.value).to.equal("1d/mm/yyyy"); 215 | 216 | await simulateInput(input, "234"); 217 | expect(input.value).to.equal("12/34/yyyy"); 218 | expect(getInputSelection(input).start).to.equal(6); 219 | expect(getInputSelection(input).end).to.equal(6); 220 | 221 | await setCursorPosition(input, 8); 222 | await simulateInput(input, "7"); 223 | expect(input.value).to.equal("12/34/yy7y"); 224 | }); 225 | 226 | it("should show placeholder on focus", async () => { 227 | const { input } = createInput(); 228 | expect(input.value).to.equal(""); 229 | 230 | await simulateFocus(input); 231 | expect(input.value).to.equal("+7 (___) ___ __ __"); 232 | }); 233 | 234 | it("should clear input on blur", async () => { 235 | const { input } = createInput(); 236 | await simulateFocus(input); 237 | expect(input.value).to.equal("+7 (___) ___ __ __"); 238 | 239 | await simulateBlur(input); 240 | expect(input.value).to.equal(""); 241 | 242 | await simulateFocus(input); 243 | await simulateInput(input, "1"); 244 | expect(input.value).to.equal("+7 (1__) ___ __ __"); 245 | 246 | await simulateBlur(input); 247 | expect(input.value).to.equal("+7 (1__) ___ __ __"); 248 | }); 249 | 250 | it("should handle escaped characters in mask", async () => { 251 | const { input } = createInput( 252 | 253 | ); 254 | await simulateFocus(input); 255 | 256 | input.value = "+49 12 9"; 257 | setSelection(input, 8, 0); 258 | TestUtils.Simulate.change(input); 259 | expect(input.value).to.equal("+49 12 99"); 260 | 261 | await setCursorPosition(input, 7); 262 | 263 | await simulateInput(input, "1"); 264 | TestUtils.Simulate.change(input); 265 | expect(input.value).to.equal("+49 12 199 "); 266 | expect(getInputSelection(input).start).to.equal(9); 267 | expect(getInputSelection(input).end).to.equal(9); 268 | 269 | await setCursorPosition(input, 8); 270 | 271 | await simulateInput(input, "9"); 272 | TestUtils.Simulate.change(input); 273 | expect(input.value).to.equal("+49 12 199 "); 274 | expect(getInputSelection(input).start).to.equal(9); 275 | expect(getInputSelection(input).end).to.equal(9); 276 | }); 277 | 278 | it("should handle alwaysShowMask", async () => { 279 | const { input, setProps } = createInput( 280 | 281 | ); 282 | expect(input.value).to.equal("+7 (___) ___ __ __"); 283 | 284 | await simulateFocus(input); 285 | expect(input.value).to.equal("+7 (___) ___ __ __"); 286 | 287 | await simulateBlur(input); 288 | expect(input.value).to.equal("+7 (___) ___ __ __"); 289 | 290 | setProps({ alwaysShowMask: false }); 291 | expect(input.value).to.equal(""); 292 | 293 | setProps({ alwaysShowMask: true }); 294 | expect(input.value).to.equal("+7 (___) ___ __ __"); 295 | }); 296 | 297 | it("should adjust cursor position on focus", async () => { 298 | const { input, setProps } = createInput( 299 | 300 | ); 301 | await simulateFocus(input); 302 | 303 | expect(getInputSelection(input).start).to.equal(4); 304 | expect(getInputSelection(input).end).to.equal(4); 305 | 306 | await simulateBlur(input); 307 | 308 | setProps({ value: "+7 (___) ___ _1 __" }); 309 | await setCursorPosition(input, 2); 310 | await simulateFocus(input); 311 | expect(getInputSelection(input).start).to.equal(16); 312 | expect(getInputSelection(input).end).to.equal(16); 313 | 314 | await simulateBlur(input); 315 | 316 | setProps({ value: "+7 (___) ___ _1 _1" }); 317 | await setCursorPosition(input, 2); 318 | await simulateFocus(input); 319 | expect(getInputSelection(input).start).to.equal(2); 320 | expect(getInputSelection(input).end).to.equal(2); 321 | 322 | await simulateBlur(input); 323 | 324 | setProps({ 325 | value: "+7 (123)", 326 | mask: "+7 (999)", 327 | maskPlaceholder: null 328 | }); 329 | await setCursorPosition(input, 2); 330 | await simulateFocus(input); 331 | expect(getInputSelection(input).start).to.equal(2); 332 | expect(getInputSelection(input).end).to.equal(2); 333 | }); 334 | 335 | it("should adjust cursor position on focus on input with autoFocus", async () => { 336 | const { input, setProps } = createInput( 337 | 338 | ); 339 | expect(getInputSelection(input).start).to.equal(4); 340 | expect(getInputSelection(input).end).to.equal(4); 341 | 342 | await simulateBlur(input); 343 | 344 | setProps({ value: "+7 (___) ___ _1 __" }); 345 | await setCursorPosition(input, 2); 346 | await simulateFocus(input); 347 | expect(getInputSelection(input).start).to.equal(16); 348 | expect(getInputSelection(input).end).to.equal(16); 349 | 350 | await simulateBlur(input); 351 | 352 | setProps({ value: "+7 (___) ___ _1 _1" }); 353 | await setCursorPosition(input, 2); 354 | await simulateFocus(input); 355 | expect(getInputSelection(input).start).to.equal(2); 356 | expect(getInputSelection(input).end).to.equal(2); 357 | }); 358 | 359 | it("should handle changes on input with autoFocus", async () => { 360 | const { input } = createInput( 361 | 362 | ); 363 | await simulateInput(input, "222 222 22 22"); 364 | 365 | await defer(); 366 | setSelection(input, 5, 0); 367 | await delay(100); 368 | await simulateInput(input, "3"); 369 | expect(input.value).to.equal("+7 (232) 222 22 22"); 370 | }); 371 | 372 | it("should format value in onChange (with maskPlaceholder)", async () => { 373 | const { input } = createInput(); 374 | await simulateFocus(input); 375 | 376 | await setCursorPosition(input, 0); 377 | input.value = `a${input.value}`; 378 | setCursorPosition(input, 1); 379 | TestUtils.Simulate.change(input); 380 | expect(input.value).to.equal("a___ ____ ____ ____"); 381 | expect(getInputSelection(input).start).to.equal(1); 382 | expect(getInputSelection(input).end).to.equal(1); 383 | 384 | await setSelection(input, 0, 19); 385 | input.value = "a"; 386 | setCursorPosition(input, 1); 387 | TestUtils.Simulate.change(input); 388 | expect(input.value).to.equal("a___ ____ ____ ____"); 389 | expect(getInputSelection(input).start).to.equal(1); 390 | expect(getInputSelection(input).end).to.equal(1); 391 | 392 | input.value = "aaaaa___ ____ ____ ____"; 393 | setSelection(input, 1, 4); 394 | TestUtils.Simulate.change(input); 395 | expect(input.value).to.equal("aaaa a___ ____ ____"); 396 | expect(getInputSelection(input).start).to.equal(6); 397 | expect(getInputSelection(input).end).to.equal(6); 398 | 399 | await setCursorPosition(input, 4); 400 | input.value = "aaa a___ ____ ____"; 401 | setCursorPosition(input, 3); 402 | TestUtils.Simulate.change(input); 403 | expect(input.value).to.equal("aaa_ a___ ____ ____"); 404 | 405 | await setSelection(input, 3, 3); 406 | input.value = "aaaaaa___ ____ ____"; 407 | setCursorPosition(input, 6); 408 | TestUtils.Simulate.change(input); 409 | expect(input.value).to.equal("aaaa aa__ ____ ____"); 410 | 411 | await setSelection(input, 3, 3); 412 | input.value = "aaaaxa__ ____ ____"; 413 | setCursorPosition(input, 5); 414 | TestUtils.Simulate.change(input); 415 | expect(input.value).to.equal("aaaa xa__ ____ ____"); 416 | expect(getInputSelection(input).start).to.equal(6); 417 | expect(getInputSelection(input).end).to.equal(6); 418 | }); 419 | 420 | it("should format value in onChange (without maskPlaceholder)", async () => { 421 | const { input } = createInput( 422 | 423 | ); 424 | await simulateFocus(input); 425 | expect(input.value).to.equal(""); 426 | 427 | await setCursorPosition(input, 0); 428 | input.value = "aaa"; 429 | setCursorPosition(input, 3); 430 | TestUtils.Simulate.change(input); 431 | expect(input.value).to.equal("aaa"); 432 | expect(getInputSelection(input).start).to.equal(3); 433 | expect(getInputSelection(input).end).to.equal(3); 434 | 435 | input.value = "aaaaa"; 436 | setCursorPosition(input, 5); 437 | TestUtils.Simulate.change(input); 438 | expect(input.value).to.equal("aaaa a"); 439 | expect(getInputSelection(input).start).to.equal(6); 440 | expect(getInputSelection(input).end).to.equal(6); 441 | 442 | input.value = "aaaa afgh ijkl mnop"; 443 | setCursorPosition(input, 19); 444 | TestUtils.Simulate.change(input); 445 | expect(input.value).to.equal("aaaa afgh ijkl mnop"); 446 | expect(getInputSelection(input).start).to.equal(19); 447 | expect(getInputSelection(input).end).to.equal(19); 448 | 449 | input.value = "aaaa afgh ijkl mnopq"; 450 | setCursorPosition(input, 20); 451 | TestUtils.Simulate.change(input); 452 | expect(input.value).to.equal("aaaa afgh ijkl mnop"); 453 | expect(getInputSelection(input).start).to.equal(19); 454 | expect(getInputSelection(input).end).to.equal(19); 455 | }); 456 | 457 | it("should handle entered characters (with maskPlaceholder)", async () => { 458 | const { input } = createInput(); 459 | await simulateFocus(input); 460 | 461 | await setCursorPosition(input, 0); 462 | await simulateInput(input, "+"); 463 | expect(input.value).to.equal("+7 (___) ___ __ __"); 464 | 465 | await setCursorPosition(input, 0); 466 | await simulateInput(input, "7"); 467 | expect(input.value).to.equal("+7 (___) ___ __ __"); 468 | 469 | await setCursorPosition(input, 0); 470 | await simulateInput(input, "8"); 471 | expect(input.value).to.equal("+7 (8__) ___ __ __"); 472 | 473 | await setCursorPosition(input, 0); 474 | await simulateInput(input, "E"); 475 | expect(input.value).to.equal("+7 (E__) ___ __ __"); 476 | 477 | await simulateInput(input, "6"); 478 | expect(input.value).to.equal("+7 (E__) ___ __ __"); 479 | 480 | await simulateInput(input, "x"); 481 | expect(input.value).to.equal("+7 (Ex_) ___ __ __"); 482 | }); 483 | 484 | it("should handle entered characters (without maskPlaceholder)", async () => { 485 | const { input } = createInput( 486 | 491 | ); 492 | await simulateFocus(input); 493 | 494 | await setCursorPosition(input, 4); 495 | await simulateInput(input, "E"); 496 | expect(input.value).to.equal("+7 (111) 123 45 6"); 497 | 498 | await setSelection(input, 4, 3); 499 | await simulateInput(input, "0"); 500 | expect(input.value).to.equal("+7 (012) 345 6"); 501 | 502 | await setCursorPosition(input, 14); 503 | await simulateInput(input, "7"); 504 | await simulateInput(input, "8"); 505 | await simulateInput(input, "9"); 506 | await simulateInput(input, "4"); 507 | expect(input.value).to.equal("+7 (012) 345 67 89"); 508 | 509 | input.value = "+7 ("; 510 | setCursorPosition(input, 4); 511 | TestUtils.Simulate.change(input); 512 | await setCursorPosition(input, 0); 513 | await simulateInput(input, "+"); 514 | expect(input.value).to.equal("+7 ("); 515 | }); 516 | 517 | it("should adjust cursor position on input (with maskPlaceholder)", async () => { 518 | const { input } = createInput(); 519 | await simulateFocus(input); 520 | 521 | await setCursorPosition(input, 3); 522 | await simulateInput(input, "x"); 523 | expect(getInputSelection(input).start).to.equal(3); 524 | expect(getInputSelection(input).end).to.equal(3); 525 | 526 | await simulateInput(input, "1"); 527 | expect(getInputSelection(input).start).to.equal(4); 528 | expect(getInputSelection(input).end).to.equal(4); 529 | 530 | await setSelection(input, 0, 4); 531 | await simulateBackspacePress(input); 532 | await setCursorPosition(input, 2); 533 | await simulateInput(input, "x"); 534 | expect(getInputSelection(input).start).to.equal(2); 535 | expect(getInputSelection(input).end).to.equal(2); 536 | }); 537 | 538 | it("should handle single character removal with Backspace (with maskPlaceholder)", async () => { 539 | const { input } = createInput( 540 | 541 | ); 542 | await simulateFocus(input); 543 | 544 | await setCursorPosition(input, 10); 545 | await simulateBackspacePress(input); 546 | expect(input.value).to.equal("+7 (495) _15 64 54"); 547 | 548 | await simulateBackspacePress(input); 549 | expect(input.value).to.equal("+7 (49_) _15 64 54"); 550 | }); 551 | 552 | it("should handle single character removal with Backspace (without maskPlaceholder)", async () => { 553 | const { input } = createInput( 554 | 559 | ); 560 | await simulateFocus(input); 561 | 562 | await setCursorPosition(input, 10); 563 | await simulateBackspacePress(input); 564 | expect(input.value).to.equal("+7 (495) 156 45 4"); 565 | 566 | input.value = "+7 ("; 567 | setCursorPosition(input, 4); 568 | TestUtils.Simulate.change(input); 569 | expect(input.value).to.equal("+7 ("); 570 | 571 | input.value = "+7 "; 572 | setCursorPosition(input, 3); 573 | TestUtils.Simulate.change(input); 574 | expect(input.value).to.equal("+7 ("); 575 | }); 576 | 577 | it("should adjust cursor position on single character removal with Backspace (with maskPlaceholder)", async () => { 578 | const { input } = createInput( 579 | 580 | ); 581 | await simulateFocus(input); 582 | 583 | await setCursorPosition(input, 10); 584 | await simulateBackspacePress(input); 585 | expect(getInputSelection(input).start).to.equal(9); 586 | expect(getInputSelection(input).end).to.equal(9); 587 | 588 | await simulateBackspacePress(input); 589 | expect(getInputSelection(input).start).to.equal(6); 590 | expect(getInputSelection(input).end).to.equal(6); 591 | 592 | await setCursorPosition(input, 4); 593 | await simulateBackspacePress(input); 594 | expect(getInputSelection(input).start).to.equal(4); 595 | expect(getInputSelection(input).end).to.equal(4); 596 | }); 597 | 598 | it("should adjust cursor position on single character removal with Backspace (without maskPlaceholder)", async () => { 599 | const { input } = createInput( 600 | 605 | ); 606 | await simulateFocus(input); 607 | 608 | await setCursorPosition(input, 16); 609 | await simulateBackspacePress(input); 610 | expect(getInputSelection(input).start).to.equal(14); 611 | expect(getInputSelection(input).end).to.equal(14); 612 | }); 613 | 614 | it("should handle multiple characters removal with Backspace (with maskPlaceholder)", async () => { 615 | const { input } = createInput( 616 | 617 | ); 618 | await simulateFocus(input); 619 | 620 | await setSelection(input, 1, 9); 621 | await simulateBackspacePress(input); 622 | expect(input.value).to.equal("+7 (___) _15 64 54"); 623 | }); 624 | 625 | it("should handle multiple characters removal with Backspace (without maskPlaceholder)", async () => { 626 | const { input } = createInput( 627 | 632 | ); 633 | await simulateFocus(input); 634 | 635 | await setSelection(input, 1, 9); 636 | await simulateBackspacePress(input); 637 | expect(input.value).to.equal("+7 (156) 454 "); 638 | }); 639 | 640 | it("should adjust cursor position on multiple characters removal with Backspace (with maskPlaceholder)", async () => { 641 | const { input } = createInput( 642 | 643 | ); 644 | await simulateFocus(input); 645 | 646 | await setSelection(input, 1, 9); 647 | await simulateBackspacePress(input); 648 | expect(getInputSelection(input).start).to.equal(4); 649 | expect(getInputSelection(input).end).to.equal(4); 650 | }); 651 | 652 | it("should handle single character removal with Backspace on mask with escaped characters (without maskPlaceholder)", async () => { 653 | const { input } = createInput( 654 | 659 | ); 660 | await simulateFocus(input); 661 | 662 | await setCursorPosition(input, 10); 663 | await simulateBackspacePress(input); 664 | expect(input.value).to.equal("+49 12 39"); 665 | 666 | await setCursorPosition(input, 9); 667 | await simulateBackspacePress(input); 668 | expect(input.value).to.equal("+49 12 "); 669 | 670 | await simulateFocus(input); 671 | input.value = "+49 12 39"; 672 | TestUtils.Simulate.change(input); 673 | await setCursorPosition(input, 6); 674 | await simulateBackspacePress(input); 675 | expect(input.value).to.equal("+49 13 "); 676 | }); 677 | 678 | it("should adjust cursor position on single character removal with Backspace on mask with escaped characters (without maskPlaceholder)", async () => { 679 | const { input } = createInput( 680 | 685 | ); 686 | await simulateFocus(input); 687 | 688 | await setCursorPosition(input, 10); 689 | await simulateBackspacePress(input); 690 | expect(getInputSelection(input).start).to.equal(9); 691 | expect(getInputSelection(input).end).to.equal(9); 692 | 693 | await setCursorPosition(input, 9); 694 | await simulateBackspacePress(input); 695 | expect(getInputSelection(input).start).to.equal(7); 696 | expect(getInputSelection(input).end).to.equal(7); 697 | 698 | await simulateFocus(input); 699 | input.value = "+49 12 39"; 700 | TestUtils.Simulate.change(input); 701 | await setCursorPosition(input, 6); 702 | await simulateBackspacePress(input); 703 | expect(getInputSelection(input).start).to.equal(5); 704 | expect(getInputSelection(input).end).to.equal(5); 705 | }); 706 | 707 | it("should handle multiple characters removal with Backspace on mask with escaped characters (without maskPlaceholder)", async () => { 708 | const { input } = createInput( 709 | 714 | ); 715 | await simulateFocus(input); 716 | 717 | await setSelection(input, 4, 2); 718 | await simulateBackspacePress(input); 719 | expect(input.value).to.equal("+49 34 "); 720 | 721 | await setSelection(input, 0, 7); 722 | input.value = "+49 12 394 5"; 723 | TestUtils.Simulate.change(input); 724 | await setSelection(input, 4, 2); 725 | await simulateBackspacePress(input); 726 | expect(input.value).to.equal("+49 34 59"); 727 | }); 728 | 729 | it("should adjust cursor position on multiple characters removal with Backspace on mask with escaped characters (without maskPlaceholder)", async () => { 730 | const { input } = createInput( 731 | 736 | ); 737 | await simulateFocus(input); 738 | 739 | await setSelection(input, 4, 2); 740 | await simulateBackspacePress(input); 741 | expect(getInputSelection(input).start).to.equal(4); 742 | expect(getInputSelection(input).end).to.equal(4); 743 | 744 | input.value = "+49 12 394 5"; 745 | TestUtils.Simulate.change(input); 746 | await setSelection(input, 4, 2); 747 | await simulateBackspacePress(input); 748 | expect(getInputSelection(input).start).to.equal(4); 749 | expect(getInputSelection(input).end).to.equal(4); 750 | }); 751 | 752 | it("should handle single character removal with Delete (with maskPlaceholder)", async () => { 753 | const { input } = createInput( 754 | 755 | ); 756 | await simulateFocus(input); 757 | 758 | await setCursorPosition(input, 0); 759 | await simulateDeletePress(input); 760 | expect(input.value).to.equal("+7 (_95) 315 64 54"); 761 | 762 | await setCursorPosition(input, 7); 763 | await simulateDeletePress(input); 764 | expect(input.value).to.equal("+7 (_95) _15 64 54"); 765 | 766 | await setCursorPosition(input, 11); 767 | await simulateDeletePress(input); 768 | expect(input.value).to.equal("+7 (_95) _1_ 64 54"); 769 | }); 770 | 771 | it("should adjust cursor position on single character removal with Delete (with maskPlaceholder)", async () => { 772 | const { input } = createInput( 773 | 774 | ); 775 | await simulateFocus(input); 776 | 777 | await setCursorPosition(input, 0); 778 | await simulateDeletePress(input); 779 | expect(getInputSelection(input).start).to.equal(4); 780 | expect(getInputSelection(input).end).to.equal(4); 781 | 782 | await setCursorPosition(input, 7); 783 | await simulateDeletePress(input); 784 | expect(getInputSelection(input).start).to.equal(9); 785 | expect(getInputSelection(input).end).to.equal(9); 786 | 787 | await setCursorPosition(input, 11); 788 | await simulateDeletePress(input); 789 | expect(getInputSelection(input).start).to.equal(11); 790 | expect(getInputSelection(input).end).to.equal(11); 791 | }); 792 | 793 | it("should handle multiple characters removal with Delete (with maskPlaceholder)", async () => { 794 | const { input } = createInput( 795 | 796 | ); 797 | await simulateFocus(input); 798 | 799 | await setSelection(input, 1, 9); 800 | await simulateDeletePress(input); 801 | expect(input.value).to.equal("+7 (___) _15 64 54"); 802 | }); 803 | 804 | it("should handle single character removal with Delete on mask with escaped characters (without maskPlaceholder)", async () => { 805 | const { input } = createInput( 806 | 811 | ); 812 | await simulateFocus(input); 813 | 814 | await setCursorPosition(input, 9); 815 | await simulateDeletePress(input); 816 | expect(input.value).to.equal("+49 12 39"); 817 | 818 | await setCursorPosition(input, 7); 819 | await simulateDeletePress(input); 820 | expect(input.value).to.equal("+49 12 "); 821 | 822 | await simulateFocus(input); 823 | input.value = "+49 12 39"; 824 | TestUtils.Simulate.change(input); 825 | await setCursorPosition(input, 5); 826 | await simulateDeletePress(input); 827 | expect(input.value).to.equal("+49 13 "); 828 | }); 829 | 830 | it("should adjust cursor position on single character removal with Delete on mask with escaped characters (without maskPlaceholder)", async () => { 831 | const { input } = createInput( 832 | 837 | ); 838 | await simulateFocus(input); 839 | 840 | await setCursorPosition(input, 9); 841 | await simulateDeletePress(input); 842 | expect(getInputSelection(input).start).to.equal(9); 843 | expect(getInputSelection(input).end).to.equal(9); 844 | 845 | await setCursorPosition(input, 7); 846 | await simulateDeletePress(input); 847 | expect(getInputSelection(input).start).to.equal(7); 848 | expect(getInputSelection(input).end).to.equal(7); 849 | 850 | await simulateFocus(input); 851 | input.value = "+49 12 39"; 852 | TestUtils.Simulate.change(input); 853 | await setCursorPosition(input, 5); 854 | await simulateDeletePress(input); 855 | expect(getInputSelection(input).start).to.equal(5); 856 | expect(getInputSelection(input).end).to.equal(5); 857 | }); 858 | 859 | it("should handle multiple characters removal with Delete on mask with escaped characters (without maskPlaceholder)", async () => { 860 | const { input } = createInput( 861 | 866 | ); 867 | await simulateFocus(input); 868 | 869 | await setSelection(input, 4, 2); 870 | await simulateDeletePress(input); 871 | expect(input.value).to.equal("+49 34 "); 872 | 873 | await setSelection(input, 0, 7); 874 | input.value = "+49 12 394 5"; 875 | TestUtils.Simulate.change(input); 876 | await setSelection(input, 4, 2); 877 | await simulateDeletePress(input); 878 | expect(input.value).to.equal("+49 34 59"); 879 | }); 880 | 881 | it("should adjust cursor position on multiple characters removal with Delete on mask with escaped characters (without maskPlaceholder)", async () => { 882 | const { input } = createInput( 883 | 888 | ); 889 | await simulateFocus(input); 890 | 891 | await setSelection(input, 4, 2); 892 | await simulateDeletePress(input); 893 | expect(getInputSelection(input).start).to.equal(4); 894 | expect(getInputSelection(input).end).to.equal(4); 895 | 896 | input.value = "+49 12 394 5"; 897 | TestUtils.Simulate.change(input); 898 | await setSelection(input, 4, 2); 899 | await simulateDeletePress(input); 900 | expect(getInputSelection(input).start).to.equal(4); 901 | expect(getInputSelection(input).end).to.equal(4); 902 | }); 903 | 904 | it("should handle mask change", async () => { 905 | const { input, setProps } = createInput( 906 | 907 | ); 908 | setProps({ mask: "9999-999999-99999" }); 909 | expect(input.value).to.equal("3478-122691-7____"); 910 | 911 | setProps({ mask: "9-9-9-9" }); 912 | expect(input.value).to.equal("3-4-7-8"); 913 | 914 | setProps({ mask: null }); 915 | expect(input.value).to.equal("3-4-7-8"); 916 | 917 | input.value = "0-1-2-3"; 918 | 919 | setProps({ mask: "9999" }); 920 | expect(input.value).to.equal("0123"); 921 | }); 922 | 923 | it("should handle mask change with on controlled input", async () => { 924 | const { input, setProps } = createInput( 925 | 926 | ); 927 | setProps({ 928 | onChange: () => { 929 | setProps({ 930 | mask: "9999-999999-99999", 931 | value: "3478-1226-917_-____" 932 | }); 933 | } 934 | }); 935 | 936 | await simulateFocus(input); 937 | 938 | expect(input.value).to.equal("3878-1226-917_-____"); 939 | 940 | await setCursorPosition(input, 1); 941 | await simulateInput(input, "4"); 942 | TestUtils.Simulate.change(input); 943 | 944 | expect(input.value).to.equal("3478-122691-7____"); 945 | }); 946 | 947 | it("should handle string paste (with maskPlaceholder)", async () => { 948 | const { input } = createInput( 949 | 950 | ); 951 | await simulateFocus(input); 952 | 953 | await setSelection(input, 3, 15); 954 | simulateInputPaste(input, "34781226917"); 955 | expect(input.value).to.equal("___3-4781-2269-17_3"); 956 | 957 | await setCursorPosition(input, 3); 958 | simulateInputPaste(input, "3-__81-2_6917"); 959 | expect(input.value).to.equal("___3-__81-2_69-17_3"); 960 | 961 | await setSelection(input, 0, 3); 962 | simulateInputPaste(input, " 333"); 963 | expect(input.value).to.equal("3333-__81-2_69-17_3"); 964 | }); 965 | 966 | it("should adjust cursor position on string paste (with maskPlaceholder)", async () => { 967 | const { input } = createInput( 968 | 969 | ); 970 | await simulateFocus(input); 971 | 972 | await setSelection(input, 3, 15); 973 | simulateInputPaste(input, "478122691"); 974 | expect(getInputSelection(input).start).to.equal(15); 975 | expect(getInputSelection(input).end).to.equal(15); 976 | 977 | await setCursorPosition(input, 3); 978 | simulateInputPaste(input, "3-__81-2_6917"); 979 | expect(getInputSelection(input).start).to.equal(17); 980 | expect(getInputSelection(input).end).to.equal(17); 981 | }); 982 | 983 | it("should handle string paste (without maskPlaceholder)", async () => { 984 | const { input, setProps } = createInput( 985 | 990 | ); 991 | await simulateFocus(input); 992 | 993 | await setSelection(input, 0, 19); 994 | simulateInputPaste(input, "34781226917"); 995 | expect(input.value).to.equal("3478-1226-917"); 996 | 997 | await setCursorPosition(input, 1); 998 | simulateInputPaste(input, "12345"); 999 | expect(input.value).to.equal("3123-4547-8122-6917"); 1000 | 1001 | await setCursorPosition(input, 1); 1002 | simulateInputPaste(input, "4321"); 1003 | expect(input.value).to.equal("3432-1547-8122-6917"); 1004 | 1005 | setProps({ 1006 | value: "", 1007 | onChange: event => { 1008 | setProps({ 1009 | value: event.target.value 1010 | }); 1011 | } 1012 | }); 1013 | 1014 | await waitForPendingSelection(); 1015 | 1016 | simulateInputPaste(input, "123"); 1017 | expect(input.value).to.equal("123"); 1018 | }); 1019 | 1020 | it("should handle string paste at position of permanent character (with maskPlaceholder)", async () => { 1021 | const { input } = createInput( 1022 | 1023 | ); 1024 | await simulateFocus(input); 1025 | 1026 | simulateInputPaste(input, "1111 1111 1111"); 1027 | expect(input.value).to.equal("1111-1111-1111"); 1028 | }); 1029 | 1030 | it("should keep placeholder on rerender on empty input with alwaysShowMask", async () => { 1031 | const { input, setProps } = createInput( 1032 | 1033 | ); 1034 | setProps({ value: "" }); 1035 | 1036 | expect(input.value).to.equal("__-__"); 1037 | }); 1038 | 1039 | it("should show empty value when input switches from uncontrolled to controlled", async () => { 1040 | const { input, setProps } = createInput( 1041 | 1042 | ); 1043 | setProps({ value: "+7 (___) ___ __ __" }); 1044 | expect(input.value).to.equal("+7 (___) ___ __ __"); 1045 | }); 1046 | 1047 | it("shouldn't affect value if mask is empty", async () => { 1048 | const { input, setProps } = createInput(); 1049 | expect(input.value).to.equal("12345"); 1050 | 1051 | setProps({ 1052 | value: "54321" 1053 | }); 1054 | expect(input.value).to.equal("54321"); 1055 | }); 1056 | 1057 | it("should show next permanent character when maskPlaceholder is null", async () => { 1058 | const { input } = createInput( 1059 | 1060 | ); 1061 | expect(input.value).to.equal("01/"); 1062 | }); 1063 | 1064 | it("should show all next consecutive permanent characters when maskPlaceholder is null", async () => { 1065 | const { input } = createInput( 1066 | 1067 | ); 1068 | expect(input.value).to.equal("01---"); 1069 | }); 1070 | 1071 | it("should show trailing permanent character when maskPlaceholder is null", async () => { 1072 | const { input } = createInput( 1073 | 1074 | ); 1075 | expect(input.value).to.equal("10%"); 1076 | }); 1077 | 1078 | it("should pass input DOM node to ref", async () => { 1079 | let inputRef; 1080 | const { input } = createInput( 1081 | { 1083 | inputRef = ref; 1084 | }} 1085 | /> 1086 | ); 1087 | expect(inputRef).to.equal(input); 1088 | }); 1089 | 1090 | it("should allow to modify value with beforeMaskedStateChange", async () => { 1091 | function beforeMaskedStateChange({ nextState }) { 1092 | const placeholder = "DD/MM/YYYY"; 1093 | const maskPlaceholder = "_"; 1094 | const value = nextState.value 1095 | .split("") 1096 | .map((char, i) => { 1097 | if (char === maskPlaceholder) { 1098 | return placeholder[i]; 1099 | } 1100 | return char; 1101 | }) 1102 | .join(""); 1103 | 1104 | return { 1105 | ...nextState, 1106 | value 1107 | }; 1108 | } 1109 | 1110 | const { input, setProps } = createInput( 1111 | 1116 | ); 1117 | expect(input.value).to.equal(""); 1118 | 1119 | setProps({ 1120 | onChange: event => { 1121 | setProps({ 1122 | value: event.target.value 1123 | }); 1124 | } 1125 | }); 1126 | 1127 | await simulateFocus(input); 1128 | 1129 | expect(input.value).to.equal("DD/MM/YYYY"); 1130 | 1131 | setProps({ value: "12345" }); 1132 | expect(input.value).to.equal("12/34/5YYY"); 1133 | 1134 | await setCursorPosition(input, 7); 1135 | 1136 | await simulateInput(input, "6"); 1137 | expect(input.value).to.equal("12/34/56YY"); 1138 | 1139 | setProps({ value: null }); 1140 | expect(input.value).to.equal("12/34/56YY"); 1141 | }); 1142 | 1143 | it("shouldn't modify value on entering non-allowed character", async () => { 1144 | const { input } = createInput(); 1145 | await simulateFocus(input); 1146 | 1147 | await setCursorPosition(input, 0); 1148 | await simulateInput(input, "a"); 1149 | 1150 | expect(input.value).to.equal("1234"); 1151 | expect(getInputSelection(input).start).to.equal(0); 1152 | expect(getInputSelection(input).end).to.equal(0); 1153 | 1154 | await setSelection(input, 0, 1); 1155 | await simulateInput(input, "a"); 1156 | 1157 | expect(input.value).to.equal("1234"); 1158 | 1159 | await setSelection(input, 1, 3); 1160 | await simulateInput(input, "a"); 1161 | 1162 | expect(input.value).to.equal("1234"); 1163 | }); 1164 | 1165 | it("should handle autofill", async () => { 1166 | const { input } = createInput( 1167 | 1168 | ); 1169 | await simulateFocus(input); 1170 | 1171 | input.value = "12345678"; 1172 | setCursorPosition(input, 8); 1173 | TestUtils.Simulate.change(input); 1174 | 1175 | expect(input.value).to.equal("1234-5678"); 1176 | }); 1177 | 1178 | it("should handle transition between masked and non-masked state", async () => { 1179 | const { input, setProps } = createInput(); 1180 | setProps({ 1181 | value: "", 1182 | onChange: event => { 1183 | setProps({ 1184 | value: event.target.value, 1185 | mask: event.target.value ? "+7 999 999 99 99" : null 1186 | }); 1187 | } 1188 | }); 1189 | 1190 | await simulateFocus(input); 1191 | 1192 | expect(getInputSelection(input).start).to.equal(0); 1193 | expect(getInputSelection(input).end).to.equal(0); 1194 | 1195 | await simulateInput(input, "1"); 1196 | expect(input.value).to.equal("+7 1__ ___ __ __"); 1197 | expect(getInputSelection(input).start).to.equal(4); 1198 | expect(getInputSelection(input).end).to.equal(4); 1199 | 1200 | await simulateBackspacePress(input); 1201 | await simulateBlur(input); 1202 | 1203 | expect(input.value).to.equal(""); 1204 | 1205 | await simulateFocus(input); 1206 | 1207 | expect(getInputSelection(input).start).to.equal(0); 1208 | expect(getInputSelection(input).end).to.equal(0); 1209 | 1210 | await simulateInput(input, "1"); 1211 | expect(input.value).to.equal("+7 1__ ___ __ __"); 1212 | expect(getInputSelection(input).start).to.equal(4); 1213 | expect(getInputSelection(input).end).to.equal(4); 1214 | }); 1215 | 1216 | it("should handle regular component as children", async () => { 1217 | let { input } = createInput( 1218 | 1219 | 1220 | 1221 | ); 1222 | input = getInputDOMNode(input); 1223 | 1224 | await simulateFocus(input); 1225 | 1226 | expect(getInputSelection(input).start).to.equal(4); 1227 | expect(getInputSelection(input).end).to.equal(4); 1228 | 1229 | await simulateInput(input, "1"); 1230 | expect(input.value).to.equal("+7 (1__) ___ __ __"); 1231 | expect(getInputSelection(input).start).to.equal(5); 1232 | expect(getInputSelection(input).end).to.equal(5); 1233 | }); 1234 | 1235 | it("should handle functional component as children", async () => { 1236 | let { input } = createInput( 1237 | 1238 | 1239 | 1240 | ); 1241 | input = getInputDOMNode(input); 1242 | 1243 | await simulateFocus(input); 1244 | 1245 | expect(getInputSelection(input).start).to.equal(4); 1246 | expect(getInputSelection(input).end).to.equal(4); 1247 | 1248 | await simulateInput(input, "1"); 1249 | expect(input.value).to.equal("+7 (1__) ___ __ __"); 1250 | expect(getInputSelection(input).start).to.equal(5); 1251 | expect(getInputSelection(input).end).to.equal(5); 1252 | }); 1253 | 1254 | it("should handle children change", async () => { 1255 | let { input, setProps } = createInput(); 1256 | function handleRef(ref) { 1257 | input = ref; 1258 | } 1259 | 1260 | setProps({ 1261 | value: "", 1262 | mask: "+7 (999) 999 99 99", 1263 | onChange: event => { 1264 | setProps({ 1265 | value: event.target.value 1266 | }); 1267 | }, 1268 | children: 1269 | }); 1270 | 1271 | input = getInputDOMNode(input); 1272 | 1273 | await simulateFocus(input); 1274 | 1275 | expect(getInputSelection(input).start).to.equal(4); 1276 | expect(getInputSelection(input).end).to.equal(4); 1277 | 1278 | await simulateInput(input, "1"); 1279 | expect(input.value).to.equal("+7 (1__) ___ __ __"); 1280 | expect(getInputSelection(input).start).to.equal(5); 1281 | expect(getInputSelection(input).end).to.equal(5); 1282 | 1283 | setProps({ 1284 | value: "22", 1285 | mask: "+7 (999) 999 99 99", 1286 | onChange: event => { 1287 | setProps({ 1288 | value: event.target.value 1289 | }); 1290 | }, 1291 | children: 1292 | }); 1293 | input = getInputDOMNode(input); 1294 | 1295 | expect(input.value).to.equal("+7 (22_) ___ __ __"); 1296 | 1297 | setProps({ 1298 | value: "22", 1299 | mask: "+7 (999) 999 99 99", 1300 | onChange: event => { 1301 | setProps({ 1302 | value: event.target.value 1303 | }); 1304 | }, 1305 | children: null, 1306 | ref: handleRef 1307 | }); 1308 | input = getInputDOMNode(input); 1309 | 1310 | expect(input.value).to.equal("+7 (22_) ___ __ __"); 1311 | }); 1312 | 1313 | it("should handle change event without focus", async () => { 1314 | const { input } = createInput( 1315 | 1316 | ); 1317 | input.value = "+71234567890"; 1318 | TestUtils.Simulate.change(input); 1319 | expect(input.value).to.equal("+7 (123) 456 78 90"); 1320 | }); 1321 | 1322 | it("shouldn't move cursor on delayed value change", async () => { 1323 | const { input, setProps } = createInput( 1324 | 1325 | ); 1326 | setProps({ 1327 | value: "+7 (9", 1328 | onChange: event => { 1329 | setProps({ 1330 | value: event.target.value 1331 | }); 1332 | } 1333 | }); 1334 | 1335 | await simulateFocus(input); 1336 | 1337 | expect(getInputSelection(input).start).to.equal(5); 1338 | expect(getInputSelection(input).end).to.equal(5); 1339 | 1340 | await delay(100); 1341 | setProps({ 1342 | value: "+7 (99" 1343 | }); 1344 | 1345 | expect(getInputSelection(input).start).to.equal(5); 1346 | expect(getInputSelection(input).end).to.equal(5); 1347 | }); 1348 | }); 1349 | -------------------------------------------------------------------------------- /tests/server-render/server-render.js: -------------------------------------------------------------------------------- 1 | /* global describe, it */ 2 | 3 | import React from "react"; 4 | import ReactDOMServer from "react-dom/server"; 5 | import { expect } from "chai"; // eslint-disable-line import/no-extraneous-dependencies 6 | import InputElement from "../../index"; 7 | 8 | describe("Test prerender", () => { 9 | it("should return a string", () => { 10 | const result = ReactDOMServer.renderToString( 11 | 12 | ); 13 | expect(typeof result).to.equal("string"); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 3 | 4 | const srcDir = path.resolve(__dirname, "./dev"); 5 | 6 | module.exports = { 7 | devtool: "cheap-module-source-map", 8 | context: srcDir, 9 | performance: { 10 | hints: false 11 | }, 12 | entry: "./index.js", 13 | output: { 14 | filename: "[name].js" 15 | }, 16 | resolve: { 17 | modules: ["node_modules", "."] 18 | }, 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.js$/, 23 | use: { 24 | loader: "babel-loader", 25 | options: { 26 | presets: [ 27 | ["@babel/preset-env", { targets: "Chrome > 70" }], 28 | "@babel/preset-react" 29 | ] 30 | } 31 | }, 32 | exclude: /node_modules/ 33 | } 34 | ] 35 | }, 36 | devServer: { 37 | host: "0.0.0.0", 38 | port: 9000, 39 | disableHostCheck: true 40 | }, 41 | plugins: [ 42 | new HtmlWebpackPlugin({ 43 | template: "index.html" 44 | }) 45 | ] 46 | }; 47 | --------------------------------------------------------------------------------