chore: adds lion-input-amount-dropdown (#2505)
Co-authored-by: gerjanvangeest <Gerjan.van.Geest@ing.com>
This commit is contained in:
parent
5f1d716627
commit
57800c4501
22 changed files with 1898 additions and 1 deletions
5
.changeset/funny-terms-grin.md
Normal file
5
.changeset/funny-terms-grin.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@lion/ui': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
adds the lion-input-amount-dropdown component
|
||||||
13
docs/components/input-amount-dropdown/index.md
Normal file
13
docs/components/input-amount-dropdown/index.md
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
---
|
||||||
|
parts:
|
||||||
|
- Input Amount Dropdown
|
||||||
|
title: Input Amount Dropdown
|
||||||
|
eleventyNavigation:
|
||||||
|
key: Input Amount Dropdown
|
||||||
|
order: 20
|
||||||
|
title: Input Amount Dropdown
|
||||||
|
---
|
||||||
|
|
||||||
|
# Input Amount Dropdown
|
||||||
|
|
||||||
|
-> go to Overview
|
||||||
103
docs/components/input-amount-dropdown/use-cases.md
Normal file
103
docs/components/input-amount-dropdown/use-cases.md
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
---
|
||||||
|
parts:
|
||||||
|
- Input Amount Dropdown
|
||||||
|
- Use Cases
|
||||||
|
title: 'Input Amount Dropdown: Use Cases'
|
||||||
|
eleventyNavigation:
|
||||||
|
key: 'Input Amount Dropdown: Use Cases'
|
||||||
|
order: 20
|
||||||
|
parent: Input Amount Dropdown
|
||||||
|
title: Use Cases
|
||||||
|
---
|
||||||
|
|
||||||
|
# Input Amount Dropdown: Use Cases
|
||||||
|
|
||||||
|
```js script
|
||||||
|
import { html } from '@mdjs/mdjs-preview';
|
||||||
|
import { loadDefaultFeedbackMessages } from '@lion/ui/validate-messages.js';
|
||||||
|
import { LionInputAmountDropdown } from '@lion/ui/input-amount-dropdown.js';
|
||||||
|
import '@lion/ui/define/lion-input-amount-dropdown.js';
|
||||||
|
```
|
||||||
|
|
||||||
|
## Input Amount Dropdown
|
||||||
|
|
||||||
|
When `.allowedCurrencies` is not configured, all currencies will be available in the dropdown
|
||||||
|
list.
|
||||||
|
|
||||||
|
```js preview-story
|
||||||
|
export const InputAmountDropdown = () => {
|
||||||
|
loadDefaultFeedbackMessages();
|
||||||
|
return html`
|
||||||
|
<lion-input-amount-dropdown
|
||||||
|
label="Select currency via dropdown"
|
||||||
|
help-text="Shows all currencies by default"
|
||||||
|
name="amount"
|
||||||
|
></lion-input-amount-dropdown>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Allowed currencies
|
||||||
|
|
||||||
|
When `.allowedCurrencies` is configured, only those currencies will be available in the dropdown
|
||||||
|
list.
|
||||||
|
|
||||||
|
```js preview-story
|
||||||
|
export const allowedCurrencies = () => {
|
||||||
|
loadDefaultFeedbackMessages();
|
||||||
|
return html`
|
||||||
|
<lion-input-amount-dropdown
|
||||||
|
label="Select currency via dropdown"
|
||||||
|
help-text="Shows only allowed currencies"
|
||||||
|
name="amount"
|
||||||
|
.allowedCurrencies=${['EUR', 'GBP']}
|
||||||
|
></lion-input-amount-dropdown>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Preferred currencies
|
||||||
|
|
||||||
|
When `.preferredCurrencies` is configured, they will show up on top of the dropdown list to enhance user experience.
|
||||||
|
|
||||||
|
```js preview-story
|
||||||
|
export const preferredCurrencies = () => {
|
||||||
|
loadDefaultFeedbackMessages();
|
||||||
|
return html`
|
||||||
|
<lion-input-amount-dropdown
|
||||||
|
label="Select currency via dropdown"
|
||||||
|
help-text="Preferred currencies show on top"
|
||||||
|
name="amount"
|
||||||
|
.allowedCurrencies=${['EUR', 'GBP', 'USD', 'JPY']}
|
||||||
|
.preferredCurrencies=${['USD', 'JPY']}
|
||||||
|
></lion-input-amount-dropdown>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Suffix or prefix
|
||||||
|
|
||||||
|
Subclassers can decide the dropdown location via `_dropdownSlot`, this can be set to either `suffix` or `prefix`.
|
||||||
|
|
||||||
|
```js preview-story
|
||||||
|
class DemoAmountDropdown extends LionInputAmountDropdown {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this._dropdownSlot = 'suffix';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('demo-amount-dropdown', DemoAmountDropdown);
|
||||||
|
|
||||||
|
export const suffixSlot = () => {
|
||||||
|
loadDefaultFeedbackMessages();
|
||||||
|
return html`
|
||||||
|
<demo-amount-dropdown
|
||||||
|
label="Select region via dropdown"
|
||||||
|
help-text="the dropdown shows in the suffix slot"
|
||||||
|
name="amount"
|
||||||
|
></demo-amount-dropdown>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,557 @@
|
||||||
|
import { html, css } from 'lit';
|
||||||
|
import { ref, createRef } from 'lit/directives/ref.js';
|
||||||
|
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||||
|
|
||||||
|
import { LionInputAmount } from '@lion/ui/input-amount.js';
|
||||||
|
import { currencyUtil } from './currencyUtil.js';
|
||||||
|
import { parseAmount } from './parsers.js';
|
||||||
|
import { formatAmount } from './formatters.js';
|
||||||
|
import { deserializer, serializer } from './serializers.js';
|
||||||
|
import { CurrencyAndAmount } from './validators.js';
|
||||||
|
import { localizeNamespaceLoader } from './localizeNamespaceLoader.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Note: one could consider to implement LionInputAmountDropdown as a
|
||||||
|
* [combobox](https://www.w3.org/TR/wai-aria-practices-1.2/#combobox).
|
||||||
|
* However, the currency dropdown does not directly set the textbox value, it only determines
|
||||||
|
* its region code. Therefore it does not comply to this criterium:
|
||||||
|
* "A combobox is an input widget with an associated popup that enables users to select a value for
|
||||||
|
* the combobox from a collection of possible values. In some implementations,
|
||||||
|
* the popup presents allowed values, while in other implementations, the popup presents suggested
|
||||||
|
* values, and users may either select one of the suggestions or type a value".
|
||||||
|
* We therefore decided to consider the dropdown a helper mechanism that does not set, but
|
||||||
|
* contributes to and helps format and validate the actual value.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('lit/directives/ref.js').Ref} Ref
|
||||||
|
* @typedef {import('lit').RenderOptions} RenderOptions
|
||||||
|
* @typedef {import('../../form-core/types/FormatMixinTypes.js').FormatHost} FormatHost
|
||||||
|
* @typedef {import('../../input-tel/types/index.js').RegionCode} RegionCode
|
||||||
|
* @typedef {import('../types/index.js').TemplateDataForDropdownInputAmount} TemplateDataForDropdownInputAmount
|
||||||
|
* @typedef {import('../types/index.js').OnDropdownChangeEvent} OnDropdownChangeEvent
|
||||||
|
* @typedef {import('../types/index.js').DropdownRef} DropdownRef
|
||||||
|
* @typedef {import('../types/index.js').RegionMeta} RegionMeta
|
||||||
|
* @typedef {import('../types/index.js').CurrencyCode} CurrencyCode
|
||||||
|
* @typedef {import('../../select-rich/src/LionSelectRich.js').LionSelectRich} LionSelectRich
|
||||||
|
* @typedef {import('../../overlays/src/OverlayController.js').OverlayController} OverlayController
|
||||||
|
* @typedef {import('../../form-core/types/FormatMixinTypes.js').FormatOptions} FormatOptions
|
||||||
|
* @typedef {FormatOptions & {locale?:string;currency:string|undefined}} AmountFormatOptions
|
||||||
|
* @typedef {TemplateDataForDropdownInputAmount & {data: {regionMetaList:RegionMeta[]}}} TemplateDataForIntlInputAmount
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LionInputAmountDropdown renders a dropdown like element next to the text field, inside the
|
||||||
|
* prefix, or suffix, slot. This could be a LionSelect, a LionSelectRich or a native select.
|
||||||
|
* By default, the native `<select>` element is used for this, so that it's as lightweight as
|
||||||
|
* possible. Also, it doesn't need to be a `FormControl`, because it's purely a helper element
|
||||||
|
* to provide better UX: the modelValue (the text field) contains all needed info:
|
||||||
|
* the currency code following ISO 4217 and its corresponding currency symbol using Intl.
|
||||||
|
*
|
||||||
|
* @customElement lion-input-amount-dropdown
|
||||||
|
*/
|
||||||
|
// @ts-expect-error - The types returned by 'parser(...)' are incompatible between these types. AmountDropdownModelValue' is not assignable to type 'number' */
|
||||||
|
export class LionInputAmountDropdown extends LionInputAmount {
|
||||||
|
/**
|
||||||
|
* @configure LitElement
|
||||||
|
* @type {any}
|
||||||
|
*/
|
||||||
|
static properties = {
|
||||||
|
preferredCurrencies: { type: Array },
|
||||||
|
allowedCurrencies: { type: Array },
|
||||||
|
__dropdownSlot: { type: String },
|
||||||
|
};
|
||||||
|
|
||||||
|
static localizeNamespaces = [
|
||||||
|
{ 'lion-input-amount-dropdown': localizeNamespaceLoader },
|
||||||
|
...super.localizeNamespaces,
|
||||||
|
];
|
||||||
|
|
||||||
|
refs = {
|
||||||
|
/** @type {DropdownRef} */
|
||||||
|
dropdown: /** @type {DropdownRef} */ (createRef()),
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method provides a TemplateData object to be fed to pure template functions, a.k.a.
|
||||||
|
* Pure Templates™. The goal is to totally decouple presentation from logic here, so that
|
||||||
|
* Subclassers can override all content without having to loose private info contained
|
||||||
|
* within the template function that was overridden.
|
||||||
|
*
|
||||||
|
* Subclassers would need to make sure all the contents of the TemplateData object are implemented
|
||||||
|
* by making sure they are coupled to the right 'ref' ([data-ref=dropdown] in this example),
|
||||||
|
* with the help of lit's spread operator directive.
|
||||||
|
* To enhance this process, the TemplateData object is completely typed. Ideally, this would be
|
||||||
|
* enhanced by providing linters that make sure all of their required members are implemented by
|
||||||
|
* a Subclasser.
|
||||||
|
* When a Subclasser wants to add more data, this can be done via:
|
||||||
|
* @example
|
||||||
|
* ```js
|
||||||
|
* get _templateDataDropdown() {
|
||||||
|
* return {
|
||||||
|
* ...super._templateDataDropdown,
|
||||||
|
* myExtraData: { x: 1, y: 2 },
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
* @overridable
|
||||||
|
* @type {TemplateDataForDropdownInputAmount}
|
||||||
|
*/
|
||||||
|
get _templateDataDropdown() {
|
||||||
|
const refs = {
|
||||||
|
dropdown: {
|
||||||
|
ref: this.refs.dropdown,
|
||||||
|
props: {
|
||||||
|
style: `height: 100%;`,
|
||||||
|
},
|
||||||
|
listeners: {
|
||||||
|
change: this._onDropdownValueChange,
|
||||||
|
'model-value-changed': this._onDropdownValueChange,
|
||||||
|
},
|
||||||
|
labels: {
|
||||||
|
selectCurrency: this._localizeManager.msg('lion-input-amount-dropdown:selectCurrency'),
|
||||||
|
allCurrencies:
|
||||||
|
this._allCurrenciesLabel ||
|
||||||
|
this._localizeManager.msg('lion-input-amount-dropdown:allCurrencies'),
|
||||||
|
preferredCurrencies:
|
||||||
|
this._preferredCurrenciesLabel ||
|
||||||
|
this._localizeManager.msg('lion-input-amount-dropdown:suggestedCurrencies'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
input: this._inputNode,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
refs,
|
||||||
|
data: {
|
||||||
|
// @ts-expect-error - cannot cast string to CurrencyCode outside a TS file
|
||||||
|
currency: this.currency,
|
||||||
|
regionMetaList: this.__regionMetaList,
|
||||||
|
regionMetaListPreferred: this.__regionMetaListPreferred,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
get _dropdownSlot() {
|
||||||
|
return /** @type {string} */ this.__dropdownSlot;
|
||||||
|
}
|
||||||
|
|
||||||
|
set _dropdownSlot(position) {
|
||||||
|
if (position !== 'suffix' && position !== 'prefix') {
|
||||||
|
throw new Error('Only the suffix and prefix slots are valid positions for the dropdown.');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.__dropdownSlot = position;
|
||||||
|
}
|
||||||
|
|
||||||
|
static templates = {
|
||||||
|
dropdown: (/** @type {TemplateDataForDropdownInputAmount} */ templateDataForDropdown) => {
|
||||||
|
const { refs, data } = templateDataForDropdown;
|
||||||
|
const renderOption = (/** @type {RegionMeta} */ regionMeta) =>
|
||||||
|
html`${this.templates.dropdownOption(templateDataForDropdown, regionMeta)} `;
|
||||||
|
|
||||||
|
// TODO: once spread directive available, use it per ref
|
||||||
|
return html`
|
||||||
|
<select
|
||||||
|
${ref(refs?.dropdown?.ref)}
|
||||||
|
aria-label="${refs?.dropdown?.labels?.selectCurrency}"
|
||||||
|
@change="${refs?.dropdown?.listeners?.change}"
|
||||||
|
style="${refs?.dropdown?.props?.style}"
|
||||||
|
>
|
||||||
|
${data?.regionMetaListPreferred?.length
|
||||||
|
? html`
|
||||||
|
<optgroup label="${refs?.dropdown?.labels?.preferredCurrencies}">
|
||||||
|
${data.regionMetaListPreferred.map(renderOption)}
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="${refs?.dropdown?.labels?.allCurrencies}">
|
||||||
|
${data?.regionMetaList?.map(renderOption)}
|
||||||
|
</optgroup>
|
||||||
|
`
|
||||||
|
: html` ${data?.regionMetaList?.map(renderOption)}`}
|
||||||
|
</select>
|
||||||
|
`;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* @param {TemplateDataForDropdownInputAmount} templateDataForDropdown
|
||||||
|
* @param {RegionMeta} contextData
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
dropdownOption: (
|
||||||
|
templateDataForDropdown,
|
||||||
|
{ currencyCode, nameForLocale, currencySymbol },
|
||||||
|
) => html`
|
||||||
|
<option
|
||||||
|
value="${currencyCode}"
|
||||||
|
aria-label="${ifDefined(
|
||||||
|
nameForLocale && currencySymbol ? `${nameForLocale}, ${currencySymbol}` : '',
|
||||||
|
)}"
|
||||||
|
>
|
||||||
|
${currencyCode} (${currencySymbol})
|
||||||
|
</option>
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @configure LitElement
|
||||||
|
* @enhance LionInputAmountDropdown
|
||||||
|
*/
|
||||||
|
static styles = [
|
||||||
|
super.styles,
|
||||||
|
css`
|
||||||
|
/**
|
||||||
|
* We need to align the height of the dropdown with the height of the text field.
|
||||||
|
* We target the HTMLDivElement (render wrapper from SlotMixin) here. Its child,
|
||||||
|
* [data-ref=dropdown], receives a 100% height as well via inline styles (since we
|
||||||
|
* can't target from shadow styles).
|
||||||
|
*/
|
||||||
|
::slotted([slot='prefix']),
|
||||||
|
::slotted([slot='suffix']) {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* visually hiding the 'after' slot, leaving it as sr-only (screen-reader only)
|
||||||
|
* source: https://www.scottohara.me/blog/2017/04/14/inclusively-hidden.html
|
||||||
|
*/
|
||||||
|
::slotted([slot='after']:not(:focus):not(:active)) {
|
||||||
|
clip: rect(0 0 0 0);
|
||||||
|
clip-path: inset(50%);
|
||||||
|
height: 1px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: absolute;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 1px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @configure SlotMixin
|
||||||
|
*/
|
||||||
|
get slots() {
|
||||||
|
return {
|
||||||
|
...super.slots,
|
||||||
|
[this._dropdownSlot]: () => {
|
||||||
|
const ctor = /** @type {typeof LionInputAmountDropdown} */ (this.constructor);
|
||||||
|
const { templates } = ctor;
|
||||||
|
|
||||||
|
return {
|
||||||
|
template: templates.dropdown(this._templateDataDropdown),
|
||||||
|
renderAsDirectHostChild: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @configure LocalizeMixin
|
||||||
|
*/
|
||||||
|
onLocaleUpdated() {
|
||||||
|
super.onLocaleUpdated();
|
||||||
|
|
||||||
|
const localeSplitted = this._localizeManager.locale.split('-');
|
||||||
|
/**
|
||||||
|
* @protected
|
||||||
|
* @type {RegionCode}
|
||||||
|
*/
|
||||||
|
this._langIso = /** @type {RegionCode} */ (
|
||||||
|
localeSplitted[localeSplitted.length - 1].toUpperCase()
|
||||||
|
);
|
||||||
|
|
||||||
|
this.__namesForLocale = new Intl.DisplayNames([this._langIso], {
|
||||||
|
type: 'currency',
|
||||||
|
});
|
||||||
|
|
||||||
|
this.__calculateActiveCurrency();
|
||||||
|
this.__createCurrencyMeta();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @lifecycle platform
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.parser = parseAmount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import("../types/index.js").AmountDropdownModelValue} modelValue
|
||||||
|
* @param {import('../../localize/types/LocalizeMixinTypes.js').FormatNumberOptions} [givenOptions] Locale Options
|
||||||
|
*/
|
||||||
|
this.formatter = (modelValue, givenOptions) => formatAmount(modelValue, givenOptions, this);
|
||||||
|
this.serializer = serializer;
|
||||||
|
this.deserializer = deserializer;
|
||||||
|
|
||||||
|
this.defaultValidators = [new CurrencyAndAmount()];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Slot position to render the dropdown in
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
this.__dropdownSlot = 'prefix';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regions that will be shown on top of the dropdown
|
||||||
|
* @type {CurrencyCode[]}
|
||||||
|
*/
|
||||||
|
this.preferredCurrencies = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regions that are allowed to be selected in the dropdown.
|
||||||
|
* @type {CurrencyCode[]}
|
||||||
|
*/
|
||||||
|
this.allowedCurrencies = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Group label for all countries, when preferredCountries are shown
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
this._allCurrenciesLabel = '';
|
||||||
|
/**
|
||||||
|
* Group label for preferred countries, when preferredCountries are shown
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
this._preferredCurrenciesLabel = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains everything needed for rendering region options:
|
||||||
|
* region code, currency code, display name according to locale, display name
|
||||||
|
* @private
|
||||||
|
* @type {RegionMeta[]}
|
||||||
|
*/
|
||||||
|
this.__regionMetaList = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A filtered `this.__regionMetaList`, containing all regions provided in `preferredCurrencies`
|
||||||
|
* @private
|
||||||
|
* @type {RegionMeta[]}
|
||||||
|
*/
|
||||||
|
this.__regionMetaListPreferred = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @protected
|
||||||
|
* @type {EventListener}
|
||||||
|
*/
|
||||||
|
this._onDropdownValueChange = this._onDropdownValueChange.bind(this);
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* @type {EventListener}
|
||||||
|
*/
|
||||||
|
this.__syncCurrencyWithDropdown = this.__syncCurrencyWithDropdown.bind(this);
|
||||||
|
|
||||||
|
this._currencyUtil = currencyUtil;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @lifecycle LitElement
|
||||||
|
* @param {import('lit-element').PropertyValues } changedProperties
|
||||||
|
*/
|
||||||
|
willUpdate(changedProperties) {
|
||||||
|
super.willUpdate(changedProperties);
|
||||||
|
|
||||||
|
if (changedProperties.has('allowedCurrencies')) {
|
||||||
|
this.__createCurrencyMeta();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('lit-element').PropertyValues } changedProperties
|
||||||
|
*/
|
||||||
|
updated(changedProperties) {
|
||||||
|
super.updated(changedProperties);
|
||||||
|
|
||||||
|
this.__syncCurrencyWithDropdown();
|
||||||
|
|
||||||
|
if (changedProperties.has('disabled') || changedProperties.has('readOnly')) {
|
||||||
|
if (this.disabled || this.readOnly) {
|
||||||
|
this.refs.dropdown?.value?.setAttribute('disabled', '');
|
||||||
|
} else {
|
||||||
|
this.refs.dropdown?.value?.removeAttribute('disabled');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changedProperties.has('allowedCurrencies') && this.allowedCurrencies.length > 0) {
|
||||||
|
this.__calculateActiveCurrency();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @lifecycle LitElement
|
||||||
|
* @param {import('lit-element').PropertyValues } changedProperties
|
||||||
|
*/
|
||||||
|
firstUpdated(changedProperties) {
|
||||||
|
super.firstUpdated?.(changedProperties);
|
||||||
|
this._initModelValueBasedOnDropdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
_initModelValueBasedOnDropdown() {
|
||||||
|
if (!this._initialModelValue && !this.dirty) {
|
||||||
|
this.__initializedCurrencyCode = this.currency;
|
||||||
|
this._initialModelValue = { currency: this.currency };
|
||||||
|
this.modelValue = this._initialModelValue;
|
||||||
|
this.initInteractionState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used for Required validation and computation of interaction states.
|
||||||
|
* We need to override this, because we prefill the input with the currency code, but for proper UX,
|
||||||
|
* we don't consider this as having interaction state `prefilled`
|
||||||
|
* @param {string} modelValue
|
||||||
|
* @return {boolean}
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
_isEmpty(modelValue = this.modelValue) {
|
||||||
|
return super._isEmpty(modelValue) || this.currency === this.__initializedCurrencyCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @protected
|
||||||
|
* @param {OnDropdownChangeEvent} event
|
||||||
|
*/
|
||||||
|
_onDropdownValueChange(event) {
|
||||||
|
const isInitializing = event.detail?.initialize;
|
||||||
|
const dropdownElement = event.target;
|
||||||
|
const dropdownValue = /** @type {RegionCode} */ (
|
||||||
|
dropdownElement.modelValue || dropdownElement.value
|
||||||
|
);
|
||||||
|
if (isInitializing || this.currency === dropdownValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const prevCurrency = this.currency;
|
||||||
|
|
||||||
|
/** @type {RegionCode | string} */
|
||||||
|
this.currency = dropdownValue;
|
||||||
|
|
||||||
|
if (prevCurrency !== this.currency && !this.focused) {
|
||||||
|
if (!this.value) {
|
||||||
|
this.modelValue = { currency: this.currency, amount: this.value };
|
||||||
|
} else {
|
||||||
|
/** @type {AmountFormatOptions} */
|
||||||
|
(this.formatOptions).currency = this.currency;
|
||||||
|
this.modelValue = this._callParser(this.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
__syncCurrencyWithDropdown(currencyCode = this.currency) {
|
||||||
|
const dropdownElement = this.refs.dropdown?.value;
|
||||||
|
if (!dropdownElement || !currencyCode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('modelValue' in dropdownElement) {
|
||||||
|
const dropdownCurrencyCode = dropdownElement.modelValue;
|
||||||
|
if (dropdownCurrencyCode === currencyCode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
/** @type {* & FormatHost} */ (dropdownElement).modelValue = currencyCode;
|
||||||
|
} else {
|
||||||
|
const dropdownCurrencyCode = dropdownElement.value;
|
||||||
|
if (dropdownCurrencyCode === currencyCode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
/** @type {HTMLSelectElement} */ (dropdownElement).value = currencyCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepares data for options, like "Greece (Ελλάδα)", where "Greece" is `nameForLocale` and
|
||||||
|
* "Ελλάδα" `nameForRegion`.
|
||||||
|
* This should be run on change of:
|
||||||
|
* - allowedCurrencies
|
||||||
|
* - locale
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
__createCurrencyMeta() {
|
||||||
|
if (!this._allowedOrAllCurrencies?.length || !this.__namesForLocale) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.__regionMetaList = [];
|
||||||
|
this.__regionMetaListPreferred = [];
|
||||||
|
|
||||||
|
this._allowedOrAllCurrencies.forEach(currencyCode => {
|
||||||
|
const destinationList = this.preferredCurrencies.includes(currencyCode)
|
||||||
|
? this.__regionMetaListPreferred
|
||||||
|
: this.__regionMetaList;
|
||||||
|
|
||||||
|
destinationList.push({
|
||||||
|
currencyCode,
|
||||||
|
nameForLocale: this.__namesForLocale?.of(currencyCode),
|
||||||
|
currencySymbol: this._currencyUtil.getCurrencySymbol(currencyCode, this._langIso ?? ''),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Usually, we don't use composition in regular LionFields (non choice-groups). Here we use a LionSelect(Rich) inside.
|
||||||
|
* We don't want to repropagate any children, since an Application Developer is not concerned with these internals (see repropate logic in FormControlMixin)
|
||||||
|
* Also, we don't want to give (wrong) info to InteractionStateMixin, that will set the wrong interaction states based on child info.
|
||||||
|
* TODO: Make "this._repropagationRole !== 'child'" the default for FormControlMixin
|
||||||
|
* (so that FormControls used within are never repropagated for LionFields)
|
||||||
|
* @protected
|
||||||
|
* @configure FormControlMixin: don't repropagate any children
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
_repropagationCondition() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
__calculateActiveCurrency() {
|
||||||
|
// 1. Get the currency from pre-configured allowed currencies (if one entry)
|
||||||
|
if (this.allowedCurrencies?.length === 1) {
|
||||||
|
[this.currency] = this.allowedCurrencies;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Try to get the currency from user input
|
||||||
|
if (this.modelValue?.currency && this.allowedCurrencies?.includes(this.modelValue?.currency)) {
|
||||||
|
this.currency = this.modelValue.currency;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Try to get the currency from the preferred currencies
|
||||||
|
if (this.preferredCurrencies?.length > 0) {
|
||||||
|
[this.currency] = this.preferredCurrencies;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Try to get the currency from locale
|
||||||
|
if (
|
||||||
|
this._langIso &&
|
||||||
|
this._currencyUtil?.countryToCurrencyMap.has(this._langIso) &&
|
||||||
|
this._allowedOrAllCurrencies.includes(
|
||||||
|
// @ts-expect-error - Set.get always returns a CurrencyCode.
|
||||||
|
this._currencyUtil?.countryToCurrencyMap.get(this._langIso),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
this.currency = this._currencyUtil?.countryToCurrencyMap.get(this._langIso);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Not derivable
|
||||||
|
this.currency = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used for rendering the region/currency list
|
||||||
|
* @property _allowedOrAllRegions
|
||||||
|
* @type {CurrencyCode[]}
|
||||||
|
*/
|
||||||
|
get _allowedOrAllCurrencies() {
|
||||||
|
return this.allowedCurrencies?.length
|
||||||
|
? this.allowedCurrencies
|
||||||
|
: Array.from(this._currencyUtil?.allCurrencies) || [];
|
||||||
|
}
|
||||||
|
}
|
||||||
324
packages/ui/components/input-amount-dropdown/src/currencyUtil.js
Normal file
324
packages/ui/components/input-amount-dropdown/src/currencyUtil.js
Normal file
|
|
@ -0,0 +1,324 @@
|
||||||
|
/**
|
||||||
|
* This country to currency list was exported from Java.
|
||||||
|
* Java contains a country:currency map according to the i18n spec, but JS does not.
|
||||||
|
*
|
||||||
|
* @type {import("../types/index.js").countryToCurrencyList}
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // The following Java code can be used to export the countryToCurrencyList
|
||||||
|
*
|
||||||
|
* import java.util.Currency;
|
||||||
|
* import java.util.Locale;
|
||||||
|
*
|
||||||
|
* public class Main {
|
||||||
|
* public static void main(String[] args) {
|
||||||
|
* // get all ISO countries
|
||||||
|
* String[] ISOCountryCodes = Locale.getISOCountries();
|
||||||
|
*
|
||||||
|
* // loop over all the countries.
|
||||||
|
* for (String country : ISOCountryCodes) {
|
||||||
|
* try {
|
||||||
|
* // get the locale for said country
|
||||||
|
* Locale locale = new Locale("", country);
|
||||||
|
*
|
||||||
|
* // creates a Currency instance which has a bunch of standardized information.
|
||||||
|
* // from that class we can get the currency linked to the country.
|
||||||
|
* String currencyCode = Currency.getInstance(locale).getCurrencyCode();
|
||||||
|
*
|
||||||
|
* // prints to the console, which can be copied to update the map if changes occurred
|
||||||
|
* String output = country + ": '" + currencyCode + "',\n";
|
||||||
|
* System.out.print(output);
|
||||||
|
* } catch (Exception e) {
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
const countryToCurrencyList = {
|
||||||
|
AD: 'EUR',
|
||||||
|
AE: 'AED',
|
||||||
|
AF: 'AFN',
|
||||||
|
AG: 'XCD',
|
||||||
|
AI: 'XCD',
|
||||||
|
AL: 'ALL',
|
||||||
|
AM: 'AMD',
|
||||||
|
AO: 'AOA',
|
||||||
|
AR: 'ARS',
|
||||||
|
AS: 'USD',
|
||||||
|
AT: 'EUR',
|
||||||
|
AU: 'AUD',
|
||||||
|
AW: 'AWG',
|
||||||
|
AX: 'EUR',
|
||||||
|
AZ: 'AZN',
|
||||||
|
BA: 'BAM',
|
||||||
|
BB: 'BBD',
|
||||||
|
BD: 'BDT',
|
||||||
|
BE: 'EUR',
|
||||||
|
BF: 'XOF',
|
||||||
|
BG: 'BGN',
|
||||||
|
BH: 'BHD',
|
||||||
|
BI: 'BIF',
|
||||||
|
BJ: 'XOF',
|
||||||
|
BL: 'EUR',
|
||||||
|
BM: 'BMD',
|
||||||
|
BN: 'BND',
|
||||||
|
BO: 'BOB',
|
||||||
|
BQ: 'USD',
|
||||||
|
BR: 'BRL',
|
||||||
|
BS: 'BSD',
|
||||||
|
BT: 'BTN',
|
||||||
|
BV: 'NOK',
|
||||||
|
BW: 'BWP',
|
||||||
|
BY: 'BYN',
|
||||||
|
BZ: 'BZD',
|
||||||
|
CA: 'CAD',
|
||||||
|
CC: 'AUD',
|
||||||
|
CD: 'CDF',
|
||||||
|
CF: 'XAF',
|
||||||
|
CG: 'XAF',
|
||||||
|
CH: 'CHF',
|
||||||
|
CI: 'XOF',
|
||||||
|
CK: 'NZD',
|
||||||
|
CL: 'CLP',
|
||||||
|
CM: 'XAF',
|
||||||
|
CN: 'CNY',
|
||||||
|
CO: 'COP',
|
||||||
|
CR: 'CRC',
|
||||||
|
CU: 'CUP',
|
||||||
|
CV: 'CVE',
|
||||||
|
CW: 'XCG',
|
||||||
|
CX: 'AUD',
|
||||||
|
CY: 'EUR',
|
||||||
|
CZ: 'CZK',
|
||||||
|
DE: 'EUR',
|
||||||
|
DJ: 'DJF',
|
||||||
|
DK: 'DKK',
|
||||||
|
DM: 'XCD',
|
||||||
|
DO: 'DOP',
|
||||||
|
DZ: 'DZD',
|
||||||
|
EC: 'USD',
|
||||||
|
EE: 'EUR',
|
||||||
|
EG: 'EGP',
|
||||||
|
EH: 'MAD',
|
||||||
|
ER: 'ERN',
|
||||||
|
ES: 'EUR',
|
||||||
|
ET: 'ETB',
|
||||||
|
FI: 'EUR',
|
||||||
|
FJ: 'FJD',
|
||||||
|
FK: 'FKP',
|
||||||
|
FM: 'USD',
|
||||||
|
FO: 'DKK',
|
||||||
|
FR: 'EUR',
|
||||||
|
GA: 'XAF',
|
||||||
|
GB: 'GBP',
|
||||||
|
GD: 'XCD',
|
||||||
|
GE: 'GEL',
|
||||||
|
GF: 'EUR',
|
||||||
|
GG: 'GBP',
|
||||||
|
GH: 'GHS',
|
||||||
|
GI: 'GIP',
|
||||||
|
GL: 'DKK',
|
||||||
|
GM: 'GMD',
|
||||||
|
GN: 'GNF',
|
||||||
|
GP: 'EUR',
|
||||||
|
GQ: 'XAF',
|
||||||
|
GR: 'EUR',
|
||||||
|
GS: 'GBP',
|
||||||
|
GT: 'GTQ',
|
||||||
|
GU: 'USD',
|
||||||
|
GW: 'XOF',
|
||||||
|
GY: 'GYD',
|
||||||
|
HK: 'HKD',
|
||||||
|
HM: 'AUD',
|
||||||
|
HN: 'HNL',
|
||||||
|
HR: 'EUR',
|
||||||
|
HT: 'HTG',
|
||||||
|
HU: 'HUF',
|
||||||
|
ID: 'IDR',
|
||||||
|
IE: 'EUR',
|
||||||
|
IL: 'ILS',
|
||||||
|
IM: 'GBP',
|
||||||
|
IN: 'INR',
|
||||||
|
IO: 'USD',
|
||||||
|
IQ: 'IQD',
|
||||||
|
IR: 'IRR',
|
||||||
|
IS: 'ISK',
|
||||||
|
IT: 'EUR',
|
||||||
|
JE: 'GBP',
|
||||||
|
JM: 'JMD',
|
||||||
|
JO: 'JOD',
|
||||||
|
JP: 'JPY',
|
||||||
|
KE: 'KES',
|
||||||
|
KG: 'KGS',
|
||||||
|
KH: 'KHR',
|
||||||
|
KI: 'AUD',
|
||||||
|
KM: 'KMF',
|
||||||
|
KN: 'XCD',
|
||||||
|
KP: 'KPW',
|
||||||
|
KR: 'KRW',
|
||||||
|
KW: 'KWD',
|
||||||
|
KY: 'KYD',
|
||||||
|
KZ: 'KZT',
|
||||||
|
LA: 'LAK',
|
||||||
|
LB: 'LBP',
|
||||||
|
LC: 'XCD',
|
||||||
|
LI: 'CHF',
|
||||||
|
LK: 'LKR',
|
||||||
|
LR: 'LRD',
|
||||||
|
LS: 'LSL',
|
||||||
|
LT: 'EUR',
|
||||||
|
LU: 'EUR',
|
||||||
|
LV: 'EUR',
|
||||||
|
LY: 'LYD',
|
||||||
|
MA: 'MAD',
|
||||||
|
MC: 'EUR',
|
||||||
|
MD: 'MDL',
|
||||||
|
ME: 'EUR',
|
||||||
|
MF: 'EUR',
|
||||||
|
MG: 'MGA',
|
||||||
|
MH: 'USD',
|
||||||
|
MK: 'MKD',
|
||||||
|
ML: 'XOF',
|
||||||
|
MM: 'MMK',
|
||||||
|
MN: 'MNT',
|
||||||
|
MO: 'MOP',
|
||||||
|
MP: 'USD',
|
||||||
|
MQ: 'EUR',
|
||||||
|
MR: 'MRU',
|
||||||
|
MS: 'XCD',
|
||||||
|
MT: 'EUR',
|
||||||
|
MU: 'MUR',
|
||||||
|
MV: 'MVR',
|
||||||
|
MW: 'MWK',
|
||||||
|
MX: 'MXN',
|
||||||
|
MY: 'MYR',
|
||||||
|
MZ: 'MZN',
|
||||||
|
NA: 'NAD',
|
||||||
|
NC: 'XPF',
|
||||||
|
NE: 'XOF',
|
||||||
|
NF: 'AUD',
|
||||||
|
NG: 'NGN',
|
||||||
|
NI: 'NIO',
|
||||||
|
NL: 'EUR',
|
||||||
|
NO: 'NOK',
|
||||||
|
NP: 'NPR',
|
||||||
|
NR: 'AUD',
|
||||||
|
NU: 'NZD',
|
||||||
|
NZ: 'NZD',
|
||||||
|
OM: 'OMR',
|
||||||
|
PA: 'PAB',
|
||||||
|
PE: 'PEN',
|
||||||
|
PF: 'XPF',
|
||||||
|
PG: 'PGK',
|
||||||
|
PH: 'PHP',
|
||||||
|
PK: 'PKR',
|
||||||
|
PL: 'PLN',
|
||||||
|
PM: 'EUR',
|
||||||
|
PN: 'NZD',
|
||||||
|
PR: 'USD',
|
||||||
|
PS: 'ILS',
|
||||||
|
PT: 'EUR',
|
||||||
|
PW: 'USD',
|
||||||
|
PY: 'PYG',
|
||||||
|
QA: 'QAR',
|
||||||
|
RE: 'EUR',
|
||||||
|
RO: 'RON',
|
||||||
|
RS: 'RSD',
|
||||||
|
RU: 'RUB',
|
||||||
|
RW: 'RWF',
|
||||||
|
SA: 'SAR',
|
||||||
|
SB: 'SBD',
|
||||||
|
SC: 'SCR',
|
||||||
|
SD: 'SDG',
|
||||||
|
SE: 'SEK',
|
||||||
|
SG: 'SGD',
|
||||||
|
SH: 'SHP',
|
||||||
|
SI: 'EUR',
|
||||||
|
SJ: 'NOK',
|
||||||
|
SK: 'EUR',
|
||||||
|
SL: 'SLE',
|
||||||
|
SM: 'EUR',
|
||||||
|
SN: 'XOF',
|
||||||
|
SO: 'SOS',
|
||||||
|
SR: 'SRD',
|
||||||
|
SS: 'SSP',
|
||||||
|
ST: 'STN',
|
||||||
|
SV: 'SVC',
|
||||||
|
SX: 'XCG',
|
||||||
|
SY: 'SYP',
|
||||||
|
SZ: 'SZL',
|
||||||
|
TC: 'USD',
|
||||||
|
TD: 'XAF',
|
||||||
|
TF: 'EUR',
|
||||||
|
TG: 'XOF',
|
||||||
|
TH: 'THB',
|
||||||
|
TJ: 'TJS',
|
||||||
|
TK: 'NZD',
|
||||||
|
TL: 'USD',
|
||||||
|
TM: 'TMT',
|
||||||
|
TN: 'TND',
|
||||||
|
TO: 'TOP',
|
||||||
|
TR: 'TRY',
|
||||||
|
TT: 'TTD',
|
||||||
|
TV: 'AUD',
|
||||||
|
TW: 'TWD',
|
||||||
|
TZ: 'TZS',
|
||||||
|
UA: 'UAH',
|
||||||
|
UG: 'UGX',
|
||||||
|
UM: 'USD',
|
||||||
|
US: 'USD',
|
||||||
|
UY: 'UYU',
|
||||||
|
UZ: 'UZS',
|
||||||
|
VA: 'EUR',
|
||||||
|
VC: 'XCD',
|
||||||
|
VE: 'VES',
|
||||||
|
VG: 'USD',
|
||||||
|
VI: 'USD',
|
||||||
|
VN: 'VND',
|
||||||
|
VU: 'VUV',
|
||||||
|
WF: 'XPF',
|
||||||
|
WS: 'WST',
|
||||||
|
YE: 'YER',
|
||||||
|
YT: 'EUR',
|
||||||
|
ZA: 'ZAR',
|
||||||
|
ZM: 'ZMW',
|
||||||
|
ZW: 'ZWG',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map containing all countries and its corresponding currency, following the i18n standard.
|
||||||
|
* @type {import("../types/index.js").RegionToCurrencyMap}
|
||||||
|
*/
|
||||||
|
const countryToCurrencyMap = /** @type {import("../types/index.js").RegionToCurrencyMap} */ (
|
||||||
|
new Map(Object.entries(countryToCurrencyList))
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set with all possible currencies derived from the i18n standard.
|
||||||
|
* @type {import("../types/index.js").AllCurrenciesSet}
|
||||||
|
*/
|
||||||
|
const allCurrencies = new Set(Object.values(countryToCurrencyList).sort());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches a currency symbol to a currency code, with a given locale based on Intl.numberFormat.
|
||||||
|
* @param {import("./LionInputAmountDropdown.js").CurrencyCode} currency
|
||||||
|
* @param {string} locale
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
const getCurrencySymbol = (currency, locale) => {
|
||||||
|
if (!locale || !currency) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
new Intl.NumberFormat(locale, { style: 'currency', currency })
|
||||||
|
.formatToParts(1)
|
||||||
|
.find(x => x.type === 'currency')?.value || ''
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const currencyUtil = {
|
||||||
|
countryToCurrencyMap,
|
||||||
|
allCurrencies,
|
||||||
|
getCurrencySymbol,
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { formatAmount as _formatAmount } from '@lion/ui/input-amount.js';
|
||||||
|
import { currencyUtil } from './currencyUtil.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('../../localize/types/LocalizeMixinTypes.js').FormatNumberOptions} FormatOptions
|
||||||
|
* @typedef {import('../types/index.js').AmountDropdownModelValue} AmountDropdownModelValue
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a number considering the default fraction digits provided by Intl.
|
||||||
|
*
|
||||||
|
* @param {import('../types/index.js').AmountDropdownModelValue} modelValue to format
|
||||||
|
* @param {FormatOptions} [givenOptions]
|
||||||
|
* @param {AmountDropdownModelValue|undefined} [context]
|
||||||
|
*/
|
||||||
|
export const formatAmount = (modelValue, givenOptions, context) => {
|
||||||
|
// @ts-expect-error - cannot cast string to CurrencyCode outside a TS file
|
||||||
|
if (currencyUtil.allCurrencies.has(modelValue?.currency) && context) {
|
||||||
|
// TODO: better way of setting parent currency
|
||||||
|
context.currency = modelValue?.currency;
|
||||||
|
}
|
||||||
|
// @ts-expect-error - cannot cast string to CurrencyCode outside a TS file
|
||||||
|
return _formatAmount(modelValue?.amount, givenOptions);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
/* eslint-disable import/no-extraneous-dependencies */
|
||||||
|
export const localizeNamespaceLoader = /** @param {string} locale */ locale => {
|
||||||
|
switch (locale) {
|
||||||
|
default:
|
||||||
|
return import('@lion/ui/input-amount-dropdown-translations/en.js');
|
||||||
|
}
|
||||||
|
};
|
||||||
23
packages/ui/components/input-amount-dropdown/src/parsers.js
Normal file
23
packages/ui/components/input-amount-dropdown/src/parsers.js
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { parseAmount as _parseAmount } from '@lion/ui/input-amount.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uses `parseAmount()` to parse a number string and return the best possible javascript number.
|
||||||
|
* Rounds up the number with the correct amount of decimals according to the currency.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* parseAmount('1,234.56', {currency: 'EUR'}); => { amount: 1234.56, currency: 'EUR' }
|
||||||
|
* parseAmount('1,234.56', {currency: 'JPY'}); => { amount: 1235, currency: 'JPY' }
|
||||||
|
* parseAmount('1,234.56', {currency: 'JOD'}); => { amount: 1234.560, currency: 'JOD' }
|
||||||
|
*
|
||||||
|
* @param {string} value Number to be parsed
|
||||||
|
* @param {import('../../localize/types/LocalizeMixinTypes.js').FormatNumberOptions} [givenOptions] Locale Options
|
||||||
|
* @returns {import('../types/index.js').AmountDropdownModelValue}
|
||||||
|
*/
|
||||||
|
export const parseAmount = (value, givenOptions) => {
|
||||||
|
const parsedAmount = _parseAmount(value, givenOptions);
|
||||||
|
|
||||||
|
return {
|
||||||
|
amount: parsedAmount,
|
||||||
|
currency: givenOptions?.currency,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
/**
|
||||||
|
* Serializes the modelValue to the international standard notation.
|
||||||
|
* @param {import("../types/index.js").AmountDropdownModelValue} modelValue
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export const serializer = modelValue =>
|
||||||
|
/**
|
||||||
|
* There seems to be some debate on a common international notation vs localization.
|
||||||
|
* The unwritten standard is, e.g: EUR 123
|
||||||
|
*/
|
||||||
|
`${modelValue?.currency} ${modelValue?.amount}`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deserializes the serializedValue back to modelValue.
|
||||||
|
* @param {string} serializedValue
|
||||||
|
* @returns {import("../types/index.js").AmountDropdownModelValue}
|
||||||
|
*/
|
||||||
|
export const deserializer = serializedValue => {
|
||||||
|
const [currency, amount] = serializedValue.split(' ');
|
||||||
|
return {
|
||||||
|
currency,
|
||||||
|
amount,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { IsNumber, Validator } from '@lion/ui/form-core.js';
|
||||||
|
import { currencyUtil } from './currencyUtil.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('../../form-core/types/validate/validate.js').FeedbackMessageData} FeedbackMessageData
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class CurrencyAndAmount extends Validator {
|
||||||
|
static validatorName = 'CurrencyAndAmount';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('../types/index.js').AmountDropdownModelValue} modelValue amount and currency symbol
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
execute(modelValue) {
|
||||||
|
// @ts-expect-error - cannot cast string to CurrencyCode outside a TS file
|
||||||
|
const validCurrencyCode = currencyUtil.allCurrencies.has(modelValue?.currency);
|
||||||
|
const isNumber = new IsNumber().execute(modelValue.amount);
|
||||||
|
|
||||||
|
return validCurrencyCode && isNumber;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
/**
|
||||||
|
* @typedef {HTMLSelectElement|HTMLElement & {modelValue:string}} DropdownElement
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {DropdownElement} dropdownEl
|
||||||
|
* @param {string} value
|
||||||
|
*/
|
||||||
|
export function mimicUserChangingDropdown(dropdownEl, value) {
|
||||||
|
if ('modelValue' in dropdownEl) {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
dropdownEl.modelValue = value;
|
||||||
|
dropdownEl.dispatchEvent(
|
||||||
|
new CustomEvent('model-value-changed', { detail: { isTriggeredByUser: true } }),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
dropdownEl.value = value;
|
||||||
|
dropdownEl.dispatchEvent(new Event('change'));
|
||||||
|
dropdownEl.dispatchEvent(new Event('input'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,322 @@
|
||||||
|
import { LionInputAmountDropdown } from '@lion/ui/input-amount-dropdown.js';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
import {
|
||||||
|
fixtureSync as _fixtureSync,
|
||||||
|
fixture as _fixture,
|
||||||
|
unsafeStatic,
|
||||||
|
aTimeout,
|
||||||
|
defineCE,
|
||||||
|
expect,
|
||||||
|
html,
|
||||||
|
} from '@open-wc/testing';
|
||||||
|
|
||||||
|
import { isActiveElement } from '../../core/test-helpers/isActiveElement.js';
|
||||||
|
import { mimicUserChangingDropdown } from '../test-helpers/mimicUserChangingDropdown.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('../types/index.js').TemplateDataForDropdownInputAmount} TemplateDataForDropdownInputAmount
|
||||||
|
* @typedef {HTMLSelectElement|HTMLElement & {modelValue:string}} DropdownElement
|
||||||
|
* @typedef {import('lit').TemplateResult} TemplateResult
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fixture = /** @type {(arg: string | TemplateResult) => Promise<LionInputAmountDropdown>} */ (
|
||||||
|
_fixture
|
||||||
|
);
|
||||||
|
const fixtureSync = /** @type {(arg: string | TemplateResult) => LionInputAmountDropdown} */ (
|
||||||
|
_fixtureSync
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {DropdownElement | HTMLSelectElement} dropdownEl
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function getDropdownValue(dropdownEl) {
|
||||||
|
if ('modelValue' in dropdownEl) {
|
||||||
|
return dropdownEl.modelValue;
|
||||||
|
}
|
||||||
|
return dropdownEl.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {{ klass:LionInputAmountDropdown }} config
|
||||||
|
*/
|
||||||
|
// @ts-expect-error
|
||||||
|
export function runInputAmountDropdownSuite({ klass } = { klass: LionInputAmountDropdown }) {
|
||||||
|
// @ts-ignore
|
||||||
|
const tagName = defineCE(/** @type {* & HTMLElement} */ (class extends klass {}));
|
||||||
|
const tag = unsafeStatic(tagName);
|
||||||
|
|
||||||
|
describe('LionInputAmountDropdown', () => {
|
||||||
|
it('syncs value of dropdown on init if input has no value', async () => {
|
||||||
|
const el = await fixture(html` <${tag}></${tag}> `);
|
||||||
|
expect(el.value).to.equal('');
|
||||||
|
expect(getDropdownValue(/** @type {DropdownElement} */ (el.refs.dropdown.value))).to.equal(
|
||||||
|
'GBP',
|
||||||
|
);
|
||||||
|
expect(el.modelValue).to.eql({ currency: 'GBP' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('syncs value of dropdown on reset if input has no value', async () => {
|
||||||
|
const el = await fixture(html` <${tag}></${tag}> `);
|
||||||
|
el.modelValue = { currency: 'EUR', amount: 123 };
|
||||||
|
await el.updateComplete;
|
||||||
|
el.reset();
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(el.modelValue).to.eql({ currency: 'GBP' });
|
||||||
|
expect(el.value).to.equal('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('syncs value of dropdown on init if input has no value does not influence interaction states', async () => {
|
||||||
|
const el = await fixture(html` <${tag}></${tag}> `);
|
||||||
|
expect(el.dirty).to.be.false;
|
||||||
|
expect(el.prefilled).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('syncs value of dropdown on reset also resets interaction states', async () => {
|
||||||
|
const el = await fixture(html` <${tag}></${tag}> `);
|
||||||
|
el.modelValue = { currency: 'EUR', amount: 123 };
|
||||||
|
await el.updateComplete;
|
||||||
|
|
||||||
|
expect(el.dirty).to.be.true;
|
||||||
|
expect(el.prefilled).to.be.false;
|
||||||
|
el.reset();
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(el.dirty).to.be.false;
|
||||||
|
expect(el.prefilled).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('syncs value of dropdown when preferredCurrency is set', async () => {
|
||||||
|
const el = await fixture(html` <${tag}
|
||||||
|
.preferredCurrencies="${['JPY', 'EUR']}"
|
||||||
|
></${tag}> `);
|
||||||
|
expect(el.modelValue).to.eql({ currency: 'JPY' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets correct interaction states on init if input has a value', async () => {
|
||||||
|
const el = await fixture(
|
||||||
|
html` <${tag} .modelValue="${{ currency: 'EUR', amount: 123 }}"></${tag}> `,
|
||||||
|
);
|
||||||
|
expect(el.dirty).to.be.false;
|
||||||
|
expect(el.prefilled).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Dropdown display', () => {
|
||||||
|
it('calls `templates.dropdown` with TemplateDataForDropdownInputAmount object', async () => {
|
||||||
|
const modelValue = { currency: 'EUR', amount: 123 };
|
||||||
|
const el = fixtureSync(html` <${tag}
|
||||||
|
.modelValue="${modelValue}"
|
||||||
|
.allowedCurrencies="${['EUR', 'GBP']}"
|
||||||
|
.preferredCurrencies="${['GBP']}"
|
||||||
|
></${tag}> `);
|
||||||
|
const spy = sinon.spy(
|
||||||
|
/** @type {typeof LionInputAmountDropdown} */ (el.constructor).templates,
|
||||||
|
'dropdown',
|
||||||
|
);
|
||||||
|
|
||||||
|
await el.updateComplete;
|
||||||
|
const dropdownNode = el.refs.dropdown.value;
|
||||||
|
const templateDataForDropdown = /** @type {TemplateDataForDropdownInputAmount} */ (
|
||||||
|
spy.args[0][0]
|
||||||
|
);
|
||||||
|
expect(templateDataForDropdown).to.eql(
|
||||||
|
/** @type {TemplateDataForDropdownInputAmount} */ ({
|
||||||
|
data: {
|
||||||
|
currency: 'EUR',
|
||||||
|
regionMetaList: [
|
||||||
|
{
|
||||||
|
currencyCode: 'EUR',
|
||||||
|
currencySymbol: '€',
|
||||||
|
nameForLocale: 'Euro',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
regionMetaListPreferred: [
|
||||||
|
{
|
||||||
|
currencyCode: 'GBP',
|
||||||
|
currencySymbol: '£',
|
||||||
|
nameForLocale: 'British Pound',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
refs: {
|
||||||
|
dropdown: {
|
||||||
|
labels: {
|
||||||
|
selectCurrency: 'Select currency',
|
||||||
|
allCurrencies: 'All currencies',
|
||||||
|
preferredCurrencies: 'Suggested currencies',
|
||||||
|
},
|
||||||
|
listeners: {
|
||||||
|
// @ts-expect-error [allow-protected]
|
||||||
|
change: el._onDropdownValueChange,
|
||||||
|
// @ts-expect-error [allow-protected]
|
||||||
|
'model-value-changed': el._onDropdownValueChange,
|
||||||
|
},
|
||||||
|
props: { style: 'height: 100%;' },
|
||||||
|
ref: {
|
||||||
|
value: dropdownNode,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// @ts-expect-error [allow-protected]
|
||||||
|
input: el._inputNode,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
spy.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can override "all-currencies-label"', async () => {
|
||||||
|
const el = fixtureSync(html` <${tag}
|
||||||
|
.preferredCurrencies="${['GBP']}"
|
||||||
|
></${tag}> `);
|
||||||
|
const spy = sinon.spy(
|
||||||
|
/** @type {typeof LionInputAmountDropdown} */ (el.constructor).templates,
|
||||||
|
'dropdown',
|
||||||
|
);
|
||||||
|
// @ts-expect-error [allow-protected]
|
||||||
|
el._allCurrenciesLabel = 'foo';
|
||||||
|
await el.updateComplete;
|
||||||
|
const templateDataForDropdown = /** @type {TemplateDataForDropdownInputAmount} */ (
|
||||||
|
spy.args[0][0]
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(templateDataForDropdown.refs.dropdown.labels.allCurrencies).to.eql('foo');
|
||||||
|
spy.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can override "preferred-currencies-label"', async () => {
|
||||||
|
const el = fixtureSync(html` <${tag}
|
||||||
|
.preferredCurrencies="${['GBP']}"
|
||||||
|
></${tag}> `);
|
||||||
|
const spy = sinon.spy(
|
||||||
|
/** @type {typeof LionInputAmountDropdown} */ (el.constructor).templates,
|
||||||
|
'dropdown',
|
||||||
|
);
|
||||||
|
// @ts-expect-error [allow-protected]
|
||||||
|
el._preferredCurrenciesLabel = 'foo';
|
||||||
|
await el.updateComplete;
|
||||||
|
const templateDataForDropdown = /** @type {TemplateDataForDropdownInputAmount} */ (
|
||||||
|
spy.args[0][0]
|
||||||
|
);
|
||||||
|
expect(templateDataForDropdown.refs.dropdown.labels.preferredCurrencies).to.eql('foo');
|
||||||
|
spy.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('syncs dropdown value initially from locale', async () => {
|
||||||
|
const el = await fixture(html` <${tag} .allowedCurrencies="${['GBP']}"></${tag}> `);
|
||||||
|
expect(getDropdownValue(/** @type {DropdownElement} */ (el.refs.dropdown.value))).to.equal(
|
||||||
|
'GBP',
|
||||||
|
);
|
||||||
|
expect(el.modelValue).to.eql({
|
||||||
|
currency: 'GBP',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('syncs disabled attribute to dropdown', async () => {
|
||||||
|
const el = await fixture(html` <${tag} disabled></${tag}> `);
|
||||||
|
expect(/** @type {HTMLElement} */ (el.refs.dropdown.value)?.hasAttribute('disabled')).to.be
|
||||||
|
.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('disables dropdown on readonly', async () => {
|
||||||
|
const el = await fixture(html` <${tag} readonly></${tag}> `);
|
||||||
|
expect(/** @type {HTMLElement} */ (el.refs.dropdown.value)?.hasAttribute('disabled')).to.be
|
||||||
|
.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders to prefix slot in light dom', async () => {
|
||||||
|
const el = await fixture(html` <${tag} .allowedCurrencies="${['GBP']}"></${tag}> `);
|
||||||
|
const prefixSlot = /** @type {HTMLElement} */ (
|
||||||
|
/** @type {HTMLElement} */ (el.refs.dropdown.value)
|
||||||
|
);
|
||||||
|
expect(prefixSlot.getAttribute('slot')).to.equal('prefix');
|
||||||
|
expect(prefixSlot.slot).to.equal('prefix');
|
||||||
|
expect(prefixSlot.parentElement).to.equal(el);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders to suffix slot in light dom', async () => {
|
||||||
|
class WithSuffixRenderInputAmountDropdown extends LionInputAmountDropdown {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this._dropdownSlot = 'suffix';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const suffixTagName = defineCE(WithSuffixRenderInputAmountDropdown);
|
||||||
|
const suffixTag = unsafeStatic(suffixTagName);
|
||||||
|
|
||||||
|
const el = await fixture(html`
|
||||||
|
<${suffixTag}
|
||||||
|
.allowedCurrencies="${['GBP']}"
|
||||||
|
></${suffixTag}>
|
||||||
|
`);
|
||||||
|
const prefixSlot = /** @type {HTMLElement} */ (
|
||||||
|
/** @type {HTMLElement} */ (el.refs.dropdown.value)
|
||||||
|
);
|
||||||
|
expect(prefixSlot.getAttribute('slot')).to.equal('suffix');
|
||||||
|
expect(prefixSlot.slot).to.equal('suffix');
|
||||||
|
expect(prefixSlot.parentElement).to.equal(el);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rerenders light dom when CurrencyUtil loaded', async () => {
|
||||||
|
const el = await fixture(html` <${tag} .allowedCurrencies="${['GBP']}"></${tag}> `);
|
||||||
|
// @ts-ignore
|
||||||
|
const spy = sinon.spy(el, '__rerenderSlot');
|
||||||
|
await aTimeout(0);
|
||||||
|
expect(spy).to.have.been.calledWith('prefix');
|
||||||
|
spy.restore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('On dropdown value change', () => {
|
||||||
|
it('changes the currently active currency code in the textbox', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<${tag}
|
||||||
|
.allowedCurrencies="${['GBP', 'EUR']}"
|
||||||
|
.modelValue="${{ currency: 'GBP', amount: 123 }}"
|
||||||
|
></${tag}>
|
||||||
|
`);
|
||||||
|
// @ts-ignore
|
||||||
|
mimicUserChangingDropdown(el.refs.dropdown.value, 'EUR');
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(el.currency).to.equal('EUR');
|
||||||
|
expect(el.modelValue).to.eql({ amount: 123, currency: 'EUR' });
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(el.value).to.equal('123.00');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('changes the currently active country code in the textbox when empty', async () => {
|
||||||
|
const el = await fixture(html` <${tag} .allowedCurrencies="${['GBP', 'EUR']}"></${tag}> `);
|
||||||
|
el.value = '';
|
||||||
|
// @ts-ignore
|
||||||
|
mimicUserChangingDropdown(el.refs.dropdown.value, 'EUR');
|
||||||
|
await el.updateComplete;
|
||||||
|
await el.updateComplete;
|
||||||
|
|
||||||
|
expect(el.modelValue).to.eql({ currency: 'EUR', amount: '' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps focus on dropdownElement after selection if selected via unopened dropdown', async () => {
|
||||||
|
const el = await fixture(
|
||||||
|
html` <${tag} .allowedCurrencies="${[
|
||||||
|
'GBP',
|
||||||
|
'EUR',
|
||||||
|
]}" .modelValue="${{ currency: 'GBP', amount: 123 }}"></${tag}> `,
|
||||||
|
);
|
||||||
|
const dropdownElement = el.refs.dropdown.value;
|
||||||
|
// @ts-ignore
|
||||||
|
mimicUserChangingDropdown(dropdownElement, 'EUR');
|
||||||
|
await el.updateComplete;
|
||||||
|
// @ts-expect-error [allow-protected-in-tests]
|
||||||
|
expect(isActiveElement(el._inputNode)).to.be.false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('is empty', () => {
|
||||||
|
it('ignores initial currencyCode', async () => {
|
||||||
|
const el = await fixture(html` <${tag}></${tag}> `);
|
||||||
|
// @ts-ignore
|
||||||
|
expect(el._isEmpty(), 'empty').to.be.true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,199 @@
|
||||||
|
import { mimicUserChangingDropdown } from '@lion/ui/input-amount-dropdown-test-helpers.js';
|
||||||
|
import { runInputAmountDropdownSuite } from '@lion/ui/input-amount-dropdown-test-suites.js';
|
||||||
|
import { LionInputAmountDropdown } from '@lion/ui/input-amount-dropdown.js';
|
||||||
|
import { aTimeout, expect, fixture } from '@open-wc/testing';
|
||||||
|
import { LionSelectRich } from '@lion/ui/select-rich.js';
|
||||||
|
import { repeat } from 'lit/directives/repeat.js';
|
||||||
|
import { LionOption } from '@lion/ui/listbox.js';
|
||||||
|
import { ref } from 'lit/directives/ref.js';
|
||||||
|
import { html } from 'lit';
|
||||||
|
|
||||||
|
import { isActiveElement } from '../../core/test-helpers/isActiveElement.js';
|
||||||
|
import { ScopedElementsMixin } from '../../core/src/ScopedElementsMixin.js';
|
||||||
|
import '@lion/ui/define/lion-input-amount-dropdown.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('../types/index.js').TemplateDataForDropdownInputAmount} TemplateDataForDropdownInputAmount
|
||||||
|
* @typedef {HTMLSelectElement|HTMLElement & {modelValue:string}} DropdownElement
|
||||||
|
* @typedef {import('../types/index.js').RegionMeta} RegionMeta
|
||||||
|
* @typedef {import('lit').TemplateResult} TemplateResult
|
||||||
|
*/
|
||||||
|
|
||||||
|
class WithFormControlInputAmountDropdown extends ScopedElementsMixin(LionInputAmountDropdown) {
|
||||||
|
/**
|
||||||
|
* @configure ScopedElementsMixin
|
||||||
|
*/
|
||||||
|
static scopedElements = {
|
||||||
|
...super.scopedElements,
|
||||||
|
'lion-select-rich': LionSelectRich,
|
||||||
|
'lion-option': LionOption,
|
||||||
|
};
|
||||||
|
|
||||||
|
static templates = {
|
||||||
|
...(super.templates || {}),
|
||||||
|
/**
|
||||||
|
* @param {TemplateDataForDropdownInputAmount} templateDataForDropdown
|
||||||
|
*/
|
||||||
|
dropdown: templateDataForDropdown => {
|
||||||
|
const { refs, data } = templateDataForDropdown;
|
||||||
|
// TODO: once spread directive available, use it per ref (like ref(refs?.dropdown?.ref))
|
||||||
|
return html`
|
||||||
|
<lion-select-rich
|
||||||
|
${ref(refs?.dropdown?.ref)}
|
||||||
|
label="${refs?.dropdown?.labels?.country}"
|
||||||
|
label-sr-only
|
||||||
|
@model-value-changed="${refs?.dropdown?.listeners['model-value-changed']}"
|
||||||
|
style="${refs?.dropdown?.props?.style}"
|
||||||
|
>
|
||||||
|
${repeat(
|
||||||
|
data.regionMetaList,
|
||||||
|
regionMeta => regionMeta.currencyCode,
|
||||||
|
regionMeta => html`
|
||||||
|
<lion-option .choiceValue="${regionMeta.currencyCode}"> </lion-option>
|
||||||
|
`,
|
||||||
|
)}
|
||||||
|
</lion-select-rich>
|
||||||
|
`;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
runInputAmountDropdownSuite();
|
||||||
|
|
||||||
|
describe('WithFormControlInputAmountDropdown', () => {
|
||||||
|
// @ts-expect-error
|
||||||
|
// Runs it for LionSelectRich, which uses .modelValue/@model-value-changed instead of .value/@change
|
||||||
|
runInputAmountDropdownSuite({ klass: WithFormControlInputAmountDropdown });
|
||||||
|
|
||||||
|
it('focuses the textbox right after selection if selected via opened dropdown if interaction-mode is mac', async () => {
|
||||||
|
class InputAmountDropdownMac extends LionInputAmountDropdown {
|
||||||
|
static templates = {
|
||||||
|
...(super.templates || {}),
|
||||||
|
/**
|
||||||
|
* @param {TemplateDataForDropdownInputAmount} templateDataForDropdown
|
||||||
|
*/
|
||||||
|
dropdown: templateDataForDropdown => {
|
||||||
|
const { refs, data } = templateDataForDropdown;
|
||||||
|
// TODO: once spread directive available, use it per ref (like ref(refs?.dropdown?.ref))
|
||||||
|
return html`
|
||||||
|
<lion-select-rich
|
||||||
|
${ref(refs?.dropdown?.ref)}
|
||||||
|
label="${refs?.dropdown?.labels?.country}"
|
||||||
|
label-sr-only
|
||||||
|
@model-value-changed="${refs?.dropdown?.listeners['model-value-changed']}"
|
||||||
|
style="${refs?.dropdown?.props?.style}"
|
||||||
|
interaction-mode="mac"
|
||||||
|
>
|
||||||
|
${repeat(
|
||||||
|
data.regionMetaList,
|
||||||
|
regionMeta => regionMeta.currencyCode,
|
||||||
|
regionMeta => html`
|
||||||
|
<lion-option .choiceValue="${regionMeta.currencyCode}"> </lion-option>
|
||||||
|
`,
|
||||||
|
)}
|
||||||
|
</lion-select-rich>
|
||||||
|
`;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
customElements.define('input-amount-dropdown-mac', InputAmountDropdownMac);
|
||||||
|
const el = /** @type {LionInputAmountDropdown} */ (
|
||||||
|
await fixture(html`
|
||||||
|
<input-amount-dropdown-mac
|
||||||
|
.allowedCurrencies="${['GBP', 'EUR']}"
|
||||||
|
.modelValue="{ currency: 'GBP', amount: '123' }"
|
||||||
|
></input-amount-dropdown-mac>
|
||||||
|
`)
|
||||||
|
);
|
||||||
|
const dropdownElement = el.refs.dropdown.value;
|
||||||
|
// @ts-expect-error [allow-protected-in-tests]
|
||||||
|
if (dropdownElement?._overlayCtrl) {
|
||||||
|
// @ts-expect-error [allow-protected-in-tests]
|
||||||
|
dropdownElement._overlayCtrl.show();
|
||||||
|
mimicUserChangingDropdown(dropdownElement, 'EUR');
|
||||||
|
await el.updateComplete;
|
||||||
|
await aTimeout(0);
|
||||||
|
// @ts-expect-error [allow-protected-in-tests]
|
||||||
|
expect(isActiveElement(el._inputNode)).to.be.true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not focus the textbox right after selection if selected via opened dropdown if interaction-mode is windows/linux', async () => {
|
||||||
|
class InputAmountDropdownWindows extends LionInputAmountDropdown {
|
||||||
|
static templates = {
|
||||||
|
...(super.templates || {}),
|
||||||
|
/**
|
||||||
|
* @param {TemplateDataForDropdownInputAmount} templateDataForDropdown
|
||||||
|
*/
|
||||||
|
dropdown: templateDataForDropdown => {
|
||||||
|
const { refs, data } = templateDataForDropdown;
|
||||||
|
// TODO: once spread directive available, use it per ref (like ref(refs?.dropdown?.ref))
|
||||||
|
return html`
|
||||||
|
<lion-select-rich
|
||||||
|
${ref(refs?.dropdown?.ref)}
|
||||||
|
label="${refs?.dropdown?.labels?.country}"
|
||||||
|
label-sr-only
|
||||||
|
@model-value-changed="${refs?.dropdown?.listeners['model-value-changed']}"
|
||||||
|
style="${refs?.dropdown?.props?.style}"
|
||||||
|
interaction-mode="windows/linux"
|
||||||
|
>
|
||||||
|
${repeat(
|
||||||
|
data.regionMetaList,
|
||||||
|
regionMeta => regionMeta.currencyCode,
|
||||||
|
regionMeta => html`
|
||||||
|
<lion-option .choiceValue="${regionMeta.currencyCode}"> </lion-option>
|
||||||
|
`,
|
||||||
|
)}
|
||||||
|
</lion-select-rich>
|
||||||
|
`;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
customElements.define('input-amount-dropdown-windows', InputAmountDropdownWindows);
|
||||||
|
const el = /** @type {LionInputAmountDropdown} */ (
|
||||||
|
await fixture(html`
|
||||||
|
<input-amount-dropdown-windows
|
||||||
|
.allowedCurrencies="${['GBP', 'EUR']}"
|
||||||
|
.modelValue="{ currency: 'GBP', amount: '123' }"
|
||||||
|
></input-amount-dropdown-windows>
|
||||||
|
`)
|
||||||
|
);
|
||||||
|
const dropdownElement = el.refs.dropdown.value;
|
||||||
|
// @ts-expect-error [allow-protected-in-tests]
|
||||||
|
if (dropdownElement?._overlayCtrl) {
|
||||||
|
// @ts-expect-error [allow-protected-in-tests]
|
||||||
|
dropdownElement._overlayCtrl.show();
|
||||||
|
mimicUserChangingDropdown(dropdownElement, 'EUR');
|
||||||
|
await el.updateComplete;
|
||||||
|
await aTimeout(0);
|
||||||
|
// @ts-expect-error [allow-protected-in-tests]
|
||||||
|
expect(isActiveElement(el._inputNode)).to.be.false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('defaultValidators', () => {
|
||||||
|
it('without interaction are not called', async () => {
|
||||||
|
const el = /** @type {LionInputAmountDropdown} */ (
|
||||||
|
await fixture(html`
|
||||||
|
<lion-input-amount-dropdown .allowedCurrencies="${['EUR']}"></lion-input-amount-dropdown>
|
||||||
|
`)
|
||||||
|
);
|
||||||
|
await aTimeout(0);
|
||||||
|
expect(el.hasFeedbackFor).to.deep.equal([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('with interaction are called', async () => {
|
||||||
|
const el = /** @type {LionInputAmountDropdown} */ (
|
||||||
|
await fixture(html`
|
||||||
|
<lion-input-amount-dropdown
|
||||||
|
.allowedCurrencies="${['GBP', 'EUR']}"
|
||||||
|
></lion-input-amount-dropdown>
|
||||||
|
`)
|
||||||
|
);
|
||||||
|
el.modelValue = { currency: 'EUR' };
|
||||||
|
await aTimeout(0);
|
||||||
|
el.modelValue = { currency: 'EUR', amount: '' };
|
||||||
|
expect(el.hasFeedbackFor).to.deep.equal(['error']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
export default {
|
||||||
|
allCurrencies: 'All currencies',
|
||||||
|
selectCurrency: 'Select currency',
|
||||||
|
suggestedCurrencies: 'Suggested currencies',
|
||||||
|
};
|
||||||
234
packages/ui/components/input-amount-dropdown/types/index.ts
Normal file
234
packages/ui/components/input-amount-dropdown/types/index.ts
Normal file
|
|
@ -0,0 +1,234 @@
|
||||||
|
import { LionSelectRich } from '@lion/ui/select-rich.js';
|
||||||
|
import { LionCombobox } from '@lion/ui/combobox.js';
|
||||||
|
import { OverlayController } from '../../overlays/src/OverlayController.js';
|
||||||
|
|
||||||
|
type RefTemplateData = {
|
||||||
|
ref?: { value?: HTMLElement };
|
||||||
|
props?: { [key: string]: any };
|
||||||
|
listeners?: { [key: string]: any };
|
||||||
|
labels?: { [key: string]: any };
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RegionMeta = {
|
||||||
|
currencyCode: CurrencyCode;
|
||||||
|
nameForLocale?: string;
|
||||||
|
currencySymbol?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OnDropdownChangeEvent = Event & {
|
||||||
|
target: { value?: string; modelValue?: string; _overlayCtrl?: OverlayController };
|
||||||
|
detail?: { initialize: boolean };
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DropdownRef = { value: HTMLSelectElement | LionSelectRich | LionCombobox | undefined };
|
||||||
|
|
||||||
|
export type TemplateDataForDropdownInputAmount = {
|
||||||
|
refs: {
|
||||||
|
dropdown: RefTemplateData & {
|
||||||
|
ref: DropdownRef;
|
||||||
|
props: { style: string };
|
||||||
|
listeners: {
|
||||||
|
change: (event: OnDropdownChangeEvent) => void;
|
||||||
|
'model-value-changed': (event: OnDropdownChangeEvent) => void;
|
||||||
|
};
|
||||||
|
labels: { selectCurrency: string };
|
||||||
|
};
|
||||||
|
input: HTMLInputElement;
|
||||||
|
};
|
||||||
|
data: {
|
||||||
|
currency: CurrencyCode | string;
|
||||||
|
regionMetaList: RegionMeta[];
|
||||||
|
regionMetaListPreferred: RegionMeta[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AmountDropdownModelValue = {
|
||||||
|
currency?: CurrencyCode | string;
|
||||||
|
amount?: number | string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All currency codes according to i18n
|
||||||
|
*/
|
||||||
|
export type CurrencyCode =
|
||||||
|
| 'EUR'
|
||||||
|
| 'AED'
|
||||||
|
| 'AFN'
|
||||||
|
| 'XCD'
|
||||||
|
| 'ALL'
|
||||||
|
| 'AMD'
|
||||||
|
| 'AOA'
|
||||||
|
| 'ARS'
|
||||||
|
| 'USD'
|
||||||
|
| 'AUD'
|
||||||
|
| 'AWG'
|
||||||
|
| 'AZN'
|
||||||
|
| 'BAM'
|
||||||
|
| 'BBD'
|
||||||
|
| 'BDT'
|
||||||
|
| 'XOF'
|
||||||
|
| 'BGN'
|
||||||
|
| 'BHD'
|
||||||
|
| 'BIF'
|
||||||
|
| 'BMD'
|
||||||
|
| 'BND'
|
||||||
|
| 'BOB'
|
||||||
|
| 'BRL'
|
||||||
|
| 'BSD'
|
||||||
|
| 'BTN'
|
||||||
|
| 'NOK'
|
||||||
|
| 'BWP'
|
||||||
|
| 'BYN'
|
||||||
|
| 'BZD'
|
||||||
|
| 'CAD'
|
||||||
|
| 'CDF'
|
||||||
|
| 'XAF'
|
||||||
|
| 'CHF'
|
||||||
|
| 'NZD'
|
||||||
|
| 'CLP'
|
||||||
|
| 'CNY'
|
||||||
|
| 'COP'
|
||||||
|
| 'CRC'
|
||||||
|
| 'CUP'
|
||||||
|
| 'CVE'
|
||||||
|
| 'ANG'
|
||||||
|
| 'CZK'
|
||||||
|
| 'DJF'
|
||||||
|
| 'DKK'
|
||||||
|
| 'DOP'
|
||||||
|
| 'DZD'
|
||||||
|
| 'EGP'
|
||||||
|
| 'MAD'
|
||||||
|
| 'ERN'
|
||||||
|
| 'ETB'
|
||||||
|
| 'FJD'
|
||||||
|
| 'FKP'
|
||||||
|
| 'GBP'
|
||||||
|
| 'GEL'
|
||||||
|
| 'GHS'
|
||||||
|
| 'GIP'
|
||||||
|
| 'GMD'
|
||||||
|
| 'GNF'
|
||||||
|
| 'GTQ'
|
||||||
|
| 'GYD'
|
||||||
|
| 'HKD'
|
||||||
|
| 'HNL'
|
||||||
|
| 'HTG'
|
||||||
|
| 'HUF'
|
||||||
|
| 'IDR'
|
||||||
|
| 'ILS'
|
||||||
|
| 'INR'
|
||||||
|
| 'IQD'
|
||||||
|
| 'IRR'
|
||||||
|
| 'ISK'
|
||||||
|
| 'JMD'
|
||||||
|
| 'JOD'
|
||||||
|
| 'JPY'
|
||||||
|
| 'KES'
|
||||||
|
| 'KGS'
|
||||||
|
| 'KHR'
|
||||||
|
| 'KMF'
|
||||||
|
| 'KPW'
|
||||||
|
| 'KRW'
|
||||||
|
| 'KWD'
|
||||||
|
| 'KYD'
|
||||||
|
| 'KZT'
|
||||||
|
| 'LAK'
|
||||||
|
| 'LBP'
|
||||||
|
| 'LKR'
|
||||||
|
| 'LRD'
|
||||||
|
| 'LSL'
|
||||||
|
| 'LYD'
|
||||||
|
| 'MDL'
|
||||||
|
| 'MGA'
|
||||||
|
| 'MKD'
|
||||||
|
| 'MMK'
|
||||||
|
| 'MNT'
|
||||||
|
| 'MOP'
|
||||||
|
| 'MRU'
|
||||||
|
| 'MUR'
|
||||||
|
| 'MVR'
|
||||||
|
| 'MWK'
|
||||||
|
| 'MXN'
|
||||||
|
| 'MYR'
|
||||||
|
| 'MZN'
|
||||||
|
| 'NAD'
|
||||||
|
| 'XPF'
|
||||||
|
| 'NGN'
|
||||||
|
| 'NIO'
|
||||||
|
| 'NPR'
|
||||||
|
| 'OMR'
|
||||||
|
| 'PAB'
|
||||||
|
| 'PEN'
|
||||||
|
| 'PGK'
|
||||||
|
| 'PHP'
|
||||||
|
| 'PKR'
|
||||||
|
| 'PLN'
|
||||||
|
| 'PYG'
|
||||||
|
| 'QAR'
|
||||||
|
| 'RON'
|
||||||
|
| 'RSD'
|
||||||
|
| 'RUB'
|
||||||
|
| 'RWF'
|
||||||
|
| 'SAR'
|
||||||
|
| 'SBD'
|
||||||
|
| 'SCR'
|
||||||
|
| 'SDG'
|
||||||
|
| 'SEK'
|
||||||
|
| 'SGD'
|
||||||
|
| 'SHP'
|
||||||
|
| 'SLE'
|
||||||
|
| 'SOS'
|
||||||
|
| 'SRD'
|
||||||
|
| 'SSP'
|
||||||
|
| 'STN'
|
||||||
|
| 'SVC'
|
||||||
|
| 'SYP'
|
||||||
|
| 'SZL'
|
||||||
|
| 'THB'
|
||||||
|
| 'TJS'
|
||||||
|
| 'TMT'
|
||||||
|
| 'TND'
|
||||||
|
| 'TOP'
|
||||||
|
| 'TRY'
|
||||||
|
| 'TTD'
|
||||||
|
| 'TWD'
|
||||||
|
| 'TZS'
|
||||||
|
| 'UAH'
|
||||||
|
| 'UGX'
|
||||||
|
| 'UYU'
|
||||||
|
| 'UZS'
|
||||||
|
| 'VED'
|
||||||
|
| 'VES'
|
||||||
|
| 'VND'
|
||||||
|
| 'VUV'
|
||||||
|
| 'WST'
|
||||||
|
| 'XCG'
|
||||||
|
| 'YER'
|
||||||
|
| 'ZAR'
|
||||||
|
| 'ZMW'
|
||||||
|
| 'ZWG';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type for country to currency list.
|
||||||
|
* The type is set to Partial as there are some islands that don't have a corresponding
|
||||||
|
* currencyCode as a standard (at the time of writing at least).
|
||||||
|
*
|
||||||
|
* Said missing countries code: AC, TA, XK
|
||||||
|
*/
|
||||||
|
export type countryToCurrencyList = Partial<{
|
||||||
|
[key in import('../../input-tel/types/index.js').RegionCode]: CurrencyCode;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a mapping of region codes to currency codes using a Map.
|
||||||
|
*/
|
||||||
|
export type RegionToCurrencyMap = Map<
|
||||||
|
import('../../input-tel/types/index.js').RegionCode,
|
||||||
|
CurrencyCode
|
||||||
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a set of all unique currency codes derived from the RegionToCurrencyMap values.
|
||||||
|
*/
|
||||||
|
export type AllCurrenciesSet = Set<CurrencyCode>;
|
||||||
|
|
@ -36,6 +36,7 @@ export type RegionCode =
|
||||||
| 'BR'
|
| 'BR'
|
||||||
| 'BS'
|
| 'BS'
|
||||||
| 'BT'
|
| 'BT'
|
||||||
|
| 'BV'
|
||||||
| 'BW'
|
| 'BW'
|
||||||
| 'BY'
|
| 'BY'
|
||||||
| 'BZ'
|
| 'BZ'
|
||||||
|
|
@ -91,11 +92,13 @@ export type RegionCode =
|
||||||
| 'GP'
|
| 'GP'
|
||||||
| 'GQ'
|
| 'GQ'
|
||||||
| 'GR'
|
| 'GR'
|
||||||
|
| 'GS'
|
||||||
| 'GT'
|
| 'GT'
|
||||||
| 'GU'
|
| 'GU'
|
||||||
| 'GW'
|
| 'GW'
|
||||||
| 'GY'
|
| 'GY'
|
||||||
| 'HK'
|
| 'HK'
|
||||||
|
| 'HM'
|
||||||
| 'HN'
|
| 'HN'
|
||||||
| 'HR'
|
| 'HR'
|
||||||
| 'HT'
|
| 'HT'
|
||||||
|
|
@ -180,6 +183,7 @@ export type RegionCode =
|
||||||
| 'PK'
|
| 'PK'
|
||||||
| 'PL'
|
| 'PL'
|
||||||
| 'PM'
|
| 'PM'
|
||||||
|
| 'PN'
|
||||||
| 'PR'
|
| 'PR'
|
||||||
| 'PS'
|
| 'PS'
|
||||||
| 'PT'
|
| 'PT'
|
||||||
|
|
@ -215,6 +219,7 @@ export type RegionCode =
|
||||||
| 'TA'
|
| 'TA'
|
||||||
| 'TC'
|
| 'TC'
|
||||||
| 'TD'
|
| 'TD'
|
||||||
|
| 'TF'
|
||||||
| 'TG'
|
| 'TG'
|
||||||
| 'TH'
|
| 'TH'
|
||||||
| 'TJ'
|
| 'TJ'
|
||||||
|
|
@ -230,6 +235,7 @@ export type RegionCode =
|
||||||
| 'TZ'
|
| 'TZ'
|
||||||
| 'UA'
|
| 'UA'
|
||||||
| 'UG'
|
| 'UG'
|
||||||
|
| 'UM'
|
||||||
| 'US'
|
| 'US'
|
||||||
| 'UY'
|
| 'UY'
|
||||||
| 'UZ'
|
| 'UZ'
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { formatNumberToParts } from './formatNumberToParts.js';
|
||||||
* getFractionDigits('JOD'); // return 3
|
* getFractionDigits('JOD'); // return 3
|
||||||
*
|
*
|
||||||
* @typedef {import('../../types/LocalizeMixinTypes.js').FormatNumberPart} FormatNumberPart
|
* @typedef {import('../../types/LocalizeMixinTypes.js').FormatNumberPart} FormatNumberPart
|
||||||
* @param {string} [currency="EUR"] Currency code e.g. EUR
|
* @param {import('../../../input-amount-dropdown/types/index.js').CurrencyCode | string} [currency="EUR"] Currency code e.g. EUR
|
||||||
* @returns {number} fraction for the given currency
|
* @returns {number} fraction for the given currency
|
||||||
*/
|
*/
|
||||||
export function getFractionDigits(currency = 'EUR') {
|
export function getFractionDigits(currency = 'EUR') {
|
||||||
|
|
|
||||||
3
packages/ui/exports/define/lion-input-amount-dropdown.js
Normal file
3
packages/ui/exports/define/lion-input-amount-dropdown.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { LionInputAmountDropdown } from '../input-amount-dropdown.js';
|
||||||
|
|
||||||
|
customElements.define('lion-input-amount-dropdown', LionInputAmountDropdown);
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export { mimicUserChangingDropdown } from '../components/input-amount-dropdown/test-helpers/mimicUserChangingDropdown.js';
|
||||||
1
packages/ui/exports/input-amount-dropdown-test-suites.js
Normal file
1
packages/ui/exports/input-amount-dropdown-test-suites.js
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { runInputAmountDropdownSuite } from '../components/input-amount-dropdown/test-suites/LionInputAmountDropdown.suite.js';
|
||||||
1
packages/ui/exports/input-amount-dropdown.js
Normal file
1
packages/ui/exports/input-amount-dropdown.js
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { LionInputAmountDropdown } from '../components/input-amount-dropdown/src/LionInputAmountDropdown.js';
|
||||||
|
|
@ -25,6 +25,7 @@
|
||||||
"./input-range-translations/*": "./components/input-range/translations/*",
|
"./input-range-translations/*": "./components/input-range/translations/*",
|
||||||
"./input-stepper-translations/*": "./components/input-stepper/translations/*",
|
"./input-stepper-translations/*": "./components/input-stepper/translations/*",
|
||||||
"./input-tel-translations/*": "./components/input-tel/translations/*",
|
"./input-tel-translations/*": "./components/input-tel/translations/*",
|
||||||
|
"./input-amount-dropdown-translations/*": "./components/input-amount-dropdown/translations/*",
|
||||||
"./overlays-translations/*": "./components/overlays/translations/*",
|
"./overlays-translations/*": "./components/overlays/translations/*",
|
||||||
"./pagination-translations/*": "./components/pagination/translations/*",
|
"./pagination-translations/*": "./components/pagination/translations/*",
|
||||||
"./progress-indicator-translations/*": "./components/progress-indicator/translations/*",
|
"./progress-indicator-translations/*": "./components/progress-indicator/translations/*",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue