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 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. 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. 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 * @return {number} new value with rounded up decimals
*/ */
function round(value, 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(value);
} }
return Number(`${Math.round(Number(`${value}e${decimals}`))}e-${decimals}`); return Number(`${Math.round(Number(`${value}e${decimals}`))}e-${decimals}`);
@ -30,10 +31,17 @@ function round(value, decimals) {
* @param {FormatOptions} [givenOptions] Locale Options * @param {FormatOptions} [givenOptions] Locale Options
*/ */
export function parseAmount(value, givenOptions) { 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); const number = parseNumber(value, givenOptions);
if (typeof number !== 'number') { if (typeof number !== 'number' || Number.isNaN(number)) {
return number; return undefined;
} }
/** @type {FormatOptions} */ /** @type {FormatOptions} */

View file

@ -74,4 +74,15 @@ describe('formatAmount()', () => {
localizeManager.locale = 'nl-NL'; localizeManager.locale = 'nl-NL';
expect(formatAmount(12345678)).to.equal('12.345.678,00'); 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 { 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'; import { parseAmount } from '@lion/ui/input-amount.js';
describe('parseAmount()', async () => { describe('parseAmount()', async () => {
const localizeManager = getLocalizeManager();
beforeEach(() => {
localizeManager.locale = 'en-GB';
});
afterEach(() => {
localizeTearDown();
});
it('with currency set to correct amount of decimals', async () => { it('with currency set to correct amount of decimals', async () => {
localize.locale = 'en-GB';
expect( expect(
parseAmount('1.015', { parseAmount('1.015', {
currency: 'EUR', currency: 'EUR',
@ -29,7 +38,26 @@ describe('parseAmount()', async () => {
}); });
it('with no currency keeps all decimals', async () => { it('with no currency keeps all decimals', async () => {
localize.locale = 'en-GB';
expect(parseAmount('1.015')).to.equal(1.015); 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 = ''; let printNumberOfParts = '';
// update numberOfParts because there may be some parts added // 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) { for (let i = 0; i < numberOfParts; i += 1) {
const part = /** @type {FormatNumberPart} */ (formattedToParts[i]); const part = /** @type {FormatNumberPart} */ (formattedToParts[i]);
printNumberOfParts += part.value; 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('normalization', () => {
describe('en-GB', () => { describe('en-GB', () => {
it('supports basics', () => { it('supports basics', () => {