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'
|
||||
| 'BS'
|
||||
| 'BT'
|
||||
| 'BV'
|
||||
| 'BW'
|
||||
| 'BY'
|
||||
| 'BZ'
|
||||
|
|
@ -91,11 +92,13 @@ export type RegionCode =
|
|||
| 'GP'
|
||||
| 'GQ'
|
||||
| 'GR'
|
||||
| 'GS'
|
||||
| 'GT'
|
||||
| 'GU'
|
||||
| 'GW'
|
||||
| 'GY'
|
||||
| 'HK'
|
||||
| 'HM'
|
||||
| 'HN'
|
||||
| 'HR'
|
||||
| 'HT'
|
||||
|
|
@ -180,6 +183,7 @@ export type RegionCode =
|
|||
| 'PK'
|
||||
| 'PL'
|
||||
| 'PM'
|
||||
| 'PN'
|
||||
| 'PR'
|
||||
| 'PS'
|
||||
| 'PT'
|
||||
|
|
@ -215,6 +219,7 @@ export type RegionCode =
|
|||
| 'TA'
|
||||
| 'TC'
|
||||
| 'TD'
|
||||
| 'TF'
|
||||
| 'TG'
|
||||
| 'TH'
|
||||
| 'TJ'
|
||||
|
|
@ -230,6 +235,7 @@ export type RegionCode =
|
|||
| 'TZ'
|
||||
| 'UA'
|
||||
| 'UG'
|
||||
| 'UM'
|
||||
| 'US'
|
||||
| 'UY'
|
||||
| 'UZ'
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { formatNumberToParts } from './formatNumberToParts.js';
|
|||
* getFractionDigits('JOD'); // return 3
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
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-stepper-translations/*": "./components/input-stepper/translations/*",
|
||||
"./input-tel-translations/*": "./components/input-tel/translations/*",
|
||||
"./input-amount-dropdown-translations/*": "./components/input-amount-dropdown/translations/*",
|
||||
"./overlays-translations/*": "./components/overlays/translations/*",
|
||||
"./pagination-translations/*": "./components/pagination/translations/*",
|
||||
"./progress-indicator-translations/*": "./components/progress-indicator/translations/*",
|
||||
|
|
|
|||
Loading…
Reference in a new issue