Merge pull request #228 from ing-bank/fix/fieldDelegation
Sync field properties instead of delegation and add integration tests for FormatMixin
This commit is contained in:
commit
5535d45419
41 changed files with 3020 additions and 1873 deletions
|
|
@ -1,9 +1,6 @@
|
||||||
import { css, html, DelegateMixin, SlotMixin, DisabledWithTabIndexMixin } from '@lion/core';
|
import { css, html, SlotMixin, DisabledWithTabIndexMixin, LitElement } from '@lion/core';
|
||||||
import { LionLitElement } from '@lion/core/src/LionLitElement.js';
|
|
||||||
|
|
||||||
export class LionButton extends DisabledWithTabIndexMixin(
|
export class LionButton extends DisabledWithTabIndexMixin(SlotMixin(LitElement)) {
|
||||||
DelegateMixin(SlotMixin(LionLitElement)),
|
|
||||||
) {
|
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
role: {
|
role: {
|
||||||
|
|
@ -14,6 +11,10 @@ export class LionButton extends DisabledWithTabIndexMixin(
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
reflect: true,
|
reflect: true,
|
||||||
},
|
},
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
reflect: true,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -107,14 +108,6 @@ export class LionButton extends DisabledWithTabIndexMixin(
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
get delegations() {
|
|
||||||
return {
|
|
||||||
...super.delegations,
|
|
||||||
target: () => this.$$slot('_button'),
|
|
||||||
attributes: ['type'],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
get slots() {
|
get slots() {
|
||||||
return {
|
return {
|
||||||
...super.slots,
|
...super.slots,
|
||||||
|
|
@ -129,9 +122,14 @@ export class LionButton extends DisabledWithTabIndexMixin(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get _nativeButtonNode() {
|
||||||
|
return this.querySelector('[slot=_button]');
|
||||||
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.role = 'button';
|
this.role = 'button';
|
||||||
|
this.type = 'submit';
|
||||||
this.active = false;
|
this.active = false;
|
||||||
this.__setupDelegationInConstructor();
|
this.__setupDelegationInConstructor();
|
||||||
}
|
}
|
||||||
|
|
@ -146,12 +144,22 @@ export class LionButton extends DisabledWithTabIndexMixin(
|
||||||
this.__teardownEvents();
|
this.__teardownEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updated(changedProperties) {
|
||||||
|
super.updated(changedProperties);
|
||||||
|
if (changedProperties.has('type')) {
|
||||||
|
const native = this._nativeButtonNode;
|
||||||
|
if (native) {
|
||||||
|
native.type = this.type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_redispatchClickEvent(oldEvent) {
|
_redispatchClickEvent(oldEvent) {
|
||||||
// replacing `MouseEvent` with `oldEvent.constructor` breaks IE
|
// replacing `MouseEvent` with `oldEvent.constructor` breaks IE
|
||||||
const newEvent = new MouseEvent(oldEvent.type, oldEvent);
|
const newEvent = new MouseEvent(oldEvent.type, oldEvent);
|
||||||
newEvent.__isRedispatchedOnNativeButton = true;
|
newEvent.__isRedispatchedOnNativeButton = true;
|
||||||
this.__enforceHostEventTarget(newEvent);
|
this.__enforceHostEventTarget(newEvent);
|
||||||
this.$$slot('_button').dispatchEvent(newEvent);
|
this._nativeButtonNode.dispatchEvent(newEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -27,23 +27,26 @@ describe('lion-button', () => {
|
||||||
expect(el.getAttribute('tabindex')).to.equal('0');
|
expect(el.getAttribute('tabindex')).to.equal('0');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has no type by default on the native button', async () => {
|
it('has .type="submit" and type="submit" by default', async () => {
|
||||||
const el = await fixture(`<lion-button>foo</lion-button>`);
|
const el = await fixture(`<lion-button>foo</lion-button>`);
|
||||||
const nativeButton = el.$$slot('_button');
|
expect(el.type).to.equal('submit');
|
||||||
expect(nativeButton.getAttribute('type')).to.be.null;
|
expect(el.getAttribute('type')).to.be.equal('submit');
|
||||||
|
expect(el._nativeButtonNode.type).to.equal('submit');
|
||||||
|
expect(el._nativeButtonNode.getAttribute('type')).to.be.equal('submit');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has type="submit" on the native button when set', async () => {
|
it('sync type down to the native button', async () => {
|
||||||
const el = await fixture(`<lion-button type="submit">foo</lion-button>`);
|
const el = await fixture(`<lion-button type="button">foo</lion-button>`);
|
||||||
const nativeButton = el.$$slot('_button');
|
expect(el.type).to.equal('button');
|
||||||
expect(nativeButton.getAttribute('type')).to.equal('submit');
|
expect(el.getAttribute('type')).to.be.equal('button');
|
||||||
|
expect(el._nativeButtonNode.type).to.equal('button');
|
||||||
|
expect(el._nativeButtonNode.getAttribute('type')).to.be.equal('button');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('hides the native button in the UI', async () => {
|
it('hides the native button in the UI', async () => {
|
||||||
const el = await fixture(`<lion-button>foo</lion-button>`);
|
const el = await fixture(`<lion-button>foo</lion-button>`);
|
||||||
const nativeButton = el.$$slot('_button');
|
expect(el._nativeButtonNode.getAttribute('tabindex')).to.equal('-1');
|
||||||
expect(nativeButton.getAttribute('tabindex')).to.equal('-1');
|
expect(window.getComputedStyle(el._nativeButtonNode).visibility).to.equal('hidden');
|
||||||
expect(window.getComputedStyle(nativeButton).visibility).to.equal('hidden');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can be disabled imperatively', async () => {
|
it('can be disabled imperatively', async () => {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ export const ChoiceInputMixin = superclass =>
|
||||||
class ChoiceInputMixin extends FormatMixin(superclass) {
|
class ChoiceInputMixin extends FormatMixin(superclass) {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
...super.properties,
|
|
||||||
/**
|
/**
|
||||||
* Boolean indicating whether or not this element is checked by the end user.
|
* Boolean indicating whether or not this element is checked by the end user.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ export const FieldCustomMixin = dedupeMixin(
|
||||||
class FieldCustomMixin extends superclass {
|
class FieldCustomMixin extends superclass {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
...super.properties,
|
|
||||||
/**
|
/**
|
||||||
* When no light dom defined and prop set
|
* When no light dom defined and prop set
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -1,52 +1,120 @@
|
||||||
import { dedupeMixin, DelegateMixin } from '@lion/core';
|
import { dedupeMixin } from '@lion/core';
|
||||||
|
|
||||||
export const FocusMixin = dedupeMixin(
|
export const FocusMixin = dedupeMixin(
|
||||||
superclass =>
|
superclass =>
|
||||||
// eslint-disable-next-line no-unused-vars, max-len, no-shadow
|
// eslint-disable-next-line no-unused-vars, max-len, no-shadow
|
||||||
class FocusMixin extends DelegateMixin(superclass) {
|
class FocusMixin extends superclass {
|
||||||
get delegations() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
...super.delegations,
|
focused: {
|
||||||
target: () => this.inputElement,
|
type: Boolean,
|
||||||
events: [...super.delegations.events, 'focus', 'blur'], // since these events don't bubble
|
reflect: true,
|
||||||
methods: [...super.delegations.methods, 'focus', 'blur'],
|
},
|
||||||
properties: [...super.delegations.properties, 'onfocus', 'onblur', 'autofocus'],
|
|
||||||
attributes: [...super.delegations.attributes, 'onfocus', 'onblur', 'autofocus'],
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.focused = false;
|
||||||
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
super.connectedCallback();
|
if (super.connectedCallback) {
|
||||||
this._onFocus = this._onFocus.bind(this);
|
super.connectedCallback();
|
||||||
this._onBlur = this._onBlur.bind(this);
|
}
|
||||||
this.inputElement.addEventListener('focusin', this._onFocus);
|
this.__registerEventsForFocusMixin();
|
||||||
this.inputElement.addEventListener('focusout', this._onBlur);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
super.disconnectedCallback();
|
if (super.disconnectedCallback) {
|
||||||
this.inputElement.removeEventListener('focusin', this._onFocus);
|
super.disconnectedCallback();
|
||||||
this.inputElement.removeEventListener('focusout', this._onBlur);
|
}
|
||||||
|
this.__teardownEventsForFocusMixin();
|
||||||
|
}
|
||||||
|
|
||||||
|
focus() {
|
||||||
|
const native = this.inputElement;
|
||||||
|
if (native) {
|
||||||
|
native.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
blur() {
|
||||||
|
const native = this.inputElement;
|
||||||
|
if (native) {
|
||||||
|
native.blur();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updated(changedProperties) {
|
||||||
|
super.updated(changedProperties);
|
||||||
|
// 'state-focused' css classes are deprecated
|
||||||
|
if (changedProperties.has('focused')) {
|
||||||
|
this.classList[this.focused ? 'add' : 'remove']('state-focused');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper Function to easily check if the element is being focused
|
* Functions should be private
|
||||||
*
|
*
|
||||||
* TODO: performance comparision vs
|
* @deprecated
|
||||||
* return this.inputElement === document.activeElement;
|
|
||||||
*/
|
*/
|
||||||
get focused() {
|
|
||||||
return this.classList.contains('state-focused');
|
|
||||||
}
|
|
||||||
|
|
||||||
_onFocus() {
|
_onFocus() {
|
||||||
if (super._onFocus) super._onFocus();
|
if (super._onFocus) {
|
||||||
this.classList.add('state-focused');
|
super._onFocus();
|
||||||
|
}
|
||||||
|
this.focused = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Functions should be private
|
||||||
|
*
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
_onBlur() {
|
_onBlur() {
|
||||||
if (super._onBlur) super._onBlur();
|
if (super._onBlur) {
|
||||||
this.classList.remove('state-focused');
|
super._onBlur();
|
||||||
|
}
|
||||||
|
this.focused = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
__registerEventsForFocusMixin() {
|
||||||
|
// focus
|
||||||
|
this.__redispatchFocus = ev => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
this.dispatchEvent(new FocusEvent('focus'));
|
||||||
|
};
|
||||||
|
this.inputElement.addEventListener('focus', this.__redispatchFocus);
|
||||||
|
|
||||||
|
// blur
|
||||||
|
this.__redispatchBlur = ev => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
this.dispatchEvent(new FocusEvent('blur'));
|
||||||
|
};
|
||||||
|
this.inputElement.addEventListener('blur', this.__redispatchBlur);
|
||||||
|
|
||||||
|
// focusin
|
||||||
|
this.__redispatchFocusin = ev => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
this._onFocus(ev);
|
||||||
|
this.dispatchEvent(new FocusEvent('focusin', { bubbles: true, composed: true }));
|
||||||
|
};
|
||||||
|
this.inputElement.addEventListener('focusin', this.__redispatchFocusin);
|
||||||
|
|
||||||
|
// focusout
|
||||||
|
this.__redispatchFocusout = ev => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
this._onBlur();
|
||||||
|
this.dispatchEvent(new FocusEvent('focusout', { bubbles: true, composed: true }));
|
||||||
|
};
|
||||||
|
this.inputElement.addEventListener('focusout', this.__redispatchFocusout);
|
||||||
|
}
|
||||||
|
|
||||||
|
__teardownEventsForFocusMixin() {
|
||||||
|
this.inputElement.removeEventListener('focus', this.__redispatchFocus);
|
||||||
|
this.inputElement.removeEventListener('blur', this.__redispatchBlur);
|
||||||
|
this.inputElement.removeEventListener('focusin', this.__redispatchFocusin);
|
||||||
|
this.inputElement.removeEventListener('focusout', this.__redispatchFocusout);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ export const FormControlMixin = dedupeMixin(
|
||||||
class FormControlMixin extends FormRegisteringMixin(ObserverMixin(SlotMixin(superclass))) {
|
class FormControlMixin extends FormRegisteringMixin(ObserverMixin(SlotMixin(superclass))) {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
...super.properties,
|
|
||||||
/**
|
/**
|
||||||
* A list of ids that will be put on the inputElement as a serialized string
|
* A list of ids that will be put on the inputElement as a serialized string
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -53,8 +53,6 @@ export const FormatMixin = dedupeMixin(
|
||||||
class FormatMixin extends ObserverMixin(superclass) {
|
class FormatMixin extends ObserverMixin(superclass) {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
...super.properties,
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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.
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ export const InteractionStateMixin = dedupeMixin(
|
||||||
class InteractionStateMixin extends ObserverMixin(superclass) {
|
class InteractionStateMixin extends ObserverMixin(superclass) {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
...super.properties,
|
|
||||||
/**
|
/**
|
||||||
* True when user has focused and left(blurred) the field.
|
* True when user has focused and left(blurred) the field.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { DelegateMixin, SlotMixin, LitElement } from '@lion/core';
|
import { SlotMixin, LitElement } from '@lion/core';
|
||||||
import { ElementMixin } from '@lion/core/src/ElementMixin.js';
|
import { ElementMixin } from '@lion/core/src/ElementMixin.js';
|
||||||
import { CssClassMixin } from '@lion/core/src/CssClassMixin.js';
|
import { DisabledMixin } from '@lion/core/src/DisabledMixin.js';
|
||||||
import { ObserverMixin } from '@lion/core/src/ObserverMixin.js';
|
import { ObserverMixin } from '@lion/core/src/ObserverMixin.js';
|
||||||
import { ValidateMixin } from '@lion/validate';
|
import { ValidateMixin } from '@lion/validate';
|
||||||
import { FormControlMixin } from './FormControlMixin.js';
|
import { FormControlMixin } from './FormControlMixin.js';
|
||||||
|
|
@ -35,40 +35,53 @@ import { FocusMixin } from './FocusMixin.js';
|
||||||
export class LionField extends FormControlMixin(
|
export class LionField extends FormControlMixin(
|
||||||
InteractionStateMixin(
|
InteractionStateMixin(
|
||||||
FocusMixin(
|
FocusMixin(
|
||||||
FormatMixin(
|
FormatMixin(ValidateMixin(DisabledMixin(ElementMixin(SlotMixin(ObserverMixin(LitElement)))))),
|
||||||
ValidateMixin(
|
|
||||||
CssClassMixin(ElementMixin(DelegateMixin(SlotMixin(ObserverMixin(LitElement))))),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
get delegations() {
|
|
||||||
return {
|
|
||||||
...super.delegations,
|
|
||||||
target: () => this.inputElement,
|
|
||||||
properties: [
|
|
||||||
...super.delegations.properties,
|
|
||||||
'name',
|
|
||||||
'type',
|
|
||||||
'disabled',
|
|
||||||
'selectionStart',
|
|
||||||
'selectionEnd',
|
|
||||||
],
|
|
||||||
attributes: [...super.delegations.attributes, 'name', 'type', 'disabled'],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
...super.properties,
|
|
||||||
submitted: {
|
submitted: {
|
||||||
// make sure validation can be triggered based on observer
|
// make sure validation can be triggered based on observer
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
},
|
},
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
reflect: true,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get selectionStart() {
|
||||||
|
const native = this.inputElement;
|
||||||
|
if (native && native.selectionStart) {
|
||||||
|
return native.selectionStart;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
set selectionStart(value) {
|
||||||
|
const native = this.inputElement;
|
||||||
|
if (native && native.selectionStart) {
|
||||||
|
native.selectionStart = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get selectionEnd() {
|
||||||
|
const native = this.inputElement;
|
||||||
|
if (native && native.selectionEnd) {
|
||||||
|
return native.selectionEnd;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
set selectionEnd(value) {
|
||||||
|
const native = this.inputElement;
|
||||||
|
if (native && native.selectionEnd) {
|
||||||
|
native.selectionEnd = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// We don't delegate, because we want to preserve caret position via _setValueAndPreserveCaret
|
// We don't delegate, because we want to preserve caret position via _setValueAndPreserveCaret
|
||||||
set value(value) {
|
set value(value) {
|
||||||
// if not yet connected to dom can't change the value
|
// if not yet connected to dom can't change the value
|
||||||
|
|
@ -82,20 +95,23 @@ export class LionField extends FormControlMixin(
|
||||||
return (this.inputElement && this.inputElement.value) || '';
|
return (this.inputElement && this.inputElement.value) || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
static get asyncObservers() {
|
constructor() {
|
||||||
return {
|
super();
|
||||||
...super.asyncObservers,
|
this.name = '';
|
||||||
_setDisabledClass: ['disabled'],
|
this.submitted = false;
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
|
// TODO: Normally we put super calls on top for predictability,
|
||||||
|
// here we temporarily need to do attribute delegation before,
|
||||||
|
// so the FormatMixin uses the right value. Should be solved
|
||||||
|
// when value delegation is part of the calculation loop of
|
||||||
|
// FormatMixin
|
||||||
|
this._delegateInitialValueAttr();
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
|
|
||||||
this._onChange = this._onChange.bind(this);
|
this._onChange = this._onChange.bind(this);
|
||||||
this.inputElement.addEventListener('change', this._onChange);
|
this.inputElement.addEventListener('change', this._onChange);
|
||||||
this._delegateInitialValueAttr();
|
|
||||||
this._setDisabledClass();
|
|
||||||
this.classList.add('form-field'); // eslint-disable-line
|
this.classList.add('form-field'); // eslint-disable-line
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -112,8 +128,22 @@ export class LionField extends FormControlMixin(
|
||||||
this.inputElement.removeEventListener('change', this._onChange);
|
this.inputElement.removeEventListener('change', this._onChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
_setDisabledClass() {
|
updated(changedProps) {
|
||||||
this.classList[this.disabled ? 'add' : 'remove']('state-disabled');
|
super.updated(changedProps);
|
||||||
|
|
||||||
|
if (changedProps.has('disabled')) {
|
||||||
|
if (this.disabled) {
|
||||||
|
this.inputElement.disabled = true;
|
||||||
|
this.classList.add('state-disabled'); // eslint-disable-line wc/no-self-class
|
||||||
|
} else {
|
||||||
|
this.inputElement.disabled = false;
|
||||||
|
this.classList.remove('state-disabled'); // eslint-disable-line wc/no-self-class
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changedProps.has('name')) {
|
||||||
|
this.inputElement.name = this.name;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
401
packages/field/test-suites/FormatMixin.suite.js
Normal file
401
packages/field/test-suites/FormatMixin.suite.js
Normal file
|
|
@ -0,0 +1,401 @@
|
||||||
|
import { expect, fixture, html, aTimeout, defineCE, unsafeStatic } from '@open-wc/testing';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
|
||||||
|
import { LitElement } from '@lion/core';
|
||||||
|
import { Unparseable } from '@lion/validate';
|
||||||
|
import { FormatMixin } from '../src/FormatMixin.js';
|
||||||
|
|
||||||
|
function mimicUserInput(formControl, newViewValue) {
|
||||||
|
formControl.value = newViewValue; // eslint-disable-line no-param-reassign
|
||||||
|
formControl.inputElement.dispatchEvent(new CustomEvent('input', { bubbles: true }));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function runFormatMixinSuite(customConfig) {
|
||||||
|
// TODO: Maybe remove suffix
|
||||||
|
const cfg = {
|
||||||
|
tagString: null,
|
||||||
|
modelValueType: String,
|
||||||
|
suffix: '',
|
||||||
|
...customConfig,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mocks a value for you based on the data type
|
||||||
|
* Optionally toggles you a different value
|
||||||
|
* for needing to mimic a value-change.
|
||||||
|
*
|
||||||
|
* TODO: The FormatMixin can know about platform types like
|
||||||
|
* Date, but not about modelValue of input-iban etc.
|
||||||
|
* Make this concept expandable by allowing 'non standard'
|
||||||
|
* modelValues to hook into this.
|
||||||
|
*/
|
||||||
|
function generateValueBasedOnType(opts = {}) {
|
||||||
|
const options = { type: cfg.modelValueType, toggleValue: false, viewValue: false, ...opts };
|
||||||
|
|
||||||
|
switch (options.type) {
|
||||||
|
case Number:
|
||||||
|
return !options.toggleValue ? '123' : '456';
|
||||||
|
case Date:
|
||||||
|
// viewValue instead of modelValue, since a Date object is unparseable.
|
||||||
|
// Note: this viewValue needs to point to the same date as the
|
||||||
|
// default returned modelValue.
|
||||||
|
if (options.viewValue) {
|
||||||
|
return !options.toggleValue ? '5-5-2005' : '10-10-2010';
|
||||||
|
}
|
||||||
|
return !options.toggleValue ? new Date('5-5-2005') : new Date('10-10-2010');
|
||||||
|
case Array:
|
||||||
|
return !options.toggleValue ? ['foo', 'bar'] : ['baz', 'yay'];
|
||||||
|
case Object:
|
||||||
|
return !options.toggleValue ? { foo: 'bar' } : { bar: 'foo' };
|
||||||
|
case Boolean:
|
||||||
|
return !options.toggleValue;
|
||||||
|
case 'email':
|
||||||
|
return !options.toggleValue ? 'some-user@ing.com' : 'another-user@ing.com';
|
||||||
|
case 'iban':
|
||||||
|
return !options.toggleValue ? 'SE3550000000054910000003' : 'CH9300762011623852957';
|
||||||
|
default:
|
||||||
|
return !options.toggleValue ? 'foo' : 'bar';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe(`FormatMixin ${cfg.suffix ? `(${cfg.suffix})` : ''}`, async () => {
|
||||||
|
let elem;
|
||||||
|
let nonFormat;
|
||||||
|
let fooFormat;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
if (!cfg.tagString) {
|
||||||
|
cfg.tagString = defineCE(
|
||||||
|
class extends FormatMixin(LitElement) {
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
<slot name="input"></slot>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
set value(newValue) {
|
||||||
|
this.inputElement.value = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
get value() {
|
||||||
|
return this.inputElement.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
get inputElement() {
|
||||||
|
return this.querySelector('input');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
elem = unsafeStatic(cfg.tagString);
|
||||||
|
nonFormat = await fixture(html`<${elem}
|
||||||
|
.formatter="${v => v}"
|
||||||
|
.parser="${v => v}"
|
||||||
|
.serializer="${v => v}"
|
||||||
|
.deserializer="${v => v}"
|
||||||
|
><input slot="input">
|
||||||
|
</${elem}>`);
|
||||||
|
fooFormat = await fixture(html`
|
||||||
|
<${elem}
|
||||||
|
.formatter="${value => `foo: ${value}`}"
|
||||||
|
.parser="${value => value.replace('foo: ', '')}"
|
||||||
|
.serializer="${value => `[foo] ${value}`}"
|
||||||
|
.deserializer="${value => value.replace('[foo] ', '')}"
|
||||||
|
><input slot="input">
|
||||||
|
</${elem}>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fires `model-value-changed` for every change on the input', async () => {
|
||||||
|
const formatEl = await fixture(html`<${elem}><input slot="input"></${elem}>`);
|
||||||
|
|
||||||
|
let counter = 0;
|
||||||
|
formatEl.addEventListener('model-value-changed', () => {
|
||||||
|
counter += 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
mimicUserInput(formatEl, generateValueBasedOnType());
|
||||||
|
expect(counter).to.equal(1);
|
||||||
|
|
||||||
|
// Counter offset +1 for Date because parseDate created a new Date object
|
||||||
|
// when the user changes the value.
|
||||||
|
// This will result in a model-value-changed trigger even if the user value was the same
|
||||||
|
// TODO: a proper solution would be to add `hasChanged` to input-date, like isSameDate()
|
||||||
|
// from calendar utils
|
||||||
|
const counterOffset = cfg.modelValueType === Date ? 1 : 0;
|
||||||
|
|
||||||
|
mimicUserInput(formatEl, generateValueBasedOnType());
|
||||||
|
expect(counter).to.equal(1 + counterOffset);
|
||||||
|
|
||||||
|
mimicUserInput(formatEl, generateValueBasedOnType({ toggleValue: true }));
|
||||||
|
expect(counter).to.equal(2 + counterOffset);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fires `model-value-changed` for every modelValue change', async () => {
|
||||||
|
const el = await fixture(html`<${elem}><input slot="input"></${elem}>`);
|
||||||
|
let counter = 0;
|
||||||
|
el.addEventListener('model-value-changed', () => {
|
||||||
|
counter += 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
el.modelValue = 'one';
|
||||||
|
expect(counter).to.equal(1);
|
||||||
|
|
||||||
|
// no change means no event
|
||||||
|
el.modelValue = 'one';
|
||||||
|
expect(counter).to.equal(1);
|
||||||
|
|
||||||
|
el.modelValue = 'two';
|
||||||
|
expect(counter).to.equal(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has modelValue, formattedValue and serializedValue which are computed synchronously', async () => {
|
||||||
|
expect(nonFormat.modelValue).to.equal('', 'modelValue initially');
|
||||||
|
expect(nonFormat.formattedValue).to.equal('', 'formattedValue initially');
|
||||||
|
expect(nonFormat.serializedValue).to.equal('', 'serializedValue initially');
|
||||||
|
const generatedValue = generateValueBasedOnType();
|
||||||
|
nonFormat.modelValue = generatedValue;
|
||||||
|
expect(nonFormat.modelValue).to.equal(generatedValue, 'modelValue as provided');
|
||||||
|
expect(nonFormat.formattedValue).to.equal(generatedValue, 'formattedValue synchronized');
|
||||||
|
expect(nonFormat.serializedValue).to.equal(generatedValue, 'serializedValue synchronized');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has an input node (like <input>/<textarea>) which holds the formatted (view) value', async () => {
|
||||||
|
fooFormat.modelValue = 'string';
|
||||||
|
expect(fooFormat.formattedValue).to.equal('foo: string');
|
||||||
|
expect(fooFormat.value).to.equal('foo: string');
|
||||||
|
expect(fooFormat.inputElement.value).to.equal('foo: string');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('converts modelValue => formattedValue (via this.formatter)', async () => {
|
||||||
|
fooFormat.modelValue = 'string';
|
||||||
|
expect(fooFormat.formattedValue).to.equal('foo: string');
|
||||||
|
expect(fooFormat.serializedValue).to.equal('[foo] string');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('converts modelValue => serializedValue (via this.serializer)', async () => {
|
||||||
|
fooFormat.modelValue = 'string';
|
||||||
|
expect(fooFormat.serializedValue).to.equal('[foo] string');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('converts formattedValue => modelValue (via this.parser)', async () => {
|
||||||
|
fooFormat.formattedValue = 'foo: string';
|
||||||
|
expect(fooFormat.modelValue).to.equal('string');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('converts serializedValue => modelValue (via this.deserializer)', async () => {
|
||||||
|
fooFormat.serializedValue = '[foo] string';
|
||||||
|
expect(fooFormat.modelValue).to.equal('string');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('synchronizes inputElement.value as a fallback mechanism', async () => {
|
||||||
|
// Note that in lion-field, the attribute would be put on <lion-field>, not on <input>
|
||||||
|
const formatElem = await fixture(html`
|
||||||
|
<${elem}
|
||||||
|
value="string",
|
||||||
|
.formatter=${value => `foo: ${value}`}
|
||||||
|
.parser=${value => value.replace('foo: ', '')}
|
||||||
|
.serializer=${value => `[foo] ${value}`}
|
||||||
|
.deserializer=${value => value.replace('[foo] ', '')}
|
||||||
|
><input slot="input" value="string"/></${elem}>`);
|
||||||
|
// Now check if the format/parse/serialize loop has been triggered
|
||||||
|
await formatElem.updateComplete;
|
||||||
|
expect(formatElem.formattedValue).to.equal('foo: string');
|
||||||
|
|
||||||
|
expect(formatElem.inputElement.value).to.equal('foo: string');
|
||||||
|
|
||||||
|
expect(formatElem.serializedValue).to.equal('[foo] string');
|
||||||
|
expect(formatElem.modelValue).to.equal('string');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reflects back formatted value to user on leave', async () => {
|
||||||
|
const formatEl = await fixture(html`
|
||||||
|
<${elem} .formatter="${value => `foo: ${value}`}">
|
||||||
|
<input slot="input" />
|
||||||
|
</${elem}>
|
||||||
|
`);
|
||||||
|
|
||||||
|
const generatedViewValue = generateValueBasedOnType({ viewValue: true });
|
||||||
|
const generatedModelValue = generateValueBasedOnType();
|
||||||
|
mimicUserInput(formatEl, generatedViewValue);
|
||||||
|
expect(formatEl.inputElement.value).to.not.equal(`foo: ${generatedModelValue}`);
|
||||||
|
|
||||||
|
// user leaves field
|
||||||
|
formatEl.inputElement.dispatchEvent(new CustomEvent(formatEl.formatOn, { bubbles: true }));
|
||||||
|
await aTimeout();
|
||||||
|
expect(formatEl.inputElement.value).to.equal(`foo: ${generatedModelValue}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reflects back .formattedValue immediately when .modelValue changed imperatively', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<${elem} .formatter="${value => `foo: ${value}`}">
|
||||||
|
<input slot="input" />
|
||||||
|
</${elem}>
|
||||||
|
`);
|
||||||
|
// The FormatMixin can be used in conjunction with the ValidateMixin, in which case
|
||||||
|
// it can hold errorState (affecting the formatting)
|
||||||
|
el.errorState = true;
|
||||||
|
|
||||||
|
// users types value 'test'
|
||||||
|
mimicUserInput(el, 'test');
|
||||||
|
expect(el.inputElement.value).to.not.equal('foo: test');
|
||||||
|
|
||||||
|
// Now see the difference for an imperative change
|
||||||
|
el.modelValue = 'test2';
|
||||||
|
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}`);
|
||||||
|
const parserSpy = sinon.spy(value => value.replace('foo: ', ''));
|
||||||
|
const serializerSpy = sinon.spy(value => `[foo] ${value}`);
|
||||||
|
const el = await fixture(html`
|
||||||
|
<${elem}
|
||||||
|
.formatter=${formatterSpy}
|
||||||
|
.parser=${parserSpy}
|
||||||
|
.serializer=${serializerSpy}
|
||||||
|
.modelValue=${'test'}
|
||||||
|
>
|
||||||
|
<input slot="input">
|
||||||
|
</${elem}>
|
||||||
|
`);
|
||||||
|
expect(formatterSpy.called).to.equal(true);
|
||||||
|
expect(serializerSpy.called).to.equal(true);
|
||||||
|
|
||||||
|
el.formattedValue = 'raw';
|
||||||
|
expect(parserSpy.called).to.equal(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have formatOptions available in formatter', async () => {
|
||||||
|
const formatterSpy = sinon.spy(value => `foo: ${value}`);
|
||||||
|
const generatedViewValue = generateValueBasedOnType({ viewValue: true });
|
||||||
|
await fixture(html`
|
||||||
|
<${elem}
|
||||||
|
value="${generatedViewValue}"
|
||||||
|
.formatter="${formatterSpy}"
|
||||||
|
.formatOptions="${{ locale: 'en-GB', decimalSeparator: '-' }}">
|
||||||
|
<input slot="input" value="${generatedViewValue}">
|
||||||
|
</${elem}>
|
||||||
|
`);
|
||||||
|
expect(formatterSpy.args[0][1].locale).to.equal('en-GB');
|
||||||
|
expect(formatterSpy.args[0][1].decimalSeparator).to.equal('-');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('will only call the parser for defined values', async () => {
|
||||||
|
const generatedValue = generateValueBasedOnType();
|
||||||
|
const parserSpy = sinon.spy();
|
||||||
|
const el = await fixture(html`
|
||||||
|
<${elem} .parser="${parserSpy}">
|
||||||
|
<input slot="input" value="${generatedValue}">
|
||||||
|
</${elem}>
|
||||||
|
`);
|
||||||
|
|
||||||
|
expect(parserSpy.callCount).to.equal(1);
|
||||||
|
// This could happen for instance in a reset
|
||||||
|
el.modelValue = undefined;
|
||||||
|
expect(parserSpy.callCount).to.equal(1);
|
||||||
|
// This could happen when the user erases the input value
|
||||||
|
mimicUserInput(el, '');
|
||||||
|
expect(parserSpy.callCount).to.equal(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('will not return Unparseable when empty strings are inputted', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<${elem}>
|
||||||
|
<input slot="input" value="string">
|
||||||
|
</${elem}>
|
||||||
|
`);
|
||||||
|
// This could happen when the user erases the input value
|
||||||
|
mimicUserInput(el, '');
|
||||||
|
// For backwards compatibility, we keep the modelValue an empty string here.
|
||||||
|
// Undefined would be more appropriate 'conceptually', however
|
||||||
|
expect(el.modelValue).to.equal('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('will only call the formatter for valid values on `user-input-changed` ', async () => {
|
||||||
|
const formatterSpy = sinon.spy(value => `foo: ${value}`);
|
||||||
|
|
||||||
|
const generatedModelValue = generateValueBasedOnType();
|
||||||
|
const generatedViewValue = generateValueBasedOnType({ viewValue: true });
|
||||||
|
const generatedViewValueAlt = generateValueBasedOnType({
|
||||||
|
viewValue: true,
|
||||||
|
toggleValue: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const el = await fixture(html`
|
||||||
|
<${elem} .formatter=${formatterSpy}>
|
||||||
|
<input slot="input" value="${generatedViewValue}">
|
||||||
|
</${elem}>
|
||||||
|
`);
|
||||||
|
expect(formatterSpy.callCount).to.equal(1);
|
||||||
|
|
||||||
|
el.errorState = true;
|
||||||
|
// Ensure errorState is always true by putting a validator on it that always returns false.
|
||||||
|
// Setting errorState = true is not enough if the element has errorValidators (uses ValidateMixin)
|
||||||
|
// that set errorState back to false when the user input is mimicked.
|
||||||
|
const alwaysInvalidator = () => ({ 'always-invalid': false });
|
||||||
|
el.errorValidators = [alwaysInvalidator];
|
||||||
|
mimicUserInput(el, generatedViewValueAlt);
|
||||||
|
|
||||||
|
expect(formatterSpy.callCount).to.equal(1);
|
||||||
|
// Due to errorState, the formatter should not have ran.
|
||||||
|
expect(el.formattedValue).to.equal(generatedViewValueAlt);
|
||||||
|
|
||||||
|
el.errorState = false;
|
||||||
|
el.errorValidators = [];
|
||||||
|
mimicUserInput(el, generatedViewValue);
|
||||||
|
expect(formatterSpy.callCount).to.equal(2);
|
||||||
|
|
||||||
|
expect(el.formattedValue).to.equal(`foo: ${generatedModelValue}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Unparseable values', () => {
|
||||||
|
it('should convert to Unparseable when wrong value inputted by user', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<${elem}
|
||||||
|
.parser=${viewValue => Number(viewValue) || undefined}
|
||||||
|
>
|
||||||
|
<input slot="input">
|
||||||
|
</${elem}>
|
||||||
|
`);
|
||||||
|
mimicUserInput(el, 'test');
|
||||||
|
expect(el.modelValue).to.be.an.instanceof(Unparseable);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve the viewValue when not parseable', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<${elem}
|
||||||
|
.parser=${viewValue => Number(viewValue) || undefined}
|
||||||
|
>
|
||||||
|
<input slot="input">
|
||||||
|
</${elem}>
|
||||||
|
`);
|
||||||
|
mimicUserInput(el, 'test');
|
||||||
|
expect(el.formattedValue).to.equal('test');
|
||||||
|
expect(el.value).to.equal('test');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display the viewValue when modelValue is of type Unparseable', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<${elem}
|
||||||
|
.parser=${viewValue => Number(viewValue) || undefined}
|
||||||
|
>
|
||||||
|
<input slot="input">
|
||||||
|
</${elem}>
|
||||||
|
`);
|
||||||
|
el.modelValue = new Unparseable('foo');
|
||||||
|
expect(el.value).to.equal('foo');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -11,7 +11,6 @@ describe('FieldCustomMixin', () => {
|
||||||
const FieldCustomMixinClass = class extends FieldCustomMixin(LionField) {
|
const FieldCustomMixinClass = class extends FieldCustomMixin(LionField) {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
...super.properties,
|
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
117
packages/field/test/FocusMixin.test.js
Normal file
117
packages/field/test/FocusMixin.test.js
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
import { expect, fixture, html, defineCE, unsafeStatic, oneEvent } from '@open-wc/testing';
|
||||||
|
|
||||||
|
import { LitElement } from '@lion/core';
|
||||||
|
import { FocusMixin } from '../src/FocusMixin.js';
|
||||||
|
|
||||||
|
describe('FocusMixin', () => {
|
||||||
|
let tag;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
const tagString = defineCE(
|
||||||
|
class extends FocusMixin(LitElement) {
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
<slot name="input"></slot>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
get inputElement() {
|
||||||
|
return this.querySelector('input');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
tag = unsafeStatic(tagString);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('focuses/blurs the underlaying native element on .focus()/.blur()', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<${tag}><input slot="input"></${tag}>
|
||||||
|
`);
|
||||||
|
el.focus();
|
||||||
|
expect(document.activeElement === el.inputElement).to.be.true;
|
||||||
|
el.blur();
|
||||||
|
expect(document.activeElement === el.inputElement).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has an attribute focused when focused', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<${tag}><input slot="input"></${tag}>
|
||||||
|
`);
|
||||||
|
el.focus();
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(el.hasAttribute('focused')).to.be.true;
|
||||||
|
|
||||||
|
el.blur();
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(el.hasAttribute('focused')).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('becomes focused/blurred if the native element gets focused/blurred', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<${tag}><input slot="input"></${tag}>
|
||||||
|
`);
|
||||||
|
expect(el.focused).to.be.false;
|
||||||
|
el.inputElement.focus();
|
||||||
|
expect(el.focused).to.be.true;
|
||||||
|
el.inputElement.blur();
|
||||||
|
expect(el.focused).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has a deprecated "state-focused" css class when focused', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<${tag}><input slot="input"></${tag}>
|
||||||
|
`);
|
||||||
|
el.focus();
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(el.classList.contains('state-focused')).to.be.true;
|
||||||
|
|
||||||
|
el.blur();
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(el.classList.contains('state-focused')).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('dispatches [focus, blur] events', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<${tag}><input slot="input"></${tag}>
|
||||||
|
`);
|
||||||
|
setTimeout(() => el.focus());
|
||||||
|
const focusEv = await oneEvent(el, 'focus');
|
||||||
|
expect(focusEv).to.be.instanceOf(FocusEvent);
|
||||||
|
expect(focusEv.target).to.equal(el);
|
||||||
|
expect(focusEv.bubbles).to.be.false;
|
||||||
|
expect(focusEv.composed).to.be.false;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
el.focus();
|
||||||
|
el.blur();
|
||||||
|
});
|
||||||
|
const blurEv = await oneEvent(el, 'blur');
|
||||||
|
expect(blurEv).to.be.instanceOf(FocusEvent);
|
||||||
|
expect(blurEv.target).to.equal(el);
|
||||||
|
expect(blurEv.bubbles).to.be.false;
|
||||||
|
expect(blurEv.composed).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('dispatches [focusin, focusout] events with { bubbles: true, composed: true }', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<${tag}><input slot="input"></${tag}>
|
||||||
|
`);
|
||||||
|
setTimeout(() => el.focus());
|
||||||
|
const focusinEv = await oneEvent(el, 'focusin');
|
||||||
|
expect(focusinEv).to.be.instanceOf(FocusEvent);
|
||||||
|
expect(focusinEv.target).to.equal(el);
|
||||||
|
expect(focusinEv.bubbles).to.be.true;
|
||||||
|
expect(focusinEv.composed).to.be.true;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
el.focus();
|
||||||
|
el.blur();
|
||||||
|
});
|
||||||
|
const focusoutEv = await oneEvent(el, 'focusout');
|
||||||
|
expect(focusoutEv).to.be.instanceOf(FocusEvent);
|
||||||
|
expect(focusoutEv.target).to.equal(el);
|
||||||
|
expect(focusoutEv.bubbles).to.be.true;
|
||||||
|
expect(focusoutEv.composed).to.be.true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -13,7 +13,6 @@ describe('FormControlMixin', () => {
|
||||||
const FormControlMixinClass = class extends FormControlMixin(SlotMixin(LionLitElement)) {
|
const FormControlMixinClass = class extends FormControlMixin(SlotMixin(LionLitElement)) {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
...super.properties,
|
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,313 +1,3 @@
|
||||||
import { expect, fixture, html, aTimeout, defineCE, unsafeStatic } from '@open-wc/testing';
|
import { runFormatMixinSuite } from '../test-suites/FormatMixin.suite.js';
|
||||||
import sinon from 'sinon';
|
|
||||||
|
|
||||||
import { LitElement } from '@lion/core';
|
runFormatMixinSuite();
|
||||||
import { Unparseable } from '@lion/validate';
|
|
||||||
import { FormatMixin } from '../src/FormatMixin.js';
|
|
||||||
|
|
||||||
function mimicUserInput(formControl, newViewValue) {
|
|
||||||
formControl.value = newViewValue; // eslint-disable-line no-param-reassign
|
|
||||||
formControl.inputElement.dispatchEvent(new CustomEvent('input', { bubbles: true }));
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('FormatMixin', () => {
|
|
||||||
let elem;
|
|
||||||
let nonFormat;
|
|
||||||
let fooFormat;
|
|
||||||
|
|
||||||
before(async () => {
|
|
||||||
const tagString = defineCE(
|
|
||||||
class extends FormatMixin(LitElement) {
|
|
||||||
render() {
|
|
||||||
return html`
|
|
||||||
<slot name="input"></slot>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
set value(newValue) {
|
|
||||||
this.inputElement.value = newValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
get value() {
|
|
||||||
return this.inputElement.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
get inputElement() {
|
|
||||||
return this.querySelector('input');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
elem = unsafeStatic(tagString);
|
|
||||||
nonFormat = await fixture(html`<${elem}><input slot="input"></${elem}>`);
|
|
||||||
fooFormat = await fixture(html`
|
|
||||||
<${elem}
|
|
||||||
.formatter="${value => `foo: ${value}`}"
|
|
||||||
.parser="${value => value.replace('foo: ', '')}"
|
|
||||||
.serializer="${value => `[foo] ${value}`}"
|
|
||||||
.deserializer="${value => value.replace('[foo] ', '')}"
|
|
||||||
><input slot="input">
|
|
||||||
</${elem}>`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('fires `model-value-changed` for every change on the input', async () => {
|
|
||||||
const formatEl = await fixture(html`<${elem}><input slot="input"></${elem}>`);
|
|
||||||
let counter = 0;
|
|
||||||
formatEl.addEventListener('model-value-changed', () => {
|
|
||||||
counter += 1;
|
|
||||||
});
|
|
||||||
|
|
||||||
mimicUserInput(formatEl, 'one');
|
|
||||||
expect(counter).to.equal(1);
|
|
||||||
|
|
||||||
// no change means no event
|
|
||||||
mimicUserInput(formatEl, 'one');
|
|
||||||
expect(counter).to.equal(1);
|
|
||||||
|
|
||||||
mimicUserInput(formatEl, 'two');
|
|
||||||
expect(counter).to.equal(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('fires `model-value-changed` for every modelValue change', async () => {
|
|
||||||
const el = await fixture(html`<${elem}><input slot="input"></${elem}>`);
|
|
||||||
let counter = 0;
|
|
||||||
el.addEventListener('model-value-changed', () => {
|
|
||||||
counter += 1;
|
|
||||||
});
|
|
||||||
|
|
||||||
el.modelValue = 'one';
|
|
||||||
expect(counter).to.equal(1);
|
|
||||||
|
|
||||||
// no change means no event
|
|
||||||
el.modelValue = 'one';
|
|
||||||
expect(counter).to.equal(1);
|
|
||||||
|
|
||||||
el.modelValue = 'two';
|
|
||||||
expect(counter).to.equal(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('has modelValue, formattedValue and serializedValue which are computed synchronously', async () => {
|
|
||||||
expect(nonFormat.modelValue).to.equal('', 'modelValue initially');
|
|
||||||
expect(nonFormat.formattedValue).to.equal('', 'formattedValue initially');
|
|
||||||
expect(nonFormat.serializedValue).to.equal('', 'serializedValue initially');
|
|
||||||
nonFormat.modelValue = 'string';
|
|
||||||
expect(nonFormat.modelValue).to.equal('string', 'modelValue as provided');
|
|
||||||
expect(nonFormat.formattedValue).to.equal('string', 'formattedValue synchronized');
|
|
||||||
expect(nonFormat.serializedValue).to.equal('string', 'serializedValue synchronized');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('has an input node (like <input>/<textarea>) which holds the formatted (view) value', async () => {
|
|
||||||
fooFormat.modelValue = 'string';
|
|
||||||
expect(fooFormat.formattedValue).to.equal('foo: string');
|
|
||||||
expect(fooFormat.value).to.equal('foo: string');
|
|
||||||
expect(fooFormat.inputElement.value).to.equal('foo: string');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('converts modelValue => formattedValue (via this.formatter)', async () => {
|
|
||||||
fooFormat.modelValue = 'string';
|
|
||||||
expect(fooFormat.formattedValue).to.equal('foo: string');
|
|
||||||
expect(fooFormat.serializedValue).to.equal('[foo] string');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('converts modelValue => serializedValue (via this.serializer)', async () => {
|
|
||||||
fooFormat.modelValue = 'string';
|
|
||||||
expect(fooFormat.serializedValue).to.equal('[foo] string');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('converts formattedValue => modelValue (via this.parser)', async () => {
|
|
||||||
fooFormat.formattedValue = 'foo: string';
|
|
||||||
expect(fooFormat.modelValue).to.equal('string');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('converts serializedValue => modelValue (via this.deserializer)', async () => {
|
|
||||||
fooFormat.serializedValue = '[foo] string';
|
|
||||||
expect(fooFormat.modelValue).to.equal('string');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('synchronizes inputElement.value as a fallback mechanism', async () => {
|
|
||||||
// Note that in lion-field, the attribute would be put on <lion-field>, not on <input>
|
|
||||||
const formatElem = await fixture(html`
|
|
||||||
<${elem}
|
|
||||||
value="string",
|
|
||||||
.formatter=${value => `foo: ${value}`}
|
|
||||||
.parser=${value => value.replace('foo: ', '')}
|
|
||||||
.serializer=${value => `[foo] ${value}`}
|
|
||||||
.deserializer=${value => value.replace('[foo] ', '')}
|
|
||||||
><input slot="input" value="string"/></${elem}>`);
|
|
||||||
// Now check if the format/parse/serialize loop has been triggered
|
|
||||||
await aTimeout();
|
|
||||||
expect(formatElem.formattedValue).to.equal('foo: string');
|
|
||||||
expect(formatElem.inputElement.value).to.equal('foo: string');
|
|
||||||
expect(formatElem.serializedValue).to.equal('[foo] string');
|
|
||||||
expect(formatElem.modelValue).to.equal('string');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('reflects back formatted value to user on leave', async () => {
|
|
||||||
const formatEl = await fixture(html`
|
|
||||||
<${elem} .formatter="${value => `foo: ${value}`}">
|
|
||||||
<input slot="input" />
|
|
||||||
</${elem}>
|
|
||||||
`);
|
|
||||||
// users types value 'test'
|
|
||||||
mimicUserInput(formatEl, 'test');
|
|
||||||
expect(formatEl.inputElement.value).to.not.equal('foo: test');
|
|
||||||
// user leaves field
|
|
||||||
formatEl.inputElement.dispatchEvent(new CustomEvent(formatEl.formatOn, { bubbles: true }));
|
|
||||||
await aTimeout();
|
|
||||||
expect(formatEl.inputElement.value).to.equal('foo: test');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('reflects back .formattedValue immediately when .modelValue changed imperatively', async () => {
|
|
||||||
const el = await fixture(html`
|
|
||||||
<${elem} .formatter="${value => `foo: ${value}`}">
|
|
||||||
<input slot="input" />
|
|
||||||
</${elem}>
|
|
||||||
`);
|
|
||||||
// The FormatMixin can be used in conjunction with the ValidateMixin, in which case
|
|
||||||
// it can hold errorState (affecting the formatting)
|
|
||||||
el.errorState = true;
|
|
||||||
|
|
||||||
// users types value 'test'
|
|
||||||
mimicUserInput(el, 'test');
|
|
||||||
expect(el.inputElement.value).to.not.equal('foo: test');
|
|
||||||
|
|
||||||
// Now see the difference for an imperative change
|
|
||||||
el.modelValue = 'test2';
|
|
||||||
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}`);
|
|
||||||
const parserSpy = sinon.spy(value => value.replace('foo: ', ''));
|
|
||||||
const serializerSpy = sinon.spy(value => `[foo] ${value}`);
|
|
||||||
const el = await fixture(html`
|
|
||||||
<${elem}
|
|
||||||
.formatter=${formatterSpy}
|
|
||||||
.parser=${parserSpy}
|
|
||||||
.serializer=${serializerSpy}
|
|
||||||
.modelValue=${'test'}
|
|
||||||
>
|
|
||||||
<input slot="input">
|
|
||||||
</${elem}>
|
|
||||||
`);
|
|
||||||
expect(formatterSpy.called).to.equal(true);
|
|
||||||
expect(serializerSpy.called).to.equal(true);
|
|
||||||
|
|
||||||
el.formattedValue = 'raw';
|
|
||||||
expect(parserSpy.called).to.equal(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have formatOptions available in formatter', async () => {
|
|
||||||
const formatterSpy = sinon.spy(value => `foo: ${value}`);
|
|
||||||
await fixture(html`
|
|
||||||
<${elem}
|
|
||||||
value="string",
|
|
||||||
.formatter="${formatterSpy}"
|
|
||||||
.formatOptions="${{ locale: 'en-GB', decimalSeparator: '-' }}">
|
|
||||||
<input slot="input" value="string">
|
|
||||||
</${elem}>`);
|
|
||||||
await aTimeout();
|
|
||||||
expect(formatterSpy.args[0][1].locale).to.equal('en-GB');
|
|
||||||
expect(formatterSpy.args[0][1].decimalSeparator).to.equal('-');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('will only call the parser for defined values', async () => {
|
|
||||||
const parserSpy = sinon.spy();
|
|
||||||
const el = await fixture(html`
|
|
||||||
<${elem} .parser="${parserSpy}">
|
|
||||||
<input slot="input" value="string">
|
|
||||||
</${elem}>
|
|
||||||
`);
|
|
||||||
el.modelValue = 'foo';
|
|
||||||
expect(parserSpy.callCount).to.equal(1);
|
|
||||||
// This could happen for instance in a reset
|
|
||||||
el.modelValue = undefined;
|
|
||||||
expect(parserSpy.callCount).to.equal(1);
|
|
||||||
// This could happen when the user erases the input value
|
|
||||||
mimicUserInput(el, '');
|
|
||||||
expect(parserSpy.callCount).to.equal(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('will not return Unparseable when empty strings are inputted', async () => {
|
|
||||||
const el = await fixture(html`
|
|
||||||
<${elem}>
|
|
||||||
<input slot="input" value="string">
|
|
||||||
</${elem}>
|
|
||||||
`);
|
|
||||||
// This could happen when the user erases the input value
|
|
||||||
mimicUserInput(el, '');
|
|
||||||
// For backwards compatibility, we keep the modelValue an empty string here.
|
|
||||||
// Undefined would be more appropriate 'conceptually', however
|
|
||||||
expect(el.modelValue).to.equal('');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('will only call the formatter for valid values on `user-input-changed` ', async () => {
|
|
||||||
const formatterSpy = sinon.spy(value => `foo: ${value}`);
|
|
||||||
const el = await fixture(html`
|
|
||||||
<${elem} .formatter=${formatterSpy}>
|
|
||||||
<input slot="input" value="init-string">
|
|
||||||
</${elem}>
|
|
||||||
`);
|
|
||||||
expect(formatterSpy.callCount).to.equal(1);
|
|
||||||
|
|
||||||
el.errorState = true;
|
|
||||||
mimicUserInput(el, 'bar');
|
|
||||||
expect(formatterSpy.callCount).to.equal(1);
|
|
||||||
expect(el.formattedValue).to.equal('bar');
|
|
||||||
|
|
||||||
el.errorState = false;
|
|
||||||
mimicUserInput(el, 'bar2');
|
|
||||||
expect(formatterSpy.callCount).to.equal(2);
|
|
||||||
|
|
||||||
expect(el.formattedValue).to.equal('foo: bar2');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Unparseable values', () => {
|
|
||||||
it('should convert to Unparseable when wrong value inputted by user', async () => {
|
|
||||||
const el = await fixture(html`
|
|
||||||
<${elem}
|
|
||||||
.parser=${viewValue => Number(viewValue) || undefined}
|
|
||||||
>
|
|
||||||
<input slot="input">
|
|
||||||
</${elem}>
|
|
||||||
`);
|
|
||||||
mimicUserInput(el, 'test');
|
|
||||||
expect(el.modelValue).to.be.an.instanceof(Unparseable);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should preserve the viewValue when not parseable', async () => {
|
|
||||||
const el = await fixture(html`
|
|
||||||
<${elem}
|
|
||||||
.parser=${viewValue => Number(viewValue) || undefined}
|
|
||||||
>
|
|
||||||
<input slot="input">
|
|
||||||
</${elem}>
|
|
||||||
`);
|
|
||||||
mimicUserInput(el, 'test');
|
|
||||||
expect(el.formattedValue).to.equal('test');
|
|
||||||
expect(el.value).to.equal('test');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should display the viewValue when modelValue is of type Unparseable', async () => {
|
|
||||||
const el = await fixture(html`
|
|
||||||
<${elem}
|
|
||||||
.parser=${viewValue => Number(viewValue) || undefined}
|
|
||||||
>
|
|
||||||
<input slot="input">
|
|
||||||
</${elem}>
|
|
||||||
`);
|
|
||||||
el.modelValue = new Unparseable('foo');
|
|
||||||
expect(el.value).to.equal('foo');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { defineCE } from '@open-wc/testing';
|
import { defineCE } from '@open-wc/testing';
|
||||||
import { runInteractionStateMixinSuite } from '../test-suites/InteractionStateMixin.suite.js';
|
import { runInteractionStateMixinSuite } from '../test-suites/InteractionStateMixin.suite.js';
|
||||||
import '../lion-field.js';
|
import '../lion-field.js';
|
||||||
|
import { runFormatMixinSuite } from '../test-suites/FormatMixin.suite.js';
|
||||||
|
|
||||||
const fieldTagString = defineCE(
|
const fieldTagString = defineCE(
|
||||||
class extends customElements.get('lion-field') {
|
class extends customElements.get('lion-field') {
|
||||||
|
|
@ -19,4 +20,8 @@ describe('<lion-field> integrations', () => {
|
||||||
tagString: fieldTagString,
|
tagString: fieldTagString,
|
||||||
suffix: 'lion-field',
|
suffix: 'lion-field',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
runFormatMixinSuite({
|
||||||
|
tagString: fieldTagString,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -31,146 +31,144 @@ beforeEach(() => {
|
||||||
|
|
||||||
describe('<lion-field>', () => {
|
describe('<lion-field>', () => {
|
||||||
it(`puts a unique id "${tagString}-[hash]" on the native input`, async () => {
|
it(`puts a unique id "${tagString}-[hash]" on the native input`, async () => {
|
||||||
const lionField = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`);
|
const el = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`);
|
||||||
expect(lionField.$$slot('input').id).to.equal(lionField._inputId);
|
expect(el.$$slot('input').id).to.equal(el._inputId);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fires focus/blur event on host and native input if focused/blurred', async () => {
|
it('fires focus/blur event on host and native input if focused/blurred', async () => {
|
||||||
const lionField = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`);
|
const el = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`);
|
||||||
const cbFocusHost = sinon.spy();
|
const cbFocusHost = sinon.spy();
|
||||||
lionField.addEventListener('focus', cbFocusHost);
|
el.addEventListener('focus', cbFocusHost);
|
||||||
const cbFocusNativeInput = sinon.spy();
|
const cbFocusNativeInput = sinon.spy();
|
||||||
lionField.inputElement.addEventListener('focus', cbFocusNativeInput);
|
el.inputElement.addEventListener('focus', cbFocusNativeInput);
|
||||||
const cbBlurHost = sinon.spy();
|
const cbBlurHost = sinon.spy();
|
||||||
lionField.addEventListener('blur', cbBlurHost);
|
el.addEventListener('blur', cbBlurHost);
|
||||||
const cbBlurNativeInput = sinon.spy();
|
const cbBlurNativeInput = sinon.spy();
|
||||||
lionField.inputElement.addEventListener('blur', cbBlurNativeInput);
|
el.inputElement.addEventListener('blur', cbBlurNativeInput);
|
||||||
|
|
||||||
await triggerFocusFor(lionField);
|
await triggerFocusFor(el);
|
||||||
|
|
||||||
expect(document.activeElement).to.equal(lionField.inputElement);
|
expect(document.activeElement).to.equal(el.inputElement);
|
||||||
expect(cbFocusHost.callCount).to.equal(1);
|
expect(cbFocusHost.callCount).to.equal(1);
|
||||||
expect(cbFocusNativeInput.callCount).to.equal(1);
|
expect(cbFocusNativeInput.callCount).to.equal(1);
|
||||||
expect(cbBlurHost.callCount).to.equal(0);
|
expect(cbBlurHost.callCount).to.equal(0);
|
||||||
expect(cbBlurNativeInput.callCount).to.equal(0);
|
expect(cbBlurNativeInput.callCount).to.equal(0);
|
||||||
|
|
||||||
await triggerBlurFor(lionField);
|
await triggerBlurFor(el);
|
||||||
expect(cbBlurHost.callCount).to.equal(1);
|
expect(cbBlurHost.callCount).to.equal(1);
|
||||||
expect(cbBlurNativeInput.callCount).to.equal(1);
|
expect(cbBlurNativeInput.callCount).to.equal(1);
|
||||||
|
|
||||||
await triggerFocusFor(lionField);
|
await triggerFocusFor(el);
|
||||||
expect(document.activeElement).to.equal(lionField.inputElement);
|
expect(document.activeElement).to.equal(el.inputElement);
|
||||||
expect(cbFocusHost.callCount).to.equal(2);
|
expect(cbFocusHost.callCount).to.equal(2);
|
||||||
expect(cbFocusNativeInput.callCount).to.equal(2);
|
expect(cbFocusNativeInput.callCount).to.equal(2);
|
||||||
|
|
||||||
await triggerBlurFor(lionField);
|
await triggerBlurFor(el);
|
||||||
expect(cbBlurHost.callCount).to.equal(2);
|
expect(cbBlurHost.callCount).to.equal(2);
|
||||||
expect(cbBlurNativeInput.callCount).to.equal(2);
|
expect(cbBlurNativeInput.callCount).to.equal(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has class "state-focused" if focused', async () => {
|
|
||||||
const el = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`);
|
|
||||||
expect(el.classList.contains('state-focused')).to.equal(false, 'no state-focused initially');
|
|
||||||
await triggerFocusFor(el.inputElement);
|
|
||||||
expect(el.classList.contains('state-focused')).to.equal(true, 'state-focused after focus()');
|
|
||||||
await triggerBlurFor(el.inputElement);
|
|
||||||
expect(el.classList.contains('state-focused')).to.equal(false, 'no state-focused after blur()');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('offers simple getter "this.focused" returning true/false for the current focus state', async () => {
|
|
||||||
const lionField = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`);
|
|
||||||
expect(lionField.focused).to.equal(false);
|
|
||||||
await triggerFocusFor(lionField);
|
|
||||||
expect(lionField.focused).to.equal(true);
|
|
||||||
await triggerBlurFor(lionField);
|
|
||||||
expect(lionField.focused).to.equal(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('can be disabled via attribute', async () => {
|
it('can be disabled via attribute', async () => {
|
||||||
const lionFieldDisabled = await fixture(
|
const elDisabled = await fixture(`<${tagString} disabled>${inputSlotString}</${tagString}>`);
|
||||||
`<${tagString} disabled>${inputSlotString}</${tagString}>`,
|
expect(elDisabled.disabled).to.equal(true);
|
||||||
);
|
expect(elDisabled.inputElement.disabled).to.equal(true);
|
||||||
expect(lionFieldDisabled.disabled).to.equal(true);
|
|
||||||
expect(lionFieldDisabled.inputElement.disabled).to.equal(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can be disabled via property', async () => {
|
it('can be disabled via property', async () => {
|
||||||
const lionField = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`);
|
const el = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`);
|
||||||
lionField.disabled = true;
|
el.disabled = true;
|
||||||
await lionField.updateComplete;
|
await el.updateComplete;
|
||||||
expect(lionField.inputElement.disabled).to.equal(true);
|
expect(el.inputElement.disabled).to.equal(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
// classes are added only for backward compatibility - they are deprecated
|
||||||
|
it('sets a state-disabled class when disabled', async () => {
|
||||||
|
const el = await fixture(`<${tagString} disabled>${inputSlotString}</${tagString}>`);
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(el.classList.contains('state-disabled')).to.equal(true);
|
||||||
|
el.disabled = false;
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(el.classList.contains('state-disabled')).to.equal(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can be cleared which erases value, validation and interaction states', async () => {
|
it('can be cleared which erases value, validation and interaction states', async () => {
|
||||||
const lionField = await fixture(
|
const el = await fixture(
|
||||||
`<${tagString} value="Some value from attribute">${inputSlotString}</${tagString}>`,
|
`<${tagString} value="Some value from attribute">${inputSlotString}</${tagString}>`,
|
||||||
);
|
);
|
||||||
lionField.clear();
|
el.clear();
|
||||||
expect(lionField.value).to.equal('');
|
expect(el.value).to.equal('');
|
||||||
lionField.value = 'Some value from property';
|
el.value = 'Some value from property';
|
||||||
expect(lionField.value).to.equal('Some value from property');
|
expect(el.value).to.equal('Some value from property');
|
||||||
lionField.clear();
|
el.clear();
|
||||||
expect(lionField.value).to.equal('');
|
expect(el.value).to.equal('');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('reads initial value from attribute value', async () => {
|
it('reads initial value from attribute value', async () => {
|
||||||
const lionField = await fixture(`<${tagString} value="one">${inputSlotString}</${tagString}>`);
|
const el = await fixture(`<${tagString} value="one">${inputSlotString}</${tagString}>`);
|
||||||
expect(lionField.$$slot('input').value).to.equal('one');
|
expect(el.$$slot('input').value).to.equal('one');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('delegates value property', async () => {
|
it('delegates value property', async () => {
|
||||||
const lionField = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`);
|
const el = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`);
|
||||||
expect(lionField.$$slot('input').value).to.equal('');
|
expect(el.$$slot('input').value).to.equal('');
|
||||||
lionField.value = 'one';
|
el.value = 'one';
|
||||||
expect(lionField.value).to.equal('one');
|
expect(el.value).to.equal('one');
|
||||||
expect(lionField.$$slot('input').value).to.equal('one');
|
expect(el.$$slot('input').value).to.equal('one');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has a name which is reflected to an attribute and is synced down to the native input', async () => {
|
||||||
|
const el = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`);
|
||||||
|
expect(el.name).to.equal('');
|
||||||
|
expect(el.getAttribute('name')).to.equal('');
|
||||||
|
expect(el.inputElement.getAttribute('name')).to.equal('');
|
||||||
|
|
||||||
|
el.name = 'foo';
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(el.getAttribute('name')).to.equal('foo');
|
||||||
|
expect(el.inputElement.getAttribute('name')).to.equal('foo');
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: find out if we could put all listeners on this.value (instead of this.inputElement.value)
|
// TODO: find out if we could put all listeners on this.value (instead of this.inputElement.value)
|
||||||
// and make it act on this.value again
|
// and make it act on this.value again
|
||||||
it('has a class "state-filled" if this.value is filled', async () => {
|
it('has a class "state-filled" if this.value is filled', async () => {
|
||||||
const lionField = await fixture(
|
const el = await fixture(`<${tagString} value="filled">${inputSlotString}</${tagString}>`);
|
||||||
`<${tagString} value="filled">${inputSlotString}</${tagString}>`,
|
expect(el.classList.contains('state-filled')).to.equal(true);
|
||||||
);
|
el.value = '';
|
||||||
expect(lionField.classList.contains('state-filled')).to.equal(true);
|
await el.updateComplete;
|
||||||
lionField.value = '';
|
expect(el.classList.contains('state-filled')).to.equal(false);
|
||||||
await lionField.updateComplete;
|
el.value = 'bla';
|
||||||
expect(lionField.classList.contains('state-filled')).to.equal(false);
|
await el.updateComplete;
|
||||||
lionField.value = 'bla';
|
expect(el.classList.contains('state-filled')).to.equal(true);
|
||||||
await lionField.updateComplete;
|
|
||||||
expect(lionField.classList.contains('state-filled')).to.equal(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('preserves the caret position on value change for native text fields (input|textarea)', async () => {
|
it('preserves the caret position on value change for native text fields (input|textarea)', async () => {
|
||||||
const lionField = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`);
|
const el = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`);
|
||||||
await triggerFocusFor(lionField);
|
await triggerFocusFor(el);
|
||||||
await lionField.updateComplete;
|
await el.updateComplete;
|
||||||
lionField.inputElement.value = 'hello world';
|
el.inputElement.value = 'hello world';
|
||||||
lionField.inputElement.selectionStart = 2;
|
el.inputElement.selectionStart = 2;
|
||||||
lionField.inputElement.selectionEnd = 2;
|
el.inputElement.selectionEnd = 2;
|
||||||
lionField.value = 'hey there universe';
|
el.value = 'hey there universe';
|
||||||
expect(lionField.inputElement.selectionStart).to.equal(2);
|
expect(el.inputElement.selectionStart).to.equal(2);
|
||||||
expect(lionField.inputElement.selectionEnd).to.equal(2);
|
expect(el.inputElement.selectionEnd).to.equal(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: add pointerEvents test for disabled
|
// TODO: add pointerEvents test for disabled
|
||||||
it('has a class "state-disabled"', async () => {
|
it('has a class "state-disabled"', async () => {
|
||||||
const lionField = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`);
|
const el = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`);
|
||||||
expect(lionField.classList.contains('state-disabled')).to.equal(false);
|
expect(el.classList.contains('state-disabled')).to.equal(false);
|
||||||
expect(lionField.inputElement.hasAttribute('disabled')).to.equal(false);
|
expect(el.inputElement.hasAttribute('disabled')).to.equal(false);
|
||||||
|
|
||||||
lionField.disabled = true;
|
el.disabled = true;
|
||||||
await lionField.updateComplete;
|
await el.updateComplete;
|
||||||
await aTimeout();
|
await aTimeout();
|
||||||
|
|
||||||
expect(lionField.classList.contains('state-disabled')).to.equal(true);
|
expect(el.classList.contains('state-disabled')).to.equal(true);
|
||||||
expect(lionField.inputElement.hasAttribute('disabled')).to.equal(true);
|
expect(el.inputElement.hasAttribute('disabled')).to.equal(true);
|
||||||
|
|
||||||
const disabledlionField = await fixture(
|
const disabledel = await fixture(`<${tagString} disabled>${inputSlotString}</${tagString}>`);
|
||||||
`<${tagString} disabled>${inputSlotString}</${tagString}>`,
|
expect(disabledel.classList.contains('state-disabled')).to.equal(true);
|
||||||
);
|
expect(disabledel.inputElement.hasAttribute('disabled')).to.equal(true);
|
||||||
expect(disabledlionField.classList.contains('state-disabled')).to.equal(true);
|
|
||||||
expect(disabledlionField.inputElement.hasAttribute('disabled')).to.equal(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe(`A11y${nameSuffix}`, () => {
|
describe(`A11y${nameSuffix}`, () => {
|
||||||
|
|
@ -188,27 +186,23 @@ describe('<lion-field>', () => {
|
||||||
<div slot="feedback" id="feedback-[id]">[feedback] </span>
|
<div slot="feedback" id="feedback-[id]">[feedback] </span>
|
||||||
</lion-field>
|
</lion-field>
|
||||||
~~~`, async () => {
|
~~~`, async () => {
|
||||||
const lionField = await fixture(`<${tagString}>
|
const el = await fixture(`<${tagString}>
|
||||||
<label slot="label">My Name</label>
|
<label slot="label">My Name</label>
|
||||||
${inputSlotString}
|
${inputSlotString}
|
||||||
<span slot="help-text">Enter your Name</span>
|
<span slot="help-text">Enter your Name</span>
|
||||||
<span slot="feedback">No name entered</span>
|
<span slot="feedback">No name entered</span>
|
||||||
</${tagString}>
|
</${tagString}>
|
||||||
`);
|
`);
|
||||||
const nativeInput = lionField.$$slot('input');
|
const nativeInput = el.$$slot('input');
|
||||||
|
|
||||||
expect(nativeInput.getAttribute('aria-labelledby')).to.equal(` label-${lionField._inputId}`);
|
expect(nativeInput.getAttribute('aria-labelledby')).to.equal(` label-${el._inputId}`);
|
||||||
expect(nativeInput.getAttribute('aria-describedby')).to.contain(
|
expect(nativeInput.getAttribute('aria-describedby')).to.contain(` help-text-${el._inputId}`);
|
||||||
` help-text-${lionField._inputId}`,
|
expect(nativeInput.getAttribute('aria-describedby')).to.contain(` feedback-${el._inputId}`);
|
||||||
);
|
|
||||||
expect(nativeInput.getAttribute('aria-describedby')).to.contain(
|
|
||||||
` feedback-${lionField._inputId}`,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`allows additional slots (prefix, suffix, before, after) to be included in labelledby
|
it(`allows additional slots (prefix, suffix, before, after) to be included in labelledby
|
||||||
(via attribute data-label) and in describedby (via attribute data-description)`, async () => {
|
(via attribute data-label) and in describedby (via attribute data-description)`, async () => {
|
||||||
const lionField = await fixture(`<${tagString}>
|
const el = await fixture(`<${tagString}>
|
||||||
${inputSlotString}
|
${inputSlotString}
|
||||||
<span slot="before" data-label>[before]</span>
|
<span slot="before" data-label>[before]</span>
|
||||||
<span slot="after" data-label>[after]</span>
|
<span slot="after" data-label>[after]</span>
|
||||||
|
|
@ -217,12 +211,12 @@ describe('<lion-field>', () => {
|
||||||
</${tagString}>
|
</${tagString}>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const nativeInput = lionField.$$slot('input');
|
const nativeInput = el.$$slot('input');
|
||||||
expect(nativeInput.getAttribute('aria-labelledby')).to.contain(
|
expect(nativeInput.getAttribute('aria-labelledby')).to.contain(
|
||||||
` before-${lionField._inputId} after-${lionField._inputId}`,
|
` before-${el._inputId} after-${el._inputId}`,
|
||||||
);
|
);
|
||||||
expect(nativeInput.getAttribute('aria-describedby')).to.contain(
|
expect(nativeInput.getAttribute('aria-describedby')).to.contain(
|
||||||
` prefix-${lionField._inputId} suffix-${lionField._inputId}`,
|
` prefix-${el._inputId} suffix-${el._inputId}`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -291,31 +285,31 @@ describe('<lion-field>', () => {
|
||||||
function hasX(str) {
|
function hasX(str) {
|
||||||
return { hasX: str.indexOf('x') > -1 };
|
return { hasX: str.indexOf('x') > -1 };
|
||||||
}
|
}
|
||||||
const lionField = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`);
|
const el = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`);
|
||||||
const feedbackEl = lionField._feedbackElement;
|
const feedbackEl = el._feedbackElement;
|
||||||
|
|
||||||
lionField.modelValue = 'a@b.nl';
|
el.modelValue = 'a@b.nl';
|
||||||
lionField.errorValidators = [[hasX]];
|
el.errorValidators = [[hasX]];
|
||||||
|
|
||||||
expect(lionField.error.hasX).to.equal(true);
|
expect(el.error.hasX).to.equal(true);
|
||||||
expect(feedbackEl.innerText.trim()).to.equal(
|
expect(feedbackEl.innerText.trim()).to.equal(
|
||||||
'',
|
'',
|
||||||
'shows no feedback, although the element has an error',
|
'shows no feedback, although the element has an error',
|
||||||
);
|
);
|
||||||
lionField.dirty = true;
|
el.dirty = true;
|
||||||
lionField.touched = true;
|
el.touched = true;
|
||||||
lionField.modelValue = 'ab@c.nl'; // retrigger validation
|
el.modelValue = 'ab@c.nl'; // retrigger validation
|
||||||
await lionField.updateComplete;
|
await el.updateComplete;
|
||||||
|
|
||||||
expect(feedbackEl.innerText.trim()).to.equal(
|
expect(feedbackEl.innerText.trim()).to.equal(
|
||||||
'This is error message for hasX',
|
'This is error message for hasX',
|
||||||
'shows feedback, because touched=true and dirty=true',
|
'shows feedback, because touched=true and dirty=true',
|
||||||
);
|
);
|
||||||
|
|
||||||
lionField.touched = false;
|
el.touched = false;
|
||||||
lionField.dirty = false;
|
el.dirty = false;
|
||||||
lionField.prefilled = true;
|
el.prefilled = true;
|
||||||
await lionField.updateComplete;
|
await el.updateComplete;
|
||||||
expect(feedbackEl.innerText.trim()).to.equal(
|
expect(feedbackEl.innerText.trim()).to.equal(
|
||||||
'This is error message for hasX',
|
'This is error message for hasX',
|
||||||
'shows feedback, because prefilled=true',
|
'shows feedback, because prefilled=true',
|
||||||
|
|
@ -323,14 +317,14 @@ describe('<lion-field>', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can be required', async () => {
|
it('can be required', async () => {
|
||||||
const lionField = await fixture(html`
|
const el = await fixture(html`
|
||||||
<${tag}
|
<${tag}
|
||||||
.errorValidators=${[['required']]}
|
.errorValidators=${[['required']]}
|
||||||
>${inputSlot}</${tag}>
|
>${inputSlot}</${tag}>
|
||||||
`);
|
`);
|
||||||
expect(lionField.error.required).to.be.true;
|
expect(el.error.required).to.be.true;
|
||||||
lionField.modelValue = 'cat';
|
el.modelValue = 'cat';
|
||||||
expect(lionField.error.required).to.be.undefined;
|
expect(el.error.required).to.be.undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('will only update formattedValue when valid on `user-input-changed`', async () => {
|
it('will only update formattedValue when valid on `user-input-changed`', async () => {
|
||||||
|
|
@ -338,7 +332,7 @@ describe('<lion-field>', () => {
|
||||||
function isBarValidator(value) {
|
function isBarValidator(value) {
|
||||||
return { isBar: value === 'bar' };
|
return { isBar: value === 'bar' };
|
||||||
}
|
}
|
||||||
const lionField = await fixture(html`
|
const el = await fixture(html`
|
||||||
<${tag}
|
<${tag}
|
||||||
.modelValue=${'init-string'}
|
.modelValue=${'init-string'}
|
||||||
.formatter=${formatterSpy}
|
.formatter=${formatterSpy}
|
||||||
|
|
@ -347,21 +341,21 @@ describe('<lion-field>', () => {
|
||||||
`);
|
`);
|
||||||
|
|
||||||
expect(formatterSpy.callCount).to.equal(0);
|
expect(formatterSpy.callCount).to.equal(0);
|
||||||
expect(lionField.formattedValue).to.equal('init-string');
|
expect(el.formattedValue).to.equal('init-string');
|
||||||
|
|
||||||
lionField.modelValue = 'bar';
|
el.modelValue = 'bar';
|
||||||
expect(formatterSpy.callCount).to.equal(1);
|
expect(formatterSpy.callCount).to.equal(1);
|
||||||
expect(lionField.formattedValue).to.equal('foo: bar');
|
expect(el.formattedValue).to.equal('foo: bar');
|
||||||
|
|
||||||
mimicUserInput(lionField, 'foo');
|
mimicUserInput(el, 'foo');
|
||||||
expect(formatterSpy.callCount).to.equal(1);
|
expect(formatterSpy.callCount).to.equal(1);
|
||||||
expect(lionField.value).to.equal('foo');
|
expect(el.value).to.equal('foo');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe(`Content projection${nameSuffix}`, () => {
|
describe(`Content projection${nameSuffix}`, () => {
|
||||||
it('renders correctly all slot elements in light DOM', async () => {
|
it('renders correctly all slot elements in light DOM', async () => {
|
||||||
const lionField = await fixture(`
|
const el = await fixture(`
|
||||||
<${tagString}>
|
<${tagString}>
|
||||||
<label slot="label">[label]</label>
|
<label slot="label">[label]</label>
|
||||||
${inputSlotString}
|
${inputSlotString}
|
||||||
|
|
@ -385,8 +379,8 @@ describe('<lion-field>', () => {
|
||||||
'feedback',
|
'feedback',
|
||||||
];
|
];
|
||||||
names.forEach(slotName => {
|
names.forEach(slotName => {
|
||||||
lionField.querySelector(`[slot="${slotName}"]`).setAttribute('test-me', 'ok');
|
el.querySelector(`[slot="${slotName}"]`).setAttribute('test-me', 'ok');
|
||||||
const slot = lionField.shadowRoot.querySelector(`slot[name="${slotName}"]`);
|
const slot = el.shadowRoot.querySelector(`slot[name="${slotName}"]`);
|
||||||
const assignedNodes = slot.assignedNodes();
|
const assignedNodes = slot.assignedNodes();
|
||||||
expect(assignedNodes.length).to.equal(1);
|
expect(assignedNodes.length).to.equal(1);
|
||||||
expect(assignedNodes[0].getAttribute('test-me')).to.equal('ok');
|
expect(assignedNodes[0].getAttribute('test-me')).to.equal('ok');
|
||||||
|
|
@ -395,12 +389,6 @@ describe('<lion-field>', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe(`Delegation${nameSuffix}`, () => {
|
describe(`Delegation${nameSuffix}`, () => {
|
||||||
it('delegates attribute autofocus', async () => {
|
|
||||||
const el = await fixture(`<${tagString} autofocus>${inputSlotString}</${tagString}>`);
|
|
||||||
expect(el.hasAttribute('autofocus')).to.be.false;
|
|
||||||
expect(el.inputElement.hasAttribute('autofocus')).to.be.true;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('delegates property value', async () => {
|
it('delegates property value', async () => {
|
||||||
const el = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`);
|
const el = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`);
|
||||||
expect(el.inputElement.value).to.equal('');
|
expect(el.inputElement.value).to.equal('');
|
||||||
|
|
@ -409,51 +397,17 @@ describe('<lion-field>', () => {
|
||||||
expect(el.inputElement.value).to.equal('one');
|
expect(el.inputElement.value).to.equal('one');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('delegates property type', async () => {
|
|
||||||
const el = await fixture(`<${tagString} type="text">${inputSlotString}</${tagString}>`);
|
|
||||||
const inputElemTag = el.inputElement.tagName.toLowerCase();
|
|
||||||
if (inputElemTag === 'select') {
|
|
||||||
// TODO: later on we might want to support multi select ?
|
|
||||||
expect(el.inputElement.type).to.contain('select-one');
|
|
||||||
} else if (inputElemTag === 'textarea') {
|
|
||||||
expect(el.inputElement.type).to.contain('textarea');
|
|
||||||
} else {
|
|
||||||
// input or custom inputElement
|
|
||||||
expect(el.inputElement.type).to.contain('text');
|
|
||||||
el.type = 'password';
|
|
||||||
expect(el.type).to.equal('password');
|
|
||||||
expect(el.inputElement.type).to.equal('password');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('delegates property onfocus', async () => {
|
|
||||||
const el = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`);
|
|
||||||
const cbFocusHost = sinon.spy();
|
|
||||||
el.onfocus = cbFocusHost;
|
|
||||||
await triggerFocusFor(el.inputElement);
|
|
||||||
expect(cbFocusHost.callCount).to.equal(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('delegates property onblur', async () => {
|
|
||||||
const el = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`);
|
|
||||||
const cbBlurHost = sinon.spy();
|
|
||||||
el.onblur = cbBlurHost;
|
|
||||||
await triggerFocusFor(el.inputElement);
|
|
||||||
await triggerBlurFor(el.inputElement);
|
|
||||||
expect(cbBlurHost.callCount).to.equal(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('delegates property selectionStart and selectionEnd', async () => {
|
it('delegates property selectionStart and selectionEnd', async () => {
|
||||||
const lionField = await fixture(html`
|
const el = await fixture(html`
|
||||||
<${tag}
|
<${tag}
|
||||||
.modelValue=${'Some text to select'}
|
.modelValue=${'Some text to select'}
|
||||||
>${unsafeHTML(inputSlotString)}</${tag}>
|
>${unsafeHTML(inputSlotString)}</${tag}>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
lionField.selectionStart = 5;
|
el.selectionStart = 5;
|
||||||
lionField.selectionEnd = 12;
|
el.selectionEnd = 12;
|
||||||
expect(lionField.inputElement.selectionStart).to.equal(5);
|
expect(el.inputElement.selectionStart).to.equal(5);
|
||||||
expect(lionField.inputElement.selectionEnd).to.equal(12);
|
expect(el.inputElement.selectionEnd).to.equal(12);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { SlotMixin, html } from '@lion/core';
|
import { SlotMixin, html } from '@lion/core';
|
||||||
import { LionLitElement } from '@lion/core/src/LionLitElement.js';
|
import { LionLitElement } from '@lion/core/src/LionLitElement.js';
|
||||||
import { CssClassMixin } from '@lion/core/src/CssClassMixin.js';
|
import { DisabledMixin } from '@lion/core/src/DisabledMixin.js';
|
||||||
import { ObserverMixin } from '@lion/core/src/ObserverMixin.js';
|
import { ObserverMixin } from '@lion/core/src/ObserverMixin.js';
|
||||||
import { ValidateMixin } from '@lion/validate';
|
import { ValidateMixin } from '@lion/validate';
|
||||||
import { FormControlMixin, FormRegistrarMixin } from '@lion/field';
|
import { FormControlMixin, FormRegistrarMixin } from '@lion/field';
|
||||||
|
|
@ -15,16 +15,10 @@ const pascalCase = str => str.charAt(0).toUpperCase() + str.slice(1);
|
||||||
* @extends LionLitElement
|
* @extends LionLitElement
|
||||||
*/
|
*/
|
||||||
export class LionFieldset extends FormRegistrarMixin(
|
export class LionFieldset extends FormRegistrarMixin(
|
||||||
FormControlMixin(ValidateMixin(CssClassMixin(SlotMixin(ObserverMixin(LionLitElement))))),
|
FormControlMixin(ValidateMixin(DisabledMixin(SlotMixin(ObserverMixin(LionLitElement))))),
|
||||||
) {
|
) {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
...super.properties,
|
|
||||||
disabled: {
|
|
||||||
type: Boolean,
|
|
||||||
reflect: true,
|
|
||||||
nonEmptyToClass: 'state-disabled',
|
|
||||||
},
|
|
||||||
name: {
|
name: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
|
|
@ -35,13 +29,6 @@ export class LionFieldset extends FormRegistrarMixin(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static get asyncObservers() {
|
|
||||||
return {
|
|
||||||
...super.asyncObservers,
|
|
||||||
_onDisabledChanged: ['disabled'],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
get inputElement() {
|
get inputElement() {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
@ -121,6 +108,36 @@ export class LionFieldset extends FormRegistrarMixin(
|
||||||
this.removeEventListener('dirty-changed', this._updateDirtyClass);
|
this.removeEventListener('dirty-changed', this._updateDirtyClass);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updated(changedProps) {
|
||||||
|
super.updated(changedProps);
|
||||||
|
|
||||||
|
if (changedProps.has('disabled')) {
|
||||||
|
if (this.disabled) {
|
||||||
|
this.__requestChildrenToBeDisabled();
|
||||||
|
this.classList.add('state-disabled'); // eslint-disable-line wc/no-self-class
|
||||||
|
} else {
|
||||||
|
this.__retractRequestChildrenToBeDisabled();
|
||||||
|
this.classList.remove('state-disabled'); // eslint-disable-line wc/no-self-class
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
__requestChildrenToBeDisabled() {
|
||||||
|
this.formElementsArray.forEach(child => {
|
||||||
|
if (child.makeRequestToBeDisabled) {
|
||||||
|
child.makeRequestToBeDisabled();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
__retractRequestChildrenToBeDisabled() {
|
||||||
|
this.formElementsArray.forEach(child => {
|
||||||
|
if (child.retractRequestToBeDisabled) {
|
||||||
|
child.retractRequestToBeDisabled();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line class-methods-use-this
|
// eslint-disable-next-line class-methods-use-this
|
||||||
inputGroupTemplate() {
|
inputGroupTemplate() {
|
||||||
return html`
|
return html`
|
||||||
|
|
@ -251,13 +268,6 @@ export class LionFieldset extends FormRegistrarMixin(
|
||||||
this.classList[this.dirty ? 'add' : 'remove']('state-dirty');
|
this.classList[this.dirty ? 'add' : 'remove']('state-dirty');
|
||||||
}
|
}
|
||||||
|
|
||||||
_onDisabledChanged({ disabled }, { disabled: oldDisabled }) {
|
|
||||||
// do not propagate/override inital disabled value on nested form elements
|
|
||||||
if (typeof oldDisabled !== 'undefined') {
|
|
||||||
this._setValueForAllFormElements('disabled', disabled);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_setRole(role) {
|
_setRole(role) {
|
||||||
this.setAttribute('role', role || 'group');
|
this.setAttribute('role', role || 'group');
|
||||||
}
|
}
|
||||||
|
|
@ -303,7 +313,7 @@ export class LionFieldset extends FormRegistrarMixin(
|
||||||
|
|
||||||
if (this.disabled) {
|
if (this.disabled) {
|
||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
child.disabled = true;
|
child.makeRequestToBeDisabled();
|
||||||
}
|
}
|
||||||
if (name.substr(-2) === '[]') {
|
if (name.substr(-2) === '[]') {
|
||||||
if (!Array.isArray(this.formElements[name])) {
|
if (!Array.isArray(this.formElements[name])) {
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,30 @@ storiesOf('Forms|Fieldset', module)
|
||||||
</lion-fieldset>
|
</lion-fieldset>
|
||||||
`,
|
`,
|
||||||
)
|
)
|
||||||
|
.add('Disabled', () => {
|
||||||
|
function toggleDisabled() {
|
||||||
|
const fieldset = document.querySelector('#fieldset');
|
||||||
|
fieldset.disabled = !fieldset.disabled;
|
||||||
|
}
|
||||||
|
return html`
|
||||||
|
<lion-fieldset name="nameGroup" label="Name" id="fieldset" disabled>
|
||||||
|
<lion-input name="FirstName" label="First Name" .modelValue=${'Foo'}></lion-input>
|
||||||
|
<lion-input name="LastName" label="Last Name" .modelValue=${'Bar'}></lion-input>
|
||||||
|
<lion-fieldset name="nameGroup2" label="Name">
|
||||||
|
<lion-input
|
||||||
|
name="FirstName2"
|
||||||
|
label="First Name"
|
||||||
|
.modelValue=${'Foo'}
|
||||||
|
disabled
|
||||||
|
></lion-input>
|
||||||
|
<lion-input name="LastName2" label="Last Name" .modelValue=${'Bar'}></lion-input>
|
||||||
|
</lion-fieldset>
|
||||||
|
</lion-fieldset>
|
||||||
|
<button @click=${toggleDisabled}>
|
||||||
|
Toggle disabled
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
})
|
||||||
.add(
|
.add(
|
||||||
'Sub Fieldsets Data',
|
'Sub Fieldsets Data',
|
||||||
() => html`
|
() => html`
|
||||||
|
|
|
||||||
|
|
@ -223,12 +223,11 @@ describe('<lion-fieldset>', () => {
|
||||||
expect(el.formElements['hobbies[]'][1].disabled).to.equal(false);
|
expect(el.formElements['hobbies[]'][1].disabled).to.equal(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not propagate/override inital disabled value on nested form elements', async () => {
|
it('does not propagate/override initial disabled value on nested form elements', async () => {
|
||||||
const el = await fixture(
|
const el = await fixture(
|
||||||
`<${tagString}><${tagString} name="sub" disabled>${inputSlotString}</${tagString}></${tagString}>`,
|
`<${tagString}><${tagString} name="sub" disabled>${inputSlotString}</${tagString}></${tagString}>`,
|
||||||
);
|
);
|
||||||
await nextFrame();
|
await el.updateComplete;
|
||||||
|
|
||||||
expect(el.disabled).to.equal(false);
|
expect(el.disabled).to.equal(false);
|
||||||
expect(el.formElements.sub.disabled).to.equal(true);
|
expect(el.formElements.sub.disabled).to.equal(true);
|
||||||
expect(el.formElements.sub.formElements.color.disabled).to.equal(true);
|
expect(el.formElements.sub.formElements.color.disabled).to.equal(true);
|
||||||
|
|
@ -236,6 +235,16 @@ describe('<lion-fieldset>', () => {
|
||||||
expect(el.formElements.sub.formElements['hobbies[]'][1].disabled).to.equal(true);
|
expect(el.formElements.sub.formElements['hobbies[]'][1].disabled).to.equal(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// classes are added only for backward compatibility - they are deprecated
|
||||||
|
it('sets a state-disabled class when disabled', async () => {
|
||||||
|
const el = await fixture(`<${tagString} disabled>${inputSlotString}</${tagString}>`);
|
||||||
|
await nextFrame();
|
||||||
|
expect(el.classList.contains('state-disabled')).to.equal(true);
|
||||||
|
el.disabled = false;
|
||||||
|
await nextFrame();
|
||||||
|
expect(el.classList.contains('state-disabled')).to.equal(false);
|
||||||
|
});
|
||||||
|
|
||||||
describe('validation', () => {
|
describe('validation', () => {
|
||||||
it('validates on init', async () => {
|
it('validates on init', async () => {
|
||||||
function isCat(value) {
|
function isCat(value) {
|
||||||
|
|
@ -715,7 +724,7 @@ describe('<lion-fieldset>', () => {
|
||||||
childAriaFixture = async (
|
childAriaFixture = async (
|
||||||
msgSlotType = 'feedback', // eslint-disable-line no-shadow
|
msgSlotType = 'feedback', // eslint-disable-line no-shadow
|
||||||
) => {
|
) => {
|
||||||
const dom = fixture(`
|
const dom = await fixture(`
|
||||||
<lion-fieldset name="l1_g">
|
<lion-fieldset name="l1_g">
|
||||||
<lion-input name="l1_fa">
|
<lion-input name="l1_fa">
|
||||||
<div slot="${msgSlotType}" id="msg_l1_fa"></div>
|
<div slot="${msgSlotType}" id="msg_l1_fa"></div>
|
||||||
|
|
@ -750,14 +759,12 @@ describe('<lion-fieldset>', () => {
|
||||||
<!-- group referred by: #msg_l1_g (local) -->
|
<!-- group referred by: #msg_l1_g (local) -->
|
||||||
</lion-fieldset>
|
</lion-fieldset>
|
||||||
`);
|
`);
|
||||||
await nextFrame();
|
|
||||||
return dom;
|
return dom;
|
||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line no-shadow
|
// eslint-disable-next-line no-shadow
|
||||||
childAriaTest = childAriaFixture => {
|
childAriaTest = childAriaFixture => {
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
|
|
||||||
// Message elements: all elements pointed at by inputs
|
// Message elements: all elements pointed at by inputs
|
||||||
const msg_l1_g = childAriaFixture.querySelector('#msg_l1_g');
|
const msg_l1_g = childAriaFixture.querySelector('#msg_l1_g');
|
||||||
const msg_l1_fa = childAriaFixture.querySelector('#msg_l1_fa');
|
const msg_l1_fa = childAriaFixture.querySelector('#msg_l1_fa');
|
||||||
|
|
@ -767,10 +774,10 @@ describe('<lion-fieldset>', () => {
|
||||||
const msg_l2_fb = childAriaFixture.querySelector('#msg_l2_fb');
|
const msg_l2_fb = childAriaFixture.querySelector('#msg_l2_fb');
|
||||||
|
|
||||||
// Field elements: all inputs pointing to message elements
|
// Field elements: all inputs pointing to message elements
|
||||||
const input_l1_fa = childAriaFixture.querySelector('[name=l1_fa]');
|
const input_l1_fa = childAriaFixture.querySelector('input[name=l1_fa]');
|
||||||
const input_l1_fb = childAriaFixture.querySelector('[name=l1_fb]');
|
const input_l1_fb = childAriaFixture.querySelector('input[name=l1_fb]');
|
||||||
const input_l2_fa = childAriaFixture.querySelector('[name=l2_fa]');
|
const input_l2_fa = childAriaFixture.querySelector('input[name=l2_fa]');
|
||||||
const input_l2_fb = childAriaFixture.querySelector('[name=l2_fb]');
|
const input_l2_fb = childAriaFixture.querySelector('input[name=l2_fb]');
|
||||||
|
|
||||||
/* eslint-enable camelcase */
|
/* eslint-enable camelcase */
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import { DelegateMixin } from '@lion/core';
|
|
||||||
import { LionFieldset } from '@lion/fieldset';
|
import { LionFieldset } from '@lion/fieldset';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -8,50 +7,58 @@ import { LionFieldset } from '@lion/fieldset';
|
||||||
* @extends LionFieldset
|
* @extends LionFieldset
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
export class LionForm extends DelegateMixin(LionFieldset) {
|
export class LionForm extends LionFieldset {
|
||||||
get delegations() {
|
|
||||||
return {
|
|
||||||
...super.delegations,
|
|
||||||
target: () => this.formElement,
|
|
||||||
events: [...super.delegations.events, 'submit', 'reset'],
|
|
||||||
methods: [...super.delegations.methods, 'submit', 'reset'],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.__boundSubmit = this._submit.bind(this);
|
|
||||||
this.__boundReset = this._reset.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
super.connectedCallback();
|
if (super.connectedCallback) {
|
||||||
this.addEventListener('submit', this.__boundSubmit);
|
super.connectedCallback();
|
||||||
this.addEventListener('reset', this.__boundReset);
|
}
|
||||||
|
this.__registerEventsForLionForm();
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
super.disconnectedCallback();
|
if (super.disconnectedCallback) {
|
||||||
this.removeEventListener('submit', this.__boundSubmit);
|
super.disconnectedCallback();
|
||||||
this.removeEventListener('reset', this.__boundReset);
|
}
|
||||||
|
this.__teardownEventsForLionForm();
|
||||||
}
|
}
|
||||||
|
|
||||||
get formElement() {
|
get formElement() {
|
||||||
return this.querySelector('form');
|
return this.querySelector('form');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
submit() {
|
||||||
|
this.formElement.submit();
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.formElement.reset();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* As we use a native form there is no need for a role
|
* As we use a native form there is no need for a role
|
||||||
*/
|
*/
|
||||||
_setRole() {} // eslint-disable-line class-methods-use-this
|
_setRole() {} // eslint-disable-line class-methods-use-this
|
||||||
|
|
||||||
_submit(ev) {
|
__registerEventsForLionForm() {
|
||||||
ev.preventDefault();
|
this._submit = ev => {
|
||||||
this.submitGroup();
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
this.dispatchEvent(new Event('submit', { bubbles: true }));
|
||||||
|
this.submitGroup();
|
||||||
|
};
|
||||||
|
this.formElement.addEventListener('submit', this._submit);
|
||||||
|
|
||||||
|
this._reset = ev => {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
this.dispatchEvent(new Event('reset', { bubbles: true }));
|
||||||
|
this.resetGroup();
|
||||||
|
};
|
||||||
|
this.formElement.addEventListener('reset', this._reset);
|
||||||
}
|
}
|
||||||
|
|
||||||
_reset(ev) {
|
__teardownEventsForLionForm() {
|
||||||
ev.preventDefault();
|
this.formElement.removeEventListener('submit', this._submit);
|
||||||
this.resetGroup();
|
this.formElement.removeEventListener('rest', this._reset);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { expect, fixture, html } from '@open-wc/testing';
|
import { expect, fixture, html, oneEvent } from '@open-wc/testing';
|
||||||
import { spy } from 'sinon';
|
import { spy } from 'sinon';
|
||||||
|
|
||||||
import '@lion/input/lion-input.js';
|
import '@lion/input/lion-input.js';
|
||||||
|
|
@ -7,7 +7,7 @@ import '@lion/fieldset/lion-fieldset.js';
|
||||||
import '../lion-form.js';
|
import '../lion-form.js';
|
||||||
|
|
||||||
describe('<lion-form>', () => {
|
describe('<lion-form>', () => {
|
||||||
it.skip('has a custom reset that gets triggered by native reset', async () => {
|
it('has a custom reset that gets triggered by native reset', async () => {
|
||||||
const withDefaults = await fixture(html`
|
const withDefaults = await fixture(html`
|
||||||
<lion-form
|
<lion-form
|
||||||
><form>
|
><form>
|
||||||
|
|
@ -39,6 +39,24 @@ describe('<lion-form>', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('dispatches reset events', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-form>
|
||||||
|
<form>
|
||||||
|
<lion-input name="firstName" .modelValue="${'Foo'}"></lion-input>
|
||||||
|
</form>
|
||||||
|
</lion-form>
|
||||||
|
`);
|
||||||
|
|
||||||
|
setTimeout(() => el.reset());
|
||||||
|
const resetEv = await oneEvent(el, 'reset');
|
||||||
|
expect(resetEv).to.be.instanceOf(Event);
|
||||||
|
expect(resetEv.type).to.equal('reset');
|
||||||
|
expect(resetEv.target).to.equal(el);
|
||||||
|
expect(resetEv.bubbles).to.be.true;
|
||||||
|
expect(resetEv.composed).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
it('works with the native submit event (triggered via a button)', async () => {
|
it('works with the native submit event (triggered via a button)', async () => {
|
||||||
const submitSpy = spy();
|
const submitSpy = spy();
|
||||||
const el = await fixture(html`
|
const el = await fixture(html`
|
||||||
|
|
@ -53,4 +71,22 @@ describe('<lion-form>', () => {
|
||||||
button.click();
|
button.click();
|
||||||
expect(submitSpy.callCount).to.equal(1);
|
expect(submitSpy.callCount).to.equal(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('dispatches submit events', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-form>
|
||||||
|
<form>
|
||||||
|
<button type="submit">submit</button>
|
||||||
|
</form>
|
||||||
|
</lion-form>
|
||||||
|
`);
|
||||||
|
const button = el.querySelector('button');
|
||||||
|
setTimeout(() => button.click());
|
||||||
|
const submitEv = await oneEvent(el, 'submit');
|
||||||
|
expect(submitEv).to.be.instanceOf(Event);
|
||||||
|
expect(submitEv.type).to.equal('submit');
|
||||||
|
expect(submitEv.target).to.equal(el);
|
||||||
|
expect(submitEv.bubbles).to.be.true;
|
||||||
|
expect(submitEv.composed).to.be.false;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ import { formatAmount } from './formatters.js';
|
||||||
export class LionInputAmount extends FieldCustomMixin(LocalizeMixin(ObserverMixin(LionInput))) {
|
export class LionInputAmount extends FieldCustomMixin(LocalizeMixin(ObserverMixin(LionInput))) {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
...super.properties,
|
|
||||||
currency: {
|
currency: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { runInteractionStateMixinSuite } from '@lion/field/test-suites/InteractionStateMixin.suite.js';
|
||||||
|
import { runFormatMixinSuite } from '@lion/field/test-suites/FormatMixin.suite.js';
|
||||||
|
import '../lion-input-amount.js';
|
||||||
|
|
||||||
|
const tagString = 'lion-input-amount';
|
||||||
|
|
||||||
|
describe('<lion-input-amount> integrations', () => {
|
||||||
|
runInteractionStateMixinSuite({
|
||||||
|
tagString,
|
||||||
|
suffix: 'lion-input-amount',
|
||||||
|
allowedModelValueTypes: [Number],
|
||||||
|
});
|
||||||
|
|
||||||
|
runFormatMixinSuite({
|
||||||
|
tagString,
|
||||||
|
modelValueType: Number,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,10 +1,18 @@
|
||||||
import { runInteractionStateMixinSuite } from '@lion/field/test-suites/InteractionStateMixin.suite.js';
|
import { runInteractionStateMixinSuite } from '@lion/field/test-suites/InteractionStateMixin.suite.js';
|
||||||
|
import { runFormatMixinSuite } from '@lion/field/test-suites/FormatMixin.suite.js';
|
||||||
import '../lion-input-date.js';
|
import '../lion-input-date.js';
|
||||||
|
|
||||||
|
const tagString = 'lion-input-date';
|
||||||
|
|
||||||
describe('<lion-input-date> integrations', () => {
|
describe('<lion-input-date> integrations', () => {
|
||||||
runInteractionStateMixinSuite({
|
runInteractionStateMixinSuite({
|
||||||
tagString: 'lion-input-date',
|
tagString,
|
||||||
suffix: 'lion-input-date',
|
suffix: tagString,
|
||||||
allowedModelValueTypes: [Date],
|
allowedModelValueTypes: [Date],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
runFormatMixinSuite({
|
||||||
|
tagString,
|
||||||
|
modelValueType: Date,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ import './lion-calendar-overlay-frame.js';
|
||||||
export class LionInputDatepicker extends LionInputDate {
|
export class LionInputDatepicker extends LionInputDate {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
...super.properties,
|
|
||||||
/**
|
/**
|
||||||
* The heading to be added on top of the calendar overlay.
|
* The heading to be added on top of the calendar overlay.
|
||||||
* Naming chosen from an Application Developer perspective.
|
* Naming chosen from an Application Developer perspective.
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,17 @@
|
||||||
import { runInteractionStateMixinSuite } from '@lion/field/test-suites/InteractionStateMixin.suite.js';
|
import { runInteractionStateMixinSuite } from '@lion/field/test-suites/InteractionStateMixin.suite.js';
|
||||||
|
import { runFormatMixinSuite } from '@lion/field/test-suites/FormatMixin.suite.js';
|
||||||
import '../lion-input-datepicker.js';
|
import '../lion-input-datepicker.js';
|
||||||
|
|
||||||
|
const tagString = 'lion-input-datepicker';
|
||||||
describe('<lion-input-datepicker> integrations', () => {
|
describe('<lion-input-datepicker> integrations', () => {
|
||||||
runInteractionStateMixinSuite({
|
runInteractionStateMixinSuite({
|
||||||
tagString: 'lion-input-datepicker',
|
tagString,
|
||||||
suffix: 'lion-input-datepicker',
|
suffix: tagString,
|
||||||
allowedModelValueTypes: [Date],
|
allowedModelValueTypes: [Date],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
runFormatMixinSuite({
|
||||||
|
tagString,
|
||||||
|
modelValueType: Date,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { runFormatMixinSuite } from '@lion/field/test-suites/FormatMixin.suite.js';
|
||||||
|
import '../lion-input-email.js';
|
||||||
|
|
||||||
|
const tagString = 'lion-input-email';
|
||||||
|
|
||||||
|
describe('<lion-input-email> integrations', () => {
|
||||||
|
runFormatMixinSuite({
|
||||||
|
tagString,
|
||||||
|
modelValueType: 'email',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -8,7 +8,7 @@ import { friendlyFormatIBAN } from '@bundled-es-modules/ibantools';
|
||||||
*/
|
*/
|
||||||
export function formatIBAN(modelValue) {
|
export function formatIBAN(modelValue) {
|
||||||
// defensive code because of ibantools
|
// defensive code because of ibantools
|
||||||
if (modelValue === '') {
|
if (!modelValue) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
return friendlyFormatIBAN(modelValue);
|
return friendlyFormatIBAN(modelValue);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import { isValidIBAN } from '@bundled-es-modules/ibantools/ibantools.js';
|
import { isValidIBAN } from '@bundled-es-modules/ibantools/ibantools.js';
|
||||||
|
|
||||||
export const isIBAN = value => isValidIBAN(value);
|
export const isIBAN = value => {
|
||||||
|
return isValidIBAN(value);
|
||||||
|
};
|
||||||
|
|
||||||
export const isIBANValidator = () => [(...params) => ({ isIBAN: isIBAN(...params) })];
|
export const isIBANValidator = () => [(...params) => ({ isIBAN: isIBAN(...params) })];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { runFormatMixinSuite } from '@lion/field/test-suites/FormatMixin.suite.js';
|
||||||
|
import '../lion-input-iban.js';
|
||||||
|
|
||||||
|
const tagString = 'lion-input-iban';
|
||||||
|
|
||||||
|
describe('<lion-input-iban> integrations', () => {
|
||||||
|
runFormatMixinSuite({
|
||||||
|
tagString,
|
||||||
|
modelValueType: 'iban',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -9,7 +9,6 @@ import { LionField } from '@lion/field';
|
||||||
export class LionInput extends LionField {
|
export class LionInput extends LionField {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
...super.properties,
|
|
||||||
/**
|
/**
|
||||||
* A Boolean attribute which, if present, indicates that the user should not be able to edit
|
* A Boolean attribute which, if present, indicates that the user should not be able to edit
|
||||||
* the value of the input. The difference between disabled and readonly is that read-only
|
* the value of the input. The difference between disabled and readonly is that read-only
|
||||||
|
|
@ -22,14 +21,14 @@ export class LionInput extends LionField {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
attribute: 'readonly',
|
attribute: 'readonly',
|
||||||
},
|
},
|
||||||
};
|
type: {
|
||||||
}
|
type: String,
|
||||||
|
reflect: true,
|
||||||
get delegations() {
|
},
|
||||||
return {
|
step: {
|
||||||
...super.delegations,
|
type: Number,
|
||||||
properties: [...super.delegations.properties, 'step'],
|
reflect: true,
|
||||||
attributes: [...super.delegations.attributes, 'step'],
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -48,6 +47,18 @@ export class LionInput extends LionField {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.readOnly = false;
|
||||||
|
this.type = 'text';
|
||||||
|
/**
|
||||||
|
* Only application to type="amount" & type="range"
|
||||||
|
*
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
|
this.step = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
_requestUpdate(name, oldValue) {
|
_requestUpdate(name, oldValue) {
|
||||||
super._requestUpdate(name, oldValue);
|
super._requestUpdate(name, oldValue);
|
||||||
if (name === 'readOnly') {
|
if (name === 'readOnly') {
|
||||||
|
|
@ -60,6 +71,16 @@ export class LionInput extends LionField {
|
||||||
this.__delegateReadOnly();
|
this.__delegateReadOnly();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updated(changedProps) {
|
||||||
|
super.updated(changedProps);
|
||||||
|
if (changedProps.has('type')) {
|
||||||
|
this.inputElement.type = this.type;
|
||||||
|
}
|
||||||
|
if (changedProps.has('step')) {
|
||||||
|
this.inputElement.step = this.step;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
__delegateReadOnly() {
|
__delegateReadOnly() {
|
||||||
if (this.inputElement) {
|
if (this.inputElement) {
|
||||||
this.inputElement.readOnly = this.readOnly;
|
this.inputElement.readOnly = this.readOnly;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,16 @@
|
||||||
import { runInteractionStateMixinSuite } from '@lion/field/test-suites/InteractionStateMixin.suite.js';
|
import { runInteractionStateMixinSuite } from '@lion/field/test-suites/InteractionStateMixin.suite.js';
|
||||||
|
import { runFormatMixinSuite } from '@lion/field/test-suites/FormatMixin.suite.js';
|
||||||
import '../lion-input.js';
|
import '../lion-input.js';
|
||||||
|
|
||||||
|
const tagString = 'lion-input';
|
||||||
|
|
||||||
describe('<lion-input> integrations', () => {
|
describe('<lion-input> integrations', () => {
|
||||||
runInteractionStateMixinSuite({
|
runInteractionStateMixinSuite({
|
||||||
tagString: 'lion-input',
|
tagString,
|
||||||
suffix: 'lion-input',
|
suffix: 'lion-input',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
runFormatMixinSuite({
|
||||||
|
tagString,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -26,4 +26,16 @@ describe('<lion-input>', () => {
|
||||||
const el = await fixture(`<lion-input></lion-input>`);
|
const el = await fixture(`<lion-input></lion-input>`);
|
||||||
expect(el.querySelector('input')).to.equal(el.inputElement);
|
expect(el.querySelector('input')).to.equal(el.inputElement);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('has a type which is reflected to an attribute and is synced down to the native input', async () => {
|
||||||
|
const el = await fixture(`<lion-input></lion-input>`);
|
||||||
|
expect(el.type).to.equal('text');
|
||||||
|
expect(el.getAttribute('type')).to.equal('text');
|
||||||
|
expect(el.inputElement.getAttribute('type')).to.equal('text');
|
||||||
|
|
||||||
|
el.type = 'foo';
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(el.getAttribute('type')).to.equal('foo');
|
||||||
|
expect(el.inputElement.getAttribute('type')).to.equal('foo');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,6 @@ export class LionSelectRich extends FormRegistrarMixin(
|
||||||
) {
|
) {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
...super.properties,
|
|
||||||
checkedValue: {
|
checkedValue: {
|
||||||
type: Object,
|
type: Object,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lion/core": "^0.1.13",
|
"@lion/core": "^0.1.13",
|
||||||
"@lion/input": "^0.1.44",
|
"@lion/field": "^0.1.42",
|
||||||
"autosize": "4.0.2"
|
"autosize": "4.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import autosize from 'autosize/src/autosize.js';
|
import autosize from 'autosize/src/autosize.js';
|
||||||
import { LionInput } from '@lion/input';
|
import { LionField } from '@lion/field';
|
||||||
import { css } from '@lion/core';
|
import { css } from '@lion/core';
|
||||||
import { ObserverMixin } from '@lion/core/src/ObserverMixin.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* LionTextarea: extension of lion-field with native input element in place and user friendly API
|
* LionTextarea: extension of lion-field with native input element in place and user friendly API
|
||||||
|
|
@ -9,31 +8,17 @@ import { ObserverMixin } from '@lion/core/src/ObserverMixin.js';
|
||||||
* @customElement
|
* @customElement
|
||||||
* @extends LionInput
|
* @extends LionInput
|
||||||
*/
|
*/
|
||||||
export class LionTextarea extends ObserverMixin(LionInput) {
|
export class LionTextarea extends LionField {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
...super.properties,
|
|
||||||
maxRows: {
|
maxRows: {
|
||||||
type: Number,
|
type: Number,
|
||||||
attribute: 'max-rows',
|
attribute: 'max-rows',
|
||||||
},
|
},
|
||||||
};
|
rows: {
|
||||||
}
|
type: Number,
|
||||||
|
reflect: true,
|
||||||
get delegations() {
|
},
|
||||||
return {
|
|
||||||
...super.delegations,
|
|
||||||
target: () => this.inputElement,
|
|
||||||
properties: [...super.delegations.properties, 'rows'],
|
|
||||||
attributes: [...super.delegations.attributes, 'rows'],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static get asyncObservers() {
|
|
||||||
return {
|
|
||||||
...super.asyncObservers,
|
|
||||||
resizeTextarea: ['maxRows', 'modelValue'],
|
|
||||||
setTextareaMaxHeight: ['maxRows', 'rows'],
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -69,6 +54,24 @@ export class LionTextarea extends ObserverMixin(LionInput) {
|
||||||
autosize.destroy(this.inputElement);
|
autosize.destroy(this.inputElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updated(changedProperties) {
|
||||||
|
super.updated(changedProperties);
|
||||||
|
if (changedProperties.has('rows')) {
|
||||||
|
const native = this.inputElement;
|
||||||
|
if (native) {
|
||||||
|
native.rows = this.rows;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changedProperties.has('modelValue')) {
|
||||||
|
this.resizeTextarea();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changedProperties.has('maxRows') || changedProperties.has('rows')) {
|
||||||
|
this.setTextareaMaxHeight();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* To support maxRows we need to set max-height of the textarea
|
* To support maxRows we need to set max-height of the textarea
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
10
packages/textarea/test/lion-input-iban-integrations.test.js
Normal file
10
packages/textarea/test/lion-input-iban-integrations.test.js
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { runFormatMixinSuite } from '@lion/field/test-suites/FormatMixin.suite.js';
|
||||||
|
import '../lion-textarea.js';
|
||||||
|
|
||||||
|
const tagString = 'lion-textarea';
|
||||||
|
|
||||||
|
describe('<lion-textarea> integrations', () => {
|
||||||
|
runFormatMixinSuite({
|
||||||
|
tagString,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -16,12 +16,28 @@ describe('<lion-textarea>', () => {
|
||||||
expect(el.querySelector('textarea').nodeName).to.equal('TEXTAREA');
|
expect(el.querySelector('textarea').nodeName).to.equal('TEXTAREA');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has default minRows and maxRows', async () => {
|
it('has .rows=2 and .maxRows=6', async () => {
|
||||||
const el = await fixture(`<lion-textarea></lion-textarea>`);
|
const el = await fixture(`<lion-textarea></lion-textarea>`);
|
||||||
expect(el.rows).to.equal(2);
|
expect(el.rows).to.equal(2);
|
||||||
expect(el.maxRows).to.equal(6);
|
expect(el.maxRows).to.equal(6);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('has .rows=2 and rows="2" by default', async () => {
|
||||||
|
const el = await fixture(`<lion-textarea>foo</lion-textarea>`);
|
||||||
|
expect(el.rows).to.equal(2);
|
||||||
|
expect(el.getAttribute('rows')).to.be.equal('2');
|
||||||
|
expect(el.inputElement.rows).to.equal(2);
|
||||||
|
expect(el.inputElement.getAttribute('rows')).to.be.equal('2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sync rows down to the native textarea', async () => {
|
||||||
|
const el = await fixture(`<lion-textarea rows="8">foo</lion-textarea>`);
|
||||||
|
expect(el.rows).to.equal(8);
|
||||||
|
expect(el.getAttribute('rows')).to.be.equal('8');
|
||||||
|
expect(el.inputElement.rows).to.equal(8);
|
||||||
|
expect(el.inputElement.getAttribute('rows')).to.be.equal('8');
|
||||||
|
});
|
||||||
|
|
||||||
it('disables user resize behavior', async () => {
|
it('disables user resize behavior', async () => {
|
||||||
if (!hasBrowserResizeSupport()) {
|
if (!hasBrowserResizeSupport()) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -102,12 +118,9 @@ describe('<lion-textarea>', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('stops shrinking after property "rows" is reached', async () => {
|
it('stops shrinking after property "rows" is reached', async () => {
|
||||||
const el = await fixture(
|
const el = await fixture(html`
|
||||||
html`
|
<lion-textarea rows="1" max-rows="3"></lion-textarea>
|
||||||
<lion-textarea rows="1" max-rows="3"></lion-textarea>
|
`);
|
||||||
`,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(el.scrollHeight).to.be.equal(el.clientHeight);
|
expect(el.scrollHeight).to.be.equal(el.clientHeight);
|
||||||
const oneRowHeight = el.clientHeight;
|
const oneRowHeight = el.clientHeight;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,6 @@ export const ValidateMixin = dedupeMixin(
|
||||||
|
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
...super.properties,
|
|
||||||
/**
|
/**
|
||||||
* List of validators that should set the input to invalid
|
* List of validators that should set the input to invalid
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ const tagString = defineCE(
|
||||||
class extends ValidateMixin(LionLitElement) {
|
class extends ValidateMixin(LionLitElement) {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
...super.properties,
|
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
|
|
@ -658,7 +657,6 @@ describe('ValidateMixin', () => {
|
||||||
class extends ValidateMixin(LionLitElement) {
|
class extends ValidateMixin(LionLitElement) {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
...super.properties,
|
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
|
|
@ -782,7 +780,6 @@ describe('ValidateMixin', () => {
|
||||||
class extends ValidateMixin(LionLitElement) {
|
class extends ValidateMixin(LionLitElement) {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
...super.properties,
|
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
|
|
@ -934,7 +931,6 @@ describe('ValidateMixin', () => {
|
||||||
class extends ValidateMixin(LionLitElement) {
|
class extends ValidateMixin(LionLitElement) {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
...super.properties,
|
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
|
|
@ -1220,7 +1216,7 @@ describe('ValidateMixin', () => {
|
||||||
const orderName = defineCE(
|
const orderName = defineCE(
|
||||||
class extends ValidateMixin(LionLitElement) {
|
class extends ValidateMixin(LionLitElement) {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return { ...super.properties, modelValue: { type: String } };
|
return { modelValue: { type: String } };
|
||||||
}
|
}
|
||||||
|
|
||||||
static get localizeNamespaces() {
|
static get localizeNamespaces() {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue