fix(input-amount): returns Unparseable as a modelValue if a wrong value has been entered (#2242)

* fix(input-amount): returns Unparseable as a modelValue if a wrong value has been entered

* fix(input-amount): do not break when a large amount has been entered

* Update docs/components/input-amount/overview.md

Co-authored-by: Thijs Louisse <thijs.louisse@ing.com>

* Update packages/ui/components/input-amount/test/parsers.test.js

Co-authored-by: Thijs Louisse <thijs.louisse@ing.com>

---------

Co-authored-by: Thijs Louisse <thijs.louisse@ing.com>
This commit is contained in:
gerjanvangeest 2024-04-10 11:20:48 +02:00 committed by GitHub
parent efcd4bf15b
commit e8e9c07ec5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 70 additions and 9 deletions

View file

@ -0,0 +1,5 @@
---
'@lion/ui': patch
---
[input-amount] returns Unparseable as a modelValue if a wrong value has been entered

View file

@ -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.

View file

@ -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} */

View file

@ -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',
);
});
});

View file

@ -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);
});
});

View file

@ -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;

View file

@ -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', () => {