├── .babelrc ├── .editorconfig ├── .eslintrc.json ├── .gitignore ├── .storybook ├── addons.js └── config.js ├── .travis.yml ├── LICENSE ├── README.md ├── jest.config.js ├── package.json ├── setupTests.js ├── src ├── currency-mask │ ├── CurrencyMask.jsx │ ├── CurrencyMask.spec.js │ └── CurrencyMask.stories.js ├── custom-mask │ ├── CustomMask.jsx │ ├── CustomMask.spec.js │ └── CustomMask.stories.js └── index.js ├── storybook-static ├── favicon.ico ├── iframe.html ├── index.html ├── main.69bc1cfd417cdd775990.bundle.js ├── main.770445c2dcb78ab6ebac.bundle.js ├── main.770445c2dcb78ab6ebac.bundle.js.map ├── runtime~main.770445c2dcb78ab6ebac.bundle.js ├── runtime~main.770445c2dcb78ab6ebac.bundle.js.map ├── runtime~main.ba735fcc62253c47f409.bundle.js ├── sb_dll │ ├── storybook_ui-manifest.json │ ├── storybook_ui_dll.LICENCE │ └── storybook_ui_dll.js ├── vendors~main.5a6e13a8ae18bb0ecc40.bundle.js ├── vendors~main.770445c2dcb78ab6ebac.bundle.js └── vendors~main.770445c2dcb78ab6ebac.bundle.js.map └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"] 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root=true 2 | 3 | [*] 4 | charset=utf-8 5 | end_of_line=lf 6 | indent_size=2 7 | indent_style=spaces 8 | insert_final_newline=true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": ["standard", "standard-react"], 4 | "plugins": [ 5 | "react-hooks" 6 | ], 7 | "rules": { 8 | "jsx-quotes": ["error", "prefer-double"], 9 | "react-hooks/rules-of-hooks": "error", 10 | "react-hooks/exhaustive-deps": "warn" 11 | }, 12 | "overrides": [ 13 | { 14 | "files": ["**/*.spec.js"], 15 | "env": { 16 | "jest": true 17 | }, 18 | "globals": { 19 | "React": true 20 | } 21 | }, 22 | { 23 | "files": ["**/*.stories.js", "**/*.spec.js"], 24 | "rules": { 25 | "react/jsx-filename-extension": ["off"], 26 | "react/prop-types": ["off"] 27 | } 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | # build output 64 | build 65 | -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-actions/register'; 2 | import '@storybook/addon-knobs/register'; 3 | import '@storybook/addon-links/register'; 4 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure, addDecorator } from '@storybook/react'; 2 | import { withKnobs } from '@storybook/addon-knobs'; 3 | 4 | addDecorator(withKnobs); 5 | 6 | configure(require.context('../src/', true, /\.stories\.js$/), module); 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: node_js 4 | 5 | node_js: 6 | - node 7 | 8 | script: 9 | - npm run lint 10 | - npm run test:coveralls 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Grifo 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-g-input-mask 2 | 3 | An input mask for template or currency 4 | 5 | [![Build Status](https://travis-ci.org/grifo/react-g-input-mask.svg?branch=master)](https://travis-ci.org/grifo/react-g-input-mask) 6 | [![Coverage Status](https://coveralls.io/repos/github/grifo/react-g-input-mask/badge.svg?branch=master)](https://coveralls.io/github/grifo/react-g-input-mask?branch=master) 7 | 8 | ## Installation: 9 | 10 | ``` 11 | yarn add react-g-input-mask 12 | ``` 13 | 14 | or 15 | 16 | ``` 17 | npm install --save react-g-input-mask 18 | ``` 19 | 20 | ## Live demo: 21 | 22 | http://gri.fo/react-g-input-mask/storybook-static 23 | 24 | ## How to: 25 | 26 | ### Currency mask 27 | 28 | This is a currency mask for input fields. The currency is masked using the `Intl.NumberFormat` in the backstage. 29 | 30 | ```js 31 | import { CurrencyMask } from 'react-g-input-mask' 32 | 33 | const MyComponent = () => ( 34 | 43 | ) 44 | ``` 45 | 46 | #### Props: 47 | 48 | | Option | Default | Description | 49 | | - | - | - | 50 | | **options.locale** (required) | - | The locale to format. For the general form and interpretation of the `locales` argument, see the [Intl page](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl#Locale_identification_and_negotiation) | 51 | | **options.currency** (required) | - | The currency to format. Check the [current currency & funds code list](https://www.currency-iso.org/en/home/tables/table-a1.html) | 52 | | **defaultValue** | `""` | The input default value | 53 | | **inputProps** | `{}` | If you want to send props to input, you should pass in this object | 54 | | **as** | `"input"` | If you want to render another element/component instead the `input`, you can use the `as` prop | 55 | 56 | You can have problems with numbers bigger than `Number.MAX_SAFE_INTEGER` (you can check this constant logging it in your browser dev tools) 57 | 58 | 59 | ### Custom mask 60 | 61 | ```js 62 | import { CustomMask } from 'react-g-input-mask' 63 | 64 | const MyComponent = () => ( 65 | 71 | ) 72 | ``` 73 | 74 | 75 | #### Props: 76 | 77 | | Option | Default | Description | 78 | | - | - | - | 79 | | **mask** (required) | - | The custom mask (See below) | 80 | | **placeholderChar** | `"_"`| If you want to use a custom placeholder, pass the char in this prop | 81 | | **defaultValue** | `""` | The input default value | 82 | | **inputProps** | `{}` | If you want to send props to input, you should pass in this object | 83 | | **as** | `"input"` | If you want to render another element/component instead the `input`, you can use the `as` prop | 84 | 85 | To create your mask, you should compose a string with the static chars and the custom validation chars (eg: `"999-99/A"`): 86 | 87 | | Mask | Description | 88 | | - | - | 89 | | **9** | Represents the numbers: `/\d/` | 90 | | **A** | Represents the alphabetic characters `/[A-Za-z]/` | 91 | | **\*** | Any char `/./` | 92 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | collectCoverage: true, 3 | coverageThreshold: { 4 | global: { 5 | branches: 90, 6 | functions: 100, 7 | lines: 95, 8 | statements: 95 9 | } 10 | }, 11 | coveragePathIgnorePatterns: [ 12 | '.stories.js' 13 | ], 14 | setupFilesAfterEnv: [ 15 | '@testing-library/react/cleanup-after-each', 16 | './setupTests.js' 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-g-input-mask", 3 | "version": "0.0.1", 4 | "description": "An input mask for template or currency", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "NODE_ENV=test jest", 8 | "test:coveralls": "yarn test --coverage --coverageReporters=text-lcov | coveralls", 9 | "lint": "eslint src", 10 | "start": "yarn storybook", 11 | "storybook": "start-storybook -p 6006", 12 | "build-storybook": "build-storybook -s public", 13 | "build": "babel src -d build --ignore \"src/**/*.spec.js\",\"src/**/*.stories.js\"", 14 | "build:copy": "cp package.json README.md build", 15 | "build:clear": "rm -rf build", 16 | "deploy": "yarn build-storybook && yarn build && yarn build:copy && cd build && npm publish && cd - && yarn build:clear" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/grifo/react-g-input-mask.git" 21 | }, 22 | "keywords": [ 23 | "react", 24 | "input", 25 | "mask", 26 | "formatter", 27 | "validation" 28 | ], 29 | "author": "Renatho De Carli Rosa (https://github.com/renatho)", 30 | "license": "MIT", 31 | "bugs": { 32 | "url": "https://github.com/grifo/react-g-input-mask/issues" 33 | }, 34 | "homepage": "https://github.com/grifo/react-g-input-mask#readme", 35 | "peerDependencies": { 36 | "react": "^16.8.*", 37 | "react-dom": "^16.8.*" 38 | }, 39 | "devDependencies": { 40 | "@babel/cli": "^7.5.5", 41 | "@babel/core": "^7.5.5", 42 | "@babel/preset-env": "^7.5.5", 43 | "@babel/preset-react": "^7.0.0", 44 | "@storybook/addon-actions": "5.2.0-beta.26", 45 | "@storybook/addon-knobs": "5.2.0-beta.26", 46 | "@storybook/addon-links": "5.2.0-beta.26", 47 | "@storybook/addons": "5.2.0-beta.26", 48 | "@storybook/react": "^5.2.0-beta.26", 49 | "@testing-library/react": "^8.0.8", 50 | "babel-eslint": "^10.0.2", 51 | "babel-loader": "^8.0.6", 52 | "coveralls": "^3.0.6", 53 | "eslint": "^6.1.0", 54 | "eslint-config-standard": "^13.0.1", 55 | "eslint-config-standard-react": "^8.0.0", 56 | "eslint-plugin-import": "^2.18.2", 57 | "eslint-plugin-node": "^9.1.0", 58 | "eslint-plugin-promise": "^4.2.1", 59 | "eslint-plugin-react": "^7.14.3", 60 | "eslint-plugin-react-hooks": "^1.6.1", 61 | "eslint-plugin-standard": "^4.0.0", 62 | "husky": "^3.0.3", 63 | "jest": "^24.8.0", 64 | "react": "^16.8.6", 65 | "react-dom": "^16.8.6" 66 | }, 67 | "husky": { 68 | "hooks": { 69 | "pre-push": "yarn test && yarn run lint" 70 | } 71 | }, 72 | "dependencies": {} 73 | } 74 | -------------------------------------------------------------------------------- /setupTests.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | global.React = React 4 | -------------------------------------------------------------------------------- /src/currency-mask/CurrencyMask.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useReducer, useRef } from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | const initialState = (defaultValue) => ({ 5 | value: defaultValue, 6 | formatting: false, 7 | cursorPosition: 0 8 | }) 9 | 10 | export const reducer = (state, action) => { 11 | switch (action.type) { 12 | case 'INPUT_VALUE': 13 | return { 14 | ...state, 15 | formatting: false, 16 | value: action.value 17 | } 18 | case 'FORMAT_VALUE': 19 | return { 20 | ...state, 21 | formatting: true, 22 | value: action.value, 23 | cursorPosition: action.cursorPosition 24 | } 25 | default: 26 | throw new Error() 27 | } 28 | } 29 | 30 | const getOffset = (prevValue, formattedValue) => { 31 | if (prevValue.length === formattedValue.length) return 0 32 | return prevValue.length < formattedValue.length ? 1 : -1 33 | } 34 | 35 | const formatCurrency = (value, { locale, currency }) => { 36 | const number = value.replace(/\D/g, '').replace(/.{2}$/, '.$&') 37 | return Intl.NumberFormat(locale, { 38 | style: 'currency', 39 | currency 40 | }).format(number) 41 | } 42 | 43 | const CurrencyMask = ({ options, defaultValue, inputProps, as }) => { 44 | const ref = useRef() 45 | const [state, dispatch] = useReducer(reducer, initialState(defaultValue)) 46 | const { value, formatting, cursorPosition } = state 47 | 48 | useEffect(() => { 49 | const formattedValue = formatCurrency(value, options) 50 | 51 | if (value !== formattedValue) { 52 | dispatch({ 53 | type: 'FORMAT_VALUE', 54 | value: formattedValue, 55 | cursorPosition: Math.max( 56 | formattedValue.search(/\d/) + 1, 57 | ref.current.selectionStart + getOffset(value, formattedValue) 58 | ) 59 | }) 60 | } 61 | }, [options, value]) 62 | 63 | useEffect(() => { 64 | if (formatting) { 65 | ref.current.setSelectionRange(cursorPosition, cursorPosition) 66 | } 67 | }, [formatting, cursorPosition]) 68 | 69 | const handleChange = e => { 70 | dispatch({ 71 | type: 'INPUT_VALUE', 72 | value: e.target.value 73 | }) 74 | 75 | if (inputProps.onChange) { 76 | inputProps.onChange(e) 77 | } 78 | } 79 | 80 | const handleKeyUp = (e) => { 81 | const { keyCode } = e 82 | // , or . 83 | if (keyCode === 188 || keyCode === 190) { 84 | const index = value.lastIndexOf(value.match(/\d+/g).slice(-1)[0]) 85 | ref.current.setSelectionRange(index, index + 2) 86 | } 87 | 88 | if (inputProps.onKeyUp) { 89 | inputProps.onKeyUp(e) 90 | } 91 | } 92 | 93 | const InputWrapper = as 94 | 95 | return ( 96 | 104 | ) 105 | } 106 | 107 | CurrencyMask.propTypes = { 108 | options: PropTypes.shape({ 109 | locale: PropTypes.string.isRequired, 110 | currency: PropTypes.string.isRequired 111 | }).isRequired, 112 | defaultValue: PropTypes.string, // TODO: Improve defaultProp code 113 | inputProps: PropTypes.shape({ 114 | onChange: PropTypes.func, 115 | onKeyUp: PropTypes.func 116 | }), 117 | as: PropTypes.oneOfType([ 118 | PropTypes.func, 119 | PropTypes.string 120 | ]) 121 | } 122 | 123 | CurrencyMask.defaultProps = { 124 | defaultValue: '', 125 | inputProps: {}, 126 | as: 'input' 127 | } 128 | 129 | export default CurrencyMask 130 | -------------------------------------------------------------------------------- /src/currency-mask/CurrencyMask.spec.js: -------------------------------------------------------------------------------- 1 | import { render, fireEvent } from '@testing-library/react' 2 | import { currencyMask } from './CurrencyMask.stories' 3 | import { reducer } from './CurrencyMask' 4 | 5 | describe('CurrencyMask', () => { 6 | it('Should format field after type', () => { 7 | const { getByTestId } = render( 8 | currencyMask(null, { 9 | locale: 'en-us', 10 | currency: 'USD' 11 | }) 12 | ) 13 | const input = getByTestId('input') 14 | 15 | fireEvent.change(input, { 16 | target: { 17 | value: '1000' 18 | } 19 | }) 20 | expect(input.value).toEqual('$10.00') 21 | 22 | fireEvent.change(input, { 23 | target: { 24 | value: '$10.000' 25 | } 26 | }) 27 | expect(input.value).toEqual('$100.00') 28 | 29 | fireEvent.change(input, { 30 | target: { 31 | value: '$ 100.00' 32 | } 33 | }) 34 | expect(input.value).toEqual('$100.00') 35 | }) 36 | 37 | it('Should select decionals on press "," (code 188) or "." (code 190)', () => { 38 | const { getByTestId } = render( 39 | currencyMask(null, { 40 | locale: 'en-us', 41 | currency: 'USD' 42 | }) 43 | ) 44 | const input = getByTestId('input') 45 | 46 | // TODO: Improve this test when this bug is fixed: https://github.com/testing-library/react-testing-library/issues/247 47 | input.setSelectionRange = jest.fn() 48 | 49 | fireEvent.change(input, { target: { value: 1000 } }) 50 | fireEvent.keyUp(input, { 51 | keyCode: 190 52 | }) 53 | 54 | expect(input.setSelectionRange).toBeCalledWith(4, 6) 55 | }) 56 | 57 | it('Should call callbacks', () => { 58 | const onChangeMock = jest.fn() 59 | const onKeyUpMock = jest.fn() 60 | const { getByTestId } = render( 61 | currencyMask(null, { 62 | locale: 'en-us', 63 | currency: 'USD', 64 | onChange: onChangeMock, 65 | onKeyUp: onKeyUpMock 66 | }) 67 | ) 68 | const input = getByTestId('input') 69 | 70 | fireEvent.change(input, { target: { value: 11 } }) 71 | fireEvent.keyUp(input, {}) 72 | 73 | expect(onChangeMock).toBeCalled() 74 | expect(onKeyUpMock).toBeCalled() 75 | }) 76 | 77 | it('Should throw error when reducer type is not found', () => { 78 | expect(() => { reducer({}, { type: 'unknown' }) }).toThrow() 79 | }) 80 | }) 81 | -------------------------------------------------------------------------------- /src/currency-mask/CurrencyMask.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { text } from '@storybook/addon-knobs' 3 | import { action } from '@storybook/addon-actions' 4 | 5 | import CurrencyMask from './CurrencyMask' 6 | 7 | export default { title: 'CurrencyMask' } 8 | 9 | export const currencyMask = (story, { locale, currency, onChange, onKeyUp } = {}) => ( 10 | 21 | ) 22 | -------------------------------------------------------------------------------- /src/custom-mask/CustomMask.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | const patterns = { 5 | 9: /\d/, 6 | A: /[A-Za-z]/, 7 | '*': /./ 8 | } 9 | 10 | const CustomMask = ({ mask, placeholderChar, defaultValue, inputProps, as }) => { 11 | const ref = useRef() 12 | const [value, setValue] = useState(defaultValue) 13 | const [sepChars, setSepChars] = useState([]) 14 | const [patternsArray, setPatternsArray] = useState([]) 15 | const [cursorPosition, setCursorPosition] = useState(0) // TODO: Improve the cursor control 16 | 17 | useEffect(() => { 18 | const patternsArray = mask 19 | .split('') 20 | .map(char => 21 | Object.keys(patterns).includes(char) ? patterns[char] : char 22 | ) 23 | setPatternsArray(patternsArray) 24 | 25 | setSepChars(patternsArray.filter(i => typeof i === 'string')) 26 | }, [mask]) 27 | 28 | useEffect(() => { 29 | ref.current.setSelectionRange(cursorPosition, cursorPosition) 30 | }, [value, cursorPosition]) 31 | 32 | const removeMask = inputValue => 33 | inputValue 34 | .split('') 35 | .filter(char => !sepChars.includes(char)) 36 | .join('') 37 | 38 | const addMask = inputValue => { 39 | let valueIndex = 0 40 | return patternsArray 41 | .map(p => { 42 | if (typeof p === 'string') { 43 | return p 44 | } 45 | 46 | const char = inputValue[valueIndex] 47 | valueIndex = valueIndex + 1 48 | if (char && p.test(char)) { 49 | return char 50 | } 51 | return placeholderChar 52 | }) 53 | .join('') 54 | } 55 | 56 | const handleChange = (e) => { 57 | const { target } = e 58 | setValue(addMask(removeMask(target.value))) 59 | 60 | const cursorOffset = 61 | typeof patternsArray[target.selectionStart] === 'string' ? 1 : 0 62 | setCursorPosition(target.selectionStart + cursorOffset) 63 | 64 | if (inputProps.onChange) { 65 | inputProps.onChange(e) 66 | } 67 | } 68 | 69 | const handleKeyUp = (e) => { 70 | const { keyCode, target } = e 71 | 72 | // Backspace 73 | if (keyCode === 8) { 74 | const pos = target.selectionStart - 1 75 | if (typeof patternsArray[pos] === 'string') { 76 | setCursorPosition(pos) 77 | } 78 | } 79 | 80 | if (inputProps.onKeyUp) { 81 | inputProps.onKeyUp(e) 82 | } 83 | } 84 | 85 | const InputWrapper = as 86 | 87 | return ( 88 | 96 | ) 97 | } 98 | 99 | CustomMask.propTypes = { 100 | mask: PropTypes.string.isRequired, 101 | placeholderChar: PropTypes.string, 102 | defaultValue: PropTypes.string, // TODO: Needs to improve defaultProp code 103 | inputProps: PropTypes.shape({ 104 | onChange: PropTypes.func, 105 | onKeyUp: PropTypes.func 106 | }), 107 | as: PropTypes.oneOfType([ 108 | PropTypes.func, 109 | PropTypes.string 110 | ]) 111 | } 112 | 113 | CustomMask.defaultProps = { 114 | placeholderChar: '_', 115 | defaultValue: '', 116 | inputProps: {}, 117 | as: 'input' 118 | } 119 | 120 | export default CustomMask 121 | -------------------------------------------------------------------------------- /src/custom-mask/CustomMask.spec.js: -------------------------------------------------------------------------------- 1 | import { render, fireEvent } from '@testing-library/react' 2 | import { customMask } from './CustomMask.stories' 3 | 4 | describe('CustomMask', () => { 5 | it('Should format field after type', () => { 6 | const { getByTestId } = render( 7 | customMask(null, { 8 | mask: '9-A-*' 9 | }) 10 | ) 11 | const input = getByTestId('input') 12 | 13 | fireEvent.change(input, { 14 | target: { 15 | value: '0X#' 16 | } 17 | }) 18 | 19 | expect(input.value).toEqual('0-X-#') 20 | }) 21 | 22 | it('Should format field after type with custom placeholder', () => { 23 | const { getByTestId } = render( 24 | customMask(null, { 25 | mask: '99/99/9999', 26 | placeholderChar: ' ' 27 | }) 28 | ) 29 | const input = getByTestId('input') 30 | 31 | fireEvent.change(input, { 32 | target: { 33 | value: '0405198 ' 34 | } 35 | }) 36 | 37 | expect(input.value).toEqual('04/05/198 ') 38 | }) 39 | 40 | it('Should not write wrong char', () => { 41 | const { getByTestId } = render( 42 | customMask(null, { 43 | mask: '99' 44 | }) 45 | ) 46 | const input = getByTestId('input') 47 | 48 | fireEvent.change(input, { 49 | target: { 50 | value: '1X' 51 | } 52 | }) 53 | 54 | expect(input.value).toEqual('1_') 55 | }) 56 | 57 | it('Should move back the cursor when keyup backspace in the mask char', () => { 58 | const { getByTestId } = render( 59 | customMask(null, { 60 | mask: '99-9' 61 | }) 62 | ) 63 | const input = getByTestId('input') 64 | 65 | fireEvent.keyUp(input, { 66 | keyCode: 8, 67 | target: { 68 | selectionStart: 3 69 | } 70 | }) 71 | 72 | // TODO: Fix this test when this js-dom bug is fixed: 73 | // https://github.com/testing-library/react-testing-library/issues/247 74 | expect(input.selectionStart).toEqual(0) 75 | }) 76 | 77 | it('Should call callbacks', () => { 78 | const onChangeMock = jest.fn() 79 | const onKeyUpMock = jest.fn() 80 | const { getByTestId } = render( 81 | customMask(null, { 82 | mask: '99', 83 | onChange: onChangeMock, 84 | onKeyUp: onKeyUpMock 85 | }) 86 | ) 87 | const input = getByTestId('input') 88 | 89 | fireEvent.change(input, { target: { value: 11 } }) 90 | fireEvent.keyUp(input, {}) 91 | 92 | expect(onChangeMock).toBeCalled() 93 | expect(onKeyUpMock).toBeCalled() 94 | }) 95 | }) 96 | -------------------------------------------------------------------------------- /src/custom-mask/CustomMask.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { text } from '@storybook/addon-knobs' 3 | import { action } from '@storybook/addon-actions' 4 | 5 | import CustomMask from './CustomMask' 6 | 7 | export default { title: 'CustomMask' } 8 | 9 | export const customMask = (story, { mask, placeholderChar, onChange, onKeyUp } = {}) => ( 10 | 19 | ) 20 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { default as CurrencyMask } from './currency-mask/CurrencyMask' 2 | export { default as CustomMask } from './custom-mask/CustomMask' 3 | -------------------------------------------------------------------------------- /storybook-static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grifo/react-g-input-mask/13aee06c561704e5b5586514239030dc7a6b10e2/storybook-static/favicon.ico -------------------------------------------------------------------------------- /storybook-static/iframe.html: -------------------------------------------------------------------------------- 1 | Storybook

No Preview

Sorry, but you either have no stories or none are selected somehow.

  • Please check the Storybook config.
  • Try reloading the page.

If the problem persists, check the browser console, or the terminal you've run Storybook from.

-------------------------------------------------------------------------------- /storybook-static/index.html: -------------------------------------------------------------------------------- 1 | Storybook
-------------------------------------------------------------------------------- /storybook-static/main.69bc1cfd417cdd775990.bundle.js: -------------------------------------------------------------------------------- 1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[0],{407:function(n,o,p){p(408),p(509),n.exports=p(861)},509:function(n,o,p){"use strict";p.r(o);p(510),p(748),p(858)}},[[407,1,2]]]); -------------------------------------------------------------------------------- /storybook-static/main.770445c2dcb78ab6ebac.bundle.js: -------------------------------------------------------------------------------- 1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[0],{246:function(module,exports,__webpack_require__){__webpack_require__(247),__webpack_require__(348),module.exports=__webpack_require__(349)},349:function(module,__webpack_exports__,__webpack_require__){"use strict";__webpack_require__.r(__webpack_exports__),function(module){var _storybook_react__WEBPACK_IMPORTED_MODULE_0__=__webpack_require__(159),_storybook_addon_knobs__WEBPACK_IMPORTED_MODULE_1__=__webpack_require__(45);Object(_storybook_react__WEBPACK_IMPORTED_MODULE_0__.addDecorator)(_storybook_addon_knobs__WEBPACK_IMPORTED_MODULE_1__.withKnobs),Object(_storybook_react__WEBPACK_IMPORTED_MODULE_0__.configure)(__webpack_require__(522),module)}.call(this,__webpack_require__(350)(module))},522:function(module,exports,__webpack_require__){var map={"./currency-mask/CurrencyMask.stories.js":542,"./custom-mask/CustomMask.stories.js":541};function webpackContext(req){var id=webpackContextResolve(req);return __webpack_require__(id)}function webpackContextResolve(req){if(!__webpack_require__.o(map,req)){var e=new Error("Cannot find module '"+req+"'");throw e.code="MODULE_NOT_FOUND",e}return map[req]}webpackContext.keys=function webpackContextKeys(){return Object.keys(map)},webpackContext.resolve=webpackContextResolve,module.exports=webpackContext,webpackContext.id=522},541:function(module,__webpack_exports__,__webpack_require__){"use strict";__webpack_require__.r(__webpack_exports__);var react=__webpack_require__(4),react_default=__webpack_require__.n(react),dist=__webpack_require__(45),addon_actions_dist=__webpack_require__(67),prop_types=(__webpack_require__(16),__webpack_require__(20),__webpack_require__(21),__webpack_require__(52),__webpack_require__(115),__webpack_require__(22),__webpack_require__(17),__webpack_require__(540),__webpack_require__(51),__webpack_require__(23),__webpack_require__(14),__webpack_require__(15),__webpack_require__(32),__webpack_require__(116),__webpack_require__(24),__webpack_require__(108),__webpack_require__(25),__webpack_require__(7)),prop_types_default=__webpack_require__.n(prop_types);function _extends(){return(_extends=Object.assign||function(target){for(var source,i=1;i 34 | * 35 | * Copyright (c) 2014-2017, Jon Schlinkert. 36 | * Released under the MIT License. 37 | */ 38 | 39 | /*! 40 | * https://github.com/paulmillr/es6-shim 41 | * @license es6-shim Copyright 2013-2016 by Paul Miller (http://paulmillr.com) 42 | * and contributors, MIT License 43 | * es6-shim: v0.35.4 44 | * see https://github.com/paulmillr/es6-shim/blob/0.35.3/LICENSE 45 | * Details and documentation: 46 | * https://github.com/paulmillr/es6-shim/ 47 | */ 48 | 49 | /** @license React v16.8.6 50 | * react.production.min.js 51 | * 52 | * Copyright (c) Facebook, Inc. and its affiliates. 53 | * 54 | * This source code is licensed under the MIT license found in the 55 | * LICENSE file in the root directory of this source tree. 56 | */ 57 | 58 | /** @license React v16.8.6 59 | * react-is.production.min.js 60 | * 61 | * Copyright (c) Facebook, Inc. and its affiliates. 62 | * 63 | * This source code is licensed under the MIT license found in the 64 | * LICENSE file in the root directory of this source tree. 65 | */ 66 | 67 | /** @license React v0.13.6 68 | * scheduler.production.min.js 69 | * 70 | * Copyright (c) Facebook, Inc. and its affiliates. 71 | * 72 | * This source code is licensed under the MIT license found in the 73 | * LICENSE file in the root directory of this source tree. 74 | */ 75 | 76 | /* 77 | object-assign 78 | (c) Sindre Sorhus 79 | @license MIT 80 | */ 81 | 82 | /*! 83 | * Fuse.js v3.4.5 - Lightweight fuzzy-search (http://fusejs.io) 84 | * 85 | * Copyright (c) 2012-2017 Kirollos Risk (http://kiro.me) 86 | * All Rights Reserved. Apache Software License 2.0 87 | * 88 | * http://www.apache.org/licenses/LICENSE-2.0 89 | */ 90 | 91 | /** @license React v16.8.6 92 | * react-dom.production.min.js 93 | * 94 | * Copyright (c) Facebook, Inc. and its affiliates. 95 | * 96 | * This source code is licensed under the MIT license found in the 97 | * LICENSE file in the root directory of this source tree. 98 | */ 99 | 100 | /*! 101 | Copyright (c) 2016 Jed Watson. 102 | Licensed under the MIT License (MIT), see 103 | http://jedwatson.github.io/classnames 104 | */ 105 | -------------------------------------------------------------------------------- /storybook-static/vendors~main.770445c2dcb78ab6ebac.bundle.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"vendors~main.770445c2dcb78ab6ebac.bundle.js","sources":["webpack:///vendors~main.770445c2dcb78ab6ebac.bundle.js"],"mappings":"AAAA;;;;;AA+1UA;;;;;AAo3DA;;;;;AAkkEA;;;;;;;;;AAukBA;;;;;;;;AA8hbA;;;;;;;;AAmCA;;;;;;;;;AA6RA;;;;;;AAq8CA;;;;;;;AAs0BA;;;;;;;AAqgCA;;;;;;;AAfA","sourceRoot":""} --------------------------------------------------------------------------------