@lion/field
Advanced tools
Comparing version 0.1.42 to 0.1.43
@@ -6,2 +6,18 @@ # Change Log | ||
## [0.1.43](https://github.com/ing-bank/lion/compare/@lion/field@0.1.42...@lion/field@0.1.43) (2019-08-14) | ||
### Bug Fixes | ||
* **field:** getter/setter for selectionStart/End instead of delegation ([07eddb3](https://github.com/ing-bank/lion/commit/07eddb3)) | ||
* **field:** move type property to input & add step property to input ([5d893f3](https://github.com/ing-bank/lion/commit/5d893f3)) | ||
* **field:** no delegate in FocusMixin; sync focused, redispatch events ([88f5264](https://github.com/ing-bank/lion/commit/88f5264)) | ||
* **field:** sync name down instead of delegating ([d2f4e3c](https://github.com/ing-bank/lion/commit/d2f4e3c)) | ||
* **field:** sync type down instead of delegating ([13b2740](https://github.com/ing-bank/lion/commit/13b2740)) | ||
* **field:** value delegation compatible with FormatMixin ([3217c1a](https://github.com/ing-bank/lion/commit/3217c1a)) | ||
## [0.1.42](https://github.com/ing-bank/lion/compare/@lion/field@0.1.41...@lion/field@0.1.42) (2019-08-07) | ||
@@ -8,0 +24,0 @@ |
{ | ||
"name": "@lion/field", | ||
"version": "0.1.42", | ||
"version": "0.1.43", | ||
"description": "Fields are the most fundamental building block of the Form System", | ||
@@ -37,3 +37,3 @@ "author": "ing-bank", | ||
"@lion/core": "^0.1.13", | ||
"@lion/validate": "^0.2.25" | ||
"@lion/validate": "^0.2.26" | ||
}, | ||
@@ -46,3 +46,3 @@ "devDependencies": { | ||
}, | ||
"gitHead": "f32aab653391872e23a25ecacb17111ad0b941a0" | ||
"gitHead": "6cd0e34abc5f4c175e9a788bb860f9fa72fa808a" | ||
} |
@@ -16,3 +16,2 @@ import { dedupeMixin, nothing } from '@lion/core'; | ||
return { | ||
...super.properties, | ||
/** | ||
@@ -19,0 +18,0 @@ * When no light dom defined and prop set |
@@ -1,2 +0,2 @@ | ||
import { dedupeMixin, DelegateMixin } from '@lion/core'; | ||
import { dedupeMixin } from '@lion/core'; | ||
@@ -6,48 +6,116 @@ export const FocusMixin = dedupeMixin( | ||
// eslint-disable-next-line no-unused-vars, max-len, no-shadow | ||
class FocusMixin extends DelegateMixin(superclass) { | ||
get delegations() { | ||
class FocusMixin extends superclass { | ||
static get properties() { | ||
return { | ||
...super.delegations, | ||
target: () => this.inputElement, | ||
events: [...super.delegations.events, 'focus', 'blur'], // since these events don't bubble | ||
methods: [...super.delegations.methods, 'focus', 'blur'], | ||
properties: [...super.delegations.properties, 'onfocus', 'onblur', 'autofocus'], | ||
attributes: [...super.delegations.attributes, 'onfocus', 'onblur', 'autofocus'], | ||
focused: { | ||
type: Boolean, | ||
reflect: true, | ||
}, | ||
}; | ||
} | ||
constructor() { | ||
super(); | ||
this.focused = false; | ||
} | ||
connectedCallback() { | ||
super.connectedCallback(); | ||
this._onFocus = this._onFocus.bind(this); | ||
this._onBlur = this._onBlur.bind(this); | ||
this.inputElement.addEventListener('focusin', this._onFocus); | ||
this.inputElement.addEventListener('focusout', this._onBlur); | ||
if (super.connectedCallback) { | ||
super.connectedCallback(); | ||
} | ||
this.__registerEventsForFocusMixin(); | ||
} | ||
disconnectedCallback() { | ||
super.disconnectedCallback(); | ||
this.inputElement.removeEventListener('focusin', this._onFocus); | ||
this.inputElement.removeEventListener('focusout', this._onBlur); | ||
if (super.disconnectedCallback) { | ||
super.disconnectedCallback(); | ||
} | ||
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 | ||
* return this.inputElement === document.activeElement; | ||
* @deprecated | ||
*/ | ||
get focused() { | ||
return this.classList.contains('state-focused'); | ||
} | ||
_onFocus() { | ||
if (super._onFocus) super._onFocus(); | ||
this.classList.add('state-focused'); | ||
if (super._onFocus) { | ||
super._onFocus(); | ||
} | ||
this.focused = true; | ||
} | ||
/** | ||
* Functions should be private | ||
* | ||
* @deprecated | ||
*/ | ||
_onBlur() { | ||
if (super._onBlur) super._onBlur(); | ||
this.classList.remove('state-focused'); | ||
if (super._onBlur) { | ||
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); | ||
} | ||
}, | ||
); |
@@ -56,4 +56,2 @@ /* eslint-disable class-methods-use-this */ | ||
return { | ||
...super.properties, | ||
/** | ||
@@ -60,0 +58,0 @@ * The model value is the result of the parser function(when available). |
@@ -21,3 +21,2 @@ import { html, css, nothing, dedupeMixin, SlotMixin } from '@lion/core'; | ||
return { | ||
...super.properties, | ||
/** | ||
@@ -24,0 +23,0 @@ * A list of ids that will be put on the inputElement as a serialized string |
@@ -21,3 +21,2 @@ import { dedupeMixin } from '@lion/core'; | ||
return { | ||
...super.properties, | ||
/** | ||
@@ -24,0 +23,0 @@ * True when user has focused and left(blurred) the field. |
@@ -1,4 +0,4 @@ | ||
import { DelegateMixin, SlotMixin, LitElement } from '@lion/core'; | ||
import { SlotMixin, LitElement } from '@lion/core'; | ||
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'; | ||
@@ -38,29 +38,8 @@ import { ValidateMixin } from '@lion/validate'; | ||
FocusMixin( | ||
FormatMixin( | ||
ValidateMixin( | ||
CssClassMixin(ElementMixin(DelegateMixin(SlotMixin(ObserverMixin(LitElement))))), | ||
), | ||
), | ||
FormatMixin(ValidateMixin(DisabledMixin(ElementMixin(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() { | ||
return { | ||
...super.properties, | ||
submitted: { | ||
@@ -70,5 +49,39 @@ // make sure validation can be triggered based on observer | ||
}, | ||
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 | ||
@@ -87,10 +100,15 @@ set value(value) { | ||
static get asyncObservers() { | ||
return { | ||
...super.asyncObservers, | ||
_setDisabledClass: ['disabled'], | ||
}; | ||
constructor() { | ||
super(); | ||
this.name = ''; | ||
this.submitted = false; | ||
} | ||
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(); | ||
@@ -100,4 +118,2 @@ | ||
this.inputElement.addEventListener('change', this._onChange); | ||
this._delegateInitialValueAttr(); | ||
this._setDisabledClass(); | ||
this.classList.add('form-field'); // eslint-disable-line | ||
@@ -119,4 +135,18 @@ } | ||
_setDisabledClass() { | ||
this.classList[this.disabled ? 'add' : 'remove']('state-disabled'); | ||
updated(changedProps) { | ||
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; | ||
} | ||
} | ||
@@ -123,0 +153,0 @@ |
import { defineCE } from '@open-wc/testing'; | ||
import { runInteractionStateMixinSuite } from '../test-suites/InteractionStateMixin.suite.js'; | ||
import '../lion-field.js'; | ||
import { runFormatMixinSuite } from '../test-suites/FormatMixin.suite.js'; | ||
@@ -22,2 +23,6 @@ const fieldTagString = defineCE( | ||
}); | ||
runFormatMixinSuite({ | ||
tagString: fieldTagString, | ||
}); | ||
}); |
@@ -14,3 +14,2 @@ import { expect, fixture, defineCE } from '@open-wc/testing'; | ||
return { | ||
...super.properties, | ||
modelValue: { | ||
@@ -17,0 +16,0 @@ type: String, |
@@ -1,313 +0,3 @@ | ||
import { expect, fixture, html, aTimeout, defineCE, unsafeStatic } from '@open-wc/testing'; | ||
import sinon from 'sinon'; | ||
import { runFormatMixinSuite } from '../test-suites/FormatMixin.suite.js'; | ||
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 })); | ||
} | ||
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'); | ||
}); | ||
}); | ||
}); | ||
runFormatMixinSuite(); |
@@ -16,3 +16,2 @@ import { expect, fixture, html, defineCE, unsafeStatic } from '@open-wc/testing'; | ||
return { | ||
...super.properties, | ||
modelValue: { | ||
@@ -19,0 +18,0 @@ type: String, |
@@ -34,20 +34,20 @@ import { | ||
it(`puts a unique id "${tagString}-[hash]" on the native input`, async () => { | ||
const lionField = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`); | ||
expect(lionField.$$slot('input').id).to.equal(lionField._inputId); | ||
const el = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`); | ||
expect(el.$$slot('input').id).to.equal(el._inputId); | ||
}); | ||
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(); | ||
lionField.addEventListener('focus', cbFocusHost); | ||
el.addEventListener('focus', cbFocusHost); | ||
const cbFocusNativeInput = sinon.spy(); | ||
lionField.inputElement.addEventListener('focus', cbFocusNativeInput); | ||
el.inputElement.addEventListener('focus', cbFocusNativeInput); | ||
const cbBlurHost = sinon.spy(); | ||
lionField.addEventListener('blur', cbBlurHost); | ||
el.addEventListener('blur', cbBlurHost); | ||
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); | ||
@@ -58,12 +58,12 @@ expect(cbFocusNativeInput.callCount).to.equal(1); | ||
await triggerBlurFor(lionField); | ||
await triggerBlurFor(el); | ||
expect(cbBlurHost.callCount).to.equal(1); | ||
expect(cbBlurNativeInput.callCount).to.equal(1); | ||
await triggerFocusFor(lionField); | ||
expect(document.activeElement).to.equal(lionField.inputElement); | ||
await triggerFocusFor(el); | ||
expect(document.activeElement).to.equal(el.inputElement); | ||
expect(cbFocusHost.callCount).to.equal(2); | ||
expect(cbFocusNativeInput.callCount).to.equal(2); | ||
await triggerBlurFor(lionField); | ||
await triggerBlurFor(el); | ||
expect(cbBlurHost.callCount).to.equal(2); | ||
@@ -73,85 +73,85 @@ 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('can be disabled via attribute', async () => { | ||
const elDisabled = await fixture(`<${tagString} disabled>${inputSlotString}</${tagString}>`); | ||
expect(elDisabled.disabled).to.equal(true); | ||
expect(elDisabled.inputElement.disabled).to.equal(true); | ||
}); | ||
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 property', async () => { | ||
const el = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`); | ||
el.disabled = true; | ||
await el.updateComplete; | ||
expect(el.inputElement.disabled).to.equal(true); | ||
}); | ||
it('can be disabled via attribute', async () => { | ||
const lionFieldDisabled = await fixture( | ||
`<${tagString} disabled>${inputSlotString}</${tagString}>`, | ||
); | ||
expect(lionFieldDisabled.disabled).to.equal(true); | ||
expect(lionFieldDisabled.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 disabled via property', async () => { | ||
const lionField = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`); | ||
lionField.disabled = true; | ||
await lionField.updateComplete; | ||
expect(lionField.inputElement.disabled).to.equal(true); | ||
}); | ||
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}>`, | ||
); | ||
lionField.clear(); | ||
expect(lionField.value).to.equal(''); | ||
lionField.value = 'Some value from property'; | ||
expect(lionField.value).to.equal('Some value from property'); | ||
lionField.clear(); | ||
expect(lionField.value).to.equal(''); | ||
el.clear(); | ||
expect(el.value).to.equal(''); | ||
el.value = 'Some value from property'; | ||
expect(el.value).to.equal('Some value from property'); | ||
el.clear(); | ||
expect(el.value).to.equal(''); | ||
}); | ||
it('reads initial value from attribute value', async () => { | ||
const lionField = await fixture(`<${tagString} value="one">${inputSlotString}</${tagString}>`); | ||
expect(lionField.$$slot('input').value).to.equal('one'); | ||
const el = await fixture(`<${tagString} value="one">${inputSlotString}</${tagString}>`); | ||
expect(el.$$slot('input').value).to.equal('one'); | ||
}); | ||
it('delegates value property', async () => { | ||
const lionField = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`); | ||
expect(lionField.$$slot('input').value).to.equal(''); | ||
lionField.value = 'one'; | ||
expect(lionField.value).to.equal('one'); | ||
expect(lionField.$$slot('input').value).to.equal('one'); | ||
const el = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`); | ||
expect(el.$$slot('input').value).to.equal(''); | ||
el.value = 'one'; | ||
expect(el.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) | ||
// and make it act on this.value again | ||
it('has a class "state-filled" if this.value is filled', async () => { | ||
const lionField = await fixture( | ||
`<${tagString} value="filled">${inputSlotString}</${tagString}>`, | ||
); | ||
expect(lionField.classList.contains('state-filled')).to.equal(true); | ||
lionField.value = ''; | ||
await lionField.updateComplete; | ||
expect(lionField.classList.contains('state-filled')).to.equal(false); | ||
lionField.value = 'bla'; | ||
await lionField.updateComplete; | ||
expect(lionField.classList.contains('state-filled')).to.equal(true); | ||
const el = await fixture(`<${tagString} value="filled">${inputSlotString}</${tagString}>`); | ||
expect(el.classList.contains('state-filled')).to.equal(true); | ||
el.value = ''; | ||
await el.updateComplete; | ||
expect(el.classList.contains('state-filled')).to.equal(false); | ||
el.value = 'bla'; | ||
await el.updateComplete; | ||
expect(el.classList.contains('state-filled')).to.equal(true); | ||
}); | ||
it('preserves the caret position on value change for native text fields (input|textarea)', async () => { | ||
const lionField = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`); | ||
await triggerFocusFor(lionField); | ||
await lionField.updateComplete; | ||
lionField.inputElement.value = 'hello world'; | ||
lionField.inputElement.selectionStart = 2; | ||
lionField.inputElement.selectionEnd = 2; | ||
lionField.value = 'hey there universe'; | ||
expect(lionField.inputElement.selectionStart).to.equal(2); | ||
expect(lionField.inputElement.selectionEnd).to.equal(2); | ||
const el = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`); | ||
await triggerFocusFor(el); | ||
await el.updateComplete; | ||
el.inputElement.value = 'hello world'; | ||
el.inputElement.selectionStart = 2; | ||
el.inputElement.selectionEnd = 2; | ||
el.value = 'hey there universe'; | ||
expect(el.inputElement.selectionStart).to.equal(2); | ||
expect(el.inputElement.selectionEnd).to.equal(2); | ||
}); | ||
@@ -161,18 +161,16 @@ | ||
it('has a class "state-disabled"', async () => { | ||
const lionField = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`); | ||
expect(lionField.classList.contains('state-disabled')).to.equal(false); | ||
expect(lionField.inputElement.hasAttribute('disabled')).to.equal(false); | ||
const el = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`); | ||
expect(el.classList.contains('state-disabled')).to.equal(false); | ||
expect(el.inputElement.hasAttribute('disabled')).to.equal(false); | ||
lionField.disabled = true; | ||
await lionField.updateComplete; | ||
el.disabled = true; | ||
await el.updateComplete; | ||
await aTimeout(); | ||
expect(lionField.classList.contains('state-disabled')).to.equal(true); | ||
expect(lionField.inputElement.hasAttribute('disabled')).to.equal(true); | ||
expect(el.classList.contains('state-disabled')).to.equal(true); | ||
expect(el.inputElement.hasAttribute('disabled')).to.equal(true); | ||
const disabledlionField = await fixture( | ||
`<${tagString} disabled>${inputSlotString}</${tagString}>`, | ||
); | ||
expect(disabledlionField.classList.contains('state-disabled')).to.equal(true); | ||
expect(disabledlionField.inputElement.hasAttribute('disabled')).to.equal(true); | ||
const disabledel = await fixture(`<${tagString} disabled>${inputSlotString}</${tagString}>`); | ||
expect(disabledel.classList.contains('state-disabled')).to.equal(true); | ||
expect(disabledel.inputElement.hasAttribute('disabled')).to.equal(true); | ||
}); | ||
@@ -195,3 +193,3 @@ | ||
~~~`, async () => { | ||
const lionField = await fixture(`<${tagString}> | ||
const el = await fixture(`<${tagString}> | ||
<label slot="label">My Name</label> | ||
@@ -203,11 +201,7 @@ ${inputSlotString} | ||
`); | ||
const nativeInput = lionField.$$slot('input'); | ||
const nativeInput = el.$$slot('input'); | ||
expect(nativeInput.getAttribute('aria-labelledby')).to.equal(` label-${lionField._inputId}`); | ||
expect(nativeInput.getAttribute('aria-describedby')).to.contain( | ||
` help-text-${lionField._inputId}`, | ||
); | ||
expect(nativeInput.getAttribute('aria-describedby')).to.contain( | ||
` feedback-${lionField._inputId}`, | ||
); | ||
expect(nativeInput.getAttribute('aria-labelledby')).to.equal(` label-${el._inputId}`); | ||
expect(nativeInput.getAttribute('aria-describedby')).to.contain(` help-text-${el._inputId}`); | ||
expect(nativeInput.getAttribute('aria-describedby')).to.contain(` feedback-${el._inputId}`); | ||
}); | ||
@@ -217,3 +211,3 @@ | ||
(via attribute data-label) and in describedby (via attribute data-description)`, async () => { | ||
const lionField = await fixture(`<${tagString}> | ||
const el = await fixture(`<${tagString}> | ||
${inputSlotString} | ||
@@ -227,8 +221,8 @@ <span slot="before" data-label>[before]</span> | ||
const nativeInput = lionField.$$slot('input'); | ||
const nativeInput = el.$$slot('input'); | ||
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( | ||
` prefix-${lionField._inputId} suffix-${lionField._inputId}`, | ||
` prefix-${el._inputId} suffix-${el._inputId}`, | ||
); | ||
@@ -302,9 +296,9 @@ }); | ||
} | ||
const lionField = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`); | ||
const feedbackEl = lionField._feedbackElement; | ||
const el = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`); | ||
const feedbackEl = el._feedbackElement; | ||
lionField.modelValue = 'a@b.nl'; | ||
lionField.errorValidators = [[hasX]]; | ||
el.modelValue = 'a@b.nl'; | ||
el.errorValidators = [[hasX]]; | ||
expect(lionField.error.hasX).to.equal(true); | ||
expect(el.error.hasX).to.equal(true); | ||
expect(feedbackEl.innerText.trim()).to.equal( | ||
@@ -314,6 +308,6 @@ '', | ||
); | ||
lionField.dirty = true; | ||
lionField.touched = true; | ||
lionField.modelValue = 'ab@c.nl'; // retrigger validation | ||
await lionField.updateComplete; | ||
el.dirty = true; | ||
el.touched = true; | ||
el.modelValue = 'ab@c.nl'; // retrigger validation | ||
await el.updateComplete; | ||
@@ -325,6 +319,6 @@ expect(feedbackEl.innerText.trim()).to.equal( | ||
lionField.touched = false; | ||
lionField.dirty = false; | ||
lionField.prefilled = true; | ||
await lionField.updateComplete; | ||
el.touched = false; | ||
el.dirty = false; | ||
el.prefilled = true; | ||
await el.updateComplete; | ||
expect(feedbackEl.innerText.trim()).to.equal( | ||
@@ -337,3 +331,3 @@ 'This is error message for hasX', | ||
it('can be required', async () => { | ||
const lionField = await fixture(html` | ||
const el = await fixture(html` | ||
<${tag} | ||
@@ -343,5 +337,5 @@ .errorValidators=${[['required']]} | ||
`); | ||
expect(lionField.error.required).to.be.true; | ||
lionField.modelValue = 'cat'; | ||
expect(lionField.error.required).to.be.undefined; | ||
expect(el.error.required).to.be.true; | ||
el.modelValue = 'cat'; | ||
expect(el.error.required).to.be.undefined; | ||
}); | ||
@@ -354,3 +348,3 @@ | ||
} | ||
const lionField = await fixture(html` | ||
const el = await fixture(html` | ||
<${tag} | ||
@@ -364,11 +358,11 @@ .modelValue=${'init-string'} | ||
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(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(lionField.value).to.equal('foo'); | ||
expect(el.value).to.equal('foo'); | ||
}); | ||
@@ -379,3 +373,3 @@ }); | ||
it('renders correctly all slot elements in light DOM', async () => { | ||
const lionField = await fixture(` | ||
const el = await fixture(` | ||
<${tagString}> | ||
@@ -404,4 +398,4 @@ <label slot="label">[label]</label> | ||
names.forEach(slotName => { | ||
lionField.querySelector(`[slot="${slotName}"]`).setAttribute('test-me', 'ok'); | ||
const slot = lionField.shadowRoot.querySelector(`slot[name="${slotName}"]`); | ||
el.querySelector(`[slot="${slotName}"]`).setAttribute('test-me', 'ok'); | ||
const slot = el.shadowRoot.querySelector(`slot[name="${slotName}"]`); | ||
const assignedNodes = slot.assignedNodes(); | ||
@@ -415,8 +409,2 @@ expect(assignedNodes.length).to.equal(1); | ||
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 () => { | ||
@@ -430,38 +418,4 @@ const el = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`); | ||
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 () => { | ||
const lionField = await fixture(html` | ||
const el = await fixture(html` | ||
<${tag} | ||
@@ -472,8 +426,8 @@ .modelValue=${'Some text to select'} | ||
lionField.selectionStart = 5; | ||
lionField.selectionEnd = 12; | ||
expect(lionField.inputElement.selectionStart).to.equal(5); | ||
expect(lionField.inputElement.selectionEnd).to.equal(12); | ||
el.selectionStart = 5; | ||
el.selectionEnd = 12; | ||
expect(el.inputElement.selectionStart).to.equal(5); | ||
expect(el.inputElement.selectionEnd).to.equal(12); | ||
}); | ||
}); | ||
}); |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
199844
33
2801
Updated@lion/validate@^0.2.26