feat(form-core): form-core types
Co-authored-by: Thijs Louisse <Thijs.Louisse@ing.com>
This commit is contained in:
parent
3e00819bdf
commit
874ff48339
105 changed files with 4691 additions and 4030 deletions
18
.changeset/beige-drinks-trade.md
Normal file
18
.changeset/beige-drinks-trade.md
Normal file
|
|
@ -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
|
||||||
|
|
@ -203,13 +203,9 @@ Excellent! Lea can now use the tabs component like so:
|
||||||
```html
|
```html
|
||||||
<lea-tabs>
|
<lea-tabs>
|
||||||
<lea-tab slot="tab">Info</lea-tab>
|
<lea-tab slot="tab">Info</lea-tab>
|
||||||
<lea-tab-panel slot="panel">
|
<lea-tab-panel slot="panel"> Info page with lots of information about us. </lea-tab-panel>
|
||||||
Info page with lots of information about us.
|
|
||||||
</lea-tab-panel>
|
|
||||||
<lea-tab slot="tab">Work</lea-tab>
|
<lea-tab slot="tab">Work</lea-tab>
|
||||||
<lea-tab-panel slot="panel">
|
<lea-tab-panel slot="panel"> Work page that showcases our work. </lea-tab-panel>
|
||||||
Work page that showcases our work.
|
|
||||||
</lea-tab-panel>
|
|
||||||
</lea-tabs>
|
</lea-tabs>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,13 +23,9 @@ export default {
|
||||||
export const main = () => html`
|
export const main = () => html`
|
||||||
<lea-tabs>
|
<lea-tabs>
|
||||||
<lea-tab slot="tab">Info</lea-tab>
|
<lea-tab slot="tab">Info</lea-tab>
|
||||||
<lea-tab-panel slot="panel">
|
<lea-tab-panel slot="panel"> Info page with lots of information about us. </lea-tab-panel>
|
||||||
Info page with lots of information about us.
|
|
||||||
</lea-tab-panel>
|
|
||||||
<lea-tab slot="tab">Work</lea-tab>
|
<lea-tab slot="tab">Work</lea-tab>
|
||||||
<lea-tab-panel slot="panel">
|
<lea-tab-panel slot="panel"> Work page that showcases our work. </lea-tab-panel>
|
||||||
Work page that showcases our work.
|
|
||||||
</lea-tab-panel>
|
|
||||||
</lea-tabs>
|
</lea-tabs>
|
||||||
`;
|
`;
|
||||||
```
|
```
|
||||||
|
|
@ -51,13 +47,9 @@ import '@lion/tabs/lea-tabs.js';
|
||||||
```html
|
```html
|
||||||
<lea-tabs>
|
<lea-tabs>
|
||||||
<lea-tab slot="tab">Info</lea-tab>
|
<lea-tab slot="tab">Info</lea-tab>
|
||||||
<lea-tab-panel slot="panel">
|
<lea-tab-panel slot="panel"> Info page with lots of information about us. </lea-tab-panel>
|
||||||
Info page with lots of information about us.
|
|
||||||
</lea-tab-panel>
|
|
||||||
<lea-tab slot="tab">Work</lea-tab>
|
<lea-tab slot="tab">Work</lea-tab>
|
||||||
<lea-tab-panel slot="panel">
|
<lea-tab-panel slot="panel"> Work page that showcases our work. </lea-tab-panel>
|
||||||
Work page that showcases our work.
|
|
||||||
</lea-tab-panel>
|
|
||||||
</lea-tabs>
|
</lea-tabs>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -71,13 +63,9 @@ You can set the `selectedIndex` to select a certain tab.
|
||||||
export const selectedIndex = () => html`
|
export const selectedIndex = () => html`
|
||||||
<lea-tabs .selectedIndex=${1}>
|
<lea-tabs .selectedIndex=${1}>
|
||||||
<lea-tab slot="tab">Info</lea-tab>
|
<lea-tab slot="tab">Info</lea-tab>
|
||||||
<lea-tab-panel slot="panel">
|
<lea-tab-panel slot="panel"> Info page with lots of information about us. </lea-tab-panel>
|
||||||
Info page with lots of information about us.
|
|
||||||
</lea-tab-panel>
|
|
||||||
<lea-tab slot="tab">Work</lea-tab>
|
<lea-tab slot="tab">Work</lea-tab>
|
||||||
<lea-tab-panel slot="panel">
|
<lea-tab-panel slot="panel"> Work page that showcases our work. </lea-tab-panel>
|
||||||
Work page that showcases our work.
|
|
||||||
</lea-tab-panel>
|
|
||||||
</lea-tabs>
|
</lea-tabs>
|
||||||
`;
|
`;
|
||||||
```
|
```
|
||||||
|
|
@ -93,12 +81,8 @@ export const slotsOrder = () => html`
|
||||||
<lea-tabs>
|
<lea-tabs>
|
||||||
<lea-tab slot="tab">Info</lea-tab>
|
<lea-tab slot="tab">Info</lea-tab>
|
||||||
<lea-tab slot="tab">Work</lea-tab>
|
<lea-tab slot="tab">Work</lea-tab>
|
||||||
<lea-tab-panel slot="panel">
|
<lea-tab-panel slot="panel"> Info page with lots of information about us. </lea-tab-panel>
|
||||||
Info page with lots of information about us.
|
<lea-tab-panel slot="panel"> Work page that showcases our work. </lea-tab-panel>
|
||||||
</lea-tab-panel>
|
|
||||||
<lea-tab-panel slot="panel">
|
|
||||||
Work page that showcases our work.
|
|
||||||
</lea-tab-panel>
|
|
||||||
</lea-tabs>
|
</lea-tabs>
|
||||||
`;
|
`;
|
||||||
```
|
```
|
||||||
|
|
@ -122,9 +106,7 @@ export const distributeNewElements = () => {
|
||||||
render() {
|
render() {
|
||||||
return html`
|
return html`
|
||||||
<h3>Append</h3>
|
<h3>Append</h3>
|
||||||
<button @click="${this.__handleAppendClick}">
|
<button @click="${this.__handleAppendClick}">Append</button>
|
||||||
Append
|
|
||||||
</button>
|
|
||||||
<lea-tabs id="appendTabs">
|
<lea-tabs id="appendTabs">
|
||||||
<lea-tab slot="tab">tab 1</lea-tab>
|
<lea-tab slot="tab">tab 1</lea-tab>
|
||||||
<lea-tab-panel slot="panel">panel 1</lea-tab-panel>
|
<lea-tab-panel slot="panel">panel 1</lea-tab-panel>
|
||||||
|
|
@ -133,9 +115,7 @@ export const distributeNewElements = () => {
|
||||||
</lea-tabs>
|
</lea-tabs>
|
||||||
<hr />
|
<hr />
|
||||||
<h3>Push</h3>
|
<h3>Push</h3>
|
||||||
<button @click="${this.__handlePushClick}">
|
<button @click="${this.__handlePushClick}">Push</button>
|
||||||
Push
|
|
||||||
</button>
|
|
||||||
<lea-tabs id="pushTabs">
|
<lea-tabs id="pushTabs">
|
||||||
<lea-tab slot="tab">tab 1</lea-tab>
|
<lea-tab slot="tab">tab 1</lea-tab>
|
||||||
<lea-tab-panel slot="panel">panel 1</lea-tab-panel>
|
<lea-tab-panel slot="panel">panel 1</lea-tab-panel>
|
||||||
|
|
|
||||||
|
|
@ -265,13 +265,9 @@ export const specialFeature = () =>
|
||||||
html`
|
html`
|
||||||
<lea-tabs>
|
<lea-tabs>
|
||||||
<lea-tab slot="tab">Info</lea-tab>
|
<lea-tab slot="tab">Info</lea-tab>
|
||||||
<lea-tab-panel slot="panel">
|
<lea-tab-panel slot="panel"> Info page with lots of information about us. </lea-tab-panel>
|
||||||
Info page with lots of information about us.
|
|
||||||
</lea-tab-panel>
|
|
||||||
<lea-tab slot="tab">Work</lea-tab>
|
<lea-tab slot="tab">Work</lea-tab>
|
||||||
<lea-tab-panel slot="panel">
|
<lea-tab-panel slot="panel"> Work page that showcases our work. </lea-tab-panel>
|
||||||
Work page that showcases our work.
|
|
||||||
</lea-tab-panel>
|
|
||||||
</lea-tabs>
|
</lea-tabs>
|
||||||
`;
|
`;
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,7 @@ export const main = () => html`
|
||||||
<h3 slot="invoker">
|
<h3 slot="invoker">
|
||||||
<button>Lorem</button>
|
<button>Lorem</button>
|
||||||
</h3>
|
</h3>
|
||||||
<p slot="content">
|
<p slot="content">Lorem ipsum dolor sit, amet consectetur adipisicing elit.</p>
|
||||||
Lorem ipsum dolor sit, amet consectetur adipisicing elit.
|
|
||||||
</p>
|
|
||||||
<h3 slot="invoker">
|
<h3 slot="invoker">
|
||||||
<button>Laboriosam</button>
|
<button>Laboriosam</button>
|
||||||
</h3>
|
</h3>
|
||||||
|
|
@ -56,9 +54,7 @@ import '@lion/accordion/lion-accordion.js';
|
||||||
<h3 slot="invoker">
|
<h3 slot="invoker">
|
||||||
<button>Lorem</button>
|
<button>Lorem</button>
|
||||||
</h3>
|
</h3>
|
||||||
<p slot="content">
|
<p slot="content">Lorem ipsum dolor sit, amet consectetur adipisicing elit.</p>
|
||||||
Lorem ipsum dolor sit, amet consectetur adipisicing elit.
|
|
||||||
</p>
|
|
||||||
<h3 slot="invoker">
|
<h3 slot="invoker">
|
||||||
<button>Laboriosam</button>
|
<button>Laboriosam</button>
|
||||||
</h3>
|
</h3>
|
||||||
|
|
@ -83,9 +79,7 @@ export const expanded = () => html`
|
||||||
<h3 slot="invoker">
|
<h3 slot="invoker">
|
||||||
<button>Lorem</button>
|
<button>Lorem</button>
|
||||||
</h3>
|
</h3>
|
||||||
<p slot="content">
|
<p slot="content">Lorem ipsum dolor sit, amet consectetur adipisicing elit.</p>
|
||||||
Lorem ipsum dolor sit, amet consectetur adipisicing elit.
|
|
||||||
</p>
|
|
||||||
<h3 slot="invoker">
|
<h3 slot="invoker">
|
||||||
<button>Laboriosam</button>
|
<button>Laboriosam</button>
|
||||||
</h3>
|
</h3>
|
||||||
|
|
@ -109,9 +103,7 @@ export const slotsOrder = () => html`
|
||||||
<h3 slot="invoker">
|
<h3 slot="invoker">
|
||||||
<button>Lorem</button>
|
<button>Lorem</button>
|
||||||
</h3>
|
</h3>
|
||||||
<p slot="content">
|
<p slot="content">Lorem ipsum dolor sit, amet consectetur adipisicing elit.</p>
|
||||||
Lorem ipsum dolor sit, amet consectetur adipisicing elit.
|
|
||||||
</p>
|
|
||||||
<h3 slot="invoker">
|
<h3 slot="invoker">
|
||||||
<button>Laboriosam</button>
|
<button>Laboriosam</button>
|
||||||
</h3>
|
</h3>
|
||||||
|
|
@ -152,9 +144,7 @@ export const distributeNewElement = () => {
|
||||||
</h4>
|
</h4>
|
||||||
<p slot="content">content 2</p>
|
<p slot="content">content 2</p>
|
||||||
</lion-accordion>
|
</lion-accordion>
|
||||||
<button @click="${this.__handleAppendClick}">
|
<button @click="${this.__handleAppendClick}">Append</button>
|
||||||
Append
|
|
||||||
</button>
|
|
||||||
<hr />
|
<hr />
|
||||||
<h3>Push</h3>
|
<h3>Push</h3>
|
||||||
<lion-accordion id="pushTabs">
|
<lion-accordion id="pushTabs">
|
||||||
|
|
@ -173,9 +163,7 @@ export const distributeNewElement = () => {
|
||||||
`,
|
`,
|
||||||
)}
|
)}
|
||||||
</lion-accordion>
|
</lion-accordion>
|
||||||
<button @click="${this.__handlePushClick}">
|
<button @click="${this.__handlePushClick}">Push</button>
|
||||||
Push
|
|
||||||
</button>
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
|
||||||
|
|
@ -276,9 +276,7 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
|
||||||
return html`
|
return html`
|
||||||
<div class="calendar__navigation__month">
|
<div class="calendar__navigation__month">
|
||||||
${this.__renderPreviousButton('Month', previousMonth, previousYear)}
|
${this.__renderPreviousButton('Month', previousMonth, previousYear)}
|
||||||
<h2 class="calendar__navigation-heading" id="month" aria-atomic="true">
|
<h2 class="calendar__navigation-heading" id="month" aria-atomic="true">${month}</h2>
|
||||||
${month}
|
|
||||||
</h2>
|
|
||||||
${this.__renderNextButton('Month', nextMonth, nextYear)}
|
${this.__renderNextButton('Month', nextMonth, nextYear)}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
@ -291,9 +289,7 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
|
||||||
return html`
|
return html`
|
||||||
<div class="calendar__navigation__year">
|
<div class="calendar__navigation__year">
|
||||||
${this.__renderPreviousButton('FullYear', month, previousYear)}
|
${this.__renderPreviousButton('FullYear', month, previousYear)}
|
||||||
<h2 class="calendar__navigation-heading" id="year" aria-atomic="true">
|
<h2 class="calendar__navigation-heading" id="year" aria-atomic="true">${year}</h2>
|
||||||
${year}
|
|
||||||
</h2>
|
|
||||||
${this.__renderNextButton('FullYear', month, nextYear)}
|
${this.__renderNextButton('FullYear', month, nextYear)}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
|
||||||
|
|
@ -67,9 +67,7 @@ export function dayTemplate(day, { weekdays, monthsLabels = defaultMonthLabels }
|
||||||
?current-month=${day.currentMonth}
|
?current-month=${day.currentMonth}
|
||||||
?next-month=${day.nextMonth}
|
?next-month=${day.nextMonth}
|
||||||
>
|
>
|
||||||
<span class="calendar__day-button__text">
|
<span class="calendar__day-button__text"> ${day.date.getDate()} </span>
|
||||||
${day.date.getDate()}
|
|
||||||
</span>
|
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
`;
|
`;
|
||||||
|
|
|
||||||
|
|
@ -52,9 +52,7 @@ import '@lion/collapsible/lion-collapsible.js';
|
||||||
```html
|
```html
|
||||||
<lion-collapsible>
|
<lion-collapsible>
|
||||||
<button slot="invoker">Invoker Text</button>
|
<button slot="invoker">Invoker Text</button>
|
||||||
<div slot="content">
|
<div slot="content">Extra content</div>
|
||||||
Extra content
|
|
||||||
</div>
|
|
||||||
</lion-collapsible>
|
</lion-collapsible>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -97,12 +95,8 @@ export const methods = () => html`
|
||||||
<button @click=${() => document.querySelector('#car-collapsible').toggle()}>
|
<button @click=${() => document.querySelector('#car-collapsible').toggle()}>
|
||||||
Toggle content
|
Toggle content
|
||||||
</button>
|
</button>
|
||||||
<button @click=${() => document.querySelector('#car-collapsible').show()}>
|
<button @click=${() => document.querySelector('#car-collapsible').show()}>Show content</button>
|
||||||
Show content
|
<button @click=${() => document.querySelector('#car-collapsible').hide()}>Hide content</button>
|
||||||
</button>
|
|
||||||
<button @click=${() => document.querySelector('#car-collapsible').hide()}>
|
|
||||||
Hide content
|
|
||||||
</button>
|
|
||||||
</section>
|
</section>
|
||||||
`;
|
`;
|
||||||
```
|
```
|
||||||
|
|
@ -140,9 +134,7 @@ A custom template can be specified to the `invoker` slot. It can be any button o
|
||||||
```js preview-story
|
```js preview-story
|
||||||
export const customInvokerTemplate = () => html`
|
export const customInvokerTemplate = () => html`
|
||||||
<lion-collapsible>
|
<lion-collapsible>
|
||||||
<button class="demo-custom-collapsible-invoker" slot="invoker">
|
<button class="demo-custom-collapsible-invoker" slot="invoker">MORE ABOUT CARS</button>
|
||||||
MORE ABOUT CARS
|
|
||||||
</button>
|
|
||||||
<div slot="content">
|
<div slot="content">
|
||||||
Most definitions of cars say that they run primarily on roads, seat one to eight people, have
|
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.
|
four tires, and mainly transport people rather than goods.
|
||||||
|
|
@ -170,9 +162,7 @@ export const customAnimation = () => html`
|
||||||
vehicle.
|
vehicle.
|
||||||
</div>
|
</div>
|
||||||
<custom-collapsible>
|
<custom-collapsible>
|
||||||
<button class="demo-custom-collapsible-invoker" slot="invoker">
|
<button class="demo-custom-collapsible-invoker" slot="invoker">MORE ABOUT MOTORCYCLES</button>
|
||||||
MORE ABOUT MOTORCYCLES
|
|
||||||
</button>
|
|
||||||
<div slot="content">
|
<div slot="content">
|
||||||
Motorcycle design varies greatly to suit a range of different purposes: long distance
|
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
|
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.
|
A car (or automobile) is a wheeled motor vehicle used for transportation.
|
||||||
</div>
|
</div>
|
||||||
<custom-collapsible opened>
|
<custom-collapsible opened>
|
||||||
<button class="demo-custom-collapsible-invoker" slot="invoker">
|
<button class="demo-custom-collapsible-invoker" slot="invoker">MORE ABOUT CARS</button>
|
||||||
MORE ABOUT CARS
|
|
||||||
</button>
|
|
||||||
<div slot="content">
|
<div slot="content">
|
||||||
Most definitions of cars say that they run primarily on roads, seat one to eight people,
|
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.
|
have four tires, and mainly transport people rather than goods.
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,9 @@ Since Scoped Elements changes tagnames under the hood, a tagname querySelector s
|
||||||
like this:
|
like this:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
this.querySelector(this.constructor.getScopedTagName('lion-input', this.constructor.scopedElements));
|
this.querySelector(
|
||||||
|
this.constructor.getScopedTagName('lion-input', this.constructor.scopedElements),
|
||||||
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
## CSS selectors
|
## CSS selectors
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@open-wc/dedupe-mixin": "^1.2.18",
|
"@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-element": "~2.4.0",
|
||||||
"lit-html": "^1.3.0"
|
"lit-html": "^1.3.0"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ import { dedupeMixin } from '@open-wc/dedupe-mixin';
|
||||||
*/
|
*/
|
||||||
const DelegateMixinImplementation = superclass =>
|
const DelegateMixinImplementation = superclass =>
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
class DelegateMixin extends superclass {
|
class extends superclass {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import { dedupeMixin } from '@open-wc/dedupe-mixin';
|
||||||
*/
|
*/
|
||||||
const DisabledMixinImplementation = superclass =>
|
const DisabledMixinImplementation = superclass =>
|
||||||
// eslint-disable-next-line no-shadow
|
// eslint-disable-next-line no-shadow
|
||||||
class DisabledMixinHost extends superclass {
|
class extends superclass {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
disabled: {
|
disabled: {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import { DisabledMixin } from './DisabledMixin.js';
|
||||||
*/
|
*/
|
||||||
const DisabledWithTabIndexMixinImplementation = superclass =>
|
const DisabledWithTabIndexMixinImplementation = superclass =>
|
||||||
// eslint-disable-next-line no-shadow
|
// eslint-disable-next-line no-shadow
|
||||||
class DisabledWithTabIndexMixinHost extends DisabledMixin(superclass) {
|
class extends DisabledMixin(superclass) {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
// we use a property here as if we use the native tabIndex we can not set a default value
|
// we use a property here as if we use the native tabIndex we can not set a default value
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import { dedupeMixin } from '@open-wc/dedupe-mixin';
|
||||||
*/
|
*/
|
||||||
const SlotMixinImplementation = superclass =>
|
const SlotMixinImplementation = superclass =>
|
||||||
// eslint-disable-next-line no-unused-vars, no-shadow
|
// eslint-disable-next-line no-unused-vars, no-shadow
|
||||||
class SlotMixinHost extends superclass {
|
class extends superclass {
|
||||||
/**
|
/**
|
||||||
* @return {SlotsMap}
|
* @return {SlotsMap}
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import { dedupeMixin } from '@open-wc/dedupe-mixin';
|
||||||
*/
|
*/
|
||||||
const UpdateStylesMixinImplementation = superclass =>
|
const UpdateStylesMixinImplementation = superclass =>
|
||||||
// eslint-disable-next-line no-shadow
|
// eslint-disable-next-line no-shadow
|
||||||
class UpdateStylesMixinHost extends superclass {
|
class extends superclass {
|
||||||
/**
|
/**
|
||||||
* @example
|
* @example
|
||||||
* <my-element>
|
* <my-element>
|
||||||
|
|
|
||||||
4
packages/core/types/DelegateMixinTypes.d.ts
vendored
4
packages/core/types/DelegateMixinTypes.d.ts
vendored
|
|
@ -9,7 +9,7 @@ export type Delegations = {
|
||||||
attributes: string[];
|
attributes: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export declare class DelegateMixinHost {
|
export declare class DelegateHost {
|
||||||
delegations: Delegations;
|
delegations: Delegations;
|
||||||
|
|
||||||
protected _connectDelegateMixin(): void;
|
protected _connectDelegateMixin(): void;
|
||||||
|
|
@ -50,6 +50,6 @@ export declare class DelegateMixinHost {
|
||||||
*/
|
*/
|
||||||
declare function DelegateMixinImplementation<T extends Constructor<LitElement>>(
|
declare function DelegateMixinImplementation<T extends Constructor<LitElement>>(
|
||||||
superclass: T,
|
superclass: T,
|
||||||
): T & Constructor<DelegateMixinHost>;
|
): T & Constructor<DelegateHost>;
|
||||||
|
|
||||||
export type DelegateMixin = typeof DelegateMixinImplementation;
|
export type DelegateMixin = typeof DelegateMixinImplementation;
|
||||||
|
|
|
||||||
10
packages/core/types/DisabledMixinTypes.d.ts
vendored
10
packages/core/types/DisabledMixinTypes.d.ts
vendored
|
|
@ -1,13 +1,7 @@
|
||||||
import { Constructor } from '@open-wc/dedupe-mixin';
|
import { Constructor } from '@open-wc/dedupe-mixin';
|
||||||
import { LitElement } from 'lit-element';
|
import { LitElement } from 'lit-element';
|
||||||
|
|
||||||
export declare class DisabledMixinHost {
|
export declare class DisabledHost {
|
||||||
static get properties(): {
|
|
||||||
disabled: {
|
|
||||||
type: BooleanConstructor;
|
|
||||||
reflect: boolean;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -26,6 +20,6 @@ export declare class DisabledMixinHost {
|
||||||
|
|
||||||
export declare function DisabledMixinImplementation<T extends Constructor<LitElement>>(
|
export declare function DisabledMixinImplementation<T extends Constructor<LitElement>>(
|
||||||
superclass: T,
|
superclass: T,
|
||||||
): T & Constructor<DisabledMixinHost>;
|
): T & Constructor<DisabledHost>;
|
||||||
|
|
||||||
export type DisabledMixin = typeof DisabledMixinImplementation;
|
export type DisabledMixin = typeof DisabledMixinImplementation;
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,7 @@
|
||||||
import { Constructor } from '@open-wc/dedupe-mixin';
|
import { Constructor } from '@open-wc/dedupe-mixin';
|
||||||
import { DisabledMixinHost } from './DisabledMixinTypes';
|
import { DisabledHost } from './DisabledMixinTypes';
|
||||||
import { LitElement } from 'lit-element';
|
import { LitElement } from 'lit-element';
|
||||||
export declare class DisabledWithTabIndexMixinHost {
|
export declare class DisabledWithTabIndexHost {
|
||||||
static get properties(): {
|
|
||||||
tabIndex: {
|
|
||||||
type: NumberConstructor;
|
|
||||||
reflect: boolean;
|
|
||||||
attribute: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
tabIndex: number;
|
tabIndex: number;
|
||||||
/**
|
/**
|
||||||
* Makes request to make the element disabled and set the tabindex
|
* Makes request to make the element disabled and set the tabindex
|
||||||
|
|
@ -27,6 +20,6 @@ export declare class DisabledWithTabIndexMixinHost {
|
||||||
|
|
||||||
export declare function DisabledWithTabIndexMixinImplementation<T extends Constructor<LitElement>>(
|
export declare function DisabledWithTabIndexMixinImplementation<T extends Constructor<LitElement>>(
|
||||||
superclass: T,
|
superclass: T,
|
||||||
): T & Constructor<DisabledWithTabIndexMixinHost> & Constructor<DisabledMixinHost>;
|
): T & Constructor<DisabledWithTabIndexHost> & Constructor<DisabledHost>;
|
||||||
|
|
||||||
export type DisabledWithTabIndexMixin = typeof DisabledWithTabIndexMixinImplementation;
|
export type DisabledWithTabIndexMixin = typeof DisabledWithTabIndexMixinImplementation;
|
||||||
|
|
|
||||||
4
packages/core/types/SlotMixinTypes.d.ts
vendored
4
packages/core/types/SlotMixinTypes.d.ts
vendored
|
|
@ -6,7 +6,7 @@ export type SlotsMap = {
|
||||||
[key: string]: typeof slotFunction;
|
[key: string]: typeof slotFunction;
|
||||||
};
|
};
|
||||||
|
|
||||||
export declare class SlotMixinHost {
|
export declare class SlotHost {
|
||||||
/**
|
/**
|
||||||
* Obtains all the slots to create
|
* Obtains all the slots to create
|
||||||
*/
|
*/
|
||||||
|
|
@ -50,6 +50,6 @@ export declare class SlotMixinHost {
|
||||||
*/
|
*/
|
||||||
export declare function SlotMixinImplementation<T extends Constructor<HTMLElement>>(
|
export declare function SlotMixinImplementation<T extends Constructor<HTMLElement>>(
|
||||||
superclass: T,
|
superclass: T,
|
||||||
): T & Constructor<SlotMixinHost>;
|
): T & Constructor<SlotHost>;
|
||||||
|
|
||||||
export type SlotMixin = typeof SlotMixinImplementation;
|
export type SlotMixin = typeof SlotMixinImplementation;
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { Constructor } from '@open-wc/dedupe-mixin';
|
||||||
export type StylesMap = {
|
export type StylesMap = {
|
||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
};
|
};
|
||||||
export declare class UpdateStylesMixinHost {
|
export declare class UpdateStylesHost {
|
||||||
/**
|
/**
|
||||||
* @example
|
* @example
|
||||||
* <my-element>
|
* <my-element>
|
||||||
|
|
@ -29,6 +29,6 @@ export declare class UpdateStylesMixinHost {
|
||||||
*/
|
*/
|
||||||
declare function UpdateStylesMixinImplementation<T extends Constructor<HTMLElement>>(
|
declare function UpdateStylesMixinImplementation<T extends Constructor<HTMLElement>>(
|
||||||
superclass: T,
|
superclass: T,
|
||||||
): T & Constructor<UpdateStylesMixinHost>;
|
): T & Constructor<UpdateStylesHost>;
|
||||||
|
|
||||||
export type UpdateStylesMixin = typeof UpdateStylesMixinImplementation;
|
export type UpdateStylesMixin = typeof UpdateStylesMixinImplementation;
|
||||||
|
|
|
||||||
|
|
@ -27,9 +27,7 @@ describe('lion-dialog', () => {
|
||||||
it('should show content on invoker click', async () => {
|
it('should show content on invoker click', async () => {
|
||||||
const el = await fixture(html`
|
const el = await fixture(html`
|
||||||
<lion-dialog>
|
<lion-dialog>
|
||||||
<div slot="content" class="dialog">
|
<div slot="content" class="dialog">Hey there</div>
|
||||||
Hey there
|
|
||||||
</div>
|
|
||||||
<button slot="invoker">Popup button</button>
|
<button slot="invoker">Popup button</button>
|
||||||
</lion-dialog>
|
</lion-dialog>
|
||||||
`);
|
`);
|
||||||
|
|
@ -45,9 +43,7 @@ describe('lion-dialog', () => {
|
||||||
<div slot="content">
|
<div slot="content">
|
||||||
open nested overlay:
|
open nested overlay:
|
||||||
<lion-dialog>
|
<lion-dialog>
|
||||||
<div slot="content">
|
<div slot="content">Nested content</div>
|
||||||
Nested content
|
|
||||||
</div>
|
|
||||||
<button slot="invoker">nested invoker button</button>
|
<button slot="invoker">nested invoker button</button>
|
||||||
</lion-dialog>
|
</lion-dialog>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,11 +1,13 @@
|
||||||
import { dedupeMixin } from '@lion/core';
|
import { dedupeMixin } from '@lion/core';
|
||||||
|
import { FormControlMixin } from './FormControlMixin.js';
|
||||||
/**
|
/**
|
||||||
* @typedef {import('../types/FocusMixinTypes').FocusMixin} FocusMixin
|
* @typedef {import('../types/FocusMixinTypes').FocusMixin} FocusMixin
|
||||||
* @type {FocusMixin}
|
* @type {FocusMixin}
|
||||||
|
* @param {import('@open-wc/dedupe-mixin').Constructor<import('@lion/core').LitElement>} superclass
|
||||||
*/
|
*/
|
||||||
const FocusMixinImplementation = superclass =>
|
const FocusMixinImplementation = superclass =>
|
||||||
// eslint-disable-next-line no-unused-vars, max-len, no-shadow
|
// eslint-disable-next-line no-unused-vars, max-len, no-shadow
|
||||||
class FocusMixin extends superclass {
|
class FocusMixin extends FormControlMixin(superclass) {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
focused: {
|
focused: {
|
||||||
|
|
@ -21,16 +23,12 @@ const FocusMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
if (super.connectedCallback) {
|
super.connectedCallback();
|
||||||
super.connectedCallback();
|
|
||||||
}
|
|
||||||
this.__registerEventsForFocusMixin();
|
this.__registerEventsForFocusMixin();
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
if (super.disconnectedCallback) {
|
super.disconnectedCallback();
|
||||||
super.disconnectedCallback();
|
|
||||||
}
|
|
||||||
this.__teardownEventsForFocusMixin();
|
this.__teardownEventsForFocusMixin();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -101,10 +99,22 @@ const FocusMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
__teardownEventsForFocusMixin() {
|
__teardownEventsForFocusMixin() {
|
||||||
this._inputNode.removeEventListener('focus', this.__redispatchFocus);
|
this._inputNode.removeEventListener(
|
||||||
this._inputNode.removeEventListener('blur', this.__redispatchBlur);
|
'focus',
|
||||||
this._inputNode.removeEventListener('focusin', this.__redispatchFocusin);
|
/** @type {EventListenerOrEventListenerObject} */ (this.__redispatchFocus),
|
||||||
this._inputNode.removeEventListener('focusout', this.__redispatchFocusout);
|
);
|
||||||
|
this._inputNode.removeEventListener(
|
||||||
|
'blur',
|
||||||
|
/** @type {EventListenerOrEventListenerObject} */ (this.__redispatchBlur),
|
||||||
|
);
|
||||||
|
this._inputNode.removeEventListener(
|
||||||
|
'focusin',
|
||||||
|
/** @type {EventListenerOrEventListenerObject} */ (this.__redispatchFocusin),
|
||||||
|
);
|
||||||
|
this._inputNode.removeEventListener(
|
||||||
|
'focusout',
|
||||||
|
/** @type {EventListenerOrEventListenerObject} */ (this.__redispatchFocusout),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { css, dedupeMixin, html, nothing, SlotMixin } from '@lion/core';
|
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 { FormRegisteringMixin } from './registration/FormRegisteringMixin.js';
|
||||||
import { getAriaElementsInRightDomOrder } from './utils/getAriaElementsInRightDomOrder.js';
|
import { getAriaElementsInRightDomOrder } from './utils/getAriaElementsInRightDomOrder.js';
|
||||||
|
import { Unparseable } from './validate/Unparseable.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates random unique identifier (for dom elements)
|
* 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:
|
* This Mixin is a shared fundament for all form components, it's applied on:
|
||||||
* - LionField (which is extended to LionInput, LionTextarea, LionSelect etc. etc.)
|
* - LionField (which is extended to LionInput, LionTextarea, LionSelect etc. etc.)
|
||||||
* - LionFieldset (which is extended to LionRadioGroup, LionCheckboxGroup, LionForm)
|
* - LionFieldset (which is extended to LionRadioGroup, LionCheckboxGroup, LionForm)
|
||||||
* @typedef {import('lit-html').TemplateResult} TemplateResult
|
* @typedef {import('@lion/core').TemplateResult} TemplateResult
|
||||||
* @typedef {import('lit-element').CSSResult} CSSResult
|
* @typedef {import('@lion/core').CSSResult} CSSResult
|
||||||
* @typedef {import('lit-html').nothing} nothing
|
* @typedef {import('@lion/core').nothing} nothing
|
||||||
* @typedef {import('@lion/core/types/SlotMixinTypes').SlotsMap} SlotsMap
|
* @typedef {import('@lion/core/types/SlotMixinTypes').SlotsMap} SlotsMap
|
||||||
* @typedef {import('../types/FormControlMixinTypes.js').FormControlMixin} FormControlMixin
|
* @typedef {import('../types/FormControlMixinTypes.js').FormControlMixin} FormControlMixin
|
||||||
* @type {FormControlMixin}
|
* @type {FormControlMixin}
|
||||||
|
* @param {import('@open-wc/dedupe-mixin').Constructor<import('@lion/core').LitElement>} superclass
|
||||||
*/
|
*/
|
||||||
const FormControlMixinImplementation = superclass =>
|
const FormControlMixinImplementation = superclass =>
|
||||||
// eslint-disable-next-line no-shadow, no-unused-vars
|
// 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() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
/**
|
/**
|
||||||
|
|
@ -48,6 +50,21 @@ const FormControlMixinImplementation = superclass =>
|
||||||
type: String,
|
type: String,
|
||||||
attribute: 'help-text',
|
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`
|
* Contains all elements that should end up in aria-labelledby of `._inputNode`
|
||||||
*/
|
*/
|
||||||
|
|
@ -112,7 +129,8 @@ const FormControlMixinImplementation = superclass =>
|
||||||
* @return {string}
|
* @return {string}
|
||||||
*/
|
*/
|
||||||
get fieldName() {
|
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() {
|
get _feedbackNode() {
|
||||||
return this.__getDirectSlotChild('feedback');
|
return /** @type {import('./validate/LionValidationFeedback').LionValidationFeedback | undefined} */ (this.__getDirectSlotChild(
|
||||||
|
'feedback',
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
@ -197,7 +217,11 @@ const FormControlMixinImplementation = superclass =>
|
||||||
this._ariaDescribedNodes = [];
|
this._ariaDescribedNodes = [];
|
||||||
/** @type {'child' | 'choice-group' | 'fieldset'} */
|
/** @type {'child' | 'choice-group' | 'fieldset'} */
|
||||||
this._repropagationRole = 'child';
|
this._repropagationRole = 'child';
|
||||||
this.addEventListener('model-value-changed', this.__repropagateChildrenValues);
|
this._isRepropagationEndpoint = false;
|
||||||
|
this.addEventListener(
|
||||||
|
'model-value-changed',
|
||||||
|
/** @type {EventListenerOrEventListenerObject} */ (this.__repropagateChildrenValues),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
|
|
@ -339,12 +363,8 @@ const FormControlMixinImplementation = superclass =>
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
return html`
|
return html`
|
||||||
<div class="form-field__group-one">
|
<div class="form-field__group-one">${this._groupOneTemplate()}</div>
|
||||||
${this._groupOneTemplate()}
|
<div class="form-field__group-two">${this._groupTwoTemplate()}</div>
|
||||||
</div>
|
|
||||||
<div class="form-field__group-two">
|
|
||||||
${this._groupTwoTemplate()}
|
|
||||||
</div>
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -479,10 +499,15 @@ const FormControlMixinImplementation = superclass =>
|
||||||
/**
|
/**
|
||||||
* @param {?} modelValue
|
* @param {?} modelValue
|
||||||
* @return {boolean}
|
* @return {boolean}
|
||||||
|
*
|
||||||
|
* FIXME: Move to FormatMixin? Since there we have access to modelValue prop
|
||||||
*/
|
*/
|
||||||
|
// @ts-expect-error
|
||||||
_isEmpty(modelValue = this.modelValue) {
|
_isEmpty(modelValue = this.modelValue) {
|
||||||
let value = modelValue;
|
let value = modelValue;
|
||||||
|
// @ts-expect-error
|
||||||
if (this.modelValue instanceof Unparseable) {
|
if (this.modelValue instanceof Unparseable) {
|
||||||
|
// @ts-expect-error
|
||||||
value = this.modelValue.viewValue;
|
value = this.modelValue.viewValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -629,7 +654,7 @@ const FormControlMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return {HTMLElement[]}
|
* @return {Array.<HTMLElement|undefined>}
|
||||||
*/
|
*/
|
||||||
// Returns dom references to all elements that should be referred to by field(s)
|
// Returns dom references to all elements that should be referred to by field(s)
|
||||||
_getAriaDescriptionElements() {
|
_getAriaDescriptionElements() {
|
||||||
|
|
@ -681,10 +706,12 @@ const FormControlMixinImplementation = superclass =>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} slotName
|
* @param {string} slotName
|
||||||
* @return {HTMLElement}
|
* @return {HTMLElement | undefined}
|
||||||
*/
|
*/
|
||||||
__getDirectSlotChild(slotName) {
|
__getDirectSlotChild(slotName) {
|
||||||
return [...this.children].find(el => el.slot === slotName);
|
return /** @type {HTMLElement[]} */ (Array.from(this.children)).find(
|
||||||
|
el => el.slot === slotName,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
__dispatchInitialModelValueChangedEvent() {
|
__dispatchInitialModelValueChangedEvent() {
|
||||||
|
|
@ -756,6 +783,7 @@ const FormControlMixinImplementation = superclass =>
|
||||||
// We only send the checked changed up (not the unchecked). In this way a choice group
|
// 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)
|
// (radio-group, checkbox-group, select/listbox) acts as an 'endpoint' (a single Field)
|
||||||
// just like the native <select>
|
// just like the native <select>
|
||||||
|
// @ts-expect-error multipleChoice is not directly available but only as side effect
|
||||||
if (this._repropagationRole === 'choice-group' && !this.multipleChoice && !target.checked) {
|
if (this._repropagationRole === 'choice-group' && !this.multipleChoice && !target.checked) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
/* eslint-disable class-methods-use-this */
|
/* eslint-disable class-methods-use-this */
|
||||||
|
|
||||||
import { dedupeMixin } from '@lion/core';
|
import { dedupeMixin } from '@lion/core';
|
||||||
|
import { FormControlMixin } from './FormControlMixin.js';
|
||||||
import { Unparseable } from './validate/Unparseable.js';
|
import { Unparseable } from './validate/Unparseable.js';
|
||||||
|
import { ValidateMixin } from './validate/ValidateMixin.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('../types/FormatMixinTypes').FormatMixin} FormatMixin
|
* @typedef {import('../types/FormatMixinTypes').FormatMixin} FormatMixin
|
||||||
|
|
@ -52,25 +54,12 @@ import { Unparseable } from './validate/Unparseable.js';
|
||||||
* Flow: serializedValue (deserializer) -> `.modelValue` (formatter) -> `.formattedValue` -> `._inputNode.value`
|
* Flow: serializedValue (deserializer) -> `.modelValue` (formatter) -> `.formattedValue` -> `._inputNode.value`
|
||||||
*
|
*
|
||||||
* @type {FormatMixin}
|
* @type {FormatMixin}
|
||||||
|
* @param {import('@open-wc/dedupe-mixin').Constructor<import('@lion/core').LitElement>} superclass
|
||||||
*/
|
*/
|
||||||
const FormatMixinImplementation = superclass =>
|
const FormatMixinImplementation = superclass =>
|
||||||
class FormatMixin extends superclass {
|
class FormatMixin extends ValidateMixin(FormControlMixin(superclass)) {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
/**
|
|
||||||
* 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 },
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The view value is the result of the formatter function (when available).
|
* The view value is the result of the formatter function (when available).
|
||||||
* The result will be stored in the native _inputNode (usually an input[type=text]).
|
* The result will be stored in the native _inputNode (usually an input[type=text]).
|
||||||
|
|
@ -296,7 +285,7 @@ const FormatMixinImplementation = superclass =>
|
||||||
*/
|
*/
|
||||||
_onModelValueChanged(...args) {
|
_onModelValueChanged(...args) {
|
||||||
this._calculateValues({ source: 'model' });
|
this._calculateValues({ source: 'model' });
|
||||||
// @ts-ignore only passing this so a subclasser can use it, but we do not use it ourselves
|
// @ts-expect-error only passing this so a subclasser can use it, but we do not use it ourselves
|
||||||
this._dispatchModelValueChangedEvent(...args);
|
this._dispatchModelValueChangedEvent(...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -405,7 +394,8 @@ const FormatMixinImplementation = superclass =>
|
||||||
this._inputNode.removeEventListener('input', this._proxyInputEvent);
|
this._inputNode.removeEventListener('input', this._proxyInputEvent);
|
||||||
this._inputNode.removeEventListener(
|
this._inputNode.removeEventListener(
|
||||||
this.formatOn,
|
this.formatOn,
|
||||||
this._reflectBackFormattedValueDebounced,
|
/** @type {EventListenerOrEventListenerObject} */ (this
|
||||||
|
._reflectBackFormattedValueDebounced),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,9 @@ import { FormControlMixin } from './FormControlMixin.js';
|
||||||
* - leaves a form field(blur) -> 'touched' will be set to true. 'prefilled' when a
|
* - leaves a form field(blur) -> 'touched' will be set to true. 'prefilled' when a
|
||||||
* field is left non-empty
|
* field is left non-empty
|
||||||
* - on keyup (actually, on the model-value-changed event) -> 'dirty' will be set to true
|
* - on keyup (actually, on the model-value-changed event) -> 'dirty' will be set to true
|
||||||
* @param {HTMLElement} superclass
|
*
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {InteractionStateMixin}
|
* @type {InteractionStateMixin}
|
||||||
|
* @param {import('@open-wc/dedupe-mixin').Constructor<import('@lion/core').LitElement>} superclass
|
||||||
*/
|
*/
|
||||||
const InteractionStateMixinImplementation = superclass =>
|
const InteractionStateMixinImplementation = superclass =>
|
||||||
class InteractionStateMixin extends FormControlMixin(superclass) {
|
class InteractionStateMixin extends FormControlMixin(superclass) {
|
||||||
|
|
@ -105,18 +103,14 @@ const InteractionStateMixinImplementation = superclass =>
|
||||||
* Register event handlers and validate prefilled inputs
|
* Register event handlers and validate prefilled inputs
|
||||||
*/
|
*/
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
if (super.connectedCallback) {
|
super.connectedCallback();
|
||||||
super.connectedCallback();
|
|
||||||
}
|
|
||||||
this.addEventListener(this._leaveEvent, this._iStateOnLeave);
|
this.addEventListener(this._leaveEvent, this._iStateOnLeave);
|
||||||
this.addEventListener(this._valueChangedEvent, this._iStateOnValueChange);
|
this.addEventListener(this._valueChangedEvent, this._iStateOnValueChange);
|
||||||
this.initInteractionState();
|
this.initInteractionState();
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
if (super.disconnectedCallback) {
|
super.disconnectedCallback();
|
||||||
super.disconnectedCallback();
|
|
||||||
}
|
|
||||||
this.removeEventListener(this._leaveEvent, this._iStateOnLeave);
|
this.removeEventListener(this._leaveEvent, this._iStateOnLeave);
|
||||||
this.removeEventListener(this._valueChangedEvent, this._iStateOnValueChange);
|
this.removeEventListener(this._valueChangedEvent, this._iStateOnValueChange);
|
||||||
}
|
}
|
||||||
|
|
@ -169,6 +163,27 @@ const InteractionStateMixinImplementation = superclass =>
|
||||||
_onDirtyChanged() {
|
_onDirtyChanged() {
|
||||||
this.dispatchEvent(new CustomEvent('dirty-changed', { bubbles: true, composed: true }));
|
this.dispatchEvent(new CustomEvent('dirty-changed', { bubbles: true, composed: true }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the validity feedback when one of the following conditions is met:
|
||||||
|
*
|
||||||
|
* - submitted
|
||||||
|
* If the form is submitted, always show the error message.
|
||||||
|
*
|
||||||
|
* - prefilled
|
||||||
|
* the user already filled in something, or the value is prefilled
|
||||||
|
* when the form is initially rendered.
|
||||||
|
*
|
||||||
|
* - touched && dirty
|
||||||
|
* When a user starts typing for the first time in a field with for instance `required`
|
||||||
|
* validation, error message should not be shown until a field becomes `touched`
|
||||||
|
* (a user leaves(blurs) a field).
|
||||||
|
* When a user enters a field without altering the value(making it `dirty`),
|
||||||
|
* an error message shouldn't be shown either.
|
||||||
|
*/
|
||||||
|
_showFeedbackConditionFor() {
|
||||||
|
return (this.touched && this.dirty) || this.prefilled || this.submitted;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const InteractionStateMixin = dedupeMixin(InteractionStateMixinImplementation);
|
export const InteractionStateMixin = dedupeMixin(InteractionStateMixinImplementation);
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,10 @@
|
||||||
import { LitElement, SlotMixin } from '@lion/core';
|
import { LitElement, SlotMixin } from '@lion/core';
|
||||||
import { DisabledMixin } from '@lion/core/src/DisabledMixin.js';
|
|
||||||
import { ValidateMixin } from './validate/ValidateMixin.js';
|
import { ValidateMixin } from './validate/ValidateMixin.js';
|
||||||
import { FocusMixin } from './FocusMixin.js';
|
import { FocusMixin } from './FocusMixin.js';
|
||||||
import { FormatMixin } from './FormatMixin.js';
|
import { FormatMixin } from './FormatMixin.js';
|
||||||
import { FormControlMixin } from './FormControlMixin.js';
|
import { FormControlMixin } from './FormControlMixin.js';
|
||||||
import { InteractionStateMixin } from './InteractionStateMixin.js'; // applies FocusMixin
|
import { InteractionStateMixin } from './InteractionStateMixin.js'; // applies FocusMixin
|
||||||
|
|
||||||
/* eslint-disable wc/guard-super-call */
|
|
||||||
|
|
||||||
// TODO: Add submitted prop to InteractionStateMixin.
|
|
||||||
/**
|
/**
|
||||||
* `LionField`: wraps <input>, <textarea>, <select> and other interactable elements.
|
* `LionField`: wraps <input>, <textarea>, <select> and other interactable elements.
|
||||||
* Also it would follow a nice hierarchy: lion-form -> lion-fieldset -> lion-field
|
* Also it would follow a nice hierarchy: lion-form -> lion-fieldset -> lion-field
|
||||||
|
|
@ -26,17 +22,12 @@ import { InteractionStateMixin } from './InteractionStateMixin.js'; // applies F
|
||||||
*
|
*
|
||||||
* @customElement lion-field
|
* @customElement lion-field
|
||||||
*/
|
*/
|
||||||
|
// @ts-expect-error base constructors same return type
|
||||||
export class LionField extends FormControlMixin(
|
export class LionField extends FormControlMixin(
|
||||||
InteractionStateMixin(
|
InteractionStateMixin(FocusMixin(FormatMixin(ValidateMixin(SlotMixin(LitElement))))),
|
||||||
FocusMixin(FormatMixin(ValidateMixin(DisabledMixin(SlotMixin(LitElement))))),
|
|
||||||
),
|
|
||||||
) {
|
) {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
submitted: {
|
|
||||||
// make sure validation can be triggered based on observer
|
|
||||||
type: Boolean,
|
|
||||||
},
|
|
||||||
autocomplete: {
|
autocomplete: {
|
||||||
type: String,
|
type: String,
|
||||||
reflect: true,
|
reflect: true,
|
||||||
|
|
@ -47,6 +38,10 @@ export class LionField extends FormControlMixin(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get _inputNode() {
|
||||||
|
return /** @type {HTMLInputElement} */ (super._inputNode); // casts type
|
||||||
|
}
|
||||||
|
|
||||||
/** @type {number} */
|
/** @type {number} */
|
||||||
get selectionStart() {
|
get selectionStart() {
|
||||||
const native = this._inputNode;
|
const native = this._inputNode;
|
||||||
|
|
@ -85,6 +80,7 @@ export class LionField extends FormControlMixin(
|
||||||
// if not yet connected to dom can't change the value
|
// if not yet connected to dom can't change the value
|
||||||
if (this._inputNode) {
|
if (this._inputNode) {
|
||||||
this._setValueAndPreserveCaret(value);
|
this._setValueAndPreserveCaret(value);
|
||||||
|
/** @type {string | undefined} */
|
||||||
this.__value = undefined;
|
this.__value = undefined;
|
||||||
} else {
|
} else {
|
||||||
this.__value = value;
|
this.__value = value;
|
||||||
|
|
@ -98,11 +94,16 @@ export class LionField extends FormControlMixin(
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.name = '';
|
this.name = '';
|
||||||
this.submitted = false;
|
/** @type {string | undefined} */
|
||||||
|
this.autocomplete = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('lit-element').PropertyValues } changedProperties
|
||||||
|
*/
|
||||||
firstUpdated(changedProperties) {
|
firstUpdated(changedProperties) {
|
||||||
super.firstUpdated(changedProperties);
|
super.firstUpdated(changedProperties);
|
||||||
|
/** @type {any} */
|
||||||
this._initialModelValue = this.modelValue;
|
this._initialModelValue = this.modelValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,6 +119,9 @@ export class LionField extends FormControlMixin(
|
||||||
this._inputNode.removeEventListener('change', this._onChange);
|
this._inputNode.removeEventListener('change', this._onChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('lit-element').PropertyValues } changedProperties
|
||||||
|
*/
|
||||||
updated(changedProperties) {
|
updated(changedProperties) {
|
||||||
super.updated(changedProperties);
|
super.updated(changedProperties);
|
||||||
|
|
||||||
|
|
@ -131,14 +135,12 @@ export class LionField extends FormControlMixin(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (changedProperties.has('autocomplete')) {
|
if (changedProperties.has('autocomplete')) {
|
||||||
this._inputNode.autocomplete = this.autocomplete;
|
this._inputNode.autocomplete = /** @type {string} */ (this.autocomplete);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resetInteractionState() {
|
resetInteractionState() {
|
||||||
if (super.resetInteractionState) {
|
super.resetInteractionState();
|
||||||
super.resetInteractionState();
|
|
||||||
}
|
|
||||||
this.submitted = false;
|
this.submitted = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -147,19 +149,15 @@ export class LionField extends FormControlMixin(
|
||||||
this.resetInteractionState();
|
this.resetInteractionState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears modelValue.
|
||||||
|
* Interaction states are not cleared (use resetInteractionState for this)
|
||||||
|
*/
|
||||||
clear() {
|
clear() {
|
||||||
if (super.clear) {
|
|
||||||
// Let validationMixin and interactionStateMixin clear their
|
|
||||||
// invalid and dirty/touched states respectively
|
|
||||||
super.clear();
|
|
||||||
}
|
|
||||||
this.modelValue = ''; // can't set null here, because IE11 treats it as a string
|
this.modelValue = ''; // can't set null here, because IE11 treats it as a string
|
||||||
}
|
}
|
||||||
|
|
||||||
_onChange() {
|
_onChange() {
|
||||||
if (super._onChange) {
|
|
||||||
super._onChange();
|
|
||||||
}
|
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent('user-input-changed', {
|
new CustomEvent('user-input-changed', {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
|
|
|
||||||
|
|
@ -2,272 +2,316 @@ import { dedupeMixin } from '@lion/core';
|
||||||
import { FormRegistrarMixin } from '../registration/FormRegistrarMixin.js';
|
import { FormRegistrarMixin } from '../registration/FormRegistrarMixin.js';
|
||||||
import { InteractionStateMixin } from '../InteractionStateMixin.js';
|
import { InteractionStateMixin } from '../InteractionStateMixin.js';
|
||||||
|
|
||||||
export const ChoiceGroupMixin = dedupeMixin(
|
/**
|
||||||
superclass =>
|
* @typedef {import('../../types/choice-group/ChoiceGroupMixinTypes').ChoiceGroupMixin} ChoiceGroupMixin
|
||||||
// eslint-disable-next-line
|
* @typedef {import('../../types/FormControlMixinTypes').FormControlHost} FormControlHost
|
||||||
class ChoiceGroupMixin extends FormRegistrarMixin(InteractionStateMixin(superclass)) {
|
* @typedef {import('../../types/registration/FormRegistrarMixinTypes').ElementWithParentFormGroup} ElementWithParentFormGroup
|
||||||
static get properties() {
|
* @typedef {FormControlHost & HTMLElement & {__parentFormGroup?:HTMLElement, checked?:boolean}} FormControl
|
||||||
return {
|
*/
|
||||||
/**
|
|
||||||
* @desc When false (default), modelValue and serializedValue will reflect the
|
/**
|
||||||
* currently selected choice (usually a string). When true, modelValue will and
|
* @type {ChoiceGroupMixin}
|
||||||
* serializedValue will be an array of strings.
|
* @param {import('@open-wc/dedupe-mixin').Constructor<import('@lion/core').LitElement>} superclass
|
||||||
* @type {boolean}
|
*/
|
||||||
*/
|
const ChoiceGroupMixinImplementation = superclass =>
|
||||||
multipleChoice: {
|
class ChoiceGroupMixin extends FormRegistrarMixin(InteractionStateMixin(superclass)) {
|
||||||
type: Boolean,
|
static get properties() {
|
||||||
attribute: 'multiple-choice',
|
return {
|
||||||
},
|
/**
|
||||||
};
|
* @desc When false (default), modelValue and serializedValue will reflect the
|
||||||
|
* currently selected choice (usually a string). When true, modelValue will and
|
||||||
|
* serializedValue will be an array of strings.
|
||||||
|
*/
|
||||||
|
multipleChoice: {
|
||||||
|
type: Boolean,
|
||||||
|
attribute: 'multiple-choice',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
get modelValue() {
|
||||||
|
const elems = this._getCheckedElements();
|
||||||
|
if (this.multipleChoice) {
|
||||||
|
return elems.map(el => el.modelValue.value);
|
||||||
}
|
}
|
||||||
|
return elems[0] ? elems[0].modelValue.value : '';
|
||||||
|
}
|
||||||
|
|
||||||
get modelValue() {
|
set modelValue(value) {
|
||||||
const elems = this._getCheckedElements();
|
/**
|
||||||
if (this.multipleChoice) {
|
* @param {{ modelValue: { value: any; }; }} el
|
||||||
return elems.map(el => el.modelValue.value);
|
* @param {any} val
|
||||||
}
|
*/
|
||||||
return elems[0] ? elems[0].modelValue.value : '';
|
const checkCondition = (el, val) => el.modelValue.value === val;
|
||||||
}
|
|
||||||
|
|
||||||
set modelValue(value) {
|
if (this.__isInitialModelValue) {
|
||||||
if (this.__isInitialModelValue) {
|
this.__isInitialModelValue = false;
|
||||||
this.__isInitialModelValue = false;
|
this.registrationComplete.then(() => {
|
||||||
this.registrationComplete.then(() => {
|
this._setCheckedElements(value, checkCondition);
|
||||||
this._setCheckedElements(value, (el, val) => el.modelValue.value === val);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this._setCheckedElements(value, (el, val) => el.modelValue.value === val);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get serializedValue() {
|
|
||||||
// We want to filter out disabled values out by default:
|
|
||||||
// The goal of serializing values could either be submitting state to a backend
|
|
||||||
// ot storing state in a backend. For this, only values that are entered by the end
|
|
||||||
// user are relevant, choice values are always defined by the Application Developer
|
|
||||||
// and known by the backend.
|
|
||||||
|
|
||||||
// Assuming values are always defined as strings, modelValues and serializedValues
|
|
||||||
// are the same.
|
|
||||||
const elems = this._getCheckedElements();
|
|
||||||
if (this.multipleChoice) {
|
|
||||||
return elems.map(el => el.serializedValue.value);
|
|
||||||
}
|
|
||||||
return elems[0] ? elems[0].serializedValue.value : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
set serializedValue(value) {
|
|
||||||
if (this.__isInitialSerializedValue) {
|
|
||||||
this.__isInitialSerializedValue = false;
|
|
||||||
this.registrationComplete.then(() => {
|
|
||||||
this._setCheckedElements(value, (el, val) => el.serializedValue.value === val);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this._setCheckedElements(value, (el, val) => el.serializedValue.value === val);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get formattedValue() {
|
|
||||||
const elems = this._getCheckedElements();
|
|
||||||
if (this.multipleChoice) {
|
|
||||||
return elems.map(el => el.formattedValue);
|
|
||||||
}
|
|
||||||
return elems[0] ? elems[0].formattedValue : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
set formattedValue(value) {
|
|
||||||
if (this.__isInitialFormattedValue) {
|
|
||||||
this.__isInitialFormattedValue = false;
|
|
||||||
this.registrationComplete.then(() => {
|
|
||||||
this._setCheckedElements(value, (el, val) => el.formattedValue === val);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this._setCheckedElements(value, (el, val) => el.formattedValue === val);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.multipleChoice = false;
|
|
||||||
this._repropagationRole = 'choice-group'; // configures event propagation logic of FormControlMixin
|
|
||||||
|
|
||||||
this.__isInitialModelValue = true;
|
|
||||||
this.__isInitialSerializedValue = true;
|
|
||||||
this.__isInitialFormattedValue = true;
|
|
||||||
this.registrationComplete = new Promise((resolve, reject) => {
|
|
||||||
this.__resolveRegistrationComplete = resolve;
|
|
||||||
this.__rejectRegistrationComplete = reject;
|
|
||||||
});
|
});
|
||||||
this.registrationComplete.done = false;
|
} else {
|
||||||
this.registrationComplete.then(
|
this._setCheckedElements(value, checkCondition);
|
||||||
() => {
|
}
|
||||||
this.registrationComplete.done = true;
|
}
|
||||||
},
|
|
||||||
() => {
|
get serializedValue() {
|
||||||
this.registrationComplete.done = true;
|
// We want to filter out disabled values out by default:
|
||||||
throw new Error(
|
// The goal of serializing values could either be submitting state to a backend
|
||||||
'Registration could not finish. Please use await el.registrationComplete;',
|
// ot storing state in a backend. For this, only values that are entered by the end
|
||||||
);
|
// user are relevant, choice values are always defined by the Application Developer
|
||||||
},
|
// and known by the backend.
|
||||||
|
|
||||||
|
// Assuming values are always defined as strings, modelValues and serializedValues
|
||||||
|
// are the same.
|
||||||
|
const elems = this._getCheckedElements();
|
||||||
|
if (this.multipleChoice) {
|
||||||
|
return elems.map(el => el.serializedValue.value);
|
||||||
|
}
|
||||||
|
return elems[0] ? elems[0].serializedValue.value : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
set serializedValue(value) {
|
||||||
|
/**
|
||||||
|
* @param {{ serializedValue: { value: any; }; }} el
|
||||||
|
* @param {string} val
|
||||||
|
*/
|
||||||
|
const checkCondition = (el, val) => el.serializedValue.value === val;
|
||||||
|
|
||||||
|
if (this.__isInitialSerializedValue) {
|
||||||
|
this.__isInitialSerializedValue = false;
|
||||||
|
this.registrationComplete.then(() => {
|
||||||
|
this._setCheckedElements(value, checkCondition);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this._setCheckedElements(value, checkCondition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get formattedValue() {
|
||||||
|
const elems = this._getCheckedElements();
|
||||||
|
if (this.multipleChoice) {
|
||||||
|
return elems.map(el => el.formattedValue);
|
||||||
|
}
|
||||||
|
return elems[0] ? elems[0].formattedValue : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
set formattedValue(value) {
|
||||||
|
/**
|
||||||
|
* @param {{ formattedValue: string }} el
|
||||||
|
* @param {string} val
|
||||||
|
*/
|
||||||
|
const checkCondition = (el, val) => el.formattedValue === val;
|
||||||
|
|
||||||
|
if (this.__isInitialFormattedValue) {
|
||||||
|
this.__isInitialFormattedValue = false;
|
||||||
|
this.registrationComplete.then(() => {
|
||||||
|
this._setCheckedElements(value, checkCondition);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this._setCheckedElements(value, checkCondition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.multipleChoice = false;
|
||||||
|
this._repropagationRole = 'choice-group'; // configures event propagation logic of FormControlMixin
|
||||||
|
|
||||||
|
this.__isInitialModelValue = true;
|
||||||
|
this.__isInitialSerializedValue = true;
|
||||||
|
this.__isInitialFormattedValue = true;
|
||||||
|
/** @type {Promise<any> & {done?:boolean}} */
|
||||||
|
this.registrationComplete = new Promise((resolve, reject) => {
|
||||||
|
this.__resolveRegistrationComplete = resolve;
|
||||||
|
this.__rejectRegistrationComplete = reject;
|
||||||
|
});
|
||||||
|
this.registrationComplete.done = false;
|
||||||
|
this.registrationComplete.then(
|
||||||
|
() => {
|
||||||
|
this.registrationComplete.done = true;
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
this.registrationComplete.done = true;
|
||||||
|
throw new Error(
|
||||||
|
'Registration could not finish. Please use await el.registrationComplete;',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
Promise.resolve().then(() => this.__resolveRegistrationComplete());
|
||||||
|
|
||||||
|
this.registrationComplete.then(() => {
|
||||||
|
this.__isInitialModelValue = false;
|
||||||
|
this.__isInitialSerializedValue = false;
|
||||||
|
this.__isInitialFormattedValue = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
|
||||||
|
if (this.registrationComplete.done === false) {
|
||||||
|
this.__rejectRegistrationComplete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @override from FormRegistrarMixin
|
||||||
|
* @param {FormControl} child
|
||||||
|
* @param {number} indexToInsertAt
|
||||||
|
*/
|
||||||
|
addFormElement(child, indexToInsertAt) {
|
||||||
|
this._throwWhenInvalidChildModelValue(child);
|
||||||
|
this.__delegateNameAttribute(child);
|
||||||
|
super.addFormElement(child, indexToInsertAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @override from FormControlMixin
|
||||||
|
*/
|
||||||
|
_triggerInitialModelValueChangedEvent() {
|
||||||
|
this.registrationComplete.then(() => {
|
||||||
|
this.__dispatchInitialModelValueChangedEvent();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @override
|
||||||
|
* @param {string} property
|
||||||
|
*/
|
||||||
|
_getFromAllFormElements(property, filterCondition = () => true) {
|
||||||
|
// For modelValue, serializedValue and formattedValue, an exception should be made,
|
||||||
|
// The reset can be requested from children
|
||||||
|
if (
|
||||||
|
property === 'modelValue' ||
|
||||||
|
property === 'serializedValue' ||
|
||||||
|
property === 'formattedValue'
|
||||||
|
) {
|
||||||
|
return this[property];
|
||||||
|
}
|
||||||
|
return this.formElements.filter(filterCondition).map(el => el.property);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {FormControl} child
|
||||||
|
*/
|
||||||
|
_throwWhenInvalidChildModelValue(child) {
|
||||||
|
if (
|
||||||
|
// @ts-expect-error
|
||||||
|
typeof child.modelValue.checked !== 'boolean' ||
|
||||||
|
!Object.prototype.hasOwnProperty.call(child.modelValue, 'value')
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
`The ${this.tagName.toLowerCase()} name="${
|
||||||
|
this.name
|
||||||
|
}" does not allow to register ${child.tagName.toLowerCase()} with .modelValue="${
|
||||||
|
child.modelValue
|
||||||
|
}" - The modelValue should represent an Object { value: "foo", checked: false }`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
_isEmpty() {
|
||||||
super.connectedCallback();
|
if (this.multipleChoice) {
|
||||||
Promise.resolve().then(() => this.__resolveRegistrationComplete());
|
return this.modelValue.length === 0;
|
||||||
|
}
|
||||||
|
|
||||||
this.registrationComplete.then(() => {
|
if (typeof this.modelValue === 'string' && this.modelValue === '') {
|
||||||
this.__isInitialModelValue = false;
|
return true;
|
||||||
this.__isInitialSerializedValue = false;
|
}
|
||||||
this.__isInitialFormattedValue = false;
|
if (this.modelValue === undefined || this.modelValue === null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {CustomEvent & {target:FormControl}} ev
|
||||||
|
*/
|
||||||
|
_checkSingleChoiceElements(ev) {
|
||||||
|
const { target } = ev;
|
||||||
|
if (target.checked === false) return;
|
||||||
|
|
||||||
|
const groupName = target.name;
|
||||||
|
this.formElements
|
||||||
|
.filter(i => i.name === groupName)
|
||||||
|
.forEach(choice => {
|
||||||
|
if (choice !== target) {
|
||||||
|
choice.checked = false; // eslint-disable-line no-param-reassign
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
// this.__triggerCheckedValueChanged();
|
||||||
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
_getCheckedElements() {
|
||||||
if (super.disconnectedCallback) {
|
// We want to filter out disabled values out by default
|
||||||
super.disconnectedCallback();
|
return this.formElements.filter(el => el.checked && !el.disabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.registrationComplete.done === false) {
|
/**
|
||||||
this.__rejectRegistrationComplete();
|
* @param {string | any[]} value
|
||||||
}
|
* @param {Function} check
|
||||||
}
|
*/
|
||||||
|
_setCheckedElements(value, check) {
|
||||||
/**
|
for (let i = 0; i < this.formElements.length; i += 1) {
|
||||||
* @override from FormRegistrarMixin
|
|
||||||
*/
|
|
||||||
addFormElement(child, indexToInsertAt) {
|
|
||||||
this._throwWhenInvalidChildModelValue(child);
|
|
||||||
this.__delegateNameAttribute(child);
|
|
||||||
super.addFormElement(child, indexToInsertAt);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @override from FormControlMixin
|
|
||||||
*/
|
|
||||||
_triggerInitialModelValueChangedEvent() {
|
|
||||||
this.registrationComplete.then(() => {
|
|
||||||
this.__dispatchInitialModelValueChangedEvent();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @override
|
|
||||||
*/
|
|
||||||
_getFromAllFormElements(property, filterCondition = () => true) {
|
|
||||||
// For modelValue, serializedValue and formattedValue, an exception should be made,
|
|
||||||
// The reset can be requested from children
|
|
||||||
if (
|
|
||||||
property === 'modelValue' ||
|
|
||||||
property === 'serializedValue' ||
|
|
||||||
property === 'formattedValue'
|
|
||||||
) {
|
|
||||||
return this[property];
|
|
||||||
}
|
|
||||||
return this.formElements.filter(filterCondition).map(el => el.property);
|
|
||||||
}
|
|
||||||
|
|
||||||
_throwWhenInvalidChildModelValue(child) {
|
|
||||||
if (
|
|
||||||
typeof child.modelValue.checked !== 'boolean' ||
|
|
||||||
!Object.prototype.hasOwnProperty.call(child.modelValue, 'value')
|
|
||||||
) {
|
|
||||||
throw new Error(
|
|
||||||
`The ${this.tagName.toLowerCase()} name="${
|
|
||||||
this.name
|
|
||||||
}" does not allow to register ${child.tagName.toLowerCase()} with .modelValue="${
|
|
||||||
child.modelValue
|
|
||||||
}" - The modelValue should represent an Object { value: "foo", checked: false }`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_isEmpty() {
|
|
||||||
if (this.multipleChoice) {
|
if (this.multipleChoice) {
|
||||||
return this.modelValue.length === 0;
|
this.formElements[i].checked = value.includes(this.formElements[i].value);
|
||||||
}
|
} else if (check(this.formElements[i], value)) {
|
||||||
|
// Allows checking against custom values e.g. formattedValue or serializedValue
|
||||||
if (typeof this.modelValue === 'string' && this.modelValue === '') {
|
this.formElements[i].checked = true;
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (this.modelValue === undefined || this.modelValue === null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_checkSingleChoiceElements(ev) {
|
|
||||||
const { target } = ev;
|
|
||||||
if (target.checked === false) return;
|
|
||||||
|
|
||||||
const groupName = target.name;
|
|
||||||
this.formElements
|
|
||||||
.filter(i => i.name === groupName)
|
|
||||||
.forEach(choice => {
|
|
||||||
if (choice !== target) {
|
|
||||||
choice.checked = false; // eslint-disable-line no-param-reassign
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.__triggerCheckedValueChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
_getCheckedElements() {
|
|
||||||
// We want to filter out disabled values out by default
|
|
||||||
return this.formElements.filter(el => el.checked && !el.disabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
_setCheckedElements(value, check) {
|
|
||||||
for (let i = 0; i < this.formElements.length; i += 1) {
|
|
||||||
if (this.multipleChoice) {
|
|
||||||
this.formElements[i].checked = value.includes(this.formElements[i].value);
|
|
||||||
} else if (check(this.formElements[i], value)) {
|
|
||||||
// Allows checking against custom values e.g. formattedValue or serializedValue
|
|
||||||
this.formElements[i].checked = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
__setChoiceGroupTouched() {
|
__setChoiceGroupTouched() {
|
||||||
const value = this.modelValue;
|
const value = this.modelValue;
|
||||||
if (value != null && value !== this.__previousCheckedValue) {
|
if (value != null && value !== this.__previousCheckedValue) {
|
||||||
// TODO: what happens here exactly? Needs to be based on user interaction (?)
|
// TODO: what happens here exactly? Needs to be based on user interaction (?)
|
||||||
this.touched = true;
|
this.touched = true;
|
||||||
this.__previousCheckedValue = value;
|
this.__previousCheckedValue = value;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
__delegateNameAttribute(child) {
|
/**
|
||||||
if (!child.name || child.name === this.name) {
|
* @param {FormControl} child
|
||||||
// eslint-disable-next-line no-param-reassign
|
*/
|
||||||
child.name = this.name;
|
__delegateNameAttribute(child) {
|
||||||
} else {
|
if (!child.name || child.name === this.name) {
|
||||||
throw new Error(
|
// eslint-disable-next-line no-param-reassign
|
||||||
`The ${this.tagName.toLowerCase()} name="${
|
child.name = this.name;
|
||||||
this.name
|
} else {
|
||||||
}" does not allow to register ${child.tagName.toLowerCase()} with custom names (name="${
|
throw new Error(
|
||||||
child.name
|
`The ${this.tagName.toLowerCase()} name="${
|
||||||
}" given)`,
|
this.name
|
||||||
);
|
}" does not allow to register ${child.tagName.toLowerCase()} with custom names (name="${
|
||||||
}
|
child.name
|
||||||
|
}" given)`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @override FormControlMixin
|
* @override FormControlMixin
|
||||||
*/
|
* @param {CustomEvent} ev
|
||||||
_onBeforeRepropagateChildrenValues(ev) {
|
*/
|
||||||
// Normalize target, since we might receive 'portal events' (from children in a modal,
|
_onBeforeRepropagateChildrenValues(ev) {
|
||||||
// see select-rich)
|
// Normalize target, since we might receive 'portal events' (from children in a modal,
|
||||||
const target = (ev.detail && ev.detail.element) || ev.target;
|
// see select-rich)
|
||||||
if (this.multipleChoice || !target.checked) {
|
const target = (ev.detail && ev.detail.element) || ev.target;
|
||||||
return;
|
if (this.multipleChoice || !target.checked) {
|
||||||
}
|
return;
|
||||||
this.formElements.forEach(option => {
|
|
||||||
if (target.choiceValue !== option.choiceValue) {
|
|
||||||
option.checked = false; // eslint-disable-line no-param-reassign
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.__setChoiceGroupTouched();
|
|
||||||
this.requestUpdate('modelValue');
|
|
||||||
}
|
}
|
||||||
},
|
this.formElements.forEach(option => {
|
||||||
);
|
if (target.choiceValue !== option.choiceValue) {
|
||||||
|
option.checked = false; // eslint-disable-line no-param-reassign
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.__setChoiceGroupTouched();
|
||||||
|
this.requestUpdate('modelValue');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ChoiceGroupMixin = dedupeMixin(ChoiceGroupMixinImplementation);
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,29 @@
|
||||||
/* eslint-disable class-methods-use-this */
|
/* eslint-disable class-methods-use-this */
|
||||||
|
|
||||||
import { css, html, nothing } from '@lion/core';
|
import { css, html, nothing, dedupeMixin } from '@lion/core';
|
||||||
import { FormatMixin } from '../FormatMixin.js';
|
import { FormatMixin } from '../FormatMixin.js';
|
||||||
|
|
||||||
export const ChoiceInputMixin = superclass =>
|
/**
|
||||||
// eslint-disable-next-line
|
* @typedef {import('../../types/FormControlMixinTypes').FormControlHost} FormControlHost
|
||||||
|
* @typedef {FormControlHost & HTMLElement & {__parentFormGroup?:HTMLElement, checked?:boolean}} FormControl
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('../../types/choice-group/ChoiceInputMixinTypes').ChoiceInputMixin} ChoiceInputMixin
|
||||||
|
* @typedef {import('../../types/choice-group/ChoiceInputMixinTypes').ChoiceInputModelValue} ChoiceInputModelValue
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {ChoiceInputModelValue} nw\
|
||||||
|
* @param {{value?:any, checked?:boolean}} old
|
||||||
|
*/
|
||||||
|
const hasChanged = (nw, old = {}) => nw.value !== old.value || nw.checked !== old.checked;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {ChoiceInputMixin}
|
||||||
|
* @param {import('@open-wc/dedupe-mixin').Constructor<import('@lion/core').LitElement>} superclass
|
||||||
|
*/
|
||||||
|
const ChoiceInputMixinImplementation = superclass =>
|
||||||
class ChoiceInputMixin extends FormatMixin(superclass) {
|
class ChoiceInputMixin extends FormatMixin(superclass) {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
|
|
@ -25,7 +44,7 @@ export const ChoiceInputMixin = superclass =>
|
||||||
*/
|
*/
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: Object,
|
type: Object,
|
||||||
hasChanged: (nw, old = {}) => nw.value !== old.value || nw.checked !== old.checked,
|
hasChanged,
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* The value property of the modelValue. It provides an easy interface for storing
|
* The value property of the modelValue. It provides an easy interface for storing
|
||||||
|
|
@ -44,10 +63,15 @@ export const ChoiceInputMixin = superclass =>
|
||||||
set choiceValue(value) {
|
set choiceValue(value) {
|
||||||
this.requestUpdate('choiceValue', this.choiceValue);
|
this.requestUpdate('choiceValue', this.choiceValue);
|
||||||
if (this.modelValue.value !== value) {
|
if (this.modelValue.value !== value) {
|
||||||
|
/** @type {ChoiceInputModelValue} */
|
||||||
this.modelValue = { value, checked: this.modelValue.checked };
|
this.modelValue = { value, checked: this.modelValue.checked };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} name
|
||||||
|
* @param {any} oldValue
|
||||||
|
*/
|
||||||
requestUpdateInternal(name, oldValue) {
|
requestUpdateInternal(name, oldValue) {
|
||||||
super.requestUpdateInternal(name, oldValue);
|
super.requestUpdateInternal(name, oldValue);
|
||||||
|
|
||||||
|
|
@ -62,6 +86,9 @@ export const ChoiceInputMixin = superclass =>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('lit-element').PropertyValues } changedProperties
|
||||||
|
*/
|
||||||
firstUpdated(changedProperties) {
|
firstUpdated(changedProperties) {
|
||||||
super.firstUpdated(changedProperties);
|
super.firstUpdated(changedProperties);
|
||||||
if (changedProperties.has('checked')) {
|
if (changedProperties.has('checked')) {
|
||||||
|
|
@ -71,6 +98,9 @@ export const ChoiceInputMixin = superclass =>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('lit-element').PropertyValues } changedProperties
|
||||||
|
*/
|
||||||
updated(changedProperties) {
|
updated(changedProperties) {
|
||||||
super.updated(changedProperties);
|
super.updated(changedProperties);
|
||||||
if (changedProperties.has('modelValue')) {
|
if (changedProperties.has('modelValue')) {
|
||||||
|
|
@ -118,9 +148,7 @@ export const ChoiceInputMixin = superclass =>
|
||||||
render() {
|
render() {
|
||||||
return html`
|
return html`
|
||||||
<slot name="input"></slot>
|
<slot name="input"></slot>
|
||||||
<div class="choice-field__graphic-container">
|
<div class="choice-field__graphic-container">${this._choiceGraphicTemplate()}</div>
|
||||||
${this._choiceGraphicTemplate()}
|
|
||||||
</div>
|
|
||||||
<div class="choice-field__label">
|
<div class="choice-field__label">
|
||||||
<slot name="label"></slot>
|
<slot name="label"></slot>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -148,10 +176,16 @@ export const ChoiceInputMixin = superclass =>
|
||||||
this.checked = !this.checked;
|
this.checked = !this.checked;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {boolean} checked
|
||||||
|
*/
|
||||||
__syncModelCheckedToChecked(checked) {
|
__syncModelCheckedToChecked(checked) {
|
||||||
this.checked = checked;
|
this.checked = checked;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {any} checked
|
||||||
|
*/
|
||||||
__syncCheckedToModel(checked) {
|
__syncCheckedToModel(checked) {
|
||||||
this.modelValue = { value: this.choiceValue, checked };
|
this.modelValue = { value: this.choiceValue, checked };
|
||||||
}
|
}
|
||||||
|
|
@ -160,7 +194,8 @@ export const ChoiceInputMixin = superclass =>
|
||||||
// ._inputNode might not be available yet(slot content)
|
// ._inputNode might not be available yet(slot content)
|
||||||
// or at all (no reliance on platform construct, in case of [role=option])
|
// or at all (no reliance on platform construct, in case of [role=option])
|
||||||
if (this._inputNode) {
|
if (this._inputNode) {
|
||||||
this._inputNode.checked = this.checked;
|
/** @type {HTMLInputElement} */
|
||||||
|
(this._inputNode).checked = this.checked;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -178,8 +213,12 @@ export const ChoiceInputMixin = superclass =>
|
||||||
* @override
|
* @override
|
||||||
* hasChanged is designed for async (updated) callback, also check for sync
|
* hasChanged is designed for async (updated) callback, also check for sync
|
||||||
* (requestUpdateInternal) callback
|
* (requestUpdateInternal) callback
|
||||||
|
* @param {{ modelValue:unknown }} newV
|
||||||
|
* @param {{ modelValue:unknown }} [oldV]
|
||||||
*/
|
*/
|
||||||
|
// @ts-expect-error
|
||||||
_onModelValueChanged({ modelValue }, { modelValue: old }) {
|
_onModelValueChanged({ modelValue }, { modelValue: old }) {
|
||||||
|
// @ts-expect-error
|
||||||
if (this.constructor._classProperties.get('modelValue').hasChanged(modelValue, old)) {
|
if (this.constructor._classProperties.get('modelValue').hasChanged(modelValue, old)) {
|
||||||
super._onModelValueChanged({ modelValue });
|
super._onModelValueChanged({ modelValue });
|
||||||
}
|
}
|
||||||
|
|
@ -195,8 +234,8 @@ export const ChoiceInputMixin = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @override
|
* @override Overridden from FormatMixin, since a different modelValue is used for choice inputs.
|
||||||
* Overridden from FormatMixin, since a different modelValue is used for choice inputs.
|
* @param {ChoiceInputModelValue } modelValue
|
||||||
*/
|
*/
|
||||||
formatter(modelValue) {
|
formatter(modelValue) {
|
||||||
return modelValue && modelValue.value !== undefined ? modelValue.value : modelValue;
|
return modelValue && modelValue.value !== undefined ? modelValue.value : modelValue;
|
||||||
|
|
@ -216,3 +255,5 @@ export const ChoiceInputMixin = superclass =>
|
||||||
*/
|
*/
|
||||||
_syncValueUpwards() {}
|
_syncValueUpwards() {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ChoiceInputMixin = dedupeMixin(ChoiceInputMixinImplementation);
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,11 @@ export class FormElementsHaveNoError extends Validator {
|
||||||
return 'FormElementsHaveNoError';
|
return 'FormElementsHaveNoError';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {unknown} [value]
|
||||||
|
* @param {string | undefined} [options]
|
||||||
|
* @param {{ node: any }} config
|
||||||
|
*/
|
||||||
// eslint-disable-next-line class-methods-use-this
|
// eslint-disable-next-line class-methods-use-this
|
||||||
execute(value, options, config) {
|
execute(value, options, config) {
|
||||||
const hasError = config.node._anyFormElementHasFeedbackFor('error');
|
const hasError = config.node._anyFormElementHasFeedbackFor('error');
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,15 @@ import { ValidateMixin } from '../validate/ValidateMixin.js';
|
||||||
import { getAriaElementsInRightDomOrder } from '../utils/getAriaElementsInRightDomOrder.js';
|
import { getAriaElementsInRightDomOrder } from '../utils/getAriaElementsInRightDomOrder.js';
|
||||||
import { FormElementsHaveNoError } from './FormElementsHaveNoError.js';
|
import { FormElementsHaveNoError } from './FormElementsHaveNoError.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('../../types/form-group/FormGroupMixinTypes').FormGroupMixin} FormGroupMixin
|
||||||
|
* @typedef {import('../../types/form-group/FormGroupMixinTypes').FormGroupHost} FormGroupHost
|
||||||
|
* @typedef {import('../../types/FormControlMixinTypes').FormControlHost} FormControlHost
|
||||||
|
* @typedef {import('../../types/registration/FormRegisteringMixinTypes').FormRegisteringHost} FormRegisteringHost
|
||||||
|
* @typedef {import('../../types/registration/FormRegistrarMixinTypes').ElementWithParentFormGroup} ElementWithParentFormGroup
|
||||||
|
* @typedef {FormControlHost & HTMLElement & {__parentFormGroup?: HTMLElement, checked?: boolean, disabled: boolean, hasFeedbackFor: string[], makeRequestToBeDisabled: Function }} FormControl
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @desc Form group mixin serves as the basis for (sub) forms. Designed to be put on
|
* @desc Form group mixin serves as the basis for (sub) forms. Designed to be put on
|
||||||
* elements with [role="group|radiogroup"] (think of checkbox-group, radio-group, fieldset).
|
* elements with [role="group|radiogroup"] (think of checkbox-group, radio-group, fieldset).
|
||||||
|
|
@ -15,448 +24,485 @@ import { FormElementsHaveNoError } from './FormElementsHaveNoError.js';
|
||||||
* It is designed to be used on top of FormRegistrarMixin and ChoiceGroupMixin.
|
* It is designed to be used on top of FormRegistrarMixin and ChoiceGroupMixin.
|
||||||
* Also, it is th basis of the LionFieldset element (which supports name based retrieval of
|
* Also, it is th basis of the LionFieldset element (which supports name based retrieval of
|
||||||
* children via formElements and the automatic grouping of formElements via '[]').
|
* children via formElements and the automatic grouping of formElements via '[]').
|
||||||
|
*
|
||||||
|
* @type {FormGroupMixin}
|
||||||
|
* @param {import('@open-wc/dedupe-mixin').Constructor<import('@lion/core').LitElement>} superclass
|
||||||
*/
|
*/
|
||||||
export const FormGroupMixin = dedupeMixin(
|
const FormGroupMixinImplementation = superclass =>
|
||||||
superclass =>
|
class FormGroupMixin extends FormRegistrarMixin(
|
||||||
// eslint-disable-next-line no-shadow
|
FormControlMixin(ValidateMixin(DisabledMixin(SlotMixin(superclass)))),
|
||||||
class FormGroupMixin extends FormRegistrarMixin(
|
) {
|
||||||
FormControlMixin(ValidateMixin(DisabledMixin(SlotMixin(superclass)))),
|
static get properties() {
|
||||||
) {
|
return {
|
||||||
static get properties() {
|
/**
|
||||||
return {
|
* Interaction state that can be used to compute the visibility of
|
||||||
/**
|
* feedback messages
|
||||||
* Interaction state that can be used to compute the visibility of
|
*/
|
||||||
* feedback messages
|
submitted: {
|
||||||
*/
|
type: Boolean,
|
||||||
submitted: {
|
reflect: true,
|
||||||
type: Boolean,
|
},
|
||||||
reflect: true,
|
/**
|
||||||
},
|
* Interaction state that will be active when any of the children
|
||||||
/**
|
* is focused.
|
||||||
* Interaction state that will be active when any of the children
|
*/
|
||||||
* is focused.
|
focused: {
|
||||||
*/
|
type: Boolean,
|
||||||
focused: {
|
reflect: true,
|
||||||
type: Boolean,
|
},
|
||||||
reflect: true,
|
/**
|
||||||
},
|
* Interaction state that will be active when any of the children
|
||||||
/**
|
* is dirty (see InteractionStateMixin for more details.)
|
||||||
* Interaction state that will be active when any of the children
|
*/
|
||||||
* is dirty (see InteractionStateMixin for more details.)
|
dirty: {
|
||||||
*/
|
type: Boolean,
|
||||||
dirty: {
|
reflect: true,
|
||||||
type: Boolean,
|
},
|
||||||
reflect: true,
|
/**
|
||||||
},
|
* Interaction state that will be active when the group as a whole is
|
||||||
/**
|
* blurred
|
||||||
* Interaction state that will be active when the group as a whole is
|
*/
|
||||||
* blurred
|
touched: {
|
||||||
*/
|
type: Boolean,
|
||||||
touched: {
|
reflect: true,
|
||||||
type: Boolean,
|
},
|
||||||
reflect: true,
|
/**
|
||||||
},
|
* Interaction state that will be active when all of the children
|
||||||
/**
|
* are prefilled (see InteractionStateMixin for more details.)
|
||||||
* Interaction state that will be active when all of the children
|
*/
|
||||||
* are prefilled (see InteractionStateMixin for more details.)
|
prefilled: {
|
||||||
*/
|
type: Boolean,
|
||||||
prefilled: {
|
reflect: true,
|
||||||
type: Boolean,
|
},
|
||||||
reflect: true,
|
};
|
||||||
},
|
}
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
get _inputNode() {
|
get _inputNode() {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
get modelValue() {
|
get modelValue() {
|
||||||
return this._getFromAllFormElements('modelValue');
|
return this._getFromAllFormElements('modelValue');
|
||||||
}
|
}
|
||||||
|
|
||||||
set modelValue(values) {
|
set modelValue(values) {
|
||||||
if (this.__isInitialModelValue) {
|
if (this.__isInitialModelValue) {
|
||||||
this.__isInitialModelValue = false;
|
this.__isInitialModelValue = false;
|
||||||
this.registrationComplete.then(() => {
|
this.registrationComplete.then(() => {
|
||||||
this._setValueMapForAllFormElements('modelValue', values);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this._setValueMapForAllFormElements('modelValue', values);
|
this._setValueMapForAllFormElements('modelValue', values);
|
||||||
}
|
});
|
||||||
|
} else {
|
||||||
|
this._setValueMapForAllFormElements('modelValue', values);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
get serializedValue() {
|
get serializedValue() {
|
||||||
return this._getFromAllFormElements('serializedValue');
|
return this._getFromAllFormElements('serializedValue');
|
||||||
}
|
}
|
||||||
|
|
||||||
set serializedValue(values) {
|
set serializedValue(values) {
|
||||||
if (this.__isInitialSerializedValue) {
|
if (this.__isInitialSerializedValue) {
|
||||||
this.__isInitialSerializedValue = false;
|
this.__isInitialSerializedValue = false;
|
||||||
this.registrationComplete.then(() => {
|
this.registrationComplete.then(() => {
|
||||||
this._setValueMapForAllFormElements('serializedValue', values);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this._setValueMapForAllFormElements('serializedValue', values);
|
this._setValueMapForAllFormElements('serializedValue', values);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get formattedValue() {
|
|
||||||
return this._getFromAllFormElements('formattedValue');
|
|
||||||
}
|
|
||||||
|
|
||||||
set formattedValue(values) {
|
|
||||||
this._setValueMapForAllFormElements('formattedValue', values);
|
|
||||||
}
|
|
||||||
|
|
||||||
get prefilled() {
|
|
||||||
return this._everyFormElementHas('prefilled');
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.disabled = false;
|
|
||||||
this.submitted = false;
|
|
||||||
this.dirty = false;
|
|
||||||
this.touched = false;
|
|
||||||
this.focused = false;
|
|
||||||
this.__addedSubValidators = false;
|
|
||||||
this.__isInitialModelValue = true;
|
|
||||||
this.__isInitialSerializedValue = true;
|
|
||||||
|
|
||||||
this._checkForOutsideClick = this._checkForOutsideClick.bind(this);
|
|
||||||
|
|
||||||
this.addEventListener('focusin', this._syncFocused);
|
|
||||||
this.addEventListener('focusout', this._onFocusOut);
|
|
||||||
this.addEventListener('dirty-changed', this._syncDirty);
|
|
||||||
this.addEventListener('validate-performed', this.__onChildValidatePerformed);
|
|
||||||
|
|
||||||
this.defaultValidators = [new FormElementsHaveNoError()];
|
|
||||||
|
|
||||||
this.registrationComplete = new Promise((resolve, reject) => {
|
|
||||||
this.__resolveRegistrationComplete = resolve;
|
|
||||||
this.__rejectRegistrationComplete = reject;
|
|
||||||
});
|
});
|
||||||
this.registrationComplete.done = false;
|
} else {
|
||||||
this.registrationComplete.then(
|
this._setValueMapForAllFormElements('serializedValue', values);
|
||||||
() => {
|
|
||||||
this.registrationComplete.done = true;
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
this.registrationComplete.done = true;
|
|
||||||
throw new Error(
|
|
||||||
'Registration could not finish. Please use await el.registrationComplete;',
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
get formattedValue() {
|
||||||
super.connectedCallback();
|
return this._getFromAllFormElements('formattedValue');
|
||||||
this.setAttribute('role', 'group');
|
}
|
||||||
Promise.resolve().then(() => this.__resolveRegistrationComplete());
|
|
||||||
|
|
||||||
this.registrationComplete.then(() => {
|
set formattedValue(values) {
|
||||||
this.__isInitialModelValue = false;
|
this._setValueMapForAllFormElements('formattedValue', values);
|
||||||
this.__isInitialSerializedValue = false;
|
}
|
||||||
this.__initInteractionStates();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
disconnectedCallback() {
|
get prefilled() {
|
||||||
if (super.disconnectedCallback) {
|
return this._everyFormElementHas('prefilled');
|
||||||
super.disconnectedCallback();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (this.__hasActiveOutsideClickHandling) {
|
constructor() {
|
||||||
document.removeEventListener('click', this._checkForOutsideClick);
|
super();
|
||||||
this.__hasActiveOutsideClickHandling = false;
|
this.disabled = false;
|
||||||
}
|
this.submitted = false;
|
||||||
if (this.registrationComplete.done === false) {
|
this.dirty = false;
|
||||||
this.__rejectRegistrationComplete();
|
this.touched = false;
|
||||||
}
|
this.focused = false;
|
||||||
}
|
this.__addedSubValidators = false;
|
||||||
|
this.__isInitialModelValue = true;
|
||||||
|
this.__isInitialSerializedValue = true;
|
||||||
|
|
||||||
__initInteractionStates() {
|
this._checkForOutsideClick = this._checkForOutsideClick.bind(this);
|
||||||
this.formElements.forEach(el => {
|
|
||||||
if (typeof el.initInteractionState === 'function') {
|
|
||||||
el.initInteractionState();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
this.addEventListener('focusin', this._syncFocused);
|
||||||
* @override from FormControlMixin
|
this.addEventListener('focusout', this._onFocusOut);
|
||||||
*/
|
this.addEventListener('dirty-changed', this._syncDirty);
|
||||||
_triggerInitialModelValueChangedEvent() {
|
this.addEventListener('validate-performed', this.__onChildValidatePerformed);
|
||||||
this.registrationComplete.then(() => {
|
|
||||||
this.__dispatchInitialModelValueChangedEvent();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
updated(changedProperties) {
|
this.defaultValidators = [new FormElementsHaveNoError()];
|
||||||
super.updated(changedProperties);
|
/** @type {Promise<any> & {done?:boolean}} */
|
||||||
|
this.registrationComplete = new Promise((resolve, reject) => {
|
||||||
if (changedProperties.has('disabled')) {
|
this.__resolveRegistrationComplete = resolve;
|
||||||
if (this.disabled) {
|
this.__rejectRegistrationComplete = reject;
|
||||||
this.__requestChildrenToBeDisabled();
|
});
|
||||||
} else {
|
this.registrationComplete.done = false;
|
||||||
this.__retractRequestChildrenToBeDisabled();
|
this.registrationComplete.then(
|
||||||
}
|
() => {
|
||||||
}
|
this.registrationComplete.done = true;
|
||||||
|
},
|
||||||
if (changedProperties.has('focused')) {
|
() => {
|
||||||
if (this.focused === true) {
|
this.registrationComplete.done = true;
|
||||||
this.__setupOutsideClickHandling();
|
throw new Error(
|
||||||
}
|
'Registration could not finish. Please use await el.registrationComplete;',
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
__setupOutsideClickHandling() {
|
|
||||||
if (!this.__hasActiveOutsideClickHandling) {
|
|
||||||
document.addEventListener('click', this._checkForOutsideClick);
|
|
||||||
this.__hasActiveOutsideClickHandling = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_checkForOutsideClick(event) {
|
|
||||||
const outsideGroupClicked = !this.contains(event.target);
|
|
||||||
if (outsideGroupClicked) {
|
|
||||||
this.touched = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
__requestChildrenToBeDisabled() {
|
|
||||||
this.formElements.forEach(child => {
|
|
||||||
if (child.makeRequestToBeDisabled) {
|
|
||||||
child.makeRequestToBeDisabled();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
__retractRequestChildrenToBeDisabled() {
|
|
||||||
this.formElements.forEach(child => {
|
|
||||||
if (child.retractRequestToBeDisabled) {
|
|
||||||
child.retractRequestToBeDisabled();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line class-methods-use-this
|
|
||||||
_inputGroupTemplate() {
|
|
||||||
return html`
|
|
||||||
<div class="input-group">
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @desc Handles interaction state 'submitted'.
|
|
||||||
* This allows children to enable visibility of validation feedback
|
|
||||||
*/
|
|
||||||
submitGroup() {
|
|
||||||
this.submitted = true;
|
|
||||||
this.formElements.forEach(child => {
|
|
||||||
if (typeof child.submitGroup === 'function') {
|
|
||||||
child.submitGroup();
|
|
||||||
} else {
|
|
||||||
child.submitted = true; // eslint-disable-line no-param-reassign
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
resetGroup() {
|
|
||||||
this.formElements.forEach(child => {
|
|
||||||
if (typeof child.resetGroup === 'function') {
|
|
||||||
child.resetGroup();
|
|
||||||
} else if (typeof child.reset === 'function') {
|
|
||||||
child.reset();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.resetInteractionState();
|
|
||||||
}
|
|
||||||
|
|
||||||
clearGroup() {
|
|
||||||
this.formElements.forEach(child => {
|
|
||||||
if (typeof child.clearGroup === 'function') {
|
|
||||||
child.clearGroup();
|
|
||||||
} else if (typeof child.clear === 'function') {
|
|
||||||
child.clear();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.resetInteractionState();
|
|
||||||
}
|
|
||||||
|
|
||||||
resetInteractionState() {
|
|
||||||
this.submitted = false;
|
|
||||||
this.touched = false;
|
|
||||||
this.dirty = false;
|
|
||||||
this.formElements.forEach(formElement => {
|
|
||||||
if (typeof formElement.resetInteractionState === 'function') {
|
|
||||||
formElement.resetInteractionState();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_getFromAllFormElements(property, filterCondition = el => !el.disabled) {
|
|
||||||
const result = {};
|
|
||||||
this.formElements._keys().forEach(name => {
|
|
||||||
const elem = this.formElements[name];
|
|
||||||
if (elem instanceof FormControlsCollection) {
|
|
||||||
result[name] = elem.filter(el => filterCondition(el)).map(el => el[property]);
|
|
||||||
} else if (filterCondition(elem)) {
|
|
||||||
if (typeof elem._getFromAllFormElements === 'function') {
|
|
||||||
result[name] = elem._getFromAllFormElements(property, filterCondition);
|
|
||||||
} else {
|
|
||||||
result[name] = elem[property];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
_setValueForAllFormElements(property, value) {
|
|
||||||
this.formElements.forEach(el => {
|
|
||||||
el[property] = value; // eslint-disable-line no-param-reassign
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_setValueMapForAllFormElements(property, values) {
|
|
||||||
if (values && typeof values === 'object') {
|
|
||||||
Object.keys(values).forEach(name => {
|
|
||||||
if (Array.isArray(this.formElements[name])) {
|
|
||||||
this.formElements[name].forEach((el, index) => {
|
|
||||||
el[property] = values[name][index]; // eslint-disable-line no-param-reassign
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (this.formElements[name]) {
|
|
||||||
this.formElements[name][property] = values[name];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_anyFormElementHas(property) {
|
|
||||||
return Object.keys(this.formElements).some(name => {
|
|
||||||
if (Array.isArray(this.formElements[name])) {
|
|
||||||
return this.formElements[name].some(el => !!el[property]);
|
|
||||||
}
|
|
||||||
return !!this.formElements[name][property];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_anyFormElementHasFeedbackFor(state) {
|
|
||||||
return Object.keys(this.formElements).some(name => {
|
|
||||||
if (Array.isArray(this.formElements[name])) {
|
|
||||||
return this.formElements[name].some(el => {
|
|
||||||
return Boolean(el.hasFeedbackFor && el.hasFeedbackFor.includes(state));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return Boolean(
|
|
||||||
this.formElements[name].hasFeedbackFor &&
|
|
||||||
this.formElements[name].hasFeedbackFor.includes(state),
|
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
}
|
);
|
||||||
|
}
|
||||||
|
|
||||||
_everyFormElementHas(property) {
|
connectedCallback() {
|
||||||
return Object.keys(this.formElements).every(name => {
|
super.connectedCallback();
|
||||||
if (Array.isArray(this.formElements[name])) {
|
this.setAttribute('role', 'group');
|
||||||
return this.formElements[name].every(el => !!el[property]);
|
Promise.resolve().then(() => this.__resolveRegistrationComplete());
|
||||||
}
|
|
||||||
return !!this.formElements[name][property];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
this.registrationComplete.then(() => {
|
||||||
* Gets triggered by event 'validate-performed' which enabled us to handle 2 different situations
|
this.__isInitialModelValue = false;
|
||||||
* - react on modelValue change, which says something about the validity as a whole
|
this.__isInitialSerializedValue = false;
|
||||||
* (at least two checkboxes for instance) and nothing about the children's values
|
this.__initInteractionStates();
|
||||||
* - children validity states have changed, so fieldset needs to update itself based on that
|
});
|
||||||
*/
|
}
|
||||||
__onChildValidatePerformed(ev) {
|
|
||||||
if (ev && this.isRegisteredFormElement(ev.target)) {
|
disconnectedCallback() {
|
||||||
this.validate();
|
super.disconnectedCallback();
|
||||||
|
|
||||||
|
if (this.__hasActiveOutsideClickHandling) {
|
||||||
|
document.removeEventListener('click', this._checkForOutsideClick);
|
||||||
|
this.__hasActiveOutsideClickHandling = false;
|
||||||
|
}
|
||||||
|
if (this.registrationComplete.done === false) {
|
||||||
|
this.__rejectRegistrationComplete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
__initInteractionStates() {
|
||||||
|
this.formElements.forEach(el => {
|
||||||
|
if (typeof el.initInteractionState === 'function') {
|
||||||
|
el.initInteractionState();
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
}
|
||||||
|
|
||||||
_syncFocused() {
|
/**
|
||||||
this.focused = this._anyFormElementHas('focused');
|
* @override from FormControlMixin
|
||||||
}
|
*/
|
||||||
|
_triggerInitialModelValueChangedEvent() {
|
||||||
|
this.registrationComplete.then(() => {
|
||||||
|
this.__dispatchInitialModelValueChangedEvent();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
_onFocusOut(ev) {
|
/**
|
||||||
const lastEl = this.formElements[this.formElements.length - 1];
|
* @param {import('lit-element').PropertyValues } changedProperties
|
||||||
if (ev.target === lastEl) {
|
*/
|
||||||
this.touched = true;
|
updated(changedProperties) {
|
||||||
}
|
super.updated(changedProperties);
|
||||||
this.focused = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_syncDirty() {
|
if (changedProperties.has('disabled')) {
|
||||||
this.dirty = this._anyFormElementHas('dirty');
|
|
||||||
}
|
|
||||||
|
|
||||||
__linkChildrenMessagesToParent(child) {
|
|
||||||
// aria-describedby of (nested) children
|
|
||||||
let parent = this;
|
|
||||||
while (parent) {
|
|
||||||
this.constructor._addDescriptionElementIdsToField(
|
|
||||||
child,
|
|
||||||
parent._getAriaDescriptionElements(),
|
|
||||||
);
|
|
||||||
// Also check if the newly added child needs to refer grandparents
|
|
||||||
parent = parent.__parentFormGroup;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @override of FormRegistrarMixin.
|
|
||||||
* @desc Connects ValidateMixin and DisabledMixin
|
|
||||||
* On top of this, error messages of children are linked to their parents
|
|
||||||
*/
|
|
||||||
addFormElement(child, indexToInsertAt) {
|
|
||||||
super.addFormElement(child, indexToInsertAt);
|
|
||||||
if (this.disabled) {
|
if (this.disabled) {
|
||||||
// eslint-disable-next-line no-param-reassign
|
this.__requestChildrenToBeDisabled();
|
||||||
|
} else {
|
||||||
|
this.__retractRequestChildrenToBeDisabled();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changedProperties.has('focused')) {
|
||||||
|
if (this.focused === true) {
|
||||||
|
this.__setupOutsideClickHandling();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
__setupOutsideClickHandling() {
|
||||||
|
if (!this.__hasActiveOutsideClickHandling) {
|
||||||
|
document.addEventListener('click', this._checkForOutsideClick);
|
||||||
|
this.__hasActiveOutsideClickHandling = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Event} event
|
||||||
|
*/
|
||||||
|
_checkForOutsideClick(event) {
|
||||||
|
const outsideGroupClicked = !this.contains(/** @type {Node} */ (event.target));
|
||||||
|
if (outsideGroupClicked) {
|
||||||
|
this.touched = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
__requestChildrenToBeDisabled() {
|
||||||
|
this.formElements.forEach(child => {
|
||||||
|
if (child.makeRequestToBeDisabled) {
|
||||||
child.makeRequestToBeDisabled();
|
child.makeRequestToBeDisabled();
|
||||||
}
|
}
|
||||||
// TODO: Unlink in removeFormElement
|
});
|
||||||
this.__linkChildrenMessagesToParent(child);
|
}
|
||||||
this.validate({ clearCurrentResult: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
__retractRequestChildrenToBeDisabled() {
|
||||||
* Gathers initial model values of all children. Used
|
this.formElements.forEach(child => {
|
||||||
* when resetGroup() is called.
|
if (child.retractRequestToBeDisabled) {
|
||||||
*/
|
child.retractRequestToBeDisabled();
|
||||||
get _initialModelValue() {
|
}
|
||||||
return this._getFromAllFormElements('_initialModelValue');
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// eslint-disable-next-line class-methods-use-this
|
||||||
* Add aria-describedby to child element(field), so that it points to feedback/help-text of
|
_inputGroupTemplate() {
|
||||||
* parent(fieldset)
|
return html`
|
||||||
* @param {LionField} field - the child: lion-field/lion-input/lion-textarea
|
<div class="input-group">
|
||||||
* @param {array} descriptionElements - description elements like feedback and help-text
|
<slot></slot>
|
||||||
*/
|
</div>
|
||||||
static _addDescriptionElementIdsToField(field, descriptionElements) {
|
`;
|
||||||
const orderedEls = getAriaElementsInRightDomOrder(descriptionElements, { reverse: true });
|
}
|
||||||
orderedEls.forEach(el => {
|
|
||||||
if (field.addToAriaDescribedBy) {
|
/**
|
||||||
field.addToAriaDescribedBy(el, { reorder: false });
|
* @desc Handles interaction state 'submitted'.
|
||||||
|
* This allows children to enable visibility of validation feedback
|
||||||
|
*/
|
||||||
|
submitGroup() {
|
||||||
|
this.submitted = true;
|
||||||
|
this.formElements.forEach(child => {
|
||||||
|
if (typeof child.submitGroup === 'function') {
|
||||||
|
child.submitGroup();
|
||||||
|
} else {
|
||||||
|
child.submitted = true; // eslint-disable-line no-param-reassign
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
resetGroup() {
|
||||||
|
this.formElements.forEach(child => {
|
||||||
|
if (typeof child.resetGroup === 'function') {
|
||||||
|
child.resetGroup();
|
||||||
|
} else if (typeof child.reset === 'function') {
|
||||||
|
child.reset();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.resetInteractionState();
|
||||||
|
}
|
||||||
|
|
||||||
|
clearGroup() {
|
||||||
|
this.formElements.forEach(child => {
|
||||||
|
if (typeof child.clearGroup === 'function') {
|
||||||
|
child.clearGroup();
|
||||||
|
} else if (typeof child.clear === 'function') {
|
||||||
|
child.clear();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.resetInteractionState();
|
||||||
|
}
|
||||||
|
|
||||||
|
resetInteractionState() {
|
||||||
|
this.submitted = false;
|
||||||
|
this.touched = false;
|
||||||
|
this.dirty = false;
|
||||||
|
this.formElements.forEach(formElement => {
|
||||||
|
if (typeof formElement.resetInteractionState === 'function') {
|
||||||
|
formElement.resetInteractionState();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} property
|
||||||
|
*/
|
||||||
|
_getFromAllFormElements(property, filterFn = (/** @type {FormControl} */ el) => !el.disabled) {
|
||||||
|
const result = {};
|
||||||
|
this.formElements._keys().forEach(name => {
|
||||||
|
const elem = this.formElements[name];
|
||||||
|
if (elem instanceof FormControlsCollection) {
|
||||||
|
result[name] = elem.filter(el => filterFn(el)).map(el => el[property]);
|
||||||
|
} else if (filterFn(elem)) {
|
||||||
|
if (typeof elem._getFromAllFormElements === 'function') {
|
||||||
|
result[name] = elem._getFromAllFormElements(property, filterFn);
|
||||||
|
} else {
|
||||||
|
result[name] = elem[property];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string | number} property
|
||||||
|
* @param {any} value
|
||||||
|
*/
|
||||||
|
_setValueForAllFormElements(property, value) {
|
||||||
|
this.formElements.forEach(el => {
|
||||||
|
el[property] = value; // eslint-disable-line no-param-reassign
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} property
|
||||||
|
* @param {{ [x: string]: any; }} values
|
||||||
|
*/
|
||||||
|
_setValueMapForAllFormElements(property, values) {
|
||||||
|
if (values && typeof values === 'object') {
|
||||||
|
Object.keys(values).forEach(name => {
|
||||||
|
if (Array.isArray(this.formElements[name])) {
|
||||||
|
this.formElements[name].forEach((
|
||||||
|
/** @type {FormControl} */ el,
|
||||||
|
/** @type {number} */ index,
|
||||||
|
) => {
|
||||||
|
el[property] = values[name][index]; // eslint-disable-line no-param-reassign
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (this.formElements[name]) {
|
||||||
|
this.formElements[name][property] = values[name];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @override of FormRegistrarMixin. Connects ValidateMixin
|
* @param {string} property
|
||||||
*/
|
*/
|
||||||
removeFormElement(...args) {
|
_anyFormElementHas(property) {
|
||||||
super.removeFormElement(...args);
|
return Object.keys(this.formElements).some(name => {
|
||||||
this.validate({ clearCurrentResult: true });
|
if (Array.isArray(this.formElements[name])) {
|
||||||
|
return this.formElements[name].some((/** @type {FormControl} */ el) => !!el[property]);
|
||||||
|
}
|
||||||
|
return !!this.formElements[name][property];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} state one of ValidateHost.validationTypes
|
||||||
|
*/
|
||||||
|
_anyFormElementHasFeedbackFor(state) {
|
||||||
|
return Object.keys(this.formElements).some(name => {
|
||||||
|
if (Array.isArray(this.formElements[name])) {
|
||||||
|
return this.formElements[name].some((/** @type {FormControl} */ el) => {
|
||||||
|
return Boolean(el.hasFeedbackFor && el.hasFeedbackFor.includes(state));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Boolean(
|
||||||
|
this.formElements[name].hasFeedbackFor &&
|
||||||
|
this.formElements[name].hasFeedbackFor.includes(state),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} property
|
||||||
|
*/
|
||||||
|
_everyFormElementHas(property) {
|
||||||
|
return Object.keys(this.formElements).every(name => {
|
||||||
|
if (Array.isArray(this.formElements[name])) {
|
||||||
|
return this.formElements[name].every((/** @type {FormControl} */ el) => !!el[property]);
|
||||||
|
}
|
||||||
|
return !!this.formElements[name][property];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets triggered by event 'validate-performed' which enabled us to handle 2 different situations
|
||||||
|
* - react on modelValue change, which says something about the validity as a whole
|
||||||
|
* (at least two checkboxes for instance) and nothing about the children's values
|
||||||
|
* - children validity states have changed, so fieldset needs to update itself based on that
|
||||||
|
* @param {Event} ev
|
||||||
|
*/
|
||||||
|
__onChildValidatePerformed(ev) {
|
||||||
|
if (ev && this.isRegisteredFormElement(/** @type {FormControl} */ (ev.target))) {
|
||||||
|
this.validate();
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
);
|
|
||||||
|
_syncFocused() {
|
||||||
|
this.focused = this._anyFormElementHas('focused');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Event} ev
|
||||||
|
*/
|
||||||
|
_onFocusOut(ev) {
|
||||||
|
const lastEl = this.formElements[this.formElements.length - 1];
|
||||||
|
if (ev.target === lastEl) {
|
||||||
|
this.touched = true;
|
||||||
|
}
|
||||||
|
this.focused = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_syncDirty() {
|
||||||
|
this.dirty = this._anyFormElementHas('dirty');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {FormControl} child
|
||||||
|
*/
|
||||||
|
__linkChildrenMessagesToParent(child) {
|
||||||
|
// aria-describedby of (nested) children
|
||||||
|
const unTypedThis = /** @type {unknown} */ (this);
|
||||||
|
let parent = /** @type {FormControlHost & { __parentFormGroup:any }} */ (unTypedThis);
|
||||||
|
const ctor = /** @type {typeof FormGroupMixin} */ (this.constructor);
|
||||||
|
while (parent) {
|
||||||
|
ctor._addDescriptionElementIdsToField(child, parent._getAriaDescriptionElements());
|
||||||
|
// Also check if the newly added child needs to refer grandparents
|
||||||
|
parent = parent.__parentFormGroup;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @override of FormRegistrarMixin.
|
||||||
|
* @desc Connects ValidateMixin and DisabledMixin
|
||||||
|
* On top of this, error messages of children are linked to their parents
|
||||||
|
* @param {FormControl} child
|
||||||
|
* @param {number} indexToInsertAt
|
||||||
|
*/
|
||||||
|
addFormElement(child, indexToInsertAt) {
|
||||||
|
super.addFormElement(child, indexToInsertAt);
|
||||||
|
if (this.disabled) {
|
||||||
|
child.makeRequestToBeDisabled();
|
||||||
|
}
|
||||||
|
// TODO: Unlink in removeFormElement
|
||||||
|
this.__linkChildrenMessagesToParent(child);
|
||||||
|
this.validate({ clearCurrentResult: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gathers initial model values of all children. Used
|
||||||
|
* when resetGroup() is called.
|
||||||
|
*/
|
||||||
|
get _initialModelValue() {
|
||||||
|
return this._getFromAllFormElements('_initialModelValue');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add aria-describedby to child element(field), so that it points to feedback/help-text of
|
||||||
|
* parent(fieldset)
|
||||||
|
* @param {FormControl} field - the child: lion-field/lion-input/lion-textarea
|
||||||
|
* @param {HTMLElement[]} descriptionElements - description elements like feedback and help-text
|
||||||
|
*/
|
||||||
|
static _addDescriptionElementIdsToField(field, descriptionElements) {
|
||||||
|
const orderedEls = getAriaElementsInRightDomOrder(descriptionElements, { reverse: true });
|
||||||
|
orderedEls.forEach(el => {
|
||||||
|
if (field.addToAriaDescribedBy) {
|
||||||
|
field.addToAriaDescribedBy(el, { reorder: false });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @override of FormRegistrarMixin. Connects ValidateMixin
|
||||||
|
* @param {FormRegisteringHost} el
|
||||||
|
*/
|
||||||
|
removeFormElement(el) {
|
||||||
|
super.removeFormElement(el);
|
||||||
|
this.validate({ clearCurrentResult: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FormGroupMixin = dedupeMixin(FormGroupMixinImplementation);
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ import { dedupeMixin } from '@lion/core';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('../../types/registration/FormRegisteringMixinTypes').FormRegisteringMixin} FormRegisteringMixin
|
* @typedef {import('../../types/registration/FormRegisteringMixinTypes').FormRegisteringMixin} FormRegisteringMixin
|
||||||
|
* @typedef {import('../../types/registration/FormRegistrarMixinTypes').ElementWithParentFormGroup} ElementWithParentFormGroup
|
||||||
|
* @typedef {import('../../types/registration/FormRegistrarMixinTypes').FormRegistrarHost} FormRegistrarHost
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -10,11 +12,17 @@ import { dedupeMixin } from '@lion/core';
|
||||||
* This Mixin registers a form element to a Registrar
|
* This Mixin registers a form element to a Registrar
|
||||||
*
|
*
|
||||||
* @type {FormRegisteringMixin}
|
* @type {FormRegisteringMixin}
|
||||||
|
* @param {import('@open-wc/dedupe-mixin').Constructor<HTMLElement>} superclass
|
||||||
*/
|
*/
|
||||||
const FormRegisteringMixinImplementation = superclass =>
|
const FormRegisteringMixinImplementation = superclass =>
|
||||||
class FormRegisteringMixin extends superclass {
|
class extends superclass {
|
||||||
|
/** @type {FormRegistrarHost | undefined} */
|
||||||
|
__parentFormGroup;
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
|
// @ts-expect-error check it anyway, because could be lit-element extension
|
||||||
if (super.connectedCallback) {
|
if (super.connectedCallback) {
|
||||||
|
// @ts-expect-error check it anyway, because could be lit-element extension
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
}
|
}
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
|
|
@ -26,7 +34,9 @@ const FormRegisteringMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
|
// @ts-expect-error check it anyway, because could be lit-element extension
|
||||||
if (super.disconnectedCallback) {
|
if (super.disconnectedCallback) {
|
||||||
|
// @ts-expect-error check it anyway, because could be lit-element extension
|
||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
}
|
}
|
||||||
if (this.__parentFormGroup) {
|
if (this.__parentFormGroup) {
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,17 @@
|
||||||
// eslint-disable-next-line max-classes-per-file
|
// eslint-disable-next-line max-classes-per-file
|
||||||
import { dedupeMixin } from '@lion/core';
|
import { dedupeMixin } from '@lion/core';
|
||||||
import { FormRegisteringMixin } from './FormRegisteringMixin.js';
|
|
||||||
import { FormControlsCollection } from './FormControlsCollection.js';
|
import { FormControlsCollection } from './FormControlsCollection.js';
|
||||||
|
import { FormRegisteringMixin } from './FormRegisteringMixin.js';
|
||||||
// TODO: rename .formElements to .formControls? (or .$controls ?)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('../../types/registration/FormRegistrarMixinTypes').FormRegistrarMixin} FormRegistrarMixin
|
* @typedef {import('../../types/registration/FormRegistrarMixinTypes').FormRegistrarMixin} FormRegistrarMixin
|
||||||
* @typedef {import('../../types/registration/FormRegistrarMixinTypes').ElementWithParentFormGroup} ElementWithParentFormGroup
|
* @typedef {import('../../types/registration/FormRegistrarMixinTypes').ElementWithParentFormGroup} ElementWithParentFormGroup
|
||||||
|
* @typedef {import('../../types/registration/FormRegisteringMixinTypes').FormRegisteringHost} FormRegisteringHost
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('../../types/FormControlMixinTypes').FormControlHost} FormControlHost
|
||||||
|
* @typedef {FormControlHost & HTMLElement & {__parentFormGroup?:HTMLElement, checked?:boolean}} FormControl
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -19,10 +23,11 @@ import { FormControlsCollection } from './FormControlsCollection.js';
|
||||||
* For choice groups, the value will only stay an array.
|
* For choice groups, the value will only stay an array.
|
||||||
* See FormControlsCollection for more information
|
* See FormControlsCollection for more information
|
||||||
* @type {FormRegistrarMixin}
|
* @type {FormRegistrarMixin}
|
||||||
|
* @param {import('@open-wc/dedupe-mixin').Constructor<import('@lion/core').LitElement>} superclass
|
||||||
*/
|
*/
|
||||||
const FormRegistrarMixinImplementation = superclass =>
|
const FormRegistrarMixinImplementation = superclass =>
|
||||||
// eslint-disable-next-line no-shadow, no-unused-vars
|
// eslint-disable-next-line no-shadow, no-unused-vars
|
||||||
class FormRegistrarMixin extends FormRegisteringMixin(superclass) {
|
class extends FormRegisteringMixin(superclass) {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
/**
|
/**
|
||||||
|
|
@ -45,7 +50,11 @@ const FormRegistrarMixinImplementation = superclass =>
|
||||||
this._isFormOrFieldset = false;
|
this._isFormOrFieldset = false;
|
||||||
|
|
||||||
this._onRequestToAddFormElement = this._onRequestToAddFormElement.bind(this);
|
this._onRequestToAddFormElement = this._onRequestToAddFormElement.bind(this);
|
||||||
this.addEventListener('form-element-register', this._onRequestToAddFormElement);
|
|
||||||
|
this.addEventListener(
|
||||||
|
'form-element-register',
|
||||||
|
/** @type {EventListenerOrEventListenerObject} */ (this._onRequestToAddFormElement),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -57,7 +66,7 @@ const FormRegistrarMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {ElementWithParentFormGroup} child the child element (field)
|
* @param {FormControl} child the child element (field)
|
||||||
* @param {number} indexToInsertAt index to insert the form element at
|
* @param {number} indexToInsertAt index to insert the form element at
|
||||||
*/
|
*/
|
||||||
addFormElement(child, indexToInsertAt) {
|
addFormElement(child, indexToInsertAt) {
|
||||||
|
|
@ -74,12 +83,12 @@ const FormRegistrarMixinImplementation = superclass =>
|
||||||
|
|
||||||
// 2. Add children as object key
|
// 2. Add children as object key
|
||||||
if (this._isFormOrFieldset) {
|
if (this._isFormOrFieldset) {
|
||||||
// @ts-ignore
|
const { name } = child;
|
||||||
const { name } = child; // FIXME: <-- ElementWithParentFormGroup should become LionFieldWithParentFormGroup so that "name" exists
|
|
||||||
if (!name) {
|
if (!name) {
|
||||||
console.info('Error Node:', child); // eslint-disable-line no-console
|
console.info('Error Node:', child); // eslint-disable-line no-console
|
||||||
throw new TypeError('You need to define a name');
|
throw new TypeError('You need to define a name');
|
||||||
}
|
}
|
||||||
|
// @ts-expect-error
|
||||||
if (name === this.name) {
|
if (name === this.name) {
|
||||||
console.info('Error Node:', child); // eslint-disable-line no-console
|
console.info('Error Node:', child); // eslint-disable-line no-console
|
||||||
throw new TypeError(`You can not have the same name "${name}" as your parent`);
|
throw new TypeError(`You can not have the same name "${name}" as your parent`);
|
||||||
|
|
@ -106,7 +115,7 @@ const FormRegistrarMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {ElementWithParentFormGroup} child the child element (field)
|
* @param {FormRegisteringHost} child the child element (field)
|
||||||
*/
|
*/
|
||||||
removeFormElement(child) {
|
removeFormElement(child) {
|
||||||
// 1. Handle array based children
|
// 1. Handle array based children
|
||||||
|
|
@ -117,7 +126,7 @@ const FormRegistrarMixinImplementation = superclass =>
|
||||||
|
|
||||||
// 2. Handle name based object keys
|
// 2. Handle name based object keys
|
||||||
if (this._isFormOrFieldset) {
|
if (this._isFormOrFieldset) {
|
||||||
// @ts-ignore
|
// @ts-expect-error
|
||||||
const { name } = child; // FIXME: <-- ElementWithParentFormGroup should become LionFieldWithParentFormGroup so that "name" exists
|
const { name } = child; // FIXME: <-- ElementWithParentFormGroup should become LionFieldWithParentFormGroup so that "name" exists
|
||||||
if (name.substr(-2) === '[]' && this.formElements[name]) {
|
if (name.substr(-2) === '[]' && this.formElements[name]) {
|
||||||
const idx = this.formElements[name].indexOf(child);
|
const idx = this.formElements[name].indexOf(child);
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,11 @@ import { dedupeMixin } from '@lion/core';
|
||||||
* </my-portal>
|
* </my-portal>
|
||||||
* // my-field will be registered within my-form
|
* // my-field will be registered within my-form
|
||||||
* @type {FormRegistrarPortalMixin}
|
* @type {FormRegistrarPortalMixin}
|
||||||
|
* @param {import('@open-wc/dedupe-mixin').Constructor<HTMLElement>} superclass
|
||||||
*/
|
*/
|
||||||
const FormRegistrarPortalMixinImplementation = superclass =>
|
const FormRegistrarPortalMixinImplementation = superclass =>
|
||||||
// eslint-disable-next-line no-shadow, no-unused-vars
|
// eslint-disable-next-line no-shadow, no-unused-vars
|
||||||
class FormRegistrarPortalMixin extends superclass {
|
class extends superclass {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
/** @type {(FormRegistrarPortalHost & HTMLElement) | undefined} */
|
/** @type {(FormRegistrarPortalHost & HTMLElement) | undefined} */
|
||||||
|
|
@ -30,7 +31,8 @@ const FormRegistrarPortalMixinImplementation = superclass =>
|
||||||
);
|
);
|
||||||
this.addEventListener(
|
this.addEventListener(
|
||||||
'form-element-register',
|
'form-element-register',
|
||||||
this.__redispatchEventForFormRegistrarPortalMixin,
|
/** @type {EventListenerOrEventListenerObject} */ (this
|
||||||
|
.__redispatchEventForFormRegistrarPortalMixin),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,10 @@ import { dedupeMixin } from '@lion/core';
|
||||||
* Whenever the implementation of `requestUpdateInternal` changes (this happened in the past for
|
* Whenever the implementation of `requestUpdateInternal` changes (this happened in the past for
|
||||||
* `requestUpdate`) we only have to change our abstraction instead of all our components
|
* `requestUpdate`) we only have to change our abstraction instead of all our components
|
||||||
* @type {SyncUpdatableMixin}
|
* @type {SyncUpdatableMixin}
|
||||||
|
* @param {import('@open-wc/dedupe-mixin').Constructor<import('@lion/core').LitElement>} superclass
|
||||||
*/
|
*/
|
||||||
const SyncUpdatableMixinImplementation = superclass =>
|
const SyncUpdatableMixinImplementation = superclass =>
|
||||||
class SyncUpdatable extends superclass {
|
class extends superclass {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
// Namespace for this mixin that guarantees naming clashes will not occur...
|
// Namespace for this mixin that guarantees naming clashes will not occur...
|
||||||
|
|
@ -52,6 +53,7 @@ const SyncUpdatableMixinImplementation = superclass =>
|
||||||
* @param {*} oldValue
|
* @param {*} oldValue
|
||||||
*/
|
*/
|
||||||
static __syncUpdatableHasChanged(name, newValue, oldValue) {
|
static __syncUpdatableHasChanged(name, newValue, oldValue) {
|
||||||
|
// @ts-expect-error FIXME: Typescript bug, superclass static method not availabe from static context
|
||||||
const properties = this._classProperties;
|
const properties = this._classProperties;
|
||||||
if (properties.get(name) && properties.get(name).hasChanged) {
|
if (properties.get(name) && properties.get(name).hasChanged) {
|
||||||
return properties.get(name).hasChanged(newValue, oldValue);
|
return properties.get(name).hasChanged(newValue, oldValue);
|
||||||
|
|
@ -61,7 +63,8 @@ const SyncUpdatableMixinImplementation = superclass =>
|
||||||
|
|
||||||
__syncUpdatableInitialize() {
|
__syncUpdatableInitialize() {
|
||||||
const ns = this.__SyncUpdatableNamespace;
|
const ns = this.__SyncUpdatableNamespace;
|
||||||
const ctor = /** @type {SyncUpdatableMixin & SyncUpdatable} */ (this.constructor);
|
const ctor = /** @type {typeof SyncUpdatableMixin & typeof import('../../types/utils/SyncUpdatableMixinTypes').SyncUpdatableHost} */ (this
|
||||||
|
.constructor);
|
||||||
|
|
||||||
ns.initialized = true;
|
ns.initialized = true;
|
||||||
// Empty queue...
|
// Empty queue...
|
||||||
|
|
@ -84,7 +87,8 @@ const SyncUpdatableMixinImplementation = superclass =>
|
||||||
this.__SyncUpdatableNamespace = this.__SyncUpdatableNamespace || {};
|
this.__SyncUpdatableNamespace = this.__SyncUpdatableNamespace || {};
|
||||||
const ns = this.__SyncUpdatableNamespace;
|
const ns = this.__SyncUpdatableNamespace;
|
||||||
|
|
||||||
const ctor = /** @type {SyncUpdatableMixin & SyncUpdatable} */ (this.constructor);
|
const ctor = /** @type {typeof SyncUpdatableMixin & typeof import('../../types/utils/SyncUpdatableMixinTypes').SyncUpdatableHost} */ (this
|
||||||
|
.constructor);
|
||||||
|
|
||||||
// Before connectedCallback: queue
|
// Before connectedCallback: queue
|
||||||
if (!ns.connected) {
|
if (!ns.connected) {
|
||||||
|
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
// TODO: this method has to be removed when EventTarget polyfill is available on IE11
|
|
||||||
// TODO: move to core and apply everywhere?
|
|
||||||
// TODO: pascalCase this filename?
|
|
||||||
/**
|
|
||||||
* @param {HTMLElement} instance
|
|
||||||
*/
|
|
||||||
export function fakeExtendsEventTarget(instance) {
|
|
||||||
const delegate = document.createDocumentFragment();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} type
|
|
||||||
* @param {EventListener} listener
|
|
||||||
* @param {Object} opts
|
|
||||||
*/
|
|
||||||
const delegatedMethodAdd = (type, listener, opts) =>
|
|
||||||
delegate.addEventListener(type, listener, opts);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Event|CustomEvent} event
|
|
||||||
*/
|
|
||||||
const delegatedMethodDispatch = event => delegate.dispatchEvent(event);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} type
|
|
||||||
* @param {EventListener} listener
|
|
||||||
* @param {Object} opts
|
|
||||||
*/
|
|
||||||
const delegatedMethodRemove = (type, listener, opts) =>
|
|
||||||
delegate.removeEventListener(type, listener, opts);
|
|
||||||
|
|
||||||
/* eslint-disable no-param-reassign */
|
|
||||||
instance.addEventListener = delegatedMethodAdd;
|
|
||||||
instance.dispatchEvent = delegatedMethodDispatch;
|
|
||||||
instance.removeEventListener = delegatedMethodRemove;
|
|
||||||
/* eslint-enable no-param-reassign */
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +1,14 @@
|
||||||
import { html, LitElement } from '@lion/core';
|
import { html, LitElement } from '@lion/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('../validate/Validator').Validator} Validator
|
||||||
|
*
|
||||||
|
* @typedef {Object} messageMap
|
||||||
|
* @property {string | Node} message
|
||||||
|
* @property {string} type
|
||||||
|
* @property {Validator} [validator]
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @desc Takes care of accessible rendering of error messages
|
* @desc Takes care of accessible rendering of error messages
|
||||||
* Should be used in conjunction with FormControl having ValidateMixin applied
|
* Should be used in conjunction with FormControl having ValidateMixin applied
|
||||||
|
|
@ -7,23 +16,27 @@ import { html, LitElement } from '@lion/core';
|
||||||
export class LionValidationFeedback extends LitElement {
|
export class LionValidationFeedback extends LitElement {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
/**
|
feedbackData: { attribute: false },
|
||||||
* @property {FeedbackData} feedbackData
|
|
||||||
*/
|
|
||||||
feedbackData: Array,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @overridable
|
* @overridable
|
||||||
|
* @param {Object} opts
|
||||||
|
* @param {string | Node} opts.message message or feedback node
|
||||||
|
* @param {string} [opts.type]
|
||||||
|
* @param {Validator} [opts.validator]
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line class-methods-use-this
|
// eslint-disable-next-line class-methods-use-this
|
||||||
_messageTemplate({ message }) {
|
_messageTemplate({ message }) {
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
updated() {
|
/**
|
||||||
super.updated();
|
* @param {import('lit-element').PropertyValues } changedProperties
|
||||||
|
*/
|
||||||
|
updated(changedProperties) {
|
||||||
|
super.updated(changedProperties);
|
||||||
if (this.feedbackData && this.feedbackData[0]) {
|
if (this.feedbackData && this.feedbackData[0]) {
|
||||||
this.setAttribute('type', this.feedbackData[0].type);
|
this.setAttribute('type', this.feedbackData[0].type);
|
||||||
this.currentType = this.feedbackData[0].type;
|
this.currentType = this.feedbackData[0].type;
|
||||||
|
|
@ -31,7 +44,8 @@ export class LionValidationFeedback extends LitElement {
|
||||||
if (this.currentType === 'success') {
|
if (this.currentType === 'success') {
|
||||||
this.removeMessage = window.setTimeout(() => {
|
this.removeMessage = window.setTimeout(() => {
|
||||||
this.removeAttribute('type');
|
this.removeAttribute('type');
|
||||||
this.feedbackData = '';
|
/** @type {messageMap[]} */
|
||||||
|
this.feedbackData = [];
|
||||||
}, 3000);
|
}, 3000);
|
||||||
}
|
}
|
||||||
} else if (this.currentType !== 'success') {
|
} else if (this.currentType !== 'success') {
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,14 @@ import { Validator } from './Validator.js';
|
||||||
*/
|
*/
|
||||||
export class ResultValidator extends Validator {
|
export class ResultValidator extends Validator {
|
||||||
/**
|
/**
|
||||||
* @param {object} context
|
* @param {Object} context
|
||||||
* @param {Validator[]} context.validationResult
|
* @param {Validator[]} context.regularValidationResult
|
||||||
* @param {Validator[]} context.prevValidationResult
|
* @param {Validator[] | undefined} context.prevValidationResult
|
||||||
* @param {Validator[]} context.validators
|
* @param {Validator[]} [context.validators]
|
||||||
* @returns {Feedback[]}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
executeOnResults({ validationResult, prevValidationResult, validators }) {} // eslint-disable-line
|
// eslint-disable-next-line no-unused-vars, class-methods-use-this
|
||||||
|
executeOnResults({ regularValidationResult, prevValidationResult, validators }) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,10 +1,16 @@
|
||||||
import { fakeExtendsEventTarget } from '../utils/fakeExtendsEventTarget.js';
|
|
||||||
|
|
||||||
export class Validator {
|
export class Validator {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {?} [param]
|
||||||
|
* @param {Object.<string,?>} [config]
|
||||||
|
*/
|
||||||
constructor(param, config) {
|
constructor(param, config) {
|
||||||
fakeExtendsEventTarget(this);
|
this.__fakeExtendsEventTarget();
|
||||||
|
|
||||||
|
/** @type {?} */
|
||||||
this.__param = param;
|
this.__param = param;
|
||||||
|
|
||||||
|
/** @type {Object.<string,?>} */
|
||||||
this.__config = config || {};
|
this.__config = config || {};
|
||||||
this.type = (config && config.type) || 'error'; // Default type supported by ValidateMixin
|
this.type = (config && config.type) || 'error'; // Default type supported by ValidateMixin
|
||||||
}
|
}
|
||||||
|
|
@ -19,21 +25,27 @@ export class Validator {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @desc The function that returns a Boolean
|
* @desc The function that returns a Boolean
|
||||||
* @param {string|Date|Number|object} modelValue
|
* @param {?} [modelValue]
|
||||||
* @param {object} param
|
* @param {?} [param]
|
||||||
|
* @param {{}} [config]
|
||||||
* @returns {Boolean|Promise<Boolean>}
|
* @returns {Boolean|Promise<Boolean>}
|
||||||
*/
|
*/
|
||||||
execute(/* modelValue, param */) {
|
// eslint-disable-next-line no-unused-vars, class-methods-use-this
|
||||||
if (!this.validatorName) {
|
execute(modelValue, param, config) {
|
||||||
|
const ctor = /** @type {typeof Validator} */ (this.constructor);
|
||||||
|
if (!ctor.validatorName) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'A validator needs to have a name! Please set it via "static get validatorName() { return \'IsCat\'; }"',
|
'A validator needs to have a name! Please set it via "static get validatorName() { return \'IsCat\'; }"',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
set param(p) {
|
set param(p) {
|
||||||
this.__param = p;
|
this.__param = p;
|
||||||
this.dispatchEvent(new Event('param-changed'));
|
if (this.dispatchEvent) {
|
||||||
|
this.dispatchEvent(new Event('param-changed'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get param() {
|
get param() {
|
||||||
|
|
@ -42,7 +54,9 @@ export class Validator {
|
||||||
|
|
||||||
set config(c) {
|
set config(c) {
|
||||||
this.__config = c;
|
this.__config = c;
|
||||||
this.dispatchEvent(new Event('config-changed'));
|
if (this.dispatchEvent) {
|
||||||
|
this.dispatchEvent(new Event('config-changed'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get config() {
|
get config() {
|
||||||
|
|
@ -51,16 +65,18 @@ export class Validator {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @overridable
|
* @overridable
|
||||||
* @param {object} data
|
* @param {object} [data]
|
||||||
* @param {*} data.modelValue
|
* @param {*} [data.modelValue]
|
||||||
* @param {string} data.fieldName
|
* @param {string} [data.fieldName]
|
||||||
* @param {*} data.params
|
* @param {HTMLElement} [data.formControl]
|
||||||
* @param {string} data.type
|
* @param {*} [data.params]
|
||||||
* @returns {string|Node|Promise<stringOrNode>|() => stringOrNode)}
|
* @param {string|undefined} [data.type]
|
||||||
|
* @returns {Promise<string|Node>}
|
||||||
*/
|
*/
|
||||||
async _getMessage(data) {
|
async _getMessage(data) {
|
||||||
|
const ctor = /** @type {typeof Validator} */ (this.constructor);
|
||||||
const composedData = {
|
const composedData = {
|
||||||
name: this.constructor.validatorName,
|
name: ctor.validatorName,
|
||||||
type: this.type,
|
type: this.type,
|
||||||
params: this.param,
|
params: this.param,
|
||||||
config: this.config,
|
config: this.config,
|
||||||
|
|
@ -75,29 +91,32 @@ export class Validator {
|
||||||
.config.getMessage}`,
|
.config.getMessage}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return this.constructor.getMessage(composedData);
|
return ctor.getMessage(composedData);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @overridable
|
* @overridable
|
||||||
* @param {object} data
|
* @param {object} [data]
|
||||||
* @param {*} data.modelValue
|
* @param {*} [data.modelValue]
|
||||||
* @param {string} data.fieldName
|
* @param {string} [data.fieldName]
|
||||||
* @param {*} data.params
|
* @param {*} [data.params]
|
||||||
* @param {string} data.type
|
* @param {string} [data.type]
|
||||||
* @returns {string|Node|Promise<stringOrNode>|() => stringOrNode)}
|
* @param {Object.<string,?>} [data.config]
|
||||||
|
* @param {string} [data.name]
|
||||||
|
* @returns {Promise<string|Node>}
|
||||||
*/
|
*/
|
||||||
static async getMessage(/* data */) {
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
static async getMessage(data) {
|
||||||
return `Please configure an error message for "${this.name}" by overriding "static async getMessage()"`;
|
return `Please configure an error message for "${this.name}" by overriding "static async getMessage()"`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {FormControl} formControl
|
* @param {HTMLElement} formControl
|
||||||
*/
|
*/
|
||||||
onFormControlConnect(formControl) {} // eslint-disable-line
|
onFormControlConnect(formControl) {} // eslint-disable-line
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {FormControl} formControl
|
* @param {HTMLElement} formControl
|
||||||
*/
|
*/
|
||||||
onFormControlDisconnect(formControl) {} // eslint-disable-line
|
onFormControlDisconnect(formControl) {} // eslint-disable-line
|
||||||
|
|
||||||
|
|
@ -111,6 +130,38 @@ export class Validator {
|
||||||
* - Or, when a webworker was started, its process could be aborted and then restarted.
|
* - Or, when a webworker was started, its process could be aborted and then restarted.
|
||||||
*/
|
*/
|
||||||
abortExecution() {} // eslint-disable-line
|
abortExecution() {} // eslint-disable-line
|
||||||
|
|
||||||
|
__fakeExtendsEventTarget() {
|
||||||
|
const delegate = document.createDocumentFragment();
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} type
|
||||||
|
* @param {EventListener} listener
|
||||||
|
* @param {Object} [opts]
|
||||||
|
*/
|
||||||
|
const delegatedAddEventListener = (type, listener, opts) =>
|
||||||
|
delegate.addEventListener(type, listener, opts);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} type
|
||||||
|
* @param {EventListener} listener
|
||||||
|
* @param {Object} [opts]
|
||||||
|
*/
|
||||||
|
const delegatedRemoveEventListener = (type, listener, opts) =>
|
||||||
|
delegate.removeEventListener(type, listener, opts);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Event|CustomEvent} event
|
||||||
|
*/
|
||||||
|
const delegatedDispatchEvent = event => delegate.dispatchEvent(event);
|
||||||
|
|
||||||
|
this.addEventListener = delegatedAddEventListener;
|
||||||
|
|
||||||
|
this.removeEventListener = delegatedRemoveEventListener;
|
||||||
|
|
||||||
|
this.dispatchEvent = delegatedDispatchEvent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// For simplicity, a default validator only handles one state:
|
// For simplicity, a default validator only handles one state:
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,29 @@
|
||||||
import { ResultValidator } from '../ResultValidator.js';
|
import { ResultValidator } from '../ResultValidator.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('../Validator').Validator} Validator
|
||||||
|
*/
|
||||||
|
|
||||||
export class DefaultSuccess extends ResultValidator {
|
export class DefaultSuccess extends ResultValidator {
|
||||||
|
/**
|
||||||
|
* @param {...any} args
|
||||||
|
*/
|
||||||
constructor(...args) {
|
constructor(...args) {
|
||||||
super(...args);
|
super(...args);
|
||||||
this.type = 'success';
|
this.type = 'success';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {Object} context
|
||||||
|
* @param {Validator[]} context.regularValidationResult
|
||||||
|
* @param {Validator[]} context.prevValidationResult
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
// eslint-disable-next-line class-methods-use-this
|
// eslint-disable-next-line class-methods-use-this
|
||||||
executeOnResults({ regularValidationResult, prevValidationResult }) {
|
executeOnResults({ regularValidationResult, prevValidationResult }) {
|
||||||
const errorOrWarning = v => v.type === 'error' || v.type === 'warning';
|
const errorOrWarning = /** @param {Validator} v */ v =>
|
||||||
|
v.type === 'error' || v.type === 'warning';
|
||||||
const hasErrorOrWarning = !!regularValidationResult.filter(errorOrWarning).length;
|
const hasErrorOrWarning = !!regularValidationResult.filter(errorOrWarning).length;
|
||||||
const prevHadErrorOrWarning = !!prevValidationResult.filter(errorOrWarning).length;
|
const prevHadErrorOrWarning = !!prevValidationResult.filter(errorOrWarning).length;
|
||||||
return !hasErrorOrWarning && prevHadErrorOrWarning;
|
return !hasErrorOrWarning && prevHadErrorOrWarning;
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,9 @@
|
||||||
import { normalizeDateTime } from '@lion/localize';
|
import { normalizeDateTime } from '@lion/localize';
|
||||||
import { Validator } from '../Validator.js';
|
import { Validator } from '../Validator.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {?} value
|
||||||
|
*/
|
||||||
function isDate(value) {
|
function isDate(value) {
|
||||||
return (
|
return (
|
||||||
Object.prototype.toString.call(value) === '[object Date]' && !Number.isNaN(value.getTime())
|
Object.prototype.toString.call(value) === '[object Date]' && !Number.isNaN(value.getTime())
|
||||||
|
|
@ -14,6 +17,9 @@ export class IsDate extends Validator {
|
||||||
return 'IsDate';
|
return 'IsDate';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {?} value
|
||||||
|
*/
|
||||||
// eslint-disable-next-line class-methods-use-this
|
// eslint-disable-next-line class-methods-use-this
|
||||||
execute(value) {
|
execute(value) {
|
||||||
let hasError = false;
|
let hasError = false;
|
||||||
|
|
@ -29,6 +35,9 @@ export class MinDate extends Validator {
|
||||||
return 'MinDate';
|
return 'MinDate';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {?} value
|
||||||
|
*/
|
||||||
execute(value, min = this.param) {
|
execute(value, min = this.param) {
|
||||||
let hasError = false;
|
let hasError = false;
|
||||||
if (!isDate(value) || value < normalizeDateTime(min)) {
|
if (!isDate(value) || value < normalizeDateTime(min)) {
|
||||||
|
|
@ -43,6 +52,9 @@ export class MaxDate extends Validator {
|
||||||
return 'MaxDate';
|
return 'MaxDate';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {?} value
|
||||||
|
*/
|
||||||
execute(value, max = this.param) {
|
execute(value, max = this.param) {
|
||||||
let hasError = false;
|
let hasError = false;
|
||||||
if (!isDate(value) || value > normalizeDateTime(max)) {
|
if (!isDate(value) || value > normalizeDateTime(max)) {
|
||||||
|
|
@ -57,6 +69,9 @@ export class MinMaxDate extends Validator {
|
||||||
return 'MinMaxDate';
|
return 'MinMaxDate';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {?} value
|
||||||
|
*/
|
||||||
execute(value, { min = 0, max = 0 } = this.param) {
|
execute(value, { min = 0, max = 0 } = this.param) {
|
||||||
let hasError = false;
|
let hasError = false;
|
||||||
if (!isDate(value) || value < normalizeDateTime(min) || value > normalizeDateTime(max)) {
|
if (!isDate(value) || value < normalizeDateTime(min) || value > normalizeDateTime(max)) {
|
||||||
|
|
@ -71,6 +86,9 @@ export class IsDateDisabled extends Validator {
|
||||||
return 'IsDateDisabled';
|
return 'IsDateDisabled';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {?} value
|
||||||
|
*/
|
||||||
execute(value, isDisabledFn = this.param) {
|
execute(value, isDisabledFn = this.param) {
|
||||||
let hasError = false;
|
let hasError = false;
|
||||||
if (!isDate(value) || isDisabledFn(value)) {
|
if (!isDate(value) || isDisabledFn(value)) {
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,9 @@ export class IsNumber extends Validator {
|
||||||
return 'IsNumber';
|
return 'IsNumber';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {?} value
|
||||||
|
*/
|
||||||
// eslint-disable-next-line class-methods-use-this
|
// eslint-disable-next-line class-methods-use-this
|
||||||
execute(value) {
|
execute(value) {
|
||||||
let isEnabled = false;
|
let isEnabled = false;
|
||||||
|
|
@ -30,6 +33,9 @@ export class MinNumber extends Validator {
|
||||||
return 'MinNumber';
|
return 'MinNumber';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {?} value
|
||||||
|
*/
|
||||||
execute(value, min = this.param) {
|
execute(value, min = this.param) {
|
||||||
let isEnabled = false;
|
let isEnabled = false;
|
||||||
if (!isNumber(value) || value < min) {
|
if (!isNumber(value) || value < min) {
|
||||||
|
|
@ -44,6 +50,9 @@ export class MaxNumber extends Validator {
|
||||||
return 'MaxNumber';
|
return 'MaxNumber';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {?} value
|
||||||
|
*/
|
||||||
execute(value, max = this.param) {
|
execute(value, max = this.param) {
|
||||||
let isEnabled = false;
|
let isEnabled = false;
|
||||||
if (!isNumber(value) || value > max) {
|
if (!isNumber(value) || value > max) {
|
||||||
|
|
@ -58,6 +67,9 @@ export class MinMaxNumber extends Validator {
|
||||||
return 'MinMaxNumber';
|
return 'MinMaxNumber';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {?} value
|
||||||
|
*/
|
||||||
execute(value, { min = 0, max = 0 } = this.param) {
|
execute(value, { min = 0, max = 0 } = this.param) {
|
||||||
let isEnabled = false;
|
let isEnabled = false;
|
||||||
if (!isNumber(value) || value < min || value > max) {
|
if (!isNumber(value) || value < min || value > max) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
import { Validator } from '../Validator.js';
|
import { Validator } from '../Validator.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('../../../types/FormControlMixinTypes.js').FormControlHost} FormControlHost
|
||||||
|
*/
|
||||||
|
|
||||||
export class Required extends Validator {
|
export class Required extends Validator {
|
||||||
static get validatorName() {
|
static get validatorName() {
|
||||||
return 'Required';
|
return 'Required';
|
||||||
|
|
@ -11,6 +15,9 @@ export class Required extends Validator {
|
||||||
* FormControl.__isEmpty / FormControl._isEmpty.
|
* FormControl.__isEmpty / FormControl._isEmpty.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {FormControlHost & HTMLElement} formControl
|
||||||
|
*/
|
||||||
// eslint-disable-next-line class-methods-use-this
|
// eslint-disable-next-line class-methods-use-this
|
||||||
onFormControlConnect(formControl) {
|
onFormControlConnect(formControl) {
|
||||||
if (formControl._inputNode) {
|
if (formControl._inputNode) {
|
||||||
|
|
@ -18,6 +25,9 @@ export class Required extends Validator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {FormControlHost & HTMLElement} formControl
|
||||||
|
*/
|
||||||
// eslint-disable-next-line class-methods-use-this
|
// eslint-disable-next-line class-methods-use-this
|
||||||
onFormControlDisconnect(formControl) {
|
onFormControlDisconnect(formControl) {
|
||||||
if (formControl._inputNode) {
|
if (formControl._inputNode) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
/* eslint-disable max-classes-per-file */
|
/* eslint-disable max-classes-per-file */
|
||||||
import { Validator } from '../Validator.js';
|
import { Validator } from '../Validator.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {?} value
|
||||||
|
*/
|
||||||
const isString = value => typeof value === 'string';
|
const isString = value => typeof value === 'string';
|
||||||
|
|
||||||
export class IsString extends Validator {
|
export class IsString extends Validator {
|
||||||
|
|
@ -8,6 +11,9 @@ export class IsString extends Validator {
|
||||||
return 'IsString';
|
return 'IsString';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {?} value
|
||||||
|
*/
|
||||||
// eslint-disable-next-line class-methods-use-this
|
// eslint-disable-next-line class-methods-use-this
|
||||||
execute(value) {
|
execute(value) {
|
||||||
let hasError = false;
|
let hasError = false;
|
||||||
|
|
@ -23,6 +29,9 @@ export class EqualsLength extends Validator {
|
||||||
return 'EqualsLength';
|
return 'EqualsLength';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {?} value
|
||||||
|
*/
|
||||||
execute(value, length = this.param) {
|
execute(value, length = this.param) {
|
||||||
let hasError = false;
|
let hasError = false;
|
||||||
if (!isString(value) || value.length !== length) {
|
if (!isString(value) || value.length !== length) {
|
||||||
|
|
@ -37,6 +46,9 @@ export class MinLength extends Validator {
|
||||||
return 'MinLength';
|
return 'MinLength';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {?} value
|
||||||
|
*/
|
||||||
execute(value, min = this.param) {
|
execute(value, min = this.param) {
|
||||||
let hasError = false;
|
let hasError = false;
|
||||||
if (!isString(value) || value.length < min) {
|
if (!isString(value) || value.length < min) {
|
||||||
|
|
@ -51,6 +63,9 @@ export class MaxLength extends Validator {
|
||||||
return 'MaxLength';
|
return 'MaxLength';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {?} value
|
||||||
|
*/
|
||||||
execute(value, max = this.param) {
|
execute(value, max = this.param) {
|
||||||
let hasError = false;
|
let hasError = false;
|
||||||
if (!isString(value) || value.length > max) {
|
if (!isString(value) || value.length > max) {
|
||||||
|
|
@ -65,6 +80,9 @@ export class MinMaxLength extends Validator {
|
||||||
return 'MinMaxLength';
|
return 'MinMaxLength';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {?} value
|
||||||
|
*/
|
||||||
execute(value, { min = 0, max = 0 } = this.param) {
|
execute(value, { min = 0, max = 0 } = this.param) {
|
||||||
let hasError = false;
|
let hasError = false;
|
||||||
if (!isString(value) || value.length < min || value.length > max) {
|
if (!isString(value) || value.length < min || value.length > max) {
|
||||||
|
|
@ -80,6 +98,9 @@ export class IsEmail extends Validator {
|
||||||
return 'IsEmail';
|
return 'IsEmail';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {?} value
|
||||||
|
*/
|
||||||
// eslint-disable-next-line class-methods-use-this
|
// eslint-disable-next-line class-methods-use-this
|
||||||
execute(value) {
|
execute(value) {
|
||||||
let hasError = false;
|
let hasError = false;
|
||||||
|
|
@ -90,12 +111,19 @@ export class IsEmail extends Validator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {?} value
|
||||||
|
* @param {RegExp} pattern
|
||||||
|
*/
|
||||||
const hasPattern = (value, pattern) => pattern.test(value);
|
const hasPattern = (value, pattern) => pattern.test(value);
|
||||||
export class Pattern extends Validator {
|
export class Pattern extends Validator {
|
||||||
static get validatorName() {
|
static get validatorName() {
|
||||||
return 'Pattern';
|
return 'Pattern';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {?} value
|
||||||
|
*/
|
||||||
// eslint-disable-next-line class-methods-use-this
|
// eslint-disable-next-line class-methods-use-this
|
||||||
execute(value, pattern = this.param) {
|
execute(value, pattern = this.param) {
|
||||||
if (!(pattern instanceof RegExp)) {
|
if (!(pattern instanceof RegExp)) {
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,9 @@ export class AlwaysValid extends Validator {
|
||||||
return 'AlwaysValid';
|
return 'AlwaysValid';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {Promise<boolean> | boolean}
|
||||||
|
*/
|
||||||
execute() {
|
execute() {
|
||||||
const showMessage = false;
|
const showMessage = false;
|
||||||
return showMessage;
|
return showMessage;
|
||||||
|
|
@ -28,7 +31,10 @@ export class AsyncAlwaysValid extends AlwaysValid {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
execute() {
|
/**
|
||||||
|
* @return {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
async execute() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -38,6 +44,9 @@ export class AsyncAlwaysInvalid extends AlwaysValid {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {Promise<boolean>}
|
||||||
|
*/
|
||||||
async execute() {
|
async execute() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,28 +6,35 @@ import { FormRegistrarPortalMixin } from '../src/registration/FormRegistrarPorta
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('../types/registration/FormRegistrarMixinTypes').FormRegistrarHost} FormRegistrarHost
|
* @typedef {import('../types/registration/FormRegistrarMixinTypes').FormRegistrarHost} FormRegistrarHost
|
||||||
* @typedef {import('../types/registration/FormRegistrarMixinTypes').FormRegistrarMixin} FormRegistrarMixin
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* @typedef {Object} customConfig
|
||||||
* @param {Object} customConfig
|
* @property {typeof HTMLElement | typeof import('@lion/core').UpdatingElement | typeof LitElement} [baseElement]
|
||||||
* @param {string} customConfig.suffix
|
* @property {string} [customConfig.suffix]
|
||||||
* @param {string} customConfig.parentTagString
|
* @property {string} [customConfig.parentTagString]
|
||||||
* @param {string} customConfig.childTagString
|
* @property {string} [customConfig.childTagString]
|
||||||
* @param {string} customConfig.portalTagString
|
* @property {string} [customConfig.portalTagString]
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {customConfig} customConfig
|
||||||
*/
|
*/
|
||||||
export const runRegistrationSuite = customConfig => {
|
export const runRegistrationSuite = customConfig => {
|
||||||
const cfg = {
|
const cfg = {
|
||||||
|
// @ts-expect-error https://github.com/microsoft/TypeScript/issues/38535
|
||||||
baseElement: HTMLElement,
|
baseElement: HTMLElement,
|
||||||
...customConfig,
|
...customConfig,
|
||||||
};
|
};
|
||||||
|
|
||||||
describe(`FormRegistrationMixins ${cfg.suffix}`, () => {
|
describe(`FormRegistrationMixins ${cfg.suffix}`, () => {
|
||||||
|
// @ts-expect-error base constructors same return type & type cannot be assigned like this
|
||||||
class RegistrarClass extends FormRegistrarMixin(cfg.baseElement) {}
|
class RegistrarClass extends FormRegistrarMixin(cfg.baseElement) {}
|
||||||
cfg.parentTagString = defineCE(RegistrarClass);
|
cfg.parentTagString = defineCE(RegistrarClass);
|
||||||
|
// @ts-expect-error base constructors same return type & type cannot be assigned like this
|
||||||
class RegisteringClass extends FormRegisteringMixin(cfg.baseElement) {}
|
class RegisteringClass extends FormRegisteringMixin(cfg.baseElement) {}
|
||||||
cfg.childTagString = defineCE(RegisteringClass);
|
cfg.childTagString = defineCE(RegisteringClass);
|
||||||
|
// @ts-expect-error base constructors same return type & type cannot be assigned like this
|
||||||
class PortalClass extends FormRegistrarPortalMixin(cfg.baseElement) {}
|
class PortalClass extends FormRegistrarPortalMixin(cfg.baseElement) {}
|
||||||
cfg.portalTagString = defineCE(PortalClass);
|
cfg.portalTagString = defineCE(PortalClass);
|
||||||
|
|
||||||
|
|
@ -84,6 +91,7 @@ export const runRegistrationSuite = customConfig => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('works for components that have a delayed render', async () => {
|
it('works for components that have a delayed render', async () => {
|
||||||
|
// @ts-expect-error base constructors same return type
|
||||||
class PerformUpdate extends FormRegistrarMixin(LitElement) {
|
class PerformUpdate extends FormRegistrarMixin(LitElement) {
|
||||||
async performUpdate() {
|
async performUpdate() {
|
||||||
await new Promise(resolve => setTimeout(() => resolve(), 10));
|
await new Promise(resolve => setTimeout(() => resolve(), 10));
|
||||||
|
|
|
||||||
|
|
@ -2,17 +2,18 @@ import { LitElement } from '@lion/core';
|
||||||
import { aTimeout, defineCE, expect, fixture, html, unsafeStatic } from '@open-wc/testing';
|
import { aTimeout, defineCE, expect, fixture, html, unsafeStatic } from '@open-wc/testing';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
import { FormatMixin } from '../src/FormatMixin.js';
|
import { FormatMixin } from '../src/FormatMixin.js';
|
||||||
// FIXME: revert once validate is typed
|
import { Unparseable, Validator } from '../index.js';
|
||||||
// import { Unparseable, Validator } from '../index.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('../types/FormatMixinTypes').FormatHost} FormatHost
|
|
||||||
* @typedef {{ _inputNode: HTMLElement }} inputNodeHost
|
|
||||||
* @typedef {{ errorState: boolean, hasFeedbackFor: string[], validators: ?[] }} validateHost // FIXME: replace with ValidateMixinHost once typed
|
|
||||||
* @typedef {ArrayConstructor | ObjectConstructor | NumberConstructor | BooleanConstructor | StringConstructor | DateConstructor | 'iban' | 'email'} modelValueType
|
* @typedef {ArrayConstructor | ObjectConstructor | NumberConstructor | BooleanConstructor | StringConstructor | DateConstructor | 'iban' | 'email'} modelValueType
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// @ts-expect-error base constructor same return type
|
||||||
class FormatClass extends FormatMixin(LitElement) {
|
class FormatClass extends FormatMixin(LitElement) {
|
||||||
|
get _inputNode() {
|
||||||
|
return /** @type {HTMLInputElement} */ (super._inputNode); // casts type
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return html`<slot name="input"></slot>`;
|
return html`<slot name="input"></slot>`;
|
||||||
}
|
}
|
||||||
|
|
@ -29,15 +30,10 @@ class FormatClass extends FormatMixin(LitElement) {
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
get _inputNode() {
|
|
||||||
return this.querySelector('input');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* @param {FormatClass} formControl
|
||||||
* @param {FormatClass & inputNodeHost} formControl
|
|
||||||
* @param {?} newViewValue
|
* @param {?} newViewValue
|
||||||
*/
|
*/
|
||||||
function mimicUserInput(formControl, newViewValue) {
|
function mimicUserInput(formControl, newViewValue) {
|
||||||
|
|
@ -46,7 +42,7 @@ function mimicUserInput(formControl, newViewValue) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {{tagString?: string, modelValueType: modelValueType}} [customConfig]
|
* @param {{tagString?: string, modelValueType?: modelValueType}} [customConfig]
|
||||||
*/
|
*/
|
||||||
export function runFormatMixinSuite(customConfig) {
|
export function runFormatMixinSuite(customConfig) {
|
||||||
const cfg = {
|
const cfg = {
|
||||||
|
|
@ -95,7 +91,7 @@ export function runFormatMixinSuite(customConfig) {
|
||||||
let elem;
|
let elem;
|
||||||
/** @type {FormatClass} */
|
/** @type {FormatClass} */
|
||||||
let nonFormat;
|
let nonFormat;
|
||||||
/** @type {FormatClass & inputNodeHost} */
|
/** @type {FormatClass} */
|
||||||
let fooFormat;
|
let fooFormat;
|
||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
|
|
@ -128,7 +124,7 @@ export function runFormatMixinSuite(customConfig) {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fires `model-value-changed` for every change on the input', async () => {
|
it('fires `model-value-changed` for every change on the input', async () => {
|
||||||
const formatEl = /** @type {FormatClass & inputNodeHost} */ (await fixture(
|
const formatEl = /** @type {FormatClass} */ (await fixture(
|
||||||
html`<${elem}><input slot="input"></${elem}>`,
|
html`<${elem}><input slot="input"></${elem}>`,
|
||||||
));
|
));
|
||||||
|
|
||||||
|
|
@ -215,7 +211,7 @@ export function runFormatMixinSuite(customConfig) {
|
||||||
|
|
||||||
it('synchronizes _inputNode.value as a fallback mechanism', async () => {
|
it('synchronizes _inputNode.value as a fallback mechanism', async () => {
|
||||||
// Note that in lion-field, the attribute would be put on <lion-field>, not on <input>
|
// Note that in lion-field, the attribute would be put on <lion-field>, not on <input>
|
||||||
const formatElem = /** @type {FormatClass & inputNodeHost} */ (await fixture(html`
|
const formatElem = /** @type {FormatClass} */ (await fixture(html`
|
||||||
<${elem}
|
<${elem}
|
||||||
value="string"
|
value="string"
|
||||||
.formatter=${/** @param {string} value */ value => `foo: ${value}`}
|
.formatter=${/** @param {string} value */ value => `foo: ${value}`}
|
||||||
|
|
@ -237,7 +233,7 @@ export function runFormatMixinSuite(customConfig) {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('reflects back formatted value to user on leave', async () => {
|
it('reflects back formatted value to user on leave', async () => {
|
||||||
const formatEl = /** @type {FormatClass & inputNodeHost} */ (await fixture(html`
|
const formatEl = /** @type {FormatClass} */ (await fixture(html`
|
||||||
<${elem} .formatter="${/** @param {string} value */ value => `foo: ${value}`}">
|
<${elem} .formatter="${/** @param {string} value */ value => `foo: ${value}`}">
|
||||||
<input slot="input" />
|
<input slot="input" />
|
||||||
</${elem}>
|
</${elem}>
|
||||||
|
|
@ -255,14 +251,14 @@ export function runFormatMixinSuite(customConfig) {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('reflects back .formattedValue immediately when .modelValue changed imperatively', async () => {
|
it('reflects back .formattedValue immediately when .modelValue changed imperatively', async () => {
|
||||||
const el = /** @type {FormatClass & inputNodeHost & validateHost} */ (await fixture(html`
|
const el = /** @type {FormatClass} */ (await fixture(html`
|
||||||
<${elem} .formatter="${/** @param {string} value */ value => `foo: ${value}`}">
|
<${elem} .formatter="${/** @param {string} value */ value => `foo: ${value}`}">
|
||||||
<input slot="input" />
|
<input slot="input" />
|
||||||
</${elem}>
|
</${elem}>
|
||||||
`));
|
`));
|
||||||
// The FormatMixin can be used in conjunction with the ValidateMixin, in which case
|
// The FormatMixin can be used in conjunction with the ValidateMixin, in which case
|
||||||
// it can hold errorState (affecting the formatting)
|
// it can hold errorState (affecting the formatting)
|
||||||
el.errorState = true;
|
el.hasFeedbackFor = ['error'];
|
||||||
|
|
||||||
// users types value 'test'
|
// users types value 'test'
|
||||||
mimicUserInput(el, 'test');
|
mimicUserInput(el, 'test');
|
||||||
|
|
@ -274,6 +270,7 @@ export function runFormatMixinSuite(customConfig) {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('works if there is no underlying _inputNode', async () => {
|
it('works if there is no underlying _inputNode', async () => {
|
||||||
|
// @ts-expect-error base constructor same return type
|
||||||
const tagNoInputString = defineCE(class extends FormatMixin(LitElement) {});
|
const tagNoInputString = defineCE(class extends FormatMixin(LitElement) {});
|
||||||
const tagNoInput = unsafeStatic(tagNoInputString);
|
const tagNoInput = unsafeStatic(tagNoInputString);
|
||||||
expect(async () => {
|
expect(async () => {
|
||||||
|
|
@ -300,7 +297,9 @@ export function runFormatMixinSuite(customConfig) {
|
||||||
|
|
||||||
it('should have formatOptions available in formatter', async () => {
|
it('should have formatOptions available in formatter', async () => {
|
||||||
const formatterSpy = sinon.spy(value => `foo: ${value}`);
|
const formatterSpy = sinon.spy(value => `foo: ${value}`);
|
||||||
const generatedViewValue = generateValueBasedOnType({ viewValue: true });
|
const generatedViewValue = /** @type {string} */ (generateValueBasedOnType({
|
||||||
|
viewValue: true,
|
||||||
|
}));
|
||||||
await fixture(html`
|
await fixture(html`
|
||||||
<${elem} value="${generatedViewValue}" .formatter="${formatterSpy}"
|
<${elem} value="${generatedViewValue}" .formatter="${formatterSpy}"
|
||||||
.formatOptions="${{ locale: 'en-GB', decimalSeparator: '-' }}">
|
.formatOptions="${{ locale: 'en-GB', decimalSeparator: '-' }}">
|
||||||
|
|
@ -319,7 +318,7 @@ export function runFormatMixinSuite(customConfig) {
|
||||||
/** @type {?} */
|
/** @type {?} */
|
||||||
const generatedValue = generateValueBasedOnType();
|
const generatedValue = generateValueBasedOnType();
|
||||||
const parserSpy = sinon.spy();
|
const parserSpy = sinon.spy();
|
||||||
const el = /** @type {FormatClass & inputNodeHost} */ (await fixture(html`
|
const el = /** @type {FormatClass} */ (await fixture(html`
|
||||||
<${elem} .parser="${parserSpy}">
|
<${elem} .parser="${parserSpy}">
|
||||||
<input slot="input" value="${generatedValue}">
|
<input slot="input" value="${generatedValue}">
|
||||||
</${elem}>
|
</${elem}>
|
||||||
|
|
@ -335,7 +334,7 @@ export function runFormatMixinSuite(customConfig) {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('will not return Unparseable when empty strings are inputted', async () => {
|
it('will not return Unparseable when empty strings are inputted', async () => {
|
||||||
const el = /** @type {FormatClass & inputNodeHost} */ (await fixture(html`
|
const el = /** @type {FormatClass} */ (await fixture(html`
|
||||||
<${elem}>
|
<${elem}>
|
||||||
<input slot="input" value="string">
|
<input slot="input" value="string">
|
||||||
</${elem}>
|
</${elem}>
|
||||||
|
|
@ -359,7 +358,7 @@ export function runFormatMixinSuite(customConfig) {
|
||||||
toggleValue: true,
|
toggleValue: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const el = /** @type {FormatClass & inputNodeHost & validateHost} */ (await fixture(html`
|
const el = /** @type {FormatClass} */ (await fixture(html`
|
||||||
<${elem} .formatter=${formatterSpy}>
|
<${elem} .formatter=${formatterSpy}>
|
||||||
<input slot="input" value="${generatedViewValue}">
|
<input slot="input" value="${generatedViewValue}">
|
||||||
</${elem}>
|
</${elem}>
|
||||||
|
|
@ -371,7 +370,7 @@ export function runFormatMixinSuite(customConfig) {
|
||||||
// Setting hasError = true is not enough if the element has errorValidators (uses ValidateMixin)
|
// Setting hasError = true is not enough if the element has errorValidators (uses ValidateMixin)
|
||||||
// that set hasError back to false when the user input is mimicked.
|
// that set hasError back to false when the user input is mimicked.
|
||||||
|
|
||||||
/* const AlwaysInvalid = class extends Validator {
|
const AlwaysInvalid = class extends Validator {
|
||||||
static get validatorName() {
|
static get validatorName() {
|
||||||
return 'AlwaysInvalid';
|
return 'AlwaysInvalid';
|
||||||
}
|
}
|
||||||
|
|
@ -379,9 +378,9 @@ export function runFormatMixinSuite(customConfig) {
|
||||||
execute() {
|
execute() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}; */
|
};
|
||||||
// @ts-ignore // FIXME: remove this ignore after Validator class is typed
|
|
||||||
// el.validators = [new AlwaysInvalid()];
|
el.validators = [new AlwaysInvalid()];
|
||||||
mimicUserInput(el, generatedViewValueAlt);
|
mimicUserInput(el, generatedViewValueAlt);
|
||||||
|
|
||||||
expect(formatterSpy.callCount).to.equal(1);
|
expect(formatterSpy.callCount).to.equal(1);
|
||||||
|
|
@ -398,19 +397,21 @@ export function runFormatMixinSuite(customConfig) {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Unparseable values', () => {
|
describe('Unparseable values', () => {
|
||||||
// it('should convert to Unparseable when wrong value inputted by user', async () => {
|
it('should convert to Unparseable when wrong value inputted by user', async () => {
|
||||||
// const el = /** @type {FormatClass & inputNodeHost} */ (await fixture(html`
|
const el = /** @type {FormatClass} */ (await fixture(html`
|
||||||
// <${elem} .parser=${viewValue => Number(viewValue) || undefined}
|
<${elem} .parser=${
|
||||||
// >
|
/** @param {string} viewValue */ viewValue => Number(viewValue) || undefined
|
||||||
// <input slot="input">
|
}
|
||||||
// </${elem}>
|
>
|
||||||
// `));
|
<input slot="input">
|
||||||
// mimicUserInput(el, 'test');
|
</${elem}>
|
||||||
// expect(el.modelValue).to.be.an.instanceof(Unparseable);
|
`));
|
||||||
// });
|
mimicUserInput(el, 'test');
|
||||||
|
expect(el.modelValue).to.be.an.instanceof(Unparseable);
|
||||||
|
});
|
||||||
|
|
||||||
it('should preserve the viewValue when not parseable', async () => {
|
it('should preserve the viewValue when not parseable', async () => {
|
||||||
const el = /** @type {FormatClass & inputNodeHost} */ (await fixture(html`
|
const el = /** @type {FormatClass} */ (await fixture(html`
|
||||||
<${elem}
|
<${elem}
|
||||||
.parser=${/** @param {string} viewValue */ viewValue => Number(viewValue) || undefined}
|
.parser=${/** @param {string} viewValue */ viewValue => Number(viewValue) || undefined}
|
||||||
>
|
>
|
||||||
|
|
@ -422,17 +423,17 @@ export function runFormatMixinSuite(customConfig) {
|
||||||
expect(el.value).to.equal('test');
|
expect(el.value).to.equal('test');
|
||||||
});
|
});
|
||||||
|
|
||||||
// it('should display the viewValue when modelValue is of type Unparseable', async () => {
|
it('should display the viewValue when modelValue is of type Unparseable', async () => {
|
||||||
// const el = /** @type {FormatClass} */ (await fixture(html`
|
const el = /** @type {FormatClass} */ (await fixture(html`
|
||||||
// <${elem}
|
<${elem}
|
||||||
// .parser=${/** @param {string} viewValue */ viewValue => Number(viewValue) || undefined}
|
.parser=${/** @param {string} viewValue */ viewValue => Number(viewValue) || undefined}
|
||||||
// >
|
>
|
||||||
// <input slot="input">
|
<input slot="input">
|
||||||
// </${elem}>
|
</${elem}>
|
||||||
// `));
|
`));
|
||||||
// el.modelValue = new Unparseable('foo');
|
el.modelValue = new Unparseable('foo');
|
||||||
// expect(el.value).to.equal('foo');
|
expect(el.value).to.equal('foo');
|
||||||
// });
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ import {
|
||||||
} from '@open-wc/testing';
|
} from '@open-wc/testing';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
import { InteractionStateMixin } from '../src/InteractionStateMixin.js';
|
import { InteractionStateMixin } from '../src/InteractionStateMixin.js';
|
||||||
|
import { ValidateMixin } from '../src/validate/ValidateMixin.js';
|
||||||
|
import { MinLength } from '../src/validate/validators/StringValidators.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {{tagString?: string, allowedModelValueTypes?: Array.<ArrayConstructor | ObjectConstructor | NumberConstructor | BooleanConstructor | StringConstructor | DateConstructor>}} [customConfig]
|
* @param {{tagString?: string, allowedModelValueTypes?: Array.<ArrayConstructor | ObjectConstructor | NumberConstructor | BooleanConstructor | StringConstructor | DateConstructor>}} [customConfig]
|
||||||
|
|
@ -22,7 +24,8 @@ export function runInteractionStateMixinSuite(customConfig) {
|
||||||
};
|
};
|
||||||
|
|
||||||
describe(`InteractionStateMixin`, async () => {
|
describe(`InteractionStateMixin`, async () => {
|
||||||
class IState extends InteractionStateMixin(LitElement) {
|
// @ts-expect-error base constructors same return type
|
||||||
|
class IState extends InteractionStateMixin(ValidateMixin(LitElement)) {
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
this.tabIndex = 0;
|
this.tabIndex = 0;
|
||||||
|
|
@ -207,8 +210,41 @@ export function runInteractionStateMixinSuite(customConfig) {
|
||||||
expect(el.prefilled).to.be.true;
|
expect(el.prefilled).to.be.true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Validation integration with states', () => {
|
||||||
|
it('has .shouldShowFeedbackFor indicating for which type to show messages', async () => {
|
||||||
|
const el = /** @type {IState} */ (await fixture(html`
|
||||||
|
<${tag}></${tag}>
|
||||||
|
`));
|
||||||
|
expect(el.shouldShowFeedbackFor).to.deep.equal([]);
|
||||||
|
el.submitted = true;
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(el.shouldShowFeedbackFor).to.deep.equal(['error']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps the feedback component in sync', async () => {
|
||||||
|
const el = /** @type {IState} */ (await fixture(html`
|
||||||
|
<${tag} .validators=${[new MinLength(3)]}></${tag}>
|
||||||
|
`));
|
||||||
|
await el.updateComplete;
|
||||||
|
await el.feedbackComplete;
|
||||||
|
expect(el._feedbackNode.feedbackData).to.deep.equal([]);
|
||||||
|
|
||||||
|
// has error but does not show/forward to component as showCondition is not met
|
||||||
|
el.modelValue = '1';
|
||||||
|
await el.updateComplete;
|
||||||
|
await el.feedbackComplete;
|
||||||
|
expect(el._feedbackNode.feedbackData).to.deep.equal([]);
|
||||||
|
|
||||||
|
el.submitted = true;
|
||||||
|
await el.updateComplete;
|
||||||
|
await el.feedbackComplete;
|
||||||
|
expect(el._feedbackNode.feedbackData?.length).to.equal(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('SubClassers', () => {
|
describe('SubClassers', () => {
|
||||||
it('can override the `_leaveEvent`', async () => {
|
it('can override the `_leaveEvent`', async () => {
|
||||||
|
// @ts-expect-error base constructor same return type
|
||||||
class IStateCustomBlur extends InteractionStateMixin(LitElement) {
|
class IStateCustomBlur extends InteractionStateMixin(LitElement) {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,9 @@ import {
|
||||||
AsyncAlwaysValid,
|
AsyncAlwaysValid,
|
||||||
} from '../test-helpers.js';
|
} from '../test-helpers.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {{tagString?: string | null, lightDom?: string}} [customConfig]
|
||||||
|
*/
|
||||||
export function runValidateMixinSuite(customConfig) {
|
export function runValidateMixinSuite(customConfig) {
|
||||||
const cfg = {
|
const cfg = {
|
||||||
tagString: null,
|
tagString: null,
|
||||||
|
|
@ -24,32 +27,19 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const lightDom = cfg.lightDom || '';
|
const lightDom = cfg.lightDom || '';
|
||||||
const tagString =
|
|
||||||
cfg.tagString ||
|
|
||||||
defineCE(
|
|
||||||
class extends ValidateMixin(LitElement) {
|
|
||||||
static get properties() {
|
|
||||||
return { modelValue: String };
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
|
// @ts-expect-error base constructor same return type
|
||||||
|
class ValidateElement extends ValidateMixin(LitElement) {
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
const inputNode = document.createElement('input');
|
||||||
|
inputNode.slot = 'input';
|
||||||
|
this.appendChild(inputNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagString = cfg.tagString || defineCE(ValidateElement);
|
||||||
const tag = unsafeStatic(tagString);
|
const tag = unsafeStatic(tagString);
|
||||||
const withInputTagString =
|
|
||||||
cfg.tagString ||
|
|
||||||
defineCE(
|
|
||||||
class extends ValidateMixin(LitElement) {
|
|
||||||
connectedCallback() {
|
|
||||||
super.connectedCallback();
|
|
||||||
this.appendChild(document.createElement('input'));
|
|
||||||
}
|
|
||||||
|
|
||||||
get _inputNode() {
|
|
||||||
return this.querySelector('input');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const withInputTag = unsafeStatic(withInputTagString);
|
|
||||||
|
|
||||||
describe('ValidateMixin', () => {
|
describe('ValidateMixin', () => {
|
||||||
/**
|
/**
|
||||||
|
|
@ -78,10 +68,11 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
it('throws and console.errors if adding not Validator instances to the validators array', async () => {
|
it('throws and console.errors if adding not Validator instances to the validators array', async () => {
|
||||||
// we throw and console error as constructor throw are not visible to the end user
|
// we throw and console error as constructor throw are not visible to the end user
|
||||||
const stub = sinon.stub(console, 'error');
|
const stub = sinon.stub(console, 'error');
|
||||||
const el = await fixture(html`<${tag}></${tag}>`);
|
const el = /** @type {ValidateElement} */ (await fixture(html`<${tag}></${tag}>`));
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
'Validators array only accepts class instances of Validator. Type "array" found.';
|
'Validators array only accepts class instances of Validator. Type "array" found.';
|
||||||
expect(() => {
|
expect(() => {
|
||||||
|
// @ts-expect-error putting the wrong value on purpose
|
||||||
el.validators = [[new Required()]];
|
el.validators = [[new Required()]];
|
||||||
}).to.throw(errorMessage);
|
}).to.throw(errorMessage);
|
||||||
expect(stub.args[0][0]).to.equal(errorMessage);
|
expect(stub.args[0][0]).to.equal(errorMessage);
|
||||||
|
|
@ -89,6 +80,7 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
const errorMessage2 =
|
const errorMessage2 =
|
||||||
'Validators array only accepts class instances of Validator. Type "string" found.';
|
'Validators array only accepts class instances of Validator. Type "string" found.';
|
||||||
expect(() => {
|
expect(() => {
|
||||||
|
// @ts-expect-error because we purposely put a wrong type
|
||||||
el.validators = ['required'];
|
el.validators = ['required'];
|
||||||
}).to.throw(errorMessage2);
|
}).to.throw(errorMessage2);
|
||||||
expect(stub.args[1][0]).to.equal(errorMessage2);
|
expect(stub.args[1][0]).to.equal(errorMessage2);
|
||||||
|
|
@ -110,7 +102,7 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
return 'MajorValidator';
|
return 'MajorValidator';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const el = await fixture(html`<${tag}></${tag}>`);
|
const el = /** @type {ValidateElement} */ (await fixture(html`<${tag}></${tag}>`));
|
||||||
expect(() => {
|
expect(() => {
|
||||||
el.validators = [new MajorValidator()];
|
el.validators = [new MajorValidator()];
|
||||||
}).to.throw(errorMessage);
|
}).to.throw(errorMessage);
|
||||||
|
|
@ -120,21 +112,21 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('validates on initialization (once form field has bootstrapped/initialized)', async () => {
|
it('validates on initialization (once form field has bootstrapped/initialized)', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||||
<${tag}
|
<${tag}
|
||||||
.validators=${[new Required()]}
|
.validators=${[new Required()]}
|
||||||
>${lightDom}</${tag}>
|
>${lightDom}</${tag}>
|
||||||
`);
|
`));
|
||||||
expect(el.hasFeedbackFor).to.deep.equal(['error']);
|
expect(el.hasFeedbackFor).to.deep.equal(['error']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('revalidates when ".modelValue" changes', async () => {
|
it('revalidates when ".modelValue" changes', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||||
<${tag}
|
<${tag}
|
||||||
.validators=${[new AlwaysValid()]}
|
.validators=${[new AlwaysValid()]}
|
||||||
.modelValue=${'myValue'}
|
.modelValue=${'myValue'}
|
||||||
>${lightDom}</${tag}>
|
>${lightDom}</${tag}>
|
||||||
`);
|
`));
|
||||||
|
|
||||||
const validateSpy = sinon.spy(el, 'validate');
|
const validateSpy = sinon.spy(el, 'validate');
|
||||||
el.modelValue = 'x';
|
el.modelValue = 'x';
|
||||||
|
|
@ -142,12 +134,12 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('revalidates when ".validators" changes', async () => {
|
it('revalidates when ".validators" changes', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||||
<${tag}
|
<${tag}
|
||||||
.validators=${[new AlwaysValid()]}
|
.validators=${[new AlwaysValid()]}
|
||||||
.modelValue=${'myValue'}
|
.modelValue=${'myValue'}
|
||||||
>${lightDom}</${tag}>
|
>${lightDom}</${tag}>
|
||||||
`);
|
`));
|
||||||
|
|
||||||
const validateSpy = sinon.spy(el, 'validate');
|
const validateSpy = sinon.spy(el, 'validate');
|
||||||
el.validators = [new MinLength(3)];
|
el.validators = [new MinLength(3)];
|
||||||
|
|
@ -155,12 +147,12 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('clears current results when ".modelValue" changes', async () => {
|
it('clears current results when ".modelValue" changes', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||||
<${tag}
|
<${tag}
|
||||||
.validators=${[new AlwaysValid()]}
|
.validators=${[new AlwaysValid()]}
|
||||||
.modelValue=${'myValue'}
|
.modelValue=${'myValue'}
|
||||||
>${lightDom}</${tag}>
|
>${lightDom}</${tag}>
|
||||||
`);
|
`));
|
||||||
|
|
||||||
const clearSpy = sinon.spy(el, '__clearValidationResults');
|
const clearSpy = sinon.spy(el, '__clearValidationResults');
|
||||||
const validateSpy = sinon.spy(el, 'validate');
|
const validateSpy = sinon.spy(el, 'validate');
|
||||||
|
|
@ -180,9 +172,9 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
it('firstly checks for empty values', async () => {
|
it('firstly checks for empty values', async () => {
|
||||||
const alwaysValid = new AlwaysValid();
|
const alwaysValid = new AlwaysValid();
|
||||||
const alwaysValidExecuteSpy = sinon.spy(alwaysValid, 'execute');
|
const alwaysValidExecuteSpy = sinon.spy(alwaysValid, 'execute');
|
||||||
const el = await fixture(html`
|
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||||
<${tag} .validators=${[alwaysValid]}>${lightDom}</${tag}>
|
<${tag} .validators=${[alwaysValid]}>${lightDom}</${tag}>
|
||||||
`);
|
`));
|
||||||
const isEmptySpy = sinon.spy(el, '__isEmpty');
|
const isEmptySpy = sinon.spy(el, '__isEmpty');
|
||||||
const validateSpy = sinon.spy(el, 'validate');
|
const validateSpy = sinon.spy(el, 'validate');
|
||||||
el.modelValue = '';
|
el.modelValue = '';
|
||||||
|
|
@ -197,9 +189,9 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('secondly checks for synchronous Validators: creates RegularValidationResult', async () => {
|
it('secondly checks for synchronous Validators: creates RegularValidationResult', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||||
<${tag} .validators=${[new AlwaysValid()]}>${lightDom}</${tag}>
|
<${tag} .validators=${[new AlwaysValid()]}>${lightDom}</${tag}>
|
||||||
`);
|
`));
|
||||||
const isEmptySpy = sinon.spy(el, '__isEmpty');
|
const isEmptySpy = sinon.spy(el, '__isEmpty');
|
||||||
const syncSpy = sinon.spy(el, '__executeSyncValidators');
|
const syncSpy = sinon.spy(el, '__executeSyncValidators');
|
||||||
el.modelValue = 'nonEmpty';
|
el.modelValue = 'nonEmpty';
|
||||||
|
|
@ -207,11 +199,11 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('thirdly schedules asynchronous Validators: creates RegularValidationResult', async () => {
|
it('thirdly schedules asynchronous Validators: creates RegularValidationResult', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||||
<${tag} .validators=${[new AlwaysValid(), new AsyncAlwaysValid()]}>
|
<${tag} .validators=${[new AlwaysValid(), new AsyncAlwaysValid()]}>
|
||||||
${lightDom}
|
${lightDom}
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`);
|
`));
|
||||||
const syncSpy = sinon.spy(el, '__executeSyncValidators');
|
const syncSpy = sinon.spy(el, '__executeSyncValidators');
|
||||||
const asyncSpy = sinon.spy(el, '__executeAsyncValidators');
|
const asyncSpy = sinon.spy(el, '__executeAsyncValidators');
|
||||||
el.modelValue = 'nonEmpty';
|
el.modelValue = 'nonEmpty';
|
||||||
|
|
@ -225,12 +217,12 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let el = await fixture(html`
|
let el = /** @type {ValidateElement} */ (await fixture(html`
|
||||||
<${tag}
|
<${tag}
|
||||||
.validators=${[new AlwaysValid(), new MyResult()]}>
|
.validators=${[new AlwaysValid(), new MyResult()]}>
|
||||||
${lightDom}
|
${lightDom}
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`);
|
`));
|
||||||
|
|
||||||
const syncSpy = sinon.spy(el, '__executeSyncValidators');
|
const syncSpy = sinon.spy(el, '__executeSyncValidators');
|
||||||
const resultSpy2 = sinon.spy(el, '__executeResultValidators');
|
const resultSpy2 = sinon.spy(el, '__executeResultValidators');
|
||||||
|
|
@ -257,11 +249,11 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
|
|
||||||
describe('Finalization', () => {
|
describe('Finalization', () => {
|
||||||
it('fires private "validate-performed" event on every cycle', async () => {
|
it('fires private "validate-performed" event on every cycle', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||||
<${tag} .validators=${[new AlwaysValid(), new AsyncAlwaysInvalid()]}>
|
<${tag} .validators=${[new AlwaysValid(), new AsyncAlwaysInvalid()]}>
|
||||||
${lightDom}
|
${lightDom}
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`);
|
`));
|
||||||
const cbSpy = sinon.spy();
|
const cbSpy = sinon.spy();
|
||||||
el.addEventListener('validate-performed', cbSpy);
|
el.addEventListener('validate-performed', cbSpy);
|
||||||
el.modelValue = 'nonEmpty';
|
el.modelValue = 'nonEmpty';
|
||||||
|
|
@ -269,11 +261,11 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('resolves ".validateComplete" Promise', async () => {
|
it('resolves ".validateComplete" Promise', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||||
<${tag} .validators=${[new AsyncAlwaysInvalid()]}>
|
<${tag} .validators=${[new AsyncAlwaysInvalid()]}>
|
||||||
${lightDom}
|
${lightDom}
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`);
|
`));
|
||||||
el.modelValue = 'nonEmpty';
|
el.modelValue = 'nonEmpty';
|
||||||
const validateResolveSpy = sinon.spy(el, '__validateCompleteResolve');
|
const validateResolveSpy = sinon.spy(el, '__validateCompleteResolve');
|
||||||
await el.validateComplete;
|
await el.validateComplete;
|
||||||
|
|
@ -284,8 +276,16 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
|
|
||||||
describe('Validator Integration', () => {
|
describe('Validator Integration', () => {
|
||||||
class IsCat extends Validator {
|
class IsCat extends Validator {
|
||||||
|
/**
|
||||||
|
* @param {...any} args
|
||||||
|
*/
|
||||||
constructor(...args) {
|
constructor(...args) {
|
||||||
super(...args);
|
super(...args);
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} modelValue
|
||||||
|
* @param {{number: number}} [param]
|
||||||
|
*/
|
||||||
this.execute = (modelValue, param) => {
|
this.execute = (modelValue, param) => {
|
||||||
const validateString = param && param.number ? `cat${param.number}` : 'cat';
|
const validateString = param && param.number ? `cat${param.number}` : 'cat';
|
||||||
const showError = modelValue !== validateString;
|
const showError = modelValue !== validateString;
|
||||||
|
|
@ -299,6 +299,9 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
}
|
}
|
||||||
|
|
||||||
class OtherValidator extends Validator {
|
class OtherValidator extends Validator {
|
||||||
|
/**
|
||||||
|
* @param {...any} args
|
||||||
|
*/
|
||||||
constructor(...args) {
|
constructor(...args) {
|
||||||
super(...args);
|
super(...args);
|
||||||
this.execute = () => true;
|
this.execute = () => true;
|
||||||
|
|
@ -318,6 +321,7 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
.modelValue=${'model'}
|
.modelValue=${'model'}
|
||||||
>${lightDom}</${tag}>
|
>${lightDom}</${tag}>
|
||||||
`);
|
`);
|
||||||
|
// @ts-expect-error weird sinon type error..
|
||||||
expect(otherValidatorSpy.calledWith('model')).to.be.true;
|
expect(otherValidatorSpy.calledWith('model')).to.be.true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -330,6 +334,7 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
.modelValue=${new Unparseable('view')}
|
.modelValue=${new Unparseable('view')}
|
||||||
>${lightDom}</${tag}>
|
>${lightDom}</${tag}>
|
||||||
`);
|
`);
|
||||||
|
// @ts-expect-error weird sinon type error..
|
||||||
expect(otherValidatorSpy.calledWith('view')).to.be.true;
|
expect(otherValidatorSpy.calledWith('view')).to.be.true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -355,13 +360,14 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
.modelValue=${'cat'}
|
.modelValue=${'cat'}
|
||||||
>${lightDom}</${tag}>
|
>${lightDom}</${tag}>
|
||||||
`);
|
`);
|
||||||
|
// @ts-expect-error another sinon type problem
|
||||||
expect(executeSpy.args[0][2].node).to.equal(el);
|
expect(executeSpy.args[0][2].node).to.equal(el);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Validators will not be called on empty values', async () => {
|
it('Validators will not be called on empty values', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||||
<${tag} .validators=${[new IsCat()]}>${lightDom}</${tag}>
|
<${tag} .validators=${[new IsCat()]}>${lightDom}</${tag}>
|
||||||
`);
|
`));
|
||||||
|
|
||||||
el.modelValue = 'cat';
|
el.modelValue = 'cat';
|
||||||
expect(el.validationStates.error.IsCat).to.be.undefined;
|
expect(el.validationStates.error.IsCat).to.be.undefined;
|
||||||
|
|
@ -374,12 +380,12 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
it('Validators get retriggered on parameter change', async () => {
|
it('Validators get retriggered on parameter change', async () => {
|
||||||
const isCatValidator = new IsCat('Felix');
|
const isCatValidator = new IsCat('Felix');
|
||||||
const catSpy = sinon.spy(isCatValidator, 'execute');
|
const catSpy = sinon.spy(isCatValidator, 'execute');
|
||||||
const el = await fixture(html`
|
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||||
<${tag}
|
<${tag}
|
||||||
.validators=${[isCatValidator]}
|
.validators=${[isCatValidator]}
|
||||||
.modelValue=${'cat'}
|
.modelValue=${'cat'}
|
||||||
>${lightDom}</${tag}>
|
>${lightDom}</${tag}>
|
||||||
`);
|
`));
|
||||||
el.modelValue = 'cat';
|
el.modelValue = 'cat';
|
||||||
expect(catSpy.callCount).to.equal(1);
|
expect(catSpy.callCount).to.equal(1);
|
||||||
isCatValidator.param = 'Garfield';
|
isCatValidator.param = 'Garfield';
|
||||||
|
|
@ -388,7 +394,9 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Async Validator Integration', () => {
|
describe('Async Validator Integration', () => {
|
||||||
|
/** @type {Promise<any>} */
|
||||||
let asyncVPromise;
|
let asyncVPromise;
|
||||||
|
/** @type {function} */
|
||||||
let asyncVResolve;
|
let asyncVResolve;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|
@ -421,36 +429,36 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
// default execution trigger is keyup (think of password availability backend)
|
// default execution trigger is keyup (think of password availability backend)
|
||||||
// can configure execution trigger (blur, etc?)
|
// can configure execution trigger (blur, etc?)
|
||||||
it('handles "execute" functions returning promises', async () => {
|
it('handles "execute" functions returning promises', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||||
<${tag}
|
<${tag}
|
||||||
.modelValue=${'dog'}
|
.modelValue=${'dog'}
|
||||||
.validators=${[new IsAsyncCat()]}>
|
.validators=${[new IsAsyncCat()]}>
|
||||||
${lightDom}
|
${lightDom}
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`);
|
`));
|
||||||
|
|
||||||
const validator = el.validators[0];
|
const validator = el.validators[0];
|
||||||
expect(validator instanceof Validator).to.be.true;
|
expect(validator instanceof Validator).to.be.true;
|
||||||
expect(el.hasFeedbackFor).to.deep.equal([]);
|
expect(el.hasFeedbackFor).to.deep.equal([]);
|
||||||
asyncVResolve();
|
asyncVResolve();
|
||||||
await aTimeout();
|
await aTimeout(0);
|
||||||
expect(el.hasFeedbackFor).to.deep.equal(['error']);
|
expect(el.hasFeedbackFor).to.deep.equal(['error']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sets ".isPending/[is-pending]" when validation is in progress', async () => {
|
it('sets ".isPending/[is-pending]" when validation is in progress', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||||
<${tag} .modelValue=${'dog'}>${lightDom}</${tag}>
|
<${tag} .modelValue=${'dog'}>${lightDom}</${tag}>
|
||||||
`);
|
`));
|
||||||
expect(el.isPending).to.be.false;
|
expect(el.isPending).to.be.false;
|
||||||
expect(el.hasAttribute('is-pending')).to.be.false;
|
expect(el.hasAttribute('is-pending')).to.be.false;
|
||||||
|
|
||||||
el.validators = [new IsAsyncCat()];
|
el.validators = [new IsAsyncCat()];
|
||||||
expect(el.isPending).to.be.true;
|
expect(el.isPending).to.be.true;
|
||||||
await aTimeout();
|
await aTimeout(0);
|
||||||
expect(el.hasAttribute('is-pending')).to.be.true;
|
expect(el.hasAttribute('is-pending')).to.be.true;
|
||||||
|
|
||||||
asyncVResolve();
|
asyncVResolve();
|
||||||
await aTimeout();
|
await aTimeout(0);
|
||||||
expect(el.isPending).to.be.false;
|
expect(el.isPending).to.be.false;
|
||||||
expect(el.hasAttribute('is-pending')).to.be.false;
|
expect(el.hasAttribute('is-pending')).to.be.false;
|
||||||
});
|
});
|
||||||
|
|
@ -460,11 +468,11 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
const asyncV = new IsAsyncCat();
|
const asyncV = new IsAsyncCat();
|
||||||
const asyncVExecuteSpy = sinon.spy(asyncV, 'execute');
|
const asyncVExecuteSpy = sinon.spy(asyncV, 'execute');
|
||||||
|
|
||||||
const el = await fixture(html`
|
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||||
<${tag} .modelValue=${'dog'}>
|
<${tag} .modelValue=${'dog'}>
|
||||||
${lightDom}
|
${lightDom}
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`);
|
`));
|
||||||
// debounce started
|
// debounce started
|
||||||
el.validators = [asyncV];
|
el.validators = [asyncV];
|
||||||
expect(asyncVExecuteSpy.called).to.equal(0);
|
expect(asyncVExecuteSpy.called).to.equal(0);
|
||||||
|
|
@ -473,7 +481,7 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
expect(asyncVExecuteSpy.called).to.equal(1);
|
expect(asyncVExecuteSpy.called).to.equal(1);
|
||||||
|
|
||||||
// New validation cycle. Now change modelValue inbetween, so validation is retriggered.
|
// New validation cycle. Now change modelValue inbetween, so validation is retriggered.
|
||||||
asyncVExecuteSpy.reset();
|
asyncVExecuteSpy.resetHistory();
|
||||||
el.modelValue = 'dogger';
|
el.modelValue = 'dogger';
|
||||||
expect(asyncVExecuteSpy.called).to.equal(0);
|
expect(asyncVExecuteSpy.called).to.equal(0);
|
||||||
el.modelValue = 'doggerer';
|
el.modelValue = 'doggerer';
|
||||||
|
|
@ -488,13 +496,13 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
|
|
||||||
it.skip('cancels and reschedules async validation on ".modelValue" change', async () => {
|
it.skip('cancels and reschedules async validation on ".modelValue" change', async () => {
|
||||||
const asyncV = new IsAsyncCat();
|
const asyncV = new IsAsyncCat();
|
||||||
const asyncVAbortSpy = sinon.spy(asyncV, 'abort');
|
const asyncVAbortSpy = sinon.spy(asyncV, 'abortExecution');
|
||||||
|
|
||||||
const el = await fixture(html`
|
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||||
<${tag} .modelValue=${'dog'}>
|
<${tag} .modelValue=${'dog'}>
|
||||||
${lightDom}
|
${lightDom}
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`);
|
`));
|
||||||
// debounce started
|
// debounce started
|
||||||
el.validators = [asyncV];
|
el.validators = [asyncV];
|
||||||
expect(asyncVAbortSpy.called).to.equal(0);
|
expect(asyncVAbortSpy.called).to.equal(0);
|
||||||
|
|
@ -508,16 +516,19 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
const asyncV = new IsAsyncCat();
|
const asyncV = new IsAsyncCat();
|
||||||
const asyncVExecuteSpy = sinon.spy(asyncV, 'execute');
|
const asyncVExecuteSpy = sinon.spy(asyncV, 'execute');
|
||||||
|
|
||||||
const el = await fixture(html`
|
const el = /** @type {ValidateElement & { isFocused: boolean }} */ (await fixture(html`
|
||||||
<${tag}
|
<${tag}
|
||||||
.isFocused=${true}
|
.isFocused=${true}
|
||||||
.modelValue=${'dog'}
|
.modelValue=${'dog'}
|
||||||
.validators=${[asyncV]}
|
.validators=${[asyncV]}
|
||||||
.asyncValidateOn=${({ formControl }) => !formControl.isFocused}
|
.asyncValidateOn=${
|
||||||
|
/** @param {{ formControl: { isFocused: boolean } }} opts */ ({ formControl }) =>
|
||||||
|
!formControl.isFocused
|
||||||
|
}
|
||||||
>
|
>
|
||||||
${lightDom}
|
${lightDom}
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`);
|
`));
|
||||||
|
|
||||||
expect(asyncVExecuteSpy.called).to.equal(0);
|
expect(asyncVExecuteSpy.called).to.equal(0);
|
||||||
el.isFocused = false;
|
el.isFocused = false;
|
||||||
|
|
@ -527,35 +538,38 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('ResultValidator Integration', () => {
|
describe('ResultValidator Integration', () => {
|
||||||
let MySuccessResultValidator;
|
const MySuccessResultValidator = class extends ResultValidator {
|
||||||
let withSuccessTagString;
|
/**
|
||||||
let withSuccessTag;
|
* @param {...any} args
|
||||||
|
*/
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args);
|
||||||
|
this.type = 'success';
|
||||||
|
}
|
||||||
|
|
||||||
before(() => {
|
/**
|
||||||
MySuccessResultValidator = class extends ResultValidator {
|
*
|
||||||
constructor(...args) {
|
* @param {{ regularValidationResult: Validator[], prevValidationResult: Validator[]}} param0
|
||||||
super(...args);
|
*/
|
||||||
this.type = 'success';
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
executeOnResults({ regularValidationResult, prevValidationResult }) {
|
||||||
|
const errorOrWarning = /** @param {Validator} v */ v =>
|
||||||
|
v.type === 'error' || v.type === 'warning';
|
||||||
|
const hasErrorOrWarning = !!regularValidationResult.filter(errorOrWarning).length;
|
||||||
|
const prevHadErrorOrWarning = !!prevValidationResult.filter(errorOrWarning).length;
|
||||||
|
return !hasErrorOrWarning && prevHadErrorOrWarning;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const withSuccessTagString = defineCE(
|
||||||
|
// @ts-expect-error
|
||||||
|
class extends ValidateMixin(LitElement) {
|
||||||
|
static get validationTypes() {
|
||||||
|
return [...super.validationTypes, 'success'];
|
||||||
}
|
}
|
||||||
|
},
|
||||||
// eslint-disable-next-line class-methods-use-this
|
);
|
||||||
executeOnResults({ regularValidationResult, prevValidationResult }) {
|
const withSuccessTag = unsafeStatic(withSuccessTagString);
|
||||||
const errorOrWarning = v => v.type === 'error' || v.type === 'warning';
|
|
||||||
const hasErrorOrWarning = !!regularValidationResult.filter(errorOrWarning).length;
|
|
||||||
const prevHadErrorOrWarning = !!prevValidationResult.filter(errorOrWarning).length;
|
|
||||||
return !hasErrorOrWarning && prevHadErrorOrWarning;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
withSuccessTagString = defineCE(
|
|
||||||
class extends ValidateMixin(LitElement) {
|
|
||||||
static get validationTypes() {
|
|
||||||
return [...super.validationTypes, 'success'];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
withSuccessTag = unsafeStatic(withSuccessTagString);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('calls ResultValidators after regular validators', async () => {
|
it('calls ResultValidators after regular validators', async () => {
|
||||||
const resultValidator = new MySuccessResultValidator();
|
const resultValidator = new MySuccessResultValidator();
|
||||||
|
|
@ -589,12 +603,12 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
const resultValidator = new MySuccessResultValidator();
|
const resultValidator = new MySuccessResultValidator();
|
||||||
const resultValidateSpy = sinon.spy(resultValidator, 'executeOnResults');
|
const resultValidateSpy = sinon.spy(resultValidator, 'executeOnResults');
|
||||||
|
|
||||||
const el = await fixture(html`
|
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||||
<${withSuccessTag}
|
<${withSuccessTag}
|
||||||
.validators=${[new MinLength(3), resultValidator]}
|
.validators=${[new MinLength(3), resultValidator]}
|
||||||
.modelValue=${'myValue'}
|
.modelValue=${'myValue'}
|
||||||
>${lightDom}</${withSuccessTag}>
|
>${lightDom}</${withSuccessTag}>
|
||||||
`);
|
`));
|
||||||
const prevValidationResult = el.__prevValidationResult;
|
const prevValidationResult = el.__prevValidationResult;
|
||||||
const regularValidationResult = [
|
const regularValidationResult = [
|
||||||
...el.__syncValidationResult,
|
...el.__syncValidationResult,
|
||||||
|
|
@ -619,26 +633,26 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
const validator = new AlwaysInvalid();
|
const validator = new AlwaysInvalid();
|
||||||
const resultV = new AlwaysInvalidResult();
|
const resultV = new AlwaysInvalidResult();
|
||||||
|
|
||||||
const el = await fixture(html`
|
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||||
<${tag}
|
<${tag}
|
||||||
.validators=${[validator, resultV]}
|
.validators=${[validator, resultV]}
|
||||||
.modelValue=${'myValue'}
|
.modelValue=${'myValue'}
|
||||||
>${lightDom}</${tag}>
|
>${lightDom}</${tag}>
|
||||||
`);
|
`));
|
||||||
|
|
||||||
const /** @type {TotalValidationResult} */ totalValidationResult = el.__validationResult;
|
const totalValidationResult = el.__validationResult;
|
||||||
expect(totalValidationResult).to.eql([resultV, validator]);
|
expect(totalValidationResult).to.eql([resultV, validator]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Required Validator integration', () => {
|
describe('Required Validator integration', () => {
|
||||||
it('will result in erroneous state when form control is empty', async () => {
|
it('will result in erroneous state when form control is empty', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||||
<${tag}
|
<${tag}
|
||||||
.validators=${[new Required()]}
|
.validators=${[new Required()]}
|
||||||
.modelValue=${''}
|
.modelValue=${''}
|
||||||
>${lightDom}</${tag}>
|
>${lightDom}</${tag}>
|
||||||
`);
|
`));
|
||||||
expect(el.validationStates.error.Required).to.be.true;
|
expect(el.validationStates.error.Required).to.be.true;
|
||||||
expect(el.hasFeedbackFor).to.deep.equal(['error']);
|
expect(el.hasFeedbackFor).to.deep.equal(['error']);
|
||||||
|
|
||||||
|
|
@ -648,13 +662,13 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls private ".__isEmpty" by default', async () => {
|
it('calls private ".__isEmpty" by default', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||||
<${tag}
|
<${tag}
|
||||||
.validators=${[new Required()]}
|
.validators=${[new Required()]}
|
||||||
.modelValue=${''}
|
.modelValue=${''}
|
||||||
>${lightDom}</${tag}>
|
>${lightDom}</${tag}>
|
||||||
`);
|
`));
|
||||||
const validator = el.validators.find(v => v instanceof Required);
|
const validator = /** @type {Validator} */ (el.validators.find(v => v instanceof Required));
|
||||||
const executeSpy = sinon.spy(validator, 'execute');
|
const executeSpy = sinon.spy(validator, 'execute');
|
||||||
const privateIsEmptySpy = sinon.spy(el, '__isEmpty');
|
const privateIsEmptySpy = sinon.spy(el, '__isEmpty');
|
||||||
el.modelValue = null;
|
el.modelValue = null;
|
||||||
|
|
@ -663,21 +677,22 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls "._isEmpty" when provided (useful for different modelValues)', async () => {
|
it('calls "._isEmpty" when provided (useful for different modelValues)', async () => {
|
||||||
const customRequiredTagString = defineCE(
|
// @ts-expect-error base constructor same return type
|
||||||
class extends ValidateMixin(LitElement) {
|
class _isEmptyValidate extends ValidateMixin(LitElement) {
|
||||||
_isEmpty() {
|
_isEmpty() {
|
||||||
return this.modelValue.model === '';
|
// @ts-expect-error
|
||||||
}
|
return this.modelValue.model === '';
|
||||||
},
|
}
|
||||||
);
|
}
|
||||||
|
const customRequiredTagString = defineCE(_isEmptyValidate);
|
||||||
const customRequiredTag = unsafeStatic(customRequiredTagString);
|
const customRequiredTag = unsafeStatic(customRequiredTagString);
|
||||||
|
|
||||||
const el = await fixture(html`
|
const el = /** @type {_isEmptyValidate} */ (await fixture(html`
|
||||||
<${customRequiredTag}
|
<${customRequiredTag}
|
||||||
.validators=${[new Required()]}
|
.validators=${[new Required()]}
|
||||||
.modelValue=${{ model: 'foo' }}
|
.modelValue=${{ model: 'foo' }}
|
||||||
>${lightDom}</${customRequiredTag}>
|
>${lightDom}</${customRequiredTag}>
|
||||||
`);
|
`));
|
||||||
|
|
||||||
const providedIsEmptySpy = sinon.spy(el, '_isEmpty');
|
const providedIsEmptySpy = sinon.spy(el, '_isEmpty');
|
||||||
el.modelValue = { model: '' };
|
el.modelValue = { model: '' };
|
||||||
|
|
@ -688,32 +703,34 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
it('prevents other Validators from being called when input is empty', async () => {
|
it('prevents other Validators from being called when input is empty', async () => {
|
||||||
const alwaysInvalid = new AlwaysInvalid();
|
const alwaysInvalid = new AlwaysInvalid();
|
||||||
const alwaysInvalidSpy = sinon.spy(alwaysInvalid, 'execute');
|
const alwaysInvalidSpy = sinon.spy(alwaysInvalid, 'execute');
|
||||||
const el = await fixture(html`
|
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||||
<${tag}
|
<${tag}
|
||||||
.validators=${[new Required(), alwaysInvalid]}
|
.validators=${[new Required(), alwaysInvalid]}
|
||||||
.modelValue=${''}
|
.modelValue=${''}
|
||||||
>${lightDom}</${tag}>
|
>${lightDom}</${tag}>
|
||||||
`);
|
`));
|
||||||
expect(alwaysInvalidSpy.callCount).to.equal(0); // __isRequired returned false (invalid)
|
expect(alwaysInvalidSpy.callCount).to.equal(0); // __isRequired returned false (invalid)
|
||||||
el.modelValue = 'foo';
|
el.modelValue = 'foo';
|
||||||
expect(alwaysInvalidSpy.callCount).to.equal(1); // __isRequired returned true (valid)
|
expect(alwaysInvalidSpy.callCount).to.equal(1); // __isRequired returned true (valid)
|
||||||
});
|
});
|
||||||
|
|
||||||
it('adds [aria-required="true"] to "._inputNode"', async () => {
|
it('adds [aria-required="true"] to "._inputNode"', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||||
<${withInputTag}
|
<${tag}
|
||||||
.validators=${[new Required()]}
|
.validators=${[new Required()]}
|
||||||
.modelValue=${''}
|
.modelValue=${''}
|
||||||
>${lightDom}</${withInputTag}>
|
>${lightDom}</${tag}>
|
||||||
`);
|
`));
|
||||||
expect(el._inputNode.getAttribute('aria-required')).to.equal('true');
|
console.log(el._inputNode);
|
||||||
|
expect(el._inputNode?.getAttribute('aria-required')).to.equal('true');
|
||||||
el.validators = [];
|
el.validators = [];
|
||||||
expect(el._inputNode.getAttribute('aria-required')).to.be.null;
|
expect(el._inputNode?.getAttribute('aria-required')).to.be.null;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Default (preconfigured) Validators', () => {
|
describe('Default (preconfigured) Validators', () => {
|
||||||
const preconfTagString = defineCE(
|
const preconfTagString = defineCE(
|
||||||
|
// @ts-expect-error base constructor same return type
|
||||||
class extends ValidateMixin(LitElement) {
|
class extends ValidateMixin(LitElement) {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
@ -724,11 +741,11 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
const preconfTag = unsafeStatic(preconfTagString);
|
const preconfTag = unsafeStatic(preconfTagString);
|
||||||
|
|
||||||
it('can be stored for custom inputs', async () => {
|
it('can be stored for custom inputs', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||||
<${preconfTag}
|
<${preconfTag}
|
||||||
.validators=${[new MinLength(3)]}
|
.validators=${[new MinLength(3)]}
|
||||||
.modelValue=${'12'}
|
.modelValue=${'12'}
|
||||||
></${preconfTag}>`);
|
></${preconfTag}>`));
|
||||||
|
|
||||||
expect(el.validationStates.error.AlwaysInvalid).to.be.true;
|
expect(el.validationStates.error.AlwaysInvalid).to.be.true;
|
||||||
expect(el.validationStates.error.MinLength).to.be.true;
|
expect(el.validationStates.error.MinLength).to.be.true;
|
||||||
|
|
@ -736,6 +753,7 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
|
|
||||||
it('can be altered by App Developers', async () => {
|
it('can be altered by App Developers', async () => {
|
||||||
const altPreconfTagString = defineCE(
|
const altPreconfTagString = defineCE(
|
||||||
|
// @ts-expect-error base constructor same return type
|
||||||
class extends ValidateMixin(LitElement) {
|
class extends ValidateMixin(LitElement) {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
@ -745,10 +763,10 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
);
|
);
|
||||||
const altPreconfTag = unsafeStatic(altPreconfTagString);
|
const altPreconfTag = unsafeStatic(altPreconfTagString);
|
||||||
|
|
||||||
const el = await fixture(html`
|
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||||
<${altPreconfTag}
|
<${altPreconfTag}
|
||||||
.modelValue=${'12'}
|
.modelValue=${'12'}
|
||||||
></${altPreconfTag}>`);
|
></${altPreconfTag}>`));
|
||||||
|
|
||||||
expect(el.validationStates.error.MinLength).to.be.true;
|
expect(el.validationStates.error.MinLength).to.be.true;
|
||||||
el.defaultValidators[0].param = 2;
|
el.defaultValidators[0].param = 2;
|
||||||
|
|
@ -756,10 +774,10 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can be requested via "._allValidators" getter', async () => {
|
it('can be requested via "._allValidators" getter', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||||
<${preconfTag}
|
<${preconfTag}
|
||||||
.validators=${[new MinLength(3)]}
|
.validators=${[new MinLength(3)]}
|
||||||
></${preconfTag}>`);
|
></${preconfTag}>`));
|
||||||
|
|
||||||
expect(el.validators.length).to.equal(1);
|
expect(el.validators.length).to.equal(1);
|
||||||
expect(el.defaultValidators.length).to.equal(1);
|
expect(el.defaultValidators.length).to.equal(1);
|
||||||
|
|
@ -776,11 +794,11 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
|
|
||||||
describe('State storage and reflection', () => {
|
describe('State storage and reflection', () => {
|
||||||
it('stores validity of individual Validators in ".validationStates.error[validator.validatorName]"', async () => {
|
it('stores validity of individual Validators in ".validationStates.error[validator.validatorName]"', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||||
<${tag}
|
<${tag}
|
||||||
.modelValue=${'a'}
|
.modelValue=${'a'}
|
||||||
.validators=${[new MinLength(3), new AlwaysInvalid()]}
|
.validators=${[new MinLength(3), new AlwaysInvalid()]}
|
||||||
>${lightDom}</${tag}>`);
|
>${lightDom}</${tag}>`));
|
||||||
|
|
||||||
expect(el.validationStates.error.MinLength).to.be.true;
|
expect(el.validationStates.error.MinLength).to.be.true;
|
||||||
expect(el.validationStates.error.AlwaysInvalid).to.be.true;
|
expect(el.validationStates.error.AlwaysInvalid).to.be.true;
|
||||||
|
|
@ -791,11 +809,11 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('removes "non active" states whenever modelValue becomes undefined', async () => {
|
it('removes "non active" states whenever modelValue becomes undefined', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||||
<${tag}
|
<${tag}
|
||||||
.validators=${[new MinLength(3)]}
|
.validators=${[new MinLength(3)]}
|
||||||
>${lightDom}</${tag}>
|
>${lightDom}</${tag}>
|
||||||
`);
|
`));
|
||||||
el.modelValue = 'a';
|
el.modelValue = 'a';
|
||||||
expect(el.hasFeedbackFor).to.deep.equal(['error']);
|
expect(el.hasFeedbackFor).to.deep.equal(['error']);
|
||||||
expect(el.validationStates.error).to.not.eql({});
|
expect(el.validationStates.error).to.not.eql({});
|
||||||
|
|
@ -807,11 +825,11 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
|
|
||||||
it('clears current validation results when validators array updated', async () => {
|
it('clears current validation results when validators array updated', async () => {
|
||||||
const validators = [new Required()];
|
const validators = [new Required()];
|
||||||
const el = await fixture(html`
|
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||||
<${tag}
|
<${tag}
|
||||||
.validators=${validators}
|
.validators=${validators}
|
||||||
>${lightDom}</${tag}>
|
>${lightDom}</${tag}>
|
||||||
`);
|
`));
|
||||||
expect(el.hasFeedbackFor).to.deep.equal(['error']);
|
expect(el.hasFeedbackFor).to.deep.equal(['error']);
|
||||||
expect(el.validationStates.error).to.eql({ Required: true });
|
expect(el.validationStates.error).to.eql({ Required: true });
|
||||||
|
|
||||||
|
|
@ -827,13 +845,13 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
describe('Events', () => {
|
describe('Events', () => {
|
||||||
it('fires "showsFeedbackForChanged" event async after feedbackData got synced to feedbackElement', async () => {
|
it('fires "showsFeedbackForChanged" event async after feedbackData got synced to feedbackElement', async () => {
|
||||||
const spy = sinon.spy();
|
const spy = sinon.spy();
|
||||||
const el = await fixture(html`
|
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||||
<${tag}
|
<${tag}
|
||||||
.submitted=${true}
|
.submitted=${true}
|
||||||
.validators=${[new MinLength(7)]}
|
.validators=${[new MinLength(7)]}
|
||||||
@showsFeedbackForChanged=${spy};
|
@showsFeedbackForChanged=${spy};
|
||||||
>${lightDom}</${tag}>
|
>${lightDom}</${tag}>
|
||||||
`);
|
`));
|
||||||
el.modelValue = 'a';
|
el.modelValue = 'a';
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(spy).to.have.callCount(1);
|
expect(spy).to.have.callCount(1);
|
||||||
|
|
@ -849,13 +867,13 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
|
|
||||||
it('fires "showsFeedbackFor{type}Changed" event async when type visibility changed', async () => {
|
it('fires "showsFeedbackFor{type}Changed" event async when type visibility changed', async () => {
|
||||||
const spy = sinon.spy();
|
const spy = sinon.spy();
|
||||||
const el = await fixture(html`
|
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||||
<${tag}
|
<${tag}
|
||||||
.submitted=${true}
|
.submitted=${true}
|
||||||
.validators=${[new MinLength(7)]}
|
.validators=${[new MinLength(7)]}
|
||||||
@showsFeedbackForErrorChanged=${spy};
|
@showsFeedbackForErrorChanged=${spy};
|
||||||
>${lightDom}</${tag}>
|
>${lightDom}</${tag}>
|
||||||
`);
|
`));
|
||||||
el.modelValue = 'a';
|
el.modelValue = 'a';
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(spy).to.have.callCount(1);
|
expect(spy).to.have.callCount(1);
|
||||||
|
|
@ -873,19 +891,25 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
|
|
||||||
describe('Accessibility', () => {
|
describe('Accessibility', () => {
|
||||||
it.skip('calls "._inputNode.setCustomValidity(errorMessage)"', async () => {
|
it.skip('calls "._inputNode.setCustomValidity(errorMessage)"', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||||
<${tag}
|
<${tag}
|
||||||
.modelValue=${'123'}
|
.modelValue=${'123'}
|
||||||
.validators=${[new MinLength(3, { message: 'foo' })]}>
|
.validators=${[new MinLength(3, { message: 'foo' })]}>
|
||||||
<input slot="input">
|
<input slot="input">
|
||||||
</${tag}>`);
|
</${tag}>`));
|
||||||
const spy = sinon.spy(el.inputElement, 'setCustomValidity');
|
|
||||||
el.modelValue = '';
|
if (el._inputNode) {
|
||||||
expect(spy.callCount).to.be(1);
|
// @ts-expect-error
|
||||||
expect(el.validationMessage).to.be('foo');
|
const spy = sinon.spy(el._inputNode, 'setCustomValidity');
|
||||||
el.modelValue = '123';
|
el.modelValue = '';
|
||||||
expect(spy.callCount).to.be(2);
|
expect(spy.callCount).to.equal(1);
|
||||||
expect(el.validationMessage).to.be('');
|
// @ts-expect-error needs to be rewritten to new API
|
||||||
|
expect(el.validationMessage).to.be('foo');
|
||||||
|
el.modelValue = '123';
|
||||||
|
expect(spy.callCount).to.equal(2);
|
||||||
|
// @ts-expect-error needs to be rewritten to new API
|
||||||
|
expect(el.validationMessage).to.be('');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: check with open a11y issues and find best solution here
|
// TODO: check with open a11y issues and find best solution here
|
||||||
|
|
@ -895,6 +919,7 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
|
|
||||||
describe('Extensibility: Custom Validator types', () => {
|
describe('Extensibility: Custom Validator types', () => {
|
||||||
const customTypeTagString = defineCE(
|
const customTypeTagString = defineCE(
|
||||||
|
// @ts-expect-error base constructor same return type
|
||||||
class extends ValidateMixin(LitElement) {
|
class extends ValidateMixin(LitElement) {
|
||||||
static get validationTypes() {
|
static get validationTypes() {
|
||||||
return [...super.validationTypes, 'x', 'y'];
|
return [...super.validationTypes, 'x', 'y'];
|
||||||
|
|
@ -904,7 +929,7 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
const customTypeTag = unsafeStatic(customTypeTagString);
|
const customTypeTag = unsafeStatic(customTypeTagString);
|
||||||
|
|
||||||
it('supports additional validationTypes in .hasFeedbackFor', async () => {
|
it('supports additional validationTypes in .hasFeedbackFor', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||||
<${customTypeTag}
|
<${customTypeTag}
|
||||||
.validators=${[
|
.validators=${[
|
||||||
new MinLength(2, { type: 'x' }),
|
new MinLength(2, { type: 'x' }),
|
||||||
|
|
@ -913,7 +938,7 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
]}
|
]}
|
||||||
.modelValue=${'1234'}
|
.modelValue=${'1234'}
|
||||||
>${lightDom}</${customTypeTag}>
|
>${lightDom}</${customTypeTag}>
|
||||||
`);
|
`));
|
||||||
expect(el.hasFeedbackFor).to.deep.equal([]);
|
expect(el.hasFeedbackFor).to.deep.equal([]);
|
||||||
|
|
||||||
el.modelValue = '123'; // triggers y
|
el.modelValue = '123'; // triggers y
|
||||||
|
|
@ -927,7 +952,7 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('supports additional validationTypes in .validationStates', async () => {
|
it('supports additional validationTypes in .validationStates', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||||
<${customTypeTag}
|
<${customTypeTag}
|
||||||
.validators=${[
|
.validators=${[
|
||||||
new MinLength(2, { type: 'x' }),
|
new MinLength(2, { type: 'x' }),
|
||||||
|
|
@ -936,7 +961,7 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
]}
|
]}
|
||||||
.modelValue=${'1234'}
|
.modelValue=${'1234'}
|
||||||
>${lightDom}</${customTypeTag}>
|
>${lightDom}</${customTypeTag}>
|
||||||
`);
|
`));
|
||||||
expect(el.validationStates).to.eql({
|
expect(el.validationStates).to.eql({
|
||||||
x: {},
|
x: {},
|
||||||
error: {},
|
error: {},
|
||||||
|
|
@ -965,33 +990,9 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// we no longer have a flag for when the error message got displayed - not really useful right?
|
|
||||||
it.skip('only shows highest prio "has{Type}Visible" flag by default', async () => {
|
|
||||||
const el = await fixture(html`
|
|
||||||
<${customTypeTag}
|
|
||||||
.validators=${[
|
|
||||||
new MinLength(2, { type: 'x' }),
|
|
||||||
new MinLength(3), // implicit 'error type'
|
|
||||||
new MinLength(4, { type: 'y' }),
|
|
||||||
]}
|
|
||||||
.modelValue=${'1234'}
|
|
||||||
>${lightDom}</${customTypeTag}>
|
|
||||||
`);
|
|
||||||
expect(el.hasYVisible).to.be.false;
|
|
||||||
expect(el.hasErrorVisible).to.be.false;
|
|
||||||
expect(el.hasXVisible).to.be.false;
|
|
||||||
|
|
||||||
el.modelValue = '1'; // triggers y, x and error
|
|
||||||
await el.feedbackComplete;
|
|
||||||
expect(el.hasYVisible).to.be.false;
|
|
||||||
// Only shows message with highest prio (determined in el.constructor.validationTypes)
|
|
||||||
expect(el.hasErrorVisible).to.be.true;
|
|
||||||
expect(el.hasXVisible).to.be.false;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('orders feedback based on provided "validationTypes"', async () => {
|
it('orders feedback based on provided "validationTypes"', async () => {
|
||||||
// we set submitted to always show error message in the test
|
// we set submitted to always show error message in the test
|
||||||
const el = await fixture(html`
|
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||||
<${customTypeTag}
|
<${customTypeTag}
|
||||||
.submitted=${true}
|
.submitted=${true}
|
||||||
._visibleMessagesAmount=${Infinity}
|
._visibleMessagesAmount=${Infinity}
|
||||||
|
|
@ -1002,16 +1003,17 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
]}
|
]}
|
||||||
.modelValue=${'1'}
|
.modelValue=${'1'}
|
||||||
>${lightDom}</${customTypeTag}>
|
>${lightDom}</${customTypeTag}>
|
||||||
`);
|
`));
|
||||||
await el.feedbackComplete;
|
await el.feedbackComplete;
|
||||||
|
|
||||||
const resultOrder = el._feedbackNode.feedbackData.map(v => v.type);
|
const feedbackNode = /** @type {import('../src/validate/LionValidationFeedback').LionValidationFeedback} */ (el._feedbackNode);
|
||||||
|
const resultOrder = feedbackNode.feedbackData?.map(v => v.type);
|
||||||
expect(resultOrder).to.deep.equal(['error', 'x', 'y']);
|
expect(resultOrder).to.deep.equal(['error', 'x', 'y']);
|
||||||
|
|
||||||
el.modelValue = '12';
|
el.modelValue = '12';
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
await el.feedbackComplete;
|
await el.feedbackComplete;
|
||||||
const resultOrder2 = el._feedbackNode.feedbackData.map(v => v.type);
|
const resultOrder2 = feedbackNode.feedbackData?.map(v => v.type);
|
||||||
expect(resultOrder2).to.deep.equal(['error', 'y']);
|
expect(resultOrder2).to.deep.equal(['error', 'y']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1025,31 +1027,31 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
describe('Subclassers', () => {
|
describe('Subclassers', () => {
|
||||||
describe('Adding new Validator types', () => {
|
describe('Adding new Validator types', () => {
|
||||||
it('can add helpers for validation types', async () => {
|
it('can add helpers for validation types', async () => {
|
||||||
const elTagString = defineCE(
|
// @ts-expect-error base constructor same return type
|
||||||
class extends ValidateMixin(LitElement) {
|
class ValidateHasX extends ValidateMixin(LitElement) {
|
||||||
static get validationTypes() {
|
static get validationTypes() {
|
||||||
return [...super.validationTypes, 'x'];
|
return [...super.validationTypes, 'x'];
|
||||||
}
|
}
|
||||||
|
|
||||||
get hasX() {
|
get hasX() {
|
||||||
return this.hasFeedbackFor.includes('x');
|
return this.hasFeedbackFor.includes('x');
|
||||||
}
|
}
|
||||||
|
|
||||||
get hasXVisible() {
|
get hasXVisible() {
|
||||||
return this.showsFeedbackFor.includes('x');
|
return this.showsFeedbackFor.includes('x');
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
);
|
const elTagString = defineCE(ValidateHasX);
|
||||||
const elTag = unsafeStatic(elTagString);
|
const elTag = unsafeStatic(elTagString);
|
||||||
|
|
||||||
// we set submitted to always show errors
|
// we set submitted to always show errors
|
||||||
const el = await fixture(html`
|
const el = /** @type {ValidateHasX} */ (await fixture(html`
|
||||||
<${elTag}
|
<${elTag}
|
||||||
.submitted=${true}
|
.submitted=${true}
|
||||||
.validators=${[new MinLength(2, { type: 'x' })]}
|
.validators=${[new MinLength(2, { type: 'x' })]}
|
||||||
.modelValue=${'1'}
|
.modelValue=${'1'}
|
||||||
>${lightDom}</${elTag}>
|
>${lightDom}</${elTag}>
|
||||||
`);
|
`));
|
||||||
await el.feedbackComplete;
|
await el.feedbackComplete;
|
||||||
expect(el.hasX).to.be.true;
|
expect(el.hasX).to.be.true;
|
||||||
expect(el.hasXVisible).to.be.true;
|
expect(el.hasXVisible).to.be.true;
|
||||||
|
|
@ -1062,17 +1064,27 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can fire custom events if needed', async () => {
|
it('can fire custom events if needed', async () => {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string[]} array1
|
||||||
|
* @param {string[]} array2
|
||||||
|
*/
|
||||||
function arrayDiff(array1 = [], array2 = []) {
|
function arrayDiff(array1 = [], array2 = []) {
|
||||||
return array1
|
return array1
|
||||||
.filter(x => !array2.includes(x))
|
.filter(x => !array2.includes(x))
|
||||||
.concat(array2.filter(x => !array1.includes(x)));
|
.concat(array2.filter(x => !array1.includes(x)));
|
||||||
}
|
}
|
||||||
const elTagString = defineCE(
|
const elTagString = defineCE(
|
||||||
|
// @ts-expect-error base constructor same return type
|
||||||
class extends ValidateMixin(LitElement) {
|
class extends ValidateMixin(LitElement) {
|
||||||
static get validationTypes() {
|
static get validationTypes() {
|
||||||
return [...super.validationTypes, 'x'];
|
return [...super.validationTypes, 'x'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} name
|
||||||
|
* @param {?} oldValue
|
||||||
|
*/
|
||||||
updateSync(name, oldValue) {
|
updateSync(name, oldValue) {
|
||||||
super.updateSync(name, oldValue);
|
super.updateSync(name, oldValue);
|
||||||
if (name === 'hasFeedbackFor') {
|
if (name === 'hasFeedbackFor') {
|
||||||
|
|
@ -1088,14 +1100,14 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
|
|
||||||
const spy = sinon.spy();
|
const spy = sinon.spy();
|
||||||
// we set prefilled to always show errors
|
// we set prefilled to always show errors
|
||||||
const el = await fixture(html`
|
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||||
<${elTag}
|
<${elTag}
|
||||||
.prefilled=${true}
|
.prefilled=${true}
|
||||||
@hasFeedbackForXChanged=${spy}
|
@hasFeedbackForXChanged=${spy}
|
||||||
.validators=${[new MinLength(2, { type: 'x' })]}
|
.validators=${[new MinLength(2, { type: 'x' })]}
|
||||||
.modelValue=${'1'}
|
.modelValue=${'1'}
|
||||||
>${lightDom}</${elTag}>
|
>${lightDom}</${elTag}>
|
||||||
`);
|
`));
|
||||||
expect(spy).to.have.callCount(1);
|
expect(spy).to.have.callCount(1);
|
||||||
el.modelValue = '1';
|
el.modelValue = '1';
|
||||||
expect(spy).to.have.callCount(1);
|
expect(spy).to.have.callCount(1);
|
||||||
|
|
@ -1112,6 +1124,7 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
// TODO: add this test on FormControl layer
|
// TODO: add this test on FormControl layer
|
||||||
it('reconsiders feedback visibility when interaction states changed', async () => {
|
it('reconsiders feedback visibility when interaction states changed', async () => {
|
||||||
const elTagString = defineCE(
|
const elTagString = defineCE(
|
||||||
|
// @ts-expect-error base constructor same return type
|
||||||
class extends ValidateMixin(LitElement) {
|
class extends ValidateMixin(LitElement) {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
|
|
@ -1129,12 +1142,12 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const elTag = unsafeStatic(elTagString);
|
const elTag = unsafeStatic(elTagString);
|
||||||
const el = await fixture(html`
|
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||||
<${elTag}
|
<${elTag}
|
||||||
.validators=${[new AlwaysInvalid()]}
|
.validators=${[new AlwaysInvalid()]}
|
||||||
.modelValue=${'myValue'}
|
.modelValue=${'myValue'}
|
||||||
>${lightDom}</${elTag}>
|
>${lightDom}</${elTag}>
|
||||||
`);
|
`));
|
||||||
|
|
||||||
const spy = sinon.spy(el, '_updateFeedbackComponent');
|
const spy = sinon.spy(el, '_updateFeedbackComponent');
|
||||||
let counter = 0;
|
let counter = 0;
|
||||||
|
|
@ -1153,43 +1166,6 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
expect(spy.callCount).to.equal(counter);
|
expect(spy.callCount).to.equal(counter);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// already shown how to add it yourself
|
|
||||||
it.skip('supports multiple "has{Type}Visible" flags', async () => {
|
|
||||||
const elTagString = defineCE(
|
|
||||||
class extends ValidateMixin(LitElement) {
|
|
||||||
static get validationTypes() {
|
|
||||||
return [...super.validationTypes, 'x', 'y'];
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this._visibleMessagesAmount = Infinity;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const elTag = unsafeStatic(elTagString);
|
|
||||||
|
|
||||||
const el = await fixture(html`
|
|
||||||
<${elTag}
|
|
||||||
.validators=${[
|
|
||||||
new MinLength(2, { type: 'x' }),
|
|
||||||
new MinLength(3), // implicit 'error type'
|
|
||||||
new MinLength(4, { type: 'y' }),
|
|
||||||
]}
|
|
||||||
.modelValue=${'1234'}
|
|
||||||
>${lightDom}</${elTag}>
|
|
||||||
`);
|
|
||||||
expect(el.hasYVisible).to.be.false;
|
|
||||||
expect(el.hasErrorVisible).to.be.false;
|
|
||||||
expect(el.hasXVisible).to.be.false;
|
|
||||||
|
|
||||||
el.modelValue = '1'; // triggers y
|
|
||||||
await el.feedbackComplete;
|
|
||||||
expect(el.hasYVisible).to.be.true;
|
|
||||||
expect(el.hasErrorVisible).to.be.true;
|
|
||||||
expect(el.hasXVisible).to.be.true; // only shows message with highest
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Changing feedback messages globally', () => {
|
describe('Changing feedback messages globally', () => {
|
||||||
|
|
|
||||||
|
|
@ -8,84 +8,67 @@ import { AlwaysInvalid } from '../test-helpers.js';
|
||||||
|
|
||||||
export function runValidateMixinFeedbackPart() {
|
export function runValidateMixinFeedbackPart() {
|
||||||
describe('Validity Feedback', () => {
|
describe('Validity Feedback', () => {
|
||||||
let tagString;
|
|
||||||
let tag;
|
|
||||||
let ContainsLowercaseA;
|
|
||||||
const lightDom = '';
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
localizeTearDown();
|
localizeTearDown();
|
||||||
});
|
});
|
||||||
|
|
||||||
before(() => {
|
// @ts-expect-error base constructor same return type
|
||||||
tagString = defineCE(
|
class ValidateElement extends ValidateMixin(LitElement) {
|
||||||
class extends ValidateMixin(LitElement) {
|
connectedCallback() {
|
||||||
static get properties() {
|
super.connectedCallback();
|
||||||
return {
|
const inputNode = document.createElement('input');
|
||||||
modelValue: { type: String },
|
inputNode.slot = 'input';
|
||||||
submitted: { type: Boolean },
|
this.appendChild(inputNode);
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
const tagString = defineCE(ValidateElement);
|
||||||
super.connectedCallback();
|
const tag = unsafeStatic(tagString);
|
||||||
this.appendChild(document.createElement('input'));
|
|
||||||
}
|
|
||||||
|
|
||||||
get _inputNode() {
|
class ContainsLowercaseA extends Validator {
|
||||||
return this.querySelector('input');
|
static get validatorName() {
|
||||||
}
|
return 'ContainsLowercaseA';
|
||||||
},
|
|
||||||
);
|
|
||||||
tag = unsafeStatic(tagString);
|
|
||||||
|
|
||||||
ContainsLowercaseA = class extends Validator {
|
|
||||||
static get validatorName() {
|
|
||||||
return 'ContainsLowercaseA';
|
|
||||||
}
|
|
||||||
|
|
||||||
execute(modelValue) {
|
|
||||||
const hasError = !modelValue.includes('a');
|
|
||||||
return hasError;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
class ContainsCat extends Validator {
|
|
||||||
static get validatorName() {
|
|
||||||
return 'ContainsCat';
|
|
||||||
}
|
|
||||||
|
|
||||||
execute(modelValue) {
|
|
||||||
const hasError = !modelValue.includes('cat');
|
|
||||||
return hasError;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AlwaysInvalid.getMessage = () => 'Message for AlwaysInvalid';
|
/**
|
||||||
MinLength.getMessage = () =>
|
* @param {?} modelValue
|
||||||
localize.locale === 'de-DE' ? 'Nachricht für MinLength' : 'Message for MinLength';
|
*/
|
||||||
ContainsLowercaseA.getMessage = () => 'Message for ContainsLowercaseA';
|
execute(modelValue) {
|
||||||
ContainsCat.getMessage = () => 'Message for ContainsCat';
|
const hasError = !modelValue.includes('a');
|
||||||
});
|
return hasError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ContainsCat extends Validator {
|
||||||
|
static get validatorName() {
|
||||||
|
return 'ContainsCat';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {?} modelValue
|
||||||
|
*/
|
||||||
|
execute(modelValue) {
|
||||||
|
const hasError = !modelValue.includes('cat');
|
||||||
|
return hasError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AlwaysInvalid.getMessage = async () => 'Message for AlwaysInvalid';
|
||||||
|
MinLength.getMessage = async () =>
|
||||||
|
localize.locale === 'de-DE' ? 'Nachricht für MinLength' : 'Message for MinLength';
|
||||||
|
ContainsLowercaseA.getMessage = async () => 'Message for ContainsLowercaseA';
|
||||||
|
ContainsCat.getMessage = async () => 'Message for ContainsCat';
|
||||||
|
|
||||||
|
const lightDom = '';
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
sinon.restore();
|
sinon.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has .shouldShowFeedbackFor indicating for which type to show messages', async () => {
|
|
||||||
const el = await fixture(html`
|
|
||||||
<${tag}>${lightDom}</${tag}>
|
|
||||||
`);
|
|
||||||
expect(el.shouldShowFeedbackFor).to.deep.equal([]);
|
|
||||||
el.submitted = true;
|
|
||||||
await el.updateComplete;
|
|
||||||
expect(el.shouldShowFeedbackFor).to.deep.equal(['error']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('has .showsFeedbackFor indicating for which type it actually shows messages', async () => {
|
it('has .showsFeedbackFor indicating for which type it actually shows messages', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||||
<${tag} submitted .validators=${[new MinLength(3)]}>${lightDom}</${tag}>
|
<${tag} submitted .validators=${[new MinLength(3)]}>${lightDom}</${tag}>
|
||||||
`);
|
`));
|
||||||
|
|
||||||
el.modelValue = 'a';
|
el.modelValue = 'a';
|
||||||
await el.feedbackComplete;
|
await el.feedbackComplete;
|
||||||
|
|
@ -97,22 +80,22 @@ export function runValidateMixinFeedbackPart() {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('reflects .showsFeedbackFor as attribute joined with "," to be used as a style hook', async () => {
|
it('reflects .showsFeedbackFor as attribute joined with "," to be used as a style hook', async () => {
|
||||||
const elTagString = defineCE(
|
// @ts-expect-error base constructors same return type
|
||||||
class extends ValidateMixin(LitElement) {
|
class ValidateElementCustomTypes extends ValidateMixin(LitElement) {
|
||||||
static get validationTypes() {
|
static get validationTypes() {
|
||||||
return [...super.validationTypes, 'x'];
|
return [...super.validationTypes, 'x'];
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
);
|
const elTagString = defineCE(ValidateElementCustomTypes);
|
||||||
const elTag = unsafeStatic(elTagString);
|
const elTag = unsafeStatic(elTagString);
|
||||||
const el = await fixture(html`
|
const el = /** @type {ValidateElementCustomTypes} */ (await fixture(html`
|
||||||
<${elTag}
|
<${elTag}
|
||||||
.submitted=${true}
|
.submitted=${true}
|
||||||
.validators=${[
|
.validators=${[
|
||||||
new MinLength(2, { type: 'x' }),
|
new MinLength(2, { type: 'x' }),
|
||||||
new MinLength(3, { type: 'error' }),
|
new MinLength(3, { type: 'error' }),
|
||||||
]}>${lightDom}</${elTag}>
|
]}>${lightDom}</${elTag}>
|
||||||
`);
|
`));
|
||||||
|
|
||||||
el.modelValue = '1';
|
el.modelValue = '1';
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
|
|
@ -134,30 +117,30 @@ export function runValidateMixinFeedbackPart() {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('passes a message to the "._feedbackNode"', async () => {
|
it('passes a message to the "._feedbackNode"', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||||
<${tag}
|
<${tag}
|
||||||
.submitted=${true}
|
.submitted=${true}
|
||||||
.modelValue=${'cat'}
|
.modelValue=${'cat'}
|
||||||
>${lightDom}</${tag}>
|
>${lightDom}</${tag}>
|
||||||
`);
|
`));
|
||||||
expect(el._feedbackNode.feedbackData).to.deep.equal([]);
|
expect(el._feedbackNode.feedbackData).to.deep.equal([]);
|
||||||
el.validators = [new AlwaysInvalid()];
|
el.validators = [new AlwaysInvalid()];
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
await el.feedbackComplete;
|
await el.feedbackComplete;
|
||||||
expect(el._feedbackNode.feedbackData[0].message).to.equal('Message for AlwaysInvalid');
|
expect(el._feedbackNode.feedbackData?.[0].message).to.equal('Message for AlwaysInvalid');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has configurable feedback visibility hook', async () => {
|
it('has configurable feedback visibility hook', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||||
<${tag}
|
<${tag}
|
||||||
.submitted=${true}
|
.submitted=${true}
|
||||||
.modelValue=${'cat'}
|
.modelValue=${'cat'}
|
||||||
.validators=${[new AlwaysInvalid()]}
|
.validators=${[new AlwaysInvalid()]}
|
||||||
>${lightDom}</${tag}>
|
>${lightDom}</${tag}>
|
||||||
`);
|
`));
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
await el.feedbackComplete;
|
await el.feedbackComplete;
|
||||||
expect(el._feedbackNode.feedbackData[0].message).to.equal('Message for AlwaysInvalid');
|
expect(el._feedbackNode.feedbackData?.[0].message).to.equal('Message for AlwaysInvalid');
|
||||||
el._prioritizeAndFilterFeedback = () => []; // filter out all errors
|
el._prioritizeAndFilterFeedback = () => []; // filter out all errors
|
||||||
await el.validate();
|
await el.validate();
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
|
|
@ -166,20 +149,21 @@ export function runValidateMixinFeedbackPart() {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('writes prioritized result to "._feedbackNode" based on Validator order', async () => {
|
it('writes prioritized result to "._feedbackNode" based on Validator order', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||||
<${tag}
|
<${tag}
|
||||||
.submitted=${true}
|
.submitted=${true}
|
||||||
.modelValue=${'cat'}
|
.modelValue=${'cat'}
|
||||||
.validators=${[new AlwaysInvalid(), new MinLength(4)]}
|
.validators=${[new AlwaysInvalid(), new MinLength(4)]}
|
||||||
>${lightDom}</${tag}>
|
>${lightDom}</${tag}>
|
||||||
`);
|
`));
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
await el.feedbackComplete;
|
await el.feedbackComplete;
|
||||||
expect(el._feedbackNode.feedbackData[0].message).to.equal('Message for AlwaysInvalid');
|
expect(el._feedbackNode.feedbackData?.[0].message).to.equal('Message for AlwaysInvalid');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders validation result to "._feedbackNode" when async messages are resolved', async () => {
|
it('renders validation result to "._feedbackNode" when async messages are resolved', async () => {
|
||||||
let unlockMessage;
|
/** @type {function} FIXME: find better way to type this kind of pattern */
|
||||||
|
let unlockMessage = () => {};
|
||||||
const messagePromise = new Promise(resolve => {
|
const messagePromise = new Promise(resolve => {
|
||||||
unlockMessage = resolve;
|
unlockMessage = resolve;
|
||||||
});
|
});
|
||||||
|
|
@ -189,23 +173,26 @@ export function runValidateMixinFeedbackPart() {
|
||||||
return 'this ends up in "._feedbackNode"';
|
return 'this ends up in "._feedbackNode"';
|
||||||
};
|
};
|
||||||
|
|
||||||
const el = await fixture(html`
|
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||||
<${tag}
|
<${tag}
|
||||||
.submitted=${true}
|
.submitted=${true}
|
||||||
.modelValue=${'cat'}
|
.modelValue=${'cat'}
|
||||||
.validators=${[new AlwaysInvalid()]}
|
.validators=${[new AlwaysInvalid()]}
|
||||||
>${lightDom}</${tag}>
|
>${lightDom}</${tag}>
|
||||||
`);
|
`));
|
||||||
expect(el._feedbackNode.feedbackData).to.be.undefined;
|
expect(el._feedbackNode.feedbackData).to.be.undefined;
|
||||||
unlockMessage();
|
unlockMessage();
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
await el.feedbackComplete;
|
await el.feedbackComplete;
|
||||||
expect(el._feedbackNode.feedbackData[0].message).to.equal('this ends up in "._feedbackNode"');
|
expect(el._feedbackNode.feedbackData?.[0].message).to.equal(
|
||||||
|
'this ends up in "._feedbackNode"',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// N.B. this replaces the 'config.hideFeedback' option we had before...
|
// N.B. this replaces the 'config.hideFeedback' option we had before...
|
||||||
it('renders empty result when Validator.getMessage() returns "null"', async () => {
|
it('renders empty result when Validator.getMessage() returns "null"', async () => {
|
||||||
let unlockMessage;
|
/** @type {function} FIXME: find better way to type this kind of pattern */
|
||||||
|
let unlockMessage = () => {};
|
||||||
const messagePromise = new Promise(resolve => {
|
const messagePromise = new Promise(resolve => {
|
||||||
unlockMessage = resolve;
|
unlockMessage = resolve;
|
||||||
});
|
});
|
||||||
|
|
@ -215,47 +202,63 @@ export function runValidateMixinFeedbackPart() {
|
||||||
return 'this ends up in "._feedbackNode"';
|
return 'this ends up in "._feedbackNode"';
|
||||||
};
|
};
|
||||||
|
|
||||||
const el = await fixture(html`
|
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||||
<${tag}
|
<${tag}
|
||||||
.submitted=${true}
|
.submitted=${true}
|
||||||
.modelValue=${'cat'}
|
.modelValue=${'cat'}
|
||||||
.validators=${[new AlwaysInvalid()]}
|
.validators=${[new AlwaysInvalid()]}
|
||||||
>${lightDom}</${tag}>
|
>${lightDom}</${tag}>
|
||||||
`);
|
`));
|
||||||
|
|
||||||
expect(el._feedbackNode.feedbackData).to.be.undefined;
|
expect(el._feedbackNode.feedbackData).to.be.undefined;
|
||||||
unlockMessage();
|
unlockMessage();
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
await el.feedbackComplete;
|
await el.feedbackComplete;
|
||||||
expect(el._feedbackNode.feedbackData[0].message).to.equal('this ends up in "._feedbackNode"');
|
expect(el._feedbackNode.feedbackData?.[0].message).to.equal(
|
||||||
|
'this ends up in "._feedbackNode"',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('supports custom element to render feedback', async () => {
|
it('supports custom element to render feedback', async () => {
|
||||||
const customFeedbackTagString = defineCE(
|
class ValidateElementCustomRender extends LitElement {
|
||||||
class extends LitElement {
|
static get properties() {
|
||||||
static get properties() {
|
return {
|
||||||
return {
|
feedbackData: { attribute: false },
|
||||||
feedbackData: Array,
|
};
|
||||||
};
|
}
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
constructor() {
|
||||||
const name =
|
super();
|
||||||
this.feedbackData && this.feedbackData[0]
|
/**
|
||||||
? this.feedbackData[0].validator.constructor.validatorName
|
* @typedef {Object} messageMap
|
||||||
: '';
|
* @property {string | Node} message
|
||||||
return html`Custom for ${name}`;
|
* @property {string} type
|
||||||
|
* @property {Validator} [validator]
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @type {messageMap[]} */
|
||||||
|
this.feedbackData = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let name = '';
|
||||||
|
if (this.feedbackData && this.feedbackData.length > 0) {
|
||||||
|
const ctor = /** @type {typeof Validator} */ (this.feedbackData[0]?.validator
|
||||||
|
.constructor);
|
||||||
|
name = ctor.validatorName;
|
||||||
}
|
}
|
||||||
},
|
return html`Custom for ${name}`;
|
||||||
);
|
}
|
||||||
|
}
|
||||||
|
const customFeedbackTagString = defineCE(ValidateElementCustomRender);
|
||||||
const customFeedbackTag = unsafeStatic(customFeedbackTagString);
|
const customFeedbackTag = unsafeStatic(customFeedbackTagString);
|
||||||
const el = await fixture(html`
|
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||||
<${tag}
|
<${tag}
|
||||||
.submitted=${true}
|
.submitted=${true}
|
||||||
.validators=${[new ContainsLowercaseA(), new AlwaysInvalid()]}>
|
.validators=${[new ContainsLowercaseA(), new AlwaysInvalid()]}>
|
||||||
<${customFeedbackTag} slot="feedback"><${customFeedbackTag}>
|
<${customFeedbackTag} slot="feedback"><${customFeedbackTag}>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`);
|
`));
|
||||||
|
|
||||||
expect(el._feedbackNode.localName).to.equal(customFeedbackTagString);
|
expect(el._feedbackNode.localName).to.equal(customFeedbackTagString);
|
||||||
|
|
||||||
|
|
@ -273,66 +276,46 @@ export function runValidateMixinFeedbackPart() {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('supports custom messages in Validator instance configuration object', async () => {
|
it('supports custom messages in Validator instance configuration object', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||||
<${tag}
|
<${tag}
|
||||||
.submitted=${true}
|
.submitted=${true}
|
||||||
.validators=${[new MinLength(3, { getMessage: () => 'custom via config' })]}
|
.validators=${[new MinLength(3, { getMessage: () => 'custom via config' })]}
|
||||||
>${lightDom}</${tag}>
|
>${lightDom}</${tag}>
|
||||||
`);
|
`));
|
||||||
|
|
||||||
el.modelValue = 'a';
|
el.modelValue = 'a';
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
await el.feedbackComplete;
|
await el.feedbackComplete;
|
||||||
expect(el._feedbackNode.feedbackData[0].message).to.equal('custom via config');
|
expect(el._feedbackNode.feedbackData?.[0].message).to.equal('custom via config');
|
||||||
});
|
|
||||||
|
|
||||||
it('keeps the feedback component in sync', async () => {
|
|
||||||
const el = await fixture(html`
|
|
||||||
<${tag} .validators=${[new MinLength(3)]}>${lightDom}</${tag}>
|
|
||||||
`);
|
|
||||||
await el.updateComplete;
|
|
||||||
await el.feedbackComplete;
|
|
||||||
expect(el._feedbackNode.feedbackData).to.deep.equal([]);
|
|
||||||
|
|
||||||
// has error but does not show/forward to component as showCondition is not met
|
|
||||||
el.modelValue = '1';
|
|
||||||
await el.updateComplete;
|
|
||||||
await el.feedbackComplete;
|
|
||||||
expect(el._feedbackNode.feedbackData).to.deep.equal([]);
|
|
||||||
|
|
||||||
el.submitted = true;
|
|
||||||
await el.updateComplete;
|
|
||||||
await el.feedbackComplete;
|
|
||||||
expect(el._feedbackNode.feedbackData.length).to.equal(1);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('updates the feedback component when locale changes', async () => {
|
it('updates the feedback component when locale changes', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||||
<${tag}
|
<${tag}
|
||||||
.submitted=${true}
|
.submitted=${true}
|
||||||
.validators=${[new MinLength(3)]}
|
.validators=${[new MinLength(3)]}
|
||||||
.modelValue=${'1'}
|
.modelValue=${'1'}
|
||||||
>${lightDom}</${tag}>
|
>${lightDom}</${tag}>
|
||||||
`);
|
`));
|
||||||
await el.feedbackComplete;
|
await el.feedbackComplete;
|
||||||
expect(el._feedbackNode.feedbackData.length).to.equal(1);
|
expect(el._feedbackNode.feedbackData?.length).to.equal(1);
|
||||||
expect(el._feedbackNode.feedbackData[0].message).to.equal('Message for MinLength');
|
expect(el._feedbackNode.feedbackData?.[0].message).to.equal('Message for MinLength');
|
||||||
|
|
||||||
localize.locale = 'de-DE';
|
localize.locale = 'de-DE';
|
||||||
await el.feedbackComplete;
|
await el.feedbackComplete;
|
||||||
expect(el._feedbackNode.feedbackData[0].message).to.equal('Nachricht für MinLength');
|
expect(el._feedbackNode.feedbackData?.[0].message).to.equal('Nachricht für MinLength');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows success message after fixing an error', async () => {
|
it('shows success message after fixing an error', async () => {
|
||||||
const elTagString = defineCE(
|
// @ts-expect-error base constructor same return type
|
||||||
class extends ValidateMixin(LitElement) {
|
class ValidateElementCustomTypes extends ValidateMixin(LitElement) {
|
||||||
static get validationTypes() {
|
static get validationTypes() {
|
||||||
return ['error', 'success'];
|
return ['error', 'success'];
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
);
|
const elTagString = defineCE(ValidateElementCustomTypes);
|
||||||
const elTag = unsafeStatic(elTagString);
|
const elTag = unsafeStatic(elTagString);
|
||||||
const el = await fixture(html`
|
const el = /** @type {ValidateElementCustomTypes} */ (await fixture(html`
|
||||||
<${elTag}
|
<${elTag}
|
||||||
.submitted=${true}
|
.submitted=${true}
|
||||||
.validators=${[
|
.validators=${[
|
||||||
|
|
@ -340,28 +323,28 @@ export function runValidateMixinFeedbackPart() {
|
||||||
new DefaultSuccess(null, { getMessage: () => 'This is a success message' }),
|
new DefaultSuccess(null, { getMessage: () => 'This is a success message' }),
|
||||||
]}
|
]}
|
||||||
>${lightDom}</${elTag}>
|
>${lightDom}</${elTag}>
|
||||||
`);
|
`));
|
||||||
|
|
||||||
el.modelValue = 'a';
|
el.modelValue = 'a';
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
await el.feedbackComplete;
|
await el.feedbackComplete;
|
||||||
expect(el._feedbackNode.feedbackData[0].message).to.equal('Message for MinLength');
|
expect(el._feedbackNode.feedbackData?.[0].message).to.equal('Message for MinLength');
|
||||||
|
|
||||||
el.modelValue = 'abcd';
|
el.modelValue = 'abcd';
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
await el.feedbackComplete;
|
await el.feedbackComplete;
|
||||||
expect(el._feedbackNode.feedbackData[0].message).to.equal('This is a success message');
|
expect(el._feedbackNode.feedbackData?.[0].message).to.equal('This is a success message');
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Accessibility', () => {
|
describe('Accessibility', () => {
|
||||||
it('sets [aria-invalid="true"] to "._inputNode" when there is an error', async () => {
|
it('sets [aria-invalid="true"] to "._inputNode" when there is an error', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||||
<${tag}
|
<${tag}
|
||||||
submitted
|
submitted
|
||||||
.validators=${[new Required()]}
|
.validators=${[new Required()]}
|
||||||
.modelValue=${'a'}
|
.modelValue=${'a'}
|
||||||
>${lightDom}</${tag}>
|
>${lightDom}</${tag}>
|
||||||
`);
|
`));
|
||||||
const inputNode = el._inputNode;
|
const inputNode = el._inputNode;
|
||||||
expect(inputNode.getAttribute('aria-invalid')).to.equal('false');
|
expect(inputNode.getAttribute('aria-invalid')).to.equal('false');
|
||||||
|
|
||||||
|
|
@ -379,25 +362,27 @@ export function runValidateMixinFeedbackPart() {
|
||||||
|
|
||||||
describe('Meta data', () => {
|
describe('Meta data', () => {
|
||||||
it('".getMessage()" gets a reference to formControl, params, modelValue and type', async () => {
|
it('".getMessage()" gets a reference to formControl, params, modelValue and type', async () => {
|
||||||
const elTagString = defineCE(
|
// @ts-expect-error base constructor same return type
|
||||||
class extends ValidateMixin(LitElement) {
|
class ValidateElementCustomTypes extends ValidateMixin(LitElement) {
|
||||||
static get validationTypes() {
|
static get validationTypes() {
|
||||||
return ['error', 'x'];
|
return ['error', 'x'];
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
);
|
|
||||||
|
const elTagString = defineCE(ValidateElementCustomTypes);
|
||||||
const elTag = unsafeStatic(elTagString);
|
const elTag = unsafeStatic(elTagString);
|
||||||
let el;
|
let el;
|
||||||
const constructorValidator = new MinLength(4, { type: 'x' }); // type to prevent duplicates
|
const constructorValidator = new MinLength(4, { type: 'x' }); // type to prevent duplicates
|
||||||
const constructorMessageSpy = sinon.spy(constructorValidator.constructor, 'getMessage');
|
const ctorValidator = /** @type {typeof MinLength} */ (constructorValidator.constructor);
|
||||||
|
const constructorMessageSpy = sinon.spy(ctorValidator, 'getMessage');
|
||||||
|
|
||||||
el = await fixture(html`
|
el = /** @type {ValidateElementCustomTypes} */ (await fixture(html`
|
||||||
<${elTag}
|
<${elTag}
|
||||||
.submitted=${true}
|
.submitted=${true}
|
||||||
.validators=${[constructorValidator]}
|
.validators=${[constructorValidator]}
|
||||||
.modelValue=${'cat'}
|
.modelValue=${'cat'}
|
||||||
>${lightDom}</${elTag}>
|
>${lightDom}</${elTag}>
|
||||||
`);
|
`));
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
await el.feedbackComplete;
|
await el.feedbackComplete;
|
||||||
expect(constructorMessageSpy.args[0][0]).to.eql({
|
expect(constructorMessageSpy.args[0][0]).to.eql({
|
||||||
|
|
@ -413,13 +398,13 @@ export function runValidateMixinFeedbackPart() {
|
||||||
const instanceMessageSpy = sinon.spy();
|
const instanceMessageSpy = sinon.spy();
|
||||||
const instanceValidator = new MinLength(4, { getMessage: instanceMessageSpy });
|
const instanceValidator = new MinLength(4, { getMessage: instanceMessageSpy });
|
||||||
|
|
||||||
el = await fixture(html`
|
el = /** @type {ValidateElementCustomTypes} */ (await fixture(html`
|
||||||
<${elTag}
|
<${elTag}
|
||||||
.submitted=${true}
|
.submitted=${true}
|
||||||
.validators=${[instanceValidator]}
|
.validators=${[instanceValidator]}
|
||||||
.modelValue=${'cat'}
|
.modelValue=${'cat'}
|
||||||
>${lightDom}</${elTag}>
|
>${lightDom}</${elTag}>
|
||||||
`);
|
`));
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
await el.feedbackComplete;
|
await el.feedbackComplete;
|
||||||
expect(instanceMessageSpy.args[0][0]).to.eql({
|
expect(instanceMessageSpy.args[0][0]).to.eql({
|
||||||
|
|
@ -437,16 +422,17 @@ export function runValidateMixinFeedbackPart() {
|
||||||
|
|
||||||
it('".getMessage()" gets .fieldName defined on instance', async () => {
|
it('".getMessage()" gets .fieldName defined on instance', async () => {
|
||||||
const constructorValidator = new MinLength(4);
|
const constructorValidator = new MinLength(4);
|
||||||
const spy = sinon.spy(constructorValidator.constructor, 'getMessage');
|
const ctorValidator = /** @type {typeof MinLength} */ (constructorValidator.constructor);
|
||||||
|
const spy = sinon.spy(ctorValidator, 'getMessage');
|
||||||
|
|
||||||
const el = await fixture(html`
|
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||||
<${tag}
|
<${tag}
|
||||||
.submitted=${true}
|
.submitted=${true}
|
||||||
.validators=${[constructorValidator]}
|
.validators=${[constructorValidator]}
|
||||||
.modelValue=${'cat'}
|
.modelValue=${'cat'}
|
||||||
.fieldName=${new Promise(resolve => resolve('myField'))}
|
.fieldName=${new Promise(resolve => resolve('myField'))}
|
||||||
>${lightDom}</${tag}>
|
>${lightDom}</${tag}>
|
||||||
`);
|
`));
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
await el.feedbackComplete;
|
await el.feedbackComplete;
|
||||||
expect(spy.args[0][0]).to.eql({
|
expect(spy.args[0][0]).to.eql({
|
||||||
|
|
@ -465,22 +451,23 @@ export function runValidateMixinFeedbackPart() {
|
||||||
const constructorValidator = new MinLength(4, {
|
const constructorValidator = new MinLength(4, {
|
||||||
fieldName: new Promise(resolve => resolve('myFieldViaCfg')),
|
fieldName: new Promise(resolve => resolve('myFieldViaCfg')),
|
||||||
});
|
});
|
||||||
const spy = sinon.spy(constructorValidator.constructor, 'getMessage');
|
const ctorValidator = /** @type {typeof MinLength} */ (constructorValidator.constructor);
|
||||||
|
const spy = sinon.spy(ctorValidator, 'getMessage');
|
||||||
|
|
||||||
const el = await fixture(html`
|
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||||
<${tag}
|
<${tag}
|
||||||
.submitted=${true}
|
.submitted=${true}
|
||||||
.validators=${[constructorValidator]}
|
.validators=${[constructorValidator]}
|
||||||
.modelValue=${'cat'}
|
.modelValue=${'cat'}
|
||||||
.fieldName=${new Promise(resolve => resolve('myField'))}
|
.fieldName=${new Promise(resolve => resolve('myField'))}
|
||||||
>${lightDom}</${tag}>
|
>${lightDom}</${tag}>
|
||||||
`);
|
`));
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
await el.feedbackComplete;
|
await el.feedbackComplete;
|
||||||
|
|
||||||
// ignore fieldName Promise as it will always be unique
|
// ignore fieldName Promise as it will always be unique
|
||||||
const compare = spy.args[0][0];
|
const compare = spy.args[0][0];
|
||||||
delete compare.config.fieldName;
|
delete compare?.config.fieldName;
|
||||||
expect(compare).to.eql({
|
expect(compare).to.eql({
|
||||||
config: {},
|
config: {},
|
||||||
params: 4,
|
params: 4,
|
||||||
|
|
@ -503,13 +490,13 @@ export function runValidateMixinFeedbackPart() {
|
||||||
* The Queue system solves this by queueing the updateFeedbackComponent tasks and
|
* The Queue system solves this by queueing the updateFeedbackComponent tasks and
|
||||||
* await them one by one.
|
* await them one by one.
|
||||||
*/
|
*/
|
||||||
const el = await fixture(html`
|
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||||
<${tag}
|
<${tag}
|
||||||
.submitted=${true}
|
.submitted=${true}
|
||||||
.validators=${[new MinLength(3)]}
|
.validators=${[new MinLength(3)]}
|
||||||
.modelValue=${'1'}
|
.modelValue=${'1'}
|
||||||
>${lightDom}</${tag}>
|
>${lightDom}</${tag}>
|
||||||
`);
|
`));
|
||||||
|
|
||||||
el.modelValue = '12345';
|
el.modelValue = '12345';
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
|
|
|
||||||
1271
packages/form-core/test-suites/form-group/FormGroupMixin.suite.js
Normal file
1271
packages/form-core/test-suites/form-group/FormGroupMixin.suite.js
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -3,14 +3,11 @@ import { defineCE, expect, fixture, html, oneEvent, unsafeStatic } from '@open-w
|
||||||
import { FocusMixin } from '../src/FocusMixin.js';
|
import { FocusMixin } from '../src/FocusMixin.js';
|
||||||
|
|
||||||
describe('FocusMixin', () => {
|
describe('FocusMixin', () => {
|
||||||
|
// @ts-expect-error base constructors same return type
|
||||||
class Focusable extends FocusMixin(LitElement) {
|
class Focusable extends FocusMixin(LitElement) {
|
||||||
render() {
|
render() {
|
||||||
return html`<slot name="input"></slot>`;
|
return html`<slot name="input"></slot>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
get _inputNode() {
|
|
||||||
return this.querySelector('input');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const tagString = defineCE(Focusable);
|
const tagString = defineCE(Focusable);
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,14 @@
|
||||||
import { expect, html, defineCE, unsafeStatic, fixture } from '@open-wc/testing';
|
import { expect, html, defineCE, unsafeStatic, fixture } from '@open-wc/testing';
|
||||||
import { LitElement, SlotMixin } from '@lion/core';
|
import { LitElement } from '@lion/core';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
import { FormControlMixin } from '../src/FormControlMixin.js';
|
import { FormControlMixin } from '../src/FormControlMixin.js';
|
||||||
import { FormRegistrarMixin } from '../src/registration/FormRegistrarMixin.js';
|
import { FormRegistrarMixin } from '../src/registration/FormRegistrarMixin.js';
|
||||||
|
|
||||||
describe('FormControlMixin', () => {
|
describe('FormControlMixin', () => {
|
||||||
const inputSlot = '<input slot="input" />';
|
const inputSlot = '<input slot="input" />';
|
||||||
class FormControlMixinClass extends FormControlMixin(SlotMixin(LitElement)) {
|
|
||||||
static get properties() {
|
// @ts-expect-error base constructor same return type
|
||||||
return {
|
class FormControlMixinClass extends FormControlMixin(LitElement) {}
|
||||||
modelValue: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const tagString = defineCE(FormControlMixinClass);
|
const tagString = defineCE(FormControlMixinClass);
|
||||||
const tag = unsafeStatic(tagString);
|
const tag = unsafeStatic(tagString);
|
||||||
|
|
@ -207,17 +201,10 @@ describe('FormControlMixin', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Model-value-changed event propagation', () => {
|
describe('Model-value-changed event propagation', () => {
|
||||||
|
// @ts-expect-error base constructor same return type
|
||||||
const FormControlWithRegistrarMixinClass = class extends FormControlMixin(
|
const FormControlWithRegistrarMixinClass = class extends FormControlMixin(
|
||||||
FormRegistrarMixin(SlotMixin(LitElement)),
|
FormRegistrarMixin(LitElement),
|
||||||
) {
|
) {};
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
modelValue: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const groupElem = defineCE(FormControlWithRegistrarMixinClass);
|
const groupElem = defineCE(FormControlWithRegistrarMixinClass);
|
||||||
const groupTag = unsafeStatic(groupElem);
|
const groupTag = unsafeStatic(groupElem);
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,46 @@
|
||||||
import { html, LitElement } from '@lion/core';
|
import { html, LitElement } from '@lion/core';
|
||||||
import '@lion/fieldset/lion-fieldset.js';
|
// @ts-expect-error
|
||||||
import { LionInput } from '@lion/input';
|
import { LionInput } from '@lion/input';
|
||||||
|
import '@lion/fieldset/lion-fieldset.js';
|
||||||
import { FormGroupMixin, Required } from '@lion/form-core';
|
import { FormGroupMixin, Required } from '@lion/form-core';
|
||||||
import { expect, fixture } from '@open-wc/testing';
|
import { expect, fixture } from '@open-wc/testing';
|
||||||
import { ChoiceGroupMixin } from '../../src/choice-group/ChoiceGroupMixin.js';
|
import { ChoiceGroupMixin } from '../../src/choice-group/ChoiceGroupMixin.js';
|
||||||
import { ChoiceInputMixin } from '../../src/choice-group/ChoiceInputMixin.js';
|
import { ChoiceInputMixin } from '../../src/choice-group/ChoiceInputMixin.js';
|
||||||
|
// import { LionField } from '../../src/LionField.js';
|
||||||
|
|
||||||
|
// class InputField extends LionField {
|
||||||
|
// get slots() {
|
||||||
|
// return {
|
||||||
|
// ...super.slots,
|
||||||
|
// input: () => document.createElement('input'),
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
describe('ChoiceGroupMixin', () => {
|
describe('ChoiceGroupMixin', () => {
|
||||||
before(() => {
|
class ChoiceInput extends ChoiceInputMixin(LionInput) {}
|
||||||
class ChoiceInput extends ChoiceInputMixin(LionInput) {}
|
// @ts-expect-error base constructors same return type
|
||||||
customElements.define('choice-group-input', ChoiceInput);
|
customElements.define('choice-group-input', ChoiceInput);
|
||||||
|
// @ts-expect-error
|
||||||
class ChoiceGroup extends ChoiceGroupMixin(FormGroupMixin(LitElement)) {}
|
class ChoiceGroup extends ChoiceGroupMixin(FormGroupMixin(LitElement)) {}
|
||||||
customElements.define('choice-group', ChoiceGroup);
|
customElements.define('choice-group', ChoiceGroup);
|
||||||
|
// @ts-expect-error
|
||||||
class ChoiceGroupMultiple extends ChoiceGroupMixin(FormGroupMixin(LitElement)) {
|
class ChoiceGroupMultiple extends ChoiceGroupMixin(FormGroupMixin(LitElement)) {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.multipleChoice = true;
|
this.multipleChoice = true;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
customElements.define('choice-group-multiple', ChoiceGroupMultiple);
|
}
|
||||||
});
|
customElements.define('choice-group-multiple', ChoiceGroupMultiple);
|
||||||
|
|
||||||
it('has a single modelValue representing the currently checked radio value', async () => {
|
it('has a single modelValue representing the currently checked radio value', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {ChoiceGroup} */ (await fixture(html`
|
||||||
<choice-group name="gender">
|
<choice-group name="gender">
|
||||||
<choice-group-input .choiceValue=${'male'}></choice-group-input>
|
<choice-group-input .choiceValue=${'male'}></choice-group-input>
|
||||||
<choice-group-input .choiceValue=${'female'} checked></choice-group-input>
|
<choice-group-input .choiceValue=${'female'} checked></choice-group-input>
|
||||||
<choice-group-input .choiceValue=${'other'}></choice-group-input>
|
<choice-group-input .choiceValue=${'other'}></choice-group-input>
|
||||||
</choice-group>
|
</choice-group>
|
||||||
`);
|
`));
|
||||||
expect(el.modelValue).to.equal('female');
|
expect(el.modelValue).to.equal('female');
|
||||||
el.formElements[0].checked = true;
|
el.formElements[0].checked = true;
|
||||||
expect(el.modelValue).to.equal('male');
|
expect(el.modelValue).to.equal('male');
|
||||||
|
|
@ -39,13 +49,13 @@ describe('ChoiceGroupMixin', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has a single formattedValue representing the currently checked radio value', async () => {
|
it('has a single formattedValue representing the currently checked radio value', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {ChoiceGroup} */ (await fixture(html`
|
||||||
<choice-group name="gender">
|
<choice-group name="gender">
|
||||||
<choice-group-input .choiceValue=${'male'}></choice-group-input>
|
<choice-group-input .choiceValue=${'male'}></choice-group-input>
|
||||||
<choice-group-input .choiceValue=${'female'} checked></choice-group-input>
|
<choice-group-input .choiceValue=${'female'} checked></choice-group-input>
|
||||||
<choice-group-input .choiceValue=${'other'}></choice-group-input>
|
<choice-group-input .choiceValue=${'other'}></choice-group-input>
|
||||||
</choice-group>
|
</choice-group>
|
||||||
`);
|
`));
|
||||||
expect(el.formattedValue).to.equal('female');
|
expect(el.formattedValue).to.equal('female');
|
||||||
el.formElements[0].checked = true;
|
el.formElements[0].checked = true;
|
||||||
expect(el.formattedValue).to.equal('male');
|
expect(el.formattedValue).to.equal('male');
|
||||||
|
|
@ -54,16 +64,16 @@ describe('ChoiceGroupMixin', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws if a child element without a modelValue like { value: "foo", checked: false } tries to register', async () => {
|
it('throws if a child element without a modelValue like { value: "foo", checked: false } tries to register', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {ChoiceGroup} */ (await fixture(html`
|
||||||
<choice-group name="gender">
|
<choice-group name="gender">
|
||||||
<choice-group-input .choiceValue=${'male'}></choice-group-input>
|
<choice-group-input .choiceValue=${'male'}></choice-group-input>
|
||||||
<choice-group-input .choiceValue=${'female'} checked></choice-group-input>
|
<choice-group-input .choiceValue=${'female'} checked></choice-group-input>
|
||||||
<choice-group-input .choiceValue=${'other'}></choice-group-input>
|
<choice-group-input .choiceValue=${'other'}></choice-group-input>
|
||||||
</choice-group>
|
</choice-group>
|
||||||
`);
|
`));
|
||||||
const invalidChild = await fixture(html`
|
const invalidChild = /** @type {ChoiceGroup} */ (await fixture(html`
|
||||||
<choice-group-input .modelValue=${'Lara'}></choice-group-input>
|
<choice-group-input .modelValue=${'Lara'}></choice-group-input>
|
||||||
`);
|
`));
|
||||||
|
|
||||||
expect(() => {
|
expect(() => {
|
||||||
el.addFormElement(invalidChild);
|
el.addFormElement(invalidChild);
|
||||||
|
|
@ -73,35 +83,35 @@ describe('ChoiceGroupMixin', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('automatically sets the name property of child radios to its own name', async () => {
|
it('automatically sets the name property of child radios to its own name', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {ChoiceGroup} */ (await fixture(html`
|
||||||
<choice-group name="gender">
|
<choice-group name="gender">
|
||||||
<choice-group-input .choiceValue=${'female'} checked></choice-group-input>
|
<choice-group-input .choiceValue=${'female'} checked></choice-group-input>
|
||||||
<choice-group-input .choiceValue=${'other'}></choice-group-input>
|
<choice-group-input .choiceValue=${'other'}></choice-group-input>
|
||||||
</choice-group>
|
</choice-group>
|
||||||
`);
|
`));
|
||||||
|
|
||||||
expect(el.formElements[0].name).to.equal('gender');
|
expect(el.formElements[0].name).to.equal('gender');
|
||||||
expect(el.formElements[1].name).to.equal('gender');
|
expect(el.formElements[1].name).to.equal('gender');
|
||||||
|
|
||||||
const validChild = await fixture(html`
|
const validChild = /** @type {ChoiceGroup} */ (await fixture(html`
|
||||||
<choice-group-input .choiceValue=${'male'}></choice-group-input>
|
<choice-group-input .choiceValue=${'male'}></choice-group-input>
|
||||||
`);
|
`));
|
||||||
el.appendChild(validChild);
|
el.appendChild(validChild);
|
||||||
|
|
||||||
expect(el.formElements[2].name).to.equal('gender');
|
expect(el.formElements[2].name).to.equal('gender');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws if a child element with a different name than the group tries to register', async () => {
|
it('throws if a child element with a different name than the group tries to register', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {ChoiceGroup} */ (await fixture(html`
|
||||||
<choice-group name="gender">
|
<choice-group name="gender">
|
||||||
<choice-group-input .choiceValue=${'female'} checked></choice-group-input>
|
<choice-group-input .choiceValue=${'female'} checked></choice-group-input>
|
||||||
<choice-group-input .choiceValue=${'other'}></choice-group-input>
|
<choice-group-input .choiceValue=${'other'}></choice-group-input>
|
||||||
</choice-group>
|
</choice-group>
|
||||||
`);
|
`));
|
||||||
|
|
||||||
const invalidChild = await fixture(html`
|
const invalidChild = /** @type {ChoiceGroup} */ (await fixture(html`
|
||||||
<choice-group-input name="foo" .choiceValue=${'male'}></choice-group-input>
|
<choice-group-input name="foo" .choiceValue=${'male'}></choice-group-input>
|
||||||
`);
|
`));
|
||||||
|
|
||||||
expect(() => {
|
expect(() => {
|
||||||
el.addFormElement(invalidChild);
|
el.addFormElement(invalidChild);
|
||||||
|
|
@ -111,39 +121,39 @@ describe('ChoiceGroupMixin', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can set initial modelValue on creation', async () => {
|
it('can set initial modelValue on creation', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {ChoiceGroup} */ (await fixture(html`
|
||||||
<choice-group name="gender" .modelValue=${'other'}>
|
<choice-group name="gender" .modelValue=${'other'}>
|
||||||
<choice-group-input .choiceValue=${'male'}></choice-group-input>
|
<choice-group-input .choiceValue=${'male'}></choice-group-input>
|
||||||
<choice-group-input .choiceValue=${'female'}></choice-group-input>
|
<choice-group-input .choiceValue=${'female'}></choice-group-input>
|
||||||
<choice-group-input .choiceValue=${'other'}></choice-group-input>
|
<choice-group-input .choiceValue=${'other'}></choice-group-input>
|
||||||
</choice-group>
|
</choice-group>
|
||||||
`);
|
`));
|
||||||
|
|
||||||
expect(el.modelValue).to.equal('other');
|
expect(el.modelValue).to.equal('other');
|
||||||
expect(el.formElements[2].checked).to.be.true;
|
expect(el.formElements[2].checked).to.be.true;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can set initial serializedValue on creation', async () => {
|
it('can set initial serializedValue on creation', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {ChoiceGroup} */ (await fixture(html`
|
||||||
<choice-group name="gender" .serializedValue=${'other'}>
|
<choice-group name="gender" .serializedValue=${'other'}>
|
||||||
<choice-group-input .choiceValue=${'male'}></choice-group-input>
|
<choice-group-input .choiceValue=${'male'}></choice-group-input>
|
||||||
<choice-group-input .choiceValue=${'female'}></choice-group-input>
|
<choice-group-input .choiceValue=${'female'}></choice-group-input>
|
||||||
<choice-group-input .choiceValue=${'other'}></choice-group-input>
|
<choice-group-input .choiceValue=${'other'}></choice-group-input>
|
||||||
</choice-group>
|
</choice-group>
|
||||||
`);
|
`));
|
||||||
|
|
||||||
expect(el.serializedValue).to.equal('other');
|
expect(el.serializedValue).to.equal('other');
|
||||||
expect(el.formElements[2].checked).to.be.true;
|
expect(el.formElements[2].checked).to.be.true;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can set initial formattedValue on creation', async () => {
|
it('can set initial formattedValue on creation', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {ChoiceGroup} */ (await fixture(html`
|
||||||
<choice-group name="gender" .formattedValue=${'other'}>
|
<choice-group name="gender" .formattedValue=${'other'}>
|
||||||
<choice-group-input .choiceValue=${'male'}></choice-group-input>
|
<choice-group-input .choiceValue=${'male'}></choice-group-input>
|
||||||
<choice-group-input .choiceValue=${'female'}></choice-group-input>
|
<choice-group-input .choiceValue=${'female'}></choice-group-input>
|
||||||
<choice-group-input .choiceValue=${'other'}></choice-group-input>
|
<choice-group-input .choiceValue=${'other'}></choice-group-input>
|
||||||
</choice-group>
|
</choice-group>
|
||||||
`);
|
`));
|
||||||
|
|
||||||
expect(el.formattedValue).to.equal('other');
|
expect(el.formattedValue).to.equal('other');
|
||||||
expect(el.formElements[2].checked).to.be.true;
|
expect(el.formElements[2].checked).to.be.true;
|
||||||
|
|
@ -152,12 +162,12 @@ describe('ChoiceGroupMixin', () => {
|
||||||
it('can handle complex data via choiceValue', async () => {
|
it('can handle complex data via choiceValue', async () => {
|
||||||
const date = new Date(2018, 11, 24, 10, 33, 30, 0);
|
const date = new Date(2018, 11, 24, 10, 33, 30, 0);
|
||||||
|
|
||||||
const el = await fixture(html`
|
const el = /** @type {ChoiceGroup} */ (await fixture(html`
|
||||||
<choice-group name="data">
|
<choice-group name="data">
|
||||||
<choice-group-input .choiceValue=${{ some: 'data' }}></choice-group-input>
|
<choice-group-input .choiceValue=${{ some: 'data' }}></choice-group-input>
|
||||||
<choice-group-input .choiceValue=${date} checked></choice-group-input>
|
<choice-group-input .choiceValue=${date} checked></choice-group-input>
|
||||||
</choice-group>
|
</choice-group>
|
||||||
`);
|
`));
|
||||||
|
|
||||||
expect(el.modelValue).to.equal(date);
|
expect(el.modelValue).to.equal(date);
|
||||||
el.formElements[0].checked = true;
|
el.formElements[0].checked = true;
|
||||||
|
|
@ -165,12 +175,12 @@ describe('ChoiceGroupMixin', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can handle 0 and empty string as valid values', async () => {
|
it('can handle 0 and empty string as valid values', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {ChoiceGroup} */ (await fixture(html`
|
||||||
<choice-group name="data">
|
<choice-group name="data">
|
||||||
<choice-group-input .choiceValue=${0} checked></choice-group-input>
|
<choice-group-input .choiceValue=${0} checked></choice-group-input>
|
||||||
<choice-group-input .choiceValue=${''}></choice-group-input>
|
<choice-group-input .choiceValue=${''}></choice-group-input>
|
||||||
</choice-group>
|
</choice-group>
|
||||||
`);
|
`));
|
||||||
|
|
||||||
expect(el.modelValue).to.equal(0);
|
expect(el.modelValue).to.equal(0);
|
||||||
el.formElements[1].checked = true;
|
el.formElements[1].checked = true;
|
||||||
|
|
@ -178,7 +188,7 @@ describe('ChoiceGroupMixin', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can check a radio by supplying an available modelValue', async () => {
|
it('can check a radio by supplying an available modelValue', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {ChoiceGroup} */ (await fixture(html`
|
||||||
<choice-group name="gender">
|
<choice-group name="gender">
|
||||||
<choice-group-input .modelValue="${{ value: 'male', checked: false }}"></choice-group-input>
|
<choice-group-input .modelValue="${{ value: 'male', checked: false }}"></choice-group-input>
|
||||||
<choice-group-input
|
<choice-group-input
|
||||||
|
|
@ -188,7 +198,7 @@ describe('ChoiceGroupMixin', () => {
|
||||||
.modelValue="${{ value: 'other', checked: false }}"
|
.modelValue="${{ value: 'other', checked: false }}"
|
||||||
></choice-group-input>
|
></choice-group-input>
|
||||||
</choice-group>
|
</choice-group>
|
||||||
`);
|
`));
|
||||||
|
|
||||||
expect(el.modelValue).to.equal('female');
|
expect(el.modelValue).to.equal('female');
|
||||||
el.modelValue = 'other';
|
el.modelValue = 'other';
|
||||||
|
|
@ -197,7 +207,7 @@ describe('ChoiceGroupMixin', () => {
|
||||||
|
|
||||||
it('expect child nodes to only fire one model-value-changed event per instance', async () => {
|
it('expect child nodes to only fire one model-value-changed event per instance', async () => {
|
||||||
let counter = 0;
|
let counter = 0;
|
||||||
const el = await fixture(html`
|
const el = /** @type {ChoiceGroup} */ (await fixture(html`
|
||||||
<choice-group
|
<choice-group
|
||||||
name="gender"
|
name="gender"
|
||||||
@model-value-changed=${() => {
|
@model-value-changed=${() => {
|
||||||
|
|
@ -208,7 +218,7 @@ describe('ChoiceGroupMixin', () => {
|
||||||
<choice-group-input .modelValue=${{ value: 'female', checked: true }}></choice-group-input>
|
<choice-group-input .modelValue=${{ value: 'female', checked: true }}></choice-group-input>
|
||||||
<choice-group-input .choiceValue=${'other'}></choice-group-input>
|
<choice-group-input .choiceValue=${'other'}></choice-group-input>
|
||||||
</choice-group>
|
</choice-group>
|
||||||
`);
|
`));
|
||||||
|
|
||||||
counter = 0; // reset after setup which may result in different results
|
counter = 0; // reset after setup which may result in different results
|
||||||
|
|
||||||
|
|
@ -231,60 +241,60 @@ describe('ChoiceGroupMixin', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can be required', async () => {
|
it('can be required', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {ChoiceGroup} */ (await fixture(html`
|
||||||
<choice-group name="gender" .validators=${[new Required()]}>
|
<choice-group name="gender" .validators=${[new Required()]}>
|
||||||
<choice-group-input .choiceValue=${'male'}></choice-group-input>
|
<choice-group-input .choiceValue=${'male'}></choice-group-input>
|
||||||
<choice-group-input
|
<choice-group-input
|
||||||
.choiceValue=${{ subObject: 'satisfies required' }}
|
.choiceValue=${{ subObject: 'satisfies required' }}
|
||||||
></choice-group-input>
|
></choice-group-input>
|
||||||
</choice-group>
|
</choice-group>
|
||||||
`);
|
`));
|
||||||
expect(el.hasFeedbackFor).to.include('error');
|
expect(el.hasFeedbackFor).to.include('error');
|
||||||
expect(el.validationStates).to.have.a.property('error');
|
expect(el.validationStates.error).to.exist;
|
||||||
expect(el.validationStates.error).to.have.a.property('Required');
|
expect(el.validationStates.error.Required).to.exist;
|
||||||
|
|
||||||
el.formElements[0].checked = true;
|
el.formElements[0].checked = true;
|
||||||
expect(el.hasFeedbackFor).not.to.include('error');
|
expect(el.hasFeedbackFor).not.to.include('error');
|
||||||
expect(el.validationStates).to.have.a.property('error');
|
expect(el.validationStates.error).to.exist;
|
||||||
expect(el.validationStates.error).not.to.have.a.property('Required');
|
expect(el.validationStates.error.Required).to.not.exist;
|
||||||
|
|
||||||
el.formElements[1].checked = true;
|
el.formElements[1].checked = true;
|
||||||
expect(el.hasFeedbackFor).not.to.include('error');
|
expect(el.hasFeedbackFor).not.to.include('error');
|
||||||
expect(el.validationStates).to.have.a.property('error');
|
expect(el.validationStates.error).to.exist;
|
||||||
expect(el.validationStates.error).not.to.have.a.property('Required');
|
expect(el.validationStates.error.Required).to.not.exist;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns serialized value', async () => {
|
it('returns serialized value', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {ChoiceGroup} */ (await fixture(html`
|
||||||
<choice-group name="gender">
|
<choice-group name="gender">
|
||||||
<choice-group-input .choiceValue=${'male'}></choice-group-input>
|
<choice-group-input .choiceValue=${'male'}></choice-group-input>
|
||||||
<choice-group-input .choiceValue=${'female'}></choice-group-input>
|
<choice-group-input .choiceValue=${'female'}></choice-group-input>
|
||||||
</choice-group>
|
</choice-group>
|
||||||
`);
|
`));
|
||||||
el.formElements[0].checked = true;
|
el.formElements[0].checked = true;
|
||||||
expect(el.serializedValue).to.deep.equal('male');
|
expect(el.serializedValue).to.deep.equal('male');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns serialized value on unchecked state', async () => {
|
it('returns serialized value on unchecked state', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {ChoiceGroup} */ (await fixture(html`
|
||||||
<choice-group name="gender">
|
<choice-group name="gender">
|
||||||
<choice-group-input .choiceValue=${'male'}></choice-group-input>
|
<choice-group-input .choiceValue=${'male'}></choice-group-input>
|
||||||
<choice-group-input .choiceValue=${'female'}></choice-group-input>
|
<choice-group-input .choiceValue=${'female'}></choice-group-input>
|
||||||
</choice-group>
|
</choice-group>
|
||||||
`);
|
`));
|
||||||
|
|
||||||
expect(el.serializedValue).to.deep.equal('');
|
expect(el.serializedValue).to.deep.equal('');
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('multipleChoice', () => {
|
describe('multipleChoice', () => {
|
||||||
it('has a single modelValue representing all currently checked values', async () => {
|
it('has a single modelValue representing all currently checked values', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {ChoiceGroup} */ (await fixture(html`
|
||||||
<choice-group-multiple name="gender[]">
|
<choice-group-multiple name="gender[]">
|
||||||
<choice-group-input .choiceValue=${'male'}></choice-group-input>
|
<choice-group-input .choiceValue=${'male'}></choice-group-input>
|
||||||
<choice-group-input .choiceValue=${'female'} checked></choice-group-input>
|
<choice-group-input .choiceValue=${'female'} checked></choice-group-input>
|
||||||
<choice-group-input .choiceValue=${'other'}></choice-group-input>
|
<choice-group-input .choiceValue=${'other'}></choice-group-input>
|
||||||
</choice-group-multiple>
|
</choice-group-multiple>
|
||||||
`);
|
`));
|
||||||
|
|
||||||
expect(el.modelValue).to.eql(['female']);
|
expect(el.modelValue).to.eql(['female']);
|
||||||
el.formElements[0].checked = true;
|
el.formElements[0].checked = true;
|
||||||
|
|
@ -294,13 +304,13 @@ describe('ChoiceGroupMixin', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has a single serializedValue representing all currently checked values', async () => {
|
it('has a single serializedValue representing all currently checked values', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {ChoiceGroup} */ (await fixture(html`
|
||||||
<choice-group-multiple name="gender[]">
|
<choice-group-multiple name="gender[]">
|
||||||
<choice-group-input .choiceValue=${'male'}></choice-group-input>
|
<choice-group-input .choiceValue=${'male'}></choice-group-input>
|
||||||
<choice-group-input .choiceValue=${'female'} checked></choice-group-input>
|
<choice-group-input .choiceValue=${'female'} checked></choice-group-input>
|
||||||
<choice-group-input .choiceValue=${'other'}></choice-group-input>
|
<choice-group-input .choiceValue=${'other'}></choice-group-input>
|
||||||
</choice-group-multiple>
|
</choice-group-multiple>
|
||||||
`);
|
`));
|
||||||
|
|
||||||
expect(el.serializedValue).to.eql(['female']);
|
expect(el.serializedValue).to.eql(['female']);
|
||||||
el.formElements[0].checked = true;
|
el.formElements[0].checked = true;
|
||||||
|
|
@ -310,13 +320,13 @@ describe('ChoiceGroupMixin', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has a single formattedValue representing all currently checked values', async () => {
|
it('has a single formattedValue representing all currently checked values', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {ChoiceGroup} */ (await fixture(html`
|
||||||
<choice-group-multiple name="gender[]">
|
<choice-group-multiple name="gender[]">
|
||||||
<choice-group-input .choiceValue=${'male'}></choice-group-input>
|
<choice-group-input .choiceValue=${'male'}></choice-group-input>
|
||||||
<choice-group-input .choiceValue=${'female'} checked></choice-group-input>
|
<choice-group-input .choiceValue=${'female'} checked></choice-group-input>
|
||||||
<choice-group-input .choiceValue=${'other'}></choice-group-input>
|
<choice-group-input .choiceValue=${'other'}></choice-group-input>
|
||||||
</choice-group-multiple>
|
</choice-group-multiple>
|
||||||
`);
|
`));
|
||||||
|
|
||||||
expect(el.formattedValue).to.eql(['female']);
|
expect(el.formattedValue).to.eql(['female']);
|
||||||
el.formElements[0].checked = true;
|
el.formElements[0].checked = true;
|
||||||
|
|
@ -326,13 +336,13 @@ describe('ChoiceGroupMixin', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can check multiple checkboxes by setting the modelValue', async () => {
|
it('can check multiple checkboxes by setting the modelValue', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {ChoiceGroup} */ (await fixture(html`
|
||||||
<choice-group-multiple name="gender[]">
|
<choice-group-multiple name="gender[]">
|
||||||
<choice-group-input .choiceValue=${'male'}></choice-group-input>
|
<choice-group-input .choiceValue=${'male'}></choice-group-input>
|
||||||
<choice-group-input .choiceValue=${'female'}></choice-group-input>
|
<choice-group-input .choiceValue=${'female'}></choice-group-input>
|
||||||
<choice-group-input .choiceValue=${'other'}></choice-group-input>
|
<choice-group-input .choiceValue=${'other'}></choice-group-input>
|
||||||
</choice-group-multiple>
|
</choice-group-multiple>
|
||||||
`);
|
`));
|
||||||
|
|
||||||
el.modelValue = ['male', 'other'];
|
el.modelValue = ['male', 'other'];
|
||||||
expect(el.modelValue).to.eql(['male', 'other']);
|
expect(el.modelValue).to.eql(['male', 'other']);
|
||||||
|
|
@ -341,13 +351,13 @@ describe('ChoiceGroupMixin', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('unchecks non-matching checkboxes when setting the modelValue', async () => {
|
it('unchecks non-matching checkboxes when setting the modelValue', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {ChoiceGroup} */ (await fixture(html`
|
||||||
<choice-group-multiple name="gender[]">
|
<choice-group-multiple name="gender[]">
|
||||||
<choice-group-input .choiceValue=${'male'} checked></choice-group-input>
|
<choice-group-input .choiceValue=${'male'} checked></choice-group-input>
|
||||||
<choice-group-input .choiceValue=${'female'}></choice-group-input>
|
<choice-group-input .choiceValue=${'female'}></choice-group-input>
|
||||||
<choice-group-input .choiceValue=${'other'} checked></choice-group-input>
|
<choice-group-input .choiceValue=${'other'} checked></choice-group-input>
|
||||||
</choice-group-multiple>
|
</choice-group-multiple>
|
||||||
`);
|
`));
|
||||||
|
|
||||||
expect(el.modelValue).to.eql(['male', 'other']);
|
expect(el.modelValue).to.eql(['male', 'other']);
|
||||||
expect(el.formElements[0].checked).to.be.true;
|
expect(el.formElements[0].checked).to.be.true;
|
||||||
|
|
@ -362,7 +372,7 @@ describe('ChoiceGroupMixin', () => {
|
||||||
|
|
||||||
describe('Integration with a parent form/fieldset', () => {
|
describe('Integration with a parent form/fieldset', () => {
|
||||||
it('will serialize all children with their serializedValue', async () => {
|
it('will serialize all children with their serializedValue', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {ChoiceGroup} */ (await fixture(html`
|
||||||
<lion-fieldset>
|
<lion-fieldset>
|
||||||
<choice-group name="gender">
|
<choice-group name="gender">
|
||||||
<choice-group-input .choiceValue=${'male'} checked disabled></choice-group-input>
|
<choice-group-input .choiceValue=${'male'} checked disabled></choice-group-input>
|
||||||
|
|
@ -370,13 +380,13 @@ describe('ChoiceGroupMixin', () => {
|
||||||
<choice-group-input .choiceValue=${'other'}></choice-group-input>
|
<choice-group-input .choiceValue=${'other'}></choice-group-input>
|
||||||
</choice-group>
|
</choice-group>
|
||||||
</lion-fieldset>
|
</lion-fieldset>
|
||||||
`);
|
`));
|
||||||
|
|
||||||
expect(el.serializedValue).to.eql({
|
expect(el.serializedValue).to.eql({
|
||||||
gender: 'female',
|
gender: 'female',
|
||||||
});
|
});
|
||||||
|
|
||||||
const choiceGroupEl = el.querySelector('[name="gender"]');
|
const choiceGroupEl = /** @type {ChoiceGroup} */ (el.querySelector('[name="gender"]'));
|
||||||
choiceGroupEl.multipleChoice = true;
|
choiceGroupEl.multipleChoice = true;
|
||||||
expect(el.serializedValue).to.eql({
|
expect(el.serializedValue).to.eql({
|
||||||
gender: ['female'],
|
gender: ['female'],
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,46 @@
|
||||||
import { html } from '@lion/core';
|
import { html } from '@lion/core';
|
||||||
|
// @ts-expect-error
|
||||||
import { LionInput } from '@lion/input';
|
import { LionInput } from '@lion/input';
|
||||||
import { Required } from '@lion/form-core';
|
import { Required } from '@lion/form-core';
|
||||||
import { expect, fixture } from '@open-wc/testing';
|
import { expect, fixture } from '@open-wc/testing';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
import { ChoiceInputMixin } from '../../src/choice-group/ChoiceInputMixin.js';
|
import { ChoiceInputMixin } from '../../src/choice-group/ChoiceInputMixin.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('../../types/choice-group/ChoiceInputMixinTypes').ChoiceInputHost} ChoiceInputHost
|
||||||
|
*/
|
||||||
|
|
||||||
|
// class InputField extends LionField {
|
||||||
|
// get slots() {
|
||||||
|
// return {
|
||||||
|
// ...super.slots,
|
||||||
|
// input: () => document.createElement('input'),
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
describe('ChoiceInputMixin', () => {
|
describe('ChoiceInputMixin', () => {
|
||||||
before(() => {
|
/** @typedef {Element & ChoiceClass} ChoiceInput */
|
||||||
class ChoiceInput extends ChoiceInputMixin(LionInput) {
|
class ChoiceClass extends ChoiceInputMixin(LionInput) {
|
||||||
connectedCallback() {
|
constructor() {
|
||||||
if (super.connectedCallback) super.connectedCallback();
|
super();
|
||||||
this.type = 'checkbox'; // could also be 'radio', should be tested in integration test
|
this.type = 'checkbox'; // could also be 'radio', should be tested in integration test
|
||||||
}
|
|
||||||
}
|
}
|
||||||
customElements.define('choice-input', ChoiceInput);
|
}
|
||||||
});
|
// @ts-expect-error base constructors same return type
|
||||||
|
customElements.define('choice-input', ChoiceClass);
|
||||||
|
|
||||||
it('is hidden when attribute hidden is true', async () => {
|
it('is hidden when attribute hidden is true', async () => {
|
||||||
const el = await fixture(html`<choice-input hidden></choice-input>`);
|
const el = /** @type {ChoiceInput} */ (await fixture(
|
||||||
|
html`<choice-input hidden></choice-input>`,
|
||||||
|
));
|
||||||
expect(el).not.to.be.displayed;
|
expect(el).not.to.be.displayed;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has choiceValue', async () => {
|
it('has choiceValue', async () => {
|
||||||
const el = await fixture(html`<choice-input .choiceValue=${'foo'}></choice-input>`);
|
const el = /** @type {ChoiceInput} */ (await fixture(
|
||||||
|
html`<choice-input .choiceValue=${'foo'}></choice-input>`,
|
||||||
|
));
|
||||||
|
|
||||||
expect(el.choiceValue).to.equal('foo');
|
expect(el.choiceValue).to.equal('foo');
|
||||||
expect(el.modelValue).to.deep.equal({
|
expect(el.modelValue).to.deep.equal({
|
||||||
|
|
@ -34,7 +52,9 @@ describe('ChoiceInputMixin', () => {
|
||||||
it('can handle complex data via choiceValue', async () => {
|
it('can handle complex data via choiceValue', async () => {
|
||||||
const date = new Date(2018, 11, 24, 10, 33, 30, 0);
|
const date = new Date(2018, 11, 24, 10, 33, 30, 0);
|
||||||
|
|
||||||
const el = await fixture(html`<choice-input .choiceValue=${date}></choice-input>`);
|
const el = /** @type {ChoiceInput} */ (await fixture(
|
||||||
|
html`<choice-input .choiceValue=${date}></choice-input>`,
|
||||||
|
));
|
||||||
|
|
||||||
expect(el.choiceValue).to.equal(date);
|
expect(el.choiceValue).to.equal(date);
|
||||||
expect(el.modelValue.value).to.equal(date);
|
expect(el.modelValue.value).to.equal(date);
|
||||||
|
|
@ -42,14 +62,14 @@ describe('ChoiceInputMixin', () => {
|
||||||
|
|
||||||
it('fires one "model-value-changed" event if choiceValue or checked state or modelValue changed', async () => {
|
it('fires one "model-value-changed" event if choiceValue or checked state or modelValue changed', async () => {
|
||||||
let counter = 0;
|
let counter = 0;
|
||||||
const el = await fixture(html`
|
const el = /** @type {ChoiceInput} */ (await fixture(html`
|
||||||
<choice-input
|
<choice-input
|
||||||
@model-value-changed=${() => {
|
@model-value-changed=${() => {
|
||||||
counter += 1;
|
counter += 1;
|
||||||
}}
|
}}
|
||||||
.choiceValue=${'foo'}
|
.choiceValue=${'foo'}
|
||||||
></choice-input>
|
></choice-input>
|
||||||
`);
|
`));
|
||||||
expect(counter).to.equal(1); // undefined to set value
|
expect(counter).to.equal(1); // undefined to set value
|
||||||
|
|
||||||
el.checked = true;
|
el.checked = true;
|
||||||
|
|
@ -67,7 +87,7 @@ describe('ChoiceInputMixin', () => {
|
||||||
|
|
||||||
it('fires one "user-input-changed" event after user interaction', async () => {
|
it('fires one "user-input-changed" event after user interaction', async () => {
|
||||||
let counter = 0;
|
let counter = 0;
|
||||||
const el = await fixture(html`
|
const el = /** @type {ChoiceInput} */ (await fixture(html`
|
||||||
<choice-input
|
<choice-input
|
||||||
@user-input-changed="${() => {
|
@user-input-changed="${() => {
|
||||||
counter += 1;
|
counter += 1;
|
||||||
|
|
@ -75,7 +95,7 @@ describe('ChoiceInputMixin', () => {
|
||||||
>
|
>
|
||||||
<input slot="input" />
|
<input slot="input" />
|
||||||
</choice-input>
|
</choice-input>
|
||||||
`);
|
`));
|
||||||
expect(counter).to.equal(0);
|
expect(counter).to.equal(0);
|
||||||
// Here we try to mimic user interaction by firing browser events
|
// Here we try to mimic user interaction by firing browser events
|
||||||
const nativeInput = el._inputNode;
|
const nativeInput = el._inputNode;
|
||||||
|
|
@ -86,31 +106,31 @@ describe('ChoiceInputMixin', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can be required', async () => {
|
it('can be required', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {ChoiceInput} */ (await fixture(html`
|
||||||
<choice-input .choiceValue=${'foo'} .validators=${[new Required()]}></choice-input>
|
<choice-input .choiceValue=${'foo'} .validators=${[new Required()]}></choice-input>
|
||||||
`);
|
`));
|
||||||
expect(el.hasFeedbackFor).to.include('error');
|
expect(el.hasFeedbackFor).to.include('error');
|
||||||
expect(el.validationStates).to.have.a.property('error');
|
expect(el.validationStates.error).to.exist;
|
||||||
expect(el.validationStates.error).to.have.a.property('Required');
|
expect(el.validationStates.error.Required).to.exist;
|
||||||
el.checked = true;
|
el.checked = true;
|
||||||
expect(el.hasFeedbackFor).not.to.include('error');
|
expect(el.hasFeedbackFor).not.to.include('error');
|
||||||
expect(el.validationStates).to.have.a.property('error');
|
expect(el.validationStates.error).to.exist;
|
||||||
expect(el.validationStates.error).not.to.have.a.property('Required');
|
expect(el.validationStates.error.Required).not.to.exist;
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Checked state synchronization', () => {
|
describe('Checked state synchronization', () => {
|
||||||
it('synchronizes checked state initially (via attribute or property)', async () => {
|
it('synchronizes checked state initially (via attribute or property)', async () => {
|
||||||
const el = await fixture(`<choice-input></choice-input>`);
|
const el = /** @type {ChoiceInput} */ (await fixture(`<choice-input></choice-input>`));
|
||||||
expect(el.checked).to.equal(false, 'initially unchecked');
|
expect(el.checked).to.equal(false, 'initially unchecked');
|
||||||
|
|
||||||
const precheckedElementAttr = await fixture(html`
|
const precheckedElementAttr = /** @type {ChoiceInput} */ (await fixture(html`
|
||||||
<choice-input .checked=${true}></choice-input>
|
<choice-input .checked=${true}></choice-input>
|
||||||
`);
|
`));
|
||||||
expect(precheckedElementAttr.checked).to.equal(true, 'initially checked via attribute');
|
expect(precheckedElementAttr.checked).to.equal(true, 'initially checked via attribute');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can be checked and unchecked programmatically', async () => {
|
it('can be checked and unchecked programmatically', async () => {
|
||||||
const el = await fixture(`<choice-input></choice-input>`);
|
const el = /** @type {ChoiceInput} */ (await fixture(`<choice-input></choice-input>`));
|
||||||
expect(el.checked).to.be.false;
|
expect(el.checked).to.be.false;
|
||||||
el.checked = true;
|
el.checked = true;
|
||||||
expect(el.checked).to.be.true;
|
expect(el.checked).to.be.true;
|
||||||
|
|
@ -120,7 +140,7 @@ describe('ChoiceInputMixin', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can be checked and unchecked via user interaction', async () => {
|
it('can be checked and unchecked via user interaction', async () => {
|
||||||
const el = await fixture(`<choice-input></choice-input>`);
|
const el = /** @type {ChoiceInput} */ (await fixture(`<choice-input></choice-input>`));
|
||||||
el._inputNode.click();
|
el._inputNode.click();
|
||||||
expect(el.checked).to.be.true;
|
expect(el.checked).to.be.true;
|
||||||
el._inputNode.click();
|
el._inputNode.click();
|
||||||
|
|
@ -128,7 +148,9 @@ describe('ChoiceInputMixin', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('synchronizes modelValue to checked state and vice versa', async () => {
|
it('synchronizes modelValue to checked state and vice versa', async () => {
|
||||||
const el = await fixture(html`<choice-input .choiceValue=${'foo'}></choice-input>`);
|
const el = /** @type {ChoiceInput} */ (await fixture(
|
||||||
|
html`<choice-input .choiceValue=${'foo'}></choice-input>`,
|
||||||
|
));
|
||||||
expect(el.checked).to.be.false;
|
expect(el.checked).to.be.false;
|
||||||
expect(el.modelValue).to.deep.equal({
|
expect(el.modelValue).to.deep.equal({
|
||||||
checked: false,
|
checked: false,
|
||||||
|
|
@ -145,7 +167,9 @@ describe('ChoiceInputMixin', () => {
|
||||||
it('ensures optimal synchronize performance by preventing redundant computation steps', async () => {
|
it('ensures optimal synchronize performance by preventing redundant computation steps', async () => {
|
||||||
/* we are checking private apis here to make sure we do not have cyclical updates
|
/* we are checking private apis here to make sure we do not have cyclical updates
|
||||||
which can be quite common for these type of connected data */
|
which can be quite common for these type of connected data */
|
||||||
const el = await fixture(html`<choice-input .choiceValue=${'foo'}></choice-input>`);
|
const el = /** @type {ChoiceInput} */ (await fixture(
|
||||||
|
html`<choice-input .choiceValue=${'foo'}></choice-input>`,
|
||||||
|
));
|
||||||
expect(el.checked).to.be.false;
|
expect(el.checked).to.be.false;
|
||||||
|
|
||||||
const spyModelCheckedToChecked = sinon.spy(el, '__syncModelCheckedToChecked');
|
const spyModelCheckedToChecked = sinon.spy(el, '__syncModelCheckedToChecked');
|
||||||
|
|
@ -168,13 +192,14 @@ describe('ChoiceInputMixin', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('synchronizes checked state to [checked] attribute for styling purposes', async () => {
|
it('synchronizes checked state to [checked] attribute for styling purposes', async () => {
|
||||||
|
/** @param {ChoiceInput} el */
|
||||||
const hasAttr = el => el.hasAttribute('checked');
|
const hasAttr = el => el.hasAttribute('checked');
|
||||||
const el = await fixture(`<choice-input></choice-input>`);
|
const el = /** @type {ChoiceInput} */ (await fixture(`<choice-input></choice-input>`));
|
||||||
const elChecked = await fixture(html`
|
const elChecked = /** @type {ChoiceInput} */ (await fixture(html`
|
||||||
<choice-input .checked=${true}>
|
<choice-input .checked=${true}>
|
||||||
<input slot="input" />
|
<input slot="input" />
|
||||||
</choice-input>
|
</choice-input>
|
||||||
`);
|
`));
|
||||||
|
|
||||||
// Initial values
|
// Initial values
|
||||||
expect(hasAttr(el)).to.equal(false, 'initial unchecked element');
|
expect(hasAttr(el)).to.equal(false, 'initial unchecked element');
|
||||||
|
|
@ -214,32 +239,38 @@ describe('ChoiceInputMixin', () => {
|
||||||
|
|
||||||
describe('Format/parse/serialize loop', () => {
|
describe('Format/parse/serialize loop', () => {
|
||||||
it('creates a modelValue object like { checked: true, value: foo } on init', async () => {
|
it('creates a modelValue object like { checked: true, value: foo } on init', async () => {
|
||||||
const el = await fixture(html`<choice-input .choiceValue=${'foo'}></choice-input>`);
|
const el = /** @type {ChoiceInput} */ (await fixture(
|
||||||
|
html`<choice-input .choiceValue=${'foo'}></choice-input>`,
|
||||||
|
));
|
||||||
expect(el.modelValue).deep.equal({ value: 'foo', checked: false });
|
expect(el.modelValue).deep.equal({ value: 'foo', checked: false });
|
||||||
|
|
||||||
const elChecked = await fixture(html`
|
const elChecked = /** @type {ChoiceInput} */ (await fixture(html`
|
||||||
<choice-input .choiceValue=${'foo'} .checked=${true}></choice-input>
|
<choice-input .choiceValue=${'foo'} .checked=${true}></choice-input>
|
||||||
`);
|
`));
|
||||||
expect(elChecked.modelValue).deep.equal({ value: 'foo', checked: true });
|
expect(elChecked.modelValue).deep.equal({ value: 'foo', checked: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('creates a formattedValue based on modelValue.value', async () => {
|
it('creates a formattedValue based on modelValue.value', async () => {
|
||||||
const el = await fixture(`<choice-input></choice-input>`);
|
const el = /** @type {ChoiceInput} */ (await fixture(`<choice-input></choice-input>`));
|
||||||
expect(el.formattedValue).to.equal('');
|
expect(el.formattedValue).to.equal('');
|
||||||
|
|
||||||
const elementWithValue = await fixture(html`
|
const elementWithValue = /** @type {ChoiceInput} */ (await fixture(html`
|
||||||
<choice-input .choiceValue=${'foo'}></choice-input>
|
<choice-input .choiceValue=${'foo'}></choice-input>
|
||||||
`);
|
`));
|
||||||
expect(elementWithValue.formattedValue).to.equal('foo');
|
expect(elementWithValue.formattedValue).to.equal('foo');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Interaction states', () => {
|
describe('Interaction states', () => {
|
||||||
it('is considered prefilled when checked and not considered prefilled when unchecked', async () => {
|
it('is considered prefilled when checked and not considered prefilled when unchecked', async () => {
|
||||||
const el = await fixture(html`<choice-input .checked=${true}></choice-input>`);
|
const el = /** @type {ChoiceInput} */ (await fixture(
|
||||||
|
html`<choice-input .checked=${true}></choice-input>`,
|
||||||
|
));
|
||||||
expect(el.prefilled).equal(true, 'checked element not considered prefilled');
|
expect(el.prefilled).equal(true, 'checked element not considered prefilled');
|
||||||
|
|
||||||
const elUnchecked = await fixture(`<choice-input></choice-input>`);
|
const elUnchecked = /** @type {ChoiceInput} */ (await fixture(
|
||||||
|
`<choice-input></choice-input>`,
|
||||||
|
));
|
||||||
expect(elUnchecked.prefilled).equal(false, 'unchecked element not considered prefilled');
|
expect(elUnchecked.prefilled).equal(false, 'unchecked element not considered prefilled');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { defineCE } from '@open-wc/testing';
|
import { defineCE } from '@open-wc/testing';
|
||||||
import { runInteractionStateMixinSuite } from '../test-suites/InteractionStateMixin.suite.js';
|
import { runInteractionStateMixinSuite } from '../test-suites/InteractionStateMixin.suite.js';
|
||||||
import '../lion-field.js';
|
import { LionField } from '../src/LionField.js';
|
||||||
import { runFormatMixinSuite } from '../test-suites/FormatMixin.suite.js';
|
import { runFormatMixinSuite } from '../test-suites/FormatMixin.suite.js';
|
||||||
|
|
||||||
const fieldTagString = defineCE(
|
const fieldTagString = defineCE(
|
||||||
class extends customElements.get('lion-field') {
|
class extends LionField {
|
||||||
get slots() {
|
get slots() {
|
||||||
return {
|
return {
|
||||||
...super.slots,
|
...super.slots,
|
||||||
|
|
@ -18,7 +18,6 @@ const fieldTagString = defineCE(
|
||||||
describe('<lion-field> integrations', () => {
|
describe('<lion-field> integrations', () => {
|
||||||
runInteractionStateMixinSuite({
|
runInteractionStateMixinSuite({
|
||||||
tagString: fieldTagString,
|
tagString: fieldTagString,
|
||||||
suffix: 'lion-field',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
runFormatMixinSuite({
|
runFormatMixinSuite({
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { runFormGroupMixinSuite } from '../../test-suites/form-group/FormGroupMixin.suite.js';
|
||||||
|
|
||||||
|
runFormGroupMixinSuite();
|
||||||
|
|
@ -16,13 +16,21 @@ import '../lion-field.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('../src/LionField.js').LionField} LionField
|
* @typedef {import('../src/LionField.js').LionField} LionField
|
||||||
|
* @typedef {import('../types/FormControlMixinTypes').FormControlHost} FormControlHost
|
||||||
|
* @typedef {FormControlHost & HTMLElement & {__parentFormGroup?:HTMLElement, checked?:boolean}} FormControl
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/** @typedef {HTMLElement & {shadowRoot: HTMLElement, assignedNodes: Function}} ShadowHTMLElement */
|
||||||
|
|
||||||
const tagString = 'lion-field';
|
const tagString = 'lion-field';
|
||||||
const tag = unsafeStatic(tagString);
|
const tag = unsafeStatic(tagString);
|
||||||
const inputSlotString = '<input slot="input" />';
|
const inputSlotString = '<input slot="input" />';
|
||||||
const inputSlot = unsafeHTML(inputSlotString);
|
const inputSlot = unsafeHTML(inputSlotString);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import("../index.js").LionField} formControl
|
||||||
|
* @param {string} newViewValue
|
||||||
|
*/
|
||||||
function mimicUserInput(formControl, newViewValue) {
|
function mimicUserInput(formControl, newViewValue) {
|
||||||
formControl.value = newViewValue; // eslint-disable-line no-param-reassign
|
formControl.value = newViewValue; // eslint-disable-line no-param-reassign
|
||||||
formControl._inputNode.dispatchEvent(new CustomEvent('input', { bubbles: true }));
|
formControl._inputNode.dispatchEvent(new CustomEvent('input', { bubbles: true }));
|
||||||
|
|
@ -32,10 +40,19 @@ beforeEach(() => {
|
||||||
localizeTearDown();
|
localizeTearDown();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLElement} el
|
||||||
|
* @param {string} slot
|
||||||
|
*/
|
||||||
|
function getSlot(el, slot) {
|
||||||
|
const children = /** @type {any[]} */ (Array.from(el.children));
|
||||||
|
return children.find(child => child.slot === slot);
|
||||||
|
}
|
||||||
|
|
||||||
describe('<lion-field>', () => {
|
describe('<lion-field>', () => {
|
||||||
it(`puts a unique id "${tagString}-[hash]" on the native input`, async () => {
|
it(`puts a unique id "${tagString}-[hash]" on the native input`, async () => {
|
||||||
const el = /** @type {LionField} */ (await fixture(html`<${tag}>${inputSlot}</${tag}>`));
|
const el = /** @type {LionField} */ (await fixture(html`<${tag}>${inputSlot}</${tag}>`));
|
||||||
expect(Array.from(el.children).find(child => child.slot === 'input').id).to.equal(el._inputId);
|
expect(getSlot(el, 'input').id).to.equal(el._inputId);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`has a fieldName based on the label`, async () => {
|
it(`has a fieldName based on the label`, async () => {
|
||||||
|
|
@ -148,15 +165,15 @@ describe('<lion-field>', () => {
|
||||||
const el = /** @type {LionField} */ (await fixture(
|
const el = /** @type {LionField} */ (await fixture(
|
||||||
html`<${tag} value="one">${inputSlot}</${tag}>`,
|
html`<${tag} value="one">${inputSlot}</${tag}>`,
|
||||||
));
|
));
|
||||||
expect(Array.from(el.children).find(child => child.slot === 'input').value).to.equal('one');
|
expect(getSlot(el, 'input').value).to.equal('one');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('delegates value property', async () => {
|
it('delegates value property', async () => {
|
||||||
const el = /** @type {LionField} */ (await fixture(html`<${tag}>${inputSlot}</${tag}>`));
|
const el = /** @type {LionField} */ (await fixture(html`<${tag}>${inputSlot}</${tag}>`));
|
||||||
expect(Array.from(el.children).find(child => child.slot === 'input').value).to.equal('');
|
expect(getSlot(el, 'input').value).to.equal('');
|
||||||
el.value = 'one';
|
el.value = 'one';
|
||||||
expect(el.value).to.equal('one');
|
expect(el.value).to.equal('one');
|
||||||
expect(Array.from(el.children).find(child => child.slot === 'input').value).to.equal('one');
|
expect(getSlot(el, 'input').value).to.equal('one');
|
||||||
});
|
});
|
||||||
|
|
||||||
// This is necessary for security, so that _inputNodes autocomplete can be set to 'off'
|
// This is necessary for security, so that _inputNodes autocomplete can be set to 'off'
|
||||||
|
|
@ -189,7 +206,7 @@ describe('<lion-field>', () => {
|
||||||
|
|
||||||
el.disabled = true;
|
el.disabled = true;
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
await aTimeout();
|
await aTimeout(0);
|
||||||
|
|
||||||
expect(el._inputNode.hasAttribute('disabled')).to.equal(true);
|
expect(el._inputNode.hasAttribute('disabled')).to.equal(true);
|
||||||
const disabledel = /** @type {LionField} */ (await fixture(
|
const disabledel = /** @type {LionField} */ (await fixture(
|
||||||
|
|
@ -220,7 +237,7 @@ describe('<lion-field>', () => {
|
||||||
<span slot="feedback">No name entered</span>
|
<span slot="feedback">No name entered</span>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`));
|
`));
|
||||||
const nativeInput = Array.from(el.children).find(child => child.slot === 'input');
|
const nativeInput = getSlot(el, 'input');
|
||||||
|
|
||||||
expect(nativeInput.getAttribute('aria-labelledby')).to.equal(`label-${el._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(`help-text-${el._inputId}`);
|
||||||
|
|
@ -238,7 +255,7 @@ describe('<lion-field>', () => {
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`));
|
`));
|
||||||
|
|
||||||
const nativeInput = Array.from(el.children).find(child => child.slot === 'input');
|
const nativeInput = getSlot(el, 'input');
|
||||||
expect(nativeInput.getAttribute('aria-labelledby')).to.contain(
|
expect(nativeInput.getAttribute('aria-labelledby')).to.contain(
|
||||||
`before-${el._inputId} after-${el._inputId}`,
|
`before-${el._inputId} after-${el._inputId}`,
|
||||||
);
|
);
|
||||||
|
|
@ -250,7 +267,7 @@ describe('<lion-field>', () => {
|
||||||
// TODO: Move test below to FormControlMixin.test.js.
|
// TODO: Move test below to FormControlMixin.test.js.
|
||||||
it(`allows to add to aria description or label via addToAriaLabelledBy() and
|
it(`allows to add to aria description or label via addToAriaLabelledBy() and
|
||||||
addToAriaDescribedBy()`, async () => {
|
addToAriaDescribedBy()`, async () => {
|
||||||
const wrapper = /** @type {LionField} */ (await fixture(html`
|
const wrapper = /** @type {HTMLElement} */ (await fixture(html`
|
||||||
<div id="wrapper">
|
<div id="wrapper">
|
||||||
<${tag}>
|
<${tag}>
|
||||||
${inputSlot}
|
${inputSlot}
|
||||||
|
|
@ -260,7 +277,7 @@ describe('<lion-field>', () => {
|
||||||
<div id="additionalLabel"> This also needs to be read whenever the input has focus</div>
|
<div id="additionalLabel"> This also needs to be read whenever the input has focus</div>
|
||||||
<div id="additionalDescription"> Same for this </div>
|
<div id="additionalDescription"> Same for this </div>
|
||||||
</div>`));
|
</div>`));
|
||||||
const el = wrapper.querySelector(tagString);
|
const el = /** @type {LionField} */ (wrapper.querySelector(tagString));
|
||||||
// wait until the field element is done rendering
|
// wait until the field element is done rendering
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
|
|
@ -270,25 +287,33 @@ describe('<lion-field>', () => {
|
||||||
// 1. addToAriaLabel()
|
// 1. addToAriaLabel()
|
||||||
// Check if the aria attr is filled initially
|
// Check if the aria attr is filled initially
|
||||||
expect(_inputNode.getAttribute('aria-labelledby')).to.contain(`label-${el._inputId}`);
|
expect(_inputNode.getAttribute('aria-labelledby')).to.contain(`label-${el._inputId}`);
|
||||||
el.addToAriaLabelledBy(wrapper.querySelector('#additionalLabel'));
|
const additionalLabel = /** @type {HTMLElement} */ (wrapper.querySelector(
|
||||||
|
'#additionalLabel',
|
||||||
|
));
|
||||||
|
el.addToAriaLabelledBy(additionalLabel);
|
||||||
|
const labelledbyAttr = /** @type {string} */ (_inputNode.getAttribute('aria-labelledby'));
|
||||||
// Now check if ids are added to the end (not overridden)
|
// Now check if ids are added to the end (not overridden)
|
||||||
expect(_inputNode.getAttribute('aria-labelledby')).to.contain(`label-${el._inputId}`);
|
expect(labelledbyAttr).to.contain(`label-${el._inputId}`);
|
||||||
// Should be placed in the end
|
// Should be placed in the end
|
||||||
expect(
|
expect(
|
||||||
_inputNode.getAttribute('aria-labelledby').indexOf(`label-${el._inputId}`) <
|
labelledbyAttr.indexOf(`label-${el._inputId}`) < labelledbyAttr.indexOf('additionalLabel'),
|
||||||
_inputNode.getAttribute('aria-labelledby').indexOf('additionalLabel'),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// 2. addToAriaDescription()
|
// 2. addToAriaDescription()
|
||||||
// Check if the aria attr is filled initially
|
// Check if the aria attr is filled initially
|
||||||
expect(_inputNode.getAttribute('aria-describedby')).to.contain(`feedback-${el._inputId}`);
|
expect(_inputNode.getAttribute('aria-describedby')).to.contain(`feedback-${el._inputId}`);
|
||||||
el.addToAriaDescribedBy(wrapper.querySelector('#additionalDescription'));
|
const additionalDescription = /** @type {HTMLElement} */ (wrapper.querySelector(
|
||||||
|
'#additionalDescription',
|
||||||
|
));
|
||||||
|
el.addToAriaDescribedBy(additionalDescription);
|
||||||
|
const describedbyAttr = /** @type {string} */ (_inputNode.getAttribute('aria-describedby'));
|
||||||
|
|
||||||
// Now check if ids are added to the end (not overridden)
|
// Now check if ids are added to the end (not overridden)
|
||||||
expect(_inputNode.getAttribute('aria-describedby')).to.contain(`feedback-${el._inputId}`);
|
expect(describedbyAttr).to.contain(`feedback-${el._inputId}`);
|
||||||
// Should be placed in the end
|
// Should be placed in the end
|
||||||
expect(
|
expect(
|
||||||
_inputNode.getAttribute('aria-describedby').indexOf(`feedback-${el._inputId}`) <
|
describedbyAttr.indexOf(`feedback-${el._inputId}`) <
|
||||||
_inputNode.getAttribute('aria-describedby').indexOf('additionalDescription'),
|
describedbyAttr.indexOf('additionalDescription'),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -310,6 +335,9 @@ describe('<lion-field>', () => {
|
||||||
return 'HasX';
|
return 'HasX';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} value
|
||||||
|
*/
|
||||||
execute(value) {
|
execute(value) {
|
||||||
const result = value.indexOf('x') === -1;
|
const result = value.indexOf('x') === -1;
|
||||||
return result;
|
return result;
|
||||||
|
|
@ -324,6 +352,10 @@ describe('<lion-field>', () => {
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`));
|
`));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import("../index.js").LionField} _sceneEl
|
||||||
|
* @param {{ index?: number; el: any; wantedShowsFeedbackFor: any; }} scenario
|
||||||
|
*/
|
||||||
const executeScenario = async (_sceneEl, scenario) => {
|
const executeScenario = async (_sceneEl, scenario) => {
|
||||||
const sceneEl = _sceneEl;
|
const sceneEl = _sceneEl;
|
||||||
sceneEl.resetInteractionState();
|
sceneEl.resetInteractionState();
|
||||||
|
|
@ -372,6 +404,9 @@ describe('<lion-field>', () => {
|
||||||
return 'HasX';
|
return 'HasX';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} value
|
||||||
|
*/
|
||||||
execute(value) {
|
execute(value) {
|
||||||
const result = value.indexOf('x') === -1;
|
const result = value.indexOf('x') === -1;
|
||||||
return result;
|
return result;
|
||||||
|
|
@ -396,7 +431,7 @@ describe('<lion-field>', () => {
|
||||||
`));
|
`));
|
||||||
|
|
||||||
expect(el.hasFeedbackFor).to.deep.equal(['error']);
|
expect(el.hasFeedbackFor).to.deep.equal(['error']);
|
||||||
expect(el.validationStates.error).to.have.a.property('HasX');
|
expect(el.validationStates.error.HasX).to.exist;
|
||||||
|
|
||||||
expect(disabledEl.hasFeedbackFor).to.deep.equal([]);
|
expect(disabledEl.hasFeedbackFor).to.deep.equal([]);
|
||||||
expect(disabledEl.validationStates.error).to.deep.equal({});
|
expect(disabledEl.validationStates.error).to.deep.equal({});
|
||||||
|
|
@ -408,6 +443,9 @@ describe('<lion-field>', () => {
|
||||||
return 'HasX';
|
return 'HasX';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} value
|
||||||
|
*/
|
||||||
execute(value) {
|
execute(value) {
|
||||||
const result = value.indexOf('x') === -1;
|
const result = value.indexOf('x') === -1;
|
||||||
return result;
|
return result;
|
||||||
|
|
@ -422,7 +460,7 @@ describe('<lion-field>', () => {
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`));
|
`));
|
||||||
expect(el.hasFeedbackFor).to.deep.equal(['error']);
|
expect(el.hasFeedbackFor).to.deep.equal(['error']);
|
||||||
expect(el.validationStates.error).to.have.a.property('HasX');
|
expect(el.validationStates.error.HasX).to.exist;
|
||||||
|
|
||||||
el.disabled = true;
|
el.disabled = true;
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
|
|
@ -437,10 +475,10 @@ describe('<lion-field>', () => {
|
||||||
>${inputSlot}</${tag}>
|
>${inputSlot}</${tag}>
|
||||||
`));
|
`));
|
||||||
expect(el.hasFeedbackFor).to.deep.equal(['error']);
|
expect(el.hasFeedbackFor).to.deep.equal(['error']);
|
||||||
expect(el.validationStates.error).to.have.a.property('Required');
|
expect(el.validationStates.error.Required).to.exist;
|
||||||
el.modelValue = 'cat';
|
el.modelValue = 'cat';
|
||||||
expect(el.hasFeedbackFor).to.deep.equal([]);
|
expect(el.hasFeedbackFor).to.deep.equal([]);
|
||||||
expect(el.validationStates.error).not.to.have.a.property('Required');
|
expect(el.validationStates.error.Required).to.not.exist;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('will only update formattedValue when valid on `user-input-changed`', async () => {
|
it('will only update formattedValue when valid on `user-input-changed`', async () => {
|
||||||
|
|
@ -450,6 +488,9 @@ describe('<lion-field>', () => {
|
||||||
return 'Bar';
|
return 'Bar';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} value
|
||||||
|
*/
|
||||||
execute(value) {
|
execute(value) {
|
||||||
const hasError = value !== 'bar';
|
const hasError = value !== 'bar';
|
||||||
return hasError;
|
return hasError;
|
||||||
|
|
@ -502,8 +543,12 @@ describe('<lion-field>', () => {
|
||||||
'feedback',
|
'feedback',
|
||||||
];
|
];
|
||||||
names.forEach(slotName => {
|
names.forEach(slotName => {
|
||||||
el.querySelector(`[slot="${slotName}"]`).setAttribute('test-me', 'ok');
|
const slotLight = /** @type {HTMLElement} */ (el.querySelector(`[slot="${slotName}"]`));
|
||||||
const slot = el.shadowRoot.querySelector(`slot[name="${slotName}"]`);
|
slotLight.setAttribute('test-me', 'ok');
|
||||||
|
// @ts-expect-error
|
||||||
|
const slot = /** @type {ShadowHTMLElement} */ (el.shadowRoot.querySelector(
|
||||||
|
`slot[name="${slotName}"]`,
|
||||||
|
));
|
||||||
const assignedNodes = slot.assignedNodes();
|
const assignedNodes = slot.assignedNodes();
|
||||||
expect(assignedNodes.length).to.equal(1);
|
expect(assignedNodes.length).to.equal(1);
|
||||||
expect(assignedNodes[0].getAttribute('test-me')).to.equal('ok');
|
expect(assignedNodes[0].getAttribute('test-me')).to.equal('ok');
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ describe('SyncUpdatableMixin', () => {
|
||||||
it('initializes all properties', async () => {
|
it('initializes all properties', async () => {
|
||||||
let hasCalledFirstUpdated = false;
|
let hasCalledFirstUpdated = false;
|
||||||
let hasCalledUpdateSync = false;
|
let hasCalledUpdateSync = false;
|
||||||
|
// @ts-expect-error base constructors same return type
|
||||||
class UpdatableImplementation extends SyncUpdatableMixin(LitElement) {
|
class UpdatableImplementation extends SyncUpdatableMixin(LitElement) {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
|
|
@ -64,6 +65,7 @@ describe('SyncUpdatableMixin', () => {
|
||||||
it('guarantees Member Order Independence', async () => {
|
it('guarantees Member Order Independence', async () => {
|
||||||
let hasCalledRunPropertyEffect = false;
|
let hasCalledRunPropertyEffect = false;
|
||||||
|
|
||||||
|
// @ts-expect-error base constructors same return type
|
||||||
class UpdatableImplementation extends SyncUpdatableMixin(LitElement) {
|
class UpdatableImplementation extends SyncUpdatableMixin(LitElement) {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
|
|
@ -134,7 +136,7 @@ describe('SyncUpdatableMixin', () => {
|
||||||
let propChangedCount = 0;
|
let propChangedCount = 0;
|
||||||
let propUpdateSyncCount = 0;
|
let propUpdateSyncCount = 0;
|
||||||
|
|
||||||
// @ts-ignore the private override is on purpose
|
// @ts-expect-error the private override is on purpose
|
||||||
class UpdatableImplementation extends SyncUpdatableMixin(LitElement) {
|
class UpdatableImplementation extends SyncUpdatableMixin(LitElement) {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
|
|
@ -152,7 +154,6 @@ describe('SyncUpdatableMixin', () => {
|
||||||
* @param {*} oldValue
|
* @param {*} oldValue
|
||||||
*/
|
*/
|
||||||
requestUpdateInternal(name, oldValue) {
|
requestUpdateInternal(name, oldValue) {
|
||||||
// @ts-ignore the private override is on purpose
|
|
||||||
super.requestUpdateInternal(name, oldValue);
|
super.requestUpdateInternal(name, oldValue);
|
||||||
if (name === 'prop') {
|
if (name === 'prop') {
|
||||||
propChangedCount += 1;
|
propChangedCount += 1;
|
||||||
|
|
@ -188,6 +189,7 @@ describe('SyncUpdatableMixin', () => {
|
||||||
|
|
||||||
describe('After firstUpdated', () => {
|
describe('After firstUpdated', () => {
|
||||||
it('calls "updateSync" immediately when the observed property is changed (newValue !== oldValue)', async () => {
|
it('calls "updateSync" immediately when the observed property is changed (newValue !== oldValue)', async () => {
|
||||||
|
// @ts-expect-error
|
||||||
class UpdatableImplementation extends SyncUpdatableMixin(LitElement) {
|
class UpdatableImplementation extends SyncUpdatableMixin(LitElement) {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
|
|
@ -243,6 +245,7 @@ describe('SyncUpdatableMixin', () => {
|
||||||
describe('Features', () => {
|
describe('Features', () => {
|
||||||
// See: https://lit-element.polymer-project.org/guide/lifecycle#haschanged
|
// See: https://lit-element.polymer-project.org/guide/lifecycle#haschanged
|
||||||
it('supports "hasChanged" from UpdatingElement', async () => {
|
it('supports "hasChanged" from UpdatingElement', async () => {
|
||||||
|
// @ts-expect-error base constructors same return type
|
||||||
class UpdatableImplementation extends SyncUpdatableMixin(LitElement) {
|
class UpdatableImplementation extends SyncUpdatableMixin(LitElement) {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ describe('Date Validation', () => {
|
||||||
it('provides new isDate() to allow only dates', () => {
|
it('provides new isDate() to allow only dates', () => {
|
||||||
let isEnabled;
|
let isEnabled;
|
||||||
const validator = new IsDate();
|
const validator = new IsDate();
|
||||||
expect(validator.constructor.validatorName).to.equal('IsDate');
|
expect(IsDate.validatorName).to.equal('IsDate');
|
||||||
|
|
||||||
isEnabled = validator.execute(new Date());
|
isEnabled = validator.execute(new Date());
|
||||||
expect(isEnabled).to.be.false;
|
expect(isEnabled).to.be.false;
|
||||||
|
|
@ -28,7 +28,7 @@ describe('Date Validation', () => {
|
||||||
it('provides new minDate(x) to allow only dates after min', () => {
|
it('provides new minDate(x) to allow only dates after min', () => {
|
||||||
let isEnabled;
|
let isEnabled;
|
||||||
const validator = new MinDate(new Date('2018/02/02'));
|
const validator = new MinDate(new Date('2018/02/02'));
|
||||||
expect(validator.constructor.validatorName).to.equal('MinDate');
|
expect(MinDate.validatorName).to.equal('MinDate');
|
||||||
|
|
||||||
isEnabled = validator.execute(new Date('2018-02-03'));
|
isEnabled = validator.execute(new Date('2018-02-03'));
|
||||||
expect(isEnabled).to.be.false;
|
expect(isEnabled).to.be.false;
|
||||||
|
|
@ -46,7 +46,7 @@ describe('Date Validation', () => {
|
||||||
it('provides maxDate() to allow only dates before max', () => {
|
it('provides maxDate() to allow only dates before max', () => {
|
||||||
let isEnabled;
|
let isEnabled;
|
||||||
const validator = new MaxDate(new Date('2018/02/02'));
|
const validator = new MaxDate(new Date('2018/02/02'));
|
||||||
expect(validator.constructor.validatorName).to.equal('MaxDate');
|
expect(MaxDate.validatorName).to.equal('MaxDate');
|
||||||
|
|
||||||
isEnabled = validator.execute(new Date('2018-02-01'));
|
isEnabled = validator.execute(new Date('2018-02-01'));
|
||||||
expect(isEnabled).to.be.false;
|
expect(isEnabled).to.be.false;
|
||||||
|
|
@ -67,7 +67,7 @@ describe('Date Validation', () => {
|
||||||
min: new Date('2018/02/02'),
|
min: new Date('2018/02/02'),
|
||||||
max: new Date('2018/02/04'),
|
max: new Date('2018/02/04'),
|
||||||
});
|
});
|
||||||
expect(validator.constructor.validatorName).to.equal('MinMaxDate');
|
expect(MinMaxDate.validatorName).to.equal('MinMaxDate');
|
||||||
|
|
||||||
isEnabled = validator.execute(new Date('2018/02/03'));
|
isEnabled = validator.execute(new Date('2018/02/03'));
|
||||||
expect(isEnabled).to.be.false;
|
expect(isEnabled).to.be.false;
|
||||||
|
|
@ -87,8 +87,8 @@ describe('Date Validation', () => {
|
||||||
|
|
||||||
it('provides new IsDateDisabled() to disable dates matching specified condition', () => {
|
it('provides new IsDateDisabled() to disable dates matching specified condition', () => {
|
||||||
let isDisabled;
|
let isDisabled;
|
||||||
const validator = new IsDateDisabled(d => d.getDate() === 3);
|
const validator = new IsDateDisabled(/** @param {Date} d */ d => d.getDate() === 3);
|
||||||
expect(validator.constructor.validatorName).to.equal('IsDateDisabled');
|
expect(IsDateDisabled.validatorName).to.equal('IsDateDisabled');
|
||||||
|
|
||||||
isDisabled = validator.execute(new Date('2018/02/04'));
|
isDisabled = validator.execute(new Date('2018/02/04'));
|
||||||
expect(isDisabled).to.be.false;
|
expect(isDisabled).to.be.false;
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ describe('Number Validation', () => {
|
||||||
it('provides new IsNumber() to allow only numbers', () => {
|
it('provides new IsNumber() to allow only numbers', () => {
|
||||||
let isEnabled;
|
let isEnabled;
|
||||||
const validator = new IsNumber();
|
const validator = new IsNumber();
|
||||||
expect(validator.constructor.validatorName).to.equal('IsNumber');
|
expect(IsNumber.validatorName).to.equal('IsNumber');
|
||||||
|
|
||||||
isEnabled = validator.execute(4);
|
isEnabled = validator.execute(4);
|
||||||
expect(isEnabled).to.be.false;
|
expect(isEnabled).to.be.false;
|
||||||
|
|
@ -26,7 +26,7 @@ describe('Number Validation', () => {
|
||||||
it('provides new MinNumber(x) to allow only numbers longer then min', () => {
|
it('provides new MinNumber(x) to allow only numbers longer then min', () => {
|
||||||
let isEnabled;
|
let isEnabled;
|
||||||
const validator = new MinNumber(3);
|
const validator = new MinNumber(3);
|
||||||
expect(validator.constructor.validatorName).to.equal('MinNumber');
|
expect(MinNumber.validatorName).to.equal('MinNumber');
|
||||||
|
|
||||||
isEnabled = validator.execute(3);
|
isEnabled = validator.execute(3);
|
||||||
expect(isEnabled).to.be.false;
|
expect(isEnabled).to.be.false;
|
||||||
|
|
@ -38,7 +38,7 @@ describe('Number Validation', () => {
|
||||||
it('provides new MaxNumber(x) to allow only number shorter then max', () => {
|
it('provides new MaxNumber(x) to allow only number shorter then max', () => {
|
||||||
let isEnabled;
|
let isEnabled;
|
||||||
const validator = new MaxNumber(3);
|
const validator = new MaxNumber(3);
|
||||||
expect(validator.constructor.validatorName).to.equal('MaxNumber');
|
expect(MaxNumber.validatorName).to.equal('MaxNumber');
|
||||||
|
|
||||||
isEnabled = validator.execute(3);
|
isEnabled = validator.execute(3);
|
||||||
expect(isEnabled).to.be.false;
|
expect(isEnabled).to.be.false;
|
||||||
|
|
@ -50,7 +50,7 @@ describe('Number Validation', () => {
|
||||||
it('provides new MinMaxNumber({ min: x, max: y}) to allow only numbers between min and max', () => {
|
it('provides new MinMaxNumber({ min: x, max: y}) to allow only numbers between min and max', () => {
|
||||||
let isEnabled;
|
let isEnabled;
|
||||||
const validator = new MinMaxNumber({ min: 2, max: 4 });
|
const validator = new MinMaxNumber({ min: 2, max: 4 });
|
||||||
expect(validator.constructor.validatorName).to.equal('MinMaxNumber');
|
expect(MinMaxNumber.validatorName).to.equal('MinMaxNumber');
|
||||||
|
|
||||||
isEnabled = validator.execute(2);
|
isEnabled = validator.execute(2);
|
||||||
expect(isEnabled).to.be.false;
|
expect(isEnabled).to.be.false;
|
||||||
|
|
|
||||||
|
|
@ -3,18 +3,29 @@ import { ResultValidator } from '../../src/validate/ResultValidator.js';
|
||||||
import { Required } from '../../src/validate/validators/Required.js';
|
import { Required } from '../../src/validate/validators/Required.js';
|
||||||
import { MinLength } from '../../src/validate/validators/StringValidators.js';
|
import { MinLength } from '../../src/validate/validators/StringValidators.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('../../src/validate/Validator').Validator} Validator
|
||||||
|
*/
|
||||||
|
|
||||||
describe('ResultValidator', () => {
|
describe('ResultValidator', () => {
|
||||||
it('has an "executeOnResults" function returning active state', async () => {
|
it('has an "executeOnResults" function returning active state', async () => {
|
||||||
// This test shows the best practice of creating executeOnResults method
|
// This test shows the best practice of creating executeOnResults method
|
||||||
class MyResultValidator extends ResultValidator {
|
class MyResultValidator extends ResultValidator {
|
||||||
executeOnResults({ regularValidateResult, prevValidationResult }) {
|
/**
|
||||||
const hasSuccess = regularValidateResult.length && !prevValidationResult.length;
|
*
|
||||||
return hasSuccess;
|
* @param {Object} context
|
||||||
|
* @param {Validator[]} context.regularValidationResult
|
||||||
|
* @param {Validator[]} context.prevValidationResult
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
executeOnResults({ regularValidationResult, prevValidationResult }) {
|
||||||
|
const hasSuccess = regularValidationResult.length && !prevValidationResult.length;
|
||||||
|
return !!hasSuccess;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
expect(
|
expect(
|
||||||
new MyResultValidator().executeOnResults({
|
new MyResultValidator().executeOnResults({
|
||||||
regularValidateResult: [new Required(), new MinLength(3)],
|
regularValidationResult: [new Required(), new MinLength(3)],
|
||||||
prevValidationResult: [],
|
prevValidationResult: [],
|
||||||
}),
|
}),
|
||||||
).to.be.true;
|
).to.be.true;
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ describe('String Validation', () => {
|
||||||
it('provides new IsString() to allow only strings', () => {
|
it('provides new IsString() to allow only strings', () => {
|
||||||
let isEnabled;
|
let isEnabled;
|
||||||
const validator = new IsString();
|
const validator = new IsString();
|
||||||
expect(validator.constructor.validatorName).to.equal('IsString');
|
expect(IsString.validatorName).to.equal('IsString');
|
||||||
|
|
||||||
isEnabled = validator.execute('foo');
|
isEnabled = validator.execute('foo');
|
||||||
expect(isEnabled).to.be.false;
|
expect(isEnabled).to.be.false;
|
||||||
|
|
@ -29,7 +29,7 @@ describe('String Validation', () => {
|
||||||
it('provides new EqualsLength(x) to allow only a specific string length', () => {
|
it('provides new EqualsLength(x) to allow only a specific string length', () => {
|
||||||
let isEnabled;
|
let isEnabled;
|
||||||
const validator = new EqualsLength(3);
|
const validator = new EqualsLength(3);
|
||||||
expect(validator.constructor.validatorName).to.equal('EqualsLength');
|
expect(EqualsLength.validatorName).to.equal('EqualsLength');
|
||||||
|
|
||||||
isEnabled = validator.execute('foo');
|
isEnabled = validator.execute('foo');
|
||||||
expect(isEnabled).to.be.false;
|
expect(isEnabled).to.be.false;
|
||||||
|
|
@ -44,7 +44,7 @@ describe('String Validation', () => {
|
||||||
it('provides new MinLength(x) to allow only strings longer then min', () => {
|
it('provides new MinLength(x) to allow only strings longer then min', () => {
|
||||||
let isEnabled;
|
let isEnabled;
|
||||||
const validator = new MinLength(3);
|
const validator = new MinLength(3);
|
||||||
expect(validator.constructor.validatorName).to.equal('MinLength');
|
expect(MinLength.validatorName).to.equal('MinLength');
|
||||||
|
|
||||||
isEnabled = validator.execute('foo');
|
isEnabled = validator.execute('foo');
|
||||||
expect(isEnabled).to.be.false;
|
expect(isEnabled).to.be.false;
|
||||||
|
|
@ -56,7 +56,7 @@ describe('String Validation', () => {
|
||||||
it('provides new MaxLength(x) to allow only strings shorter then max', () => {
|
it('provides new MaxLength(x) to allow only strings shorter then max', () => {
|
||||||
let isEnabled;
|
let isEnabled;
|
||||||
const validator = new MaxLength(3);
|
const validator = new MaxLength(3);
|
||||||
expect(validator.constructor.validatorName).to.equal('MaxLength');
|
expect(MaxLength.validatorName).to.equal('MaxLength');
|
||||||
|
|
||||||
isEnabled = validator.execute('foo');
|
isEnabled = validator.execute('foo');
|
||||||
expect(isEnabled).to.be.false;
|
expect(isEnabled).to.be.false;
|
||||||
|
|
@ -68,7 +68,7 @@ describe('String Validation', () => {
|
||||||
it('provides new MinMaxValidator({ min: x, max: y}) to allow only strings between min and max', () => {
|
it('provides new MinMaxValidator({ min: x, max: y}) to allow only strings between min and max', () => {
|
||||||
let isEnabled;
|
let isEnabled;
|
||||||
const validator = new MinMaxLength({ min: 2, max: 4 });
|
const validator = new MinMaxLength({ min: 2, max: 4 });
|
||||||
expect(validator.constructor.validatorName).to.equal('MinMaxLength');
|
expect(MinMaxLength.validatorName).to.equal('MinMaxLength');
|
||||||
|
|
||||||
isEnabled = validator.execute('foo');
|
isEnabled = validator.execute('foo');
|
||||||
expect(isEnabled).to.be.false;
|
expect(isEnabled).to.be.false;
|
||||||
|
|
@ -83,7 +83,7 @@ describe('String Validation', () => {
|
||||||
it('provides new IsEmail() to allow only valid email formats', () => {
|
it('provides new IsEmail() to allow only valid email formats', () => {
|
||||||
let isEnabled;
|
let isEnabled;
|
||||||
const validator = new IsEmail();
|
const validator = new IsEmail();
|
||||||
expect(validator.constructor.validatorName).to.equal('IsEmail');
|
expect(IsEmail.validatorName).to.equal('IsEmail');
|
||||||
|
|
||||||
isEnabled = validator.execute('foo@bar.com');
|
isEnabled = validator.execute('foo@bar.com');
|
||||||
expect(isEnabled).to.be.false;
|
expect(isEnabled).to.be.false;
|
||||||
|
|
@ -116,7 +116,7 @@ describe('String Validation', () => {
|
||||||
it('provides new Pattern() to allow only valid patterns', () => {
|
it('provides new Pattern() to allow only valid patterns', () => {
|
||||||
let isEnabled;
|
let isEnabled;
|
||||||
let validator = new Pattern(/#LionRocks/);
|
let validator = new Pattern(/#LionRocks/);
|
||||||
expect(validator.constructor.validatorName).to.equal('Pattern');
|
expect(Pattern.validatorName).to.equal('Pattern');
|
||||||
|
|
||||||
isEnabled = validator.execute('#LionRocks');
|
isEnabled = validator.execute('#LionRocks');
|
||||||
expect(isEnabled).to.be.false;
|
expect(isEnabled).to.be.false;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,13 @@
|
||||||
import { expect, fixture, html, unsafeStatic, defineCE } from '@open-wc/testing';
|
|
||||||
import sinon from 'sinon';
|
|
||||||
import { LitElement } from '@lion/core';
|
import { LitElement } from '@lion/core';
|
||||||
|
import { defineCE, expect, fixture, html, unsafeStatic } from '@open-wc/testing';
|
||||||
|
import sinon from 'sinon';
|
||||||
import { ValidateMixin } from '../../src/validate/ValidateMixin.js';
|
import { ValidateMixin } from '../../src/validate/ValidateMixin.js';
|
||||||
import { Validator } from '../../src/validate/Validator.js';
|
import { Validator } from '../../src/validate/Validator.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {function} method
|
||||||
|
* @param {string} errorMessage
|
||||||
|
*/
|
||||||
async function expectThrowsAsync(method, errorMessage) {
|
async function expectThrowsAsync(method, errorMessage) {
|
||||||
let error = null;
|
let error = null;
|
||||||
try {
|
try {
|
||||||
|
|
@ -20,6 +24,10 @@ async function expectThrowsAsync(method, errorMessage) {
|
||||||
describe('Validator', () => {
|
describe('Validator', () => {
|
||||||
it('has an "execute" function returning "shown" state', async () => {
|
it('has an "execute" function returning "shown" state', async () => {
|
||||||
class MyValidator extends Validator {
|
class MyValidator extends Validator {
|
||||||
|
/**
|
||||||
|
* @param {string} [modelValue]
|
||||||
|
* @param {string} [param]
|
||||||
|
*/
|
||||||
execute(modelValue, param) {
|
execute(modelValue, param) {
|
||||||
const hasError = modelValue === 'test' && param === 'me';
|
const hasError = modelValue === 'test' && param === 'me';
|
||||||
return hasError;
|
return hasError;
|
||||||
|
|
@ -79,20 +87,24 @@ describe('Validator', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has access to name, type, params, config in static get getMessage', () => {
|
it('has access to name, type, params, config in static get getMessage', () => {
|
||||||
let staticArgs;
|
let data;
|
||||||
class MyValidator extends Validator {
|
class MyValidator extends Validator {
|
||||||
static get validatorName() {
|
static get validatorName() {
|
||||||
return 'MyValidator';
|
return 'MyValidator';
|
||||||
}
|
}
|
||||||
|
|
||||||
static getMessage(...args) {
|
/**
|
||||||
staticArgs = args;
|
* @param {Object.<string,?>} _data
|
||||||
|
*/
|
||||||
|
static async getMessage(_data) {
|
||||||
|
data = _data;
|
||||||
|
return '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const vali = new MyValidator('myParam', { my: 'config' });
|
const vali = new MyValidator('myParam', { my: 'config' });
|
||||||
vali._getMessage();
|
vali._getMessage();
|
||||||
|
|
||||||
expect(staticArgs[0]).to.deep.equal({
|
expect(data).to.deep.equal({
|
||||||
name: 'MyValidator',
|
name: 'MyValidator',
|
||||||
type: 'error',
|
type: 'error',
|
||||||
params: 'myParam',
|
params: 'myParam',
|
||||||
|
|
@ -103,7 +115,9 @@ describe('Validator', () => {
|
||||||
it('fires "param-changed" event on param change', async () => {
|
it('fires "param-changed" event on param change', async () => {
|
||||||
const vali = new Validator('foo');
|
const vali = new Validator('foo');
|
||||||
const cb = sinon.spy(() => {});
|
const cb = sinon.spy(() => {});
|
||||||
vali.addEventListener('param-changed', cb);
|
if (vali.addEventListener) {
|
||||||
|
vali.addEventListener('param-changed', cb);
|
||||||
|
}
|
||||||
vali.param = 'bar';
|
vali.param = 'bar';
|
||||||
expect(cb.callCount).to.equal(1);
|
expect(cb.callCount).to.equal(1);
|
||||||
});
|
});
|
||||||
|
|
@ -111,47 +125,41 @@ describe('Validator', () => {
|
||||||
it('fires "config-changed" event on config change', async () => {
|
it('fires "config-changed" event on config change', async () => {
|
||||||
const vali = new Validator('foo', { foo: 'bar' });
|
const vali = new Validator('foo', { foo: 'bar' });
|
||||||
const cb = sinon.spy(() => {});
|
const cb = sinon.spy(() => {});
|
||||||
vali.addEventListener('config-changed', cb);
|
if (vali.addEventListener) {
|
||||||
|
vali.addEventListener('config-changed', cb);
|
||||||
|
}
|
||||||
vali.config = { bar: 'foo' };
|
vali.config = { bar: 'foo' };
|
||||||
expect(cb.callCount).to.equal(1);
|
expect(cb.callCount).to.equal(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has access to FormControl', async () => {
|
it('has access to FormControl', async () => {
|
||||||
const lightDom = '';
|
const lightDom = '';
|
||||||
const tagString = defineCE(
|
// @ts-expect-error base constructors same return type
|
||||||
class extends ValidateMixin(LitElement) {
|
class ValidateElement extends ValidateMixin(LitElement) {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return { modelValue: String };
|
return { modelValue: String };
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
);
|
const tagString = defineCE(ValidateElement);
|
||||||
const tag = unsafeStatic(tagString);
|
const tag = unsafeStatic(tagString);
|
||||||
|
|
||||||
class MyValidator extends Validator {
|
class MyValidator extends Validator {
|
||||||
|
/**
|
||||||
|
* @param {string} modelValue
|
||||||
|
* @param {string} param
|
||||||
|
*/
|
||||||
execute(modelValue, param) {
|
execute(modelValue, param) {
|
||||||
const hasError = modelValue === 'forbidden' && param === 'values';
|
const hasError = modelValue === 'forbidden' && param === 'values';
|
||||||
return hasError;
|
return hasError;
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line
|
|
||||||
onFormControlConnect(formControl) {
|
|
||||||
// I could do something like:
|
|
||||||
// - add aria-required="true"
|
|
||||||
// - add type restriction for MaxLength(3, { isBlocking: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line
|
|
||||||
onFormControlDisconnect(formControl) {
|
|
||||||
// I will cleanup what I did in connect
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const myVal = new MyValidator();
|
const myVal = new MyValidator();
|
||||||
const connectSpy = sinon.spy(myVal, 'onFormControlConnect');
|
const connectSpy = sinon.spy(myVal, 'onFormControlConnect');
|
||||||
const disconnectSpy = sinon.spy(myVal, 'onFormControlDisconnect');
|
const disconnectSpy = sinon.spy(myVal, 'onFormControlDisconnect');
|
||||||
|
|
||||||
const el = await fixture(html`
|
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||||
<${tag} .validators=${[myVal]}>${lightDom}</${tag}>
|
<${tag} .validators=${[myVal]}>${lightDom}</${tag}>
|
||||||
`);
|
`));
|
||||||
|
|
||||||
expect(connectSpy.callCount).to.equal(1);
|
expect(connectSpy.callCount).to.equal(1);
|
||||||
expect(connectSpy.calledWith(el)).to.equal(true);
|
expect(connectSpy.calledWith(el)).to.equal(true);
|
||||||
|
|
@ -171,6 +179,9 @@ describe('Validator', () => {
|
||||||
it('supports customized types', async () => {
|
it('supports customized types', async () => {
|
||||||
// This test shows the best practice of adding custom types
|
// This test shows the best practice of adding custom types
|
||||||
class MyValidator extends Validator {
|
class MyValidator extends Validator {
|
||||||
|
/**
|
||||||
|
* @param {...any} args
|
||||||
|
*/
|
||||||
constructor(...args) {
|
constructor(...args) {
|
||||||
super(...args);
|
super(...args);
|
||||||
this.type = 'my-type';
|
this.type = 'my-type';
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,15 @@ import sinon from 'sinon';
|
||||||
import '../../lion-validation-feedback.js';
|
import '../../lion-validation-feedback.js';
|
||||||
import { AlwaysInvalid, AlwaysValid } from '../../test-helpers.js';
|
import { AlwaysInvalid, AlwaysValid } from '../../test-helpers.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('../../src/validate/LionValidationFeedback').LionValidationFeedback} LionValidationFeedback
|
||||||
|
*/
|
||||||
|
|
||||||
describe('lion-validation-feedback', () => {
|
describe('lion-validation-feedback', () => {
|
||||||
it('renders a validation message', async () => {
|
it('renders a validation message', async () => {
|
||||||
const el = await fixture(html`<lion-validation-feedback></lion-validation-feedback>`);
|
const el = /** @type {LionValidationFeedback} */ (await fixture(
|
||||||
|
html`<lion-validation-feedback></lion-validation-feedback>`,
|
||||||
|
));
|
||||||
expect(el).shadowDom.to.equal('');
|
expect(el).shadowDom.to.equal('');
|
||||||
el.feedbackData = [{ message: 'hello', type: 'error', validator: new AlwaysInvalid() }];
|
el.feedbackData = [{ message: 'hello', type: 'error', validator: new AlwaysInvalid() }];
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
|
|
@ -14,7 +20,9 @@ describe('lion-validation-feedback', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders the validation type attribute', async () => {
|
it('renders the validation type attribute', async () => {
|
||||||
const el = await fixture(html`<lion-validation-feedback></lion-validation-feedback>`);
|
const el = /** @type {LionValidationFeedback} */ (await fixture(
|
||||||
|
html`<lion-validation-feedback></lion-validation-feedback>`,
|
||||||
|
));
|
||||||
el.feedbackData = [{ message: 'hello', type: 'error', validator: new AlwaysInvalid() }];
|
el.feedbackData = [{ message: 'hello', type: 'error', validator: new AlwaysInvalid() }];
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(el.getAttribute('type')).to.equal('error');
|
expect(el.getAttribute('type')).to.equal('error');
|
||||||
|
|
@ -25,7 +33,9 @@ describe('lion-validation-feedback', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('success message clears after 3s', async () => {
|
it('success message clears after 3s', async () => {
|
||||||
const el = await fixture(html`<lion-validation-feedback></lion-validation-feedback>`);
|
const el = /** @type {LionValidationFeedback} */ (await fixture(
|
||||||
|
html`<lion-validation-feedback></lion-validation-feedback>`,
|
||||||
|
));
|
||||||
|
|
||||||
const clock = sinon.useFakeTimers();
|
const clock = sinon.useFakeTimers();
|
||||||
|
|
||||||
|
|
@ -45,7 +55,9 @@ describe('lion-validation-feedback', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not clear error messages', async () => {
|
it('does not clear error messages', async () => {
|
||||||
const el = await fixture(html`<lion-validation-feedback></lion-validation-feedback>`);
|
const el = /** @type {LionValidationFeedback} */ (await fixture(
|
||||||
|
html`<lion-validation-feedback></lion-validation-feedback>`,
|
||||||
|
));
|
||||||
|
|
||||||
const clock = sinon.useFakeTimers();
|
const clock = sinon.useFakeTimers();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,8 @@
|
||||||
import { Constructor } from '@open-wc/dedupe-mixin';
|
import { Constructor } from '@open-wc/dedupe-mixin';
|
||||||
import { LitElement } from '@lion/core';
|
import { LitElement } from '@lion/core';
|
||||||
|
import { FormControlHost } from './FormControlMixinTypes';
|
||||||
|
|
||||||
export declare class FocusHost {
|
export declare class FocusHost {
|
||||||
static properties: {
|
|
||||||
focused: {
|
|
||||||
type: BooleanConstructor;
|
|
||||||
reflect: boolean;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
focused: boolean;
|
focused: boolean;
|
||||||
|
|
||||||
connectedCallback(): void;
|
connectedCallback(): void;
|
||||||
|
|
@ -23,6 +18,6 @@ export declare class FocusHost {
|
||||||
|
|
||||||
export declare function FocusImplementation<T extends Constructor<LitElement>>(
|
export declare function FocusImplementation<T extends Constructor<LitElement>>(
|
||||||
superclass: T,
|
superclass: T,
|
||||||
): T & Constructor<FocusHost> & FocusHost;
|
): T & Constructor<FocusHost> & FocusHost & Constructor<FormControlHost> & typeof FormControlHost;
|
||||||
|
|
||||||
export type FocusMixin = typeof FocusImplementation;
|
export type FocusMixin = typeof FocusImplementation;
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,16 @@
|
||||||
|
import { CSSResult, LitElement, nothing, TemplateResult } from '@lion/core';
|
||||||
|
import { SlotsMap, SlotHost } from '@lion/core/types/SlotMixinTypes';
|
||||||
import { Constructor } from '@open-wc/dedupe-mixin';
|
import { Constructor } from '@open-wc/dedupe-mixin';
|
||||||
import { SlotsMap } from '@lion/core/types/SlotMixinTypes';
|
import { DisabledHost } from '@lion/core/types/DisabledMixinTypes';
|
||||||
import { LitElement, CSSResult, TemplateResult, nothing } from '@lion/core';
|
|
||||||
|
|
||||||
export class FormControlMixinHost {
|
import { LionValidationFeedback } from '../src/validate/LionValidationFeedback';
|
||||||
static get properties(): {
|
import { FormRegisteringHost } from './registration/FormRegisteringMixinTypes';
|
||||||
name: {
|
|
||||||
type: StringConstructor;
|
export class FormControlHost {
|
||||||
reflect: boolean;
|
|
||||||
};
|
|
||||||
label: {
|
|
||||||
attribute: boolean;
|
|
||||||
};
|
|
||||||
helpText: {
|
|
||||||
type: StringConstructor;
|
|
||||||
attribute: string;
|
|
||||||
};
|
|
||||||
_ariaLabelledNodes: {
|
|
||||||
attribute: boolean;
|
|
||||||
};
|
|
||||||
_ariaDescribedNodes: {
|
|
||||||
attribute: boolean;
|
|
||||||
};
|
|
||||||
_repropagationRole: {
|
|
||||||
attribute: boolean;
|
|
||||||
};
|
|
||||||
_isRepropagationEndpoint: {
|
|
||||||
attribute: boolean;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
static get styles(): CSSResult | CSSResult[];
|
static get styles(): CSSResult | CSSResult[];
|
||||||
|
|
||||||
|
name: string;
|
||||||
|
modelValue: unknown;
|
||||||
set label(arg: string);
|
set label(arg: string);
|
||||||
get label(): string;
|
get label(): string;
|
||||||
__label: string | undefined;
|
__label: string | undefined;
|
||||||
|
|
@ -43,11 +24,12 @@ export class FormControlMixinHost {
|
||||||
get _inputNode(): HTMLElement;
|
get _inputNode(): HTMLElement;
|
||||||
get _labelNode(): HTMLElement;
|
get _labelNode(): HTMLElement;
|
||||||
get _helpTextNode(): HTMLElement;
|
get _helpTextNode(): HTMLElement;
|
||||||
get _feedbackNode(): HTMLElement;
|
get _feedbackNode(): LionValidationFeedback | undefined;
|
||||||
_inputId: string;
|
_inputId: string;
|
||||||
_ariaLabelledNodes: HTMLElement[];
|
_ariaLabelledNodes: HTMLElement[];
|
||||||
_ariaDescribedNodes: HTMLElement[];
|
_ariaDescribedNodes: HTMLElement[];
|
||||||
_repropagationRole: 'child' | 'choice-group' | 'fieldset';
|
_repropagationRole: string; // 'child' | 'choice-group' | 'fieldset';
|
||||||
|
_isRepropagationEndpoint: boolean;
|
||||||
|
|
||||||
connectedCallback(): void;
|
connectedCallback(): void;
|
||||||
updated(changedProperties: import('lit-element').PropertyValues): void;
|
updated(changedProperties: import('lit-element').PropertyValues): void;
|
||||||
|
|
@ -99,6 +81,14 @@ export class FormControlMixinHost {
|
||||||
|
|
||||||
export declare function FormControlImplementation<T extends Constructor<LitElement>>(
|
export declare function FormControlImplementation<T extends Constructor<LitElement>>(
|
||||||
superclass: T,
|
superclass: T,
|
||||||
): T & Constructor<FormControlMixinHost> & FormControlMixinHost;
|
): T &
|
||||||
|
Constructor<FormControlHost> &
|
||||||
|
FormControlHost &
|
||||||
|
Constructor<FormRegisteringHost> &
|
||||||
|
typeof FormRegisteringHost &
|
||||||
|
Constructor<DisabledHost> &
|
||||||
|
typeof DisabledHost &
|
||||||
|
Constructor<SlotHost> &
|
||||||
|
typeof SlotHost;
|
||||||
|
|
||||||
export type FormControlMixin = typeof FormControlImplementation;
|
export type FormControlMixin = typeof FormControlImplementation;
|
||||||
|
|
|
||||||
21
packages/form-core/types/FormatMixinTypes.d.ts
vendored
21
packages/form-core/types/FormatMixinTypes.d.ts
vendored
|
|
@ -1,5 +1,7 @@
|
||||||
import { Constructor } from '@open-wc/dedupe-mixin';
|
import { Constructor } from '@open-wc/dedupe-mixin';
|
||||||
import { LitElement } from '@lion/core';
|
import { LitElement } from '@lion/core';
|
||||||
|
import { ValidateHost } from './validate/ValidateMixinTypes';
|
||||||
|
import { FormControlHost } from './FormControlMixinTypes';
|
||||||
|
|
||||||
export declare interface FormatOptions {
|
export declare interface FormatOptions {
|
||||||
locale?: string;
|
locale?: string;
|
||||||
|
|
@ -7,15 +9,6 @@ export declare interface FormatOptions {
|
||||||
}
|
}
|
||||||
|
|
||||||
export declare class FormatHost {
|
export declare class FormatHost {
|
||||||
static properties: {
|
|
||||||
modelValue: { attribute: false };
|
|
||||||
formattedValue: { attribute: false };
|
|
||||||
serializedValue: { attribute: false };
|
|
||||||
formatOn: { attribute: false };
|
|
||||||
formatOptions: { attribute: false };
|
|
||||||
};
|
|
||||||
|
|
||||||
modelValue: unknown;
|
|
||||||
formattedValue: string;
|
formattedValue: string;
|
||||||
serializedValue: string;
|
serializedValue: string;
|
||||||
formatOn: string;
|
formatOn: string;
|
||||||
|
|
@ -32,7 +25,7 @@ export declare class FormatHost {
|
||||||
_calculateValues(opts: { source: 'model' | 'serialized' | 'formatted' | null }): void;
|
_calculateValues(opts: { source: 'model' | 'serialized' | 'formatted' | null }): void;
|
||||||
__callParser(value: string | undefined): object;
|
__callParser(value: string | undefined): object;
|
||||||
__callFormatter(): string;
|
__callFormatter(): string;
|
||||||
_onModelValueChanged(args: { modelValue: unknown }[]): void;
|
_onModelValueChanged(arg: { modelValue: unknown }): void;
|
||||||
_dispatchModelValueChangedEvent(): void;
|
_dispatchModelValueChangedEvent(): void;
|
||||||
_syncValueUpwards(): void;
|
_syncValueUpwards(): void;
|
||||||
_reflectBackFormattedValueToUser(): void;
|
_reflectBackFormattedValueToUser(): void;
|
||||||
|
|
@ -47,6 +40,12 @@ export declare class FormatHost {
|
||||||
|
|
||||||
export declare function FormatImplementation<T extends Constructor<LitElement>>(
|
export declare function FormatImplementation<T extends Constructor<LitElement>>(
|
||||||
superclass: T,
|
superclass: T,
|
||||||
): T & Constructor<FormatHost> & FormatHost;
|
): T &
|
||||||
|
Constructor<FormatHost> &
|
||||||
|
FormatHost &
|
||||||
|
Constructor<ValidateHost> &
|
||||||
|
typeof ValidateHost &
|
||||||
|
Constructor<FormControlHost> &
|
||||||
|
typeof FormControlHost;
|
||||||
|
|
||||||
export type FormatMixin = typeof FormatImplementation;
|
export type FormatMixin = typeof FormatImplementation;
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,8 @@
|
||||||
import { Constructor } from '@open-wc/dedupe-mixin';
|
import { Constructor } from '@open-wc/dedupe-mixin';
|
||||||
import { LitElement } from '@lion/core';
|
import { LitElement } from '@lion/core';
|
||||||
|
import { FormControlHost } from './FormControlMixinTypes';
|
||||||
|
|
||||||
export declare class InteractionStateHost {
|
export declare class InteractionStateHost {
|
||||||
static get properties(): {
|
|
||||||
touched: {
|
|
||||||
type: BooleanConstructor;
|
|
||||||
reflect: true;
|
|
||||||
};
|
|
||||||
dirty: {
|
|
||||||
type: BooleanConstructor;
|
|
||||||
reflect: true;
|
|
||||||
};
|
|
||||||
filled: {
|
|
||||||
type: BooleanConstructor;
|
|
||||||
reflect: true;
|
|
||||||
};
|
|
||||||
prefilled: {
|
|
||||||
attribute: false;
|
|
||||||
};
|
|
||||||
submitted: {
|
|
||||||
attribute: false;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
prefilled: boolean;
|
prefilled: boolean;
|
||||||
filled: boolean;
|
filled: boolean;
|
||||||
touched: boolean;
|
touched: boolean;
|
||||||
|
|
@ -43,6 +24,10 @@ export declare class InteractionStateHost {
|
||||||
|
|
||||||
export declare function InteractionStateImplementation<T extends Constructor<LitElement>>(
|
export declare function InteractionStateImplementation<T extends Constructor<LitElement>>(
|
||||||
superclass: T,
|
superclass: T,
|
||||||
): T & Constructor<InteractionStateHost> & InteractionStateHost;
|
): T &
|
||||||
|
Constructor<InteractionStateHost> &
|
||||||
|
InteractionStateHost &
|
||||||
|
Constructor<FormControlHost> &
|
||||||
|
typeof FormControlHost;
|
||||||
|
|
||||||
export type InteractionStateMixin = typeof InteractionStateImplementation;
|
export type InteractionStateMixin = typeof InteractionStateImplementation;
|
||||||
|
|
|
||||||
62
packages/form-core/types/choice-group/ChoiceGroupMixinTypes.d.ts
vendored
Normal file
62
packages/form-core/types/choice-group/ChoiceGroupMixinTypes.d.ts
vendored
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
import { Constructor } from '@open-wc/dedupe-mixin';
|
||||||
|
import { LitElement } from '@lion/core';
|
||||||
|
import { FormControlHost } from '../FormControlMixinTypes';
|
||||||
|
import { FormRegistrarHost } from '../registration/FormRegistrarMixinTypes';
|
||||||
|
import { InteractionStateHost } from '../InteractionStateMixinTypes';
|
||||||
|
|
||||||
|
export declare class ChoiceGroupHost {
|
||||||
|
multipleChoice: boolean;
|
||||||
|
|
||||||
|
connectedCallback(): void;
|
||||||
|
disconnectedCallback(): void;
|
||||||
|
|
||||||
|
get modelValue(): any;
|
||||||
|
|
||||||
|
set modelValue(value: any);
|
||||||
|
|
||||||
|
get serializedValue(): string;
|
||||||
|
|
||||||
|
set serializedValue(value: string);
|
||||||
|
|
||||||
|
get formattedValue(): string;
|
||||||
|
|
||||||
|
set formattedValue(value: string);
|
||||||
|
|
||||||
|
connectedCallback(): void;
|
||||||
|
|
||||||
|
disconnectedCallback(): void;
|
||||||
|
|
||||||
|
addFormElement(child: FormControlHost, indexToInsertAt: number): void;
|
||||||
|
|
||||||
|
_triggerInitialModelValueChangedEvent(): void;
|
||||||
|
|
||||||
|
_getFromAllFormElements(property: string, filterCondition: Function): void;
|
||||||
|
|
||||||
|
_throwWhenInvalidChildModelValue(child: FormControlHost): void;
|
||||||
|
|
||||||
|
_isEmpty(): void;
|
||||||
|
|
||||||
|
_checkSingleChoiceElements(ev: Event): void;
|
||||||
|
|
||||||
|
_getCheckedElements(): void;
|
||||||
|
|
||||||
|
_setCheckedElements(value: any, check: boolean): void;
|
||||||
|
|
||||||
|
__setChoiceGroupTouched(): void;
|
||||||
|
|
||||||
|
__delegateNameAttribute(child: FormControlHost): void;
|
||||||
|
|
||||||
|
_onBeforeRepropagateChildrenValues(ev: Event): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare function ChoiceGroupImplementation<T extends Constructor<LitElement>>(
|
||||||
|
superclass: T,
|
||||||
|
): T &
|
||||||
|
Constructor<ChoiceGroupHost> &
|
||||||
|
ChoiceGroupHost &
|
||||||
|
Constructor<FormRegistrarHost> &
|
||||||
|
typeof FormRegistrarHost &
|
||||||
|
Constructor<InteractionStateHost> &
|
||||||
|
typeof InteractionStateHost;
|
||||||
|
|
||||||
|
export type ChoiceGroupMixin = typeof ChoiceGroupImplementation;
|
||||||
69
packages/form-core/types/choice-group/ChoiceInputMixinTypes.d.ts
vendored
Normal file
69
packages/form-core/types/choice-group/ChoiceInputMixinTypes.d.ts
vendored
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
import { Constructor } from '@open-wc/dedupe-mixin';
|
||||||
|
import { CSSResult, LitElement, TemplateResult } from '@lion/core';
|
||||||
|
import { FormatHost } from '../FormatMixinTypes';
|
||||||
|
|
||||||
|
export interface ChoiceInputModelValue {
|
||||||
|
checked: boolean;
|
||||||
|
value: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare class ChoiceInputHost {
|
||||||
|
modelValue: ChoiceInputModelValue;
|
||||||
|
|
||||||
|
get choiceValue(): any;
|
||||||
|
|
||||||
|
set choiceValue(value: any);
|
||||||
|
|
||||||
|
protected requestUpdateInternal(name: string, oldValue: any): void;
|
||||||
|
|
||||||
|
firstUpdated(changedProperties: Map<string, any>): void;
|
||||||
|
|
||||||
|
updated(changedProperties: Map<string, any>): void;
|
||||||
|
|
||||||
|
static get styles(): CSSResult | CSSResult[];
|
||||||
|
|
||||||
|
render(): TemplateResult;
|
||||||
|
|
||||||
|
_choiceGraphicTemplate(): TemplateResult;
|
||||||
|
|
||||||
|
connectedCallback(): void;
|
||||||
|
disconnectedCallback(): void;
|
||||||
|
|
||||||
|
__toggleChecked(): void;
|
||||||
|
|
||||||
|
__syncModelCheckedToChecked(checked: boolean): void;
|
||||||
|
|
||||||
|
__syncCheckedToModel(checked: boolean): void;
|
||||||
|
|
||||||
|
__syncCheckedToInputElement(): void;
|
||||||
|
|
||||||
|
_proxyInputEvent(): void;
|
||||||
|
|
||||||
|
_onModelValueChanged(
|
||||||
|
newV: { modelValue: ChoiceInputModelValue },
|
||||||
|
oldV: { modelValue: ChoiceInputModelValue },
|
||||||
|
): void;
|
||||||
|
|
||||||
|
parser(): any;
|
||||||
|
|
||||||
|
formatter(modelValue: ChoiceInputModelValue): string;
|
||||||
|
|
||||||
|
_isEmpty(): void;
|
||||||
|
|
||||||
|
_syncValueUpwards(): void;
|
||||||
|
|
||||||
|
type: string;
|
||||||
|
|
||||||
|
_inputNode: HTMLInputElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare function ChoiceInputImplementation<T extends Constructor<LitElement>>(
|
||||||
|
superclass: T,
|
||||||
|
): T &
|
||||||
|
Constructor<ChoiceInputHost> &
|
||||||
|
ChoiceInputHost &
|
||||||
|
Constructor<FormatHost> &
|
||||||
|
FormatHost &
|
||||||
|
HTMLElement;
|
||||||
|
|
||||||
|
export type ChoiceInputMixin = typeof ChoiceInputImplementation;
|
||||||
43
packages/form-core/types/form-group/FormGroupMixinTypes.d.ts
vendored
Normal file
43
packages/form-core/types/form-group/FormGroupMixinTypes.d.ts
vendored
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { Constructor } from '@open-wc/dedupe-mixin';
|
||||||
|
import { LitElement } from '@lion/core';
|
||||||
|
import { DisabledHost } from '@lion/core/types/DisabledMixinTypes';
|
||||||
|
import { SlotHost } from '@lion/core/types/SlotMixinTypes';
|
||||||
|
import { FormControlHost } from '../FormControlMixinTypes';
|
||||||
|
import { FormRegistrarHost } from '../registration/FormRegistrarMixinTypes';
|
||||||
|
import { ValidateHost } from '../validate/ValidateMixinTypes';
|
||||||
|
|
||||||
|
export declare class FormGroupHost {
|
||||||
|
protected static _addDescriptionElementIdsToField(): void;
|
||||||
|
get _inputNode(): HTMLElement;
|
||||||
|
resetGroup(): void;
|
||||||
|
prefilled: boolean;
|
||||||
|
touched: boolean;
|
||||||
|
dirty: boolean;
|
||||||
|
submitted: boolean;
|
||||||
|
serializedValue: string;
|
||||||
|
modelValue: { [x: string]: any };
|
||||||
|
formattedValue: string;
|
||||||
|
children: Array<HTMLElement & FormControlHost>;
|
||||||
|
_initialModelValue: { [x: string]: any };
|
||||||
|
_setValueForAllFormElements(property: string, value: any): void;
|
||||||
|
resetInteractionState(): void;
|
||||||
|
clearGroup(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare function FormGroupImplementation<T extends Constructor<LitElement>>(
|
||||||
|
superclass: T,
|
||||||
|
): T &
|
||||||
|
Constructor<FormGroupHost> &
|
||||||
|
FormGroupHost &
|
||||||
|
Constructor<FormRegistrarHost> &
|
||||||
|
typeof FormRegistrarHost &
|
||||||
|
Constructor<FormControlHost> &
|
||||||
|
typeof FormControlHost &
|
||||||
|
Constructor<ValidateHost> &
|
||||||
|
typeof ValidateHost &
|
||||||
|
Constructor<DisabledHost> &
|
||||||
|
typeof DisabledHost &
|
||||||
|
Constructor<SlotHost> &
|
||||||
|
typeof SlotHost;
|
||||||
|
|
||||||
|
export type FormGroupMixin = typeof FormGroupImplementation;
|
||||||
|
|
@ -1,12 +1,15 @@
|
||||||
import { Constructor } from '@open-wc/dedupe-mixin';
|
import { Constructor } from '@open-wc/dedupe-mixin';
|
||||||
|
import { FormRegistrarHost } from './FormRegistrarMixinTypes';
|
||||||
|
import { LitElement } from '@lion/core';
|
||||||
|
|
||||||
export declare class FormRegisteringHost {
|
export declare class FormRegisteringHost {
|
||||||
connectedCallback(): void;
|
connectedCallback(): void;
|
||||||
disconnectedCallback(): void;
|
disconnectedCallback(): void;
|
||||||
|
__parentFormGroup?: FormRegistrarHost;
|
||||||
}
|
}
|
||||||
|
|
||||||
export declare function FormRegisteringImplementation<T extends Constructor<HTMLElement>>(
|
export declare function FormRegisteringImplementation<T extends Constructor<LitElement>>(
|
||||||
superclass: T,
|
superclass: T,
|
||||||
): T & Constructor<FormRegisteringHost> & FormRegisteringHost;
|
): T & Constructor<FormRegisteringHost> & typeof FormRegisteringHost;
|
||||||
|
|
||||||
export type FormRegisteringMixin = typeof FormRegisteringImplementation;
|
export type FormRegisteringMixin = typeof FormRegisteringImplementation;
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,35 @@
|
||||||
import { Constructor } from '@open-wc/dedupe-mixin';
|
import { Constructor } from '@open-wc/dedupe-mixin';
|
||||||
|
import { FormControlsCollection } from '../../src/registration/FormControlsCollection';
|
||||||
export declare class FormControlsCollection {
|
import { FormRegisteringHost } from '../../types/registration/FormRegisteringMixinTypes';
|
||||||
_keys(): string[];
|
import { FormControlHost } from '../../types/FormControlMixinTypes';
|
||||||
}
|
import { LitElement } from '@lion/core';
|
||||||
|
|
||||||
export declare class ElementWithParentFormGroup {
|
export declare class ElementWithParentFormGroup {
|
||||||
__parentFormGroup: FormRegistrarHost;
|
__parentFormGroup: FormRegistrarHost;
|
||||||
}
|
}
|
||||||
|
|
||||||
export declare class FormRegistrarHost {
|
export declare class FormRegistrarHost {
|
||||||
static get properties(): {
|
|
||||||
_isFormOrFieldset: {
|
|
||||||
type: BooleanConstructor;
|
|
||||||
reflect: boolean;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
_isFormOrFieldset: boolean;
|
_isFormOrFieldset: boolean;
|
||||||
formElements: FormControlsCollection;
|
formElements: FormControlsCollection & { [x: string]: any };
|
||||||
addFormElement(child: HTMLElement & ElementWithParentFormGroup, indexToInsertAt: number): void;
|
addFormElement(
|
||||||
|
child:
|
||||||
|
| (FormControlHost & ElementWithParentFormGroup)
|
||||||
|
| (FormControlHost & HTMLElement)
|
||||||
|
| (HTMLElement & ElementWithParentFormGroup),
|
||||||
|
indexToInsertAt?: number,
|
||||||
|
): void;
|
||||||
|
removeFormElement(child: FormRegisteringHost): void;
|
||||||
|
_onRequestToAddFormElement(e: CustomEvent): void;
|
||||||
|
isRegisteredFormElement(el: FormControlHost): boolean;
|
||||||
|
registrationComplete: Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export declare function FormRegistrarImplementation<T extends Constructor<HTMLElement>>(
|
export declare function FormRegistrarImplementation<T extends Constructor<LitElement>>(
|
||||||
superclass: T,
|
superclass: T,
|
||||||
): T & Constructor<FormRegistrarHost> & FormRegistrarHost;
|
): T &
|
||||||
|
Constructor<FormRegistrarHost> &
|
||||||
|
typeof FormRegistrarHost &
|
||||||
|
Constructor<FormRegisteringHost> &
|
||||||
|
typeof FormRegisteringHost;
|
||||||
|
|
||||||
export type FormRegistrarMixin = typeof FormRegistrarImplementation;
|
export type FormRegistrarMixin = typeof FormRegistrarImplementation;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import { Constructor } from '@open-wc/dedupe-mixin';
|
import { Constructor } from '@open-wc/dedupe-mixin';
|
||||||
|
import { LitElement } from '@lion/core';
|
||||||
|
|
||||||
export declare class FormRegistrarPortalHost {
|
export declare class FormRegistrarPortalHost {
|
||||||
__redispatchEventForFormRegistrarPortalMixin(ev: CustomEvent): void;
|
__redispatchEventForFormRegistrarPortalMixin(ev: CustomEvent): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export declare function FormRegistrarPortalImplementation<T extends Constructor<HTMLElement>>(
|
export declare function FormRegistrarPortalImplementation<T extends Constructor<LitElement>>(
|
||||||
superclass: T,
|
superclass: T,
|
||||||
): T & Constructor<FormRegistrarPortalHost> & FormRegistrarPortalHost;
|
): T & Constructor<FormRegistrarPortalHost> & FormRegistrarPortalHost;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,6 @@ export type SyncUpdatableHostType = typeof SyncUpdatableHost;
|
||||||
|
|
||||||
export declare function SyncUpdatableImplementation<T extends Constructor<LitElement>>(
|
export declare function SyncUpdatableImplementation<T extends Constructor<LitElement>>(
|
||||||
superclass: T,
|
superclass: T,
|
||||||
): T & Constructor<SyncUpdatableHost> & SyncUpdatableHost;
|
): T & Constructor<SyncUpdatableHost> & typeof SyncUpdatableHost;
|
||||||
|
|
||||||
export type SyncUpdatableMixin = typeof SyncUpdatableImplementation;
|
export type SyncUpdatableMixin = typeof SyncUpdatableImplementation;
|
||||||
|
|
|
||||||
90
packages/form-core/types/validate/ValidateMixinTypes.d.ts
vendored
Normal file
90
packages/form-core/types/validate/ValidateMixinTypes.d.ts
vendored
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
import { LitElement } from '@lion/core';
|
||||||
|
import { DisabledHost } from '@lion/core/types/DisabledMixinTypes';
|
||||||
|
import { SlotsMap, SlotHost } from '@lion/core/types/SlotMixinTypes';
|
||||||
|
import { Constructor } from '@open-wc/dedupe-mixin';
|
||||||
|
import { ScopedElementsHost } from '@open-wc/scoped-elements/src/types';
|
||||||
|
import { FormControlHost } from '../FormControlMixinTypes';
|
||||||
|
import { LionValidationFeedback } from '../../src/validate/LionValidationFeedback';
|
||||||
|
import { SyncUpdatableHost } from '../utils/SyncUpdatableMixinTypes';
|
||||||
|
import { Validator } from '../../src/validate/Validator';
|
||||||
|
|
||||||
|
type ScopedElementsMap = {
|
||||||
|
[key: string]: typeof HTMLElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
type FeedbackMessage = {
|
||||||
|
message: string | Node;
|
||||||
|
type: string;
|
||||||
|
validator?: Validator;
|
||||||
|
};
|
||||||
|
|
||||||
|
export declare class ValidateHost {
|
||||||
|
validators: Validator[];
|
||||||
|
hasFeedbackFor: string[];
|
||||||
|
shouldShowFeedbackFor: string[];
|
||||||
|
showsFeedbackFor: string[];
|
||||||
|
validationStates: { [key: string]: { [key: string]: Object } };
|
||||||
|
isPending: boolean;
|
||||||
|
defaultValidators: Validator[];
|
||||||
|
_visibleMessagesAmount: number;
|
||||||
|
fieldName: string;
|
||||||
|
|
||||||
|
static validationTypes: string[];
|
||||||
|
slots: SlotsMap;
|
||||||
|
_feedbackNode: LionValidationFeedback;
|
||||||
|
_allValidators: Validator[];
|
||||||
|
|
||||||
|
__syncValidationResult: Validator[];
|
||||||
|
__asyncValidationResult: Validator[];
|
||||||
|
__validationResult: Validator[];
|
||||||
|
__prevValidationResult: Validator[];
|
||||||
|
|
||||||
|
connectedCallback(): void;
|
||||||
|
disconnectedCallback(): void;
|
||||||
|
firstUpdated(changedProperties: import('lit-element').PropertyValues): void;
|
||||||
|
updateSync(name: string, oldValue: unknown): void;
|
||||||
|
updated(changedProperties: import('lit-element').PropertyValues): void;
|
||||||
|
|
||||||
|
validate(opts?: { clearCurrentResult?: boolean }): void;
|
||||||
|
__storePrevResult(): void;
|
||||||
|
__executeValidators(): void;
|
||||||
|
validateComplete: Promise<void>;
|
||||||
|
feedbackComplete: Promise<void>;
|
||||||
|
__validateCompleteResolve(): void;
|
||||||
|
__executeSyncValidators(
|
||||||
|
syncValidators: Validator[],
|
||||||
|
value: unknown,
|
||||||
|
opts: { hasAsync: boolean },
|
||||||
|
): void;
|
||||||
|
__executeAsyncValidators(asyncValidators: Validator[], value: unknown): void;
|
||||||
|
__executeResultValidators(regularValidationResult: Validator[]): Validator[];
|
||||||
|
__finishValidation(options: { source: 'sync' | 'async'; hasAsync?: boolean }): void;
|
||||||
|
__clearValidationResults(): void;
|
||||||
|
__onValidatorUpdated(e: Event | CustomEvent): void;
|
||||||
|
__setupValidators(): void;
|
||||||
|
__isEmpty(v: unknown): boolean;
|
||||||
|
__getFeedbackMessages(validators: Validator[]): Promise<FeedbackMessage[]>;
|
||||||
|
_updateFeedbackComponent(): void;
|
||||||
|
_showFeedbackConditionFor(type: string): boolean;
|
||||||
|
_hasFeedbackVisibleFor(type: string): boolean;
|
||||||
|
_updateShouldShowFeedbackFor(): void;
|
||||||
|
_prioritizeAndFilterFeedback(opts: { validationResult: Validator[] }): Validator[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare function ValidateImplementation<T extends Constructor<LitElement>>(
|
||||||
|
superclass: T,
|
||||||
|
): T &
|
||||||
|
Constructor<ValidateHost> &
|
||||||
|
typeof ValidateHost &
|
||||||
|
Constructor<FormControlHost> &
|
||||||
|
typeof FormControlHost &
|
||||||
|
Constructor<SyncUpdatableHost> &
|
||||||
|
typeof SyncUpdatableHost &
|
||||||
|
Constructor<DisabledHost> &
|
||||||
|
typeof DisabledHost &
|
||||||
|
Constructor<SlotHost> &
|
||||||
|
typeof SlotHost &
|
||||||
|
Constructor<ScopedElementsHost> &
|
||||||
|
typeof ScopedElementsHost;
|
||||||
|
|
||||||
|
export type ValidateMixin = typeof ValidateImplementation;
|
||||||
|
|
@ -163,9 +163,7 @@ export const feedbackCondition = () => {
|
||||||
</form>
|
</form>
|
||||||
</lion-form>
|
</lion-form>
|
||||||
<h-output .field="${fieldElement}" .show="${[...props, 'hasFeedbackFor']}"> </h-output>
|
<h-output .field="${fieldElement}" .show="${[...props, 'hasFeedbackFor']}"> </h-output>
|
||||||
<h3>
|
<h3>Set conditions for validation feedback visibility</h3>
|
||||||
Set conditions for validation feedback visibility
|
|
||||||
</h3>
|
|
||||||
<lion-checkbox-group name="props[]" @model-value-changed="${fetchConditionsAndReevaluate}">
|
<lion-checkbox-group name="props[]" @model-value-changed="${fetchConditionsAndReevaluate}">
|
||||||
${props.map(p => html` <lion-checkbox .label="${p}" .choiceValue="${p}"> </lion-checkbox> `)}
|
${props.map(p => html` <lion-checkbox .label="${p}" .choiceValue="${p}"> </lion-checkbox> `)}
|
||||||
</lion-checkbox-group>
|
</lion-checkbox-group>
|
||||||
|
|
|
||||||
|
|
@ -53,9 +53,7 @@ export const disabled = () => {
|
||||||
<lion-input name="LastName2" label="Last Name" .modelValue=${'Bar'}></lion-input>
|
<lion-input name="LastName2" label="Last Name" .modelValue=${'Bar'}></lion-input>
|
||||||
</lion-fieldset>
|
</lion-fieldset>
|
||||||
</lion-fieldset>
|
</lion-fieldset>
|
||||||
<button @click=${toggleDisabled}>
|
<button @click=${toggleDisabled}>Toggle disabled</button>
|
||||||
Toggle disabled
|
|
||||||
</button>
|
|
||||||
`;
|
`;
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -113,9 +113,7 @@ describe(`Submitting/Resetting Form`, async () => {
|
||||||
></lion-textarea>
|
></lion-textarea>
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<lion-button id="submit_button" type="submit" raised>Submit</lion-button>
|
<lion-button id="submit_button" type="submit" raised>Submit</lion-button>
|
||||||
<lion-button id="reset_button" type="reset" raised>
|
<lion-button id="reset_button" type="reset" raised> Reset </lion-button>
|
||||||
Reset
|
|
||||||
</lion-button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</lion-form>
|
</lion-form>
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,7 @@ export class SbLocaleSwitcher extends LitElement {
|
||||||
return html`
|
return html`
|
||||||
${this.showLocales.map(
|
${this.showLocales.map(
|
||||||
showLocale => html`
|
showLocale => html`
|
||||||
<button @click=${() => this.callback(showLocale)}>
|
<button @click=${() => this.callback(showLocale)}>${showLocale}</button>
|
||||||
${showLocale}
|
|
||||||
</button>
|
|
||||||
`,
|
`,
|
||||||
)}
|
)}
|
||||||
`;
|
`;
|
||||||
|
|
|
||||||
|
|
@ -225,9 +225,7 @@ export class LionInputDatepicker extends ScopedElementsMixin(OverlayMixin(LionIn
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return html`
|
return html`
|
||||||
<div class="form-field__group-one">
|
<div class="form-field__group-one">${this._groupOneTemplate()}</div>
|
||||||
${this._groupOneTemplate()}
|
|
||||||
</div>
|
|
||||||
<div class="form-field__group-two">
|
<div class="form-field__group-two">
|
||||||
${this._groupTwoTemplate()} ${this._overlayTemplate()}
|
${this._groupTwoTemplate()} ${this._overlayTemplate()}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -79,11 +79,7 @@ class MyHelloComponent extends LocalizeMixin(LitElement) {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return html`
|
return html` <div>${this.msgLit('my-hello-component:greeting')}</div> `;
|
||||||
<div>
|
|
||||||
${this.msgLit('my-hello-component:greeting')}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -112,9 +108,7 @@ import { localize } from '@lion/localize';
|
||||||
|
|
||||||
export function myTemplate(someData) {
|
export function myTemplate(someData) {
|
||||||
return html`
|
return html`
|
||||||
<div>
|
<div>${localize.msg('my-hello-component:feeling', { feeling: someData.feeling })}</div>
|
||||||
${localize.msg('my-hello-component:feeling', { feeling: someData.feeling })}
|
|
||||||
</div>
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
// @ts-expect-error no types for this package
|
||||||
import MessageFormat from '@bundled-es-modules/message-format/MessageFormat.js';
|
import MessageFormat from '@bundled-es-modules/message-format/MessageFormat.js';
|
||||||
import isLocalizeESModule from './isLocalizeESModule.js';
|
import isLocalizeESModule from './isLocalizeESModule.js';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,9 @@ export function getDateFormatBasedOnLocale() {
|
||||||
function getPartByIndex(index) {
|
function getPartByIndex(index) {
|
||||||
/** @type {Object.<string,string>} */
|
/** @type {Object.<string,string>} */
|
||||||
const template = {
|
const template = {
|
||||||
'2012': 'year',
|
2012: 'year',
|
||||||
'12': 'month',
|
12: 'month',
|
||||||
'20': 'day',
|
20: 'day',
|
||||||
};
|
};
|
||||||
const key = dateParts[index];
|
const key = dateParts[index];
|
||||||
return template[key];
|
return template[key];
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { expect, oneEvent, aTimeout } from '@open-wc/testing';
|
import { expect, oneEvent, aTimeout } from '@open-wc/testing';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
|
// @ts-expect-error no types for this package
|
||||||
import { fetchMock } from '@bundled-es-modules/fetch-mock';
|
import { fetchMock } from '@bundled-es-modules/fetch-mock';
|
||||||
import { setupFakeImport, resetFakeImport, fakeImport } from '../test-helpers.js';
|
import { setupFakeImport, resetFakeImport, fakeImport } from '../test-helpers.js';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@ describe('LocalizeMixin', () => {
|
||||||
'child-element': loc => fakeImport(`./child-element/${loc}.js`),
|
'child-element': loc => fakeImport(`./child-element/${loc}.js`),
|
||||||
};
|
};
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-expect-error
|
||||||
class ParentElement extends LocalizeMixin(LitElement) {
|
class ParentElement extends LocalizeMixin(LitElement) {
|
||||||
static get localizeNamespaces() {
|
static get localizeNamespaces() {
|
||||||
return [parentElementNs, defaultNs, ...super.localizeNamespaces];
|
return [parentElementNs, defaultNs, ...super.localizeNamespaces];
|
||||||
|
|
@ -79,7 +79,7 @@ describe('LocalizeMixin', () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const tagString = defineCE(
|
const tagString = defineCE(
|
||||||
// @ts-ignore
|
// @ts-expect-error
|
||||||
class ChildElement extends LocalizeMixin(ParentElement) {
|
class ChildElement extends LocalizeMixin(ParentElement) {
|
||||||
static get localizeNamespaces() {
|
static get localizeNamespaces() {
|
||||||
return [childElementNs, defaultNs, ...super.localizeNamespaces];
|
return [childElementNs, defaultNs, ...super.localizeNamespaces];
|
||||||
|
|
|
||||||
|
|
@ -91,9 +91,7 @@ export const isTooltip = () => {
|
||||||
<button slot="invoker" @mouseenter="${showTooltip}" @mouseleave="${hideTooltip}">
|
<button slot="invoker" @mouseenter="${showTooltip}" @mouseleave="${hideTooltip}">
|
||||||
Hover me to open the tooltip!
|
Hover me to open the tooltip!
|
||||||
</button>
|
</button>
|
||||||
<div slot="content" class="demo-overlay">
|
<div slot="content" class="demo-overlay">Hello!</div>
|
||||||
Hello!
|
|
||||||
</div>
|
|
||||||
</demo-overlay-system>
|
</demo-overlay-system>
|
||||||
`;
|
`;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -167,11 +167,7 @@ export function runOverlayMixinSuite({ tagString, tag, suffix = '' }) {
|
||||||
function sendCloseEvent(e) {
|
function sendCloseEvent(e) {
|
||||||
e.target.dispatchEvent(new Event('close-overlay', { bubbles: true }));
|
e.target.dispatchEvent(new Event('close-overlay', { bubbles: true }));
|
||||||
}
|
}
|
||||||
const closeBtn = await fixture(html`
|
const closeBtn = await fixture(html` <button @click=${sendCloseEvent}>close</button> `);
|
||||||
<button @click=${sendCloseEvent}>
|
|
||||||
close
|
|
||||||
</button>
|
|
||||||
`);
|
|
||||||
|
|
||||||
const el = await fixture(html`
|
const el = await fixture(html`
|
||||||
<${tag} opened>
|
<${tag} opened>
|
||||||
|
|
|
||||||
|
|
@ -67,11 +67,7 @@ describe('OverlayController', () => {
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
if (mode === 'inline') {
|
if (mode === 'inline') {
|
||||||
contentNode = await fixture(html`
|
contentNode = await fixture(html` <div>I should be on top</div> `);
|
||||||
<div>
|
|
||||||
I should be on top
|
|
||||||
</div>
|
|
||||||
`);
|
|
||||||
contentNode.style.zIndex = zIndexVal;
|
contentNode.style.zIndex = zIndexVal;
|
||||||
}
|
}
|
||||||
return contentNode;
|
return contentNode;
|
||||||
|
|
|
||||||
|
|
@ -103,9 +103,7 @@ describe('Local Positioning', () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await fixture(html`
|
await fixture(html`
|
||||||
<div style="position: absolute; top: 0;">
|
<div style="position: absolute; top: 0;">${ctrl.invokerNode}${ctrl.content}</div>
|
||||||
${ctrl.invokerNode}${ctrl.content}
|
|
||||||
</div>
|
|
||||||
`);
|
`);
|
||||||
|
|
||||||
await ctrl.show();
|
await ctrl.show();
|
||||||
|
|
|
||||||
|
|
@ -35,9 +35,7 @@ const interactionElementsNode = renderLitAsNode(html`
|
||||||
const lightDomTemplate = html`
|
const lightDomTemplate = html`
|
||||||
<div>
|
<div>
|
||||||
<button id="outside-1">outside 1</button>
|
<button id="outside-1">outside 1</button>
|
||||||
<div id="rootElement">
|
<div id="rootElement">${interactionElementsNode}</div>
|
||||||
${interactionElementsNode}
|
|
||||||
</div>
|
|
||||||
<button id="outside-2">outside 2</button>
|
<button id="outside-2">outside 2</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
|
||||||
|
|
@ -94,17 +94,11 @@ export const methods = () => {
|
||||||
<button @click=${() => document.getElementById('pagination-method').previous()}>
|
<button @click=${() => document.getElementById('pagination-method').previous()}>
|
||||||
Previous
|
Previous
|
||||||
</button>
|
</button>
|
||||||
<button @click=${() => document.getElementById('pagination-method').next()}>
|
<button @click=${() => document.getElementById('pagination-method').next()}>Next</button>
|
||||||
Next
|
|
||||||
</button>
|
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
<button @click=${() => document.getElementById('pagination-method').first()}>
|
<button @click=${() => document.getElementById('pagination-method').first()}>First</button>
|
||||||
First
|
<button @click=${() => document.getElementById('pagination-method').last()}>Last</button>
|
||||||
</button>
|
|
||||||
<button @click=${() => document.getElementById('pagination-method').last()}>
|
|
||||||
Last
|
|
||||||
</button>
|
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
<button @click=${() => document.getElementById('pagination-method').goto(55)}>
|
<button @click=${() => document.getElementById('pagination-method').goto(55)}>
|
||||||
|
|
|
||||||
|
|
@ -204,9 +204,7 @@ class PBoard extends DecorateMixin(LitElement) {
|
||||||
<h1 class="heading">providence <span class="heading__part">dashboard</span> (alpha)</h1>
|
<h1 class="heading">providence <span class="heading__part">dashboard</span> (alpha)</h1>
|
||||||
<div class="u-ml2">
|
<div class="u-ml2">
|
||||||
${this._activeAnalyzerSelectTemplate()}
|
${this._activeAnalyzerSelectTemplate()}
|
||||||
<button @click="${() => downloadFile('data.csv', this._createCsv())}">
|
<button @click="${() => downloadFile('data.csv', this._createCsv())}">get csv</button>
|
||||||
get csv
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
${this._selectionMenuTemplate(this.__menuData)}
|
${this._selectionMenuTemplate(this.__menuData)}
|
||||||
|
|
|
||||||
|
|
@ -367,9 +367,7 @@ You can use this `selectedElement` to then render the content to your own invoke
|
||||||
```html
|
```html
|
||||||
<lion-select-rich>
|
<lion-select-rich>
|
||||||
<my-invoker-button slot="invoker"></my-invoker-button>
|
<my-invoker-button slot="invoker"></my-invoker-button>
|
||||||
<lion-options slot="input">
|
<lion-options slot="input"> ... </lion-options>
|
||||||
...
|
|
||||||
</lion-options>
|
|
||||||
</lion-select-rich>
|
</lion-select-rich>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,14 @@ export class LionOption extends DisabledMixin(ChoiceInputMixin(FormRegisteringMi
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @override We want to start with a clean slate, so we omit slots inherited from FormControl
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
get slots() {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.active = false;
|
this.active = false;
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,7 @@ export class LionSelectInvoker extends LionButton {
|
||||||
|
|
||||||
_contentTemplate() {
|
_contentTemplate() {
|
||||||
if (this.selectedElement) {
|
if (this.selectedElement) {
|
||||||
const labelNodes = Array.from(this.selectedElement.querySelectorAll('*'));
|
const labelNodes = Array.from(this.selectedElement.childNodes);
|
||||||
if (labelNodes.length > 0) {
|
if (labelNodes.length > 0) {
|
||||||
return labelNodes.map(node => node.cloneNode(true));
|
return labelNodes.map(node => node.cloneNode(true));
|
||||||
}
|
}
|
||||||
|
|
@ -113,11 +113,7 @@ export class LionSelectInvoker extends LionButton {
|
||||||
}
|
}
|
||||||
|
|
||||||
_beforeTemplate() {
|
_beforeTemplate() {
|
||||||
return html`
|
return html` <div id="content-wrapper">${this._contentTemplate()}</div> `;
|
||||||
<div id="content-wrapper">
|
|
||||||
${this._contentTemplate()}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line class-methods-use-this
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,14 @@ describe('lion-select-invoker', () => {
|
||||||
|
|
||||||
it('renders invoker info based on selectedElement child elements', async () => {
|
it('renders invoker info based on selectedElement child elements', async () => {
|
||||||
const el = await fixture(html`<lion-select-invoker></lion-select-invoker>`);
|
const el = await fixture(html`<lion-select-invoker></lion-select-invoker>`);
|
||||||
el.selectedElement = await fixture(`<div class="option"><h2>I am</h2><p>2 lines</p></div>`);
|
el.selectedElement = await fixture(
|
||||||
|
`<div class="option">Textnode<h2>I am</h2><p>2 lines</p></div>`,
|
||||||
|
);
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
|
|
||||||
expect(el._contentWrapperNode).lightDom.to.equal(
|
expect(el._contentWrapperNode).lightDom.to.equal(
|
||||||
`
|
`
|
||||||
|
Textnode
|
||||||
<h2>I am</h2>
|
<h2>I am</h2>
|
||||||
<p>2 lines</p>
|
<p>2 lines</p>
|
||||||
`,
|
`,
|
||||||
|
|
|
||||||
|
|
@ -38,12 +38,8 @@ export class LionSwitch extends ScopedElementsMixin(ChoiceInputMixin(LionField))
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
return html`
|
return html`
|
||||||
<div class="form-field__group-one">
|
<div class="form-field__group-one">${this._groupOneTemplate()}</div>
|
||||||
${this._groupOneTemplate()}
|
<div class="form-field__group-two">${this._groupTwoTemplate()}</div>
|
||||||
</div>
|
|
||||||
<div class="form-field__group-two">
|
|
||||||
${this._groupTwoTemplate()}
|
|
||||||
</div>
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue