diff --git a/.changeset/ninety-rats-retire.md b/.changeset/ninety-rats-retire.md new file mode 100644 index 0000000000..d6e3ab7295 --- /dev/null +++ b/.changeset/ninety-rats-retire.md @@ -0,0 +1,5 @@ +--- +'@lion/ui': patch +--- + +[input-amount] returns Unparseable as a modelValue if a wrong value has been entered diff --git a/docs/components/input-amount/overview.md b/docs/components/input-amount/overview.md index 158121389f..8a2c988c6a 100644 --- a/docs/components/input-amount/overview.md +++ b/docs/components/input-amount/overview.md @@ -4,7 +4,7 @@ A web component based on the generic text input field. Its purpose is to provide For formatting, we use [Intl NumberFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/NumberFormat) with some overrides. -For parsing user input, we provide our own parser that takes into account a number of heuristics, locale and ignores invalid characters. +For parsing user input, we provide our own parser that takes into account locale, a number of heuristics and allows for pasting amount strings like 'EUR 100,00'. Valid characters are digits and separators. Formatting happens on-blur. If there are no valid characters in the input whatsoever, it will provide an error feedback. diff --git a/packages/ui/components/input-amount/src/parsers.js b/packages/ui/components/input-amount/src/parsers.js index be422f2c2c..ecd80dcc11 100644 --- a/packages/ui/components/input-amount/src/parsers.js +++ b/packages/ui/components/input-amount/src/parsers.js @@ -11,7 +11,8 @@ import { parseNumber, getFractionDigits } from '@lion/ui/localize-no-side-effect * @return {number} new value with rounded up decimals */ function round(value, decimals) { - if (typeof decimals === 'undefined') { + const numberContainsExponent = value?.toString().includes('e'); + if (typeof decimals === 'undefined' || numberContainsExponent) { return Number(value); } return Number(`${Math.round(Number(`${value}e${decimals}`))}e-${decimals}`); @@ -30,10 +31,17 @@ function round(value, decimals) { * @param {FormatOptions} [givenOptions] Locale Options */ export function parseAmount(value, givenOptions) { + const unmatchedInput = value.match(/[^0-9,.\- ]/g); + // for the full paste behavior documentation: + // ./docs/components/input-amount/use-cases.md#paste-behavior + if (unmatchedInput && givenOptions?.mode !== 'pasted') { + return undefined; + } + const number = parseNumber(value, givenOptions); - if (typeof number !== 'number') { - return number; + if (typeof number !== 'number' || Number.isNaN(number)) { + return undefined; } /** @type {FormatOptions} */ diff --git a/packages/ui/components/input-amount/test/formatters.test.js b/packages/ui/components/input-amount/test/formatters.test.js index feefc3f8b4..0488b7ae8c 100644 --- a/packages/ui/components/input-amount/test/formatters.test.js +++ b/packages/ui/components/input-amount/test/formatters.test.js @@ -74,4 +74,15 @@ describe('formatAmount()', () => { localizeManager.locale = 'nl-NL'; expect(formatAmount(12345678)).to.equal('12.345.678,00'); }); + + // TODO: make it work with big numbers, e.g. make use of BigInt + it.skip('rounds up big numbers', async () => { + expect(formatAmount(1e21, { locale: 'en-GB', currency: 'EUR' })).to.equal( + '1,000,000,000,000,000,000,000.00', + ); + // eslint-disable-next-line no-loss-of-precision + expect(formatAmount(12345678987654321.42, { locale: 'en-GB', currency: 'EUR' })).to.equal( + '12,345,678,987,654,321.42', + ); + }); }); diff --git a/packages/ui/components/input-amount/test/parsers.test.js b/packages/ui/components/input-amount/test/parsers.test.js index 2af83baabc..6cd58449c5 100644 --- a/packages/ui/components/input-amount/test/parsers.test.js +++ b/packages/ui/components/input-amount/test/parsers.test.js @@ -1,11 +1,20 @@ import { expect } from '@open-wc/testing'; -import { localize } from '@lion/ui/localize.js'; - +import { getLocalizeManager } from '@lion/ui/localize-no-side-effects.js'; +import { localizeTearDown } from '@lion/ui/localize-test-helpers.js'; import { parseAmount } from '@lion/ui/input-amount.js'; describe('parseAmount()', async () => { + const localizeManager = getLocalizeManager(); + + beforeEach(() => { + localizeManager.locale = 'en-GB'; + }); + + afterEach(() => { + localizeTearDown(); + }); + it('with currency set to correct amount of decimals', async () => { - localize.locale = 'en-GB'; expect( parseAmount('1.015', { currency: 'EUR', @@ -29,7 +38,26 @@ describe('parseAmount()', async () => { }); it('with no currency keeps all decimals', async () => { - localize.locale = 'en-GB'; expect(parseAmount('1.015')).to.equal(1.015); }); + + // TODO: make it work with big numbers, e.g. make use of BigInt + it.skip('rounds up big numbers', async () => { + // eslint-disable-next-line no-loss-of-precision + expect(parseAmount('999999999999999999999,42')).to.equal(999999999999999999999.42); + // eslint-disable-next-line no-loss-of-precision + expect(parseAmount('12,345,678,987,654,321.42')).to.equal(12345678987654321.42); + }); + + it('returns undefined if an invalid value is entered', async () => { + expect(parseAmount('foo')).to.equal(undefined); + expect(parseAmount('foo1')).to.equal(undefined); + expect(parseAmount('EUR 1,50')).to.equal(undefined); + expect(parseAmount('--1')).to.equal(undefined); + }); + + it('ignores letters when "pasted" mode used', async () => { + expect(parseAmount('foo1', { mode: 'pasted' })).to.equal(1); + expect(parseAmount('EUR 1,50', { mode: 'pasted' })).to.equal(1.5); + }); }); diff --git a/packages/ui/components/localize/src/number/formatNumber.js b/packages/ui/components/localize/src/number/formatNumber.js index 1c7ee984e1..e75decbaa9 100644 --- a/packages/ui/components/localize/src/number/formatNumber.js +++ b/packages/ui/components/localize/src/number/formatNumber.js @@ -26,7 +26,7 @@ export function formatNumber(number, options = /** @type {FormatOptions} */ ({}) } let printNumberOfParts = ''; // update numberOfParts because there may be some parts added - const numberOfParts = formattedToParts && formattedToParts.length; + const numberOfParts = formattedToParts ? formattedToParts.length : 0; for (let i = 0; i < numberOfParts; i += 1) { const part = /** @type {FormatNumberPart} */ (formattedToParts[i]); printNumberOfParts += part.value; diff --git a/packages/ui/components/localize/test/number/formatNumber.test.js b/packages/ui/components/localize/test/number/formatNumber.test.js index 8c5f963c28..7237defa24 100644 --- a/packages/ui/components/localize/test/number/formatNumber.test.js +++ b/packages/ui/components/localize/test/number/formatNumber.test.js @@ -246,6 +246,15 @@ Please specify .groupSeparator / .decimalSeparator on the formatOptions object t }); }); + // TODO: make it work with big numbers, e.g. make use of BigInt + it.skip('can handle big numbers', async () => { + expect(formatNumber(1e21)).to.equal('1,000,000,000,000,000,000,000'); + // eslint-disable-next-line no-loss-of-precision + expect(formatNumber(999999999999999999999.42)).to.equal('999,999,999,999,999,999,999.42'); + // eslint-disable-next-line no-loss-of-precision + expect(formatNumber(12345678987654321.42)).to.equal('12,345,678,987,654,321.42'); + }); + describe('normalization', () => { describe('en-GB', () => { it('supports basics', () => {