diff --git a/.changeset/beige-drinks-trade.md b/.changeset/beige-drinks-trade.md new file mode 100644 index 000000000..57e3b38bc --- /dev/null +++ b/.changeset/beige-drinks-trade.md @@ -0,0 +1,18 @@ +--- +'@lion/form-core': minor +'@lion/core': minor +'@lion/fieldset': patch +'@lion/select-rich': patch +--- + +Form-core typings + +#### Features + +Provided typings for the form-core package and core package. +This also means that mixins that previously had implicit dependencies, now have explicit ones. + +#### Patches + +- lion-select-rich: invoker selectedElement now also clones text nodes (fix) +- fieldset: runs a FormGroup suite diff --git a/demo/README.md b/demo/README.md index 8e0f442d7..3f3663a7d 100644 --- a/demo/README.md +++ b/demo/README.md @@ -203,13 +203,9 @@ Excellent! Lea can now use the tabs component like so: ```html Info - - Info page with lots of information about us. - + Info page with lots of information about us. Work - - Work page that showcases our work. - + Work page that showcases our work. ``` diff --git a/demo/docs/20-lea-tabs.md b/demo/docs/20-lea-tabs.md index 4a38545e8..f909bf81e 100644 --- a/demo/docs/20-lea-tabs.md +++ b/demo/docs/20-lea-tabs.md @@ -23,13 +23,9 @@ export default { export const main = () => html` Info - - Info page with lots of information about us. - + Info page with lots of information about us. Work - - Work page that showcases our work. - + Work page that showcases our work. `; ``` @@ -51,13 +47,9 @@ import '@lion/tabs/lea-tabs.js'; ```html Info - - Info page with lots of information about us. - + Info page with lots of information about us. Work - - Work page that showcases our work. - + Work page that showcases our work. ``` @@ -71,13 +63,9 @@ You can set the `selectedIndex` to select a certain tab. export const selectedIndex = () => html` Info - - Info page with lots of information about us. - + Info page with lots of information about us. Work - - Work page that showcases our work. - + Work page that showcases our work. `; ``` @@ -93,12 +81,8 @@ export const slotsOrder = () => html` Info Work - - Info page with lots of information about us. - - - Work page that showcases our work. - + Info page with lots of information about us. + Work page that showcases our work. `; ``` @@ -122,9 +106,7 @@ export const distributeNewElements = () => { render() { return html`

Append

- + tab 1 panel 1 @@ -133,9 +115,7 @@ export const distributeNewElements = () => {

Push

- + tab 1 panel 1 diff --git a/docs/extending-documentation.md b/docs/extending-documentation.md index d76d410e8..b83158016 100644 --- a/docs/extending-documentation.md +++ b/docs/extending-documentation.md @@ -265,13 +265,9 @@ export const specialFeature = () => html` Info - - Info page with lots of information about us. - + Info page with lots of information about us. Work - - Work page that showcases our work. - + Work page that showcases our work. `; ``` diff --git a/packages/accordion/README.md b/packages/accordion/README.md index 6c35da962..f5b169580 100644 --- a/packages/accordion/README.md +++ b/packages/accordion/README.md @@ -21,9 +21,7 @@ export const main = () => html`

-

- Lorem ipsum dolor sit, amet consectetur adipisicing elit. -

+

Lorem ipsum dolor sit, amet consectetur adipisicing elit.

@@ -56,9 +54,7 @@ import '@lion/accordion/lion-accordion.js';

-

- Lorem ipsum dolor sit, amet consectetur adipisicing elit. -

+

Lorem ipsum dolor sit, amet consectetur adipisicing elit.

@@ -83,9 +79,7 @@ export const expanded = () => html`

-

- Lorem ipsum dolor sit, amet consectetur adipisicing elit. -

+

Lorem ipsum dolor sit, amet consectetur adipisicing elit.

@@ -109,9 +103,7 @@ export const slotsOrder = () => html`

-

- Lorem ipsum dolor sit, amet consectetur adipisicing elit. -

+

Lorem ipsum dolor sit, amet consectetur adipisicing elit.

@@ -152,9 +144,7 @@ export const distributeNewElement = () => {

content 2

- +

Push

@@ -173,9 +163,7 @@ export const distributeNewElement = () => { `, )} - + `; } constructor() { diff --git a/packages/calendar/src/LionCalendar.js b/packages/calendar/src/LionCalendar.js index 7d9f2bfec..b3f716417 100644 --- a/packages/calendar/src/LionCalendar.js +++ b/packages/calendar/src/LionCalendar.js @@ -276,9 +276,7 @@ export class LionCalendar extends LocalizeMixin(LitElement) { return html`
${this.__renderPreviousButton('Month', previousMonth, previousYear)} -

- ${month} -

+

${month}

${this.__renderNextButton('Month', nextMonth, nextYear)}
`; @@ -291,9 +289,7 @@ export class LionCalendar extends LocalizeMixin(LitElement) { return html`
${this.__renderPreviousButton('FullYear', month, previousYear)} -

- ${year} -

+

${year}

${this.__renderNextButton('FullYear', month, nextYear)}
`; diff --git a/packages/calendar/src/utils/dayTemplate.js b/packages/calendar/src/utils/dayTemplate.js index 65f42c2ae..783a255ac 100644 --- a/packages/calendar/src/utils/dayTemplate.js +++ b/packages/calendar/src/utils/dayTemplate.js @@ -67,9 +67,7 @@ export function dayTemplate(day, { weekdays, monthsLabels = defaultMonthLabels } ?current-month=${day.currentMonth} ?next-month=${day.nextMonth} > - - ${day.date.getDate()} - + ${day.date.getDate()} `; diff --git a/packages/collapsible/README.md b/packages/collapsible/README.md index 89d306321..4e9c683b6 100644 --- a/packages/collapsible/README.md +++ b/packages/collapsible/README.md @@ -52,9 +52,7 @@ import '@lion/collapsible/lion-collapsible.js'; ```html -
- Extra content -
+
Extra content
``` @@ -97,12 +95,8 @@ export const methods = () => html` - - + + `; ``` @@ -140,9 +134,7 @@ A custom template can be specified to the `invoker` slot. It can be any button o ```js preview-story export const customInvokerTemplate = () => html` - +
Most definitions of cars say that they run primarily on roads, seat one to eight people, have four tires, and mainly transport people rather than goods. @@ -170,9 +162,7 @@ export const customAnimation = () => html` vehicle.
- +
Motorcycle design varies greatly to suit a range of different purposes: long distance travel, commuting, cruising, sport including racing, and off-road riding. Motorcycling is @@ -186,9 +176,7 @@ export const customAnimation = () => html` A car (or automobile) is a wheeled motor vehicle used for transportation.
- +
Most definitions of cars say that they run primarily on roads, seat one to eight people, have four tires, and mainly transport people rather than goods. diff --git a/packages/core/docs/guidelines/30-guidelines-scoped-elements.md b/packages/core/docs/guidelines/30-guidelines-scoped-elements.md index 031361727..42a893ed1 100644 --- a/packages/core/docs/guidelines/30-guidelines-scoped-elements.md +++ b/packages/core/docs/guidelines/30-guidelines-scoped-elements.md @@ -40,7 +40,9 @@ Since Scoped Elements changes tagnames under the hood, a tagname querySelector s like this: ```js -this.querySelector(this.constructor.getScopedTagName('lion-input', this.constructor.scopedElements)); +this.querySelector( + this.constructor.getScopedTagName('lion-input', this.constructor.scopedElements), +); ``` ## CSS selectors diff --git a/packages/core/package.json b/packages/core/package.json index 4e9b21418..1449b46f0 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -30,7 +30,7 @@ "sideEffects": false, "dependencies": { "@open-wc/dedupe-mixin": "^1.2.18", - "@open-wc/scoped-elements": "^1.0.3", + "@open-wc/scoped-elements": "^1.2.2", "lit-element": "~2.4.0", "lit-html": "^1.3.0" }, diff --git a/packages/core/src/DelegateMixin.js b/packages/core/src/DelegateMixin.js index 591a29bb5..cbe124d5f 100644 --- a/packages/core/src/DelegateMixin.js +++ b/packages/core/src/DelegateMixin.js @@ -18,7 +18,7 @@ import { dedupeMixin } from '@open-wc/dedupe-mixin'; */ const DelegateMixinImplementation = superclass => // eslint-disable-next-line - class DelegateMixin extends superclass { + class extends superclass { constructor() { super(); diff --git a/packages/core/src/DisabledMixin.js b/packages/core/src/DisabledMixin.js index 05a5dab9b..5511fac19 100644 --- a/packages/core/src/DisabledMixin.js +++ b/packages/core/src/DisabledMixin.js @@ -10,7 +10,7 @@ import { dedupeMixin } from '@open-wc/dedupe-mixin'; */ const DisabledMixinImplementation = superclass => // eslint-disable-next-line no-shadow - class DisabledMixinHost extends superclass { + class extends superclass { static get properties() { return { disabled: { diff --git a/packages/core/src/DisabledWithTabIndexMixin.js b/packages/core/src/DisabledWithTabIndexMixin.js index ca91a539c..06fc249d3 100644 --- a/packages/core/src/DisabledWithTabIndexMixin.js +++ b/packages/core/src/DisabledWithTabIndexMixin.js @@ -11,7 +11,7 @@ import { DisabledMixin } from './DisabledMixin.js'; */ const DisabledWithTabIndexMixinImplementation = superclass => // eslint-disable-next-line no-shadow - class DisabledWithTabIndexMixinHost extends DisabledMixin(superclass) { + class extends DisabledMixin(superclass) { static get properties() { return { // we use a property here as if we use the native tabIndex we can not set a default value diff --git a/packages/core/src/SlotMixin.js b/packages/core/src/SlotMixin.js index 7f3dcfbc3..d537b310e 100644 --- a/packages/core/src/SlotMixin.js +++ b/packages/core/src/SlotMixin.js @@ -12,7 +12,7 @@ import { dedupeMixin } from '@open-wc/dedupe-mixin'; */ const SlotMixinImplementation = superclass => // eslint-disable-next-line no-unused-vars, no-shadow - class SlotMixinHost extends superclass { + class extends superclass { /** * @return {SlotsMap} */ diff --git a/packages/core/src/UpdateStylesMixin.js b/packages/core/src/UpdateStylesMixin.js index dc44f7d2b..9b3634ecf 100644 --- a/packages/core/src/UpdateStylesMixin.js +++ b/packages/core/src/UpdateStylesMixin.js @@ -12,7 +12,7 @@ import { dedupeMixin } from '@open-wc/dedupe-mixin'; */ const UpdateStylesMixinImplementation = superclass => // eslint-disable-next-line no-shadow - class UpdateStylesMixinHost extends superclass { + class extends superclass { /** * @example * diff --git a/packages/core/types/DelegateMixinTypes.d.ts b/packages/core/types/DelegateMixinTypes.d.ts index 3a7e7ad7a..01422a6ca 100644 --- a/packages/core/types/DelegateMixinTypes.d.ts +++ b/packages/core/types/DelegateMixinTypes.d.ts @@ -9,7 +9,7 @@ export type Delegations = { attributes: string[]; }; -export declare class DelegateMixinHost { +export declare class DelegateHost { delegations: Delegations; protected _connectDelegateMixin(): void; @@ -50,6 +50,6 @@ export declare class DelegateMixinHost { */ declare function DelegateMixinImplementation>( superclass: T, -): T & Constructor; +): T & Constructor; export type DelegateMixin = typeof DelegateMixinImplementation; diff --git a/packages/core/types/DisabledMixinTypes.d.ts b/packages/core/types/DisabledMixinTypes.d.ts index 520ab57ea..a88aeeddb 100644 --- a/packages/core/types/DisabledMixinTypes.d.ts +++ b/packages/core/types/DisabledMixinTypes.d.ts @@ -1,13 +1,7 @@ import { Constructor } from '@open-wc/dedupe-mixin'; import { LitElement } from 'lit-element'; -export declare class DisabledMixinHost { - static get properties(): { - disabled: { - type: BooleanConstructor; - reflect: boolean; - }; - }; +export declare class DisabledHost { disabled: boolean; /** @@ -26,6 +20,6 @@ export declare class DisabledMixinHost { export declare function DisabledMixinImplementation>( superclass: T, -): T & Constructor; +): T & Constructor; export type DisabledMixin = typeof DisabledMixinImplementation; diff --git a/packages/core/types/DisabledWithTabIndexMixinTypes.d.ts b/packages/core/types/DisabledWithTabIndexMixinTypes.d.ts index 0e3083409..d7a40eb84 100644 --- a/packages/core/types/DisabledWithTabIndexMixinTypes.d.ts +++ b/packages/core/types/DisabledWithTabIndexMixinTypes.d.ts @@ -1,14 +1,7 @@ import { Constructor } from '@open-wc/dedupe-mixin'; -import { DisabledMixinHost } from './DisabledMixinTypes'; +import { DisabledHost } from './DisabledMixinTypes'; import { LitElement } from 'lit-element'; -export declare class DisabledWithTabIndexMixinHost { - static get properties(): { - tabIndex: { - type: NumberConstructor; - reflect: boolean; - attribute: string; - }; - }; +export declare class DisabledWithTabIndexHost { tabIndex: number; /** * Makes request to make the element disabled and set the tabindex @@ -27,6 +20,6 @@ export declare class DisabledWithTabIndexMixinHost { export declare function DisabledWithTabIndexMixinImplementation>( superclass: T, -): T & Constructor & Constructor; +): T & Constructor & Constructor; export type DisabledWithTabIndexMixin = typeof DisabledWithTabIndexMixinImplementation; diff --git a/packages/core/types/SlotMixinTypes.d.ts b/packages/core/types/SlotMixinTypes.d.ts index 67e1c5844..9bca61a6a 100644 --- a/packages/core/types/SlotMixinTypes.d.ts +++ b/packages/core/types/SlotMixinTypes.d.ts @@ -6,7 +6,7 @@ export type SlotsMap = { [key: string]: typeof slotFunction; }; -export declare class SlotMixinHost { +export declare class SlotHost { /** * Obtains all the slots to create */ @@ -50,6 +50,6 @@ export declare class SlotMixinHost { */ export declare function SlotMixinImplementation>( superclass: T, -): T & Constructor; +): T & Constructor; export type SlotMixin = typeof SlotMixinImplementation; diff --git a/packages/core/types/UpdateStylesMixinTypes.d.ts b/packages/core/types/UpdateStylesMixinTypes.d.ts index a0d931c3c..fed904166 100644 --- a/packages/core/types/UpdateStylesMixinTypes.d.ts +++ b/packages/core/types/UpdateStylesMixinTypes.d.ts @@ -3,7 +3,7 @@ import { Constructor } from '@open-wc/dedupe-mixin'; export type StylesMap = { [key: string]: string; }; -export declare class UpdateStylesMixinHost { +export declare class UpdateStylesHost { /** * @example * @@ -29,6 +29,6 @@ export declare class UpdateStylesMixinHost { */ declare function UpdateStylesMixinImplementation>( superclass: T, -): T & Constructor; +): T & Constructor; export type UpdateStylesMixin = typeof UpdateStylesMixinImplementation; diff --git a/packages/dialog/test/lion-dialog.test.js b/packages/dialog/test/lion-dialog.test.js index bda0d4d66..dabb9c95d 100644 --- a/packages/dialog/test/lion-dialog.test.js +++ b/packages/dialog/test/lion-dialog.test.js @@ -27,9 +27,7 @@ describe('lion-dialog', () => { it('should show content on invoker click', async () => { const el = await fixture(html` -
- Hey there -
+
Hey there
`); @@ -45,9 +43,7 @@ describe('lion-dialog', () => {
open nested overlay: -
- Nested content -
+
Nested content
diff --git a/packages/fieldset/test/lion-fieldset.test.js b/packages/fieldset/test/lion-fieldset.test.js index 24fb8f9c7..f40f86092 100644 --- a/packages/fieldset/test/lion-fieldset.test.js +++ b/packages/fieldset/test/lion-fieldset.test.js @@ -1,1198 +1,4 @@ -import { LionField, IsNumber, Validator } from '@lion/form-core'; -import '@lion/form-core/lion-field.js'; -import { localizeTearDown } from '@lion/localize/test-helpers.js'; -import { - defineCE, - expect, - html, - triggerFocusFor, - unsafeStatic, - fixture, - aTimeout, -} from '@open-wc/testing'; -import sinon from 'sinon'; +import { runFormGroupMixinSuite } from '@lion/form-core/test-suites/form-group/FormGroupMixin.suite.js'; import '../lion-fieldset.js'; -const childTagString = defineCE( - class extends LionField { - get slots() { - return { - input: () => document.createElement('input'), - }; - } - }, -); - -const tagString = 'lion-fieldset'; -const tag = unsafeStatic(tagString); -const childTag = unsafeStatic(childTagString); -const inputSlots = html` - <${childTag} name="gender[]"> - <${childTag} name="gender[]"> - <${childTag} name="color"> - <${childTag} name="hobbies[]"> - <${childTag} name="hobbies[]"> -`; - -beforeEach(() => { - localizeTearDown(); -}); - -// TODO: seperate fieldset and FormGroup tests -describe('', () => { - // TODO: Tests below belong to FormControlMixin. Preferably run suite integration test - it(`has a fieldName based on the label`, async () => { - const el1 = await fixture(html`<${tag} label="foo">${inputSlots}`); - expect(el1.fieldName).to.equal(el1._labelNode.textContent); - - const el2 = await fixture(html`<${tag}>${inputSlots}`); - expect(el2.fieldName).to.equal(el2._labelNode.textContent); - }); - - it(`has a fieldName based on the name if no label exists`, async () => { - const el = await fixture(html`<${tag} name="foo">${inputSlots}`); - expect(el.fieldName).to.equal(el.name); - }); - - it(`can override fieldName`, async () => { - const el = await fixture(html` - <${tag} label="foo" .fieldName="${'bar'}">${inputSlots} - `); - expect(el.__fieldName).to.equal(el.fieldName); - }); - - // TODO: Tests below belong to FormRegistrarMixin. Preferably run suite integration test - it(`${tagString} has an up to date list of every form element in .formElements`, async () => { - const el = await fixture(html`<${tag}>${inputSlots}`); - expect(el.formElements._keys().length).to.equal(3); - expect(el.formElements['hobbies[]'].length).to.equal(2); - el.removeChild(el.formElements['hobbies[]'][0]); - expect(el.formElements._keys().length).to.equal(3); - expect(el.formElements['hobbies[]'].length).to.equal(1); - }); - - it(`supports in html wrapped form elements`, async () => { - const el = await fixture(html` - <${tag}> -
- <${childTag} name="foo"> -
- - `); - expect(el.formElements.length).to.equal(1); - el.children[0].removeChild(el.formElements.foo); - expect(el.formElements.length).to.equal(0); - }); - - it('handles names with ending [] as an array', async () => { - const el = await fixture(html`<${tag}>${inputSlots}`); - el.formElements['gender[]'][0].modelValue = { value: 'male' }; - el.formElements['hobbies[]'][0].modelValue = { checked: false, value: 'chess' }; - el.formElements['hobbies[]'][1].modelValue = { checked: false, value: 'rugby' }; - - expect(el.formElements._keys().length).to.equal(3); - expect(el.formElements['hobbies[]'].length).to.equal(2); - expect(el.formElements['hobbies[]'][0].modelValue.value).to.equal('chess'); - expect(el.formElements['gender[]'][0].modelValue.value).to.equal('male'); - expect(el.modelValue['hobbies[]']).to.deep.equal([ - { checked: false, value: 'chess' }, - { checked: false, value: 'rugby' }, - ]); - }); - - it('throws if an element without a name tries to register', async () => { - const orig = console.info; - console.info = () => {}; - - let error = false; - const el = await fixture(html`<${tag}>`); - try { - // we test the api directly as errors thrown from a web component are in a - // different context and we can not catch them here => register fake elements - el.addFormElement({}); - } catch (err) { - error = err; - } - expect(error).to.be.instanceOf(TypeError); - expect(error.message).to.equal('You need to define a name'); - - console.info = orig; // restore original console - }); - - it('throws if name is the same as its parent', async () => { - const orig = console.info; - console.info = () => {}; - - let error = false; - const el = await fixture(html`<${tag} name="foo">`); - try { - // we test the api directly as errors thrown from a web component are in a - // different context and we can not catch them here => register fake elements - el.addFormElement({ name: 'foo' }); - } catch (err) { - error = err; - } - expect(error).to.be.instanceOf(TypeError); - expect(error.message).to.equal('You can not have the same name "foo" as your parent'); - - console.info = orig; // restore original console - }); - - it('throws if same name without ending [] is used', async () => { - const orig = console.info; - console.info = () => {}; - - let error = false; - const el = await fixture(html`<${tag}>`); - try { - // we test the api directly as errors thrown from a web component are in a - // different context and we can not catch them here => register fake elements - el.addFormElement({ name: 'fooBar' }); - el.addFormElement({ name: 'fooBar' }); - } catch (err) { - error = err; - } - expect(error).to.be.instanceOf(TypeError); - expect(error.message).to.equal( - 'Name "fooBar" is already registered - if you want an array add [] to the end', - ); - - console.info = orig; // restore original console - }); - /* eslint-enable no-console */ - - it('can dynamically add/remove elements', async () => { - const el = await fixture(html`<${tag}>${inputSlots}`); - const newField = await fixture(html`<${childTag} name="lastName">`); - - expect(el.formElements._keys().length).to.equal(3); - - el.appendChild(newField); - expect(el.formElements._keys().length).to.equal(4); - - el._inputNode.removeChild(newField); - expect(el.formElements._keys().length).to.equal(3); - }); - - // TODO: Tests below belong to FormGroupMixin. Preferably run suite integration test - - it('can read/write all values (of every input) via this.modelValue', async () => { - const el = await fixture(html` - <${tag}> - <${childTag} name="lastName"> - <${tag} name="newfieldset">${inputSlots} - - `); - const newFieldset = el.querySelector('lion-fieldset'); - el.formElements.lastName.modelValue = 'Bar'; - newFieldset.formElements['hobbies[]'][0].modelValue = { checked: true, value: 'chess' }; - newFieldset.formElements['hobbies[]'][1].modelValue = { checked: false, value: 'football' }; - newFieldset.formElements['gender[]'][0].modelValue = { checked: false, value: 'male' }; - newFieldset.formElements['gender[]'][1].modelValue = { checked: false, value: 'female' }; - newFieldset.formElements.color.modelValue = { checked: false, value: 'blue' }; - - expect(el.modelValue).to.deep.equal({ - lastName: 'Bar', - newfieldset: { - 'hobbies[]': [ - { checked: true, value: 'chess' }, - { checked: false, value: 'football' }, - ], - 'gender[]': [ - { checked: false, value: 'male' }, - { checked: false, value: 'female' }, - ], - color: { checked: false, value: 'blue' }, - }, - }); - - // make sure values are full settled before changing them - await aTimeout(); - el.modelValue = { - lastName: 2, - newfieldset: { - 'hobbies[]': [ - { checked: true, value: 'chess' }, - { checked: false, value: 'baseball' }, - ], - 'gender[]': [ - { checked: false, value: 'male' }, - { checked: false, value: 'female' }, - ], - color: { checked: false, value: 'blue' }, - }, - }; - - expect(newFieldset.formElements['hobbies[]'][0].modelValue).to.deep.equal({ - checked: true, - value: 'chess', - }); - expect(newFieldset.formElements['hobbies[]'][1].modelValue).to.deep.equal({ - checked: false, - value: 'baseball', - }); - expect(el.formElements.lastName.modelValue).to.equal(2); - }); - - it('works with document.createElement', async () => { - const el = document.createElement(tagString); - const childEl = document.createElement(childTagString); - childEl.name = 'planet'; - childEl.modelValue = 'earth'; - expect(el.formElements.length).to.equal(0); - - const wrapper = await fixture('
'); - el.appendChild(childEl); - wrapper.appendChild(el); - - expect(el.formElements.length).to.equal(1); - - await el.registrationComplete; - expect(el.modelValue).to.deep.equal({ planet: 'earth' }); - }); - - it('does not list disabled values in this.modelValue', async () => { - const el = await fixture(html` - <${tag}> - <${childTag} name="a" disabled .modelValue="${'x'}"> - <${childTag} name="b" .modelValue="${'x'}"> - <${tag} name="newFieldset"> - <${childTag} name="c" .modelValue="${'x'}"> - <${childTag} name="d" disabled .modelValue="${'x'}"> - - <${tag} name="disabledFieldset" disabled> - <${childTag} name="e" .modelValue="${'x'}"> - - - `); - expect(el.modelValue).to.deep.equal({ - b: 'x', - newFieldset: { - c: 'x', - }, - }); - }); - - it('does not throw if setter data of this.modelValue can not be handled', async () => { - const el = await fixture(html` - <${tag}> - <${childTag} name="firstName" .modelValue=${'foo'}> - <${childTag} name="lastName" .modelValue=${'bar'}> - - `); - const initState = { - firstName: 'foo', - lastName: 'bar', - }; - expect(el.modelValue).to.deep.equal(initState); - - el.modelValue = undefined; - expect(el.modelValue).to.deep.equal(initState); - - el.modelValue = null; - expect(el.modelValue).to.deep.equal(initState); - }); - - it('disables/enables all its formElements if it becomes disabled/enabled', async () => { - const el = await fixture(html`<${tag} disabled>${inputSlots}`); - expect(el.formElements.color.disabled).to.be.true; - expect(el.formElements['hobbies[]'][0].disabled).to.be.true; - expect(el.formElements['hobbies[]'][1].disabled).to.be.true; - - el.disabled = false; - await el.updateComplete; - expect(el.formElements.color.disabled).to.equal(false); - expect(el.formElements['hobbies[]'][0].disabled).to.equal(false); - expect(el.formElements['hobbies[]'][1].disabled).to.equal(false); - }); - - it('does not propagate/override initial disabled value on nested form elements', async () => { - const el = await fixture(html` - <${tag}> - <${tag} name="sub" disabled>${inputSlots} - - `); - - expect(el.disabled).to.equal(false); - expect(el.formElements.sub.disabled).to.be.true; - expect(el.formElements.sub.formElements.color.disabled).to.be.true; - expect(el.formElements.sub.formElements['hobbies[]'][0].disabled).to.be.true; - expect(el.formElements.sub.formElements['hobbies[]'][1].disabled).to.be.true; - }); - - it('can set initial modelValue on creation', async () => { - const el = await fixture(html` - <${tag} .modelValue=${{ lastName: 'Bar' }}> - <${childTag} name="lastName"> - - `); - - expect(el.modelValue).to.eql({ - lastName: 'Bar', - }); - }); - - it('can set initial serializedValue on creation', async () => { - const el = await fixture(html` - <${tag} .modelValue=${{ lastName: 'Bar' }}> - <${childTag} name="lastName"> - - `); - - expect(el.modelValue).to.eql({ lastName: 'Bar' }); - }); - - it('does not fail when conditional (not rendered) formElements are populated', async () => { - const showC = false; - const el = await fixture(html` - <${tag}> - - <${childTag} name="a"> - <${childTag} name="b"> - ${showC ? html`<${childTag} name="c">` : ''} - - `); - expect(el.formElements.a).to.be.not.undefined; - expect(el.formElements.b).to.be.not.undefined; - expect(el.formElements.c).to.be.undefined; - - let wasSuccessful = false; - try { - // c does not exist, because it is not render - el.serializedValue = { a: 'x', b: 'y', c: 'z' }; - wasSuccessful = true; - // eslint-disable-next-line no-empty - } catch (_) {} - - expect(wasSuccessful).to.be.true; - expect(el.formElements.a.serializedValue).to.equal('x'); - expect(el.formElements.b.serializedValue).to.equal('y'); - }); - - describe('Validation', () => { - it('validates on init', async () => { - class IsCat extends Validator { - static get validatorName() { - return 'IsCat'; - } - - execute(value) { - const hasError = value !== 'cat'; - return hasError; - } - } - - const el = await fixture(html` - <${tag}> - <${childTag} name="color" .validators=${[ - new IsCat(), - ]} .modelValue=${'blue'}> - - `); - expect(el.formElements.color.validationStates.error.IsCat).to.be.true; - }); - - it('validates when a value changes', async () => { - const el = await fixture(html`<${tag}>${inputSlots}`); - - const spy = sinon.spy(el, 'validate'); - el.formElements.color.modelValue = { checked: true, value: 'red' }; - expect(spy.callCount).to.equal(1); - }); - - it('has a special validator for all children - can be checked via this.error.FormElementsHaveNoError', async () => { - class IsCat extends Validator { - static get validatorName() { - return 'IsCat'; - } - - execute(value) { - const hasError = value !== 'cat'; - return hasError; - } - } - - const el = await fixture(html` - <${tag}> - <${childTag} name="color" .validators=${[ - new IsCat(), - ]} .modelValue=${'blue'}> - - `); - - expect(el.validationStates.error.FormElementsHaveNoError).to.be.true; - expect(el.formElements.color.validationStates.error.IsCat).to.be.true; - el.formElements.color.modelValue = 'cat'; - expect(el.validationStates.error).to.deep.equal({}); - }); - - it('validates on children (de)registration', async () => { - class HasEvenNumberOfChildren extends Validator { - static get validatorName() { - return 'HasEvenNumberOfChildren'; - } - - execute(value) { - const hasError = Object.keys(value).length % 2 !== 0; - return hasError; - } - } - const el = await fixture(html` - <${tag} .validators=${[new HasEvenNumberOfChildren()]}> - <${childTag} id="c1" name="c1"> - - `); - const child2 = await fixture(html` - <${childTag} name="c2"> - `); - expect(el.validationStates.error.HasEvenNumberOfChildren).to.be.true; - - el.appendChild(child2); - expect(el.validationStates.error.HasEvenNumberOfChildren).to.equal(undefined); - - el.removeChild(child2); - expect(el.validationStates.error.HasEvenNumberOfChildren).to.be.true; - - // Edge case: remove all children - el.removeChild(el.querySelector('[id=c1]')); - - expect(el.validationStates.error.HasEvenNumberOfChildren).to.equal(undefined); - }); - }); - - describe('Interaction states', () => { - it('has false states (dirty, touched, prefilled) on init', async () => { - const fieldset = await fixture(html`<${tag}>${inputSlots}`); - expect(fieldset.dirty).to.equal(false, 'dirty'); - expect(fieldset.touched).to.equal(false, 'touched'); - expect(fieldset.prefilled).to.equal(false, 'prefilled'); - }); - - it('sets dirty when value changed', async () => { - const fieldset = await fixture(html`<${tag}>${inputSlots}`); - fieldset.formElements['hobbies[]'][0].modelValue = { checked: true, value: 'football' }; - expect(fieldset.dirty).to.be.true; - }); - - it('sets touched when last field in fieldset left after focus', async () => { - const el = await fixture(html`<${tag}>${inputSlots}`); - - await triggerFocusFor(el.formElements['hobbies[]'][0]._inputNode); - await triggerFocusFor( - el.formElements['hobbies[]'][el.formElements['gender[]'].length - 1]._inputNode, - ); - const button = await fixture(html``); - button.focus(); - - expect(el.touched).to.be.true; - }); - - it('sets attributes [touched][dirty]', async () => { - const el = await fixture(html`<${tag}>`); - el.touched = true; - await el.updateComplete; - expect(el).to.have.attribute('touched'); - - el.dirty = true; - await el.updateComplete; - expect(el).to.have.attribute('dirty'); - }); - - it('becomes prefilled if all form elements are prefilled', async () => { - const el = await fixture(html` - <${tag}> - <${childTag} name="input1" .modelValue="${'prefilled'}"> - <${childTag} name="input2"> - - `); - expect(el.prefilled).to.be.false; - - const el2 = await fixture(html` - <${tag}> - <${childTag} name="input1" .modelValue="${'prefilled'}"> - <${childTag} name="input2" .modelValue="${'prefilled'}"> - - `); - expect(el2.prefilled).to.be.true; - }); - - it(`becomes "touched" once the last element of a group becomes blurred by keyboard - interaction (e.g. tabbing through the checkbox-group)`, async () => { - const el = await fixture(html` - <${tag}> - - <${childTag} name="myGroup[]" label="Option 1" value="1"> - <${childTag} name="myGroup[]" label="Option 2" value="2"> - - `); - - const button = await fixture(``); - - expect(el.touched).to.equal(false, 'initially, touched state is false'); - el.children[2].focus(); - expect(el.touched).to.equal(false, 'focus is on second checkbox'); - button.focus(); - expect(el.touched).to.equal( - true, - `focus is on element behind second checkbox (group has blurred)`, - ); - }); - - it(`becomes "touched" once the group as a whole becomes blurred via mouse interaction after - keyboard interaction (e.g. focus is moved inside the group and user clicks somewhere outside - the group)`, async () => { - const el = await fixture(html` - <${tag}> - <${childTag} name="input1"> - <${childTag} name="input2"> - - `); - const el2 = await fixture(html` - <${tag}> - <${childTag} name="input1"> - <${childTag} name="input2"> - - `); - - const outside = await fixture(html``); - - outside.click(); - expect(el.touched, 'unfocused fieldset should stay untouched').to.be.false; - - el.children[1].focus(); - el.children[2].focus(); - expect(el.touched).to.be.false; - - outside.click(); // blur the group via a click - outside.focus(); // a real mouse click moves focus as well - expect(el.touched).to.be.true; - expect(el2.touched).to.be.false; - }); - - it('potentially shows fieldset error message on interaction change', async () => { - class Input1IsTen extends Validator { - static get validatorName() { - return 'Input1IsTen'; - } - - execute(value) { - const hasError = value.input1 !== 10; - return hasError; - } - } - - const outSideButton = await fixture(html``); - const el = await fixture(html` - <${tag} .validators=${[new Input1IsTen()]}> - <${childTag} name="input1" .validators=${[new IsNumber()]}> - - `); - const input1 = el.querySelector(childTagString); - input1.modelValue = 2; - input1.focus(); - outSideButton.focus(); - - await el.updateComplete; - expect(el.validationStates.error.Input1IsTen).to.be.true; - expect(el.showsFeedbackFor).to.deep.equal(['error']); - }); - - it('show error if tabbing "out" of last ', async () => { - class Input1IsTen extends Validator { - static get validatorName() { - return 'Input1IsTen'; - } - - execute(value) { - const hasError = value.input1 !== 10; - return hasError; - } - } - const outSideButton = await fixture(html``); - const el = await fixture(html` - <${tag} .validators=${[new Input1IsTen()]}> - <${childTag} name="input1" .validators=${[new IsNumber()]}> - <${childTag} name="input2" .validators=${[new IsNumber()]}> - - `); - const inputs = el.querySelectorAll(childTagString); - inputs[1].modelValue = 2; // make it dirty - inputs[1].focus(); - - outSideButton.focus(); - - expect(el.validationStates.error.Input1IsTen).to.be.true; - expect(el.hasFeedbackFor).to.deep.equal(['error']); - }); - }); - - // TODO: this should be tested in FormGroupMixin - describe('serializedValue', () => { - it('use form elements serializedValue', async () => { - const fieldset = await fixture(html`<${tag}>${inputSlots}`); - fieldset.formElements['hobbies[]'][0].serializer = v => `${v.value}-serialized`; - fieldset.formElements['hobbies[]'][0].modelValue = { checked: false, value: 'Bar' }; - fieldset.formElements['hobbies[]'][1].modelValue = { checked: false, value: 'rugby' }; - fieldset.formElements['gender[]'][0].modelValue = { checked: false, value: 'male' }; - fieldset.formElements['gender[]'][1].modelValue = { checked: false, value: 'female' }; - fieldset.formElements.color.modelValue = { checked: false, value: 'blue' }; - expect(fieldset.formElements['hobbies[]'][0].serializedValue).to.equal('Bar-serialized'); - expect(fieldset.serializedValue).to.deep.equal({ - 'hobbies[]': ['Bar-serialized', { checked: false, value: 'rugby' }], - 'gender[]': [ - { checked: false, value: 'male' }, - { checked: false, value: 'female' }, - ], - color: { checked: false, value: 'blue' }, - }); - }); - - it('treats names with ending [] as arrays', async () => { - const fieldset = await fixture(html`<${tag}>${inputSlots}`); - fieldset.formElements['hobbies[]'][0].modelValue = { checked: false, value: 'chess' }; - fieldset.formElements['hobbies[]'][1].modelValue = { checked: false, value: 'rugby' }; - fieldset.formElements['gender[]'][0].modelValue = { checked: false, value: 'male' }; - fieldset.formElements['gender[]'][1].modelValue = { checked: false, value: 'female' }; - fieldset.formElements.color.modelValue = { checked: false, value: 'blue' }; - expect(fieldset.serializedValue).to.deep.equal({ - 'hobbies[]': [ - { checked: false, value: 'chess' }, - { checked: false, value: 'rugby' }, - ], - 'gender[]': [ - { checked: false, value: 'male' }, - { checked: false, value: 'female' }, - ], - color: { checked: false, value: 'blue' }, - }); - }); - - it('0 is a valid value to be serialized', async () => { - const fieldset = await fixture(html` - <${tag}> - <${childTag} name="price"> - `); - fieldset.formElements.price.modelValue = 0; - expect(fieldset.serializedValue).to.deep.equal({ price: 0 }); - }); - - it('serializes undefined values as ""(nb radios/checkboxes are always serialized)', async () => { - const fieldset = await fixture(html` - <${tag}> - <${childTag} name="custom[]"> - <${childTag} name="custom[]"> - - `); - fieldset.formElements['custom[]'][0].modelValue = 'custom 1'; - fieldset.formElements['custom[]'][1].modelValue = undefined; - - expect(fieldset.serializedValue).to.deep.equal({ - 'custom[]': ['custom 1', ''], - }); - }); - - it('allows for nested fieldsets', async () => { - const fieldset = await fixture(html` - <${tag} name="userData"> - <${childTag} name="comment"> - <${tag} name="newfieldset">${inputSlots} - - `); - const newFieldset = fieldset.querySelector('lion-fieldset'); - newFieldset.formElements['hobbies[]'][0].modelValue = { checked: false, value: 'chess' }; - newFieldset.formElements['hobbies[]'][1].modelValue = { checked: false, value: 'rugby' }; - newFieldset.formElements['gender[]'][0].modelValue = { checked: false, value: 'male' }; - newFieldset.formElements['gender[]'][1].modelValue = { checked: false, value: 'female' }; - newFieldset.formElements.color.modelValue = { checked: false, value: 'blue' }; - fieldset.formElements.comment.modelValue = 'Foo'; - expect(fieldset.formElements._keys().length).to.equal(2); - expect(newFieldset.formElements._keys().length).to.equal(3); - expect(fieldset.serializedValue).to.deep.equal({ - comment: 'Foo', - newfieldset: { - 'hobbies[]': [ - { checked: false, value: 'chess' }, - { checked: false, value: 'rugby' }, - ], - 'gender[]': [ - { checked: false, value: 'male' }, - { checked: false, value: 'female' }, - ], - color: { checked: false, value: 'blue' }, - }, - }); - }); - - it('does not serialize disabled values', async () => { - const fieldset = await fixture(html` - <${tag}> - <${childTag} name="custom[]"> - <${childTag} name="custom[]"> - - `); - fieldset.formElements['custom[]'][0].modelValue = 'custom 1'; - fieldset.formElements['custom[]'][1].disabled = true; - - expect(fieldset.serializedValue).to.deep.equal({ - 'custom[]': ['custom 1'], - }); - }); - - it('will exclude form elements within a disabled fieldset', async () => { - const fieldset = await fixture(html` - <${tag} name="userData"> - <${childTag} name="comment"> - <${tag} name="newfieldset">${inputSlots} - - `); - - const newFieldset = fieldset.querySelector('lion-fieldset'); - fieldset.formElements.comment.modelValue = 'Foo'; - newFieldset.formElements['hobbies[]'][0].modelValue = { checked: false, value: 'chess' }; - newFieldset.formElements['hobbies[]'][1].modelValue = { checked: false, value: 'rugby' }; - newFieldset.formElements['gender[]'][0].modelValue = { checked: false, value: 'male' }; - newFieldset.formElements['gender[]'][1].modelValue = { checked: false, value: 'female' }; - newFieldset.formElements.color.modelValue = { checked: false, value: 'blue' }; - newFieldset.formElements.color.disabled = true; - - expect(fieldset.serializedValue).to.deep.equal({ - comment: 'Foo', - newfieldset: { - 'hobbies[]': [ - { checked: false, value: 'chess' }, - { checked: false, value: 'rugby' }, - ], - 'gender[]': [ - { checked: false, value: 'male' }, - { checked: false, value: 'female' }, - ], - }, - }); - - newFieldset.formElements.color.disabled = false; - expect(fieldset.serializedValue).to.deep.equal({ - comment: 'Foo', - newfieldset: { - 'hobbies[]': [ - { checked: false, value: 'chess' }, - { checked: false, value: 'rugby' }, - ], - 'gender[]': [ - { checked: false, value: 'male' }, - { checked: false, value: 'female' }, - ], - color: { checked: false, value: 'blue' }, - }, - }); - }); - }); - - describe('Reset', () => { - it('restores default values if changes were made', async () => { - const el = await fixture(html` - <${tag}> - <${childTag} id="firstName" name="firstName" .modelValue="${'Foo'}"> - - `); - await el.querySelector(childTagString).updateComplete; - - const input = el.querySelector('#firstName'); - - input.modelValue = 'Bar'; - expect(el.modelValue).to.deep.equal({ firstName: 'Bar' }); - expect(input.modelValue).to.equal('Bar'); - - el.resetGroup(); - expect(el.modelValue).to.deep.equal({ firstName: 'Foo' }); - expect(input.modelValue).to.equal('Foo'); - }); - - it('restores default values of arrays if changes were made', async () => { - const el = await fixture(html` - <${tag}> - <${childTag} id="firstName" name="firstName[]" .modelValue="${'Foo'}"> - - `); - await el.querySelector(childTagString).updateComplete; - - const input = el.querySelector('#firstName'); - - input.modelValue = 'Bar'; - expect(el.modelValue).to.deep.equal({ 'firstName[]': ['Bar'] }); - expect(input.modelValue).to.equal('Bar'); - - el.resetGroup(); - expect(el.modelValue).to.deep.equal({ 'firstName[]': ['Foo'] }); - expect(input.modelValue).to.equal('Foo'); - }); - - it('restores default values of a nested fieldset if changes were made', async () => { - const el = await fixture(html` - <${tag}> - <${tag} id="name" name="name[]"> - <${childTag} id="firstName" name="firstName" .modelValue="${'Foo'}"> - - - `); - await Promise.all([ - el.querySelector('lion-fieldset').updateComplete, - el.querySelector(childTagString).updateComplete, - ]); - - const input = el.querySelector('#firstName'); - const nestedFieldset = el.querySelector('#name'); - - input.modelValue = 'Bar'; - expect(el.modelValue).to.deep.equal({ 'name[]': [{ firstName: 'Bar' }] }); - expect(nestedFieldset.modelValue).to.deep.equal({ firstName: 'Bar' }); - expect(input.modelValue).to.equal('Bar'); - - el.resetGroup(); - expect(el.modelValue).to.deep.equal({ 'name[]': [{ firstName: 'Foo' }] }); - expect(nestedFieldset.modelValue).to.deep.equal({ firstName: 'Foo' }); - expect(input.modelValue).to.equal('Foo'); - }); - - it('clears interaction state', async () => { - const el = await fixture(html`<${tag} touched dirty>${inputSlots}`); - // Safety check initially - el._setValueForAllFormElements('prefilled', true); - expect(el.dirty).to.equal(true, '"dirty" initially'); - expect(el.touched).to.equal(true, '"touched" initially'); - expect(el.prefilled).to.equal(true, '"prefilled" initially'); - - // Reset all children states, with prefilled false - el._setValueForAllFormElements('modelValue', {}); - el.resetInteractionState(); - expect(el.dirty).to.equal(false, 'not "dirty" after reset'); - expect(el.touched).to.equal(false, 'not "touched" after reset'); - expect(el.prefilled).to.equal(false, 'not "prefilled" after reset'); - - // Reset all children states with prefilled true - el._setValueForAllFormElements('modelValue', { checked: true }); // not prefilled - el.resetInteractionState(); - expect(el.dirty).to.equal(false, 'not "dirty" after 2nd reset'); - expect(el.touched).to.equal(false, 'not "touched" after 2nd reset'); - // prefilled state is dependant on value - expect(el.prefilled).to.equal(true, '"prefilled" after 2nd reset'); - }); - - it('clears submitted state', async () => { - const fieldset = await fixture(html`<${tag}>${inputSlots}`); - fieldset.submitted = true; - fieldset.resetGroup(); - expect(fieldset.submitted).to.equal(false); - fieldset.formElements.forEach(el => { - expect(el.submitted).to.equal(false); - }); - }); - - it('has correct validation afterwards', async () => { - class IsCat extends Validator { - static get validatorName() { - return 'IsCat'; - } - - execute(value) { - const hasError = value !== 'cat'; - return hasError; - } - } - class ColorContainsA extends Validator { - static get validatorName() { - return 'ColorContainsA'; - } - - execute(value) { - let hasError = true; - if (value && value.color) { - hasError = value.color.indexOf('a') === -1; - } - return hasError; - } - } - - const el = await fixture(html` - <${tag} .validators=${[new ColorContainsA()]}> - <${childTag} name="color" .validators=${[new IsCat()]}> - <${childTag} name="color2"> - - `); - expect(el.hasFeedbackFor).to.deep.equal(['error']); - expect(el.validationStates.error.ColorContainsA).to.be.true; - expect(el.formElements.color.hasFeedbackFor).to.deep.equal([]); - - el.formElements.color.modelValue = 'onlyb'; - expect(el.hasFeedbackFor).to.deep.equal(['error']); - expect(el.validationStates.error.ColorContainsA).to.be.true; - expect(el.formElements.color.validationStates.error.IsCat).to.be.true; - - el.formElements.color.modelValue = 'cat'; - expect(el.hasFeedbackFor).to.deep.equal([]); - - el.resetGroup(); - expect(el.hasFeedbackFor).to.deep.equal(['error']); - expect(el.validationStates.error.ColorContainsA).to.be.true; - expect(el.formElements.color.hasFeedbackFor).to.deep.equal([]); - }); - - it('has access to `_initialModelValue` based on initial children states', async () => { - const el = await fixture(html` - <${tag}> - <${childTag} name="child[]" .modelValue="${'foo1'}"> - - <${childTag} name="child[]" .modelValue="${'bar1'}"> - - - `); - await el.updateComplete; - el.modelValue['child[]'] = ['foo2', 'bar2']; - expect(el._initialModelValue['child[]']).to.eql(['foo1', 'bar1']); - }); - - it('does not wrongly recompute `_initialModelValue` after dynamic changes of children', async () => { - const el = await fixture(html` - <${tag}> - <${childTag} name="child[]" .modelValue="${'foo1'}"> - - - `); - el.modelValue['child[]'] = ['foo2']; - const childEl = await fixture(html` - <${childTag} name="child[]" .modelValue="${'bar1'}"> - - `); - el.appendChild(childEl); - expect(el._initialModelValue['child[]']).to.eql(['foo1', 'bar1']); - }); - - describe('resetGroup method', () => { - it('calls resetGroup on children fieldsets', async () => { - const el = await fixture(html` - <${tag} name="parentFieldset"> - <${tag} name="childFieldset"> - <${childTag} name="child[]" .modelValue="${'foo1'}"> - - - - `); - const childFieldsetEl = el.querySelector(tagString); - const resetGroupSpy = sinon.spy(childFieldsetEl, 'resetGroup'); - el.resetGroup(); - expect(resetGroupSpy.callCount).to.equal(1); - }); - - it('calls reset on children fields', async () => { - const el = await fixture(html` - <${tag} name="parentFieldset"> - <${tag} name="childFieldset"> - <${childTag} name="child[]" .modelValue="${'foo1'}"> - - - - `); - const childFieldsetEl = el.querySelector(childTagString); - const resetSpy = sinon.spy(childFieldsetEl, 'reset'); - el.resetGroup(); - expect(resetSpy.callCount).to.equal(1); - }); - }); - - describe('clearGroup method', () => { - it('calls clearGroup on children fieldset', async () => { - const el = await fixture(html` - <${tag} name="parentFieldset"> - <${tag} name="childFieldset"> - <${childTag} name="child[]" .modelValue="${'foo1'}"> - - - - `); - const childFieldsetEl = el.querySelector(tagString); - const clearGroupSpy = sinon.spy(childFieldsetEl, 'clearGroup'); - el.clearGroup(); - expect(clearGroupSpy.callCount).to.equal(1); - }); - - it('calls clear on children fields', async () => { - const el = await fixture(html` - <${tag} name="parentFieldset"> - <${tag} name="childFieldset"> - <${childTag} name="child[]" .modelValue="${'foo1'}"> - - - - `); - const childFieldsetEl = el.querySelector(childTagString); - const clearSpy = sinon.spy(childFieldsetEl, 'clear'); - el.clearGroup(); - expect(clearSpy.callCount).to.equal(1); - }); - - it('should clear the value of fields', async () => { - const el = await fixture(html` - <${tag} name="parentFieldset"> - <${tag} name="childFieldset"> - <${childTag} name="child" .modelValue="${'foo1'}"> - - - - `); - el.clearGroup(); - expect(el.querySelector('[name="child"]').modelValue).to.equal(''); - }); - }); - }); - - describe('Accessibility', () => { - it('has role="group" set', async () => { - const fieldset = await fixture(html`<${tag}>${inputSlots}`); - fieldset.formElements['hobbies[]'][0].modelValue = { checked: false, value: 'chess' }; - fieldset.formElements['hobbies[]'][1].modelValue = { checked: false, value: 'rugby' }; - fieldset.formElements['gender[]'][0].modelValue = { checked: false, value: 'male' }; - fieldset.formElements['gender[]'][1].modelValue = { checked: false, value: 'female' }; - fieldset.formElements.color.modelValue = { checked: false, value: 'blue' }; - expect(fieldset.hasAttribute('role')).to.be.true; - expect(fieldset.getAttribute('role')).to.contain('group'); - }); - - it('has an aria-labelledby from element with slot="label"', async () => { - const el = await fixture(html` - <${tag}> - - ${inputSlots} - - `); - const label = Array.from(el.children).find(child => child.slot === 'label'); - expect(el.hasAttribute('aria-labelledby')).to.equal(true); - expect(el.getAttribute('aria-labelledby')).contains(label.id); - }); - - describe('Screen reader relations (aria-describedby) for child fields and fieldsets', () => { - let childAriaFixture; // function - let childAriaTest; // function - - before(() => { - // Legend: - // - l1 means level 1 (outer) fieldset - // - l2 means level 2 (inner) fieldset - // - g means group: the help-text or feedback belongs to group - // - f means field(lion-input in fixture below): the help-text or feedback belongs to field - // - 'a' or 'b' behind 'f' indicate which field in a fieldset is meant (a: first, b: second) - - childAriaFixture = async ( - msgSlotType = 'feedback', // eslint-disable-line no-shadow - ) => { - const dom = await fixture(html` - <${tag} name="l1_g"> - <${childTag} name="l1_fa"> -
- - - - <${childTag} name="l1_fb"> -
- - - - - - <${tag} name="l2_g"> - <${childTag} name="l2_fa"> -
- - - - <${childTag} name="l2_fb"> -
- - - -
- - - - - -
- - - `); - return dom; - }; - - // eslint-disable-next-line no-shadow - childAriaTest = childAriaFixture => { - /* eslint-disable camelcase */ - // Message elements: all elements pointed at by inputs - const msg_l1_g = childAriaFixture.querySelector('#msg_l1_g'); - const msg_l1_fa = childAriaFixture.querySelector('#msg_l1_fa'); - const msg_l1_fb = childAriaFixture.querySelector('#msg_l1_fb'); - const msg_l2_g = childAriaFixture.querySelector('#msg_l2_g'); - const msg_l2_fa = childAriaFixture.querySelector('#msg_l2_fa'); - const msg_l2_fb = childAriaFixture.querySelector('#msg_l2_fb'); - - // Field elements: all inputs pointing to message elements - const input_l1_fa = childAriaFixture.querySelector('input[name=l1_fa]'); - const input_l1_fb = childAriaFixture.querySelector('input[name=l1_fb]'); - const input_l2_fa = childAriaFixture.querySelector('input[name=l2_fa]'); - const input_l2_fb = childAriaFixture.querySelector('input[name=l2_fb]'); - - /* eslint-enable camelcase */ - - // 'L1' fields (inside lion-fieldset[name="l1_g"]) should point to l1(group) msg - expect(input_l1_fa.getAttribute('aria-describedby')).to.contain( - msg_l1_g.id, - 'l1 input(a) refers parent/group', - ); - expect(input_l1_fb.getAttribute('aria-describedby')).to.contain( - msg_l1_g.id, - 'l1 input(b) refers parent/group', - ); - - // Also check that aria-describedby of the inputs are not overridden (this relation was - // put there in lion-input(using lion-field)). - expect(input_l1_fa.getAttribute('aria-describedby')).to.contain( - msg_l1_fa.id, - 'l1 input(a) refers local field', - ); - expect(input_l1_fb.getAttribute('aria-describedby')).to.contain( - msg_l1_fb.id, - 'l1 input(b) refers local field', - ); - - // Also make feedback element point to nested fieldset inputs - expect(input_l2_fa.getAttribute('aria-describedby')).to.contain( - msg_l1_g.id, - 'l2 input(a) refers grandparent/group.group', - ); - expect(input_l2_fb.getAttribute('aria-describedby')).to.contain( - msg_l1_g.id, - 'l2 input(b) refers grandparent/group.group', - ); - - // Check order: the nearest ('dom wise': so 1. local, 2. parent, 3. grandparent) message - // should be read first by screen reader - const dA = input_l2_fa.getAttribute('aria-describedby'); - expect( - dA.indexOf(msg_l2_fa.id) < dA.indexOf(msg_l2_g.id) < dA.indexOf(msg_l1_g.id), - ).to.equal(true, 'order of ids'); - const dB = input_l2_fb.getAttribute('aria-describedby'); - expect( - dB.indexOf(msg_l2_fb.id) < dB.indexOf(msg_l2_g.id) < dB.indexOf(msg_l1_g.id), - ).to.equal(true, 'order of ids'); - }; - }); - - it(`reads feedback message belonging to fieldset when child input is focused - (via aria-describedby)`, async () => { - childAriaTest(await childAriaFixture('feedback')); - }); - - it(`reads help-text message belonging to fieldset when child input is focused - (via aria-describedby)`, async () => { - childAriaTest(await childAriaFixture('help-text')); - }); - }); - }); -}); +runFormGroupMixinSuite({ tagString: 'lion-fieldset' }); diff --git a/packages/form-core/src/FocusMixin.js b/packages/form-core/src/FocusMixin.js index 1f83cf146..f9871ddca 100644 --- a/packages/form-core/src/FocusMixin.js +++ b/packages/form-core/src/FocusMixin.js @@ -1,11 +1,13 @@ import { dedupeMixin } from '@lion/core'; +import { FormControlMixin } from './FormControlMixin.js'; /** * @typedef {import('../types/FocusMixinTypes').FocusMixin} FocusMixin * @type {FocusMixin} + * @param {import('@open-wc/dedupe-mixin').Constructor} superclass */ const FocusMixinImplementation = superclass => // eslint-disable-next-line no-unused-vars, max-len, no-shadow - class FocusMixin extends superclass { + class FocusMixin extends FormControlMixin(superclass) { static get properties() { return { focused: { @@ -21,16 +23,12 @@ const FocusMixinImplementation = superclass => } connectedCallback() { - if (super.connectedCallback) { - super.connectedCallback(); - } + super.connectedCallback(); this.__registerEventsForFocusMixin(); } disconnectedCallback() { - if (super.disconnectedCallback) { - super.disconnectedCallback(); - } + super.disconnectedCallback(); this.__teardownEventsForFocusMixin(); } @@ -101,10 +99,22 @@ const FocusMixinImplementation = superclass => } __teardownEventsForFocusMixin() { - this._inputNode.removeEventListener('focus', this.__redispatchFocus); - this._inputNode.removeEventListener('blur', this.__redispatchBlur); - this._inputNode.removeEventListener('focusin', this.__redispatchFocusin); - this._inputNode.removeEventListener('focusout', this.__redispatchFocusout); + this._inputNode.removeEventListener( + 'focus', + /** @type {EventListenerOrEventListenerObject} */ (this.__redispatchFocus), + ); + this._inputNode.removeEventListener( + 'blur', + /** @type {EventListenerOrEventListenerObject} */ (this.__redispatchBlur), + ); + this._inputNode.removeEventListener( + 'focusin', + /** @type {EventListenerOrEventListenerObject} */ (this.__redispatchFocusin), + ); + this._inputNode.removeEventListener( + 'focusout', + /** @type {EventListenerOrEventListenerObject} */ (this.__redispatchFocusout), + ); } }; diff --git a/packages/form-core/src/FormControlMixin.js b/packages/form-core/src/FormControlMixin.js index ff9c923d1..fde787992 100644 --- a/packages/form-core/src/FormControlMixin.js +++ b/packages/form-core/src/FormControlMixin.js @@ -1,7 +1,8 @@ import { css, dedupeMixin, html, nothing, SlotMixin } from '@lion/core'; -import { Unparseable } from './validate/Unparseable.js'; +import { DisabledMixin } from '@lion/core/src/DisabledMixin.js'; import { FormRegisteringMixin } from './registration/FormRegisteringMixin.js'; import { getAriaElementsInRightDomOrder } from './utils/getAriaElementsInRightDomOrder.js'; +import { Unparseable } from './validate/Unparseable.js'; /** * Generates random unique identifier (for dom elements) @@ -17,16 +18,17 @@ function uuid(prefix) { * This Mixin is a shared fundament for all form components, it's applied on: * - LionField (which is extended to LionInput, LionTextarea, LionSelect etc. etc.) * - LionFieldset (which is extended to LionRadioGroup, LionCheckboxGroup, LionForm) - * @typedef {import('lit-html').TemplateResult} TemplateResult - * @typedef {import('lit-element').CSSResult} CSSResult - * @typedef {import('lit-html').nothing} nothing + * @typedef {import('@lion/core').TemplateResult} TemplateResult + * @typedef {import('@lion/core').CSSResult} CSSResult + * @typedef {import('@lion/core').nothing} nothing * @typedef {import('@lion/core/types/SlotMixinTypes').SlotsMap} SlotsMap * @typedef {import('../types/FormControlMixinTypes.js').FormControlMixin} FormControlMixin * @type {FormControlMixin} + * @param {import('@open-wc/dedupe-mixin').Constructor} superclass */ const FormControlMixinImplementation = superclass => // eslint-disable-next-line no-shadow, no-unused-vars - class FormControlMixin extends FormRegisteringMixin(SlotMixin(superclass)) { + class FormControlMixin extends FormRegisteringMixin(DisabledMixin(SlotMixin(superclass))) { static get properties() { return { /** @@ -48,6 +50,21 @@ const FormControlMixinImplementation = superclass => type: String, attribute: 'help-text', }, + + /** + * 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. + * The model value is 'ready for consumption' by the outside world (think of a Date + * object or a float). The modelValue can(and is recommended to) be used as both input + * value and output value of the `LionField`. + * + * Examples: + * - For a date input: a String '20/01/1999' will be converted to new Date('1999/01/20') + * - For a number input: a formatted String '1.234,56' will be converted to a Number: + * 1234.56 + */ + modelValue: { attribute: false }, + /** * Contains all elements that should end up in aria-labelledby of `._inputNode` */ @@ -112,7 +129,8 @@ const FormControlMixinImplementation = superclass => * @return {string} */ get fieldName() { - return this.__fieldName || this.label || this.name; + // @ts-expect-error + return this.__fieldName || this.label || this.name; // FIXME: when LionField is typed we can inherit this prop } /** @@ -184,7 +202,9 @@ const FormControlMixinImplementation = superclass => } get _feedbackNode() { - return this.__getDirectSlotChild('feedback'); + return /** @type {import('./validate/LionValidationFeedback').LionValidationFeedback | undefined} */ (this.__getDirectSlotChild( + 'feedback', + )); } constructor() { @@ -197,7 +217,11 @@ const FormControlMixinImplementation = superclass => this._ariaDescribedNodes = []; /** @type {'child' | 'choice-group' | 'fieldset'} */ this._repropagationRole = 'child'; - this.addEventListener('model-value-changed', this.__repropagateChildrenValues); + this._isRepropagationEndpoint = false; + this.addEventListener( + 'model-value-changed', + /** @type {EventListenerOrEventListenerObject} */ (this.__repropagateChildrenValues), + ); } connectedCallback() { @@ -339,12 +363,8 @@ const FormControlMixinImplementation = superclass => */ render() { return html` -
- ${this._groupOneTemplate()} -
-
- ${this._groupTwoTemplate()} -
+
${this._groupOneTemplate()}
+
${this._groupTwoTemplate()}
`; } @@ -479,10 +499,15 @@ const FormControlMixinImplementation = superclass => /** * @param {?} modelValue * @return {boolean} + * + * FIXME: Move to FormatMixin? Since there we have access to modelValue prop */ + // @ts-expect-error _isEmpty(modelValue = this.modelValue) { let value = modelValue; + // @ts-expect-error if (this.modelValue instanceof Unparseable) { + // @ts-expect-error value = this.modelValue.viewValue; } @@ -629,7 +654,7 @@ const FormControlMixinImplementation = superclass => } /** - * @return {HTMLElement[]} + * @return {Array.} */ // Returns dom references to all elements that should be referred to by field(s) _getAriaDescriptionElements() { @@ -681,10 +706,12 @@ const FormControlMixinImplementation = superclass => /** * @param {string} slotName - * @return {HTMLElement} + * @return {HTMLElement | undefined} */ __getDirectSlotChild(slotName) { - return [...this.children].find(el => el.slot === slotName); + return /** @type {HTMLElement[]} */ (Array.from(this.children)).find( + el => el.slot === slotName, + ); } __dispatchInitialModelValueChangedEvent() { @@ -756,6 +783,7 @@ const FormControlMixinImplementation = superclass => // We only send the checked changed up (not the unchecked). In this way a choice group // (radio-group, checkbox-group, select/listbox) acts as an 'endpoint' (a single Field) // just like the native ,