├── .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 | [](https://travis-ci.org/sanniassin/react-input-mask) [](https://www.npmjs.com/package/react-input-mask) [](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 |
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 |
--------------------------------------------------------------------------------