import { css } from '@lion/core'; import { LionInput } from '@lion/input'; import { getCurrencyName, localize, LocalizeMixin } from '@lion/localize'; import { IsNumber } from '@lion/form-core'; import { formatAmount, formatCurrencyLabel } from './formatters.js'; import { parseAmount } from './parsers.js'; /** * `LionInputAmount` is a class for an amount custom form element (``). * * @customElement lion-input-amount */ export class LionInputAmount extends LocalizeMixin(LionInput) { /** @type {any} */ static get properties() { return { /** * @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 */ 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. */ modelValue: Number, }; } get slots() { return { ...super.slots, after: () => { if (this.currency) { const el = document.createElement('span'); // The data-label attribute will make sure that FormControl adds this to // input[aria-labelledby] el.setAttribute('data-label', ''); el.textContent = this.__currencyLabel; return el; } return undefined; }, }; } get _currencyDisplayNode() { return Array.from(this.children).find(child => child.slot === 'after'); } static get styles() { return [ ...super.styles, css` .input-group__container > .input-group__input ::slotted(.form-control) { text-align: right; } `, ]; } constructor() { super(); this.parser = parseAmount; this.formatter = formatAmount; /** @type {string | undefined} */ this.currency = undefined; this.__isPasting = false; this.addEventListener('paste', () => { this.__isPasting = true; this.__parserCallcountSincePaste = 0; }); this.defaultValidators.push(new IsNumber()); } connectedCallback() { // eslint-disable-next-line wc/guard-super-call super.connectedCallback(); this.type = 'text'; this._inputNode.setAttribute('inputmode', 'decimal'); if (this.currency) { this.__setCurrencyDisplayLabel(); } } /** @param {import('@lion/core').PropertyValues } changedProperties */ updated(changedProperties) { super.updated(changedProperties); if (changedProperties.has('currency') && this.currency) { this._onCurrencyChanged({ currency: this.currency }); } } /** * @override of FormatMixin */ __callParser(value = this.formattedValue) { // TODO: (@daKmor) input and change events both trigger parsing therefore we need to handle the second parse this.__parserCallcountSincePaste += 1; this.__isPasting = this.__parserCallcountSincePaste === 2; this.formatOptions.mode = this.__isPasting === true ? 'pasted' : 'auto'; return super.__callParser(value); } /** * @override of FormatMixin */ _reflectBackOn() { return super._reflectBackOn() || this.__isPasting; } /** * @param {Object} opts * @param {string} opts.currency */ _onCurrencyChanged({ currency }) { if (this._isPrivateSlot('after') && this._currencyDisplayNode) { this._currencyDisplayNode.textContent = this.__currencyLabel; } this.formatOptions.currency = currency; this._calculateValues({ source: null }); this.__setCurrencyDisplayLabel(); } __setCurrencyDisplayLabel() { // 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. if (this.currency && this._currencyDisplayNode) { this._currencyDisplayNode.setAttribute('aria-label', getCurrencyName(this.currency, {})); } } get __currencyLabel() { return this.currency ? formatCurrencyLabel(this.currency, localize.locale) : ''; } }