feat(input-amount): add types
This commit is contained in:
parent
b9327627c6
commit
3f50c798f1
8 changed files with 121 additions and 55 deletions
5
.changeset/great-coins-pretend.md
Normal file
5
.changeset/great-coins-pretend.md
Normal 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
3
packages/input-amount/index.d.ts
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export { LionInputAmount } from './src/LionInputAmount.js';
|
||||
export { formatAmount } from './src/formatters.js';
|
||||
export { parseAmount } from './src/parsers.js';
|
||||
|
|
@ -11,6 +11,7 @@ import { parseAmount } from './parsers.js';
|
|||
* @customElement lion-input-amount
|
||||
* @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) {
|
||||
static get properties() {
|
||||
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
|
||||
* and from which an accessible label (like 'euros') is computed for screen
|
||||
* reader users
|
||||
* @type {string}
|
||||
*/
|
||||
currency: String,
|
||||
/**
|
||||
* @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
|
||||
* validators.
|
||||
* @type {number}
|
||||
*/
|
||||
modelValue: Number,
|
||||
};
|
||||
|
|
@ -55,7 +54,7 @@ export class LionInputAmount extends LocalizeMixin(LionInput) {
|
|||
|
||||
static get styles() {
|
||||
return [
|
||||
...super.styles,
|
||||
super.styles,
|
||||
css`
|
||||
.input-group__container > .input-group__input ::slotted(.form-control) {
|
||||
text-align: right;
|
||||
|
|
@ -68,6 +67,8 @@ export class LionInputAmount extends LocalizeMixin(LionInput) {
|
|||
super();
|
||||
this.parser = parseAmount;
|
||||
this.formatter = formatAmount;
|
||||
/** @type {string | undefined} */
|
||||
this.currency = undefined;
|
||||
this.__isPasting = false;
|
||||
|
||||
this.addEventListener('paste', () => {
|
||||
|
|
@ -89,9 +90,10 @@ export class LionInputAmount extends LocalizeMixin(LionInput) {
|
|||
}
|
||||
}
|
||||
|
||||
/** @param {import('lit-element').PropertyValues } changedProperties */
|
||||
updated(changedProperties) {
|
||||
super.updated(changedProperties);
|
||||
if (changedProperties.has('currency')) {
|
||||
if (changedProperties.has('currency') && this.currency) {
|
||||
this._onCurrencyChanged({ currency: this.currency });
|
||||
}
|
||||
}
|
||||
|
|
@ -114,12 +116,16 @@ export class LionInputAmount extends LocalizeMixin(LionInput) {
|
|||
return super._reflectBackOn() || this.__isPasting;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} opts
|
||||
* @param {string} opts.currency
|
||||
*/
|
||||
_onCurrencyChanged({ currency }) {
|
||||
if (this._isPrivateSlot('after')) {
|
||||
if (this._isPrivateSlot('after') && this._currencyDisplayNode) {
|
||||
this._currencyDisplayNode.textContent = this.__currencyLabel;
|
||||
}
|
||||
this.formatOptions.currency = currency;
|
||||
this._calculateValues();
|
||||
this._calculateValues({ source: null });
|
||||
this.__setCurrencyDisplayLabel();
|
||||
}
|
||||
|
||||
|
|
@ -127,10 +133,12 @@ export class LionInputAmount extends LocalizeMixin(LionInput) {
|
|||
// 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
|
||||
// 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() {
|
||||
return formatCurrencyLabel(this.currency, localize.locale);
|
||||
return this.currency ? formatCurrencyLabel(this.currency, localize.locale) : '';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,22 @@
|
|||
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.
|
||||
*
|
||||
* @param {float} modelValue Number to format
|
||||
* @param {object} givenOptions Options for Intl
|
||||
* @param {number} modelValue Number to format
|
||||
* @param {FormatOptions} [givenOptions]
|
||||
*/
|
||||
export function formatAmount(modelValue, givenOptions) {
|
||||
if (modelValue === '') {
|
||||
return '';
|
||||
}
|
||||
/** @type {FormatOptions} */
|
||||
const options = {
|
||||
currency: 'EUR',
|
||||
...givenOptions,
|
||||
};
|
||||
|
||||
if (typeof options.minimumFractionDigits === 'undefined') {
|
||||
options.minimumFractionDigits = getFractionDigits(options.currency);
|
||||
}
|
||||
|
|
@ -24,6 +27,11 @@ export function formatAmount(modelValue, givenOptions) {
|
|||
return formatNumber(modelValue, options);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} currency
|
||||
* @param {string} locale
|
||||
*/
|
||||
export function formatCurrencyLabel(currency, locale) {
|
||||
if (currency === '') {
|
||||
return '';
|
||||
|
|
|
|||
|
|
@ -64,15 +64,19 @@ function getParseMode(value, { mode = 'auto' } = {}) {
|
|||
* parseWithLocale('1,234', { locale: 'en-GB' }) => 1234
|
||||
*
|
||||
* @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) {
|
||||
const locale = options && options.locale ? options.locale : null;
|
||||
const separator = getDecimalSeparator(locale);
|
||||
const regexNumberAndLocaleSeparator = new RegExp(`[0-9${separator}-]`, 'g');
|
||||
let numberAndLocaleSeparator = value.match(regexNumberAndLocaleSeparator).join('');
|
||||
let numberAndLocaleSeparator = value.match(regexNumberAndLocaleSeparator)?.join('');
|
||||
if (separator === ',') {
|
||||
numberAndLocaleSeparator = numberAndLocaleSeparator.replace(',', '.');
|
||||
numberAndLocaleSeparator = numberAndLocaleSeparator?.replace(',', '.');
|
||||
}
|
||||
if (!numberAndLocaleSeparator) {
|
||||
return NaN;
|
||||
}
|
||||
return parseFloat(numberAndLocaleSeparator);
|
||||
}
|
||||
|
|
@ -84,7 +88,7 @@ function parseWithLocale(value, options) {
|
|||
* Warning: This function works only with numbers 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) {
|
||||
if (value.match(/[0-9., ]/g)) {
|
||||
|
|
@ -115,7 +119,7 @@ function parseHeuristic(value) {
|
|||
* parseAmount('1,234.56'); // method: heuristic => 1234.56
|
||||
*
|
||||
* @param {string} value Number to be parsed
|
||||
* @param {object} options Locale Options
|
||||
* @param {object} [options] Locale Options
|
||||
*/
|
||||
export function parseAmount(value, options) {
|
||||
const containsNumbers = value.match(/\d/g);
|
||||
|
|
@ -129,10 +133,15 @@ export function parseAmount(value, options) {
|
|||
const cleanedInput = matchedInput.join('');
|
||||
const parseMode = getParseMode(cleanedInput, options);
|
||||
switch (parseMode) {
|
||||
case 'unparseable':
|
||||
return parseFloat(cleanedInput.match(/[0-9]/g).join(''));
|
||||
case 'unparseable': {
|
||||
const cleanedInputMatchStr = cleanedInput.match(/[0-9]/g)?.join('');
|
||||
if (!cleanedInputMatchStr) {
|
||||
return NaN;
|
||||
}
|
||||
return parseFloat(cleanedInputMatchStr);
|
||||
}
|
||||
case 'withLocale':
|
||||
return parseWithLocale(cleanedInput, options);
|
||||
return parseWithLocale(cleanedInput, options || {});
|
||||
case 'heuristic':
|
||||
return parseHeuristic(cleanedInput);
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ const tagString = 'lion-input-amount';
|
|||
describe('<lion-input-amount> integrations', () => {
|
||||
runInteractionStateMixinSuite({
|
||||
tagString,
|
||||
suffix: 'lion-input-amount',
|
||||
allowedModelValueTypes: [Number],
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -6,34 +6,40 @@ import '../lion-input-amount.js';
|
|||
import { formatAmount } from '../src/formatters.js';
|
||||
import { parseAmount } from '../src/parsers.js';
|
||||
|
||||
/**
|
||||
* @typedef {import('../src/LionInputAmount').LionInputAmount} LionInputAmount
|
||||
*/
|
||||
|
||||
describe('<lion-input-amount>', () => {
|
||||
beforeEach(() => {
|
||||
localizeTearDown();
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
it('formatAmount uses currency provided on webcomponent', async () => {
|
||||
// JOD displays 3 fraction digits by default
|
||||
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>`,
|
||||
);
|
||||
));
|
||||
expect(el.formattedValue).to.equal('123,000');
|
||||
});
|
||||
|
||||
it('formatAmount uses locale provided in formatOptions', async () => {
|
||||
let el = await fixture(
|
||||
let el = /** @type {LionInputAmount} */ (await fixture(
|
||||
html`
|
||||
<lion-input-amount
|
||||
.formatOptions="${{ locale: 'en-GB' }}"
|
||||
.modelValue="${123}"
|
||||
></lion-input-amount>
|
||||
`,
|
||||
);
|
||||
));
|
||||
expect(el.formattedValue).to.equal('123.00');
|
||||
el = await fixture(
|
||||
html`
|
||||
|
|
@ -47,27 +53,33 @@ describe('<lion-input-amount>', () => {
|
|||
});
|
||||
|
||||
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>
|
||||
`);
|
||||
`));
|
||||
expect(el.formattedValue).to.equal('123,456.78'); // British
|
||||
localize.locale = 'nl-NL';
|
||||
await aTimeout();
|
||||
await aTimeout(0);
|
||||
expect(el.formattedValue).to.equal('123,456.78'); // should stay British
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
|
|
@ -77,53 +89,74 @@ describe('<lion-input-amount>', () => {
|
|||
});
|
||||
|
||||
it('displays currency if provided', async () => {
|
||||
const el = await fixture(`<lion-input-amount currency="EUR"></lion-input-amount>`);
|
||||
expect(Array.from(el.children).find(child => child.slot === 'after').innerText).to.equal('EUR');
|
||||
const el = /** @type {LionInputAmount} */ (await fixture(
|
||||
`<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 () => {
|
||||
localize.locale = 'tr-TR';
|
||||
const el = await fixture(`<lion-input-amount currency="TRY"></lion-input-amount>`);
|
||||
expect(Array.from(el.children).find(child => child.slot === 'after').innerText).to.equal('TL');
|
||||
const el = /** @type {LionInputAmount} */ (await fixture(
|
||||
`<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 () => {
|
||||
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';
|
||||
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 () => {
|
||||
const el = await fixture(
|
||||
const el = /** @type {LionInputAmount} */ (await fixture(
|
||||
`<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(
|
||||
'my-currency',
|
||||
);
|
||||
));
|
||||
expect(
|
||||
/** @type {HTMLElement[]} */ (Array.from(el.children)).find(child => child.slot === 'suffix')
|
||||
?.innerText,
|
||||
).to.equal('my-currency');
|
||||
el.currency = 'EUR';
|
||||
await el.updateComplete;
|
||||
expect(Array.from(el.children).find(child => child.slot === 'suffix').innerText).to.equal(
|
||||
'my-currency',
|
||||
);
|
||||
expect(
|
||||
/** @type {HTMLElement[]} */ (Array.from(el.children)).find(child => child.slot === 'suffix')
|
||||
?.innerText,
|
||||
).to.equal('my-currency');
|
||||
});
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('adds currency id to aria-labelledby of input', async () => {
|
||||
const el = await fixture(`<lion-input-amount currency="EUR"></lion-input-amount>`);
|
||||
expect(el._currencyDisplayNode.getAttribute('data-label')).to.be.not.null;
|
||||
expect(el._inputNode.getAttribute('aria-labelledby')).to.contain(el._currencyDisplayNode.id);
|
||||
const el = /** @type {LionInputAmount} */ (await fixture(
|
||||
`<lion-input-amount currency="EUR"></lion-input-amount>`,
|
||||
));
|
||||
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 () => {
|
||||
const el = await fixture(`<lion-input-amount currency="EUR"></lion-input-amount>`);
|
||||
expect(el._currencyDisplayNode.getAttribute('aria-label')).to.equal('euros');
|
||||
const el = /** @type {LionInputAmount} */ (await fixture(
|
||||
`<lion-input-amount currency="EUR"></lion-input-amount>`,
|
||||
));
|
||||
expect(el._currencyDisplayNode?.getAttribute('aria-label')).to.equal('euros');
|
||||
el.currency = 'USD';
|
||||
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';
|
||||
await el.updateComplete;
|
||||
expect(el._currencyDisplayNode.getAttribute('aria-label')).to.equal('Philippine pisos');
|
||||
expect(el._currencyDisplayNode?.getAttribute('aria-label')).to.equal('Philippine pisos');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,8 @@
|
|||
"packages/tooltip/**/*.js",
|
||||
"packages/button/src/**/*.js",
|
||||
"packages/listbox/src/*.js",
|
||||
"packages/input/**/*.js"
|
||||
"packages/input/**/*.js",
|
||||
"packages/input-amount/**/*.js"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
|
|
|
|||
Loading…
Reference in a new issue