Merge pull request #357 from ing-bank/fix/validationFieldset
fix(fieldset): manage when to show error messages
This commit is contained in:
commit
b95791fc69
14 changed files with 618 additions and 335 deletions
|
|
@ -1,73 +1,6 @@
|
|||
import { LionFieldset } from '@lion/fieldset';
|
||||
|
||||
export class LionCheckboxGroup extends LionFieldset {
|
||||
constructor() {
|
||||
super();
|
||||
this._checkboxGroupTouched = false;
|
||||
this._setTouchedAndPrefilled = this._setTouchedAndPrefilled.bind(this);
|
||||
this._checkForOutsideClick = this._checkForOutsideClick.bind(this);
|
||||
this._checkForChildrenClick = this._checkForChildrenClick.bind(this);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
// We listen for focusin(instead of foxus), because it bubbles and gives the right event order
|
||||
window.addEventListener('focusin', this._setTouchedAndPrefilled);
|
||||
|
||||
document.addEventListener('click', this._checkForOutsideClick);
|
||||
this.addEventListener('click', this._checkForChildrenClick);
|
||||
|
||||
// checks for any of the children to be prefilled
|
||||
this._checkboxGroupPrefilled = super.prefilled;
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
window.removeEventListener('focusin', this._setTouchedAndPrefilled);
|
||||
document.removeEventListener('click', this._checkForOutsideClick);
|
||||
this.removeEventListener('click', this._checkForChildrenClick);
|
||||
}
|
||||
|
||||
get touched() {
|
||||
return this._checkboxGroupTouched;
|
||||
}
|
||||
|
||||
/**
|
||||
* Leave event will be fired when previous document.activeElement
|
||||
* is inside group and current document.activeElement is outside.
|
||||
*/
|
||||
_setTouchedAndPrefilled() {
|
||||
const groupHasFocus = this.focused;
|
||||
if (this.__groupHadFocus && !groupHasFocus) {
|
||||
this._checkboxGroupTouched = true;
|
||||
this._checkboxGroupPrefilled = super.prefilled; // right time to reconsider prefilled
|
||||
this.__checkboxGroupPrefilledHasBeenSet = true;
|
||||
}
|
||||
this.__groupHadFocus = groupHasFocus;
|
||||
}
|
||||
|
||||
_checkForOutsideClick(event) {
|
||||
const outsideGroupClicked = !this.contains(event.target);
|
||||
if (outsideGroupClicked) {
|
||||
this._setTouchedAndPrefilled();
|
||||
}
|
||||
}
|
||||
|
||||
// Whenever a user clicks a checkbox, error messages should become visible
|
||||
_checkForChildrenClick(event) {
|
||||
const childClicked = this._childArray.some(c => c === event.target || c.contains(event.target));
|
||||
if (childClicked) {
|
||||
this._checkboxGroupTouched = true;
|
||||
}
|
||||
}
|
||||
|
||||
get _childArray() {
|
||||
// We assume here that the fieldset has one set of checkboxes/radios that are grouped via attr
|
||||
// name="groupName[]"
|
||||
const arrayKey = Object.keys(this.formElements).filter(k => k.substr(-2) === '[]')[0];
|
||||
return this.formElements[arrayKey] || [];
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
__isRequired(modelValues) {
|
||||
const keys = Object.keys(modelValues);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { storiesOf, html } from '@open-wc/demoing-storybook';
|
|||
import '../lion-checkbox-group.js';
|
||||
import '@lion/checkbox/lion-checkbox.js';
|
||||
import '@lion/form/lion-form.js';
|
||||
import { localize } from '@lion/localize';
|
||||
|
||||
storiesOf('Forms|Checkbox Group', module)
|
||||
.add(
|
||||
|
|
@ -123,4 +124,58 @@ storiesOf('Forms|Checkbox Group', module)
|
|||
</form></lion-form
|
||||
>
|
||||
`;
|
||||
})
|
||||
.add('Validation 2 checked', () => {
|
||||
const hasMinTwoChecked = value => {
|
||||
const selectedValues = value['scientists[]'].filter(v => v.checked === true);
|
||||
return {
|
||||
hasMinTwoChecked: selectedValues.length >= 2,
|
||||
};
|
||||
};
|
||||
localize.locale = 'en-GB';
|
||||
try {
|
||||
localize.addData('en-GB', 'lion-validate+hasMinTwoChecked', {
|
||||
error: {
|
||||
hasMinTwoChecked: 'You need to select at least 2 values',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
// expected as it's a demo
|
||||
}
|
||||
|
||||
const submit = () => {
|
||||
const form = document.querySelector('#form');
|
||||
if (form.errorState === false) {
|
||||
console.log(form.serializeGroup());
|
||||
}
|
||||
};
|
||||
return html`
|
||||
<lion-form id="form" @submit="${submit}"
|
||||
><form>
|
||||
<lion-checkbox-group
|
||||
name="scientistsGroup"
|
||||
label="Who are your favorite scientists?"
|
||||
help-text="You should have at least 2 of those"
|
||||
.errorValidators=${[[hasMinTwoChecked]]}
|
||||
>
|
||||
<lion-checkbox
|
||||
name="scientists[]"
|
||||
label="Archimedes"
|
||||
.choiceValue=${'Archimedes'}
|
||||
></lion-checkbox>
|
||||
<lion-checkbox
|
||||
name="scientists[]"
|
||||
label="Francis Bacon"
|
||||
.choiceValue=${'Francis Bacon'}
|
||||
></lion-checkbox>
|
||||
<lion-checkbox
|
||||
name="scientists[]"
|
||||
label="Marie Curie"
|
||||
.choiceValue=${'Marie Curie'}
|
||||
></lion-checkbox>
|
||||
</lion-checkbox-group>
|
||||
<button type="submit">Submit</button>
|
||||
</form></lion-form
|
||||
>
|
||||
`;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { expect, html, fixture, triggerFocusFor, nextFrame } from '@open-wc/testing';
|
||||
import sinon from 'sinon';
|
||||
import { expect, html, fixture, nextFrame } from '@open-wc/testing';
|
||||
|
||||
import { localizeTearDown } from '@lion/localize/test-helpers.js';
|
||||
|
||||
|
|
@ -11,87 +10,6 @@ beforeEach(() => {
|
|||
});
|
||||
|
||||
describe('<lion-checkbox-group>', () => {
|
||||
// Note: these requirements seem to hold for checkbox-group only, not for radio-group (since we
|
||||
// cannot tab through all input elements).
|
||||
|
||||
it(`becomes "touched" once the last element of a group becomes blurred by keyboard
|
||||
interaction (e.g. tabbing through the checkbox-group)`, async () => {
|
||||
const el = await fixture(`
|
||||
<lion-checkbox-group>
|
||||
<label slot="label">My group</label>
|
||||
<lion-checkbox name="myGroup[]" label="Option 1" value="1"></lion-checkbox>
|
||||
<lion-checkbox name="myGroup[]" label="Option 2" value="2"></lion-checkbox>
|
||||
</lion-checkbox-group>
|
||||
`);
|
||||
await nextFrame();
|
||||
|
||||
const button = await fixture(`<button>Blur</button>`);
|
||||
|
||||
el.children[1].focus();
|
||||
expect(el.touched).to.equal(false, 'initially, touched state is false');
|
||||
el.children[2].focus();
|
||||
expect(el.touched).to.equal(false, 'focus is on second checkbox');
|
||||
button.focus();
|
||||
expect(el.touched).to.equal(
|
||||
true,
|
||||
`focus is on element behind second checkbox
|
||||
(group has blurred)`,
|
||||
);
|
||||
});
|
||||
|
||||
it(`becomes "touched" once the group as a whole becomes blurred via mouse interaction after
|
||||
keyboard interaction (e.g. focus is moved inside the group and user clicks somewhere outside
|
||||
the group)`, async () => {
|
||||
const groupWrapper = await fixture(`
|
||||
<div tabindex="0">
|
||||
<lion-checkbox-group>
|
||||
<label slot="label">My group</label>
|
||||
<lion-checkbox name="myGroup[]" label="Option 1" value="1"></lion-checkbox>
|
||||
<lion-checkbox name="myGroup[]" label="Option 2" vallue="2"></lion-checkbox>
|
||||
</lion-checkbox-group>
|
||||
</div>
|
||||
`);
|
||||
await nextFrame();
|
||||
|
||||
const el = groupWrapper.children[0];
|
||||
await el.children[1].updateComplete;
|
||||
el.children[1].focus();
|
||||
expect(el.touched).to.equal(false, 'initially, touched state is false');
|
||||
el.children[2].focus(); // simulate tab
|
||||
expect(el.touched).to.equal(false, 'focus is on second checkbox');
|
||||
// simulate click outside
|
||||
sinon.spy(el, '_setTouchedAndPrefilled');
|
||||
groupWrapper.click(); // blur the group via a click
|
||||
expect(el._setTouchedAndPrefilled.callCount).to.equal(1);
|
||||
// For some reason, document.activeElement is not updated after groupWrapper.click() (this
|
||||
// happens on user clicks, not on imperative clicks). So we check if the private callbacks
|
||||
// for outside clicks are called (they trigger _setTouchedAndPrefilled call).
|
||||
// To make sure focus is moved, we 'help' the test here to mimic browser behavior.
|
||||
// groupWrapper.focus();
|
||||
await triggerFocusFor(groupWrapper);
|
||||
expect(el.touched).to.equal(true, 'focus is on element outside checkbox group');
|
||||
});
|
||||
|
||||
it(`becomes "touched" once a single element of the group becomes "touched" via mouse interaction
|
||||
(e.g. user clicks on checkbox)`, async () => {
|
||||
const el = await fixture(`
|
||||
<lion-checkbox-group>
|
||||
<lion-checkbox name="myGroup[]"></lion-checkbox>
|
||||
<lion-checkbox name="myGroup[]"></lion-checkbox>
|
||||
</lion-checkbox-group>
|
||||
`);
|
||||
await nextFrame();
|
||||
|
||||
el.children[1].focus();
|
||||
expect(el.touched).to.equal(false, 'initially, touched state is false');
|
||||
el.children[1].click();
|
||||
expect(el.touched).to.equal(
|
||||
true,
|
||||
`focus is initiated via a mouse event, thus
|
||||
fieldset/checkbox-group as a whole is considered touched`,
|
||||
);
|
||||
});
|
||||
|
||||
it('can be required', async () => {
|
||||
const el = await fixture(html`
|
||||
<lion-checkbox-group .errorValidators=${[['required']]}>
|
||||
|
|
|
|||
|
|
@ -495,7 +495,7 @@ export const FormControlMixin = dedupeMixin(
|
|||
* an error message shouldn't be shown either.
|
||||
*
|
||||
*/
|
||||
return (this.touched && this.dirty && !this.prefilled) || this.prefilled || this.submitted;
|
||||
return (this.touched && this.dirty) || this.prefilled || this.submitted;
|
||||
}
|
||||
|
||||
// aria-labelledby and aria-describedby helpers
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { dedupeMixin } from '@lion/core';
|
||||
import { ObserverMixin } from '@lion/core/src/ObserverMixin.js';
|
||||
import { Unparseable } from '@lion/validate';
|
||||
|
||||
// For a future breaking release:
|
||||
|
|
@ -50,7 +49,7 @@ import { Unparseable } from '@lion/validate';
|
|||
export const FormatMixin = dedupeMixin(
|
||||
superclass =>
|
||||
// eslint-disable-next-line no-unused-vars, no-shadow
|
||||
class FormatMixin extends ObserverMixin(superclass) {
|
||||
class FormatMixin extends superclass {
|
||||
static get properties() {
|
||||
return {
|
||||
/**
|
||||
|
|
@ -121,13 +120,24 @@ export const FormatMixin = dedupeMixin(
|
|||
};
|
||||
}
|
||||
|
||||
static get syncObservers() {
|
||||
return {
|
||||
...super.syncObservers,
|
||||
_onModelValueChanged: ['modelValue'],
|
||||
_onSerializedValueChanged: ['serializedValue'],
|
||||
_onFormattedValueChanged: ['formattedValue'],
|
||||
};
|
||||
_requestUpdate(name, oldVal) {
|
||||
super._requestUpdate(name, oldVal);
|
||||
|
||||
if (name === 'modelValue' && this.modelValue !== oldVal) {
|
||||
this._onModelValueChanged({ modelValue: this.modelValue }, { modelValue: oldVal });
|
||||
}
|
||||
if (name === 'serializedValue' && this.serializedValue !== oldVal) {
|
||||
this._onSerializedValueChanged(
|
||||
{ serializedValue: this.serializedValue },
|
||||
{ serializedValue: oldVal },
|
||||
);
|
||||
}
|
||||
if (name === 'formattedValue' && this.formattedValue !== oldVal) {
|
||||
this._onFormattedValueChanged(
|
||||
{ formattedValue: this.formattedValue },
|
||||
{ formattedValue: oldVal },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { dedupeMixin } from '@lion/core';
|
||||
import { ObserverMixin } from '@lion/core/src/ObserverMixin.js';
|
||||
import { Unparseable } from '@lion/validate';
|
||||
|
||||
/**
|
||||
|
|
@ -15,7 +14,7 @@ import { Unparseable } from '@lion/validate';
|
|||
export const InteractionStateMixin = dedupeMixin(
|
||||
superclass =>
|
||||
// eslint-disable-next-line no-unused-vars, no-shadow
|
||||
class InteractionStateMixin extends ObserverMixin(superclass) {
|
||||
class InteractionStateMixin extends superclass {
|
||||
static get properties() {
|
||||
return {
|
||||
/**
|
||||
|
|
@ -45,12 +44,15 @@ export const InteractionStateMixin = dedupeMixin(
|
|||
};
|
||||
}
|
||||
|
||||
static get syncObservers() {
|
||||
return {
|
||||
...super.syncObservers,
|
||||
_onTouchedChanged: ['touched'],
|
||||
_onDirtyChanged: ['dirty'],
|
||||
};
|
||||
_requestUpdate(name, oldVal) {
|
||||
super._requestUpdate(name, oldVal);
|
||||
if (name === 'touched' && this.touched !== oldVal) {
|
||||
this._onTouchedChanged();
|
||||
}
|
||||
|
||||
if (name === 'dirty' && this.dirty !== oldVal) {
|
||||
this._onDirtyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
static _isPrefilled(modelValue) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { SlotMixin, html } from '@lion/core';
|
||||
import { LionLitElement } from '@lion/core/src/LionLitElement.js';
|
||||
import { SlotMixin, html, LitElement } from '@lion/core';
|
||||
import { DisabledMixin } from '@lion/core/src/DisabledMixin.js';
|
||||
import { ObserverMixin } from '@lion/core/src/ObserverMixin.js';
|
||||
import { ValidateMixin } from '@lion/validate';
|
||||
|
|
@ -15,7 +14,7 @@ const pascalCase = str => str.charAt(0).toUpperCase() + str.slice(1);
|
|||
* @extends LionLitElement
|
||||
*/
|
||||
export class LionFieldset extends FormRegistrarMixin(
|
||||
FormControlMixin(ValidateMixin(DisabledMixin(SlotMixin(ObserverMixin(LionLitElement))))),
|
||||
FormControlMixin(ValidateMixin(DisabledMixin(SlotMixin(ObserverMixin(LitElement))))),
|
||||
) {
|
||||
static get properties() {
|
||||
return {
|
||||
|
|
@ -24,11 +23,33 @@ export class LionFieldset extends FormRegistrarMixin(
|
|||
},
|
||||
submitted: {
|
||||
type: Boolean,
|
||||
nonEmptyToClass: 'state-submitted',
|
||||
reflect: true,
|
||||
},
|
||||
focused: {
|
||||
type: Boolean,
|
||||
reflect: true,
|
||||
},
|
||||
dirty: {
|
||||
type: Boolean,
|
||||
reflect: true,
|
||||
},
|
||||
touched: {
|
||||
type: Boolean,
|
||||
reflect: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
get touched() {
|
||||
return this.__touched;
|
||||
}
|
||||
|
||||
set touched(value) {
|
||||
const oldVal = this.__touched;
|
||||
this.__touched = value;
|
||||
this.requestUpdate('touched', oldVal);
|
||||
}
|
||||
|
||||
get inputElement() {
|
||||
return this;
|
||||
}
|
||||
|
|
@ -57,20 +78,8 @@ export class LionFieldset extends FormRegistrarMixin(
|
|||
this._setValueMapForAllFormElements('formattedValue', values);
|
||||
}
|
||||
|
||||
get touched() {
|
||||
return this._anyFormElementHas('touched');
|
||||
}
|
||||
|
||||
get dirty() {
|
||||
return this._anyFormElementHas('dirty');
|
||||
}
|
||||
|
||||
get prefilled() {
|
||||
return this._anyFormElementHas('prefilled');
|
||||
}
|
||||
|
||||
get focused() {
|
||||
return this._anyFormElementHas('focused');
|
||||
return this._everyFormElementHas('prefilled');
|
||||
}
|
||||
|
||||
get formElementsArray() {
|
||||
|
|
@ -84,28 +93,33 @@ export class LionFieldset extends FormRegistrarMixin(
|
|||
super();
|
||||
this.disabled = false;
|
||||
this.submitted = false;
|
||||
this.dirty = false;
|
||||
this.touched = false;
|
||||
this.focused = false;
|
||||
this.formElements = {};
|
||||
this.__addedSubValidators = false;
|
||||
this.__createTypeAbsenceValidators();
|
||||
|
||||
this._checkForOutsideClick = this._checkForOutsideClick.bind(this);
|
||||
|
||||
this.addEventListener('focusin', this._syncFocused);
|
||||
this.addEventListener('focusout', this._onFocusOut);
|
||||
this.addEventListener('validation-done', this.__validate);
|
||||
this.addEventListener('dirty-changed', this._syncDirty);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
// eslint-disable-next-line wc/guard-super-call
|
||||
super.connectedCallback();
|
||||
this.addEventListener('validation-done', this.__validate);
|
||||
this.addEventListener('focused-changed', this._updateFocusedClass);
|
||||
this.addEventListener('touched-changed', this._updateTouchedClass);
|
||||
this.addEventListener('dirty-changed', this._updateDirtyClass);
|
||||
super.connectedCallback(); // eslint-disable-line wc/guard-super-call
|
||||
this._setRole();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
// eslint-disable-next-line wc/guard-super-call
|
||||
super.disconnectedCallback();
|
||||
this.removeEventListener('validation-done', this.__validate);
|
||||
this.removeEventListener('focused-changed', this._updateFocusedClass);
|
||||
this.removeEventListener('touched-changed', this._updateTouchedClass);
|
||||
this.removeEventListener('dirty-changed', this._updateDirtyClass);
|
||||
super.disconnectedCallback(); // eslint-disable-line wc/guard-super-call
|
||||
|
||||
if (this.__hasActiveOutsideClickHandling) {
|
||||
document.removeEventListener('click', this._checkForOutsideClick);
|
||||
this.__hasActiveOutsideClickHandling = false;
|
||||
}
|
||||
}
|
||||
|
||||
updated(changedProps) {
|
||||
|
|
@ -114,12 +128,45 @@ export class LionFieldset extends FormRegistrarMixin(
|
|||
if (changedProps.has('disabled')) {
|
||||
if (this.disabled) {
|
||||
this.__requestChildrenToBeDisabled();
|
||||
/** @deprecated use disabled attribute instead */
|
||||
this.classList.add('state-disabled'); // eslint-disable-line wc/no-self-class
|
||||
} else {
|
||||
this.__retractRequestChildrenToBeDisabled();
|
||||
/** @deprecated use disabled attribute instead */
|
||||
this.classList.remove('state-disabled'); // eslint-disable-line wc/no-self-class
|
||||
}
|
||||
}
|
||||
if (changedProps.has('touched')) {
|
||||
/** @deprecated use touched attribute instead */
|
||||
this.classList[this.touched ? 'add' : 'remove']('state-touched');
|
||||
}
|
||||
|
||||
if (changedProps.has('dirty')) {
|
||||
/** @deprecated use dirty attribute instead */
|
||||
this.classList[this.dirty ? 'add' : 'remove']('state-dirty');
|
||||
}
|
||||
|
||||
if (changedProps.has('focused')) {
|
||||
/** @deprecated use touched attribute instead */
|
||||
this.classList[this.focused ? 'add' : 'remove']('state-focused');
|
||||
if (this.focused === true) {
|
||||
this.__setupOutsideClickHandling();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
__setupOutsideClickHandling() {
|
||||
if (!this.__hasActiveOutsideClickHandling) {
|
||||
document.addEventListener('click', this._checkForOutsideClick);
|
||||
this.__hasActiveOutsideClickHandling = true;
|
||||
}
|
||||
}
|
||||
|
||||
_checkForOutsideClick(event) {
|
||||
const outsideGroupClicked = !this.contains(event.target);
|
||||
if (outsideGroupClicked) {
|
||||
this.touched = true;
|
||||
}
|
||||
}
|
||||
|
||||
__requestChildrenToBeDisabled() {
|
||||
|
|
@ -190,6 +237,8 @@ export class LionFieldset extends FormRegistrarMixin(
|
|||
resetInteractionState() {
|
||||
// TODO: add submitted prop to InteractionStateMixin
|
||||
this.submitted = false;
|
||||
this.touched = false;
|
||||
this.dirty = false;
|
||||
this.formElementsArray.forEach(formElement => {
|
||||
if (typeof formElement.resetInteractionState === 'function') {
|
||||
formElement.resetInteractionState();
|
||||
|
|
@ -251,6 +300,15 @@ export class LionFieldset extends FormRegistrarMixin(
|
|||
});
|
||||
}
|
||||
|
||||
_everyFormElementHas(property) {
|
||||
return Object.keys(this.formElements).every(name => {
|
||||
if (Array.isArray(this.formElements[name])) {
|
||||
return this.formElements[name].every(el => !!el[property]);
|
||||
}
|
||||
return !!this.formElements[name][property];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets triggered by event 'validation-done' which enabled us to handle 2 different situations
|
||||
* - react on modelValue change, which says something about the validity as a whole
|
||||
|
|
@ -263,16 +321,20 @@ export class LionFieldset extends FormRegistrarMixin(
|
|||
}
|
||||
}
|
||||
|
||||
_updateFocusedClass() {
|
||||
this.classList[this.touched ? 'add' : 'remove']('state-focused');
|
||||
_syncFocused() {
|
||||
this.focused = this._anyFormElementHas('focused');
|
||||
}
|
||||
|
||||
_updateTouchedClass() {
|
||||
this.classList[this.touched ? 'add' : 'remove']('state-touched');
|
||||
_onFocusOut(ev) {
|
||||
const lastEl = this.formElementsArray[this.formElementsArray.length - 1];
|
||||
if (ev.target === lastEl) {
|
||||
this.touched = true;
|
||||
}
|
||||
this.focused = false;
|
||||
}
|
||||
|
||||
_updateDirtyClass() {
|
||||
this.classList[this.dirty ? 'add' : 'remove']('state-dirty');
|
||||
_syncDirty() {
|
||||
this.dirty = this._anyFormElementHas('dirty');
|
||||
}
|
||||
|
||||
_setRole(role) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
import { storiesOf, html } from '@open-wc/demoing-storybook';
|
||||
|
||||
import '../lion-fieldset.js';
|
||||
import { localize } from '@lion/localize';
|
||||
import { minLengthValidator } from '@lion/validate';
|
||||
|
||||
import '../../form-system/stories/helper-wc/h-output.js';
|
||||
|
||||
storiesOf('Forms|Fieldset', module)
|
||||
.add(
|
||||
|
|
@ -79,4 +83,129 @@ storiesOf('Forms|Fieldset', module)
|
|||
</button>
|
||||
</lion-fieldset>
|
||||
`,
|
||||
);
|
||||
)
|
||||
.add('Validation', () => {
|
||||
function isDemoValidator() {
|
||||
return false;
|
||||
}
|
||||
|
||||
const demoValidator = (...factoryParams) => [
|
||||
(...params) => ({ validator: isDemoValidator(...params) }),
|
||||
...factoryParams,
|
||||
];
|
||||
|
||||
try {
|
||||
localize.addData('en-GB', 'lion-validate+validator', {
|
||||
error: {
|
||||
validator: 'Demo error message',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
// expected as it's a demo
|
||||
}
|
||||
|
||||
return html`
|
||||
<lion-fieldset id="someId" .errorValidators=${[demoValidator()]}>
|
||||
<lion-input name="input1" label="Label"></lion-input>
|
||||
<button
|
||||
@click=${() => {
|
||||
document.getElementById('someId').serializeGroup();
|
||||
}}
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</lion-fieldset>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
<button>
|
||||
Tab-able
|
||||
</button>
|
||||
`;
|
||||
})
|
||||
.add('Validation 2 inputs', () => {
|
||||
const isCatsAndDogs = value => ({
|
||||
isCatsAndDogs: value.input1 === 'cats' && value.input2 === 'dogs',
|
||||
});
|
||||
localize.locale = 'en-GB';
|
||||
try {
|
||||
localize.addData('en-GB', 'lion-validate+isCatsAndDogs', {
|
||||
error: {
|
||||
isCatsAndDogs:
|
||||
'[Fieldset Error] Input 1 needs to be "cats" and Input 2 needs to be "dogs"',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
// expected as it's a demo
|
||||
}
|
||||
|
||||
return html`
|
||||
<lion-fieldset .errorValidators=${[[isCatsAndDogs]]}>
|
||||
<lion-input
|
||||
label="An all time YouTube favorite"
|
||||
name="input1"
|
||||
help-text="longer then 2 characters"
|
||||
.errorValidators=${[minLengthValidator(3)]}
|
||||
></lion-input>
|
||||
<lion-input
|
||||
label="Another all time YouTube favorite"
|
||||
name="input2"
|
||||
help-text="longer then 2 characters"
|
||||
.errorValidators=${[minLengthValidator(3)]}
|
||||
></lion-input>
|
||||
</lion-fieldset>
|
||||
`;
|
||||
})
|
||||
.add('Validation 2 fieldsets', () => {
|
||||
const isCats = value => ({
|
||||
isCats: value.input1 === 'cats',
|
||||
});
|
||||
localize.locale = 'en-GB';
|
||||
try {
|
||||
localize.addData('en-GB', 'lion-validate+isCats', {
|
||||
error: {
|
||||
isCats: '[Fieldset Nr. 1 Error] Input 1 needs to be "cats"',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
// expected as it's a demo
|
||||
}
|
||||
|
||||
const isDogs = value => ({
|
||||
isDogs: value.input1 === 'dogs',
|
||||
});
|
||||
localize.locale = 'en-GB';
|
||||
try {
|
||||
localize.addData('en-GB', 'lion-validate+isDogs', {
|
||||
error: {
|
||||
isDogs: '[Fieldset Nr. 2 Error] Input 1 needs to be "dogs"',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
// expected as it's a demo
|
||||
}
|
||||
|
||||
return html`
|
||||
<lion-fieldset .errorValidators=${[[isCats]]}>
|
||||
<label slot="label">Fieldset Nr. 1</label>
|
||||
<lion-input
|
||||
label="An all time YouTube favorite"
|
||||
name="input1"
|
||||
help-text="longer then 2 characters"
|
||||
.errorValidators=${[minLengthValidator(3)]}
|
||||
></lion-input>
|
||||
</lion-fieldset>
|
||||
<br />
|
||||
<hr />
|
||||
<br />
|
||||
<lion-fieldset .errorValidators=${[[isDogs]]}>
|
||||
<label slot="label">Fieldset Nr. 2</label>
|
||||
<lion-input
|
||||
label="An all time YouTube favorite"
|
||||
name="input1"
|
||||
help-text="longer then 2 characters"
|
||||
.errorValidators=${[minLengthValidator(3)]}
|
||||
></lion-input>
|
||||
</lion-fieldset>
|
||||
`;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,12 +1,4 @@
|
|||
import {
|
||||
expect,
|
||||
fixture,
|
||||
html,
|
||||
unsafeStatic,
|
||||
triggerFocusFor,
|
||||
triggerBlurFor,
|
||||
nextFrame,
|
||||
} from '@open-wc/testing';
|
||||
import { expect, fixture, html, unsafeStatic, triggerFocusFor, nextFrame } from '@open-wc/testing';
|
||||
import sinon from 'sinon';
|
||||
import { localizeTearDown } from '@lion/localize/test-helpers.js';
|
||||
import '@lion/input/lion-input.js';
|
||||
|
|
@ -23,8 +15,6 @@ const inputSlots = html`
|
|||
<${childTag} name="hobbies[]"></${childTag}>
|
||||
<${childTag} name="hobbies[]"></${childTag}>
|
||||
`;
|
||||
const nonPrefilledModelValue = '';
|
||||
const prefilledModelValue = 'prefill';
|
||||
|
||||
beforeEach(() => {
|
||||
localizeTearDown();
|
||||
|
|
@ -342,66 +332,164 @@ describe('<lion-fieldset>', () => {
|
|||
expect(fieldset.dirty).to.equal(true);
|
||||
});
|
||||
|
||||
it('sets touched when field left after focus', async () => {
|
||||
it('sets touched when last field in fieldset left after focus', async () => {
|
||||
const fieldset = await fixture(html`<${tag}>${inputSlots}</${tag}>`);
|
||||
await nextFrame();
|
||||
await triggerFocusFor(fieldset.formElements['gender[]'][0].inputElement);
|
||||
await triggerBlurFor(fieldset.formElements['gender[]'][0].inputElement);
|
||||
expect(fieldset.touched).to.equal(true);
|
||||
});
|
||||
|
||||
it('sets a class "state-(touched|dirty)"', async () => {
|
||||
const fieldset = await fixture(html`<${tag}>${inputSlots}</${tag}>`);
|
||||
await nextFrame();
|
||||
fieldset.formElements.color.touched = true;
|
||||
await fieldset.updateComplete;
|
||||
expect(fieldset.classList.contains('state-touched')).to.equal(
|
||||
true,
|
||||
'has class "state-touched"',
|
||||
await triggerFocusFor(fieldset.formElements['hobbies[]'][0].inputElement);
|
||||
await triggerFocusFor(
|
||||
fieldset.formElements['hobbies[]'][fieldset.formElements['gender[]'].length - 1]
|
||||
.inputElement,
|
||||
);
|
||||
const el = await fixture(html`
|
||||
<button></button>
|
||||
`);
|
||||
el.focus();
|
||||
|
||||
fieldset.formElements.color.dirty = true;
|
||||
await fieldset.updateComplete;
|
||||
expect(fieldset.classList.contains('state-dirty')).to.equal(true, 'has class "state-dirty"');
|
||||
expect(fieldset.touched).to.be.true;
|
||||
});
|
||||
|
||||
it('sets prefilled when field left and value non-empty', async () => {
|
||||
const fieldset = await fixture(html`<${tag}>${inputSlots}</${tag}>`);
|
||||
await nextFrame();
|
||||
fieldset.formElements['hobbies[]'][0].modelValue = { checked: false, value: 'chess' };
|
||||
fieldset.formElements['hobbies[]'][1].modelValue = { checked: false, value: 'football' };
|
||||
fieldset.formElements['gender[]'][0].modelValue = { checked: false, value: 'male' };
|
||||
fieldset.formElements['gender[]'][1].modelValue = { checked: false, value: 'female' };
|
||||
it('sets attributes [touched][dirty]', async () => {
|
||||
const el = await fixture(html`<${tag}></${tag}>`);
|
||||
el.touched = true;
|
||||
await el.updateComplete;
|
||||
expect(el).to.have.attribute('touched');
|
||||
|
||||
fieldset.formElements.color.modelValue = nonPrefilledModelValue;
|
||||
await triggerFocusFor(fieldset.formElements.color.inputElement);
|
||||
fieldset.formElements.color.modelValue = prefilledModelValue;
|
||||
await triggerBlurFor(fieldset.formElements.color.inputElement);
|
||||
expect(fieldset.prefilled).to.equal(true, 'sets prefilled when left non empty');
|
||||
|
||||
await triggerFocusFor(fieldset.formElements.color.inputElement);
|
||||
fieldset.formElements.color.modelValue = nonPrefilledModelValue;
|
||||
await triggerBlurFor(fieldset.formElements.color.inputElement);
|
||||
expect(fieldset.prefilled).to.equal(false, 'unsets prefilled when left empty');
|
||||
el.dirty = true;
|
||||
await el.updateComplete;
|
||||
expect(el).to.have.attribute('dirty');
|
||||
});
|
||||
|
||||
it('sets prefilled once instantiated', async () => {
|
||||
// no prefilled when nothing has value
|
||||
const fieldsetNotPrefilled = await fixture(html`<${tag}>${inputSlots}</${tag}>`);
|
||||
expect(fieldsetNotPrefilled.prefilled).to.equal(false, 'not prefilled on init');
|
||||
it('[deprecated] sets a class "state-(touched|dirty)"', async () => {
|
||||
const el = await fixture(html`<${tag}></${tag}>`);
|
||||
el.touched = true;
|
||||
await el.updateComplete;
|
||||
expect(el.classList.contains('state-touched')).to.equal(true, 'has class "state-touched"');
|
||||
|
||||
// prefilled when at least one child has value
|
||||
const fieldsetPrefilled = await fixture(html`
|
||||
el.dirty = true;
|
||||
await el.updateComplete;
|
||||
expect(el.classList.contains('state-dirty')).to.equal(true, 'has class "state-dirty"');
|
||||
});
|
||||
|
||||
it('becomes prefilled if all form elements are prefilled', async () => {
|
||||
const el = await fixture(html`
|
||||
<${tag}>
|
||||
<${childTag} name="gender[]" .modelValue=${prefilledModelValue}></${childTag}>
|
||||
<${childTag} name="gender[]"></${childTag}>
|
||||
<${childTag} name="color"></${childTag}>
|
||||
<${childTag} name="hobbies[]"></${childTag}>
|
||||
<${childTag} name="hobbies[]"></${childTag}>
|
||||
<${childTag} name="input1" prefilled></${childTag}>
|
||||
<${childTag} name="input2"></${childTag}>
|
||||
</${tag}>
|
||||
`);
|
||||
await nextFrame();
|
||||
expect(fieldsetPrefilled.prefilled).to.equal(true, 'prefilled on init');
|
||||
expect(el.prefilled).to.be.false;
|
||||
|
||||
const el2 = await fixture(html`
|
||||
<${tag}>
|
||||
<${childTag} name="input1" prefilled></${childTag}>
|
||||
<${childTag} name="input2" prefilled></${childTag}>
|
||||
</${tag}>
|
||||
`);
|
||||
await nextFrame();
|
||||
expect(el2.prefilled).to.be.true;
|
||||
});
|
||||
|
||||
it(`becomes "touched" once the last element of a group becomes blurred by keyboard
|
||||
interaction (e.g. tabbing through the checkbox-group)`, async () => {
|
||||
const el = await fixture(html`
|
||||
<${tag}>
|
||||
<label slot="label">My group</label>
|
||||
<${childTag} name="myGroup[]" label="Option 1" value="1"></${childTag}>
|
||||
<${childTag} name="myGroup[]" label="Option 2" value="2"></${childTag}>
|
||||
</${tag}>
|
||||
`);
|
||||
await nextFrame();
|
||||
|
||||
const button = await fixture(`<button>Blur</button>`);
|
||||
|
||||
expect(el.touched).to.equal(false, 'initially, touched state is false');
|
||||
el.children[2].focus();
|
||||
expect(el.touched).to.equal(false, 'focus is on second checkbox');
|
||||
button.focus();
|
||||
expect(el.touched).to.equal(
|
||||
true,
|
||||
`focus is on element behind second checkbox (group has blurred)`,
|
||||
);
|
||||
});
|
||||
|
||||
it(`becomes "touched" once the group as a whole becomes blurred via mouse interaction after
|
||||
keyboard interaction (e.g. focus is moved inside the group and user clicks somewhere outside
|
||||
the group)`, async () => {
|
||||
const el = await fixture(html`
|
||||
<${tag}>
|
||||
<${childTag} name="input1"></${childTag}>
|
||||
<${childTag} name="input2"></${childTag}>
|
||||
</${tag}>
|
||||
`);
|
||||
const el2 = await fixture(html`
|
||||
<${tag}>
|
||||
<${childTag} name="input1"></${childTag}>
|
||||
<${childTag} name="input2"></${childTag}>
|
||||
</${tag}>
|
||||
`);
|
||||
|
||||
await nextFrame();
|
||||
const outside = await fixture(html`
|
||||
<button>outside</button>
|
||||
`);
|
||||
|
||||
outside.click();
|
||||
expect(el.touched, 'unfocused fieldset should stays untouched').to.be.false;
|
||||
|
||||
el.children[1].focus();
|
||||
el.children[2].focus();
|
||||
expect(el.touched).to.be.false;
|
||||
|
||||
outside.click(); // blur the group via a click
|
||||
outside.focus(); // a real mouse click moves focus as well
|
||||
expect(el.touched).to.be.true;
|
||||
|
||||
expect(el2.touched).to.be.false;
|
||||
});
|
||||
|
||||
it('potentially shows fieldset error message on interaction change', async () => {
|
||||
const input1IsTen = value => ({ input1IsTen: value.input1 === 10 });
|
||||
const isNumber = value => ({ isNumber: typeof value === 'number' });
|
||||
const outSideButton = await fixture(html`
|
||||
<button>outside</button>
|
||||
`);
|
||||
const el = await fixture(html`
|
||||
<${tag} .errorValidators=${[[input1IsTen]]}>
|
||||
<${childTag} name="input1" .errorValidators=${[[isNumber]]}></${childTag}>
|
||||
</${tag}>
|
||||
`);
|
||||
await nextFrame();
|
||||
const input1 = el.querySelector(childTagString);
|
||||
input1.modelValue = 2;
|
||||
input1.focus();
|
||||
outSideButton.focus();
|
||||
|
||||
await el.updateComplete;
|
||||
expect(el.error.input1IsTen).to.be.true;
|
||||
expect(el.errorShow).to.be.true;
|
||||
});
|
||||
|
||||
it('show error if tabbing "out" of last ', async () => {
|
||||
const input1IsTen = value => ({ input1IsTen: value.input1 === 10 });
|
||||
const isNumber = value => ({ isNumber: typeof value === 'number' });
|
||||
const outSideButton = await fixture(html`
|
||||
<button>outside</button>
|
||||
`);
|
||||
const el = await fixture(html`
|
||||
<${tag} .errorValidators=${[[input1IsTen]]}>
|
||||
<${childTag} name="input1" .errorValidators=${[[isNumber]]}></${childTag}>
|
||||
<${childTag} name="input2" .errorValidators=${[[isNumber]]}></${childTag}>
|
||||
</${tag}>
|
||||
`);
|
||||
const inputs = el.querySelectorAll(childTagString);
|
||||
inputs[1].modelValue = 2; // make it dirty
|
||||
inputs[1].focus();
|
||||
|
||||
outSideButton.focus();
|
||||
await nextFrame();
|
||||
|
||||
expect(el.error.input1IsTen).to.be.true;
|
||||
expect(el.errorShow).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -609,32 +697,28 @@ describe('<lion-fieldset>', () => {
|
|||
});
|
||||
|
||||
it('clears interaction state', async () => {
|
||||
const fieldset = await fixture(html`<${tag}>${inputSlots}</${tag}>`);
|
||||
const el = await fixture(html`<${tag} touched dirty>${inputSlots}</${tag}>`);
|
||||
await nextFrame();
|
||||
// Safety check initially
|
||||
fieldset._setValueForAllFormElements('dirty', true);
|
||||
fieldset._setValueForAllFormElements('touched', true);
|
||||
fieldset._setValueForAllFormElements('prefilled', true);
|
||||
expect(fieldset.dirty).to.equal(true, '"dirty" initially');
|
||||
expect(fieldset.touched).to.equal(true, '"touched" initially');
|
||||
expect(fieldset.prefilled).to.equal(true, '"prefilled" initially');
|
||||
el._setValueForAllFormElements('prefilled', true);
|
||||
expect(el.dirty).to.equal(true, '"dirty" initially');
|
||||
expect(el.touched).to.equal(true, '"touched" initially');
|
||||
expect(el.prefilled).to.equal(true, '"prefilled" initially');
|
||||
|
||||
// Reset all children states, with prefilled false
|
||||
fieldset._setValueForAllFormElements('modelValue', {});
|
||||
fieldset.resetInteractionState();
|
||||
expect(fieldset.dirty).to.equal(false, 'not "dirty" after reset');
|
||||
expect(fieldset.touched).to.equal(false, 'not "touched" after reset');
|
||||
expect(fieldset.prefilled).to.equal(false, 'not "prefilled" after reset');
|
||||
el._setValueForAllFormElements('modelValue', {});
|
||||
el.resetInteractionState();
|
||||
expect(el.dirty).to.equal(false, 'not "dirty" after reset');
|
||||
expect(el.touched).to.equal(false, 'not "touched" after reset');
|
||||
expect(el.prefilled).to.equal(false, 'not "prefilled" after reset');
|
||||
|
||||
// Reset all children states with prefilled true
|
||||
fieldset._setValueForAllFormElements('dirty', true);
|
||||
fieldset._setValueForAllFormElements('touched', true);
|
||||
fieldset._setValueForAllFormElements('modelValue', { checked: true }); // not prefilled
|
||||
fieldset.resetInteractionState();
|
||||
expect(fieldset.dirty).to.equal(false, 'not "dirty" after 2nd reset');
|
||||
expect(fieldset.touched).to.equal(false, 'not "touched" after 2nd reset');
|
||||
el._setValueForAllFormElements('modelValue', { checked: true }); // not prefilled
|
||||
el.resetInteractionState();
|
||||
expect(el.dirty).to.equal(false, 'not "dirty" after 2nd reset');
|
||||
expect(el.touched).to.equal(false, 'not "touched" after 2nd reset');
|
||||
// prefilled state is dependant on value
|
||||
expect(fieldset.prefilled).to.equal(true, '"prefilled" after 2nd reset');
|
||||
expect(el.prefilled).to.equal(true, '"prefilled" after 2nd reset');
|
||||
});
|
||||
|
||||
it('clears submitted state', async () => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { LitElement, html, css } from '@lion/core';
|
||||
import { LionField } from '@lion/field';
|
||||
import { LionFieldset } from '@lion/fieldset';
|
||||
|
||||
export class HelperOutput extends LitElement {
|
||||
static get properties() {
|
||||
|
|
@ -41,7 +42,7 @@ export class HelperOutput extends LitElement {
|
|||
if (!this.field) {
|
||||
// Fuzzy logic, but... practical
|
||||
const prev = this.previousElementSibling;
|
||||
if (prev instanceof LionField) {
|
||||
if (prev instanceof LionField || prev instanceof LionFieldset) {
|
||||
this.field = prev;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,12 +92,18 @@ export class LionRadioGroup extends LionFieldset {
|
|||
}
|
||||
}
|
||||
|
||||
_onFocusOut() {
|
||||
this.touched = true;
|
||||
this.focused = false;
|
||||
}
|
||||
|
||||
__triggerCheckedValueChanged() {
|
||||
const value = this.checkedValue;
|
||||
if (value != null && value !== this.__previousCheckedValue) {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('checked-value-changed', { bubbles: true, composed: true }),
|
||||
);
|
||||
this.touched = true;
|
||||
this.__previousCheckedValue = value;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import { storiesOf, html } from '@open-wc/demoing-storybook';
|
||||
import { localize } from '@lion/localize';
|
||||
|
||||
import '../lion-radio-group.js';
|
||||
import '@lion/radio/lion-radio.js';
|
||||
import '@lion/form/lion-form.js';
|
||||
import '../lion-radio-group.js';
|
||||
|
||||
storiesOf('Forms|Radio Group', module)
|
||||
.add(
|
||||
|
|
@ -98,4 +100,34 @@ storiesOf('Forms|Radio Group', module)
|
|||
</form></lion-form
|
||||
>
|
||||
`;
|
||||
})
|
||||
.add('Validation Item', () => {
|
||||
const isBrontosaurus = value => {
|
||||
const selectedValue = value['dinos[]'].find(v => v.checked === true);
|
||||
return {
|
||||
isBrontosaurus: selectedValue ? selectedValue.value === 'brontosaurus' : false,
|
||||
};
|
||||
};
|
||||
localize.locale = 'en-GB';
|
||||
try {
|
||||
localize.addData('en-GB', 'lion-validate+isBrontosaurus', {
|
||||
error: {
|
||||
isBrontosaurus: 'You need to select "brontosaurus"',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
// expected as it's a demo
|
||||
}
|
||||
|
||||
return html`
|
||||
<lion-radio-group
|
||||
name="dinosGroup"
|
||||
label="What are your favourite dinosaurs?"
|
||||
.errorValidators=${[['required'], [isBrontosaurus]]}
|
||||
>
|
||||
<lion-radio name="dinos[]" label="allosaurus" .choiceValue=${'allosaurus'}></lion-radio>
|
||||
<lion-radio name="dinos[]" label="brontosaurus" .choiceValue=${'brontosaurus'}></lion-radio>
|
||||
<lion-radio name="dinos[]" label="diplodocus" .choiceValue=${'diplodocus'}></lion-radio>
|
||||
</lion-radio-group>
|
||||
`;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -252,4 +252,19 @@ describe('<lion-radio-group>', () => {
|
|||
|
||||
expect(group.serializedValue).to.deep.equal('');
|
||||
});
|
||||
|
||||
it(`becomes "touched" once a single element of the group changes`, async () => {
|
||||
const el = await fixture(html`
|
||||
<lion-radio-group>
|
||||
<lion-radio name="myGroup[]"></lion-radio>
|
||||
<lion-radio name="myGroup[]"></lion-radio>
|
||||
</lion-radio-group>
|
||||
`);
|
||||
await nextFrame();
|
||||
|
||||
el.children[1].focus();
|
||||
expect(el.touched).to.equal(false, 'initially, touched state is false');
|
||||
el.children[1].checked = true;
|
||||
expect(el.touched, `focused via a mouse click, group should be touched`).to.be.true;
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
/* eslint-disable class-methods-use-this, camelcase, no-param-reassign */
|
||||
|
||||
import { dedupeMixin, SlotMixin } from '@lion/core';
|
||||
import { ObserverMixin } from '@lion/core/src/ObserverMixin.js';
|
||||
import { localize, LocalizeMixin } from '@lion/localize';
|
||||
import { Unparseable } from './Unparseable.js';
|
||||
import { randomOk } from './validators.js';
|
||||
|
|
@ -14,10 +13,15 @@ const pascalCase = str => str.charAt(0).toUpperCase() + str.slice(1);
|
|||
export const ValidateMixin = dedupeMixin(
|
||||
superclass =>
|
||||
// eslint-disable-next-line no-unused-vars, no-shadow, max-len
|
||||
class ValidateMixin extends ObserverMixin(LocalizeMixin(SlotMixin(superclass))) {
|
||||
class ValidateMixin extends LocalizeMixin(SlotMixin(superclass)) {
|
||||
/* * * * * * * * * *
|
||||
Configuration */
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.__oldValues = {};
|
||||
}
|
||||
|
||||
get slots() {
|
||||
return {
|
||||
...super.slots,
|
||||
|
|
@ -196,12 +200,11 @@ export const ValidateMixin = dedupeMixin(
|
|||
};
|
||||
}
|
||||
|
||||
static get asyncObservers() {
|
||||
return {
|
||||
...super.asyncObservers,
|
||||
// TODO: consider adding 'touched', 'dirty', 'submitted', 'prefilled' on LionFieldFundament
|
||||
// level, since ValidateMixin doesn't have a direct dependency on interactionState
|
||||
_createMessageAndRenderFeedback: [
|
||||
updated(changedProperties) {
|
||||
super.updated(changedProperties);
|
||||
|
||||
if (
|
||||
[
|
||||
'error',
|
||||
'warning',
|
||||
'info',
|
||||
|
|
@ -211,30 +214,79 @@ export const ValidateMixin = dedupeMixin(
|
|||
'submitted',
|
||||
'prefilled',
|
||||
'label',
|
||||
],
|
||||
_onErrorShowChangedAsync: ['errorShow'],
|
||||
};
|
||||
].some(key => changedProperties.has(key))
|
||||
) {
|
||||
this._createMessageAndRenderFeedback();
|
||||
}
|
||||
|
||||
if (changedProperties.has('errorShow')) {
|
||||
this._onErrorShowChangedAsync();
|
||||
}
|
||||
}
|
||||
|
||||
static get syncObservers() {
|
||||
return {
|
||||
...super.syncObservers,
|
||||
validate: [
|
||||
_requestUpdate(name, oldVal) {
|
||||
super._requestUpdate(name, oldVal);
|
||||
|
||||
/**
|
||||
* Validation needs to happen before other updates
|
||||
* E.g. formatting should not happen before we know the updated errorState
|
||||
*/
|
||||
if (
|
||||
[
|
||||
'errorValidators',
|
||||
'warningValidators',
|
||||
'infoValidators',
|
||||
'successValidators',
|
||||
'modelValue',
|
||||
],
|
||||
_onErrorChanged: ['error'],
|
||||
_onWarningChanged: ['warning'],
|
||||
_onInfoChanged: ['info'],
|
||||
_onSuccessChanged: ['success'],
|
||||
_onErrorStateChanged: ['errorState'],
|
||||
_onWarningStateChanged: ['warningState'],
|
||||
_onInfoStateChanged: ['infoState'],
|
||||
_onSuccessStateChanged: ['successState'],
|
||||
};
|
||||
].some(key => name === key)
|
||||
) {
|
||||
this.validate();
|
||||
}
|
||||
|
||||
// @deprecated adding css classes for backwards compatibility
|
||||
this.constructor.validationTypes.forEach(type => {
|
||||
if (name === `${type}State`) {
|
||||
this.classList[this[`${type}State`] ? 'add' : 'remove'](`state-${type}`);
|
||||
}
|
||||
if (name === `${type}Show`) {
|
||||
this.classList[this[`${type}Show`] ? 'add' : 'remove'](`state-${type}-show`);
|
||||
}
|
||||
});
|
||||
if (name === 'invalid') {
|
||||
this.classList[this.invalid ? 'add' : 'remove'](`state-invalid`);
|
||||
}
|
||||
|
||||
if (name === 'error' && this.error !== oldVal) {
|
||||
this._onErrorChanged();
|
||||
}
|
||||
|
||||
if (name === 'warning' && this.warning !== oldVal) {
|
||||
this._onWarningChanged();
|
||||
}
|
||||
|
||||
if (name === 'info' && this.info !== oldVal) {
|
||||
this._onInfoChanged();
|
||||
}
|
||||
|
||||
if (name === 'success' && this.success !== oldVal) {
|
||||
this._onSuccessChanged();
|
||||
}
|
||||
|
||||
if (name === 'errorState' && this.errorState !== oldVal) {
|
||||
this._onErrorStateChanged();
|
||||
}
|
||||
|
||||
if (name === 'warningState' && this.warningState !== oldVal) {
|
||||
this._onWarningStateChanged();
|
||||
}
|
||||
|
||||
if (name === 'infoState' && this.infoState !== oldVal) {
|
||||
this._onInfoStateChanged();
|
||||
}
|
||||
|
||||
if (name === 'successState' && this.successState !== oldVal) {
|
||||
this._onSuccessStateChanged();
|
||||
}
|
||||
}
|
||||
|
||||
static get validationTypes() {
|
||||
|
|
@ -245,23 +297,6 @@ export const ValidateMixin = dedupeMixin(
|
|||
return (this.$$slot && this.$$slot('feedback')) || this.querySelector('[slot="feedback"]');
|
||||
}
|
||||
|
||||
updated(changedProperties) {
|
||||
super.updated(changedProperties);
|
||||
|
||||
// @deprecated adding css classes for backwards compatibility
|
||||
this.constructor.validationTypes.forEach(name => {
|
||||
if (changedProperties.has(`${name}State`)) {
|
||||
this.classList[this[`${name}State`] ? 'add' : 'remove'](`state-${name}`);
|
||||
}
|
||||
if (changedProperties.has(`${name}Show`)) {
|
||||
this.classList[this[`${name}Show`] ? 'add' : 'remove'](`state-${name}-show`);
|
||||
}
|
||||
});
|
||||
if (changedProperties.has('invalid')) {
|
||||
this.classList[this.invalid ? 'add' : 'remove'](`state-invalid`);
|
||||
}
|
||||
}
|
||||
|
||||
getFieldName(validatorParams) {
|
||||
const label =
|
||||
this.label || (this.$$slot && this.$$slot('label') && this.$$slot('label').textContent);
|
||||
|
|
@ -327,26 +362,26 @@ export const ValidateMixin = dedupeMixin(
|
|||
}
|
||||
}
|
||||
|
||||
_onErrorChanged(newValues, oldValues) {
|
||||
if (!this.constructor._objectEquals(newValues.error, oldValues.error)) {
|
||||
_onErrorChanged() {
|
||||
if (!this.constructor._objectEquals(this.error, this.__oldValues.error)) {
|
||||
this.dispatchEvent(new CustomEvent('error-changed', { bubbles: true, composed: true }));
|
||||
}
|
||||
}
|
||||
|
||||
_onWarningChanged(newValues, oldValues) {
|
||||
if (!this.constructor._objectEquals(newValues.warning, oldValues.warning)) {
|
||||
_onWarningChanged() {
|
||||
if (!this.constructor._objectEquals(this.warning, this.__oldValues.warning)) {
|
||||
this.dispatchEvent(new CustomEvent('warning-changed', { bubbles: true, composed: true }));
|
||||
}
|
||||
}
|
||||
|
||||
_onInfoChanged(newValues, oldValues) {
|
||||
if (!this.constructor._objectEquals(newValues.info, oldValues.info)) {
|
||||
_onInfoChanged() {
|
||||
if (!this.constructor._objectEquals(this.info, this.__oldValues.info)) {
|
||||
this.dispatchEvent(new CustomEvent('info-changed', { bubbles: true, composed: true }));
|
||||
}
|
||||
}
|
||||
|
||||
_onSuccessChanged(newValues, oldValues) {
|
||||
if (!this.constructor._objectEquals(newValues.success, oldValues.success)) {
|
||||
_onSuccessChanged() {
|
||||
if (!this.constructor._objectEquals(this.success, this.__oldValues.success)) {
|
||||
this.dispatchEvent(new CustomEvent('success-changed', { bubbles: true, composed: true }));
|
||||
}
|
||||
}
|
||||
|
|
@ -385,10 +420,10 @@ export const ValidateMixin = dedupeMixin(
|
|||
}
|
||||
}
|
||||
|
||||
_onErrorShowChangedAsync({ errorShow }) {
|
||||
_onErrorShowChangedAsync() {
|
||||
// Screen reader output should be in sync with visibility of error messages
|
||||
if (this.inputElement) {
|
||||
this.inputElement.setAttribute('aria-invalid', errorShow);
|
||||
this.inputElement.setAttribute('aria-invalid', this.errorShow);
|
||||
// TODO: test and see if needed for a11y
|
||||
// this.inputElement.setCustomValidity(this._validationMessage || '');
|
||||
}
|
||||
|
|
@ -602,6 +637,7 @@ export const ValidateMixin = dedupeMixin(
|
|||
}
|
||||
|
||||
this[`${type}State`] = resultList.length > 0;
|
||||
this.__oldValues[type] = this[type];
|
||||
this[type] = result;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue