Merge pull request #1167 from ing-bank/feat/isTriggerByUser
[form-core] add detail.isTriggeredByUser
This commit is contained in:
commit
4958dad22f
47 changed files with 463 additions and 100 deletions
7
.changeset/fast-years-search.md
Normal file
7
.changeset/fast-years-search.md
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
'@lion/form-integrations': patch
|
||||||
|
'@lion/localize': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Improved localize DX by making it clear from source code structure what are main (exported) functions and what are util/helper functions consumed by those main functions.
|
||||||
|
Added Chrome Intl corrections for Philippine currency names and en-GB short month names.
|
||||||
9
.changeset/slow-forks-speak.md
Normal file
9
.changeset/slow-forks-speak.md
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
---
|
||||||
|
'@lion/form-core': minor
|
||||||
|
'@lion/listbox': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Added `isTriggeredByUser` meta data in `model-value-changed` event
|
||||||
|
|
||||||
|
Sometimes it can be helpful to detect whether a value change was caused by a user or via a programmatical change.
|
||||||
|
This feature acts as a normalization layer: since we use `model-value-changed` as a single source of truth event for all FormControls, there should be no use cases for (inconsistently implemented (cross browser)) events like `input`/`change`/`user-input-changed` etc.
|
||||||
|
|
@ -4,6 +4,16 @@ import { FormRegisteringMixin } from './registration/FormRegisteringMixin.js';
|
||||||
import { getAriaElementsInRightDomOrder } from './utils/getAriaElementsInRightDomOrder.js';
|
import { getAriaElementsInRightDomOrder } from './utils/getAriaElementsInRightDomOrder.js';
|
||||||
import { Unparseable } from './validate/Unparseable.js';
|
import { Unparseable } from './validate/Unparseable.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('@lion/core').TemplateResult} TemplateResult
|
||||||
|
* @typedef {import('@lion/core').CSSResult} CSSResult
|
||||||
|
* @typedef {import('@lion/core').nothing} nothing
|
||||||
|
* @typedef {import('@lion/core/types/SlotMixinTypes').SlotsMap} SlotsMap
|
||||||
|
* @typedef {import('../types/FormControlMixinTypes.js').FormControlHost} FormControlHost
|
||||||
|
* @typedef {import('../types/FormControlMixinTypes.js').FormControlMixin} FormControlMixin
|
||||||
|
* @typedef {import('../types/FormControlMixinTypes.js').ModelValueEventDetails} ModelValueEventDetails
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates random unique identifier (for dom elements)
|
* Generates random unique identifier (for dom elements)
|
||||||
* @param {string} prefix
|
* @param {string} prefix
|
||||||
|
|
@ -18,11 +28,6 @@ function uuid(prefix) {
|
||||||
* This Mixin is a shared fundament for all form components, it's applied on:
|
* This Mixin is a shared fundament for all form components, it's applied on:
|
||||||
* - LionField (which is extended to LionInput, LionTextarea, LionSelect etc. etc.)
|
* - LionField (which is extended to LionInput, LionTextarea, LionSelect etc. etc.)
|
||||||
* - LionFieldset (which is extended to LionRadioGroup, LionCheckboxGroup, LionForm)
|
* - LionFieldset (which is extended to LionRadioGroup, LionCheckboxGroup, LionForm)
|
||||||
* @typedef {import('@lion/core').TemplateResult} TemplateResult
|
|
||||||
* @typedef {import('@lion/core').CSSResult} CSSResult
|
|
||||||
* @typedef {import('@lion/core').nothing} nothing
|
|
||||||
* @typedef {import('@lion/core/types/SlotMixinTypes').SlotsMap} SlotsMap
|
|
||||||
* @typedef {import('../types/FormControlMixinTypes.js').FormControlMixin} FormControlMixin
|
|
||||||
* @param {import('@open-wc/dedupe-mixin').Constructor<import('@lion/core').LitElement>} superclass
|
* @param {import('@open-wc/dedupe-mixin').Constructor<import('@lion/core').LitElement>} superclass
|
||||||
* @type {FormControlMixin}
|
* @type {FormControlMixin}
|
||||||
*/
|
*/
|
||||||
|
|
@ -750,7 +755,11 @@ const FormControlMixinImplementation = superclass =>
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent('model-value-changed', {
|
new CustomEvent('model-value-changed', {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
detail: { formPath: [this], initialize: true },
|
detail: /** @type {ModelValueEventDetails} */ ({
|
||||||
|
formPath: [this],
|
||||||
|
initialize: true,
|
||||||
|
isTriggeredByUser: false,
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -822,7 +831,13 @@ const FormControlMixinImplementation = superclass =>
|
||||||
//
|
//
|
||||||
// Since for a11y everything needs to be in lightdom, we don't add 'composed:true'
|
// Since for a11y everything needs to be in lightdom, we don't add 'composed:true'
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent('model-value-changed', { bubbles: true, detail: { formPath } }),
|
new CustomEvent('model-value-changed', {
|
||||||
|
bubbles: true,
|
||||||
|
detail: /** @type {ModelValueEventDetails} */ ({
|
||||||
|
formPath,
|
||||||
|
isTriggeredByUser: Boolean(ev.detail?.isTriggeredByUser),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { ValidateMixin } from './validate/ValidateMixin.js';
|
||||||
/**
|
/**
|
||||||
* @typedef {import('../types/FormatMixinTypes').FormatMixin} FormatMixin
|
* @typedef {import('../types/FormatMixinTypes').FormatMixin} FormatMixin
|
||||||
* @typedef {import('@lion/localize/types/LocalizeMixinTypes').FormatNumberOptions} FormatOptions
|
* @typedef {import('@lion/localize/types/LocalizeMixinTypes').FormatNumberOptions} FormatOptions
|
||||||
|
* @typedef {import('../types/FormControlMixinTypes.js').ModelValueEventDetails} ModelValueEventDetails
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// For a future breaking release:
|
// For a future breaking release:
|
||||||
|
|
@ -316,7 +317,10 @@ const FormatMixinImplementation = superclass =>
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent('model-value-changed', {
|
new CustomEvent('model-value-changed', {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
detail: { formPath: [this] },
|
detail: /** @type { ModelValueEventDetails } */ ({
|
||||||
|
formPath: [this],
|
||||||
|
isTriggeredByUser: Boolean(this.__isHandlingUserInput),
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -175,6 +175,7 @@ const ChoiceInputMixinImplementation = superclass =>
|
||||||
<small class="choice-field__help-text">
|
<small class="choice-field__help-text">
|
||||||
<slot name="help-text"></slot>
|
<slot name="help-text"></slot>
|
||||||
</small>
|
</small>
|
||||||
|
${this._afterTemplate()}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -182,6 +183,10 @@ const ChoiceInputMixinImplementation = superclass =>
|
||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_afterTemplate() {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
this.addEventListener('user-input-changed', this.__toggleChecked);
|
this.addEventListener('user-input-changed', this.__toggleChecked);
|
||||||
|
|
@ -196,7 +201,9 @@ const ChoiceInputMixinImplementation = superclass =>
|
||||||
if (this.disabled) {
|
if (this.disabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this.__isHandlingUserInput = true;
|
||||||
this.checked = !this.checked;
|
this.checked = !this.checked;
|
||||||
|
this.__isHandlingUserInput = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -123,18 +123,23 @@ export function runFormatMixinSuite(customConfig) {
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fires `model-value-changed` for every change on the input', async () => {
|
it('fires `model-value-changed` for every input triggered by user', async () => {
|
||||||
const formatEl = /** @type {FormatClass} */ (await fixture(
|
const formatEl = /** @type {FormatClass} */ (await fixture(
|
||||||
html`<${elem}><input slot="input"></${elem}>`,
|
html`<${elem}><input slot="input"></${elem}>`,
|
||||||
));
|
));
|
||||||
|
|
||||||
let counter = 0;
|
let counter = 0;
|
||||||
formatEl.addEventListener('model-value-changed', () => {
|
let isTriggeredByUser = false;
|
||||||
|
formatEl.addEventListener('model-value-changed', (
|
||||||
|
/** @param {CustomEvent} event */ event,
|
||||||
|
) => {
|
||||||
counter += 1;
|
counter += 1;
|
||||||
|
isTriggeredByUser = /** @type {CustomEvent} */ (event).detail.isTriggeredByUser;
|
||||||
});
|
});
|
||||||
|
|
||||||
mimicUserInput(formatEl, generateValueBasedOnType());
|
mimicUserInput(formatEl, generateValueBasedOnType());
|
||||||
expect(counter).to.equal(1);
|
expect(counter).to.equal(1);
|
||||||
|
expect(isTriggeredByUser).to.be.true;
|
||||||
|
|
||||||
// Counter offset +1 for Date because parseDate created a new Date object
|
// Counter offset +1 for Date because parseDate created a new Date object
|
||||||
// when the user changes the value.
|
// when the user changes the value.
|
||||||
|
|
@ -150,17 +155,21 @@ export function runFormatMixinSuite(customConfig) {
|
||||||
expect(counter).to.equal(2 + counterOffset);
|
expect(counter).to.equal(2 + counterOffset);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fires `model-value-changed` for every modelValue change', async () => {
|
it('fires `model-value-changed` for every programmatic modelValue change', async () => {
|
||||||
const el = /** @type {FormatClass} */ (await fixture(
|
const el = /** @type {FormatClass} */ (await fixture(
|
||||||
html`<${elem}><input slot="input"></${elem}>`,
|
html`<${elem}><input slot="input"></${elem}>`,
|
||||||
));
|
));
|
||||||
let counter = 0;
|
let counter = 0;
|
||||||
el.addEventListener('model-value-changed', () => {
|
let isTriggeredByUser = false;
|
||||||
|
|
||||||
|
el.addEventListener('model-value-changed', event => {
|
||||||
counter += 1;
|
counter += 1;
|
||||||
|
isTriggeredByUser = /** @type {CustomEvent} */ (event).detail.isTriggeredByUser;
|
||||||
});
|
});
|
||||||
|
|
||||||
el.modelValue = 'one';
|
el.modelValue = 'one';
|
||||||
expect(counter).to.equal(1);
|
expect(counter).to.equal(1);
|
||||||
|
expect(isTriggeredByUser).to.be.false;
|
||||||
|
|
||||||
// no change means no event
|
// no change means no event
|
||||||
el.modelValue = 'one';
|
el.modelValue = 'one';
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,21 @@ export function runChoiceInputMixinSuite({ tagString } = {}) {
|
||||||
expect(counter).to.equal(1);
|
expect(counter).to.equal(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('adds "isTriggerByUser" flag on model-value-changed', async () => {
|
||||||
|
let isTriggeredByUser;
|
||||||
|
const el = /** @type {ChoiceInput} */ (await fixture(html`
|
||||||
|
<${tag}
|
||||||
|
@model-value-changed="${(/** @type {CustomEvent} */ event) => {
|
||||||
|
isTriggeredByUser = event.detail.isTriggeredByUser;
|
||||||
|
}}"
|
||||||
|
>
|
||||||
|
<input slot="input" />
|
||||||
|
</${tag}>
|
||||||
|
`));
|
||||||
|
el._inputNode.dispatchEvent(new CustomEvent('change', { bubbles: true }));
|
||||||
|
expect(isTriggeredByUser).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
it('can be required', async () => {
|
it('can be required', async () => {
|
||||||
const el = /** @type {ChoiceInput} */ (await fixture(html`
|
const el = /** @type {ChoiceInput} */ (await fixture(html`
|
||||||
<${tag} .choiceValue=${'foo'} .validators=${[new Required()]}></${tag}>
|
<${tag} .choiceValue=${'foo'} .validators=${[new Required()]}></${tag}>
|
||||||
|
|
|
||||||
|
|
@ -238,14 +238,19 @@ describe('FormControlMixin', () => {
|
||||||
const fieldsetEv = fieldsetSpy.firstCall.args[0];
|
const fieldsetEv = fieldsetSpy.firstCall.args[0];
|
||||||
expect(fieldsetEv.target).to.equal(fieldsetEl);
|
expect(fieldsetEv.target).to.equal(fieldsetEl);
|
||||||
expect(fieldsetEv.detail.formPath).to.eql([fieldsetEl]);
|
expect(fieldsetEv.detail.formPath).to.eql([fieldsetEl]);
|
||||||
|
expect(fieldsetEv.detail.initialize).to.be.true;
|
||||||
|
|
||||||
expect(formSpy.callCount).to.equal(1);
|
expect(formSpy.callCount).to.equal(1);
|
||||||
const formEv = formSpy.firstCall.args[0];
|
const formEv = formSpy.firstCall.args[0];
|
||||||
expect(formEv.target).to.equal(formEl);
|
expect(formEv.target).to.equal(formEl);
|
||||||
expect(formEv.detail.formPath).to.eql([formEl]);
|
expect(formEv.detail.formPath).to.eql([formEl]);
|
||||||
|
expect(formEv.detail.initialize).to.be.true;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* After initialization means: events triggered programmatically or by user actions
|
||||||
|
*/
|
||||||
describe('After initialization', () => {
|
describe('After initialization', () => {
|
||||||
it('redispatches one event from host and keeps formPath history', async () => {
|
it('redispatches one event from host and keeps formPath history', async () => {
|
||||||
const formSpy = sinon.spy();
|
const formSpy = sinon.spy();
|
||||||
|
|
@ -310,11 +315,45 @@ describe('FormControlMixin', () => {
|
||||||
const choiceGroupEv = choiceGroupSpy.firstCall.args[0];
|
const choiceGroupEv = choiceGroupSpy.firstCall.args[0];
|
||||||
expect(choiceGroupEv.target).to.equal(choiceGroupEl);
|
expect(choiceGroupEv.target).to.equal(choiceGroupEl);
|
||||||
expect(choiceGroupEv.detail.formPath).to.eql([choiceGroupEl]);
|
expect(choiceGroupEv.detail.formPath).to.eql([choiceGroupEl]);
|
||||||
|
expect(choiceGroupEv.detail.isTriggeredByUser).to.be.false;
|
||||||
|
|
||||||
expect(formSpy.callCount).to.equal(1);
|
expect(formSpy.callCount).to.equal(1);
|
||||||
const formEv = formSpy.firstCall.args[0];
|
const formEv = formSpy.firstCall.args[0];
|
||||||
expect(formEv.target).to.equal(formEl);
|
expect(formEv.target).to.equal(formEl);
|
||||||
expect(formEv.detail.formPath).to.eql([choiceGroupEl, formEl]);
|
expect(formEv.detail.formPath).to.eql([choiceGroupEl, formEl]);
|
||||||
|
expect(formEv.detail.isTriggeredByUser).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets "isTriggeredByUser" event detail when event triggered by user', async () => {
|
||||||
|
const formSpy = sinon.spy();
|
||||||
|
const fieldsetSpy = sinon.spy();
|
||||||
|
const fieldSpy = sinon.spy();
|
||||||
|
const formEl = await fixture(html`
|
||||||
|
<${groupTag} name="form">
|
||||||
|
<${groupTag} name="fieldset">
|
||||||
|
<${tag} name="field"></${tag}>
|
||||||
|
</${groupTag}>
|
||||||
|
</${groupTag}>
|
||||||
|
`);
|
||||||
|
const fieldEl = formEl.querySelector('[name=field]');
|
||||||
|
const fieldsetEl = formEl.querySelector('[name=fieldset]');
|
||||||
|
|
||||||
|
formEl.addEventListener('model-value-changed', formSpy);
|
||||||
|
fieldsetEl?.addEventListener('model-value-changed', fieldsetSpy);
|
||||||
|
fieldEl?.addEventListener('model-value-changed', fieldSpy);
|
||||||
|
|
||||||
|
fieldEl?.dispatchEvent(
|
||||||
|
new CustomEvent('model-value-changed', {
|
||||||
|
bubbles: true,
|
||||||
|
detail: { isTriggeredByUser: true },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const fieldsetEv = fieldsetSpy.firstCall.args[0];
|
||||||
|
expect(fieldsetEv.detail.isTriggeredByUser).to.be.true;
|
||||||
|
|
||||||
|
const formEv = formSpy.firstCall.args[0];
|
||||||
|
expect(formEv.detail.isTriggeredByUser).to.be.true;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,31 @@ import { DisabledHost } from '@lion/core/types/DisabledMixinTypes';
|
||||||
import { LionValidationFeedback } from '../src/validate/LionValidationFeedback';
|
import { LionValidationFeedback } from '../src/validate/LionValidationFeedback';
|
||||||
import { FormRegisteringHost } from './registration/FormRegisteringMixinTypes';
|
import { FormRegisteringHost } from './registration/FormRegisteringMixinTypes';
|
||||||
|
|
||||||
|
export type ModelValueEventDetails = {
|
||||||
|
/**
|
||||||
|
* A list that represents the path of FormControls the model-value-changed event
|
||||||
|
* 'traveled through'.
|
||||||
|
* (every FormControl stops propagation of its child and sends a new event, hereby adding
|
||||||
|
* itself to the beginning of formPath)
|
||||||
|
*/
|
||||||
|
formPath: HTMLElement[];
|
||||||
|
/**
|
||||||
|
* Sometimes it can be helpful to detect whether a value change was caused by a user or
|
||||||
|
* via a programmatical change.
|
||||||
|
* This feature acts as a normalization layer: since we use `model-value-changed` as a single
|
||||||
|
* source of truth event for all FormControls, there should be no use cases for
|
||||||
|
* (inconsistently implemented (cross browser)) events
|
||||||
|
* like 'input'/'change'/'user-input-changed' etc.)
|
||||||
|
*/
|
||||||
|
isTriggeredByUser: boolean;
|
||||||
|
/**
|
||||||
|
* Whether it is the first event sent on initialization of the form (other
|
||||||
|
* model-value-changed events are triggered imperatively or via user input (in the latter
|
||||||
|
* case `isTriggeredByUser` is true))
|
||||||
|
*/
|
||||||
|
initialize?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
declare interface HTMLElementWithValue extends HTMLElement {
|
declare interface HTMLElementWithValue extends HTMLElement {
|
||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
@ -40,12 +65,12 @@ export declare class FormControlHost {
|
||||||
* controls until they are enabled.
|
* controls until they are enabled.
|
||||||
* (From: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-readonly)
|
* (From: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-readonly)
|
||||||
*/
|
*/
|
||||||
readOnly: boolean;
|
public readOnly: boolean;
|
||||||
/**
|
/**
|
||||||
* The name the element will be registered on to the .formElements collection
|
* The name the element will be registered with to the .formElements collection
|
||||||
* of the parent.
|
* of the parent.
|
||||||
*/
|
*/
|
||||||
name: string;
|
public name: string;
|
||||||
/**
|
/**
|
||||||
* The model value is the result of the parser function(when available).
|
* The model value is the result of the parser function(when available).
|
||||||
* It should be considered as the internal value used for validation and reasoning/logic.
|
* It should be considered as the internal value used for validation and reasoning/logic.
|
||||||
|
|
@ -58,25 +83,25 @@ export declare class FormControlHost {
|
||||||
* - For a number input: a formatted String '1.234,56' will be converted to a Number:
|
* - For a number input: a formatted String '1.234,56' will be converted to a Number:
|
||||||
* 1234.56
|
* 1234.56
|
||||||
*/
|
*/
|
||||||
modelValue: unknown;
|
public modelValue: unknown;
|
||||||
/**
|
/**
|
||||||
* The label text for the input node.
|
* The label text for the input node.
|
||||||
* When no light dom defined via [slot=label], this value will be used
|
* When no light dom defined via [slot=label], this value will be used
|
||||||
*/
|
*/
|
||||||
get label(): string;
|
public get label(): string;
|
||||||
set label(arg: string);
|
public set label(arg: string);
|
||||||
__label: string | undefined;
|
__label: string | undefined;
|
||||||
/**
|
/**
|
||||||
* The helpt text for the input node.
|
* The helpt text for the input node.
|
||||||
* When no light dom defined via [slot=help-text], this value will be used
|
* When no light dom defined via [slot=help-text], this value will be used
|
||||||
*/
|
*/
|
||||||
get helpText(): string;
|
public get helpText(): string;
|
||||||
set helpText(arg: string);
|
public set helpText(arg: string);
|
||||||
__helpText: string | undefined;
|
__helpText: string | undefined;
|
||||||
set fieldName(arg: string);
|
public set fieldName(arg: string);
|
||||||
get fieldName(): string;
|
public get fieldName(): string;
|
||||||
__fieldName: string | undefined;
|
__fieldName: string | undefined;
|
||||||
get slots(): SlotsMap;
|
public get slots(): SlotsMap;
|
||||||
get _inputNode(): HTMLElementWithValue;
|
get _inputNode(): HTMLElementWithValue;
|
||||||
get _labelNode(): HTMLElement;
|
get _labelNode(): HTMLElement;
|
||||||
get _helpTextNode(): HTMLElement;
|
get _helpTextNode(): HTMLElement;
|
||||||
|
|
@ -123,7 +148,7 @@ export declare class FormControlHost {
|
||||||
__reflectAriaAttr(attrName: string, nodes: HTMLElement[], reorder: boolean | undefined): void;
|
__reflectAriaAttr(attrName: string, nodes: HTMLElement[], reorder: boolean | undefined): void;
|
||||||
_isEmpty(modelValue?: unknown): boolean;
|
_isEmpty(modelValue?: unknown): boolean;
|
||||||
_getAriaDescriptionElements(): HTMLElement[];
|
_getAriaDescriptionElements(): HTMLElement[];
|
||||||
addToAriaLabelledBy(
|
public addToAriaLabelledBy(
|
||||||
element: HTMLElement,
|
element: HTMLElement,
|
||||||
customConfig?: {
|
customConfig?: {
|
||||||
idPrefix?: string | undefined;
|
idPrefix?: string | undefined;
|
||||||
|
|
@ -131,7 +156,7 @@ export declare class FormControlHost {
|
||||||
},
|
},
|
||||||
): void;
|
): void;
|
||||||
__reorderAriaLabelledNodes: boolean | undefined;
|
__reorderAriaLabelledNodes: boolean | undefined;
|
||||||
addToAriaDescribedBy(
|
public addToAriaDescribedBy(
|
||||||
element: HTMLElement,
|
element: HTMLElement,
|
||||||
customConfig?: {
|
customConfig?: {
|
||||||
idPrefix?: string | undefined;
|
idPrefix?: string | undefined;
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ export declare class ChoiceInputHost {
|
||||||
render(): TemplateResult;
|
render(): TemplateResult;
|
||||||
|
|
||||||
_choiceGraphicTemplate(): TemplateResult;
|
_choiceGraphicTemplate(): TemplateResult;
|
||||||
|
_afterTemplate(): TemplateResult;
|
||||||
|
|
||||||
connectedCallback(): void;
|
connectedCallback(): void;
|
||||||
disconnectedCallback(): void;
|
disconnectedCallback(): void;
|
||||||
|
|
@ -47,6 +48,8 @@ export declare class ChoiceInputHost {
|
||||||
|
|
||||||
__syncCheckedToInputElement(): void;
|
__syncCheckedToInputElement(): void;
|
||||||
|
|
||||||
|
__isHandlingUserInput: boolean;
|
||||||
|
|
||||||
_proxyInputEvent(): void;
|
_proxyInputEvent(): void;
|
||||||
|
|
||||||
_onModelValueChanged(
|
_onModelValueChanged(
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,8 @@ import '@lion/listbox/lion-option.js';
|
||||||
import '@lion/select-rich/lion-select-rich.js';
|
import '@lion/select-rich/lion-select-rich.js';
|
||||||
|
|
||||||
import '@lion/fieldset/lion-fieldset.js';
|
import '@lion/fieldset/lion-fieldset.js';
|
||||||
|
import '@lion/form/lion-form.js';
|
||||||
|
import '@lion/form-core/lion-field.js';
|
||||||
|
|
||||||
const featureName = 'model value';
|
const featureName = 'model value';
|
||||||
|
|
||||||
|
|
@ -278,3 +280,178 @@ describe('lion-fieldset', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('detail.isTriggeredByUser', () => {
|
||||||
|
const allFormControls = [
|
||||||
|
// 1) Fields
|
||||||
|
'field',
|
||||||
|
// 1a) Input Fields
|
||||||
|
'input',
|
||||||
|
'input-amount',
|
||||||
|
'input-date',
|
||||||
|
'input-datepicker',
|
||||||
|
'input-email',
|
||||||
|
'input-iban',
|
||||||
|
'input-range',
|
||||||
|
'textarea',
|
||||||
|
// 1b) Choice Fields
|
||||||
|
'option',
|
||||||
|
'checkbox',
|
||||||
|
'radio',
|
||||||
|
// 1c) Choice Group Fields
|
||||||
|
'select',
|
||||||
|
'listbox',
|
||||||
|
'select-rich',
|
||||||
|
'combobox',
|
||||||
|
// 2) FormGroups
|
||||||
|
// 2a) Choice FormGroups
|
||||||
|
'checkbox-group',
|
||||||
|
'radio-group',
|
||||||
|
// 2v) Fieldset
|
||||||
|
'fieldset',
|
||||||
|
// 2c) Form
|
||||||
|
'form',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* "isTriggeredByUser" for different types of fields:
|
||||||
|
*
|
||||||
|
* RegularField:
|
||||||
|
* - true: when change/input (c.q. user-input-changed) fired
|
||||||
|
* - false: when .modelValue set programmatically
|
||||||
|
*
|
||||||
|
* ChoiceField:
|
||||||
|
* - true: when 'change' event fired
|
||||||
|
* - false: when .modelValue (or checked) set programmatically
|
||||||
|
*
|
||||||
|
* OptionChoiceField:
|
||||||
|
* - true: when 'click' event fired
|
||||||
|
* - false: when .modelValue (or checked) set programmatically
|
||||||
|
*
|
||||||
|
* ChoiceGroupField (listbox, select-rich, combobox, radio-group, checkbox-group):
|
||||||
|
* - true: when child formElement condition for ChoiceField(Option) is met
|
||||||
|
* - false: when child formElement condition for ChoiceField(Option) is not met
|
||||||
|
*
|
||||||
|
* FormOrFieldset (fieldset, form):
|
||||||
|
* - true: when child formElement condition for RegularField is met
|
||||||
|
* - false: when child formElement condition for RegularField is not met
|
||||||
|
*/
|
||||||
|
|
||||||
|
const featureDetectChoiceField = el => 'checked' in el && 'choiceValue' in el;
|
||||||
|
const featureDetectOptionChoiceField = el => 'active' in el;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {FormControl} el
|
||||||
|
* @returns {'RegularField'|'ChoiceField'|'OptionChoiceField'|'ChoiceGroupField'|'FormOrFieldset'}
|
||||||
|
*/
|
||||||
|
function detectType(el) {
|
||||||
|
if (el._repropagationRole === 'child') {
|
||||||
|
if (featureDetectChoiceField(el)) {
|
||||||
|
return featureDetectOptionChoiceField(el) ? 'OptionChoiceField' : 'ChoiceField';
|
||||||
|
}
|
||||||
|
return 'RegularField';
|
||||||
|
}
|
||||||
|
return el._repropagationRole === 'choice-group' ? 'ChoiceGroupField' : 'FormOrFieldset';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {FormControl} el
|
||||||
|
* @param {string} newViewValue
|
||||||
|
* @returns {'RegularField'|'ChoiceField'|'OptionChoiceField'|'ChoiceGroupField'|'FormOrFieldset'}
|
||||||
|
*/
|
||||||
|
function mimicUserInput(el, newViewValue) {
|
||||||
|
const type = detectType(el);
|
||||||
|
let userInputEv;
|
||||||
|
if (type === 'RegularField') {
|
||||||
|
userInputEv = el._inputNode.tagName === 'SELECT' ? 'change' : 'input';
|
||||||
|
el.value = newViewValue; // eslint-disable-line no-param-reassign
|
||||||
|
el._inputNode.dispatchEvent(new Event(userInputEv, { bubbles: true }));
|
||||||
|
} else if (type === 'ChoiceField') {
|
||||||
|
el._inputNode.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
|
} else if (type === 'OptionChoiceField') {
|
||||||
|
el.dispatchEvent(new Event('click', { bubbles: true }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allFormControls.forEach(controlName => {
|
||||||
|
it(`lion-${controlName} adds "detail.isTriggeredByUser" to model-value-changed event`, async () => {
|
||||||
|
const spy = sinon.spy();
|
||||||
|
|
||||||
|
const tagname = `lion-${controlName}`;
|
||||||
|
const tag = unsafeStatic(tagname);
|
||||||
|
let childrenEl;
|
||||||
|
if (controlName === 'select') {
|
||||||
|
childrenEl = await fixture(
|
||||||
|
html`<select slot="input">
|
||||||
|
<option value="x"></option>
|
||||||
|
</select>`,
|
||||||
|
);
|
||||||
|
} else if (controlName === 'form') {
|
||||||
|
childrenEl = await fixture(html`<form></form>`);
|
||||||
|
} else if (controlName === 'field') {
|
||||||
|
childrenEl = await fixture(html`<input slot="input" />`);
|
||||||
|
}
|
||||||
|
const el = await fixture(html`<${tag}>${childrenEl}</${tag}>`);
|
||||||
|
await el.registrationComplete;
|
||||||
|
el.addEventListener('model-value-changed', spy);
|
||||||
|
|
||||||
|
function expectCorrectEventMetaRegularField(formControl) {
|
||||||
|
mimicUserInput(formControl, 'userValue', 'RegularField');
|
||||||
|
expect(spy.firstCall.args[0].detail.isTriggeredByUser).to.be.true;
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
formControl.modelValue = 'programmaticValue';
|
||||||
|
expect(spy.secondCall.args[0].detail.isTriggeredByUser).to.be.false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetChoiceFieldToForceRepropagation(formControl) {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
formControl.checked = false;
|
||||||
|
spy.resetHistory();
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectCorrectEventMetaChoiceField(formControl) {
|
||||||
|
resetChoiceFieldToForceRepropagation(formControl);
|
||||||
|
mimicUserInput(formControl, 'userValue', 'ChoiceField');
|
||||||
|
expect(spy.firstCall.args[0].detail.isTriggeredByUser).to.be.true;
|
||||||
|
|
||||||
|
resetChoiceFieldToForceRepropagation(formControl);
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
formControl.checked = true;
|
||||||
|
expect(spy.firstCall.args[0].detail.isTriggeredByUser).to.be.false;
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
formControl.modelValue = { value: 'programmaticValue', checked: false };
|
||||||
|
expect(spy.secondCall.args[0].detail.isTriggeredByUser).to.be.false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Derive the type of field we're dealing with
|
||||||
|
const type = detectType(el);
|
||||||
|
if (type === 'RegularField') {
|
||||||
|
expectCorrectEventMetaRegularField(el);
|
||||||
|
} else if (type === 'ChoiceField' || type === 'OptionChoiceField') {
|
||||||
|
expectCorrectEventMetaChoiceField(el);
|
||||||
|
} else if (type === 'ChoiceGroupField') {
|
||||||
|
let childName = 'option';
|
||||||
|
if (controlName.endsWith('-group')) {
|
||||||
|
[childName] = controlName.split('-group');
|
||||||
|
}
|
||||||
|
const childTagName = `lion-${childName}`;
|
||||||
|
const childTag = unsafeStatic(childTagName);
|
||||||
|
const childrenEls = await fixture(
|
||||||
|
html`<div><${childTag}></${childTag}><${childTag}></${childTag}></div>`,
|
||||||
|
);
|
||||||
|
el.appendChild(childrenEls);
|
||||||
|
await el.registrationComplete;
|
||||||
|
expectCorrectEventMetaChoiceField(el.formElements[0]);
|
||||||
|
} else if (type === 'FormOrFieldset') {
|
||||||
|
const childrenEls = await fixture(
|
||||||
|
html`<div><lion-input name="one"></lion-input><lion-input name="two"></lion-input></div>`,
|
||||||
|
);
|
||||||
|
el.appendChild(childrenEls);
|
||||||
|
await el.registrationComplete;
|
||||||
|
await el.updateComplete;
|
||||||
|
expectCorrectEventMetaRegularField(el.formElements[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -123,6 +123,7 @@ export class LionOption extends DisabledMixin(ChoiceInputMixin(FormRegisteringMi
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const parentForm = /** @type {unknown} */ (this.__parentFormGroup);
|
const parentForm = /** @type {unknown} */ (this.__parentFormGroup);
|
||||||
|
this.__isHandlingUserInput = true;
|
||||||
if (parentForm && /** @type {ChoiceGroupHost} */ (parentForm).multipleChoice) {
|
if (parentForm && /** @type {ChoiceGroupHost} */ (parentForm).multipleChoice) {
|
||||||
this.checked = !this.checked;
|
this.checked = !this.checked;
|
||||||
this.active = !this.active;
|
this.active = !this.active;
|
||||||
|
|
@ -130,5 +131,6 @@ export class LionOption extends DisabledMixin(ChoiceInputMixin(FormRegisteringMi
|
||||||
this.checked = true;
|
this.checked = true;
|
||||||
this.active = true;
|
this.active = true;
|
||||||
}
|
}
|
||||||
|
this.__isHandlingUserInput = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import { LionOptions } from './LionOptions.js';
|
||||||
* @typedef {import('../types/ListboxMixinTypes').ListboxMixin} ListboxMixin
|
* @typedef {import('../types/ListboxMixinTypes').ListboxMixin} ListboxMixin
|
||||||
* @typedef {import('../types/ListboxMixinTypes').ListboxHost} ListboxHost
|
* @typedef {import('../types/ListboxMixinTypes').ListboxHost} ListboxHost
|
||||||
* @typedef {import('@lion/form-core/types/registration/FormRegistrarPortalMixinTypes').FormRegistrarPortalHost} FormRegistrarPortalHost
|
* @typedef {import('@lion/form-core/types/registration/FormRegistrarPortalMixinTypes').FormRegistrarPortalHost} FormRegistrarPortalHost
|
||||||
|
* @typedef {import('@lion/form-core/types/FormControlMixinTypes.js').ModelValueEventDetails} ModelValueEventDetails
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function uuid() {
|
function uuid() {
|
||||||
|
|
@ -699,7 +700,13 @@ const ListboxMixinImplementation = superclass =>
|
||||||
// only send model-value-changed if the event is caused by one of its children
|
// only send model-value-changed if the event is caused by one of its children
|
||||||
if (ev.detail && ev.detail.formPath) {
|
if (ev.detail && ev.detail.formPath) {
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent('model-value-changed', { detail: { element: ev.target } }),
|
new CustomEvent('model-value-changed', {
|
||||||
|
detail: /** @type {ModelValueEventDetails} */ ({
|
||||||
|
formPath: ev.detail.formPath,
|
||||||
|
isTriggeredByUser: ev.detail.isTriggeredByUser,
|
||||||
|
element: ev.target,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.__oldModelValue = this.modelValue;
|
this.__oldModelValue = this.modelValue;
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,38 @@ describe('lion-option', () => {
|
||||||
expect(el.modelValue).to.deep.equal({ value: 10, checked: false });
|
expect(el.modelValue).to.deep.equal({ value: 10, checked: false });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('fires model-value-changed on click', async () => {
|
||||||
|
let isTriggeredByUser;
|
||||||
|
const el = /** @type {LionOption} */ (await fixture(html`
|
||||||
|
<lion-option
|
||||||
|
@model-value-changed="${(/** @type {CustomEvent} */ event) => {
|
||||||
|
isTriggeredByUser = event.detail.isTriggeredByUser;
|
||||||
|
}}"
|
||||||
|
>
|
||||||
|
</lion-option>
|
||||||
|
`));
|
||||||
|
el.dispatchEvent(new CustomEvent('click', { bubbles: true }));
|
||||||
|
expect(isTriggeredByUser).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fires model-value-changed on programmatic "checked" change', async () => {
|
||||||
|
let count = 0;
|
||||||
|
let isTriggeredByUser;
|
||||||
|
|
||||||
|
const el = /** @type {LionOption} */ (await fixture(html`
|
||||||
|
<lion-option
|
||||||
|
@model-value-changed="${(/** @type {CustomEvent} */ event) => {
|
||||||
|
count += 1;
|
||||||
|
isTriggeredByUser = event.detail.isTriggeredByUser;
|
||||||
|
}}"
|
||||||
|
>
|
||||||
|
</lion-option>
|
||||||
|
`));
|
||||||
|
el.checked = true;
|
||||||
|
expect(count).to.equal(1);
|
||||||
|
expect(isTriggeredByUser).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
it('can be checked', async () => {
|
it('can be checked', async () => {
|
||||||
const el = /** @type {LionOption} */ (await fixture(
|
const el = /** @type {LionOption} */ (await fixture(
|
||||||
html`<lion-option .choiceValue=${10} checked></lion-option>`,
|
html`<lion-option .choiceValue=${10} checked></lion-option>`,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { localize } from '../localize.js';
|
import { localize } from '../localize.js';
|
||||||
import { getLocale } from './getLocale.js';
|
import { getLocale } from '../utils/getLocale.js';
|
||||||
import { normalizeIntlDate } from './normalizeIntlDate.js';
|
import { normalizeIntlDate } from './utils/normalizeIntlDate.js';
|
||||||
|
|
||||||
/** @typedef {import('../../types/LocalizeMixinTypes').DatePostProcessor} DatePostProcessor */
|
/** @typedef {import('../../types/LocalizeMixinTypes').DatePostProcessor} DatePostProcessor */
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { sanitizedDateTimeFormat } from './sanitizedDateTimeFormat.js';
|
import { sanitizedDateTimeFormat } from './utils/sanitizedDateTimeFormat.js';
|
||||||
import { splitDate } from './splitDate.js';
|
import { splitDate } from './utils/splitDate.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* To compute the localized date format
|
* To compute the localized date format
|
||||||
|
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
import { localize } from '../localize.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the locale to use
|
|
||||||
*
|
|
||||||
* @param {string|undefined} locale Locale to override browser locale
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
export function getLocale(locale) {
|
|
||||||
if (locale) {
|
|
||||||
return locale;
|
|
||||||
}
|
|
||||||
if (localize && localize.locale) {
|
|
||||||
return localize.locale;
|
|
||||||
}
|
|
||||||
return 'en-GB';
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { normalizeIntlDate } from './normalizeIntlDate.js';
|
import { normalizeIntlDate } from './utils/normalizeIntlDate.js';
|
||||||
|
import { forceShortMonthNamesForEnGb } from './utils/forceShortMonthNamesForEnGb.js';
|
||||||
|
|
||||||
/** @type {Object.<string, Object.<string,string[]>>} */
|
/** @type {Object.<string, Object.<string,string[]>>} */
|
||||||
const monthsLocaleCache = {};
|
const monthsLocaleCache = {};
|
||||||
|
|
@ -26,7 +27,9 @@ export function getMonthNames({ locale, style = 'long' } = {}) {
|
||||||
const normalizedDate = normalizeIntlDate(formattedDate);
|
const normalizedDate = normalizeIntlDate(formattedDate);
|
||||||
months.push(normalizedDate);
|
months.push(normalizedDate);
|
||||||
}
|
}
|
||||||
|
if (locale === 'en-GB' && style === 'short') {
|
||||||
|
months = forceShortMonthNamesForEnGb(months);
|
||||||
|
}
|
||||||
monthsLocaleCache[locale] = monthsLocaleCache[locale] || {};
|
monthsLocaleCache[locale] = monthsLocaleCache[locale] || {};
|
||||||
monthsLocaleCache[locale][style] = months;
|
monthsLocaleCache[locale][style] = months;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { normalizeIntlDate } from './normalizeIntlDate.js';
|
import { normalizeIntlDate } from './utils/normalizeIntlDate.js';
|
||||||
|
|
||||||
/** @type {Object.<string, Object.<string,string[]>>} */
|
/** @type {Object.<string, Object.<string,string[]>>} */
|
||||||
const weekdayNamesCache = {};
|
const weekdayNamesCache = {};
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { localize } from '../localize.js';
|
import { localize } from '../localize.js';
|
||||||
import { getDateFormatBasedOnLocale } from './getDateFormatBasedOnLocale.js';
|
import { getDateFormatBasedOnLocale } from './getDateFormatBasedOnLocale.js';
|
||||||
import { addLeadingZero } from './addLeadingZero.js';
|
import { addLeadingZero } from './utils/addLeadingZero.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {function} fn
|
* @param {function} fn
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
/**
|
||||||
|
* @param {string[]} months
|
||||||
|
*/
|
||||||
|
export function forceShortMonthNamesForEnGb(months) {
|
||||||
|
if (months[8] === 'Sept') {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
months[8] = 'Sep';
|
||||||
|
}
|
||||||
|
return months;
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { formatDate } from './formatDate.js';
|
import { formatDate } from '../formatDate.js';
|
||||||
import { clean } from './clean.js';
|
import { clean } from './clean.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { localize } from '../localize.js';
|
import { localize } from '../localize.js';
|
||||||
import { getLocale } from './getLocale.js';
|
import { getLocale } from '../utils/getLocale.js';
|
||||||
import { formatNumberToParts } from './formatNumberToParts.js';
|
import { formatNumberToParts } from './formatNumberToParts.js';
|
||||||
|
|
||||||
/** @typedef {import('../../types/LocalizeMixinTypes').NumberPostProcessor} NumberPostProcessor */
|
/** @typedef {import('../../types/LocalizeMixinTypes').NumberPostProcessor} NumberPostProcessor */
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,30 @@
|
||||||
import { emptyStringWhenNumberNan } from './emptyStringWhenNumberNan.js';
|
import { emptyStringWhenNumberNan } from './utils/emptyStringWhenNumberNan.js';
|
||||||
import { getDecimalSeparator } from './getDecimalSeparator.js';
|
import { getDecimalSeparator } from './getDecimalSeparator.js';
|
||||||
import { getGroupSeparator } from './getGroupSeparator.js';
|
import { getGroupSeparator } from './getGroupSeparator.js';
|
||||||
import { getLocale } from './getLocale.js';
|
import { getLocale } from '../utils/getLocale.js';
|
||||||
import { normalizeIntl } from './normalizeIntl.js';
|
import { normalizeIntl } from './utils/normalize-format-number-to-parts/normalizeIntl.js';
|
||||||
import { normalSpaces } from './normalSpaces.js';
|
import { normalSpaces } from './utils/normalSpaces.js';
|
||||||
import { roundNumber } from './roundNumber.js';
|
|
||||||
|
/**
|
||||||
|
* Round the number based on the options
|
||||||
|
*
|
||||||
|
* @param {number} number
|
||||||
|
* @param {string} roundMode
|
||||||
|
* @throws {Error} roundMode can only be round|floor|ceiling
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
export function roundNumber(number, roundMode) {
|
||||||
|
switch (roundMode) {
|
||||||
|
case 'floor':
|
||||||
|
return Math.floor(number);
|
||||||
|
case 'ceiling':
|
||||||
|
return Math.ceil(number);
|
||||||
|
case 'round':
|
||||||
|
return Math.round(number);
|
||||||
|
default:
|
||||||
|
throw new Error('roundMode can only be round|floor|ceiling');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Splits a number up in parts for integer, fraction, group, literal, decimal and currency.
|
* Splits a number up in parts for integer, fraction, group, literal, decimal and currency.
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
import { formatNumberToParts } from './formatNumberToParts.js';
|
import { formatNumberToParts } from './formatNumberToParts.js';
|
||||||
|
import { localize } from '../localize.js';
|
||||||
|
import { forceCurrencyNameForPHPEnGB } from './utils/normalize-get-currency-name/forceCurrencyNameForPHPEnGB.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Based on number, returns currency name like 'US dollar'
|
* Based on number, returns currency name like 'US dollar'
|
||||||
|
|
@ -15,9 +17,13 @@ export function getCurrencyName(currencyIso, options) {
|
||||||
currency: currencyIso,
|
currency: currencyIso,
|
||||||
currencyDisplay: 'name',
|
currencyDisplay: 'name',
|
||||||
}));
|
}));
|
||||||
const currencyName = parts
|
let currencyName = parts
|
||||||
.filter(p => p.type === 'currency')
|
.filter(p => p.type === 'currency')
|
||||||
.map(o => o.value)
|
.map(o => o.value)
|
||||||
.join(' ');
|
.join(' ');
|
||||||
|
const locale = options?.locale || localize.locale;
|
||||||
|
if (currencyIso === 'PHP' && locale === 'en-GB') {
|
||||||
|
currencyName = forceCurrencyNameForPHPEnGB(currencyName);
|
||||||
|
}
|
||||||
return currencyName;
|
return currencyName;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { getLocale } from './getLocale.js';
|
import { getLocale } from '../utils/getLocale.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* To get the decimal separator
|
* To get the decimal separator
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { getLocale } from './getLocale.js';
|
import { getLocale } from '../utils/getLocale.js';
|
||||||
import { normalSpaces } from './normalSpaces.js';
|
import { normalSpaces } from './utils/normalSpaces.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the group separator
|
* Gets the group separator
|
||||||
|
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
/**
|
|
||||||
* Round the number based on the options
|
|
||||||
*
|
|
||||||
* @param {number} number
|
|
||||||
* @param {string} roundMode
|
|
||||||
* @throws {Error} roundMode can only be round|floor|ceiling
|
|
||||||
* @returns {number}
|
|
||||||
*/
|
|
||||||
export function roundNumber(number, roundMode) {
|
|
||||||
switch (roundMode) {
|
|
||||||
case 'floor':
|
|
||||||
return Math.floor(number);
|
|
||||||
case 'ceiling':
|
|
||||||
return Math.ceil(number);
|
|
||||||
case 'round':
|
|
||||||
return Math.round(number);
|
|
||||||
default:
|
|
||||||
throw new Error('roundMode can only be round|floor|ceiling');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { localize } from '../localize.js';
|
import { localize } from '../../localize.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When number is NaN we should return an empty string or returnIfNaN param
|
* When number is NaN we should return an empty string or returnIfNaN param
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* Add separators when they are not present
|
* Add separators when they are not present
|
||||||
*
|
*
|
||||||
* @typedef {import('../../types/LocalizeMixinTypes').FormatNumberPart} FormatNumberPart
|
* @typedef {import('../../../../types/LocalizeMixinTypes').FormatNumberPart} FormatNumberPart
|
||||||
* @param {FormatNumberPart[]} formattedParts
|
* @param {FormatNumberPart[]} formattedParts
|
||||||
* @param {string} groupSeparator
|
* @param {string} groupSeparator
|
||||||
* @returns {FormatNumberPart[]}
|
* @returns {FormatNumberPart[]}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* For Dutch and Belgian amounts the currency should be at the end of the string
|
* For Dutch and Belgian amounts the currency should be at the end of the string
|
||||||
*
|
*
|
||||||
* @typedef {import('../../types/LocalizeMixinTypes').FormatNumberPart} FormatNumberPart
|
* @typedef {import('../../../../types/LocalizeMixinTypes').FormatNumberPart} FormatNumberPart
|
||||||
* @param {FormatNumberPart[]} formattedParts
|
* @param {FormatNumberPart[]} formattedParts
|
||||||
* @returns {FormatNumberPart[]}
|
* @returns {FormatNumberPart[]}
|
||||||
*/
|
*/
|
||||||
|
|
@ -8,9 +8,9 @@ const CURRENCY_CODE_SYMBOL_MAP = {
|
||||||
/**
|
/**
|
||||||
* Change the symbols for locale 'en-AU', due to bug in Chrome
|
* Change the symbols for locale 'en-AU', due to bug in Chrome
|
||||||
*
|
*
|
||||||
* @typedef {import('../../types/LocalizeMixinTypes').FormatNumberPart} FormatNumberPart
|
* @typedef {import('../../../../types/LocalizeMixinTypes').FormatNumberPart} FormatNumberPart
|
||||||
* @param {FormatNumberPart[]} formattedParts
|
* @param {FormatNumberPart[]} formattedParts
|
||||||
* @param {import('../../types/LocalizeMixinTypes').FormatNumberOptions} [options]
|
* @param {import('../../../../types/LocalizeMixinTypes').FormatNumberOptions} [options]
|
||||||
* @returns {FormatNumberPart[]}
|
* @returns {FormatNumberPart[]}
|
||||||
*/
|
*/
|
||||||
export function forceENAUSymbols(formattedParts, { currency, currencyDisplay } = {}) {
|
export function forceENAUSymbols(formattedParts, { currency, currencyDisplay } = {}) {
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { normalSpaces } from './normalSpaces.js';
|
import { normalSpaces } from '../normalSpaces.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parts with forced "normal" spaces
|
* Parts with forced "normal" spaces
|
||||||
*
|
*
|
||||||
* @typedef {import('../../types/LocalizeMixinTypes').FormatNumberPart} FormatNumberPart
|
* @typedef {import('../../../../types/LocalizeMixinTypes').FormatNumberPart} FormatNumberPart
|
||||||
* @param {FormatNumberPart[]} formattedParts
|
* @param {FormatNumberPart[]} formattedParts
|
||||||
* @returns {FormatNumberPart[]}
|
* @returns {FormatNumberPart[]}
|
||||||
*/
|
*/
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
/**
|
/**
|
||||||
* When in some locales there is no space between currency and amount it is added
|
* When in some locales there is no space between currency and amount it is added
|
||||||
*
|
*
|
||||||
* @typedef {import('../../types/LocalizeMixinTypes').FormatNumberPart} FormatNumberPart
|
* @typedef {import('../../../../types/LocalizeMixinTypes').FormatNumberPart} FormatNumberPart
|
||||||
* @param {FormatNumberPart[]} formattedParts
|
* @param {FormatNumberPart[]} formattedParts
|
||||||
* @param {import('../../types/LocalizeMixinTypes').FormatNumberOptions} [options]
|
* @param {import('../../../../types/LocalizeMixinTypes').FormatNumberOptions} [options]
|
||||||
* @returns {FormatNumberPart[]}
|
* @returns {FormatNumberPart[]}
|
||||||
*/
|
*/
|
||||||
export function forceSpaceBetweenCurrencyCodeAndNumber(
|
export function forceSpaceBetweenCurrencyCodeAndNumber(
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
* @desc Intl uses 0 as group separator for bg-BG locale.
|
* @desc Intl uses 0 as group separator for bg-BG locale.
|
||||||
* This should be a ' '
|
* This should be a ' '
|
||||||
*
|
*
|
||||||
* @typedef {import('../../types/LocalizeMixinTypes').FormatNumberPart} FormatNumberPart
|
* @typedef {import('../../../../types/LocalizeMixinTypes').FormatNumberPart} FormatNumberPart
|
||||||
* @param {FormatNumberPart[]} formattedParts
|
* @param {FormatNumberPart[]} formattedParts
|
||||||
* @returns {FormatNumberPart[]} corrected formatted parts
|
* @returns {FormatNumberPart[]} corrected formatted parts
|
||||||
*/
|
*/
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* @typedef {import('../../types/LocalizeMixinTypes').FormatNumberPart} FormatNumberPart
|
* @typedef {import('../../../../types/LocalizeMixinTypes').FormatNumberPart} FormatNumberPart
|
||||||
* @param {FormatNumberPart[]} formattedParts
|
* @param {FormatNumberPart[]} formattedParts
|
||||||
* @param {import('../../types/LocalizeMixinTypes').FormatNumberOptions} [options]
|
* @param {import('../../../../types/LocalizeMixinTypes').FormatNumberOptions} [options]
|
||||||
* @returns {FormatNumberPart[]}
|
* @returns {FormatNumberPart[]}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* @typedef {import('../../types/LocalizeMixinTypes').FormatNumberPart} FormatNumberPart
|
* @typedef {import('../../../../types/LocalizeMixinTypes').FormatNumberPart} FormatNumberPart
|
||||||
* @param {FormatNumberPart[]} formattedParts
|
* @param {FormatNumberPart[]} formattedParts
|
||||||
* @param {import('../../types/LocalizeMixinTypes').FormatNumberOptions} [options]
|
* @param {import('../../../../types/LocalizeMixinTypes').FormatNumberOptions} [options]
|
||||||
* @returns {FormatNumberPart[]}
|
* @returns {FormatNumberPart[]}
|
||||||
*/
|
*/
|
||||||
export function forceYenSymbol(formattedParts, { currency, currencyDisplay } = {}) {
|
export function forceYenSymbol(formattedParts, { currency, currencyDisplay } = {}) {
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { getGroupSeparator } from './getGroupSeparator.js';
|
import { getGroupSeparator } from '../../getGroupSeparator.js';
|
||||||
import { forceAddGroupSeparators } from './forceAddGroupSeparators.js';
|
import { forceAddGroupSeparators } from './forceAddGroupSeparators.js';
|
||||||
import { forceCurrencyToEnd } from './forceCurrencyToEnd.js';
|
import { forceCurrencyToEnd } from './forceCurrencyToEnd.js';
|
||||||
import { forceNormalSpaces } from './forceNormalSpaces.js';
|
import { forceNormalSpaces } from './forceNormalSpaces.js';
|
||||||
|
|
@ -9,11 +9,11 @@ import { forceTryCurrencyCode } from './forceTryCurrencyCode.js';
|
||||||
import { forceENAUSymbols } from './forceENAUSymbols.js';
|
import { forceENAUSymbols } from './forceENAUSymbols.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function with all fixes on localize
|
* Normalizes function "formatNumberToParts"
|
||||||
*
|
*
|
||||||
* @typedef {import('../../types/LocalizeMixinTypes').FormatNumberPart} FormatNumberPart
|
* @typedef {import('../../../../types/LocalizeMixinTypes').FormatNumberPart} FormatNumberPart
|
||||||
* @param {FormatNumberPart[]} formattedParts
|
* @param {FormatNumberPart[]} formattedParts
|
||||||
* @param {import('../../types/LocalizeMixinTypes').FormatNumberOptions} options
|
* @param {import('../../../../types/LocalizeMixinTypes').FormatNumberOptions} options
|
||||||
* @param {string} _locale
|
* @param {string} _locale
|
||||||
* @returns {FormatNumberPart[]}
|
* @returns {FormatNumberPart[]}
|
||||||
*/
|
*/
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
/**
|
||||||
|
* @param {string} currencyName
|
||||||
|
*/
|
||||||
|
export function forceCurrencyNameForPHPEnGB(currencyName) {
|
||||||
|
if (currencyName === 'Philippine pesos') {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
currencyName = 'Philippine pisos';
|
||||||
|
}
|
||||||
|
return currencyName;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue