├── .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 | [](https://travis-ci.org/grifo/react-g-input-mask) 6 | [](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 | StorybookNo PreviewSorry, 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":""} --------------------------------------------------------------------------------
Sorry, but you either have no stories or none are selected somehow.
If the problem persists, check the browser console, or the terminal you've run Storybook from.