feat(input-amount): add types

This commit is contained in:
Joren Broekema 2020-09-14 18:36:23 +02:00 committed by Thomas Allmer
parent b9327627c6
commit 3f50c798f1
8 changed files with 121 additions and 55 deletions

View file

@ -0,0 +1,5 @@
---
'@lion/input-amount': minor
---
InputAmount component is now typed properly.

3
packages/input-amount/index.d.ts vendored Normal file
View file

@ -0,0 +1,3 @@
export { LionInputAmount } from './src/LionInputAmount.js';
export { formatAmount } from './src/formatters.js';
export { parseAmount } from './src/parsers.js';

View file

@ -11,6 +11,7 @@ import { parseAmount } from './parsers.js';
* @customElement lion-input-amount * @customElement lion-input-amount
* @extends {LionInput} * @extends {LionInput}
*/ */
// @ts-expect-error false positive for incompatible static get properties. Lit-element merges super properties already for you.
export class LionInputAmount extends LocalizeMixin(LionInput) { export class LionInputAmount extends LocalizeMixin(LionInput) {
static get properties() { static get properties() {
return { return {
@ -18,14 +19,12 @@ export class LionInputAmount extends LocalizeMixin(LionInput) {
* @desc an iso code like 'EUR' or 'USD' that will be displayed next to the input * @desc an iso code like 'EUR' or 'USD' that will be displayed next to the input
* and from which an accessible label (like 'euros') is computed for screen * and from which an accessible label (like 'euros') is computed for screen
* reader users * reader users
* @type {string}
*/ */
currency: String, currency: String,
/** /**
* @desc the modelValue of the input-amount has the 'Number' type. This allows * @desc the modelValue of the input-amount has the 'Number' type. This allows
* Application Developers to easily read from and write to this input or write custom * Application Developers to easily read from and write to this input or write custom
* validators. * validators.
* @type {number}
*/ */
modelValue: Number, modelValue: Number,
}; };
@ -55,7 +54,7 @@ export class LionInputAmount extends LocalizeMixin(LionInput) {
static get styles() { static get styles() {
return [ return [
...super.styles, super.styles,
css` css`
.input-group__container > .input-group__input ::slotted(.form-control) { .input-group__container > .input-group__input ::slotted(.form-control) {
text-align: right; text-align: right;
@ -68,6 +67,8 @@ export class LionInputAmount extends LocalizeMixin(LionInput) {
super(); super();
this.parser = parseAmount; this.parser = parseAmount;
this.formatter = formatAmount; this.formatter = formatAmount;
/** @type {string | undefined} */
this.currency = undefined;
this.__isPasting = false; this.__isPasting = false;
this.addEventListener('paste', () => { this.addEventListener('paste', () => {
@ -89,9 +90,10 @@ export class LionInputAmount extends LocalizeMixin(LionInput) {
} }
} }
/** @param {import('lit-element').PropertyValues } changedProperties */
updated(changedProperties) { updated(changedProperties) {
super.updated(changedProperties); super.updated(changedProperties);
if (changedProperties.has('currency')) { if (changedProperties.has('currency') && this.currency) {
this._onCurrencyChanged({ currency: this.currency }); this._onCurrencyChanged({ currency: this.currency });
} }
} }
@ -114,12 +116,16 @@ export class LionInputAmount extends LocalizeMixin(LionInput) {
return super._reflectBackOn() || this.__isPasting; return super._reflectBackOn() || this.__isPasting;
} }
/**
* @param {Object} opts
* @param {string} opts.currency
*/
_onCurrencyChanged({ currency }) { _onCurrencyChanged({ currency }) {
if (this._isPrivateSlot('after')) { if (this._isPrivateSlot('after') && this._currencyDisplayNode) {
this._currencyDisplayNode.textContent = this.__currencyLabel; this._currencyDisplayNode.textContent = this.__currencyLabel;
} }
this.formatOptions.currency = currency; this.formatOptions.currency = currency;
this._calculateValues(); this._calculateValues({ source: null });
this.__setCurrencyDisplayLabel(); this.__setCurrencyDisplayLabel();
} }
@ -127,10 +133,12 @@ export class LionInputAmount extends LocalizeMixin(LionInput) {
// TODO: (@erikkroes) for optimal a11y, abbreviations should be part of aria-label // TODO: (@erikkroes) for optimal a11y, abbreviations should be part of aria-label
// example, for a language switch with text 'en', an aria-label of 'english' is not // example, for a language switch with text 'en', an aria-label of 'english' is not
// sufficient, it should also contain the abbreviation. // sufficient, it should also contain the abbreviation.
this._currencyDisplayNode.setAttribute('aria-label', getCurrencyName(this.currency)); if (this.currency && this._currencyDisplayNode) {
this._currencyDisplayNode.setAttribute('aria-label', getCurrencyName(this.currency, {}));
}
} }
get __currencyLabel() { get __currencyLabel() {
return formatCurrencyLabel(this.currency, localize.locale); return this.currency ? formatCurrencyLabel(this.currency, localize.locale) : '';
} }
} }

View file

@ -1,19 +1,22 @@
import { formatNumber, getFractionDigits, normalizeCurrencyLabel } from '@lion/localize'; import { formatNumber, getFractionDigits, normalizeCurrencyLabel } from '@lion/localize';
/**
* @typedef {import('@lion/localize/types/LocalizeMixinTypes').FormatNumberOptions} FormatOptions
*/
/** /**
* Formats a number considering the default fraction digits provided by Intl. * Formats a number considering the default fraction digits provided by Intl.
* *
* @param {float} modelValue Number to format * @param {number} modelValue Number to format
* @param {object} givenOptions Options for Intl * @param {FormatOptions} [givenOptions]
*/ */
export function formatAmount(modelValue, givenOptions) { export function formatAmount(modelValue, givenOptions) {
if (modelValue === '') { /** @type {FormatOptions} */
return '';
}
const options = { const options = {
currency: 'EUR', currency: 'EUR',
...givenOptions, ...givenOptions,
}; };
if (typeof options.minimumFractionDigits === 'undefined') { if (typeof options.minimumFractionDigits === 'undefined') {
options.minimumFractionDigits = getFractionDigits(options.currency); options.minimumFractionDigits = getFractionDigits(options.currency);
} }
@ -24,6 +27,11 @@ export function formatAmount(modelValue, givenOptions) {
return formatNumber(modelValue, options); return formatNumber(modelValue, options);
} }
/**
*
* @param {string} currency
* @param {string} locale
*/
export function formatCurrencyLabel(currency, locale) { export function formatCurrencyLabel(currency, locale) {
if (currency === '') { if (currency === '') {
return ''; return '';

View file

@ -64,15 +64,19 @@ function getParseMode(value, { mode = 'auto' } = {}) {
* parseWithLocale('1,234', { locale: 'en-GB' }) => 1234 * parseWithLocale('1,234', { locale: 'en-GB' }) => 1234
* *
* @param {string} value Number to be parsed * @param {string} value Number to be parsed
* @param {object} options Locale Options * @param {Object} options Locale Options
* @param {string} [options.locale]
*/ */
function parseWithLocale(value, options) { function parseWithLocale(value, options) {
const locale = options && options.locale ? options.locale : null; const locale = options && options.locale ? options.locale : null;
const separator = getDecimalSeparator(locale); const separator = getDecimalSeparator(locale);
const regexNumberAndLocaleSeparator = new RegExp(`[0-9${separator}-]`, 'g'); const regexNumberAndLocaleSeparator = new RegExp(`[0-9${separator}-]`, 'g');
let numberAndLocaleSeparator = value.match(regexNumberAndLocaleSeparator).join(''); let numberAndLocaleSeparator = value.match(regexNumberAndLocaleSeparator)?.join('');
if (separator === ',') { if (separator === ',') {
numberAndLocaleSeparator = numberAndLocaleSeparator.replace(',', '.'); numberAndLocaleSeparator = numberAndLocaleSeparator?.replace(',', '.');
}
if (!numberAndLocaleSeparator) {
return NaN;
} }
return parseFloat(numberAndLocaleSeparator); return parseFloat(numberAndLocaleSeparator);
} }
@ -84,7 +88,7 @@ function parseWithLocale(value, options) {
* Warning: This function works only with numbers that can be heuristically parsed. * Warning: This function works only with numbers that can be heuristically parsed.
* *
* @param {string} value Number that can be heuristically parsed * @param {string} value Number that can be heuristically parsed
* @return {float} parsed javascript number * @return {number} parsed javascript number
*/ */
function parseHeuristic(value) { function parseHeuristic(value) {
if (value.match(/[0-9., ]/g)) { if (value.match(/[0-9., ]/g)) {
@ -115,7 +119,7 @@ function parseHeuristic(value) {
* parseAmount('1,234.56'); // method: heuristic => 1234.56 * parseAmount('1,234.56'); // method: heuristic => 1234.56
* *
* @param {string} value Number to be parsed * @param {string} value Number to be parsed
* @param {object} options Locale Options * @param {object} [options] Locale Options
*/ */
export function parseAmount(value, options) { export function parseAmount(value, options) {
const containsNumbers = value.match(/\d/g); const containsNumbers = value.match(/\d/g);
@ -129,10 +133,15 @@ export function parseAmount(value, options) {
const cleanedInput = matchedInput.join(''); const cleanedInput = matchedInput.join('');
const parseMode = getParseMode(cleanedInput, options); const parseMode = getParseMode(cleanedInput, options);
switch (parseMode) { switch (parseMode) {
case 'unparseable': case 'unparseable': {
return parseFloat(cleanedInput.match(/[0-9]/g).join('')); const cleanedInputMatchStr = cleanedInput.match(/[0-9]/g)?.join('');
if (!cleanedInputMatchStr) {
return NaN;
}
return parseFloat(cleanedInputMatchStr);
}
case 'withLocale': case 'withLocale':
return parseWithLocale(cleanedInput, options); return parseWithLocale(cleanedInput, options || {});
case 'heuristic': case 'heuristic':
return parseHeuristic(cleanedInput); return parseHeuristic(cleanedInput);
default: default:

View file

@ -7,7 +7,6 @@ const tagString = 'lion-input-amount';
describe('<lion-input-amount> integrations', () => { describe('<lion-input-amount> integrations', () => {
runInteractionStateMixinSuite({ runInteractionStateMixinSuite({
tagString, tagString,
suffix: 'lion-input-amount',
allowedModelValueTypes: [Number], allowedModelValueTypes: [Number],
}); });

View file

@ -6,34 +6,40 @@ import '../lion-input-amount.js';
import { formatAmount } from '../src/formatters.js'; import { formatAmount } from '../src/formatters.js';
import { parseAmount } from '../src/parsers.js'; import { parseAmount } from '../src/parsers.js';
/**
* @typedef {import('../src/LionInputAmount').LionInputAmount} LionInputAmount
*/
describe('<lion-input-amount>', () => { describe('<lion-input-amount>', () => {
beforeEach(() => { beforeEach(() => {
localizeTearDown(); localizeTearDown();
}); });
it('uses formatAmount for formatting', async () => { it('uses formatAmount for formatting', async () => {
const el = await fixture(`<lion-input-amount></lion-input-amount>`); const el = /** @type {LionInputAmount} */ (await fixture(
`<lion-input-amount></lion-input-amount>`,
));
expect(el.formatter).to.equal(formatAmount); expect(el.formatter).to.equal(formatAmount);
}); });
it('formatAmount uses currency provided on webcomponent', async () => { it('formatAmount uses currency provided on webcomponent', async () => {
// JOD displays 3 fraction digits by default // JOD displays 3 fraction digits by default
localize.locale = 'fr-FR'; localize.locale = 'fr-FR';
const el = await fixture( const el = /** @type {LionInputAmount} */ (await fixture(
html`<lion-input-amount currency="JOD" .modelValue="${123}"></lion-input-amount>`, html`<lion-input-amount currency="JOD" .modelValue="${123}"></lion-input-amount>`,
); ));
expect(el.formattedValue).to.equal('123,000'); expect(el.formattedValue).to.equal('123,000');
}); });
it('formatAmount uses locale provided in formatOptions', async () => { it('formatAmount uses locale provided in formatOptions', async () => {
let el = await fixture( let el = /** @type {LionInputAmount} */ (await fixture(
html` html`
<lion-input-amount <lion-input-amount
.formatOptions="${{ locale: 'en-GB' }}" .formatOptions="${{ locale: 'en-GB' }}"
.modelValue="${123}" .modelValue="${123}"
></lion-input-amount> ></lion-input-amount>
`, `,
); ));
expect(el.formattedValue).to.equal('123.00'); expect(el.formattedValue).to.equal('123.00');
el = await fixture( el = await fixture(
html` html`
@ -47,27 +53,33 @@ describe('<lion-input-amount>', () => {
}); });
it('ignores global locale change if property is provided', async () => { it('ignores global locale change if property is provided', async () => {
const el = await fixture(html` const el = /** @type {LionInputAmount} */ (await fixture(html`
<lion-input-amount .modelValue=${123456.78} .locale="${'en-GB'}"></lion-input-amount> <lion-input-amount .modelValue=${123456.78} .locale="${'en-GB'}"></lion-input-amount>
`); `));
expect(el.formattedValue).to.equal('123,456.78'); // British expect(el.formattedValue).to.equal('123,456.78'); // British
localize.locale = 'nl-NL'; localize.locale = 'nl-NL';
await aTimeout(); await aTimeout(0);
expect(el.formattedValue).to.equal('123,456.78'); // should stay British expect(el.formattedValue).to.equal('123,456.78'); // should stay British
}); });
it('uses parseAmount for parsing', async () => { it('uses parseAmount for parsing', async () => {
const el = await fixture(`<lion-input-amount></lion-input-amount>`); const el = /** @type {LionInputAmount} */ (await fixture(
`<lion-input-amount></lion-input-amount>`,
));
expect(el.parser).to.equal(parseAmount); expect(el.parser).to.equal(parseAmount);
}); });
it('sets inputmode attribute to decimal', async () => { it('sets inputmode attribute to decimal', async () => {
const el = await fixture(`<lion-input-amount></lion-input-amount>`); const el = /** @type {LionInputAmount} */ (await fixture(
`<lion-input-amount></lion-input-amount>`,
));
expect(el._inputNode.inputMode).to.equal('decimal'); expect(el._inputNode.inputMode).to.equal('decimal');
}); });
it('has type="text" to activate default keyboard on mobile with all necessary symbols', async () => { it('has type="text" to activate default keyboard on mobile with all necessary symbols', async () => {
const el = await fixture(`<lion-input-amount></lion-input-amount>`); const el = /** @type {LionInputAmount} */ (await fixture(
`<lion-input-amount></lion-input-amount>`,
));
expect(el._inputNode.type).to.equal('text'); expect(el._inputNode.type).to.equal('text');
}); });
@ -77,53 +89,74 @@ describe('<lion-input-amount>', () => {
}); });
it('displays currency if provided', async () => { it('displays currency if provided', async () => {
const el = await fixture(`<lion-input-amount currency="EUR"></lion-input-amount>`); const el = /** @type {LionInputAmount} */ (await fixture(
expect(Array.from(el.children).find(child => child.slot === 'after').innerText).to.equal('EUR'); `<lion-input-amount currency="EUR"></lion-input-amount>`,
));
expect(
/** @type {HTMLElement[]} */ (Array.from(el.children)).find(child => child.slot === 'after')
?.innerText,
).to.equal('EUR');
}); });
it('displays correct currency for TRY if locale is tr-TR', async () => { it('displays correct currency for TRY if locale is tr-TR', async () => {
localize.locale = 'tr-TR'; localize.locale = 'tr-TR';
const el = await fixture(`<lion-input-amount currency="TRY"></lion-input-amount>`); const el = /** @type {LionInputAmount} */ (await fixture(
expect(Array.from(el.children).find(child => child.slot === 'after').innerText).to.equal('TL'); `<lion-input-amount currency="TRY"></lion-input-amount>`,
));
expect(
/** @type {HTMLElement[]} */ (Array.from(el.children)).find(child => child.slot === 'after')
?.innerText,
).to.equal('TL');
}); });
it('can update currency', async () => { it('can update currency', async () => {
const el = await fixture(`<lion-input-amount currency="EUR"></lion-input-amount>`); const el = /** @type {LionInputAmount} */ (await fixture(
`<lion-input-amount currency="EUR"></lion-input-amount>`,
));
el.currency = 'USD'; el.currency = 'USD';
await el.updateComplete; await el.updateComplete;
expect(Array.from(el.children).find(child => child.slot === 'after').innerText).to.equal('USD'); expect(
/** @type {HTMLElement[]} */ (Array.from(el.children)).find(child => child.slot === 'after')
?.innerText,
).to.equal('USD');
}); });
it('ignores currency if a suffix is already present', async () => { it('ignores currency if a suffix is already present', async () => {
const el = await fixture( const el = /** @type {LionInputAmount} */ (await fixture(
`<lion-input-amount currency="EUR"><span slot="suffix">my-currency</span></lion-input-amount>`, `<lion-input-amount currency="EUR"><span slot="suffix">my-currency</span></lion-input-amount>`,
); ));
expect(Array.from(el.children).find(child => child.slot === 'suffix').innerText).to.equal( expect(
'my-currency', /** @type {HTMLElement[]} */ (Array.from(el.children)).find(child => child.slot === 'suffix')
); ?.innerText,
).to.equal('my-currency');
el.currency = 'EUR'; el.currency = 'EUR';
await el.updateComplete; await el.updateComplete;
expect(Array.from(el.children).find(child => child.slot === 'suffix').innerText).to.equal( expect(
'my-currency', /** @type {HTMLElement[]} */ (Array.from(el.children)).find(child => child.slot === 'suffix')
); ?.innerText,
).to.equal('my-currency');
}); });
describe('Accessibility', () => { describe('Accessibility', () => {
it('adds currency id to aria-labelledby of input', async () => { it('adds currency id to aria-labelledby of input', async () => {
const el = await fixture(`<lion-input-amount currency="EUR"></lion-input-amount>`); const el = /** @type {LionInputAmount} */ (await fixture(
expect(el._currencyDisplayNode.getAttribute('data-label')).to.be.not.null; `<lion-input-amount currency="EUR"></lion-input-amount>`,
expect(el._inputNode.getAttribute('aria-labelledby')).to.contain(el._currencyDisplayNode.id); ));
expect(el._currencyDisplayNode?.getAttribute('data-label')).to.be.not.null;
expect(el._inputNode.getAttribute('aria-labelledby')).to.contain(el._currencyDisplayNode?.id);
}); });
it('adds an aria-label to currency slot', async () => { it('adds an aria-label to currency slot', async () => {
const el = await fixture(`<lion-input-amount currency="EUR"></lion-input-amount>`); const el = /** @type {LionInputAmount} */ (await fixture(
expect(el._currencyDisplayNode.getAttribute('aria-label')).to.equal('euros'); `<lion-input-amount currency="EUR"></lion-input-amount>`,
));
expect(el._currencyDisplayNode?.getAttribute('aria-label')).to.equal('euros');
el.currency = 'USD'; el.currency = 'USD';
await el.updateComplete; await el.updateComplete;
expect(el._currencyDisplayNode.getAttribute('aria-label')).to.equal('US dollars'); expect(el._currencyDisplayNode?.getAttribute('aria-label')).to.equal('US dollars');
el.currency = 'PHP'; el.currency = 'PHP';
await el.updateComplete; await el.updateComplete;
expect(el._currencyDisplayNode.getAttribute('aria-label')).to.equal('Philippine pisos'); expect(el._currencyDisplayNode?.getAttribute('aria-label')).to.equal('Philippine pisos');
}); });
}); });

View file

@ -24,7 +24,8 @@
"packages/tooltip/**/*.js", "packages/tooltip/**/*.js",
"packages/button/src/**/*.js", "packages/button/src/**/*.js",
"packages/listbox/src/*.js", "packages/listbox/src/*.js",
"packages/input/**/*.js" "packages/input/**/*.js",
"packages/input-amount/**/*.js"
], ],
"exclude": [ "exclude": [
"node_modules", "node_modules",