diff --git a/.enzymerc.js b/.enzymerc.js deleted file mode 100644 index a050dad..0000000 --- a/.enzymerc.js +++ /dev/null @@ -1,4 +0,0 @@ -const { configure } = require('enzyme'); -const Adapter = require('enzyme-adapter-react-16'); - -configure({ adapter: new Adapter() }); diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index d1d60f1..c5ed269 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -24,7 +24,7 @@ jobs: - name: Run linting run: yarn lint - name: Run tests - run: yarn test + run: yarn test-ci - name: Code coverage env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d7c5b5f..af1d6c6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,7 +25,7 @@ jobs: - name: Build package run: yarn build - name: Run tests - run: yarn test + run: yarn test-ci - name: Code coverage env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 21b0dd0..5caafc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,93 @@ +# [3.0.0-beta.11](https://github.com/cchanxzy/react-currency-input-field/compare/v3.0.0-beta.10...v3.0.0-beta.11) (2021-01-04) + +# [3.0.0-beta.10](https://github.com/cchanxzy/react-currency-input-field/compare/v3.0.0-beta.9...v3.0.0-beta.10) (2021-01-02) + + +### Features + +* allow customInput prop ([f372201](https://github.com/cchanxzy/react-currency-input-field/commit/f3722015650c24efd522f93dfb8a482bc4ba87a4)) + +# [3.0.0-beta.9](https://github.com/cchanxzy/react-currency-input-field/compare/v3.0.0-beta.8...v3.0.0-beta.9) (2021-01-02) + + +### Bug Fixes + +* fixed a few inconsistencies and added intl config to examples ([796e623](https://github.com/cchanxzy/react-currency-input-field/commit/796e623ef679ca047140d6ee421961877bdaa181)) + +# [3.0.0-beta.8](https://github.com/cchanxzy/react-currency-input-field/compare/v3.0.0-beta.7...v3.0.0-beta.8) (2021-01-02) + + +### Features + +* handle backspace with suffix ([fc84301](https://github.com/cchanxzy/react-currency-input-field/commit/fc8430162d2c51cc374b0b7f4ed221a1329972b5)) + +# [3.0.0-beta.7](https://github.com/cchanxzy/react-currency-input-field/compare/v3.0.0-beta.6...v3.0.0-beta.7) (2020-12-10) + +# [3.0.0-beta.6](https://github.com/cchanxzy/react-currency-input-field/compare/v3.0.0-beta.5...v3.0.0-beta.6) (2020-12-10) + +# [3.0.0-beta.5](https://github.com/cchanxzy/react-currency-input-field/compare/v3.0.0-beta.4...v3.0.0-beta.5) (2020-12-09) + + +### Features + +* depreciate onBlurValue prop ([8651e76](https://github.com/cchanxzy/react-currency-input-field/commit/8651e76c201b029787490efcf37d307a1b5d8d97)) + + +### BREAKING CHANGES + +* "onBlurValue" no longer supported + +# [3.0.0-beta.4](https://github.com/cchanxzy/react-currency-input-field/compare/v3.0.0-beta.3...v3.0.0-beta.4) (2020-12-09) + + +### Features + +* renamed onChange prop to onValueChange ([83d3806](https://github.com/cchanxzy/react-currency-input-field/commit/83d380660597797bfc38e609599c103f8176fd28)) + + +### BREAKING CHANGES + +* Renamed "onChange" to "onValueChange" + +# [3.0.0-beta.3](https://github.com/cchanxzy/react-currency-input-field/compare/v3.0.0-beta.2...v3.0.0-beta.3) (2020-12-06) + + +### Features + +* renamed precision to decimalScale ([c545b78](https://github.com/cchanxzy/react-currency-input-field/commit/c545b780815bff7c98c66e527f3f3f4a1cc8ee67)) +* renamed turnOffAbbreviations to disableAbbreviations ([7751a43](https://github.com/cchanxzy/react-currency-input-field/commit/7751a4386baee5554aa030839d1cdc0f3750f360)) +* renamed turnOffSeparators to disableGroupSeparators ([b16f577](https://github.com/cchanxzy/react-currency-input-field/commit/b16f577e29646e2fba9db370fb4eda2c73ae632e)) + + +### BREAKING CHANGES + +* Renamed "turnOffAbbreviations" to "disableAbbreviations" +* Renamed "turnOffSeparators" to "disableGroupSeparators" +* renamed precision to decimalScale + +# [3.0.0-beta.2](https://github.com/cchanxzy/react-currency-input-field/compare/v3.0.0-beta.1...v3.0.0-beta.2) (2020-12-03) + + +### Features + +* add intl locale config option ([e119352](https://github.com/cchanxzy/react-currency-input-field/commit/e119352212b1aaa8bafdb02dfd312de7a7302cfc)) + + +### BREAKING CHANGES + +* Using Intl.NumberFormat to format value + +# [3.0.0-beta.1](https://github.com/cchanxzy/react-currency-input-field/compare/v2.7.0...v3.0.0-beta.1) (2020-11-19) + + +### Features + +* wrap component in forwardRef ([3a1f5bc](https://github.com/cchanxzy/react-currency-input-field/commit/3a1f5bcd6422c49ea85ad9980109cd183ceec2f1)) + + +### BREAKING CHANGES + +* can pass in component ref ## [2.7.1](https://github.com/cchanxzy/react-currency-input-field/compare/v2.7.0...v2.7.1) (2020-12-10) # [2.7.0](https://github.com/cchanxzy/react-currency-input-field/compare/v2.6.0...v2.7.0) (2020-11-18) diff --git a/README.md b/README.md index ebbcdc3..a76592f 100644 --- a/README.md +++ b/README.md @@ -2,20 +2,41 @@ [![npm](https://img.shields.io/npm/v/react-currency-input-field)](https://www.npmjs.com/package/react-currency-input-field) [![NPM](https://img.shields.io/npm/l/react-currency-input-field)](https://www.npmjs.com/package/react-currency-input-field) [![Codecov Coverage](https://img.shields.io/codecov/c/github/cchanxzy/react-currency-input-field)](https://codecov.io/gh/cchanxzy/react-currency-input-field/) [![Release build](https://github.com/cchanxzy/react-currency-input-field/workflows/Release/badge.svg)](https://github.com/cchanxzy/react-currency-input-field/actions?query=workflow%3ARelease) -## V3 Pre-release - -There will be a couple of breaking changes in v3.0.0, so I recommend taking a look at the [v3.0.0 beta](https://www.npmjs.com/package/react-currency-input-field/v/3.0.0-beta.7) which is available now for testing. - -Any early feedback will be very much welcomed. +- [React Currency Input Field Component](#react-currency-input-field-component) + - [Migrating to v3.0.0](#migrating-to-v300) + - [Features](#features) + - [Examples](#examples) + - [Install](#install) + - [Usage](#usage) + - [Props](#props) + - [Abbreviations](#abbreviations) + - [Separators](#separators) + - [Intl Locale Config](#intl-locale-config) + - [Fixed Decimal Length](#fixed-decimal-length) + - [Decimal Scale and Decimals Limit](#decimal-scale-and-decimals-limit) + - [Format values for display](#format-values-for-display) + - [v3.0.0 Release Notes](#v300-release-notes) + - [Breaking Changes](#breaking-changes) + - [Improvements in v3](#improvements-in-v3) + - [Reasoning](#reasoning) + - [Issues](#issues) + +## Migrating to v3.0.0 + +There are a number of breaking changes in v3.0.0, please read the [release notes](#v300-release-notes) if migrating from v2 to v3. + +:warning: Most important change is: `onChange` has been renamed to `onValueChange` ## Features -- Allows abbreviations eg. 1k = 1,000 2.5m = 2,500,000 +- Allows [abbreviations](#abbreviations) eg. 1k = 1,000 2.5m = 2,500,000 - Prefix option eg. £ or \$ +- Automatically inserts [group separator](#separators) +- Accepts [Intl locale config](#intl-locale-config) +- Can use arrow down/up to step - Can allow/disallow decimals -- Automatically inserts comma separator -- Only allows valid numbers -- Lightweight and simple +- Written in TypeScript and has type support +- Does not use any third party packages ## Examples @@ -39,17 +60,39 @@ import CurrencyInput from 'react-currency-input-field'; console.log(value, name)} + onValueChange={(value, name) => console.log(value, name)} />; ``` Have a look in [`src/examples`](https://github.com/cchanxzy/react-currency-input-field/tree/master/src/examples) for more examples on implementing and validation. -## Abbreviations +## Props + +| Name | Type | Default | Description | +| ---------------------- | ---------- | -------------- | ---------------------------------------------------------------------------------------------- | +| allowDecimals | `boolean` | `true` | Allow decimals | +| allowNegativeValue | `boolean` | `true` | Allow user to enter negative value | +| defaultValue | `number` | | Default value | +| value | `number` | | Programmatically set the value | +| onValueChange | `function` | | Handle change in value | +| placeholder | `string` | | Placeholder if no value | +| decimalsLimit | `number` | `2` | Limit length of decimals allowed | +| decimalScale | `number` | | Specify decimal scale for padding/trimming eg. 1.5 -> 1.50 or 1.234 -> 1.23 if decimal scale 2 | +| fixedDecimalLength | `number` | | Value will always have the specified length of decimals | +| prefix | `string` | | Include a prefix eg. £ or \$ | +| decimalSeparator | `string` | locale default | Separator between integer part and fractional part of value | +| groupSeparator | `string` | locale default | Separator between thousand, million and billion | +| intlConfig | `object` | | International locale config | +| disabled | `boolean` | `false` | Disabled | +| disableAbbreviations | `boolean` | `false` | Disable abbreviations eg. 1k -> 1,000, 2m -> 2,000,000 | +| disableGroupSeparators | `boolean` | `false` | Disable auto adding the group separator between values, eg. 1000 -> 1,000 | +| maxLength | `number` | | Maximum characters the user can enter | +| step | `number` | | Incremental value change on arrow down and arrow up key press | + +### Abbreviations It can parse values with abbreviations `k`, `m` and `b` @@ -59,7 +102,9 @@ Examples: - 2.5m = 2,500,000 - 3.456B = 3,456,000,000 -## Separators +This can be turned off by passing in `disableAbbreviations`. + +### Separators You can change the decimal and group separators by passing in `decimalSeparator` and `groupSeparator`. @@ -68,21 +113,40 @@ Example: ```js import CurrencyInput from 'react-currency-input-field'; -; +; ``` Note: the separators cannot be a number, and `decimalSeparator` must be different to `groupSeparator`. -To turn off auto adding the group separator, add `turnOffSeparators={true}`. +To turn off auto adding the group separator, add `disableGroupSeparators={true}`. + +### Intl Locale Config + +This component can also accept international locale config to format the currency to locale setting. + +Examples: + +```javascript +import CurrencyInput from 'react-currency-input-field'; + +; + +; + +; +``` + +`locale` should be a [BCP 47 language tag](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl#Locale_identification_and_negotiation), such as "en-US" or "en-IN". -## Fixed Decimal Length +`currency` should be a [ISO 4217 currency code](https://en.wikipedia.org/wiki/ISO_4217), such as "USD" for the US dollar, "EUR" for the euro, or "CNY" for the Chinese RMB. -Use `fixedDecimalLength` so that the value will always have the specified length of decimals. This formatting happens onBlur. +Any prefix, group separator and decimal separator options passed in will override the default locale settings. + +### Fixed Decimal Length + +Use `fixedDecimalLength` so that the value will always have the specified length of decimals. + +This formatting happens onBlur. Example if `fixedDecimalLength` was 2: @@ -93,30 +157,11 @@ Example if `fixedDecimalLength` was 2: - 12.34 -> 12.34 ``` -## Props +## Decimal Scale and Decimals Limit + +`decimalsLimit` and `decimalScale` are similar, the difference is `decimalsLimit` prevents the user from typing more than the limit, and `decimalScale` will format the decimals `onBlur` to the specified length, padding or trimming as necessary. -| Name | Type | Default | Description | -| -------------------- | ---------- | ------- | ------------------------------------------------------------------------ | -| allowDecimals | `boolean` | `true` | Allow decimals | -| allowNegativeValue | `boolean` | `true` | Allow user to enter negative value | -| className | `string` | | Class names | -| decimalsLimit | `number` | `2` | Limit length of decimals allowed | -| defaultValue | `number` | | Default value | -| value | `number` | | Programmatically set the value | -| disabled | `boolean` | `false` | Disabled | -| fixedDecimalLength | `number` | | Value will always have the specified length of decimals | -| id | `string` | | Component id | -| maxLength | `number` | | Maximum characters the user can enter | -| onChange | `function` | | Handle change in value | -| onBlurValue | `function` | | Handle value onBlur | -| placeholder | `string` | | Placeholder if no value | -| precision | `number` | | Specify decimal precision for padding/trimming | -| prefix | `string` | | Include a prefix eg. £ or \$ | -| step | `number` | | Incremental value change on arrow down and arrow up key press | -| decimalSeparator | `string` | `.` | Separator between integer part and fractional part of value | -| groupSeparator | `string` | `,` | Separator between thousand, million and billion | -| turnOffAbbreviations | `boolean` | `false` | Disable abbreviations eg. 1k > 1,000, 2m > 2,000,000 | -| turnOffSeparators | `boolean` | `false` | Disable auto adding the group separator between values, eg. 1000 > 1,000 | +Example: If decimal scale 2, 1.5 -> 1.50 and 1.234 -> 1.23 ## Format values for display @@ -125,15 +170,54 @@ Use the `formatValue` function to format the values to a more user friendly stri ```javascript import { formatValue } from 'react-currency-input-field'; -const formattedValue = formatValue({ - value = 123456, - groupSeparator = ',', - decimalSeparator = '.', - turnOffSeparators = false, - prefix = '$', +// Format using prefix, groupSeparator and decimalSeparator +const formattedValue1 = formatValue({ + value: 123456, + groupSeparator: ',', + decimalSeparator: '.', + prefix: '$', }); + +console.log(formattedValue1); +// $123,456 + +// Format using intl locale config +const formattedValue2 = formatValue({ + value: 500000, + intlConfig: { locale: 'en-IN', currency: 'INR' }, +}); + +console.log(formattedValue2); +// ₹5,00,000 ``` +## v3.0.0 Release Notes + +### Breaking Changes + +- :warning: `onChange` renamed to `onValueChange` :warning: +- `onBlurValue` has been removed. +- `turnOffAbbreviations` renamed to `disableAbbreviations`. +- `turnOffSeparators` renamed to `disableGroupSeparators`. +- `precision` renamed to `decimalScale` + +### Improvements in v3 + +- [Intl locale config](#intl-locale-config) can be passed in. _Please note: formatting where the currency symbol is placed after the value like a suffix eg. (1 234,56 €) might cause problems, this is still in development._ +- Group separator will default to browser locale if not specified. +- Can pass `ref` to the component. +- `onChange` and `onBlur` functions can be passed in and will be called with original event. + +### Reasoning + +As this component grew in usage, I started getting more bug reports and feature requests. That wasn't a problem though, because I was always happy to fix any bugs and implement any features if I could. + +However, this meant sometimes I was a bit trigger happy, and didn't always think about how the different options interacted with each other. I found that it was getting a bit convoluted for my liking, and choices I had made earlier in development, now seemed like it could be improved. + +Therefore, I took the opportunity of v3 to do a bit of tidying up for the component, in order to make it more future proof and intuitive to use. + +I apologize if any of the changes cause new bugs or issues. Please let me know and I will fix asap. + ## Issues Feel free to raise an issue on Github if you find a bug or have a feature request diff --git a/demo/.DS_Store b/demo/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/demo/.DS_Store differ diff --git a/demo/demo.gif b/demo/demo.gif index f303c9c..2cd9655 100644 Binary files a/demo/demo.gif and b/demo/demo.gif differ diff --git a/package.json b/package.json index 3e20deb..758c764 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "react-currency-input-field", "version": "2.7.1", - "description": "React component for formatting currency and numbers.", + "description": "React component for formatting currency and numbers.", "files": [ "dist/**/*" ], @@ -12,7 +12,7 @@ "build": "rm -rf dist && tsc && NODE_ENV='production' webpack --config-name=prod --mode=production", "start": "NODE_ENV='development' webpack-dev-server --config-name=dev --mode=development --hot", "test": "jest --coverage", - "test:watch": "jest --coverage --watch", + "test-ci": "cross-env NODE_ICU_DATA=node_modules/full-icu jest --coverage", "lint": "tsc && eslint 'src/**/*.{js,ts,tsx}' --max-warnings=0", "gh-predeploy": "NODE_ENV='production' webpack --config-name=dev --mode=production", "gh-deploy": "yarn gh-predeploy && gh-pages -d demo/examples", @@ -35,7 +35,9 @@ "form", "field", "number", - "input" + "input", + "intl", + "locale" ], "author": "cchanxzy", "license": "MIT", @@ -44,7 +46,10 @@ "@commitlint/config-conventional": "^9.1.1", "@semantic-release/changelog": "^5.0.1", "@semantic-release/git": "^9.0.0", - "@types/enzyme": "^3.10.5", + "@testing-library/dom": "^7.29.0", + "@testing-library/jest-dom": "^5.11.6", + "@testing-library/react": "^11.2.2", + "@testing-library/user-event": "^12.6.0", "@types/jest": "^26.0.9", "@types/react": "^16.9.46", "@types/react-dom": "^16.9.8", @@ -52,12 +57,12 @@ "@typescript-eslint/parser": "^3.9.0", "awesome-typescript-loader": "^5.2.1", "codecov": "^3.7.2", - "enzyme": "^3.11.0", - "enzyme-adapter-react-16": "^1.15.3", + "cross-env": "^7.0.3", "eslint": "^7.6.0", "eslint-config-prettier": "^6.11.0", "eslint-plugin-prettier": "^3.1.4", "eslint-plugin-react": "^7.20.6", + "full-icu": "^1.3.1", "gh-pages": "^3.1.0", "html-webpack-plugin": "^4.3.0", "husky": "^4.2.5", @@ -82,9 +87,6 @@ "jest": { "verbose": false, "collectCoverage": true, - "setupFilesAfterEnv": [ - "./.enzymerc.js" - ], "transform": { "^.+\\.tsx?$": "ts-jest" }, diff --git a/src/components/CurrencyInput.tsx b/src/components/CurrencyInput.tsx index 2b24e05..9dfe97b 100644 --- a/src/components/CurrencyInput.tsx +++ b/src/components/CurrencyInput.tsx @@ -1,184 +1,256 @@ -import React, { FC, useState, useEffect, useRef } from 'react'; +import React, { FC, useState, useEffect, useRef, forwardRef, useMemo } from 'react'; import { CurrencyInputProps } from './CurrencyInputProps'; import { isNumber, cleanValue, fixedDecimalValue, formatValue, + getLocaleConfig, padTrimValue, CleanValueOptions, + getSuffix, } from './utils'; -export const CurrencyInput: FC = ({ - allowDecimals = true, - allowNegativeValue = true, - id, - name, - className, - decimalsLimit, - defaultValue, - disabled = false, - maxLength: userMaxLength, - value: userValue, - onChange, - onBlurValue, - fixedDecimalLength, - placeholder, - precision, - prefix, - step, - decimalSeparator = '.', - groupSeparator = ',', - turnOffSeparators = false, - turnOffAbbreviations = false, - ...props -}: CurrencyInputProps) => { - if (decimalSeparator === groupSeparator) { - throw new Error('decimalSeparator cannot be the same as groupSeparator'); - } - - if (isNumber(decimalSeparator)) { - throw new Error('decimalSeparator cannot be a number'); - } - - if (isNumber(groupSeparator)) { - throw new Error('groupSeparator cannot be a number'); - } - - const formatValueOptions = { - decimalSeparator, - groupSeparator, - turnOffSeparators, - prefix, - }; - - const cleanValueOptions: Partial = { - decimalSeparator, - groupSeparator, - allowDecimals, - decimalsLimit: decimalsLimit || fixedDecimalLength || 2, - allowNegativeValue, - turnOffAbbreviations, - prefix, - }; - - const _defaultValue = - defaultValue !== undefined - ? formatValue({ value: String(defaultValue), ...formatValueOptions }) - : ''; - const [stateValue, setStateValue] = useState(_defaultValue); - const [cursor, setCursor] = useState(0); - const inputRef = useRef(null); - - const onFocus = (): number => (stateValue ? stateValue.length : 0); - - const processChange = (value: string, selectionStart?: number | null): void => { - const valueOnly = cleanValue({ value, ...cleanValueOptions }); - - if (!valueOnly) { - onChange && onChange(undefined, name); - setStateValue(''); - return; +export const CurrencyInput: FC = forwardRef< + HTMLInputElement, + CurrencyInputProps +>( + ( + { + allowDecimals = true, + allowNegativeValue = true, + id, + name, + className, + customInput, + decimalsLimit, + defaultValue, + disabled = false, + maxLength: userMaxLength, + value: userValue, + onValueChange, + fixedDecimalLength, + placeholder, + decimalScale, + prefix, + intlConfig, + step, + disableGroupSeparators = false, + disableAbbreviations = false, + decimalSeparator: _decimalSeparator, + groupSeparator: _groupSeparator, + onChange, + onFocus, + onBlur, + onKeyDown, + onKeyUp, + ...props + }: CurrencyInputProps, + ref + ) => { + if (_decimalSeparator && _groupSeparator && _decimalSeparator === _groupSeparator) { + throw new Error('decimalSeparator cannot be the same as groupSeparator'); } - if (userMaxLength && valueOnly.replace(/-/g, '').length > userMaxLength) { - return; + if (_decimalSeparator && isNumber(_decimalSeparator)) { + throw new Error('decimalSeparator cannot be a number'); } - if (valueOnly === '-') { - onChange && onChange(undefined, name); - setStateValue(value); - return; + if (_groupSeparator && isNumber(_groupSeparator)) { + throw new Error('groupSeparator cannot be a number'); } - const formattedValue = formatValue({ value: valueOnly, ...formatValueOptions }); + const localeConfig = useMemo(() => getLocaleConfig(intlConfig), [intlConfig]); + const decimalSeparator = _decimalSeparator || localeConfig.decimalSeparator || ''; + const groupSeparator = _groupSeparator || localeConfig.groupSeparator || ''; - /* istanbul ignore next */ - if (selectionStart !== undefined && selectionStart !== null) { - const cursor = selectionStart + (formattedValue.length - value.length) || 1; - setCursor(cursor); - } + const formatValueOptions = { + decimalSeparator, + groupSeparator, + disableGroupSeparators, + intlConfig, + prefix, + }; - setStateValue(formattedValue); + const cleanValueOptions: Partial = { + decimalSeparator, + groupSeparator, + allowDecimals, + decimalsLimit: decimalsLimit || fixedDecimalLength || 2, + allowNegativeValue, + disableAbbreviations, + prefix, + }; - onChange && onChange(valueOnly, name); - }; + const _defaultValue = + defaultValue !== undefined + ? formatValue({ value: String(defaultValue), ...formatValueOptions }) + : userValue !== undefined + ? formatValue({ value: String(userValue), ...formatValueOptions }) + : ''; + const [stateValue, setStateValue] = useState(_defaultValue); + const [cursor, setCursor] = useState(0); + const inputRef = ref || useRef(null); - const handleOnChange = ({ - target: { value, selectionStart }, - }: React.ChangeEvent): void => { - processChange(value, selectionStart); - }; + const processChange = (value: string, selectionStart?: number | null): void => { + const valueOnly = cleanValue({ value, ...cleanValueOptions }); - const handleOnBlur = ({ target: { value } }: React.ChangeEvent): void => { - const valueOnly = cleanValue({ value, ...cleanValueOptions }); + if (valueOnly === '') { + onValueChange && onValueChange(undefined, name); + setStateValue(''); + return; + } - if (valueOnly === '-' || !valueOnly) { - onBlurValue && onBlurValue(undefined, name); - setStateValue(''); - return; - } + if (userMaxLength && valueOnly.replace(/-/g, '').length > userMaxLength) { + return; + } - const fixedDecimals = fixedDecimalValue(valueOnly, decimalSeparator, fixedDecimalLength); - - // Add padding or trim value to precision - const newValue = padTrimValue(fixedDecimals, decimalSeparator, precision || fixedDecimalLength); - onChange && onChange(newValue, name); - onBlurValue && onBlurValue(newValue, name); - - const formattedValue = formatValue({ value: newValue, ...formatValueOptions }); - setStateValue(formattedValue); - }; - - const handleOnKeyDown = ({ key }: React.KeyboardEvent) => { - if (step && (key === 'ArrowUp' || key === 'ArrowDown')) { - const currentValue = - Number( - userValue !== undefined - ? userValue - : cleanValue({ value: stateValue, ...cleanValueOptions }) - ) || 0; - const newValue = - key === 'ArrowUp' - ? String(currentValue + Number(step)) - : String(currentValue - Number(step)); - - processChange(newValue); - } - }; + if (valueOnly === '-') { + onValueChange && onValueChange(undefined, name); + setStateValue(value); + return; + } - /* istanbul ignore next */ - useEffect(() => { - if (inputRef && inputRef.current) { - inputRef.current.setSelectionRange(cursor, cursor); - } - }, [cursor, inputRef]); - - const formattedPropsValue = - userValue !== undefined - ? formatValue({ value: String(userValue), ...formatValueOptions }) - : undefined; - - return ( - ): void => { + const { + target: { value, selectionStart }, + } = event; + + processChange(value, selectionStart); + + onChange && onChange(event); + }; + + const handleOnFocus = (event: React.FocusEvent): number => { + onFocus && onFocus(event); + return stateValue ? stateValue.length : 0; + }; + + const handleOnBlur = (event: React.FocusEvent): void => { + const { + target: { value }, + } = event; + + const valueOnly = cleanValue({ value, ...cleanValueOptions }); + + if (valueOnly === '-' || !valueOnly) { + setStateValue(''); + onBlur && onBlur(event); + return; } - ref={inputRef} - {...props} - /> - ); -}; + + const fixedDecimals = fixedDecimalValue(valueOnly, decimalSeparator, fixedDecimalLength); + + // Add padding or trim value to decimalScale + const newValue = padTrimValue( + fixedDecimals, + decimalSeparator, + decimalScale || fixedDecimalLength + ); + + onValueChange && onValueChange(newValue, name); + + const formattedValue = formatValue({ + ...formatValueOptions, + value: newValue, + }); + + setStateValue(formattedValue); + + onBlur && onBlur(event); + }; + + const handleOnKeyDown = (event: React.KeyboardEvent) => { + const { key } = event; + + if (step && (key === 'ArrowUp' || key === 'ArrowDown')) { + event.preventDefault(); + const currentValue = + Number( + userValue !== undefined + ? userValue + : cleanValue({ value: stateValue, ...cleanValueOptions }) + ) || 0; + const newValue = key === 'ArrowUp' ? currentValue + step : currentValue - step; + processChange(String(newValue)); + } + + onKeyDown && onKeyDown(event); + }; + + const handleOnKeyUp = (event: React.KeyboardEvent) => { + const { + currentTarget: { selectionStart }, + } = event; + const suffix = getSuffix(stateValue, { groupSeparator, decimalSeparator }); + + if (suffix && selectionStart && selectionStart > stateValue.length - suffix.length) { + if (inputRef && typeof inputRef === 'object' && inputRef.current) { + const newCursor = stateValue.length - suffix.length; + inputRef.current.setSelectionRange(newCursor, newCursor); + } + } + + onKeyUp && onKeyUp(event); + }; + + /* istanbul ignore next */ + useEffect(() => { + if (inputRef && typeof inputRef === 'object' && inputRef.current) { + inputRef.current.setSelectionRange(cursor, cursor); + } + }, [cursor, inputRef]); + + const formattedPropsValue = + userValue !== undefined + ? formatValue({ + ...formatValueOptions, + value: String(userValue), + }) + : undefined; + + const inputProps: React.InputHTMLAttributes = { + type: 'text', + inputMode: 'decimal', + id, + name, + className, + onChange: handleOnChange, + onBlur: handleOnBlur, + onFocus: handleOnFocus, + onKeyDown: handleOnKeyDown, + onKeyUp: handleOnKeyUp, + placeholder, + disabled, + value: + formattedPropsValue !== undefined && stateValue !== '-' ? formattedPropsValue : stateValue, + ref: inputRef, + ...props, + }; + + if (customInput) { + const CustomInput = customInput; + return ; + } + + return ; + } +); + +CurrencyInput.displayName = 'CurrencyInput'; export default CurrencyInput; diff --git a/src/components/CurrencyInputProps.ts b/src/components/CurrencyInputProps.ts index 855a706..205c76a 100644 --- a/src/components/CurrencyInputProps.ts +++ b/src/components/CurrencyInputProps.ts @@ -1,6 +1,11 @@ +import { Ref, ElementType } from 'react'; + type Overwrite = Pick> & U; -export type Separator = ',' | '.'; +export type IntlConfig = { + locale: string; + currency: string; +}; export type CurrencyInputProps = Overwrite< React.InputHTMLAttributes, @@ -34,6 +39,13 @@ export type CurrencyInputProps = Overwrite< */ className?: string; + /** + * Custom component + * + * Default = + */ + customInput?: ElementType; + /** * Limit length of decimals allowed * @@ -42,7 +54,14 @@ export type CurrencyInputProps = Overwrite< decimalsLimit?: number; /** - * Default value + * Specify decimal scale for padding/trimming + * + * Eg. 1.5 -> 1.50 or 1.234 -> 1.23 + */ + decimalScale?: number; + + /** + * Default value if not passing in value via props */ defaultValue?: number | string; @@ -55,30 +74,23 @@ export type CurrencyInputProps = Overwrite< /** * Value will always have the specified length of decimals + * + * Eg. 123 -> 1.23 + * + * Note: This formatting only happens onBlur */ fixedDecimalLength?: number; /** * Handle change in value */ - onChange?: (value: string | undefined, name?: string) => void; + onValueChange?: (value: string | undefined, name?: string) => void; /** - * Handle value onBlur - * - */ - onBlurValue?: (value: string | undefined, name?: string) => void; - - /** - * Placeholder + * Placeholder if there is no value */ placeholder?: string; - /** - * Specify decimal precision for padding/trimming - */ - precision?: number; - /** * Include a prefix eg. £ */ @@ -90,31 +102,46 @@ export type CurrencyInputProps = Overwrite< step?: number; /** - * Separator between integer part and fractional part of value. Cannot be a number + * Separator between integer part and fractional part of value. * - * Default = "." + * This cannot be a number */ decimalSeparator?: string; /** - * Separator between thousand, million and billion. Cannot be a number + * Separator between thousand, million and billion * - * Default = "," + * This cannot be a number */ groupSeparator?: string; /** - * Disable auto adding separator between values eg. 1000 > 1,000 + * Disable auto adding separator between values eg. 1000 -> 1,000 * * Default = false */ - turnOffSeparators?: boolean; + disableGroupSeparators?: boolean; /** - * Disable abbreviations eg. 1k > 1,000, 2m > 2,000,000 + * Disable abbreviations eg. 1k -> 1,000, 2m -> 2,000,000 * * Default = false */ - turnOffAbbreviations?: boolean; + disableAbbreviations?: boolean; + + /** + * International locale config, examples: + * { locale: 'ja-JP', currency: 'JPY' } + * { locale: 'en-IN', currency: 'INR' } + * + * Any prefix, groupSeparator or decimalSeparator options passed in + * will override Intl Locale config + */ + intlConfig?: IntlConfig; + + /** + * Ref property + */ + ref?: Ref; } >; diff --git a/src/components/__tests__/CurrencyInput-abbreviated.spec.tsx b/src/components/__tests__/CurrencyInput-abbreviated.spec.tsx index b35977b..942b097 100644 --- a/src/components/__tests__/CurrencyInput-abbreviated.spec.tsx +++ b/src/components/__tests__/CurrencyInput-abbreviated.spec.tsx @@ -1,72 +1,88 @@ -import { shallow } from 'enzyme'; import React from 'react'; +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import CurrencyInput from '../CurrencyInput'; -const id = 'validationCustom01'; - -describe(' component > abbreviated', () => { - const onChangeSpy = jest.fn(); +describe(' abbreviated', () => { + const onValueChangeSpy = jest.fn(); beforeEach(() => { jest.clearAllMocks(); }); it('should allow abbreviated values with k', () => { - const view = shallow(); - view.find(`#${id}`).simulate('change', { target: { value: '£1.5k' } }); - expect(onChangeSpy).toBeCalledWith('1500', undefined); + render(); + userEvent.type(screen.getByRole('textbox'), '1.5k'); + + expect(onValueChangeSpy).toBeCalledWith('1500', undefined); - expect(view.update().find(`#${id}`).prop('value')).toBe('£1,500'); + expect(screen.getByRole('textbox')).toHaveValue('£1,500'); }); it('should allow abbreviated values with m', () => { - const view = shallow(); - view.find(`#${id}`).simulate('change', { target: { value: '£2.123M' } }); - expect(onChangeSpy).toBeCalledWith('2123000', undefined); + render(); + userEvent.type(screen.getByRole('textbox'), '2.123M'); + userEvent.tab(); - expect(view.update().find(`#${id}`).prop('value')).toBe('£2,123,000'); + expect(screen.getByRole('textbox')).toHaveValue('£2,123,000'); + + expect(onValueChangeSpy).toBeCalledWith('2123000', undefined); }); it('should allow abbreviated values with b', () => { - const view = shallow(); - view.find(`#${id}`).simulate('change', { target: { value: '£1.599B' } }); - expect(onChangeSpy).toBeCalledWith('1599000000', undefined); + render(); + userEvent.type(screen.getByRole('textbox'), '1.599B'); + + expect(onValueChangeSpy).toBeCalledWith('1599000000', undefined); - expect(view.update().find(`#${id}`).prop('value')).toBe('£1,599,000,000'); + expect(screen.getByRole('textbox')).toHaveValue('£1,599,000,000'); }); it('should not abbreviate any other letters', () => { - const view = shallow(); - view.find(`#${id}`).simulate('change', { target: { value: '£1.5e' } }); - expect(onChangeSpy).toBeCalledWith('1.5', undefined); + render(); + userEvent.type(screen.getByRole('textbox'), '1.5e'); - expect(view.update().find(`#${id}`).prop('value')).toBe('£1.5'); + expect(onValueChangeSpy).toBeCalledWith('1.5', undefined); + + expect(screen.getByRole('textbox')).toHaveValue('£1.5'); }); it('should not allow abbreviation without number', () => { - const view = shallow(); - view.find(`#${id}`).simulate('change', { target: { value: 'k' } }); - expect(onChangeSpy).toBeCalledWith(undefined, undefined); - expect(view.update().find(`#${id}`).prop('value')).toBe(''); - - view.find(`#${id}`).simulate('change', { target: { value: 'M' } }); - expect(onChangeSpy).toBeCalledWith(undefined, undefined); - expect(view.update().find(`#${id}`).prop('value')).toBe(''); + render(); + userEvent.type(screen.getByRole('textbox'), 'k'); + + expect(onValueChangeSpy).toBeCalledWith(undefined, undefined); + + expect(screen.getByRole('textbox')).toHaveValue(''); + + userEvent.type(screen.getByRole('textbox'), 'M'); + + expect(onValueChangeSpy).toBeCalledWith(undefined, undefined); + + expect(screen.getByRole('textbox')).toHaveValue(''); }); - describe('turnOffAbbreviations', () => { - it('should not allow abbreviations if turnOffAbbreviations is true', () => { - const view = shallow(); - view.find(`#${id}`).simulate('change', { target: { value: '1k' } }); - expect(view.update().find(`#${id}`).prop('value')).toBe('1'); + describe('disableAbbreviations', () => { + it('should not allow abbreviations if disableAbbreviations is true', () => { + render(); + userEvent.type(screen.getByRole('textbox'), '1k'); + + expect(screen.getByRole('textbox')).toHaveValue('1'); + + userEvent.clear(screen.getByRole('textbox')); + userEvent.type(screen.getByRole('textbox'), '23m'); + + expect(onValueChangeSpy).toBeCalledWith('23', undefined); + + expect(screen.getByRole('textbox')).toHaveValue('23'); + + userEvent.clear(screen.getByRole('textbox')); + userEvent.type(screen.getByRole('textbox'), '55b'); - view.find(`#${id}`).simulate('change', { target: { value: '23m' } }); - expect(onChangeSpy).toBeCalledWith('23', undefined); - expect(view.update().find(`#${id}`).prop('value')).toBe('23'); + expect(onValueChangeSpy).toBeCalledWith('55', undefined); - view.find(`#${id}`).simulate('change', { target: { value: '55b' } }); - expect(onChangeSpy).toBeCalledWith('55', undefined); - expect(view.update().find(`#${id}`).prop('value')).toBe('55'); + expect(screen.getByRole('textbox')).toHaveValue('55'); }); }); }); diff --git a/src/components/__tests__/CurrencyInput-backspace.spec.tsx b/src/components/__tests__/CurrencyInput-backspace.spec.tsx new file mode 100644 index 0000000..683b3d8 --- /dev/null +++ b/src/components/__tests__/CurrencyInput-backspace.spec.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import CurrencyInput from '../CurrencyInput'; + +describe(' backspace', () => { + const onValueChangeSpy = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should handle backspace with suffix', () => { + render( + + ); + + expect(screen.getByRole('textbox')).toHaveValue('12,34\xa0€'); + + userEvent.type(screen.getByRole('textbox'), '56'); + + expect(screen.getByRole('textbox')).toHaveValue('12,3456\xa0€'); + + userEvent.type(screen.getByRole('textbox'), '{backspace}{backspace}{backspace}'); + + expect(screen.getByRole('textbox')).toHaveValue('12,3\xa0€'); + }); +}); diff --git a/src/components/__tests__/CurrencyInput-customInput.spec.tsx b/src/components/__tests__/CurrencyInput-customInput.spec.tsx new file mode 100644 index 0000000..72f2a91 --- /dev/null +++ b/src/components/__tests__/CurrencyInput-customInput.spec.tsx @@ -0,0 +1,20 @@ +import React, { forwardRef } from 'react'; +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import CurrencyInput from '../CurrencyInput'; + +describe(' customInput', () => { + it('should render with customInput', () => { + const customInput = forwardRef( + (props: React.InputHTMLAttributes, ref) => { + return ; + } + ); + + customInput.displayName = 'CustomInput'; + + render(); + + expect(screen.getByRole('textbox')).toHaveValue('1,234'); + }); +}); diff --git a/src/components/__tests__/CurrencyInput-decimals.spec.tsx b/src/components/__tests__/CurrencyInput-decimals.spec.tsx index 28a15a2..c9483b6 100644 --- a/src/components/__tests__/CurrencyInput-decimals.spec.tsx +++ b/src/components/__tests__/CurrencyInput-decimals.spec.tsx @@ -1,53 +1,46 @@ -import { shallow } from 'enzyme'; import React from 'react'; +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import CurrencyInput from '../CurrencyInput'; -const id = 'validationCustom01'; - -describe(' component > decimals', () => { - const onChangeSpy = jest.fn(); +describe(' decimals', () => { + const onValueChangeSpy = jest.fn(); beforeEach(() => { jest.clearAllMocks(); }); it('should allow value with decimals if allowDecimals is true', () => { - const view = shallow( - - ); - view.find(`#${id}`).simulate('change', { target: { value: '£1,234.56' } }); - expect(onChangeSpy).toBeCalledWith('1234.56', undefined); - - const updatedView = view.update(); - expect(updatedView.find(`#${id}`).prop('value')).toBe('£1,234.56'); + render(); + userEvent.type(screen.getByRole('textbox'), '1,234.56'); + + expect(onValueChangeSpy).toBeCalledWith('1234.56', undefined); + + expect(screen.getByRole('textbox')).toHaveValue('£1,234.56'); }); it('should disallow value with decimals if allowDecimals is false', () => { - const view = shallow( - - ); - view.find(`#${id}`).simulate('change', { target: { value: '£1,234.56' } }); - expect(onChangeSpy).toBeCalledWith('1234', undefined); - - const updatedView = view.update(); - expect(updatedView.find(`#${id}`).prop('value')).toBe('£1,234'); + render(); + userEvent.type(screen.getByRole('textbox'), '1,234.56'); + + expect(onValueChangeSpy).toBeCalledWith('123456', undefined); + + expect(screen.getByRole('textbox')).toHaveValue('£123,456'); }); it('should limit decimals to decimalsLimit length', () => { - const view = shallow( - - ); - view.find(`#${id}`).simulate('change', { target: { value: '£1,234.56789' } }); - expect(onChangeSpy).toBeCalledWith('1234.567', undefined); - - const updatedView = view.update(); - expect(updatedView.find(`#${id}`).prop('value')).toBe('£1,234.567'); + render(); + userEvent.type(screen.getByRole('textbox'), '1,234.56789'); + + expect(onValueChangeSpy).toBeCalledWith('1234.567', undefined); + + expect(screen.getByRole('textbox')).toHaveValue('£1,234.567'); }); it('should be disabled if disabled prop is true', () => { - const view = shallow( - - ); - expect(view.find(`#${id}`).prop('disabled')).toBe(true); + render(); + + expect(screen.getByRole('textbox')).toBeDisabled(); }); }); diff --git a/src/components/__tests__/CurrencyInput-fixedDecimalLength.spec.tsx b/src/components/__tests__/CurrencyInput-fixedDecimalLength.spec.tsx index 42ea3dd..898c8de 100644 --- a/src/components/__tests__/CurrencyInput-fixedDecimalLength.spec.tsx +++ b/src/components/__tests__/CurrencyInput-fixedDecimalLength.spec.tsx @@ -1,11 +1,11 @@ -import { shallow } from 'enzyme'; import React from 'react'; +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import CurrencyInput from '../CurrencyInput'; -const id = 'validationCustom01'; - -describe(' component > fixedDecimalLength', () => { - const onBlurValueSpy = jest.fn(); +describe(' fixedDecimalLength', () => { + const onValueChangeSpy = jest.fn(); beforeEach(() => { jest.clearAllMocks(); @@ -13,48 +13,45 @@ describe(' component > fixedDecimalLength', () => { describe('fixedDecimalLength', () => { it('should convert value on blur if fixedDecimalLength specified', () => { - const view = shallow( + render( ); - const input = view.find(`#${id}`); - expect(input.prop('value')).toBe('$123'); + expect(screen.getByRole('textbox')).toHaveValue('$123'); + + screen.getByRole('textbox').focus(); + userEvent.tab(); - view.find(`#${id}`).simulate('blur', { target: { value: '$123' } }); - expect(onBlurValueSpy).toBeCalledWith('1.230', undefined); + expect(onValueChangeSpy).toBeCalledWith('1.230', undefined); - const updatedView = view.update(); - expect(updatedView.find(`#${id}`).prop('value')).toBe('$1.230'); + expect(screen.getByRole('textbox')).toHaveValue('$1.230'); }); - it('should work with precision and decimalSeparator', () => { - const view = shallow( + it('should work with decimalScale and decimalSeparator', () => { + render( ); - const input = view.find(`#${id}`); - expect(input.prop('value')).toBe('$123'); + expect(screen.getByRole('textbox')).toHaveValue('$123'); + + screen.getByRole('textbox').focus(); + userEvent.tab(); - view.find(`#${id}`).simulate('blur', { target: { value: '$123' } }); - expect(onBlurValueSpy).toBeCalledWith('1,23', undefined); + expect(onValueChangeSpy).toBeCalledWith('1,23', undefined); - const updatedView = view.update(); - expect(updatedView.find(`#${id}`).prop('value')).toBe('$1,23'); + expect(screen.getByRole('textbox')).toHaveValue('$1,23'); }); }); }); diff --git a/src/components/__tests__/CurrencyInput-handleKeyDown.spec.tsx b/src/components/__tests__/CurrencyInput-handleKeyDown.spec.tsx index e26fe1b..e1db46c 100644 --- a/src/components/__tests__/CurrencyInput-handleKeyDown.spec.tsx +++ b/src/components/__tests__/CurrencyInput-handleKeyDown.spec.tsx @@ -1,155 +1,147 @@ -import { shallow } from 'enzyme'; import React from 'react'; +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import CurrencyInput from '../CurrencyInput'; -const id = 'validationCustom01'; - -describe(' component > handleKeyDown', () => { - const onChangeSpy = jest.fn(); +describe(' handleKeyDown', () => { + const onValueChangeSpy = jest.fn(); beforeEach(() => { jest.clearAllMocks(); }); it('should not change value if no step prop', () => { - const view = shallow( - - ); + render(); // Arrow up - view.find(`#${id}`).simulate('keyDown', { key: 'ArrowUp' }); - expect(onChangeSpy).not.toBeCalled(); - expect(view.update().find(`#${id}`).prop('value')).toBe('£100'); + userEvent.type(screen.getByRole('textbox'), '{arrowup}'); + expect(onValueChangeSpy).not.toBeCalled(); + expect(screen.getByRole('textbox')).toHaveValue('£100'); // Arrow down - view.find(`#${id}`).simulate('keyDown', { key: 'ArrowDown' }); - expect(onChangeSpy).not.toBeCalled(); - expect(view.update().find(`#${id}`).prop('value')).toBe('£100'); + userEvent.type(screen.getByRole('textbox'), '{arrowdown}'); + expect(onValueChangeSpy).not.toBeCalled(); + expect(screen.getByRole('textbox')).toHaveValue('£100'); }); it('should handle negative step', () => { - const view = shallow( - + render( + ); - view.find(`#${id}`).simulate('keyDown', { key: 'ArrowUp' }); - expect(onChangeSpy).toHaveBeenCalledWith('98', undefined); - expect(view.update().find(`#${id}`).prop('value')).toBe('£98'); + userEvent.type(screen.getByRole('textbox'), '{arrowup}'); + expect(onValueChangeSpy).toHaveBeenCalledWith('98', undefined); + expect(screen.getByRole('textbox')).toHaveValue('£98'); - view.find(`#${id}`).simulate('keyDown', { key: 'ArrowDown' }); - expect(onChangeSpy).toHaveBeenCalledWith('100', undefined); - expect(view.update().find(`#${id}`).prop('value')).toBe('£100'); + userEvent.type(screen.getByRole('textbox'), '{arrowdown}'); + expect(onValueChangeSpy).toHaveBeenCalledWith('100', undefined); + expect(screen.getByRole('textbox')).toHaveValue('£100'); }); describe('without value ie. default 0', () => { it('should handle arrow down key', () => { - const view = shallow(); + render(); - view.find(`#${id}`).simulate('keyDown', { key: 'ArrowDown' }); - expect(onChangeSpy).toBeCalledWith('-1', undefined); - expect(view.update().find(`#${id}`).prop('value')).toBe('-£1'); + userEvent.type(screen.getByRole('textbox'), '{arrowdown}'); + expect(onValueChangeSpy).toBeCalledWith('-1', undefined); + expect(screen.getByRole('textbox')).toHaveValue('-£1'); }); it('should handle arrow down key', () => { - const view = shallow(); + render(); - view.find(`#${id}`).simulate('keyDown', { key: 'ArrowUp' }); - expect(onChangeSpy).toBeCalledWith('1', undefined); - expect(view.update().find(`#${id}`).prop('value')).toBe('£1'); + userEvent.type(screen.getByRole('textbox'), '{arrowup}'); + expect(onValueChangeSpy).toBeCalledWith('1', undefined); + expect(screen.getByRole('textbox')).toHaveValue('£1'); }); }); describe('with value 99 and step 1.25', () => { it('should handle arrow down key', () => { - const view = shallow( - - ); + render(); - view.find(`#${id}`).simulate('keyDown', { key: 'ArrowDown' }); - expect(onChangeSpy).toHaveBeenCalledWith('97.75', undefined); + userEvent.type(screen.getByRole('textbox'), '{arrowdown}'); + expect(onValueChangeSpy).toHaveBeenCalledWith('97.75', undefined); }); it('should handle arrow up key', () => { - const view = shallow( - - ); + render(); - view.find(`#${id}`).simulate('keyDown', { key: 'ArrowUp' }); - expect(onChangeSpy).toHaveBeenCalledWith('100.25', undefined); + userEvent.type(screen.getByRole('textbox'), '{arrowup}'); + expect(onValueChangeSpy).toHaveBeenCalledWith('100.25', undefined); }); }); describe('with defaultValue 100 and step 5.5', () => { it('should handle arrow down key', () => { - const view = shallow( - + render( + ); - view.find(`#${id}`).simulate('keyDown', { key: 'ArrowDown' }); - expect(onChangeSpy).toBeCalledWith('94.5', undefined); - expect(view.update().find(`#${id}`).prop('value')).toBe('£94.5'); + userEvent.type(screen.getByRole('textbox'), '{arrowdown}'); + expect(onValueChangeSpy).toBeCalledWith('94.5', undefined); + expect(screen.getByRole('textbox')).toHaveValue('£94.5'); - view.find(`#${id}`).simulate('keyDown', { key: 'ArrowDown' }); - expect(onChangeSpy).toBeCalledWith('89', undefined); - expect(view.update().find(`#${id}`).prop('value')).toBe('£89'); + userEvent.type(screen.getByRole('textbox'), '{arrowdown}'); + expect(onValueChangeSpy).toBeCalledWith('89', undefined); + expect(screen.getByRole('textbox')).toHaveValue('£89'); }); it('should handle arrow up key', () => { - const view = shallow( - + render( + ); - view.find(`#${id}`).simulate('keyDown', { key: 'ArrowUp' }); - expect(onChangeSpy).toBeCalledWith('105.5', undefined); - expect(view.update().find(`#${id}`).prop('value')).toBe('£105.5'); + userEvent.type(screen.getByRole('textbox'), '{arrowup}'); + expect(onValueChangeSpy).toBeCalledWith('105.5', undefined); + expect(screen.getByRole('textbox')).toHaveValue('£105.5'); - view.find(`#${id}`).simulate('keyDown', { key: 'ArrowUp' }); - expect(onChangeSpy).toBeCalledWith('111', undefined); - expect(view.update().find(`#${id}`).prop('value')).toBe('£111'); + userEvent.type(screen.getByRole('textbox'), '{arrowup}'); + expect(onValueChangeSpy).toBeCalledWith('111', undefined); + expect(screen.getByRole('textbox')).toHaveValue('£111'); }); }); describe('with max length 2', () => { it('should handle negative value', () => { - const view = shallow( + render( ); - view.find(`#${id}`).simulate('keyDown', { key: 'ArrowDown' }); - expect(onChangeSpy).not.toBeCalled(); - expect(view.update().find(`#${id}`).prop('value')).toBe('-£99'); + userEvent.type(screen.getByRole('textbox'), '{arrowdown}'); + expect(onValueChangeSpy).not.toBeCalled(); + expect(screen.getByRole('textbox')).toHaveValue('-£99'); - view.find(`#${id}`).simulate('keyDown', { key: 'ArrowUp' }); - expect(onChangeSpy).toHaveBeenCalledWith('-98', undefined); - expect(view.update().find(`#${id}`).prop('value')).toBe('-£98'); + userEvent.type(screen.getByRole('textbox'), '{arrowup}'); + expect(onValueChangeSpy).toHaveBeenCalledWith('-98', undefined); + expect(screen.getByRole('textbox')).toHaveValue('-£98'); }); it('should handle positive value', () => { - const view = shallow( + render( ); - view.find(`#${id}`).simulate('keyDown', { key: 'ArrowUp' }); - expect(onChangeSpy).not.toBeCalled(); - expect(view.update().find(`#${id}`).prop('value')).toBe('£99'); + userEvent.type(screen.getByRole('textbox'), '{arrowup}'); + expect(onValueChangeSpy).not.toBeCalled(); + expect(screen.getByRole('textbox')).toHaveValue('£99'); - view.find(`#${id}`).simulate('keyDown', { key: 'ArrowDown' }); - expect(onChangeSpy).toHaveBeenCalledWith('98', undefined); - expect(view.update().find(`#${id}`).prop('value')).toBe('£98'); + userEvent.type(screen.getByRole('textbox'), '{arrowdown}'); + expect(onValueChangeSpy).toHaveBeenCalledWith('98', undefined); + expect(screen.getByRole('textbox')).toHaveValue('£98'); }); }); }); diff --git a/src/components/__tests__/CurrencyInput-intl-config.spec.tsx b/src/components/__tests__/CurrencyInput-intl-config.spec.tsx new file mode 100644 index 0000000..79a1225 --- /dev/null +++ b/src/components/__tests__/CurrencyInput-intl-config.spec.tsx @@ -0,0 +1,111 @@ +import React from 'react'; +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import CurrencyInput from '../CurrencyInput'; + +const id = 'validationCustom01'; + +describe(' intlConfig', () => { + it('should use intl config settings (en-US, USD)', () => { + render( + + ); + + expect(screen.getByRole('textbox')).toHaveValue('$123,456,789'); + }); + + it('should use intl config settings (en-IN, INR)', () => { + render( + + ); + + expect(screen.getByRole('textbox')).toHaveValue('₹5,00,000'); + }); + + it('should use intl config settings (ja-JP, JPY)', () => { + render( + + ); + + expect(screen.getByRole('textbox')).toHaveValue('¥123456'); + + userEvent.type(screen.getByRole('textbox'), '{backspace}{backspace}{backspace}'); + + expect(screen.getByRole('textbox')).toHaveValue('¥123'); + + userEvent.type(screen.getByRole('textbox'), '.99'); + + expect(screen.getByRole('textbox')).toHaveValue('¥12,399'); + }); + + it('should override locale currency symbol if prefix passed in', () => { + render( + + ); + + expect(screen.getByRole('textbox')).toHaveValue('£100'); + }); + + it('should override locale group separator if groupSeparator passed in', () => { + render( + + ); + + expect(screen.getByRole('textbox')).toHaveValue('$123/456/789.99'); + }); + + it('should override locale decimal separator if decimalSeparator passed in', () => { + render( + + ); + + expect(screen.getByRole('textbox')).toHaveValue('$123,456-789'); + }); + + describe('onValueChange', () => { + const onValueChangeSpy = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should handle onValueChange with intl config settings (en-IN, INR)', () => { + render( + + ); + + expect(screen.getByRole('textbox')).toHaveValue(''); + + userEvent.type(screen.getByRole('textbox'), '₹12,34,567'); + + expect(onValueChangeSpy).toBeCalledWith('1234567', undefined); + + expect(screen.getByRole('textbox')).toHaveValue('₹12,34,567'); + }); + }); +}); diff --git a/src/components/__tests__/CurrencyInput-maxLength.spec.tsx b/src/components/__tests__/CurrencyInput-maxLength.spec.tsx index 553263b..b2ebae4 100644 --- a/src/components/__tests__/CurrencyInput-maxLength.spec.tsx +++ b/src/components/__tests__/CurrencyInput-maxLength.spec.tsx @@ -1,59 +1,47 @@ -import { shallow } from 'enzyme'; import React from 'react'; +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import CurrencyInput from '../CurrencyInput'; -const id = 'validationCustom01'; - -describe(' component > maxLength', () => { - const onChangeSpy = jest.fn(); +describe(' maxLength', () => { + const onValueChangeSpy = jest.fn(); beforeEach(() => { jest.clearAllMocks(); }); it('should not allow more values than max length', () => { - const view = shallow( - + render( + ); - const input = view.find(`#${id}`); - expect(input.prop('value')).toBe('£123'); + expect(screen.getByRole('textbox')).toHaveValue('£123'); - input.simulate('change', { target: { value: '£1234' } }); - expect(onChangeSpy).not.toBeCalled(); + userEvent.type(screen.getByRole('textbox'), '4'); + expect(onValueChangeSpy).not.toBeCalled(); - const updatedView = view.update(); - expect(updatedView.find(`#${id}`).prop('value')).toBe('£123'); + expect(screen.getByRole('textbox')).toHaveValue('£123'); }); it('should apply max length rule to negative value', () => { - const view = shallow( + render( ); - const input = view.find(`#${id}`); - expect(input.prop('value')).toBe('-£123'); + expect(screen.getByRole('textbox')).toHaveValue('-£123'); - input.simulate('change', { target: { value: '-£1234' } }); - expect(onChangeSpy).not.toBeCalled(); - expect(view.update().find(`#${id}`).prop('value')).toBe('-£123'); + userEvent.type(screen.getByRole('textbox'), '4'); + expect(onValueChangeSpy).not.toBeCalled(); + expect(screen.getByRole('textbox')).toHaveValue('-£123'); - input.simulate('change', { target: { value: '-£125' } }); - expect(onChangeSpy).toHaveBeenCalledWith('-125', ''); - expect(view.update().find(`#${id}`).prop('value')).toBe('-£125'); + userEvent.type(screen.getByRole('textbox'), '{backspace}5'); + expect(onValueChangeSpy).toHaveBeenCalledWith('-125', undefined); + expect(screen.getByRole('textbox')).toHaveValue('-£125'); }); }); diff --git a/src/components/__tests__/CurrencyInput-negative.spec.tsx b/src/components/__tests__/CurrencyInput-negative.spec.tsx index 6dd9eb7..8b4fb07 100644 --- a/src/components/__tests__/CurrencyInput-negative.spec.tsx +++ b/src/components/__tests__/CurrencyInput-negative.spec.tsx @@ -1,79 +1,97 @@ -import { shallow } from 'enzyme'; import React from 'react'; +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import CurrencyInput from '../CurrencyInput'; const id = 'validationCustom01'; -describe(' component > negative value', () => { - const onChangeSpy = jest.fn(); +describe(' negative value', () => { + const onValueChangeSpy = jest.fn(); beforeEach(() => { jest.clearAllMocks(); }); it('should handle negative value input', () => { - const view = shallow( - + render( + ); - const input = view.find(`#${id}`); - expect(input.prop('value')).toBe('$123'); + expect(screen.getByRole('textbox')).toHaveValue('$123'); - input.simulate('change', { target: { value: '-$1234' } }); - expect(onChangeSpy).toBeCalledWith('-1234', undefined); + userEvent.clear(screen.getByRole('textbox')); + userEvent.type(screen.getByRole('textbox'), '-1234'); + expect(onValueChangeSpy).toBeCalledWith('-1234', undefined); - const updatedView = view.update(); - expect(updatedView.find(`#${id}`).prop('value')).toBe('-$1,234'); + expect(screen.getByRole('textbox')).toHaveValue('-$1,234'); }); - it('should call onChange with undefined and keep "-" sign as state value', () => { - const view = shallow( - + it('should call onValueChange with undefined and keep "-" sign as state value', () => { + render( + ); - const input = view.find(`#${id}`); - expect(input.prop('value')).toBe('$123'); + expect(screen.getByRole('textbox')).toHaveValue('$123'); - input.simulate('change', { target: { value: '-' } }); - expect(onChangeSpy).toBeCalledWith(undefined, undefined); + userEvent.clear(screen.getByRole('textbox')); + userEvent.type(screen.getByRole('textbox'), '-'); + expect(onValueChangeSpy).toBeCalledWith(undefined, undefined); - const updatedView = view.update(); - expect(updatedView.find(`#${id}`).prop('value')).toBe('-'); + expect(screen.getByRole('textbox')).toHaveValue('-'); }); it('should not call onBlur if only negative sign and clears value', () => { - const view = shallow( - + render( + ); - const input = view.find(`#${id}`); - expect(input.prop('value')).toBe('$123'); + expect(screen.getByRole('textbox')).toHaveValue('$123'); - input.simulate('blur', { target: { value: '-' } }); - expect(onChangeSpy).not.toBeCalled(); + userEvent.type(screen.getByRole('textbox'), '{backspace}{backspace}{backspace}{backspace}-'); + expect(screen.getByRole('textbox')).toHaveValue('-'); + expect(onValueChangeSpy).toBeCalledTimes(4); + expect(onValueChangeSpy).toHaveBeenLastCalledWith(undefined, undefined); - const updatedView = view.update(); - expect(updatedView.find(`#${id}`).prop('value')).toBe(''); + userEvent.tab(); + expect(screen.getByRole('textbox')).toHaveValue(''); }); it('should not allow negative value if allowNegativeValue is false', () => { - const view = shallow( + render( ); - const input = view.find(`#${id}`); - expect(input.prop('value')).toBe('$123'); + expect(screen.getByRole('textbox')).toHaveValue('$123'); - input.simulate('change', { target: { value: '-$1234' } }); - expect(onChangeSpy).toBeCalledWith('1234', undefined); + userEvent.clear(screen.getByRole('textbox')); + userEvent.type(screen.getByRole('textbox'), '-1234'); + expect(onValueChangeSpy).toBeCalledWith('1234', undefined); - const updatedView = view.update(); - expect(updatedView.find(`#${id}`).prop('value')).toBe('$1,234'); + expect(screen.getByRole('textbox')).toHaveValue('$1,234'); }); }); diff --git a/src/components/__tests__/CurrencyInput-no-locale.spec.tsx b/src/components/__tests__/CurrencyInput-no-locale.spec.tsx new file mode 100644 index 0000000..45a77b9 --- /dev/null +++ b/src/components/__tests__/CurrencyInput-no-locale.spec.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import CurrencyInput from '../CurrencyInput'; +import { getLocaleConfig } from '../utils'; + +jest.mock('../utils/getLocaleConfig', () => ({ + getLocaleConfig: jest.fn().mockReturnValue({ groupSeparator: ',', decimalSeparator: '.' }), +})); + +describe(' no locale', () => { + it('should have empty string for groupSeparator and decimalSeparator if not passed in and cannot find default locale', () => { + (getLocaleConfig as jest.Mock).mockReturnValue({ groupSeparator: '', decimalSeparator: '' }); + render(); + expect(screen.getByRole('textbox')).toHaveValue('123456789'); + }); +}); diff --git a/src/components/__tests__/CurrencyInput-onBlur.spec.tsx b/src/components/__tests__/CurrencyInput-onBlur.spec.tsx new file mode 100644 index 0000000..6e56380 --- /dev/null +++ b/src/components/__tests__/CurrencyInput-onBlur.spec.tsx @@ -0,0 +1,71 @@ +import React from 'react'; +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import CurrencyInput from '../CurrencyInput'; + +const name = 'inputName'; + +describe(' onBlur', () => { + const onBlurSpy = jest.fn(); + const onValueChangeSpy = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should call onBlur and onValueChange', () => { + render( + + ); + + userEvent.type(screen.getByRole('textbox'), '123'); + userEvent.tab(); + + expect(onBlurSpy).toBeCalled(); + + expect(onValueChangeSpy).toBeCalledWith('123.00', name); + + expect(screen.getByRole('textbox')).toHaveValue('$123.00'); + }); + + it('should call onBlur for 0', () => { + render(); + + userEvent.type(screen.getByRole('textbox'), '0'); + userEvent.tab(); + + expect(onBlurSpy).toBeCalled(); + + expect(screen.getByRole('textbox')).toHaveValue('$0'); + }); + + it('should call onBlur for empty value', () => { + render(); + + userEvent.type(screen.getByRole('textbox'), ''); + userEvent.tab(); + + expect(onBlurSpy).toBeCalled(); + + expect(screen.getByRole('textbox')).toHaveValue(''); + }); + + it('should call onBlur for "-" char', () => { + render(); + + userEvent.type(screen.getByRole('textbox'), '-'); + userEvent.tab(); + + expect(onBlurSpy).toBeCalled(); + + expect(screen.getByRole('textbox')).toHaveValue(''); + }); +}); diff --git a/src/components/__tests__/CurrencyInput-onBlurValue.spec.tsx b/src/components/__tests__/CurrencyInput-onBlurValue.spec.tsx deleted file mode 100644 index 27f46b1..0000000 --- a/src/components/__tests__/CurrencyInput-onBlurValue.spec.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { shallow } from 'enzyme'; -import React from 'react'; -import CurrencyInput from '../CurrencyInput'; - -const id = 'validationCustom01'; -const name = 'inputName'; - -describe(' component > onBlurValue', () => { - const onBlurValueSpy = jest.fn(); - const onChangeSpy = jest.fn(); - - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('should call onBlurValue and onChange', () => { - const view = shallow( - - ); - view.find(`#${id}`).simulate('blur', { target: { value: '123' } }); - expect(onBlurValueSpy).toBeCalledWith('123.00', name); - expect(onChangeSpy).toBeCalledWith('123.00', name); - - const updatedView = view.update(); - expect(updatedView.find(`#${id}`).prop('value')).toBe('$123.00'); - }); - - it('should call onBlurValue for 0', () => { - const view = shallow( - - ); - view.find(`#${id}`).simulate('blur', { target: { value: '0' } }); - expect(onBlurValueSpy).toBeCalledWith('0', name); - - const updatedView = view.update(); - expect(updatedView.find(`#${id}`).prop('value')).toBe('$0'); - }); - - it('should call onBlurValue for empty value', () => { - const view = shallow( - - ); - view.find(`#${id}`).simulate('blur', { target: { value: '' } }); - expect(onBlurValueSpy).toBeCalledWith(undefined, name); - - const updatedView = view.update(); - expect(updatedView.find(`#${id}`).prop('value')).toBe(''); - }); - - it('should call onBlurValue for "-" char', () => { - const view = shallow( - - ); - view.find(`#${id}`).simulate('blur', { target: { value: '-' } }); - expect(onBlurValueSpy).toBeCalledWith(undefined, name); - - const updatedView = view.update(); - expect(updatedView.find(`#${id}`).prop('value')).toBe(''); - }); - - it('should callback name as second parameter if name prop provided', () => { - const view = shallow( - - ); - view.find(`#${id}`).simulate('blur', { target: { value: '$123' } }); - expect(onBlurValueSpy).toBeCalledWith('123', name); - }); - - it('should not allow invalid characters', () => { - const view = shallow( - - ); - view.find(`#${id}`).simulate('blur', { target: { value: 'hello' } }); - expect(onBlurValueSpy).toBeCalledWith(undefined, name); - - const updatedView = view.update(); - expect(updatedView.find(`#${id}`).prop('value')).toBe(''); - }); - - it('should ignore invalid characters', () => { - const view = shallow( - - ); - view.find(`#${id}`).simulate('blur', { target: { value: '$123hello' } }); - expect(onBlurValueSpy).toBeCalledWith('123', name); - - const updatedView = view.update(); - expect(updatedView.find(`#${id}`).prop('value')).toBe('$123'); - }); -}); diff --git a/src/components/__tests__/CurrencyInput-precision.spec.tsx b/src/components/__tests__/CurrencyInput-precision.spec.tsx index e9f898a..74213e9 100644 --- a/src/components/__tests__/CurrencyInput-precision.spec.tsx +++ b/src/components/__tests__/CurrencyInput-precision.spec.tsx @@ -1,35 +1,43 @@ -import { shallow } from 'enzyme'; import React from 'react'; +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import CurrencyInput from '../CurrencyInput'; -const id = 'validationCustom01'; - -describe(' component > precision', () => { - const onBlurValueSpy = jest.fn(); +describe(' decimalScale', () => { + const onValueChangeSpy = jest.fn(); beforeEach(() => { jest.clearAllMocks(); }); - it('should pad to precision of 5 on blur', () => { - const view = shallow( - - ); - view.find(`#${id}`).simulate('blur', { target: { value: '£1.5' } }); - expect(onBlurValueSpy).toBeCalledWith('1.50000', undefined); + it('should pad to decimalScale of 5 on blur', () => { + render(); + + userEvent.type(screen.getByRole('textbox'), '1.5'); + userEvent.tab(); + expect(onValueChangeSpy).toBeCalledWith('1.50000', undefined); - const updatedView = view.update(); - expect(updatedView.find(`#${id}`).prop('value')).toBe('£1.50000'); + expect(screen.getByRole('textbox')).toHaveValue('£1.50000'); }); - it('should pad to precision of 2 on blur', () => { - const view = shallow( - + it('should pad to decimalScale of 2 on blur', () => { + const onBlurSpy = jest.fn(); + render( + ); - view.find(`#${id}`).simulate('blur', { target: { value: '£1' } }); - expect(onBlurValueSpy).toBeCalledWith('1.00', undefined); - const updatedView = view.update(); - expect(updatedView.find(`#${id}`).prop('value')).toBe('£1.00'); + userEvent.type(screen.getByRole('textbox'), '1'); + userEvent.tab(); + expect(onBlurSpy).toBeCalled(); + + expect(onValueChangeSpy).toBeCalledWith('1.00', undefined); + + expect(screen.getByRole('textbox')).toHaveValue('£1.00'); }); }); diff --git a/src/components/__tests__/CurrencyInput-separators.spec.tsx b/src/components/__tests__/CurrencyInput-separators.spec.tsx index 300e6f2..4c97cb9 100644 --- a/src/components/__tests__/CurrencyInput-separators.spec.tsx +++ b/src/components/__tests__/CurrencyInput-separators.spec.tsx @@ -1,86 +1,106 @@ -import { shallow } from 'enzyme'; import React from 'react'; +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import CurrencyInput from '../CurrencyInput'; -const id = 'validationCustom01'; const name = 'inputName'; -describe(' component > separators', () => { - const onChangeSpy = jest.fn(); +describe(' separators', () => { + const onValueChangeSpy = jest.fn(); beforeEach(() => { jest.clearAllMocks(); }); it('should not include separator if turned off', () => { - const view = shallow( + render( ); - const input = view.find(`#${id}`); - expect(input.prop('value')).toBe('£10000'); + expect(screen.getByRole('textbox')).toHaveValue('£10000'); - input.simulate('change', { target: { value: '£123456' } }); - expect(onChangeSpy).toBeCalledWith('123456', name); + userEvent.clear(screen.getByRole('textbox')); + userEvent.type(screen.getByRole('textbox'), '123456'); + expect(onValueChangeSpy).toBeCalledWith('123456', name); - const updatedView = view.update(); - expect(updatedView.find(`#${id}`).prop('value')).toBe('£123456'); + expect(screen.getByRole('textbox')).toHaveValue('£123456'); }); it('should handle decimal and group separators passed in', () => { - const view = shallow( + render( ); - view.find(`#${id}`).simulate('change', { target: { value: '£123.456,33' } }); - expect(onChangeSpy).toBeCalledWith('123456,33', name); - const updatedView = view.update(); - expect(updatedView.find(`#${id}`).prop('value')).toBe('£123.456,33'); - }); + userEvent.clear(screen.getByRole('textbox')); + userEvent.type(screen.getByRole('textbox'), '123456,33'); + expect(onValueChangeSpy).toHaveBeenLastCalledWith('123456,33', name); - it('should throw error if decimalSeparator and groupSeparator are the same', () => { - expect(() => - shallow( - - ) - ).toThrow('decimalSeparator cannot be the same as groupSeparator'); + expect(screen.getByRole('textbox')).toHaveValue('£123.456,33'); }); - it('should throw error if decimalSeparator is a number', () => { - expect(() => - shallow( - - ) - ).toThrow('decimalSeparator cannot be a number'); - }); + describe('throwing errors', () => { + // Ensure console error fails tests by replacing with a function that throws + const { error: originalError } = console; + + beforeAll(() => { + jest.spyOn(console, 'error').mockImplementation((...args) => { + originalError(...args); + }); + }); + + beforeEach(() => { + (console.error as jest.Mock).mockImplementation(jest.fn()); + }); + + afterAll(() => { + (console.error as jest.Mock).mockRestore(); + }); + + afterEach(() => { + (console.error as jest.Mock).mockClear(); + }); + + it('should throw error if decimalSeparator and groupSeparator are the same', () => { + expect(() => + render() + ).toThrow('decimalSeparator cannot be the same as groupSeparator'); + expect(console.error).toHaveBeenCalled(); + }); + + it('should throw error if decimalSeparator is a number', () => { + expect(() => + render() + ).toThrow('decimalSeparator cannot be a number'); + expect(console.error).toHaveBeenCalled(); + }); - it('should throw error if groupSeparator is a number', () => { - expect(() => - shallow( - - ) - ).toThrow('groupSeparator cannot be a number'); + it('should throw error if groupSeparator is a number', () => { + expect(() => + render( + + ) + ).toThrow('groupSeparator cannot be a number'); + expect(console.error).toHaveBeenCalled(); + }); }); }); diff --git a/src/components/__tests__/CurrencyInput.spec.tsx b/src/components/__tests__/CurrencyInput.spec.tsx index e6fc30d..48f0204 100644 --- a/src/components/__tests__/CurrencyInput.spec.tsx +++ b/src/components/__tests__/CurrencyInput.spec.tsx @@ -1,135 +1,163 @@ -import { shallow } from 'enzyme'; import React from 'react'; +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import CurrencyInput from '../CurrencyInput'; -const id = 'validationCustom01'; -const className = 'form-control'; -const placeholder = '£1,000'; -const name = 'inputName'; - -describe(' component', () => { - const onChangeSpy = jest.fn(); +describe('', () => { + const onValueChangeSpy = jest.fn(); beforeEach(() => { jest.clearAllMocks(); }); it('Renders without error', () => { - const view = shallow( - + render( + ); - const input = view.find(`#${id}`); + const input = screen.getByRole('textbox'); expect(input).toMatchSnapshot(); - expect(input.prop('id')).toBe(id); - expect(input.prop('name')).toBe(name); - expect(input.prop('placeholder')).toBe(placeholder); - expect(input.prop('className')).toBe(className); + + expect(input).toHaveValue(''); }); it('Renders with default value', () => { - const view = shallow( - - ); - const input = view.find(`#${id}`); + render(); + const input = screen.getByRole('textbox'); + + expect(input).toMatchSnapshot(); - expect(view.html()).toMatchSnapshot(); - expect(input.prop('id')).toBe(id); - expect(input.prop('value')).toBe('£1,234.56'); - expect(input.prop('className')).toBe(className); + expect(input).toHaveValue('£1,234.56'); }); it('Renders with default value 0', () => { - const view = shallow( - - ); - const input = view.find(`#${id}`); - expect(input.prop('value')).toBe('£0'); + render(); + + expect(screen.getByRole('textbox')).toHaveValue('£0'); }); it('Renders with value prop', () => { - const view = shallow(); - const input = view.find(`#${id}`); + render(); - expect(input.prop('value')).toBe('£49.99'); + expect(screen.getByRole('textbox')).toHaveValue('£49.99'); }); it('Renders with value 0', () => { - const view = shallow(); - const input = view.find(`#${id}`); + render(); - expect(input.prop('value')).toBe('£0'); + expect(screen.getByRole('textbox')).toHaveValue('£0'); }); it('should go to end of string on focus', () => { - const view = shallow(); - view.find(`#${id}`).simulate('focus'); + render(); + userEvent.type(screen.getByRole('textbox'), '{arrowleft}4{arrowright}6'); - const updatedView = view.update(); - expect(updatedView.find(`#${id}`).prop('value')).toBe('123'); - }); - - it('should go to beginning on focus if no value', () => { - const view = shallow(); - view.find(`#${id}`).simulate('focus'); - - const updatedView = view.update(); - expect(updatedView.find(`#${id}`).prop('value')).toBe(''); + expect(screen.getByRole('textbox')).toHaveValue('12,436'); }); it('should allow value change with number', () => { - const view = shallow(); - view.find(`#${id}`).simulate('change', { target: { value: '100' } }); + render(); + userEvent.type(screen.getByRole('textbox'), '100'); - expect(onChangeSpy).toBeCalledWith('100', undefined); + expect(onValueChangeSpy).toBeCalledWith('100', undefined); }); it('should prefix 0 value', () => { - const view = shallow( - - ); - expect(view.find(`#${id}`).prop('value')).toBe('£0'); + render(); + + expect(screen.getByRole('textbox')).toHaveValue('£0'); }); it('should allow 0 value on change', () => { - const view = shallow(); - view.find(`#${id}`).simulate('change', { target: { value: '0' } }); - expect(onChangeSpy).toBeCalledWith('0', name); + render(); + userEvent.type(screen.getByRole('textbox'), '0'); + + expect(onValueChangeSpy).toBeCalledWith('0', undefined); - const updatedView = view.update(); - expect(updatedView.find(`#${id}`).prop('value')).toBe('£0'); + expect(screen.getByRole('textbox')).toHaveValue('£0'); }); it('should allow empty value', () => { - const view = shallow(); - view.find(`#${id}`).simulate('change', { target: { value: '' } }); - expect(onChangeSpy).toBeCalledWith(undefined, name); + render(); + userEvent.clear(screen.getByRole('textbox')); - const updatedView = view.update(); - expect(updatedView.find(`#${id}`).prop('value')).toBe(''); + expect(onValueChangeSpy).toBeCalledWith(undefined, undefined); + + expect(screen.getByRole('textbox')).toHaveValue(''); }); it('should callback name as second parameter if name prop provided', () => { - const view = shallow(); - view.find(`#${id}`).simulate('change', { target: { value: '£123' } }); - expect(onChangeSpy).toBeCalledWith('123', name); + const name = 'inputName'; + render(); + userEvent.type(screen.getByRole('textbox'), '123'); + + expect(onValueChangeSpy).toBeCalledWith('123', name); }); it('should not allow invalid characters', () => { - const view = shallow(); - view.find(`#${id}`).simulate('change', { target: { value: 'hello' } }); - expect(onChangeSpy).toBeCalledWith(undefined, name); + render(); + userEvent.type(screen.getByRole('textbox'), 'hello'); - const updatedView = view.update(); - expect(updatedView.find(`#${id}`).prop('value')).toBe(''); + expect(onValueChangeSpy).toBeCalledWith(undefined, undefined); + + expect(screen.getByRole('textbox')).toHaveValue(''); }); it('should ignore invalid characters', () => { - const view = shallow(); - view.find(`#${id}`).simulate('change', { target: { value: '£123hello' } }); - expect(onChangeSpy).toBeCalledWith('123', name); + render(); + userEvent.type(screen.getByRole('textbox'), '£123hello'); + + expect(onValueChangeSpy).toBeCalledWith('123', undefined); + + expect(screen.getByRole('textbox')).toHaveValue('£123'); + }); + + it('should call onChange', () => { + const onChangeSpy = jest.fn(); + render(); + userEvent.type(screen.getByRole('textbox'), '123'); + + expect(onChangeSpy).toBeCalledTimes(3); + + expect(screen.getByRole('textbox')).toHaveValue('£123'); + }); + + it('should call onBlur', () => { + const onBlurSpy = jest.fn(); + render(); + userEvent.click(screen.getByRole('textbox')); + userEvent.tab(); + + expect(onBlurSpy).toBeCalledTimes(1); + }); + + it('should call onFocus', () => { + const onFocusSpy = jest.fn(); + render(); + userEvent.click(screen.getByRole('textbox')); + + expect(onFocusSpy).toBeCalledTimes(1); + }); + + it('should call onKeyDown', () => { + const onKeyDownSpy = jest.fn(); + render(); + userEvent.type(screen.getByRole('textbox'), '1'); + + expect(onKeyDownSpy).toBeCalledTimes(1); + }); + + it('should call onKeyUp', () => { + const onKeyUpSpy = jest.fn(); + render(); + userEvent.type(screen.getByRole('textbox'), '1'); - const updatedView = view.update(); - expect(updatedView.find(`#${id}`).prop('value')).toBe('£123'); + expect(onKeyUpSpy).toBeCalledTimes(1); }); }); diff --git a/src/components/__tests__/__snapshots__/CurrencyInput.spec.tsx.snap b/src/components/__tests__/__snapshots__/CurrencyInput.spec.tsx.snap index 84e8512..76b1bb3 100644 --- a/src/components/__tests__/__snapshots__/CurrencyInput.spec.tsx.snap +++ b/src/components/__tests__/__snapshots__/CurrencyInput.spec.tsx.snap @@ -1,5 +1,21 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[` component Renders with default value 1`] = `""`; +exports[` Renders with default value 1`] = ` + +`; -exports[` component Renders without error 1`] = `ShallowWrapper {}`; +exports[` Renders without error 1`] = ` + +`; diff --git a/src/components/utils/__tests__/cleanValue.spec.ts b/src/components/utils/__tests__/cleanValue.spec.ts index d609917..82dd8fc 100644 --- a/src/components/utils/__tests__/cleanValue.spec.ts +++ b/src/components/utils/__tests__/cleanValue.spec.ts @@ -35,6 +35,22 @@ describe('cleanValue', () => { ).toEqual('5.5'); }); + it('should remove suffix', () => { + expect( + cleanValue({ + value: '123 €', + }) + ).toEqual('123'); + + expect( + cleanValue({ + groupSeparator: '.', + decimalSeparator: ',', + value: '123.456,99 €', + }) + ).toEqual('123456,99'); + }); + it('should remove extra decimals', () => { expect( cleanValue({ @@ -99,6 +115,19 @@ describe('cleanValue', () => { ).toEqual('-99999.99'); }); + it('should handle negative value with group separator', () => { + expect( + cleanValue({ + value: '-£99-999.99', + decimalSeparator: '.', + groupSeparator: '-', + allowDecimals: true, + decimalsLimit: 2, + prefix: '£', + }) + ).toEqual('-99999.99'); + }); + it('should handle not allow negative value if allowNegativeValue is false', () => { expect( cleanValue({ @@ -150,21 +179,21 @@ describe('cleanValue', () => { expect( cleanValue({ value: 'k', - turnOffAbbreviations: true, + disableAbbreviations: true, }) ).toEqual(''); expect( cleanValue({ value: 'm', - turnOffAbbreviations: true, + disableAbbreviations: true, }) ).toEqual(''); expect( cleanValue({ value: 'b', - turnOffAbbreviations: true, + disableAbbreviations: true, }) ).toEqual(''); }); @@ -174,7 +203,7 @@ describe('cleanValue', () => { cleanValue({ value: '$k', prefix: '$', - turnOffAbbreviations: true, + disableAbbreviations: true, }) ).toEqual(''); @@ -182,37 +211,37 @@ describe('cleanValue', () => { cleanValue({ value: '£m', prefix: '£', - turnOffAbbreviations: true, + disableAbbreviations: true, }) ).toEqual(''); }); - it('should ignore abbreviations if turnOffAbbreviations is true', () => { + it('should ignore abbreviations if disableAbbreviations is true', () => { expect( cleanValue({ value: '1k', - turnOffAbbreviations: true, + disableAbbreviations: true, }) ).toEqual('1'); expect( cleanValue({ value: '-2k', - turnOffAbbreviations: true, + disableAbbreviations: true, }) ).toEqual('-2'); expect( cleanValue({ value: '25.6m', - turnOffAbbreviations: true, + disableAbbreviations: true, }) ).toEqual('25.6'); expect( cleanValue({ value: '9b', - turnOffAbbreviations: true, + disableAbbreviations: true, }) ).toEqual('9'); }); diff --git a/src/components/utils/__tests__/formatValue.spec.ts b/src/components/utils/__tests__/formatValue.spec.ts index 3bcb5d5..4647a5b 100644 --- a/src/components/utils/__tests__/formatValue.spec.ts +++ b/src/components/utils/__tests__/formatValue.spec.ts @@ -9,29 +9,20 @@ describe('formatValue', () => { ).toEqual(''); }); - it('should add separator', () => { + it('should add group separator', () => { expect( formatValue({ value: '1234567', + groupSeparator: '/', }) - ).toEqual('1,234,567'); - }); - - it('should handle period separator', () => { - expect( - formatValue({ - value: '1234567', - decimalSeparator: '.', - groupSeparator: '.', - }) - ).toEqual('1.234.567'); + ).toEqual('1/234/567'); }); it('should handle comma separator for decimals', () => { expect( formatValue({ value: '1234567,89', - decimalSeparator: '.', + decimalSeparator: ',', groupSeparator: '.', }) ).toEqual('1.234.567,89'); @@ -50,29 +41,29 @@ describe('formatValue', () => { it('should handle empty decimal separator', () => { expect( formatValue({ - value: '1234567-89', + value: '123456789', decimalSeparator: '', - groupSeparator: '.', + groupSeparator: '', }) - ).toEqual('1.234.567-89'); + ).toEqual('123456789'); }); - it('should NOT add separator if "turnOffSeparators" is true', () => { + it('should NOT add separator if "disableGroupSeparators" is true', () => { expect( formatValue({ value: '1234567', - turnOffSeparators: true, + disableGroupSeparators: true, }) ).toEqual('1234567'); }); - it('should NOT add separator if "turnOffSeparators" is true even if decimal and group separators specified', () => { + it('should NOT add separator if "disableGroupSeparators" is true even if decimal and group separators specified', () => { expect( formatValue({ value: '1234567', decimalSeparator: '.', groupSeparator: ',', - turnOffSeparators: true, + disableGroupSeparators: true, }) ).toEqual('1234567'); }); @@ -86,18 +77,30 @@ describe('formatValue', () => { ).toEqual('£123'); }); - it('should include "."', () => { + it('should include decimal separator if last char', () => { expect( formatValue({ value: '1234567.', + groupSeparator: ',', + decimalSeparator: '.', }) ).toEqual('1,234,567.'); + + expect( + formatValue({ + value: '1234567,', + groupSeparator: '.', + decimalSeparator: ',', + }) + ).toEqual('1.234.567,'); }); it('should include decimals', () => { expect( formatValue({ value: '1234.567', + groupSeparator: ',', + decimalSeparator: '.', }) ).toEqual('1,234.567'); }); @@ -106,11 +109,28 @@ describe('formatValue', () => { expect( formatValue({ value: '1234567.89', + groupSeparator: ',', + decimalSeparator: '.', prefix: '£', }) ).toEqual('£1,234,567.89'); }); + it('should handle decimals 999999', () => { + expect( + formatValue({ + value: '1.99999', + intlConfig: { locale: 'en-GB', currency: 'GBP' }, + }) + ).toEqual('£1.99999'); + + expect( + formatValue({ + value: '1.99999', + }) + ).toEqual('1.99999'); + }); + it('should handle 0 value', () => { expect( formatValue({ @@ -125,6 +145,8 @@ describe('formatValue', () => { expect( formatValue({ value: '-1234', + groupSeparator: ',', + decimalSeparator: '.', prefix: '£', }) ).toEqual('-£1,234'); @@ -169,4 +191,102 @@ describe('formatValue', () => { }) ).toEqual('-£123-456'); }); + + describe('intlConfig', () => { + it('should handle intlConfig passed in', () => { + expect( + formatValue({ + value: '-500000', + intlConfig: { locale: 'en-IN', currency: 'INR' }, + }) + ).toEqual('-₹5,00,000'); + + expect( + formatValue({ + value: '123456.79', + intlConfig: { locale: 'zh-CN', currency: 'CNY' }, + }) + ).toEqual('¥123,456.79'); + }); + + it('should handle suffix', () => { + expect( + formatValue({ + value: '1', + decimalSeparator: ',', + intlConfig: { locale: 'de-DE', currency: 'EUR' }, + }) + ).toEqual(`1\xa0€`); + }); + + it('should handle suffix ending with decimal separator', () => { + expect( + formatValue({ + value: '1,', + decimalSeparator: ',', + intlConfig: { locale: 'de-DE', currency: 'EUR' }, + }) + ).toEqual(`1,\xa0€`); + }); + + it('should handle suffix ending with decimal separator and decimals', () => { + expect( + formatValue({ + value: '123,00', + decimalSeparator: ',', + intlConfig: { locale: 'de-DE', currency: 'EUR' }, + }) + ).toEqual(`123,00\xa0€`); + + expect( + formatValue({ + value: '123,98', + decimalSeparator: ',', + intlConfig: { locale: 'de-DE', currency: 'EUR' }, + }) + ).toEqual(`123,98\xa0€`); + }); + + it('should override locale if prefix passed in', () => { + expect( + formatValue({ + value: '345', + intlConfig: { locale: 'en-US', currency: 'USD' }, + prefix: '₹', + }) + ).toEqual('₹345'); + }); + + it('should override locale if groupSeparator passed in', () => { + expect( + formatValue({ + value: '-123456', + intlConfig: { locale: 'en-IN', currency: 'INR' }, + groupSeparator: '-', + }) + ).toEqual('-₹1-23-456'); + }); + + it('should override locale if decimalSeparator passed in', () => { + expect( + formatValue({ + value: '654321-00', + intlConfig: { locale: 'zh-CN', currency: 'CNY' }, + decimalSeparator: '-', + }) + ).toEqual('¥654,321-00'); + }); + + it('should override locale if disableGroupSeparators passed in', () => { + expect( + formatValue({ + value: '987654321', + intlConfig: { locale: 'zh-CN', currency: 'CNY' }, + decimalSeparator: '.', + groupSeparator: ',', + disableGroupSeparators: true, + }) + ).toEqual('¥987654321'); + }); + }); }); diff --git a/src/components/utils/__tests__/getLocaleConfig.spec.ts b/src/components/utils/__tests__/getLocaleConfig.spec.ts new file mode 100644 index 0000000..f01544e --- /dev/null +++ b/src/components/utils/__tests__/getLocaleConfig.spec.ts @@ -0,0 +1,19 @@ +import { getLocaleConfig } from '../getLocaleConfig'; + +describe('getLocaleConfig', () => { + it('should return locale config even if no intlConfig', () => { + expect(getLocaleConfig()).toStrictEqual({ + currencySymbol: '', + decimalSeparator: '.', + groupSeparator: ',', + }); + }); + + it('should return locale config from intlConfig', () => { + expect(getLocaleConfig({ locale: 'ja-JP', currency: 'JPY' })).toStrictEqual({ + currencySymbol: '¥', + decimalSeparator: '', + groupSeparator: ',', + }); + }); +}); diff --git a/src/components/utils/__tests__/padTrimValue.spec.ts b/src/components/utils/__tests__/padTrimValue.spec.ts index 2606b8a..af0fc25 100644 --- a/src/components/utils/__tests__/padTrimValue.spec.ts +++ b/src/components/utils/__tests__/padTrimValue.spec.ts @@ -1,7 +1,7 @@ import { padTrimValue } from '../padTrimValue'; describe('padTrimValue', () => { - it('should return original value if no precision', () => { + it('should return original value if no decimalScale', () => { const value = padTrimValue('1000000'); expect(value).toEqual('1000000'); }); @@ -21,12 +21,12 @@ describe('padTrimValue', () => { expect(value).toEqual('99.000'); }); - it('should pad with 0 if decimal length is less than precision', () => { + it('should pad with 0 if decimal length is less than decimalScale', () => { const value = padTrimValue('10.5', '.', 5); expect(value).toEqual('10.50000'); }); - it('should trim if decimal length is larger than precision', () => { + it('should trim if decimal length is larger than decimalScale', () => { const value = padTrimValue('10.599', '.', 2); expect(value).toEqual('10.59'); }); diff --git a/src/components/utils/cleanValue.ts b/src/components/utils/cleanValue.ts index 54ee034..7c5c6b3 100644 --- a/src/components/utils/cleanValue.ts +++ b/src/components/utils/cleanValue.ts @@ -10,7 +10,7 @@ export type CleanValueOptions = { allowDecimals?: boolean; decimalsLimit?: number; allowNegativeValue?: boolean; - turnOffAbbreviations?: boolean; + disableAbbreviations?: boolean; prefix?: string; }; @@ -24,11 +24,15 @@ export const cleanValue = ({ allowDecimals = true, decimalsLimit = 2, allowNegativeValue = true, - turnOffAbbreviations = false, + disableAbbreviations = false, prefix = '', }: CleanValueOptions): string => { - const abbreviations = turnOffAbbreviations ? [] : ['k', 'm', 'b']; - const isNegative = value.includes('-'); + if (value === '-') { + return value; + } + + const abbreviations = disableAbbreviations ? [] : ['k', 'm', 'b']; + const isNegative = new RegExp(`^\\d?-${prefix ? `${escapeRegExp(prefix)}?` : ''}\\d`).test(value); const [prefixWithValue, preValue] = RegExp(`(\\d+)-?${escapeRegExp(prefix)}`).exec(value) || []; const withoutPrefix = prefix ? value.replace(prefixWithValue, '').concat(preValue) : value; @@ -41,7 +45,7 @@ export const cleanValue = ({ let valueOnly = withoutInvalidChars; - if (!turnOffAbbreviations) { + if (!disableAbbreviations) { // disallow letter without number if (abbreviations.some((letter) => letter === withoutInvalidChars.toLowerCase())) { return ''; @@ -54,9 +58,9 @@ export const cleanValue = ({ const includeNegative = isNegative && allowNegativeValue ? '-' : ''; - if (String(valueOnly).includes(decimalSeparator)) { + if (decimalSeparator && valueOnly.includes(decimalSeparator)) { const [int, decimals] = withoutInvalidChars.split(decimalSeparator); - const trimmedDecimals = decimalsLimit ? decimals.slice(0, decimalsLimit) : decimals; + const trimmedDecimals = decimalsLimit && decimals ? decimals.slice(0, decimalsLimit) : decimals; const includeDecimals = allowDecimals ? `${decimalSeparator}${trimmedDecimals}` : ''; return `${includeNegative}${int}${includeDecimals}`; diff --git a/src/components/utils/formatValue.ts b/src/components/utils/formatValue.ts index de435e4..affdb05 100644 --- a/src/components/utils/formatValue.ts +++ b/src/components/utils/formatValue.ts @@ -1,10 +1,12 @@ -import { addSeparators } from './addSeparators'; +import { IntlConfig } from '../CurrencyInputProps'; +import { escapeRegExp } from './escapeRegExp'; +import { getSuffix } from './getSuffix'; -type Props = { +type FormatValueOptions = { /** * Value to format */ - value: number | string | undefined; + value: string | undefined; /** * Decimal separator @@ -27,7 +29,12 @@ type Props = { * * Default = false */ - turnOffSeparators?: boolean; + disableGroupSeparators?: boolean; + + /** + * Intl locale currency config + */ + intlConfig?: IntlConfig; /** * Prefix @@ -38,41 +45,127 @@ type Props = { /** * Format value with decimal separator, group separator and prefix */ -export const formatValue = (props: Props): string => { - const { - value: _value, - groupSeparator = ',', - decimalSeparator = '.', - turnOffSeparators = false, - prefix, - } = props; +export const formatValue = (options: FormatValueOptions): string => { + const { value: _value, decimalSeparator, intlConfig, prefix = '' } = options; if (_value === '' || _value === undefined) { return ''; } - const value = String(_value); - - if (value === '-') { + if (_value === '-') { return '-'; } - const isNegative = RegExp('^-\\d+').test(value); - const hasDecimalSeparator = decimalSeparator && value.includes(decimalSeparator); + const isNegative = new RegExp(`^\\d?-${prefix ? `${escapeRegExp(prefix)}?` : ''}\\d`).test( + _value + ); + const value = + decimalSeparator !== '.' + ? replaceDecimalSeparator(_value, decimalSeparator, isNegative) + : _value; + + const numberFormatter = intlConfig + ? new Intl.NumberFormat(intlConfig.locale, { + style: 'currency', + currency: intlConfig.currency, + minimumFractionDigits: 0, + maximumFractionDigits: 20, + }) + : new Intl.NumberFormat(undefined, { + minimumFractionDigits: 0, + maximumFractionDigits: 20, + }); + + const parts = numberFormatter.formatToParts(Number(value)); + + let formatted = replaceParts(parts, options); + + // Without intl config, number formatter won't include currency symbol ie. prefix + if (!intlConfig) { + formatted = isNegative ? formatted.replace(/^-/g, `-${prefix}`) : `${prefix}${formatted}`; + } + + // Does intl formatting add a suffix? + const suffix = getSuffix(formatted, { ...options }); + + // Include decimal separator if user input ends with decimal separator + const includeDecimalSeparator = _value.slice(-1) === decimalSeparator ? decimalSeparator : ''; + + // Keep original decimal padding + const [, decimals] = value.match(RegExp('\\d+\\.(\\d+)')) || []; - const valueOnly = isNegative ? value.replace('-', '') : value; - const [int, decimals] = hasDecimalSeparator ? valueOnly.split(decimalSeparator) : [valueOnly]; + if (decimals && decimalSeparator) { + if (formatted.includes(decimalSeparator)) { + formatted = formatted.replace( + RegExp(`(\\d+)(${escapeRegExp(decimalSeparator)})(\\d+)`, 'g'), + `$1$2${decimals}` + ); + } else { + if (suffix) { + formatted = formatted.replace(suffix, `${decimalSeparator}${decimals}${suffix}`); + } else { + formatted = `${formatted}${decimalSeparator}${decimals}`; + } + } + } + + if (suffix && includeDecimalSeparator) { + return formatted.replace(suffix, `${includeDecimalSeparator}${suffix}`); + } + + return [formatted, includeDecimalSeparator].join(''); +}; + +/** + * Before converting to Number, decimal separator has to be . + */ +const replaceDecimalSeparator = ( + value: string, + decimalSeparator: FormatValueOptions['decimalSeparator'], + isNegative: boolean +): string => { + let newValue = value; + if (decimalSeparator && decimalSeparator !== '.') { + newValue = newValue.replace(RegExp(escapeRegExp(decimalSeparator), 'g'), '.'); + if (isNegative && decimalSeparator === '-') { + newValue = `-${newValue.slice(1)}`; + } + } + return newValue; +}; + +const replaceParts = ( + parts: Intl.NumberFormatPart[], + { + prefix, + groupSeparator, + decimalSeparator, + disableGroupSeparators = false, + }: Pick< + FormatValueOptions, + 'prefix' | 'groupSeparator' | 'decimalSeparator' | 'disableGroupSeparators' + > +): string => { + return parts + .reduce( + (prev, { type, value }) => { + if (type === 'currency' && prefix) { + return [...prev, prefix]; + } - const formattedInt = turnOffSeparators ? int : addSeparators(int, groupSeparator); + if (type === 'group') { + return !disableGroupSeparators + ? [...prev, groupSeparator !== undefined ? groupSeparator : value] + : prev; + } - const includePrefix = prefix ? prefix : ''; - const includeNegative = isNegative ? '-' : ''; - const includeDecimals = - hasDecimalSeparator && decimals - ? `${decimalSeparator}${decimals}` - : hasDecimalSeparator - ? `${decimalSeparator}` - : ''; + if (type === 'decimal') { + return [...prev, decimalSeparator !== undefined ? decimalSeparator : value]; + } - return `${includeNegative}${includePrefix}${formattedInt}${includeDecimals}`; + return [...prev, value]; + }, + [''] + ) + .join(''); }; diff --git a/src/components/utils/getLocaleConfig.ts b/src/components/utils/getLocaleConfig.ts new file mode 100644 index 0000000..37898ef --- /dev/null +++ b/src/components/utils/getLocaleConfig.ts @@ -0,0 +1,38 @@ +import { IntlConfig } from '../CurrencyInputProps'; + +type LocaleConfig = { + currencySymbol: string; + groupSeparator: string; + decimalSeparator: string; +}; + +const defaultConfig: LocaleConfig = { + currencySymbol: '', + groupSeparator: '', + decimalSeparator: '', +}; + +/** + * Get locale config from input or default + */ +export const getLocaleConfig = (intlConfig?: IntlConfig): LocaleConfig => { + const { locale, currency } = intlConfig || {}; + const numberFormatter = + locale && currency + ? new Intl.NumberFormat(locale, { currency, style: 'currency' }) + : new Intl.NumberFormat(); + + return numberFormatter.formatToParts(1000.1).reduce((prev, curr): LocaleConfig => { + if (curr.type === 'currency') { + return { ...prev, currencySymbol: curr.value }; + } + if (curr.type === 'group') { + return { ...prev, groupSeparator: curr.value }; + } + if (curr.type === 'decimal') { + return { ...prev, decimalSeparator: curr.value }; + } + + return prev; + }, defaultConfig); +}; diff --git a/src/components/utils/getSuffix.ts b/src/components/utils/getSuffix.ts new file mode 100644 index 0000000..64ce329 --- /dev/null +++ b/src/components/utils/getSuffix.ts @@ -0,0 +1,16 @@ +import { escapeRegExp } from './escapeRegExp'; +type Options = { + decimalSeparator?: string; + groupSeparator?: string; +}; + +export const getSuffix = ( + value: string, + { groupSeparator = ',', decimalSeparator = '.' }: Options +): string | undefined => { + const suffixReg = new RegExp( + `\\d([^${escapeRegExp(groupSeparator)}${escapeRegExp(decimalSeparator)}0-9].)` + ); + const suffixMatch = value.match(suffixReg); + return suffixMatch ? suffixMatch[1] : undefined; +}; diff --git a/src/components/utils/index.ts b/src/components/utils/index.ts index b63670c..c130e10 100644 --- a/src/components/utils/index.ts +++ b/src/components/utils/index.ts @@ -1,5 +1,7 @@ export * from './cleanValue'; export * from './fixedDecimalValue'; export * from './formatValue'; +export * from './getLocaleConfig'; +export * from './getSuffix'; export * from './isNumber'; export * from './padTrimValue'; diff --git a/src/components/utils/padTrimValue.ts b/src/components/utils/padTrimValue.ts index 9840389..55aa851 100644 --- a/src/components/utils/padTrimValue.ts +++ b/src/components/utils/padTrimValue.ts @@ -1,5 +1,9 @@ -export const padTrimValue = (value: string, decimalSeparator = '.', precision?: number): string => { - if (!precision || value === '' || value === undefined) { +export const padTrimValue = ( + value: string, + decimalSeparator = '.', + decimalScale?: number +): string => { + if (!decimalScale || value === '' || value === undefined) { return value; } @@ -10,12 +14,12 @@ export const padTrimValue = (value: string, decimalSeparator = '.', precision?: const [int, decimals] = value.split(decimalSeparator); let newValue = decimals || ''; - if (newValue.length < precision) { - while (newValue.length < precision) { + if (newValue.length < decimalScale) { + while (newValue.length < decimalScale) { newValue += '0'; } } else { - newValue = newValue.slice(0, precision); + newValue = newValue.slice(0, decimalScale); } return `${int}${decimalSeparator}${newValue}`; diff --git a/src/components/utils/parseAbbrValue.ts b/src/components/utils/parseAbbrValue.ts index 9e3b7ef..b0eb73b 100644 --- a/src/components/utils/parseAbbrValue.ts +++ b/src/components/utils/parseAbbrValue.ts @@ -33,9 +33,8 @@ export const parseAbbrValue = (value: string, decimalSeparator = '.'): number | if (match) { const [, digits, , abbr] = match; const multiplier = abbrMap[abbr.toLowerCase()]; - if (digits && multiplier) { - return Number(digits.replace(decimalSeparator, '.')) * multiplier; - } + + return Number(digits.replace(decimalSeparator, '.')) * multiplier; } return undefined; diff --git a/src/example.html b/src/example.html index 6a979bb..786ce45 100644 --- a/src/example.html +++ b/src/example.html @@ -26,10 +26,12 @@

React Currency Input Field

  • Allows abbreviations eg. 1k = 1,000 2.5m = 2,500,000
  • Prefix option eg. £ or $
  • +
  • Automatically inserts group separator
  • +
  • Accepts Intl locale config
  • +
  • Can use arrow down/up to step
  • Can allow/disallow decimals
  • -
  • Automatically inserts comma separator
  • -
  • Only allows valid numbers
  • -
  • Lightweight and simple
  • +
  • Written in TypeScript and has type support
  • +
  • Does not use any third party packages

Please visit the @@ -54,6 +56,10 @@

React Currency Input Field


+
+ +
+

diff --git a/src/example.tsx b/src/example.tsx index 7452197..c50525d 100644 --- a/src/example.tsx +++ b/src/example.tsx @@ -4,6 +4,7 @@ import ReactDOM from 'react-dom'; import Example1 from './examples/Example1'; import Example2 from './examples/Example2'; import Example3 from './examples/Example3'; +import Example4 from './examples/Example4'; import FormatValuesExample from './examples/FormatValuesExample'; ReactDOM.render(, document.getElementById('example-1')); @@ -12,4 +13,6 @@ ReactDOM.render(, document.getElementById('example-2')); ReactDOM.render(, document.getElementById('example-3')); +ReactDOM.render(, document.getElementById('example-4')); + ReactDOM.render(, document.getElementById('format-values-example')); diff --git a/src/examples/Example1.tsx b/src/examples/Example1.tsx index e50acab..9b186eb 100644 --- a/src/examples/Example1.tsx +++ b/src/examples/Example1.tsx @@ -7,16 +7,14 @@ export const Example1: FC = () => { const [errorMessage, setErrorMessage] = useState(''); const [className, setClassName] = useState(''); - const [value, setValue] = useState(999.99); + const [value, setValue] = useState(123.45); const [rawValue, setRawValue] = useState(' '); - const [rawBlurValue, setRawBlurValue] = useState(' '); /** * Handle validation */ - const validateValue = (value: string | undefined): void => { - const rawValue = value === undefined ? 'undefined' : value; - setRawValue(rawValue || ' '); + const handleOnValueChange = (value: string | undefined): void => { + setRawValue(value === undefined ? 'undefined' : value || ' '); if (!value) { setClassName(''); @@ -41,11 +39,6 @@ export const Example1: FC = () => { setValue(value); }; - const handleOnBlurValue = (value: string | undefined) => { - const rawBlurValue = value === undefined ? 'undefined' : value; - setRawBlurValue(rawBlurValue || ' '); - }; - return (
@@ -55,7 +48,6 @@ export const Example1: FC = () => {
  • {`'£'`} prefix
  • Allows decimals (up to 2 decimal places)
  • -
  • Has default value (999.99)
  • Value is set programmatically (passed in via props)
@@ -66,13 +58,11 @@ export const Example1: FC = () => {
{errorMessage}
@@ -81,13 +71,9 @@ export const Example1: FC = () => {
                 
-
onChange:
+
onValueChange:
{rawValue}
-
-
onBlurValue:
- {rawBlurValue} -
diff --git a/src/examples/Example2.tsx b/src/examples/Example2.tsx index 3827c52..a0f4657 100644 --- a/src/examples/Example2.tsx +++ b/src/examples/Example2.tsx @@ -6,7 +6,6 @@ export const Example2: FC = () => { const [errorMessage, setErrorMessage] = useState(''); const [className, setClassName] = useState(''); const [rawValue, setRawValue] = useState(' '); - const [rawBlurValue, setRawBlurValue] = useState(' '); const validateValue = (value: string | undefined): void => { const rawValue = value === undefined ? 'undefined' : value; @@ -22,11 +21,6 @@ export const Example2: FC = () => { } }; - const handleOnBlurValue = (value: string | undefined) => { - const rawBlurValue = value === undefined ? 'undefined' : value; - setRawBlurValue(rawBlurValue || ' '); - }; - return (
@@ -48,8 +42,7 @@ export const Example2: FC = () => { placeholder="$1,234,567" allowDecimals={false} className={`form-control ${className}`} - onChange={validateValue} - onBlurValue={handleOnBlurValue} + onValueChange={validateValue} prefix={'$'} step={10} /> @@ -59,13 +52,9 @@ export const Example2: FC = () => {
                 
-
onChange:
+
onValueChange:
{rawValue}
-
-
onBlurValue:
- {rawBlurValue} -
diff --git a/src/examples/Example3.tsx b/src/examples/Example3.tsx index 7b96098..286ee35 100644 --- a/src/examples/Example3.tsx +++ b/src/examples/Example3.tsx @@ -1,87 +1,47 @@ -import React, { FC, useReducer } from 'react'; -import CurrencyInput, { formatValue } from '../'; - -type Field = { - value: number | undefined; - validationClass: string; - errorMessage: string; -}; - -type ExampleState = { - field1: Field; - field2: Field; -}; - -type Action = { - fieldName: string; - value: Field; -}; - -function reducer(state: ExampleState, { fieldName, value }: Action): ExampleState { - return { - ...state, - [fieldName]: value, - }; -} - -const initialState: ExampleState = { - field1: { - value: 100, - validationClass: '', - errorMessage: '', +import React, { FC, useState } from 'react'; +import CurrencyInput from '../components/CurrencyInput'; +import { CurrencyInputProps } from '../components/CurrencyInputProps'; + +const options: ReadonlyArray = [ + { + locale: 'de-DE', + currency: 'EUR', }, - field2: { - value: 200, - validationClass: '', - errorMessage: '', + { + locale: 'en-US', + currency: 'USD', }, -}; + { + locale: 'en-GB', + currency: 'GBP', + }, + { + locale: 'ja-JP', + currency: 'JPY', + }, + { + locale: 'en-IN', + currency: 'INR', + }, +]; export const Example3: FC = () => { - const prefix = '£'; - const [state, dispatch] = useReducer(reducer, initialState); + const [intlConfig, setIntlConfig] = useState(options[0]); + const [value, setValue] = useState('123'); + const [rawValue, setRawValue] = useState(' '); - const handleOnChange = (_value: string | undefined, fieldName: string | undefined): void => { - if (!fieldName) { - return; - } - - if (!_value) { - return dispatch({ - fieldName, - value: { - value: undefined, - validationClass: '', - errorMessage: '', - }, - }); - } - - const value = Number(_value); + const handleOnValueChange = (value: string | undefined): void => { + setRawValue(value === undefined ? 'undefined' : value || ' '); + setValue(value); + }; - if (!Number.isNaN(value)) { - dispatch({ - fieldName, - value: { - value, - validationClass: 'is-valid', - errorMessage: '', - }, - }); - } else { - dispatch({ - fieldName, - value: { - value, - validationClass: 'is-invalid', - errorMessage: 'Please enter a valid number', - }, - }); + const handleIntlSelect = (event: React.ChangeEvent) => { + const config = options[Number(event.target.value)]; + if (config) { + setIntlConfig(config); } }; - const total = (state.field1.value || 0) + (state.field2.value || 0); - return (
@@ -89,46 +49,56 @@ export const Example3: FC = () => {

Example 3

    -
  • Add two values together
  • -
  • Format the total value
  • +
  • Intl config
-
-
-
- - -
{state.field1.errorMessage}
-
- -
- - -
{state.field1.errorMessage}
-
- -
-
- -
{formatValue({ prefix, value: total })}
+
+ +
+
+
+
+ +
+
+ +
- +
+
+              
+
+
onValueChange:
+ {rawValue} +
intlConfig:
+ {JSON.stringify(intlConfig)} +
+
+
+
+
); diff --git a/src/examples/Example4.tsx b/src/examples/Example4.tsx new file mode 100644 index 0000000..9439d4a --- /dev/null +++ b/src/examples/Example4.tsx @@ -0,0 +1,137 @@ +import React, { FC, useReducer } from 'react'; +import CurrencyInput, { formatValue } from '..'; + +type Field = { + value: number | undefined; + validationClass: string; + errorMessage: string; +}; + +type ExampleState = { + field1: Field; + field2: Field; +}; + +type Action = { + fieldName: string; + value: Field; +}; + +function reducer(state: ExampleState, { fieldName, value }: Action): ExampleState { + return { + ...state, + [fieldName]: value, + }; +} + +const initialState: ExampleState = { + field1: { + value: 100, + validationClass: '', + errorMessage: '', + }, + field2: { + value: 200, + validationClass: '', + errorMessage: '', + }, +}; + +export const Example4: FC = () => { + const prefix = '£'; + const [state, dispatch] = useReducer(reducer, initialState); + + const handleOnValueChange = (_value: string | undefined, fieldName: string | undefined): void => { + if (!fieldName) { + return; + } + + if (!_value) { + return dispatch({ + fieldName, + value: { + value: undefined, + validationClass: '', + errorMessage: '', + }, + }); + } + + const value = Number(_value); + + if (!Number.isNaN(value)) { + dispatch({ + fieldName, + value: { + value, + validationClass: 'is-valid', + errorMessage: '', + }, + }); + } else { + dispatch({ + fieldName, + value: { + value, + validationClass: 'is-invalid', + errorMessage: 'Please enter a valid number', + }, + }); + } + }; + + const total = (state.field1.value || 0) + (state.field2.value || 0); + + return ( +
+
+ +

Example 4

+
+
    +
  • Add two values together
  • +
  • Format the total value
  • +
+ +
+
+
+ + +
{state.field1.errorMessage}
+
+ +
+ + +
{state.field1.errorMessage}
+
+ +
+
+ +
{formatValue({ prefix, value: String(total) })}
+
+
+
+
+
+
+ ); +}; + +export default Example4; diff --git a/src/examples/FormatValuesExample.tsx b/src/examples/FormatValuesExample.tsx index 6810435..d277a59 100644 --- a/src/examples/FormatValuesExample.tsx +++ b/src/examples/FormatValuesExample.tsx @@ -6,7 +6,7 @@ const FormatValuesExample: FC = () => { const [prefix, setPrefix] = useState('$'); const [groupSeparator, setGroupSeparator] = useState(','); const [decimalSeparator, setDecimalSeparator] = useState('.'); - const [turnOffSeparators, setTurnOffSeparators] = useState(false); + const [disableGroupSeparators, setdisableGroupSeparators] = useState(false); const handleValueChange = ({ target: { value } }: React.ChangeEvent) => { setValue(value); @@ -31,7 +31,7 @@ const FormatValuesExample: FC = () => { const handleTurnOffSeparatorChange = ({ target: { value }, }: React.ChangeEvent) => { - setTurnOffSeparators(value === 'true' ? true : false); + setdisableGroupSeparators(value === 'true' ? true : false); }; return ( @@ -46,9 +46,9 @@ const FormatValuesExample: FC = () => {
- + {
-
-
@@ -120,7 +120,7 @@ const FormatValuesExample: FC = () => { value, groupSeparator, decimalSeparator, - turnOffSeparators, + disableGroupSeparators, prefix, })}
diff --git a/yarn.lock b/yarn.lock index 19032b3..d33943a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -199,6 +199,21 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" +"@babel/runtime-corejs3@^7.10.2": + version "7.12.5" + resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.12.5.tgz#ffee91da0eb4c6dae080774e94ba606368e414f4" + integrity sha512-roGr54CsTmNPPzZoCP1AmDXuBoNao7tnSA83TXTwt+UK5QVyh1DIJnrgYRPWKCF2flqZQXwa7Yr8v7VmLzF0YQ== + dependencies: + core-js-pure "^3.0.0" + regenerator-runtime "^0.13.4" + +"@babel/runtime@^7.10.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.9.2": + version "7.12.5" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e" + integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/runtime@^7.8.7": version "7.8.7" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.8.7.tgz#8fefce9802db54881ba59f90bb28719b4996324d" @@ -618,6 +633,17 @@ "@types/yargs" "^15.0.0" chalk "^4.0.0" +"@jest/types@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.6.2.tgz#bef5a532030e1d88a2f5a6d933f84e97226ed48e" + integrity sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^15.0.0" + chalk "^4.0.0" + "@nodelib/fs.scandir@2.1.3": version "2.1.3" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b" @@ -849,6 +875,49 @@ dependencies: "@sinonjs/commons" "^1.7.0" +"@testing-library/dom@^7.28.1", "@testing-library/dom@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-7.29.0.tgz#60b18065bab50a5cde21fe80275a47a43024d9cc" + integrity sha512-0hhuJSmw/zLc6ewR9cVm84TehuTd7tbqBX9pRNSp8znJ9gTmSgesdbiGZtt8R6dL+2rgaPFp9Yjr7IU1HWm49w== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/runtime" "^7.12.5" + "@types/aria-query" "^4.2.0" + aria-query "^4.2.2" + chalk "^4.1.0" + dom-accessibility-api "^0.5.4" + lz-string "^1.4.4" + pretty-format "^26.6.2" + +"@testing-library/jest-dom@^5.11.6": + version "5.11.6" + resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.11.6.tgz#782940e82e5cd17bc0a36f15156ba16f3570ac81" + integrity sha512-cVZyUNRWwUKI0++yepYpYX7uhrP398I+tGz4zOlLVlUYnZS+Svuxv4fwLeCIy7TnBYKXUaOlQr3vopxL8ZfEnA== + dependencies: + "@babel/runtime" "^7.9.2" + "@types/testing-library__jest-dom" "^5.9.1" + aria-query "^4.2.2" + chalk "^3.0.0" + css "^3.0.0" + css.escape "^1.5.1" + lodash "^4.17.15" + redent "^3.0.0" + +"@testing-library/react@^11.2.2": + version "11.2.2" + resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-11.2.2.tgz#099c6c195140ff069211143cb31c0f8337bdb7b7" + integrity sha512-jaxm0hwUjv+hzC+UFEywic7buDC9JQ1q3cDsrWVSDAPmLotfA6E6kUHlYm/zOeGCac6g48DR36tFHxl7Zb+N5A== + dependencies: + "@babel/runtime" "^7.12.5" + "@testing-library/dom" "^7.28.1" + +"@testing-library/user-event@^12.6.0": + version "12.6.0" + resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-12.6.0.tgz#2d0229e399eb5a0c6c112e848611432356cac886" + integrity sha512-FNEH/HLmOk5GO70I52tKjs7WvGYckeE/SrnLX/ip7z2IGbffyd5zOUM1tZ10vsTphqm+VbDFI0oaXu0wcfQsAQ== + dependencies: + "@babel/runtime" "^7.12.5" + "@tootallnate/once@1": version "1.0.0" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.0.0.tgz#9c13c2574c92d4503b005feca8f2e16cc1611506" @@ -859,6 +928,11 @@ resolved "https://registry.yarnpkg.com/@types/anymatch/-/anymatch-1.3.1.tgz#336badc1beecb9dacc38bea2cf32adf627a8421a" integrity sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA== +"@types/aria-query@^4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-4.2.0.tgz#14264692a9d6e2fa4db3df5e56e94b5e25647ac0" + integrity sha512-iIgQNzCm0v7QMhhe4Jjn9uRh+I6GoPmt03CbEtwx3ao8/EfoQcmgtqH4vQ5Db/lxiIGaWDv6nwvunuh0RyX0+A== + "@types/babel__core@^7.0.0": version "7.1.9" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.9.tgz#77e59d438522a6fb898fa43dc3455c6e72f3963d" @@ -903,26 +977,11 @@ dependencies: "@babel/types" "^7.3.0" -"@types/cheerio@*": - version "0.22.16" - resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.16.tgz#c748a97b8a6f781b04bbda4a552e11b35bcc77e4" - integrity sha512-bSbnU/D4yzFdzLpp3+rcDj0aQQMIRUBNJU7azPxdqMpnexjUSvGJyDuOBQBHeOZh1mMKgsJm6Dy+LLh80Ew4tQ== - dependencies: - "@types/node" "*" - "@types/color-name@^1.1.1": version "1.1.1" resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== -"@types/enzyme@^3.10.5": - version "3.10.5" - resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.10.5.tgz#fe7eeba3550369eed20e7fb565bfb74eec44f1f0" - integrity sha512-R+phe509UuUYy9Tk0YlSbipRpfVtIzb/9BHn5pTEtjJTF5LXvUjrIQcZvNyANNEyFrd2YGs196PniNT1fgvOQA== - dependencies: - "@types/cheerio" "*" - "@types/react" "*" - "@types/eslint-visitor-keys@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d" @@ -981,6 +1040,14 @@ dependencies: "@types/istanbul-lib-report" "*" +"@types/jest@*": + version "26.0.19" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.19.tgz#e6fa1e3def5842ec85045bd5210e9bb8289de790" + integrity sha512-jqHoirTG61fee6v6rwbnEuKhpSKih0tuhqeFbCmMmErhtu3BYlOZaXWjffgOstMM4S/3iQD31lI5bGLTrs97yQ== + dependencies: + jest-diff "^26.0.0" + pretty-format "^26.0.0" + "@types/jest@26.x", "@types/jest@^26.0.9": version "26.0.9" resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.9.tgz#0543b57da5f0cd949c5f423a00c56c492289c989" @@ -1072,6 +1139,13 @@ resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.5.tgz#9adbc12950582aa65ead76bffdf39fe0c27a3c02" integrity sha512-/gG2M/Imw7cQFp8PGvz/SwocNrmKFjFsm5Pb8HdbHkZ1K8pmuPzOX4VeVoiEecFCVf4CsN1r3/BRvx+6sNqwtQ== +"@types/testing-library__jest-dom@^5.9.1": + version "5.9.5" + resolved "https://registry.yarnpkg.com/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.9.5.tgz#5bf25c91ad2d7b38f264b12275e5c92a66d849b0" + integrity sha512-ggn3ws+yRbOHog9GxnXiEZ/35Mow6YtPZpd7Z5mKDeZS/o7zx3yAle0ov/wjhVB5QT4N2Dt+GNoGCdqkBGCajQ== + dependencies: + "@types/jest" "*" + "@types/uglify-js@*": version "3.0.5" resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.0.5.tgz#2c70d5c68f6e002e3b2e4f849adc5f162546f633" @@ -1427,21 +1501,6 @@ aggregate-error@^3.0.0: clean-stack "^2.0.0" indent-string "^4.0.0" -airbnb-prop-types@^2.16.0: - version "2.16.0" - resolved "https://registry.yarnpkg.com/airbnb-prop-types/-/airbnb-prop-types-2.16.0.tgz#b96274cefa1abb14f623f804173ee97c13971dc2" - integrity sha512-7WHOFolP/6cS96PhKNrslCLMYAI8yB1Pp6u6XmxozQOiZbsI5ycglZr5cHhBFfuRcQQjzCMith5ZPZdYiJCxUg== - dependencies: - array.prototype.find "^2.1.1" - function.prototype.name "^1.1.2" - is-regex "^1.1.0" - object-is "^1.1.2" - object.assign "^4.1.0" - object.entries "^1.1.2" - prop-types "^15.7.2" - prop-types-exact "^1.2.0" - react-is "^16.13.1" - ajv-errors@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d" @@ -1597,6 +1656,14 @@ argv@0.0.2: resolved "https://registry.yarnpkg.com/argv/-/argv-0.0.2.tgz#ecbd16f8949b157183711b1bda334f37840185ab" integrity sha1-7L0W+JSbFXGDcRsb2jNPN4QBhas= +aria-query@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-4.2.2.tgz#0d2ca6c9aceb56b8977e9fed6aed7e15bbd2f83b" + integrity sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA== + dependencies: + "@babel/runtime" "^7.10.2" + "@babel/runtime-corejs3" "^7.10.2" + arr-diff@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" @@ -1612,11 +1679,6 @@ arr-union@^3.1.0: resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= -array-filter@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-1.0.0.tgz#baf79e62e6ef4c2a4c0b831232daffec251f9d83" - integrity sha1-uveeYubvTCpMC4MSMtr/7CUfnYM= - array-find-index@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" @@ -1668,22 +1730,6 @@ array-unique@^0.3.2: resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= -array.prototype.find@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/array.prototype.find/-/array.prototype.find-2.1.1.tgz#3baca26108ca7affb08db06bf0be6cb3115a969c" - integrity sha512-mi+MYNJYLTx2eNYy+Yh6raoQacCsNeeMUaspFPh9Y141lFSsWxxB8V9mM2ye+eqiRs917J6/pJ4M9ZPzenWckA== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.4" - -array.prototype.flat@^1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.3.tgz#0de82b426b0318dbfdb940089e38b043d37f6c7b" - integrity sha512-gBlRZV0VSmfPIeWfuuy56XZMvbVfbEUnOXUvt3F/eUUUSyzlgLxhEX4YAEpxNAogRGehPSnfXyPtYyKAhkzQhQ== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.0-next.1" - array.prototype.flatmap@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.2.3.tgz#1c13f84a178566042dd63de4414440db9222e443" @@ -2290,7 +2336,7 @@ caseless@~0.12.0: resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= -chalk@4.1.0: +chalk@4.1.0, chalk@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== @@ -2328,18 +2374,6 @@ char-regex@^1.0.2: resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== -cheerio@^1.0.0-rc.3: - version "1.0.0-rc.3" - resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.3.tgz#094636d425b2e9c0f4eb91a46c05630c9a1a8bf6" - integrity sha512-0td5ijfUPuubwLUu0OBoe98gZj8C/AA+RW3v67GPlGOrvxWjZmBXiBCRU+I8VEiNyJzjth40POfHiz2RB3gImA== - dependencies: - css-select "~1.2.0" - dom-serializer "~0.1.1" - entities "~1.1.1" - htmlparser2 "^3.9.1" - lodash "^4.15.0" - parse5 "^3.0.1" - chokidar@^2.1.8: version "2.1.8" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" @@ -2610,7 +2644,7 @@ combined-stream@^1.0.6, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" -commander@^2.18.0, commander@^2.19.0, commander@^2.20.0, commander@~2.20.3: +commander@^2.18.0, commander@^2.20.0, commander@~2.20.3: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== @@ -2823,6 +2857,11 @@ copy-descriptor@^0.1.0: resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= +core-js-pure@^3.0.0: + version "3.8.1" + resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.8.1.tgz#23f84048f366fdfcf52d3fd1c68fec349177d119" + integrity sha512-Se+LaxqXlVXGvmexKGPvnUIYC1jwXu1H6Pkyb3uBM5d8/NELMYCHs/4/roD7721NxrTLyv7e5nXd5/QLBO+10g== + core-js@^3.6.1: version "3.6.5" resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.6.5.tgz#7395dc273af37fb2e50e9bd3d9fe841285231d1a" @@ -2882,6 +2921,13 @@ create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4: safe-buffer "^5.0.1" sha.js "^2.4.8" +cross-env@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" + integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw== + dependencies: + cross-spawn "^7.0.1" + cross-spawn@^5.0.1: version "5.1.0" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" @@ -2911,7 +2957,7 @@ cross-spawn@^7.0.0: shebang-command "^2.0.0" which "^2.0.1" -cross-spawn@^7.0.2: +cross-spawn@^7.0.1, cross-spawn@^7.0.2: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== @@ -2947,7 +2993,7 @@ crypto-random-string@^2.0.0: resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== -css-select@^1.1.0, css-select@~1.2.0: +css-select@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858" integrity sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg= @@ -2962,6 +3008,20 @@ css-what@2.1: resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2" integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg== +css.escape@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb" + integrity sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s= + +css@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/css/-/css-3.0.0.tgz#4447a4d58fdd03367c516ca9f64ae365cee4aa5d" + integrity sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ== + dependencies: + inherits "^2.0.4" + source-map "^0.6.1" + source-map-resolve "^0.6.0" + cssom@^0.4.4: version "0.4.4" resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10" @@ -3258,6 +3318,11 @@ diff-sequences@^26.3.0: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.3.0.tgz#62a59b1b29ab7fd27cef2a33ae52abe73042d0a2" integrity sha512-5j5vdRcw3CNctePNYN0Wy2e/JbWT6cAYnXv5OuqPhDpyCGc0uLu2TK0zOCJWNB9kOIfYMSpIulRaDgIi4HJ6Ig== +diff-sequences@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.6.2.tgz#48ba99157de1923412eed41db6b6d4aa9ca7c0b1" + integrity sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q== + diffie-hellman@^5.0.0: version "5.0.3" resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" @@ -3274,11 +3339,6 @@ dir-glob@^3.0.0, dir-glob@^3.0.1: dependencies: path-type "^4.0.0" -discontinuous-range@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/discontinuous-range/-/discontinuous-range-1.0.0.tgz#e38331f0844bba49b9a9cb71c771585aab1bc65a" - integrity sha1-44Mx8IRLukm5qctxx3FYWqsbxlo= - dns-equal@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d" @@ -3313,6 +3373,11 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dom-accessibility-api@^0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.4.tgz#b06d059cdd4a4ad9a79275f9d414a5c126241166" + integrity sha512-TvrjBckDy2c6v6RLxPv5QXOnU+SmF9nBII5621Ve5fu6Z/BDrENurBEvlC1f44lKEUVqOpK4w9E5Idc5/EgkLQ== + dom-converter@^0.2: version "0.2.0" resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768" @@ -3328,14 +3393,6 @@ dom-serializer@0: domelementtype "^2.0.1" entities "^2.0.0" -dom-serializer@~0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.1.tgz#1ec4059e284babed36eec2941d4a970a189ce7c0" - integrity sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA== - dependencies: - domelementtype "^1.3.0" - entities "^1.1.1" - dom-walk@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.1.tgz#672226dc74c8f799ad35307df936aba11acd6018" @@ -3346,7 +3403,7 @@ domain-browser@^1.1.1: resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA== -domelementtype@1, domelementtype@^1.3.0, domelementtype@^1.3.1: +domelementtype@1, domelementtype@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== @@ -3544,7 +3601,7 @@ enquirer@^2.3.5: dependencies: ansi-colors "^3.2.1" -entities@^1.1.1, entities@~1.1.1: +entities@^1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== @@ -3567,77 +3624,6 @@ env-paths@^2.2.0: resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.0.tgz#cdca557dc009152917d6166e2febe1f039685e43" integrity sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA== -enzyme-adapter-react-16@^1.15.3: - version "1.15.3" - resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.15.3.tgz#90154055be3318d70a51df61ac89cfa22e3d5f60" - integrity sha512-98rqNI4n9HZslWIPuuwy4hK1bxRuMy+XX0CU1dS8iUqcgisTxeBaap6oPp2r4MWC8OphCbbqAT8EU/xHz3zIaQ== - dependencies: - enzyme-adapter-utils "^1.13.1" - enzyme-shallow-equal "^1.0.4" - has "^1.0.3" - object.assign "^4.1.0" - object.values "^1.1.1" - prop-types "^15.7.2" - react-is "^16.13.1" - react-test-renderer "^16.0.0-0" - semver "^5.7.0" - -enzyme-adapter-utils@^1.13.1: - version "1.13.1" - resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.13.1.tgz#59c1b734b0927543e3d8dc477299ec957feb312d" - integrity sha512-5A9MXXgmh/Tkvee3bL/9RCAAgleHqFnsurTYCbymecO4ohvtNO5zqIhHxV370t7nJAwaCfkgtffarKpC0GPt0g== - dependencies: - airbnb-prop-types "^2.16.0" - function.prototype.name "^1.1.2" - object.assign "^4.1.0" - object.fromentries "^2.0.2" - prop-types "^15.7.2" - semver "^5.7.1" - -enzyme-shallow-equal@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/enzyme-shallow-equal/-/enzyme-shallow-equal-1.0.1.tgz#7afe03db3801c9b76de8440694096412a8d9d49e" - integrity sha512-hGA3i1so8OrYOZSM9whlkNmVHOicJpsjgTzC+wn2JMJXhq1oO4kA4bJ5MsfzSIcC71aLDKzJ6gZpIxrqt3QTAQ== - dependencies: - has "^1.0.3" - object-is "^1.0.2" - -enzyme-shallow-equal@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/enzyme-shallow-equal/-/enzyme-shallow-equal-1.0.4.tgz#b9256cb25a5f430f9bfe073a84808c1d74fced2e" - integrity sha512-MttIwB8kKxypwHvRynuC3ahyNc+cFbR8mjVIltnmzQ0uKGqmsfO4bfBuLxb0beLNPhjblUEYvEbsg+VSygvF1Q== - dependencies: - has "^1.0.3" - object-is "^1.1.2" - -enzyme@^3.11.0: - version "3.11.0" - resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-3.11.0.tgz#71d680c580fe9349f6f5ac6c775bc3e6b7a79c28" - integrity sha512-Dw8/Gs4vRjxY6/6i9wU0V+utmQO9kvh9XLnz3LIudviOnVYDEe2ec+0k+NQoMamn1VrjKgCUOWj5jG/5M5M0Qw== - dependencies: - array.prototype.flat "^1.2.3" - cheerio "^1.0.0-rc.3" - enzyme-shallow-equal "^1.0.1" - function.prototype.name "^1.1.2" - has "^1.0.3" - html-element-map "^1.2.0" - is-boolean-object "^1.0.1" - is-callable "^1.1.5" - is-number-object "^1.0.4" - is-regex "^1.0.5" - is-string "^1.0.5" - is-subset "^0.1.1" - lodash.escape "^4.0.1" - lodash.isequal "^4.5.0" - object-inspect "^1.7.0" - object-is "^1.0.2" - object.assign "^4.1.0" - object.entries "^1.1.1" - object.values "^1.1.1" - raf "^3.4.1" - rst-selector-parser "^2.2.3" - string.prototype.trim "^1.2.1" - err-code@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/err-code/-/err-code-1.1.2.tgz#06e0116d3028f6aef4806849eb0ea6a748ae6960" @@ -3657,7 +3643,7 @@ error-ex@^1.2.0, error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -es-abstract@^1.17.0, es-abstract@^1.17.0-next.1, es-abstract@^1.17.4: +es-abstract@^1.17.0, es-abstract@^1.17.0-next.1: version "1.17.4" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.4.tgz#e3aedf19706b20e7c2594c35fc0d57605a79e184" integrity sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ== @@ -4481,30 +4467,21 @@ fsevents@~2.1.2: resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== +full-icu@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/full-icu/-/full-icu-1.3.1.tgz#e67fdf58523f1d1e0d9143b1542fe2024c1c8997" + integrity sha512-VMtK//85QJomhk3cXOCksNwOYaw1KWnYTS37GYGgyf7A3ajdBoPGhaJuJWAH2S2kq8GZeXkdKn+3Mfmgy11cVw== + function-bind@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== -function.prototype.name@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.2.tgz#5cdf79d7c05db401591dfde83e3b70c5123e9a45" - integrity sha512-C8A+LlHBJjB2AdcRPorc5JvJ5VUoWlXdEHLOJdCI7kjHEtGTpHQUiqMvCIKUwIsGwZX2jZJy761AXsn356bJQg== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.0-next.1" - functions-have-names "^1.2.0" - functional-red-black-tree@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= -functions-have-names@^1.2.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.1.tgz#a981ac397fa0c9964551402cdc5533d7a4d52f91" - integrity sha512-j48B/ZI7VKs3sgeI2cZp7WXWmZXu7Iq5pl5/vptV5N2mq+DGFuS/ulaDjtaoLpYzuD6u8UgrUKHfgo7fDTSiBA== - gauge@~2.7.3: version "2.7.4" resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" @@ -4948,13 +4925,6 @@ hpack.js@^2.1.6: readable-stream "^2.0.1" wbuf "^1.1.0" -html-element-map@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/html-element-map/-/html-element-map-1.2.0.tgz#dfbb09efe882806af63d990cf6db37993f099f22" - integrity sha512-0uXq8HsuG1v2TmQ8QkIhzbrqeskE4kn52Q18QJ9iAA/SnHoEKXWiUxHQtclRsCFWEUD2So34X+0+pZZu862nnw== - dependencies: - array-filter "^1.0.0" - html-encoding-sniffer@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz#42a6dc4fd33f00281176e8b23759ca4e4fa185f3" @@ -5000,7 +4970,7 @@ html-webpack-plugin@^4.3.0: tapable "^1.1.3" util.promisify "1.0.0" -htmlparser2@^3.3.0, htmlparser2@^3.9.1: +htmlparser2@^3.3.0: version "3.10.1" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f" integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ== @@ -5411,11 +5381,6 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" -is-boolean-object@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.0.1.tgz#10edc0900dd127697a92f6f9807c7617d68ac48e" - integrity sha512-TqZuVwa/sppcrhUCAYkGBk7w0yxfQQnxq28fjkO53tnK9FQXmdwz2JS5+GjsWQ6RByES1K40nI+yDic5c9/aAQ== - is-buffer@^1.1.5: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" @@ -5555,11 +5520,6 @@ is-npm@^1.0.0: resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-1.0.0.tgz#f2fb63a65e4905b406c86072765a1a4dc793b9f4" integrity sha1-8vtjpl5JBbQGyGBydloaTceTufQ= -is-number-object@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.4.tgz#36ac95e741cf18b283fc1ddf5e83da798e3ec197" - integrity sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw== - is-number@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" @@ -5671,11 +5631,6 @@ is-string@^1.0.5: resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6" integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ== -is-subset@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/is-subset/-/is-subset-0.1.1.tgz#8a59117d932de1de00f245fcdd39ce43f1e939a6" - integrity sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY= - is-symbol@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937" @@ -5879,6 +5834,16 @@ jest-diff@^25.2.1: jest-get-type "^25.2.6" pretty-format "^25.3.0" +jest-diff@^26.0.0: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-26.6.2.tgz#1aa7468b52c3a68d7d5c5fdcdfcd5e49bd164394" + integrity sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA== + dependencies: + chalk "^4.0.0" + diff-sequences "^26.6.2" + jest-get-type "^26.3.0" + pretty-format "^26.6.2" + jest-diff@^26.4.0: version "26.4.0" resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-26.4.0.tgz#d073a0a11952b5bd9f1ff39bb9ad24304a0c55f7" @@ -6717,26 +6682,11 @@ lodash.clonedeep@^4.5.0, lodash.clonedeep@~4.5.0: resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= -lodash.escape@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-4.0.1.tgz#c9044690c21e04294beaa517712fded1fa88de98" - integrity sha1-yQRGkMIeBClL6qUXcS/e0fqI3pg= - lodash.escaperegexp@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347" integrity sha1-ZHYsSGGAglGKw99Mz11YhtriA0c= -lodash.flattendeep@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2" - integrity sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI= - -lodash.isequal@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" - integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA= - lodash.ismatch@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz#756cb5150ca3ba6f11085a78849645f188f85f37" @@ -6802,7 +6752,7 @@ lodash.without@~4.4.0: resolved "https://registry.yarnpkg.com/lodash.without/-/lodash.without-4.4.0.tgz#3cd4574a00b67bae373a94b748772640507b7aac" integrity sha1-PNRXSgC2e643OpS3SHcmQFB7eqw= -lodash@^4.15.0, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.4, lodash@^4.17.5: +lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.4, lodash@^4.17.5: version "4.17.20" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== @@ -6886,6 +6836,11 @@ lru-cache@^5.1.1: dependencies: yallist "^3.0.2" +lz-string@^1.4.4: + version "1.4.4" + resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26" + integrity sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY= + macos-release@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-2.3.0.tgz#eb1930b036c0800adebccd5f17bc4c12de8bb71f" @@ -7155,6 +7110,11 @@ min-document@^2.19.0: dependencies: dom-walk "^0.1.0" +min-indent@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" + integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== + minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" @@ -7263,11 +7223,6 @@ modify-values@^1.0.0: resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022" integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw== -moo@^0.5.0: - version "0.5.1" - resolved "https://registry.yarnpkg.com/moo/-/moo-0.5.1.tgz#7aae7f384b9b09f620b6abf6f74ebbcd1b65dbc4" - integrity sha512-I1mnb5xn4fO80BH9BLcF0yLypy2UKl+Cb01Fu0hJRkJjlCRtxZMWkTdAtDd5ZqCOxtCkhmRwyI57vWT+1iZ67w== - move-concurrently@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92" @@ -7340,17 +7295,6 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= -nearley@^2.7.10: - version "2.19.1" - resolved "https://registry.yarnpkg.com/nearley/-/nearley-2.19.1.tgz#4af4006e16645ff800e9f993c3af039857d9dbdc" - integrity sha512-xq47GIUGXxU9vQg7g/y1o1xuKnkO7ev4nRWqftmQrLkfnE/FjRqDaGOUakM8XHPn/6pW3bGjU2wgoJyId90rqg== - dependencies: - commander "^2.19.0" - moo "^0.5.0" - railroad-diagrams "^1.0.0" - randexp "0.4.6" - semver "^5.4.1" - negotiator@0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" @@ -7815,19 +7759,11 @@ object-inspect@^1.7.0: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67" integrity sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw== -object-is@^1.0.1, object-is@^1.0.2: +object-is@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.0.2.tgz#6b80eb84fe451498f65007982f035a5b445edec4" integrity sha512-Epah+btZd5wrrfjkJZq1AOB9O6OxUQto45hzFd7lXGrpHPGE0W1k+426yrZV+k6NJOzLNNW/nVsmZdIWsAqoOQ== -object-is@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.2.tgz#c5d2e87ff9e119f78b7a088441519e2eec1573b6" - integrity sha512-5lHCz+0uufF6wZ7CRFWJN3hp8Jqblpgve06U5CMQ3f//6iDjPr2PEo9MWCjEssDsa+UZEL4PkFpr+BMop6aKzQ== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.5" - object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" @@ -7850,16 +7786,6 @@ object.assign@^4.1.0: has-symbols "^1.0.0" object-keys "^1.0.11" -object.entries@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.1.tgz#ee1cf04153de02bb093fec33683900f57ce5399b" - integrity sha512-ilqR7BgdyZetJutmDPfXCDffGa0/Yzl2ivVNpbx/g4UeWrCdRnFDUBrKJGLhGieRHDATnyZXWBeCb29k9CJysQ== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.0-next.1" - function-bind "^1.1.1" - has "^1.0.3" - object.entries@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.2.tgz#bc73f00acb6b6bb16c203434b10f9a7e797d3add" @@ -8271,13 +8197,6 @@ parse5@5.1.1: resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178" integrity sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug== -parse5@^3.0.1: - version "3.0.3" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.3.tgz#042f792ffdd36851551cf4e9e066b3874ab45b5c" - integrity sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA== - dependencies: - "@types/node" "*" - parseurl@~1.3.2, parseurl@~1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" @@ -8508,6 +8427,16 @@ pretty-format@^25.2.1, pretty-format@^25.3.0: ansi-styles "^4.0.0" react-is "^16.12.0" +pretty-format@^26.0.0, pretty-format@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.6.2.tgz#e35c2705f14cb7fe2fe94fa078345b444120fc93" + integrity sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg== + dependencies: + "@jest/types" "^26.6.2" + ansi-regex "^5.0.0" + ansi-styles "^4.0.0" + react-is "^17.0.1" + pretty-format@^26.4.0: version "26.4.0" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.4.0.tgz#c08073f531429e9e5024049446f42ecc9f933a3b" @@ -8561,15 +8490,6 @@ promzard@^0.3.0: dependencies: read "1" -prop-types-exact@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/prop-types-exact/-/prop-types-exact-1.2.0.tgz#825d6be46094663848237e3925a98c6e944e9869" - integrity sha512-K+Tk3Kd9V0odiXFP9fwDHUYRyvK3Nun3GVyPapSIs5OBkITAm15W0CPFD/YKTkMUAbc0b9CUwRQp2ybiBIq+eA== - dependencies: - has "^1.0.3" - object.assign "^4.1.0" - reflect.ownkeys "^0.2.0" - prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" @@ -8728,26 +8648,6 @@ qw@~1.0.1: resolved "https://registry.yarnpkg.com/qw/-/qw-1.0.1.tgz#efbfdc740f9ad054304426acb183412cc8b996d4" integrity sha1-77/cdA+a0FQwRCassYNBLMi5ltQ= -raf@^3.4.1: - version "3.4.1" - resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" - integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA== - dependencies: - performance-now "^2.1.0" - -railroad-diagrams@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz#eb7e6267548ddedfb899c1b90e57374559cddb7e" - integrity sha1-635iZ1SN3t+4mcG5Dlc3RVnN234= - -randexp@0.4.6: - version "0.4.6" - resolved "https://registry.yarnpkg.com/randexp/-/randexp-0.4.6.tgz#e986ad5e5e31dae13ddd6f7b3019aa7c87f60ca3" - integrity sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ== - dependencies: - discontinuous-range "1.0.0" - ret "~0.1.10" - randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" @@ -8812,31 +8712,21 @@ react-hot-loader@^4.12.21: shallowequal "^1.1.0" source-map "^0.7.3" -react-is@^16.12.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.6: +react-is@^16.12.0, react-is@^16.7.0, react-is@^16.8.1: version "16.13.0" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.0.tgz#0f37c3613c34fe6b37cd7f763a0d6293ab15c527" integrity sha512-GFMtL0vHkiBv9HluwNZTggSn/sCyEt9n02aM0dSAjGGyqyNlAyftYm4phPxdvCigG15JreC5biwxCgTAJZ7yAA== -react-is@^16.13.1: - version "16.13.1" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" - integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== +react-is@^17.0.1: + version "17.0.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339" + integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA== react-lifecycles-compat@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== -react-test-renderer@^16.0.0-0: - version "16.13.0" - resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.13.0.tgz#39ba3bf72cedc8210c3f81983f0bb061b14a3014" - integrity sha512-NQ2S9gdMUa7rgPGpKGyMcwl1d6D9MCF0lftdI3kts6kkiX+qvpC955jNjAZXlIDTjnN9jwFI8A8XhRh/9v0spA== - dependencies: - object-assign "^4.1.1" - prop-types "^15.6.2" - react-is "^16.8.6" - scheduler "^0.19.0" - react@^16.13.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e" @@ -9014,6 +8904,14 @@ redent@^2.0.0: indent-string "^3.0.0" strip-indent "^2.0.0" +redent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" + integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg== + dependencies: + indent-string "^4.0.0" + strip-indent "^3.0.0" + redeyed@~2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/redeyed/-/redeyed-2.1.1.tgz#8984b5815d99cb220469c99eeeffe38913e6cc0b" @@ -9021,11 +8919,6 @@ redeyed@~2.1.0: dependencies: esprima "~4.0.0" -reflect.ownkeys@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460" - integrity sha1-dJrO7H8/34tj+SegSAnpDFwLNGA= - regenerator-runtime@0.13.5: version "0.13.5" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz#d878a1d094b4306d10b9096484b33ebd55e26697" @@ -9297,14 +9190,6 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^3.0.0" inherits "^2.0.1" -rst-selector-parser@^2.2.3: - version "2.2.3" - resolved "https://registry.yarnpkg.com/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz#81b230ea2fcc6066c89e3472de794285d9b03d91" - integrity sha1-gbIw6i/MYGbInjRy3nlChdmwPZE= - dependencies: - lodash.flattendeep "^4.4.0" - nearley "^2.7.10" - rsvp@^4.8.4: version "4.8.5" resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734" @@ -9373,14 +9258,6 @@ saxes@^5.0.0: dependencies: xmlchars "^2.2.0" -scheduler@^0.19.0: - version "0.19.0" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.19.0.tgz#a715d56302de403df742f4a9be11975b32f5698d" - integrity sha512-xowbVaTPe9r7y7RUejcK73/j8tt2jfiyTednOvHbA8JoClvMYCp+r8QegLwK/n8zWQAtZb1fFnER4XLBZXrCxA== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - scheduler@^0.19.1: version "0.19.1" resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.19.1.tgz#4f3e2ed2c1a7d65681f4c854fa8c5a1ccb40f196" @@ -9477,7 +9354,7 @@ semver-regex@^2.0.0: resolved "https://registry.yarnpkg.com/semver-regex/-/semver-regex-2.0.0.tgz#a93c2c5844539a770233379107b38c7b4ac9d338" integrity sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw== -"semver@2 || 3 || 4 || 5", "semver@2.x || 3.x || 4 || 5", "semver@^2.3.0 || 3.x || 4 || 5", semver@^5.0.3, semver@^5.1.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.0, semver@^5.7.1: +"semver@2 || 3 || 4 || 5", "semver@2.x || 3.x || 4 || 5", "semver@^2.3.0 || 3.x || 4 || 5", semver@^5.0.3, semver@^5.1.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.1: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== @@ -9801,6 +9678,14 @@ source-map-resolve@^0.5.0: source-map-url "^0.4.0" urix "^0.1.0" +source-map-resolve@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.6.0.tgz#3d9df87e236b53f16d01e58150fc7711138e5ed2" + integrity sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w== + dependencies: + atob "^2.1.2" + decode-uri-component "^0.2.0" + source-map-support@^0.5.3, source-map-support@^0.5.6, source-map-support@~0.5.12: version "0.5.16" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.16.tgz#0ae069e7fe3ba7538c64c98515e35339eac5a042" @@ -10093,15 +9978,6 @@ string.prototype.matchall@^4.0.2: regexp.prototype.flags "^1.3.0" side-channel "^1.0.2" -string.prototype.trim@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.1.tgz#141233dff32c82bfad80684d7e5f0869ee0fb782" - integrity sha512-MjGFEeqixw47dAMFMtgUro/I0+wNqZB5GKXGt1fFr24u3TzDXCPu7J9Buppzoe3r/LqkSDLDDJzE15RGWDGAVw== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.0-next.1" - function-bind "^1.1.1" - string.prototype.trimend@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913" @@ -10220,6 +10096,13 @@ strip-indent@^2.0.0: resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68" integrity sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g= +strip-indent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" + integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ== + dependencies: + min-indent "^1.0.0" + strip-json-comments@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.0.tgz#7638d31422129ecf4457440009fba03f9f9ac180"