diff --git a/packages/field/src/FormControlMixin.js b/packages/field/src/FormControlMixin.js
index c684ff78c..109edba76 100644
--- a/packages/field/src/FormControlMixin.js
+++ b/packages/field/src/FormControlMixin.js
@@ -1,4 +1,4 @@
-import { html, css, nothing, dedupeMixin } from '@lion/core';
+import { html, css, nothing, dedupeMixin, SlotMixin } from '@lion/core';
import { ObserverMixin } from '@lion/core/src/ObserverMixin.js';
/**
@@ -14,7 +14,7 @@ import { ObserverMixin } from '@lion/core/src/ObserverMixin.js';
export const FormControlMixin = dedupeMixin(
superclass =>
// eslint-disable-next-line no-shadow, no-unused-vars
- class FormControlMixin extends ObserverMixin(superclass) {
+ class FormControlMixin extends ObserverMixin(SlotMixin(superclass)) {
static get properties() {
return {
...super.properties,
@@ -75,8 +75,21 @@ export const FormControlMixin = dedupeMixin(
};
}
+ /** @deprecated will be this._inputNode in next breaking release */
get inputElement() {
- return (this.$$slot && this.$$slot('input')) || this.querySelector('[slot=input]'); // eslint-disable-line
+ return this.__getDirectSlotChild('input');
+ }
+
+ get _labelNode() {
+ return this.__getDirectSlotChild('label');
+ }
+
+ get _helpTextNode() {
+ return this.__getDirectSlotChild('help-text');
+ }
+
+ get _feedbackNode() {
+ return this.__getDirectSlotChild('feedback');
}
constructor() {
@@ -107,29 +120,31 @@ export const FormControlMixin = dedupeMixin(
}
_enhanceLightDomA11y() {
- if (this.inputElement) {
- this.inputElement.id = this.inputElement.id || this._inputId;
+ const { inputElement, _labelNode, _helpTextNode, _feedbackNode } = this;
+
+ if (inputElement) {
+ inputElement.id = inputElement.id || this._inputId;
}
- if (this.$$slot('label')) {
- this.$$slot('label').setAttribute('for', this._inputId);
- this.$$slot('label').id = this.$$slot('label').id || `label-${this._inputId}`;
- const labelledById = ` ${this.$$slot('label').id}`;
+ if (_labelNode) {
+ _labelNode.setAttribute('for', this._inputId);
+ _labelNode.id = _labelNode.id || `label-${this._inputId}`;
+ const labelledById = ` ${_labelNode.id}`;
if (this._ariaLabelledby.indexOf(labelledById) === -1) {
- this._ariaLabelledby += ` ${this.$$slot('label').id}`;
+ this._ariaLabelledby += ` ${_labelNode.id}`;
}
}
- if (this.$$slot('help-text')) {
- this.$$slot('help-text').id = this.$$slot('help-text').id || `help-text-${this._inputId}`;
- const describeIdHelpText = ` ${this.$$slot('help-text').id}`;
+ if (_helpTextNode) {
+ _helpTextNode.id = _helpTextNode.id || `help-text-${this._inputId}`;
+ const describeIdHelpText = ` ${_helpTextNode.id}`;
if (this._ariaDescribedby.indexOf(describeIdHelpText) === -1) {
- this._ariaDescribedby += ` ${this.$$slot('help-text').id}`;
+ this._ariaDescribedby += ` ${_helpTextNode.id}`;
}
}
- if (this.$$slot('feedback')) {
- this.$$slot('feedback').id = this.$$slot('feedback').id || `feedback-${this._inputId}`;
- const describeIdFeedback = ` ${this.$$slot('feedback').id}`;
+ if (_feedbackNode) {
+ _feedbackNode.id = _feedbackNode.id || `feedback-${this._inputId}`;
+ const describeIdFeedback = ` ${_feedbackNode.id}`;
if (this._ariaDescribedby.indexOf(describeIdFeedback) === -1) {
- this._ariaDescribedby += ` ${this.$$slot('feedback').id}`;
+ this._ariaDescribedby += ` ${_feedbackNode.id}`;
}
}
this._enhanceLightDomA11yForAdditionalSlots();
@@ -181,7 +196,7 @@ export const FormControlMixin = dedupeMixin(
additionalSlots = ['prefix', 'suffix', 'before', 'after'],
) {
additionalSlots.forEach(additionalSlot => {
- const element = this.$$slot(additionalSlot);
+ const element = this.__getDirectSlotChild(additionalSlot);
if (element) {
element.id = element.id || `${additionalSlot}-${this._inputId}`;
if (element.hasAttribute('data-label') === true) {
@@ -218,14 +233,14 @@ export const FormControlMixin = dedupeMixin(
}
_onLabelChanged({ label }) {
- if (this.$$slot && this.$$slot('label')) {
- this.$$slot('label').textContent = label;
+ if (this._labelNode) {
+ this._labelNode.textContent = label;
}
}
_onHelpTextChanged({ helpText }) {
- if (this.$$slot && this.$$slot('help-text')) {
- this.$$slot('help-text').textContent = helpText;
+ if (this._helpTextNode) {
+ this._helpTextNode.textContent = helpText;
}
}
@@ -552,7 +567,7 @@ export const FormControlMixin = dedupeMixin(
// Returns dom references to all elements that should be referred to by field(s)
_getAriaDescriptionElements() {
- return [this.$$slot('help-text'), this.$$slot('feedback')];
+ return [this._helpTextNode, this._feedbackNode];
}
/**
@@ -574,5 +589,9 @@ export const FormControlMixin = dedupeMixin(
addToAriaDescription(id) {
this._ariaDescribedby += ` ${id}`;
}
+
+ __getDirectSlotChild(slotName) {
+ return [...this.children].find(el => el.slot === slotName);
+ }
},
);
diff --git a/packages/field/src/FormatMixin.js b/packages/field/src/FormatMixin.js
index ea33cc36d..a63fcef44 100644
--- a/packages/field/src/FormatMixin.js
+++ b/packages/field/src/FormatMixin.js
@@ -1,7 +1,6 @@
/* eslint-disable class-methods-use-this */
import { dedupeMixin } from '@lion/core';
-import { EventMixin } from '@lion/core/src/EventMixin.js';
import { ObserverMixin } from '@lion/core/src/ObserverMixin.js';
import { Unparseable } from '@lion/validate';
@@ -21,7 +20,7 @@ import { Unparseable } from '@lion/validate';
export const FormatMixin = dedupeMixin(
superclass =>
// eslint-disable-next-line no-unused-vars, no-shadow
- class FormatMixin extends EventMixin(ObserverMixin(superclass)) {
+ class FormatMixin extends ObserverMixin(superclass) {
static get properties() {
return {
...super.properties,
@@ -231,7 +230,7 @@ export const FormatMixin = dedupeMixin(
// imperatively, we DO want to format a value (it is the only way to get meaningful
// input into `.inputElement` with modelValue as input)
- if (this.__isHandlingUserInput && this.errorState) {
+ if (this.__isHandlingUserInput && this.errorState && this.inputElement) {
return this.inputElement ? this.value : undefined;
}
return this.formatter(this.modelValue, this.formatOptions);
@@ -336,8 +335,6 @@ export const FormatMixin = dedupeMixin(
// is guaranteed to be calculated
setTimeout(this._reflectBackFormattedValueToUser);
};
- this.inputElement.addEventListener(this.formatOn, this._reflectBackFormattedValueDebounced);
- this.inputElement.addEventListener('input', this._proxyInputEvent);
this.addEventListener('user-input-changed', this._onUserInputChanged);
// Connect the value found in to the formatting/parsing/serializing loop as a
// fallback mechanism. Assume the user uses the value property of the
@@ -348,16 +345,26 @@ export const FormatMixin = dedupeMixin(
this._syncValueUpwards();
}
this._reflectBackFormattedValueToUser();
+
+ if (this.inputElement) {
+ this.inputElement.addEventListener(
+ this.formatOn,
+ this._reflectBackFormattedValueDebounced,
+ );
+ this.inputElement.addEventListener('input', this._proxyInputEvent);
+ }
}
disconnectedCallback() {
super.disconnectedCallback();
- this.inputElement.removeEventListener('input', this._proxyInputEvent);
this.removeEventListener('user-input-changed', this._onUserInputChanged);
- this.inputElement.removeEventListener(
- this.formatOn,
- this._reflectBackFormattedValueDebounced,
- );
+ if (this.inputElement) {
+ this.inputElement.removeEventListener('input', this._proxyInputEvent);
+ this.inputElement.removeEventListener(
+ this.formatOn,
+ this._reflectBackFormattedValueDebounced,
+ );
+ }
}
},
);
diff --git a/packages/field/src/InteractionStateMixin.js b/packages/field/src/InteractionStateMixin.js
index 17472f870..5d9968a6b 100644
--- a/packages/field/src/InteractionStateMixin.js
+++ b/packages/field/src/InteractionStateMixin.js
@@ -1,8 +1,6 @@
import { dedupeMixin } from '@lion/core';
-import { CssClassMixin } from '@lion/core/src/CssClassMixin.js';
import { ObserverMixin } from '@lion/core/src/ObserverMixin.js';
import { Unparseable } from '@lion/validate';
-import { FocusMixin } from './FocusMixin.js';
/**
* `InteractionStateMixin` adds meta information about touched and dirty states, that can
@@ -16,7 +14,7 @@ import { FocusMixin } from './FocusMixin.js';
export const InteractionStateMixin = dedupeMixin(
superclass =>
// eslint-disable-next-line no-unused-vars, no-shadow
- class InteractionStateMixin extends CssClassMixin(FocusMixin(ObserverMixin(superclass))) {
+ class InteractionStateMixin extends ObserverMixin(superclass) {
static get properties() {
return {
...super.properties,
@@ -25,7 +23,7 @@ export const InteractionStateMixin = dedupeMixin(
*/
touched: {
type: Boolean,
- nonEmptyToClass: 'state-touched',
+ reflect: true,
},
/**
@@ -33,7 +31,7 @@ export const InteractionStateMixin = dedupeMixin(
*/
dirty: {
type: Boolean,
- nonEmptyToClass: 'state-dirty',
+ reflect: true,
},
/**
@@ -75,7 +73,7 @@ export const InteractionStateMixin = dedupeMixin(
this.touched = false;
this.dirty = false;
this.prefilled = false;
- this.leaveEvent = 'blur';
+ this._leaveEvent = 'blur';
this._valueChangedEvent = 'model-value-changed';
this._iStateOnLeave = this._iStateOnLeave.bind(this);
@@ -89,7 +87,7 @@ export const InteractionStateMixin = dedupeMixin(
if (super.connectedCallback) {
super.connectedCallback();
}
- this.addEventListener(this.leaveEvent, this._iStateOnLeave);
+ this.addEventListener(this._leaveEvent, this._iStateOnLeave);
this.addEventListener(this._valueChangedEvent, this._iStateOnValueChange);
this.initInteractionState();
}
@@ -98,10 +96,21 @@ export const InteractionStateMixin = dedupeMixin(
if (super.disconnectedCallback) {
super.disconnectedCallback();
}
- this.removeEventListener(this.leaveEvent, this._iStateOnLeave);
+ this.removeEventListener(this._leaveEvent, this._iStateOnLeave);
this.removeEventListener(this._valueChangedEvent, this._iStateOnValueChange);
}
+ updated(changedProperties) {
+ super.updated(changedProperties);
+ // classes are added only for backward compatibility - they are deprecated
+ if (changedProperties.has('touched')) {
+ this.classList[this.touched ? 'add' : 'remove']('state-touched');
+ }
+ if (changedProperties.has('dirty')) {
+ this.classList[this.dirty ? 'add' : 'remove']('state-dirty');
+ }
+ }
+
/**
* Evaluations performed on connectedCallback. Since some components can be out of sync
* (due to interdependence on light children that can only be processed
@@ -150,5 +159,19 @@ export const InteractionStateMixin = dedupeMixin(
_onDirtyChanged() {
this.dispatchEvent(new CustomEvent('dirty-changed', { bubbles: true, composed: true }));
}
+
+ /**
+ * @deprecated
+ */
+ get leaveEvent() {
+ return this._leaveEvent;
+ }
+
+ /**
+ * @deprecated
+ */
+ set leaveEvent(eventName) {
+ this._leaveEvent = eventName;
+ }
},
);
diff --git a/packages/field/src/LionField.js b/packages/field/src/LionField.js
index 7cd490e38..9b06e0c0f 100644
--- a/packages/field/src/LionField.js
+++ b/packages/field/src/LionField.js
@@ -8,6 +8,7 @@ import { ValidateMixin } from '@lion/validate';
import { FormControlMixin } from './FormControlMixin.js';
import { InteractionStateMixin } from './InteractionStateMixin.js'; // applies FocusMixin
import { FormatMixin } from './FormatMixin.js';
+import { FocusMixin } from './FocusMixin.js';
/**
* LionField: wraps components input, textarea and select and potentially others
@@ -29,9 +30,11 @@ import { FormatMixin } from './FormatMixin.js';
// eslint-disable-next-line max-len, no-unused-vars
export class LionField extends FormControlMixin(
InteractionStateMixin(
- FormatMixin(
- ValidateMixin(
- CssClassMixin(ElementMixin(DelegateMixin(SlotMixin(ObserverMixin(LionLitElement))))),
+ FocusMixin(
+ FormatMixin(
+ ValidateMixin(
+ CssClassMixin(ElementMixin(DelegateMixin(SlotMixin(ObserverMixin(LionLitElement))))),
+ ),
),
),
),
diff --git a/packages/field/test/FormatMixin.test.js b/packages/field/test/FormatMixin.test.js
index 1c7905364..aa004ca70 100644
--- a/packages/field/test/FormatMixin.test.js
+++ b/packages/field/test/FormatMixin.test.js
@@ -1,7 +1,7 @@
import { expect, fixture, html, aTimeout, defineCE, unsafeStatic } from '@open-wc/testing';
import sinon from 'sinon';
-import { LionLitElement } from '@lion/core/src/LionLitElement.js';
+import { LitElement } from '@lion/core';
import { Unparseable } from '@lion/validate';
import { FormatMixin } from '../src/FormatMixin.js';
@@ -17,7 +17,7 @@ describe('FormatMixin', () => {
before(async () => {
const tagString = defineCE(
- class extends FormatMixin(LionLitElement) {
+ class extends FormatMixin(LitElement) {
render() {
return html`
@@ -176,6 +176,14 @@ describe('FormatMixin', () => {
expect(el.inputElement.value).to.equal('foo: test2');
});
+ it('works if there is no underlying inputElement', async () => {
+ const tagNoInputString = defineCE(class extends FormatMixin(LitElement) {});
+ const tagNoInput = unsafeStatic(tagNoInputString);
+ expect(async () => {
+ await fixture(html`<${tagNoInput}>${tagNoInput}>`);
+ }).to.not.throw();
+ });
+
describe('parsers/formatters/serializers', () => {
it('should call the parser|formatter|serializer provided by user', async () => {
const formatterSpy = sinon.spy(value => `foo: ${value}`);
diff --git a/packages/field/test/InteractionStateMixin.test.js b/packages/field/test/InteractionStateMixin.test.js
index 06b8d476f..c95555b66 100644
--- a/packages/field/test/InteractionStateMixin.test.js
+++ b/packages/field/test/InteractionStateMixin.test.js
@@ -1,14 +1,28 @@
-import { expect, fixture, unsafeStatic, html, defineCE } from '@open-wc/testing';
+import {
+ expect,
+ fixture,
+ unsafeStatic,
+ html,
+ defineCE,
+ triggerFocusFor,
+ triggerBlurFor,
+} from '@open-wc/testing';
import sinon from 'sinon';
-import { LionLitElement } from '@lion/core/src/LionLitElement.js';
+import { LitElement } from '@lion/core';
import { InteractionStateMixin } from '../src/InteractionStateMixin.js';
describe('InteractionStateMixin', async () => {
- let elem;
+ let tagString;
+ let tag;
before(() => {
- elem = defineCE(
- class IState extends InteractionStateMixin(LionLitElement) {
+ tagString = defineCE(
+ class IState extends InteractionStateMixin(LitElement) {
+ connectedCallback() {
+ super.connectedCallback();
+ this.tabIndex = 0;
+ }
+
set modelValue(v) {
this._modelValue = v;
this.dispatchEvent(
@@ -19,149 +33,157 @@ describe('InteractionStateMixin', async () => {
get modelValue() {
return this._modelValue;
}
-
- get inputElement() {
- return this.querySelector('input');
- }
},
);
+ tag = unsafeStatic(tagString);
});
it('sets states to false on init', async () => {
- const input = await fixture(`<${elem}>${elem}>`);
- expect(input.dirty).to.equal(false);
- expect(input.touched).to.equal(false);
- expect(input.prefilled).to.equal(false);
+ const el = await fixture(html`<${tag}>${tag}>`);
+ expect(el.dirty).to.be.false;
+ expect(el.touched).to.be.false;
+ expect(el.prefilled).to.be.false;
});
it('sets dirty when value changed', async () => {
- const input = await fixture(`<${elem}>${elem}>`);
- input.modelValue = 'foobar';
- expect(input.dirty).to.equal(true);
+ const el = await fixture(html`<${tag}>${tag}>`);
+ expect(el.dirty).to.be.false;
+ el.modelValue = 'foobar';
+ expect(el.dirty).to.be.true;
});
- // Skipping, since this issue (not being able to set focus on element extending from LitElement)
- // only occurs in WCT context (not in Storybook/Stackblitz).
- // See: https://stackblitz.com/edit/lit-element-request-update-bug-g59tjq?file=blurry.js
- // it.skip
it('sets touched to true when field left after focus', async () => {
- // const formElement = await LionTest.htmlFixture(`<${elem}>${elem}>`);
- // await triggerFocusFor(formElement.inputElement); // focus/blur can't be delegated
- // await triggerBlurFor(formElement.inputElement);
- // expect(formElement.touched).to.equal(true);
+ const el = await fixture(html`<${tag}>${tag}>`);
+ await triggerFocusFor(el);
+ await triggerBlurFor(el);
+ expect(el.touched).to.be.true;
});
+ // classes are added only for backward compatibility - they are deprecated
it('sets a class "state-(touched|dirty)"', async () => {
- const state = await fixture(`<${elem}>${elem}>`);
- state.touched = true;
- await state.updateComplete;
- expect(state.classList.contains('state-touched')).to.equal(true, 'has class "state-touched"');
+ 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"');
- state.dirty = true;
- await state.updateComplete;
- expect(state.classList.contains('state-dirty')).to.equal(true, 'has class "state-dirty"');
+ el.dirty = true;
+ await el.updateComplete;
+ expect(el.classList.contains('state-dirty')).to.equal(true, 'has class "state-dirty"');
+ });
+
+ it('sets an attribute "touched', async () => {
+ const el = await fixture(html`<${tag}>${tag}>`);
+ el.touched = true;
+ await el.updateComplete;
+ expect(el.hasAttribute('touched')).to.be.true;
+ });
+
+ it('sets an attribute "dirty', async () => {
+ const el = await fixture(html`<${tag}>${tag}>`);
+ el.dirty = true;
+ await el.updateComplete;
+ expect(el.hasAttribute('dirty')).to.be.true;
});
it('fires "(touched|dirty)-state-changed" event when state changes', async () => {
- const iState = await fixture(`<${elem}>${elem}>`);
- const cbTouched = sinon.spy();
- const cbDirty = sinon.spy();
+ const touchedSpy = sinon.spy();
+ const dirtySpy = sinon.spy();
+ const el = await fixture(
+ html`<${tag} @touched-changed=${touchedSpy} @dirty-changed=${dirtySpy}>${tag}>`,
+ );
- iState.addEventListener('touched-changed', cbTouched);
- iState.addEventListener('dirty-changed', cbDirty);
+ el.touched = true;
+ expect(touchedSpy.callCount).to.equal(1);
- iState.touched = true;
- expect(cbTouched.callCount).to.equal(1);
-
- iState.dirty = true;
- expect(cbDirty.callCount).to.equal(1);
- });
-
- // Skipping, since this issue (not being able to set focus on element extending from LitElement)
- // only occurs in WCT context (not in Storybook/Stackblitz).
- // See: https://stackblitz.com/edit/lit-element-request-update-bug-g59tjq?file=blurry.js
- // it.skip
- it('sets prefilled to true when field left and value non-empty', async () => {
- // const iState = await LionTest.htmlFixture(`<${elem}>${elem}>`);
- // await triggerFocusFor(iState.inputElement);
- // iState.modelValue = externalVariables.prefilledModelValue || '000';
- // await triggerBlurFor(iState.inputElement);
- // expect(iState.prefilled).to.equal(true);
- // await triggerFocusFor(iState.inputElement);
- // iState.modelValue = externalVariables.nonPrefilledModelValue || '';
- // await triggerBlurFor(iState.inputElement);
- // expect(iState.prefilled).to.equal(false);
+ el.dirty = true;
+ expect(dirtySpy.callCount).to.equal(1);
});
it('sets prefilled once instantiated', async () => {
- const tag = unsafeStatic(elem);
- const element = await fixture(html`
- <${tag}
- .modelValue=${'prefilled'}
- >${tag}>`);
- expect(element.prefilled).to.equal(true);
+ const el = await fixture(html`
+ <${tag} .modelValue=${'prefilled'}>${tag}>
+ `);
+ expect(el.prefilled).to.be.true;
const nonPrefilled = await fixture(html`
- <${tag}
- .modelValue=''}
- >${tag}>`);
- expect(nonPrefilled.prefilled).to.equal(false);
+ <${tag} .modelValue=${''}>${tag}>
+ `);
+ expect(nonPrefilled.prefilled).to.be.false;
});
// This method actually tests the implementation of the _isPrefilled method.
it(`can determine "prefilled" based on different modelValue types (Arrays, Objects, Numbers,
Booleans, Strings)`, async () => {
- const input = await fixture(`<${elem}>${elem}>`);
+ const el = await fixture(html`<${tag}>${tag}>`);
const changeModelValueAndLeave = modelValue => {
- input.dispatchEvent(new Event('focus', { bubbles: true }));
- input.modelValue = modelValue;
- input.dispatchEvent(new Event('blur', { bubbles: true }));
+ el.dispatchEvent(new Event('focus', { bubbles: true }));
+ el.modelValue = modelValue;
+ el.dispatchEvent(new Event('blur', { bubbles: true }));
};
// Prefilled
- changeModelValueAndLeave(input, ['bla']);
- expect(input.prefilled).to.equal(false, 'empty array should be considered "prefilled"');
- changeModelValueAndLeave(input, { bla: 'bla' });
- expect(input.prefilled).to.equal(false, 'empty object should be considered "prefilled"');
- changeModelValueAndLeave(input, 0);
- expect(input.prefilled).to.equal(false, 'numbers should be considered "prefilled"');
- changeModelValueAndLeave(input, false);
- expect(input.prefilled).to.equal(false, 'Booleans should be considered "prefilled"');
- changeModelValueAndLeave(input, '');
- expect(input.prefilled).to.equal(false, 'empty string should be considered "prefilled"');
+ changeModelValueAndLeave(['not-empty']);
+ expect(el.prefilled, 'not empty array should be "prefilled"').to.be.true;
+ changeModelValueAndLeave({ not: 'empty' });
+ expect(el.prefilled, 'not empty object should be "prefilled"').to.be.true;
+ changeModelValueAndLeave(0);
+ expect(el.prefilled, 'numbers should be "prefilled"').to.be.true;
+ changeModelValueAndLeave(false);
+ expect(el.prefilled, 'booleans should be "prefilled"').to.be.true;
// Not prefilled
- changeModelValueAndLeave(input, []);
- expect(input.prefilled).to.equal(false, 'empty array should not be considered "prefilled"');
- changeModelValueAndLeave(input, {});
- expect(input.prefilled).to.equal(false, 'empty object should not be considered "prefilled"');
- changeModelValueAndLeave(input, '');
- expect(input.prefilled).to.equal(false, 'empty string should not be considered "prefilled"');
-
- changeModelValueAndLeave(input, null);
- expect(input.prefilled).to.equal(false, 'null should not be considered "prefilled"');
- changeModelValueAndLeave(input, undefined);
- expect(input.prefilled).to.equal(false, 'undefined should not be considered "prefilled"');
+ changeModelValueAndLeave([]);
+ expect(el.prefilled, 'empty array should not be "prefilled"').to.be.false;
+ changeModelValueAndLeave({});
+ expect(el.prefilled, 'empty object should not be "prefilled"').to.be.false;
+ changeModelValueAndLeave('');
+ expect(el.prefilled, 'empty string should not be "prefilled"').to.be.false;
+ changeModelValueAndLeave(null);
+ expect(el.prefilled, 'null should not be "prefilled"').to.be.false;
+ changeModelValueAndLeave(undefined);
+ expect(el.prefilled, 'undefined should not be "prefilled"').to.be.false;
});
it('has a method resetInteractionState()', async () => {
- const input = await fixture(`<${elem}>${elem}>`);
- input.dirty = true;
- input.touched = true;
- input.prefilled = true;
- input.resetInteractionState();
- expect(input.dirty).to.equal(false);
- expect(input.touched).to.equal(false);
- expect(input.prefilled).to.equal(false);
+ const el = await fixture(html`<${tag}>${tag}>`);
+ el.dirty = true;
+ el.touched = true;
+ el.prefilled = true;
+ el.resetInteractionState();
+ expect(el.dirty).to.be.false;
+ expect(el.touched).to.be.false;
+ expect(el.prefilled).to.be.false;
- input.dirty = true;
- input.touched = true;
- input.prefilled = false;
- input.modelValue = 'Some value';
- input.resetInteractionState();
- expect(input.dirty).to.equal(false);
- expect(input.touched).to.equal(false);
- expect(input.prefilled).to.equal(true);
+ el.dirty = true;
+ el.touched = true;
+ el.prefilled = false;
+ el.modelValue = 'Some value';
+ el.resetInteractionState();
+ expect(el.dirty).to.be.false;
+ expect(el.touched).to.be.false;
+ expect(el.prefilled).to.be.true;
+ });
+
+ describe('SubClassers', () => {
+ it('can override the `_leaveEvent`', async () => {
+ const tagLeaveString = defineCE(
+ class IState extends InteractionStateMixin(LitElement) {
+ constructor() {
+ super();
+ this._leaveEvent = 'custom-blur';
+ }
+ },
+ );
+ const tagLeave = unsafeStatic(tagLeaveString);
+ const el = await fixture(html`<${tagLeave}>${tagLeave}>`);
+ el.dispatchEvent(new Event('custom-blur'));
+ expect(el.touched).to.be.true;
+ });
+
+ it('can override the deprecated `leaveEvent`', async () => {
+ const el = await fixture(html`<${tag} .leaveEvent=${'custom-blur'}>${tag}>`);
+ expect(el._leaveEvent).to.equal('custom-blur');
+ });
});
});
diff --git a/packages/validate/src/ValidateMixin.js b/packages/validate/src/ValidateMixin.js
index 0de305129..886e7d225 100644
--- a/packages/validate/src/ValidateMixin.js
+++ b/packages/validate/src/ValidateMixin.js
@@ -2,7 +2,6 @@
import { dedupeMixin, SlotMixin } from '@lion/core';
import { ObserverMixin } from '@lion/core/src/ObserverMixin.js';
-import { CssClassMixin } from '@lion/core/src/CssClassMixin.js';
import { localize, LocalizeMixin } from '@lion/localize';
import { Unparseable } from './Unparseable.js';
import { randomOk } from './validators.js';
@@ -15,7 +14,7 @@ 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 CssClassMixin(ObserverMixin(LocalizeMixin(SlotMixin(superclass)))) {
+ class ValidateMixin extends ObserverMixin(LocalizeMixin(SlotMixin(superclass))) {
/* * * * * * * * * *
Configuration */
@@ -120,11 +119,13 @@ export const ValidateMixin = dedupeMixin(
},
errorState: {
type: Boolean,
- nonEmptyToClass: 'state-error',
+ attribute: 'error-state',
+ reflect: true,
},
errorShow: {
type: Boolean,
- nonEmptyToClass: 'state-error-show',
+ attribute: 'error-show',
+ reflect: true,
},
warningValidators: {
type: Object,
@@ -134,11 +135,13 @@ export const ValidateMixin = dedupeMixin(
},
warningState: {
type: Boolean,
- nonEmptyToClass: 'state-warning',
+ attribute: 'warning-state',
+ reflect: true,
},
warningShow: {
type: Boolean,
- nonEmptyToClass: 'state-warning-show',
+ attribute: 'warning-show',
+ reflect: true,
},
infoValidators: {
type: Object,
@@ -148,11 +151,13 @@ export const ValidateMixin = dedupeMixin(
},
infoState: {
type: Boolean,
- nonEmptyToClass: 'state-info',
+ attribute: 'info-state',
+ reflect: true,
},
infoShow: {
type: Boolean,
- nonEmptyToClass: 'state-info-show',
+ attribute: 'info-show',
+ reflect: true,
},
successValidators: {
type: Object,
@@ -162,15 +167,17 @@ export const ValidateMixin = dedupeMixin(
},
successState: {
type: Boolean,
- nonEmptyToClass: 'state-success',
+ attribute: 'success-state',
+ reflect: true,
},
successShow: {
type: Boolean,
- nonEmptyToClass: 'state-success-show',
+ attribute: 'success-show',
+ reflect: true,
},
invalid: {
type: Boolean,
- nonEmptyToClass: 'state-invalid',
+ reflect: true,
},
message: {
type: Boolean,
@@ -236,6 +243,23 @@ 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);
diff --git a/packages/validate/test/ValidateMixin.test.js b/packages/validate/test/ValidateMixin.test.js
index 99dd36d44..f58896071 100644
--- a/packages/validate/test/ValidateMixin.test.js
+++ b/packages/validate/test/ValidateMixin.test.js
@@ -143,6 +143,100 @@ describe('ValidateMixin', () => {
expect(otherValidatorSpy.calledWith('foo')).to.equal(true);
});
+ // classes are added only for backward compatibility - they are deprecated
+ it('sets a class "state-(error|warning|info|success|invalid)"', async () => {
+ const el = await fixture(html`<${tag}>${tag}>`);
+ el.errorState = true;
+ await el.updateComplete;
+ expect(el.classList.contains('state-error')).to.equal(true, 'has class "state-error"');
+
+ el.warningState = true;
+ await el.updateComplete;
+ expect(el.classList.contains('state-warning')).to.equal(true, 'has class "state-warning"');
+
+ el.infoState = true;
+ await el.updateComplete;
+ expect(el.classList.contains('state-info')).to.equal(true, 'has class "state-info"');
+
+ el.successState = true;
+ await el.updateComplete;
+ expect(el.classList.contains('state-success')).to.equal(true, 'has class "state-success"');
+
+ el.invalid = true;
+ await el.updateComplete;
+ expect(el.classList.contains('state-invalid')).to.equal(true, 'has class "state-invalid"');
+ });
+
+ it('sets a class "state-(error|warning|info|success)-show"', async () => {
+ const el = await fixture(html`<${tag}>${tag}>`);
+ el.errorShow = true;
+ await el.updateComplete;
+ expect(el.classList.contains('state-error-show')).to.equal(
+ true,
+ 'has class "state-error-show"',
+ );
+
+ el.warningShow = true;
+ await el.updateComplete;
+ expect(el.classList.contains('state-warning-show')).to.equal(
+ true,
+ 'has class "state-warning-show"',
+ );
+
+ el.infoShow = true;
+ await el.updateComplete;
+ expect(el.classList.contains('state-info-show')).to.equal(true, 'has class "state-info-show"');
+
+ el.successShow = true;
+ await el.updateComplete;
+ expect(el.classList.contains('state-success-show')).to.equal(
+ true,
+ 'has class "state-success-show"',
+ );
+ });
+
+ it('sets attribute "(error|warning|info|success|invalid)-state"', async () => {
+ const el = await fixture(html`<${tag}>${tag}>`);
+ el.errorState = true;
+ await el.updateComplete;
+ expect(el.hasAttribute('error-state'), 'has error-state attribute').to.be.true;
+
+ el.warningState = true;
+ await el.updateComplete;
+ expect(el.hasAttribute('warning-state'), 'has warning-state attribute').to.be.true;
+
+ el.infoState = true;
+ await el.updateComplete;
+ expect(el.hasAttribute('info-state'), 'has info-state attribute').to.be.true;
+
+ el.successState = true;
+ await el.updateComplete;
+ expect(el.hasAttribute('success-state'), 'has error-state attribute').to.be.true;
+
+ el.invalid = true;
+ await el.updateComplete;
+ expect(el.hasAttribute('invalid'), 'has invalid attribute').to.be.true;
+ });
+
+ it('sets attribute "(error|warning|info|success)-show"', async () => {
+ const el = await fixture(html`<${tag}>${tag}>`);
+ el.errorShow = true;
+ await el.updateComplete;
+ expect(el.hasAttribute('error-show'), 'has error-show attribute').to.be.true;
+
+ el.warningShow = true;
+ await el.updateComplete;
+ expect(el.hasAttribute('warning-show'), 'has warning-show attribute').to.be.true;
+
+ el.infoShow = true;
+ await el.updateComplete;
+ expect(el.hasAttribute('info-show'), 'has info-show attribute').to.be.true;
+
+ el.successShow = true;
+ await el.updateComplete;
+ expect(el.hasAttribute('success-show'), 'has success-show attribute').to.be.true;
+ });
+
describe(`Validators ${suffixName}`, () => {
function isCat(modelValue, opts) {
const validateString = opts && opts.number ? `cat${opts.number}` : 'cat';