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
|
||||
<lea-tabs>
|
||||
<lea-tab slot="tab">Info</lea-tab>
|
||||
<lea-tab-panel slot="panel">
|
||||
Info page with lots of information about us.
|
||||
</lea-tab-panel>
|
||||
<lea-tab-panel slot="panel"> Info page with lots of information about us. </lea-tab-panel>
|
||||
<lea-tab slot="tab">Work</lea-tab>
|
||||
<lea-tab-panel slot="panel">
|
||||
Work page that showcases our work.
|
||||
</lea-tab-panel>
|
||||
<lea-tab-panel slot="panel"> Work page that showcases our work. </lea-tab-panel>
|
||||
</lea-tabs>
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -23,13 +23,9 @@ export default {
|
|||
export const main = () => html`
|
||||
<lea-tabs>
|
||||
<lea-tab slot="tab">Info</lea-tab>
|
||||
<lea-tab-panel slot="panel">
|
||||
Info page with lots of information about us.
|
||||
</lea-tab-panel>
|
||||
<lea-tab-panel slot="panel"> Info page with lots of information about us. </lea-tab-panel>
|
||||
<lea-tab slot="tab">Work</lea-tab>
|
||||
<lea-tab-panel slot="panel">
|
||||
Work page that showcases our work.
|
||||
</lea-tab-panel>
|
||||
<lea-tab-panel slot="panel"> Work page that showcases our work. </lea-tab-panel>
|
||||
</lea-tabs>
|
||||
`;
|
||||
```
|
||||
|
|
@ -51,13 +47,9 @@ import '@lion/tabs/lea-tabs.js';
|
|||
```html
|
||||
<lea-tabs>
|
||||
<lea-tab slot="tab">Info</lea-tab>
|
||||
<lea-tab-panel slot="panel">
|
||||
Info page with lots of information about us.
|
||||
</lea-tab-panel>
|
||||
<lea-tab-panel slot="panel"> Info page with lots of information about us. </lea-tab-panel>
|
||||
<lea-tab slot="tab">Work</lea-tab>
|
||||
<lea-tab-panel slot="panel">
|
||||
Work page that showcases our work.
|
||||
</lea-tab-panel>
|
||||
<lea-tab-panel slot="panel"> Work page that showcases our work. </lea-tab-panel>
|
||||
</lea-tabs>
|
||||
```
|
||||
|
||||
|
|
@ -71,13 +63,9 @@ You can set the `selectedIndex` to select a certain tab.
|
|||
export const selectedIndex = () => html`
|
||||
<lea-tabs .selectedIndex=${1}>
|
||||
<lea-tab slot="tab">Info</lea-tab>
|
||||
<lea-tab-panel slot="panel">
|
||||
Info page with lots of information about us.
|
||||
</lea-tab-panel>
|
||||
<lea-tab-panel slot="panel"> Info page with lots of information about us. </lea-tab-panel>
|
||||
<lea-tab slot="tab">Work</lea-tab>
|
||||
<lea-tab-panel slot="panel">
|
||||
Work page that showcases our work.
|
||||
</lea-tab-panel>
|
||||
<lea-tab-panel slot="panel"> Work page that showcases our work. </lea-tab-panel>
|
||||
</lea-tabs>
|
||||
`;
|
||||
```
|
||||
|
|
@ -93,12 +81,8 @@ export const slotsOrder = () => html`
|
|||
<lea-tabs>
|
||||
<lea-tab slot="tab">Info</lea-tab>
|
||||
<lea-tab slot="tab">Work</lea-tab>
|
||||
<lea-tab-panel slot="panel">
|
||||
Info page with lots of information about us.
|
||||
</lea-tab-panel>
|
||||
<lea-tab-panel slot="panel">
|
||||
Work page that showcases our work.
|
||||
</lea-tab-panel>
|
||||
<lea-tab-panel slot="panel"> Info page with lots of information about us. </lea-tab-panel>
|
||||
<lea-tab-panel slot="panel"> Work page that showcases our work. </lea-tab-panel>
|
||||
</lea-tabs>
|
||||
`;
|
||||
```
|
||||
|
|
@ -122,9 +106,7 @@ export const distributeNewElements = () => {
|
|||
render() {
|
||||
return html`
|
||||
<h3>Append</h3>
|
||||
<button @click="${this.__handleAppendClick}">
|
||||
Append
|
||||
</button>
|
||||
<button @click="${this.__handleAppendClick}">Append</button>
|
||||
<lea-tabs id="appendTabs">
|
||||
<lea-tab slot="tab">tab 1</lea-tab>
|
||||
<lea-tab-panel slot="panel">panel 1</lea-tab-panel>
|
||||
|
|
@ -133,9 +115,7 @@ export const distributeNewElements = () => {
|
|||
</lea-tabs>
|
||||
<hr />
|
||||
<h3>Push</h3>
|
||||
<button @click="${this.__handlePushClick}">
|
||||
Push
|
||||
</button>
|
||||
<button @click="${this.__handlePushClick}">Push</button>
|
||||
<lea-tabs id="pushTabs">
|
||||
<lea-tab slot="tab">tab 1</lea-tab>
|
||||
<lea-tab-panel slot="panel">panel 1</lea-tab-panel>
|
||||
|
|
|
|||
|
|
@ -265,13 +265,9 @@ export const specialFeature = () =>
|
|||
html`
|
||||
<lea-tabs>
|
||||
<lea-tab slot="tab">Info</lea-tab>
|
||||
<lea-tab-panel slot="panel">
|
||||
Info page with lots of information about us.
|
||||
</lea-tab-panel>
|
||||
<lea-tab-panel slot="panel"> Info page with lots of information about us. </lea-tab-panel>
|
||||
<lea-tab slot="tab">Work</lea-tab>
|
||||
<lea-tab-panel slot="panel">
|
||||
Work page that showcases our work.
|
||||
</lea-tab-panel>
|
||||
<lea-tab-panel slot="panel"> Work page that showcases our work. </lea-tab-panel>
|
||||
</lea-tabs>
|
||||
`;
|
||||
```
|
||||
|
|
|
|||
|
|
@ -21,9 +21,7 @@ export const main = () => html`
|
|||
<h3 slot="invoker">
|
||||
<button>Lorem</button>
|
||||
</h3>
|
||||
<p slot="content">
|
||||
Lorem ipsum dolor sit, amet consectetur adipisicing elit.
|
||||
</p>
|
||||
<p slot="content">Lorem ipsum dolor sit, amet consectetur adipisicing elit.</p>
|
||||
<h3 slot="invoker">
|
||||
<button>Laboriosam</button>
|
||||
</h3>
|
||||
|
|
@ -56,9 +54,7 @@ import '@lion/accordion/lion-accordion.js';
|
|||
<h3 slot="invoker">
|
||||
<button>Lorem</button>
|
||||
</h3>
|
||||
<p slot="content">
|
||||
Lorem ipsum dolor sit, amet consectetur adipisicing elit.
|
||||
</p>
|
||||
<p slot="content">Lorem ipsum dolor sit, amet consectetur adipisicing elit.</p>
|
||||
<h3 slot="invoker">
|
||||
<button>Laboriosam</button>
|
||||
</h3>
|
||||
|
|
@ -83,9 +79,7 @@ export const expanded = () => html`
|
|||
<h3 slot="invoker">
|
||||
<button>Lorem</button>
|
||||
</h3>
|
||||
<p slot="content">
|
||||
Lorem ipsum dolor sit, amet consectetur adipisicing elit.
|
||||
</p>
|
||||
<p slot="content">Lorem ipsum dolor sit, amet consectetur adipisicing elit.</p>
|
||||
<h3 slot="invoker">
|
||||
<button>Laboriosam</button>
|
||||
</h3>
|
||||
|
|
@ -109,9 +103,7 @@ export const slotsOrder = () => html`
|
|||
<h3 slot="invoker">
|
||||
<button>Lorem</button>
|
||||
</h3>
|
||||
<p slot="content">
|
||||
Lorem ipsum dolor sit, amet consectetur adipisicing elit.
|
||||
</p>
|
||||
<p slot="content">Lorem ipsum dolor sit, amet consectetur adipisicing elit.</p>
|
||||
<h3 slot="invoker">
|
||||
<button>Laboriosam</button>
|
||||
</h3>
|
||||
|
|
@ -152,9 +144,7 @@ export const distributeNewElement = () => {
|
|||
</h4>
|
||||
<p slot="content">content 2</p>
|
||||
</lion-accordion>
|
||||
<button @click="${this.__handleAppendClick}">
|
||||
Append
|
||||
</button>
|
||||
<button @click="${this.__handleAppendClick}">Append</button>
|
||||
<hr />
|
||||
<h3>Push</h3>
|
||||
<lion-accordion id="pushTabs">
|
||||
|
|
@ -173,9 +163,7 @@ export const distributeNewElement = () => {
|
|||
`,
|
||||
)}
|
||||
</lion-accordion>
|
||||
<button @click="${this.__handlePushClick}">
|
||||
Push
|
||||
</button>
|
||||
<button @click="${this.__handlePushClick}">Push</button>
|
||||
`;
|
||||
}
|
||||
constructor() {
|
||||
|
|
|
|||
|
|
@ -276,9 +276,7 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
|
|||
return html`
|
||||
<div class="calendar__navigation__month">
|
||||
${this.__renderPreviousButton('Month', previousMonth, previousYear)}
|
||||
<h2 class="calendar__navigation-heading" id="month" aria-atomic="true">
|
||||
${month}
|
||||
</h2>
|
||||
<h2 class="calendar__navigation-heading" id="month" aria-atomic="true">${month}</h2>
|
||||
${this.__renderNextButton('Month', nextMonth, nextYear)}
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -291,9 +289,7 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
|
|||
return html`
|
||||
<div class="calendar__navigation__year">
|
||||
${this.__renderPreviousButton('FullYear', month, previousYear)}
|
||||
<h2 class="calendar__navigation-heading" id="year" aria-atomic="true">
|
||||
${year}
|
||||
</h2>
|
||||
<h2 class="calendar__navigation-heading" id="year" aria-atomic="true">${year}</h2>
|
||||
${this.__renderNextButton('FullYear', month, nextYear)}
|
||||
</div>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -67,9 +67,7 @@ export function dayTemplate(day, { weekdays, monthsLabels = defaultMonthLabels }
|
|||
?current-month=${day.currentMonth}
|
||||
?next-month=${day.nextMonth}
|
||||
>
|
||||
<span class="calendar__day-button__text">
|
||||
${day.date.getDate()}
|
||||
</span>
|
||||
<span class="calendar__day-button__text"> ${day.date.getDate()} </span>
|
||||
</button>
|
||||
</td>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -52,9 +52,7 @@ import '@lion/collapsible/lion-collapsible.js';
|
|||
```html
|
||||
<lion-collapsible>
|
||||
<button slot="invoker">Invoker Text</button>
|
||||
<div slot="content">
|
||||
Extra content
|
||||
</div>
|
||||
<div slot="content">Extra content</div>
|
||||
</lion-collapsible>
|
||||
```
|
||||
|
||||
|
|
@ -97,12 +95,8 @@ export const methods = () => html`
|
|||
<button @click=${() => document.querySelector('#car-collapsible').toggle()}>
|
||||
Toggle content
|
||||
</button>
|
||||
<button @click=${() => document.querySelector('#car-collapsible').show()}>
|
||||
Show content
|
||||
</button>
|
||||
<button @click=${() => document.querySelector('#car-collapsible').hide()}>
|
||||
Hide content
|
||||
</button>
|
||||
<button @click=${() => document.querySelector('#car-collapsible').show()}>Show content</button>
|
||||
<button @click=${() => document.querySelector('#car-collapsible').hide()}>Hide content</button>
|
||||
</section>
|
||||
`;
|
||||
```
|
||||
|
|
@ -140,9 +134,7 @@ A custom template can be specified to the `invoker` slot. It can be any button o
|
|||
```js preview-story
|
||||
export const customInvokerTemplate = () => html`
|
||||
<lion-collapsible>
|
||||
<button class="demo-custom-collapsible-invoker" slot="invoker">
|
||||
MORE ABOUT CARS
|
||||
</button>
|
||||
<button class="demo-custom-collapsible-invoker" slot="invoker">MORE ABOUT CARS</button>
|
||||
<div slot="content">
|
||||
Most definitions of cars say that they run primarily on roads, seat one to eight people, have
|
||||
four tires, and mainly transport people rather than goods.
|
||||
|
|
@ -170,9 +162,7 @@ export const customAnimation = () => html`
|
|||
vehicle.
|
||||
</div>
|
||||
<custom-collapsible>
|
||||
<button class="demo-custom-collapsible-invoker" slot="invoker">
|
||||
MORE ABOUT MOTORCYCLES
|
||||
</button>
|
||||
<button class="demo-custom-collapsible-invoker" slot="invoker">MORE ABOUT MOTORCYCLES</button>
|
||||
<div slot="content">
|
||||
Motorcycle design varies greatly to suit a range of different purposes: long distance
|
||||
travel, commuting, cruising, sport including racing, and off-road riding. Motorcycling is
|
||||
|
|
@ -186,9 +176,7 @@ export const customAnimation = () => html`
|
|||
A car (or automobile) is a wheeled motor vehicle used for transportation.
|
||||
</div>
|
||||
<custom-collapsible opened>
|
||||
<button class="demo-custom-collapsible-invoker" slot="invoker">
|
||||
MORE ABOUT CARS
|
||||
</button>
|
||||
<button class="demo-custom-collapsible-invoker" slot="invoker">MORE ABOUT CARS</button>
|
||||
<div slot="content">
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -40,7 +40,9 @@ Since Scoped Elements changes tagnames under the hood, a tagname querySelector s
|
|||
like this:
|
||||
|
||||
```js
|
||||
this.querySelector(this.constructor.getScopedTagName('lion-input', this.constructor.scopedElements));
|
||||
this.querySelector(
|
||||
this.constructor.getScopedTagName('lion-input', this.constructor.scopedElements),
|
||||
);
|
||||
```
|
||||
|
||||
## CSS selectors
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@
|
|||
"sideEffects": false,
|
||||
"dependencies": {
|
||||
"@open-wc/dedupe-mixin": "^1.2.18",
|
||||
"@open-wc/scoped-elements": "^1.0.3",
|
||||
"@open-wc/scoped-elements": "^1.2.2",
|
||||
"lit-element": "~2.4.0",
|
||||
"lit-html": "^1.3.0"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import { dedupeMixin } from '@open-wc/dedupe-mixin';
|
|||
*/
|
||||
const DelegateMixinImplementation = superclass =>
|
||||
// eslint-disable-next-line
|
||||
class DelegateMixin extends superclass {
|
||||
class extends superclass {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { dedupeMixin } from '@open-wc/dedupe-mixin';
|
|||
*/
|
||||
const DisabledMixinImplementation = superclass =>
|
||||
// eslint-disable-next-line no-shadow
|
||||
class DisabledMixinHost extends superclass {
|
||||
class extends superclass {
|
||||
static get properties() {
|
||||
return {
|
||||
disabled: {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { DisabledMixin } from './DisabledMixin.js';
|
|||
*/
|
||||
const DisabledWithTabIndexMixinImplementation = superclass =>
|
||||
// eslint-disable-next-line no-shadow
|
||||
class DisabledWithTabIndexMixinHost extends DisabledMixin(superclass) {
|
||||
class extends DisabledMixin(superclass) {
|
||||
static get properties() {
|
||||
return {
|
||||
// we use a property here as if we use the native tabIndex we can not set a default value
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { dedupeMixin } from '@open-wc/dedupe-mixin';
|
|||
*/
|
||||
const SlotMixinImplementation = superclass =>
|
||||
// eslint-disable-next-line no-unused-vars, no-shadow
|
||||
class SlotMixinHost extends superclass {
|
||||
class extends superclass {
|
||||
/**
|
||||
* @return {SlotsMap}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { dedupeMixin } from '@open-wc/dedupe-mixin';
|
|||
*/
|
||||
const UpdateStylesMixinImplementation = superclass =>
|
||||
// eslint-disable-next-line no-shadow
|
||||
class UpdateStylesMixinHost extends superclass {
|
||||
class extends superclass {
|
||||
/**
|
||||
* @example
|
||||
* <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[];
|
||||
};
|
||||
|
||||
export declare class DelegateMixinHost {
|
||||
export declare class DelegateHost {
|
||||
delegations: Delegations;
|
||||
|
||||
protected _connectDelegateMixin(): void;
|
||||
|
|
@ -50,6 +50,6 @@ export declare class DelegateMixinHost {
|
|||
*/
|
||||
declare function DelegateMixinImplementation<T extends Constructor<LitElement>>(
|
||||
superclass: T,
|
||||
): T & Constructor<DelegateMixinHost>;
|
||||
): T & Constructor<DelegateHost>;
|
||||
|
||||
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 { LitElement } from 'lit-element';
|
||||
|
||||
export declare class DisabledMixinHost {
|
||||
static get properties(): {
|
||||
disabled: {
|
||||
type: BooleanConstructor;
|
||||
reflect: boolean;
|
||||
};
|
||||
};
|
||||
export declare class DisabledHost {
|
||||
disabled: boolean;
|
||||
|
||||
/**
|
||||
|
|
@ -26,6 +20,6 @@ export declare class DisabledMixinHost {
|
|||
|
||||
export declare function DisabledMixinImplementation<T extends Constructor<LitElement>>(
|
||||
superclass: T,
|
||||
): T & Constructor<DisabledMixinHost>;
|
||||
): T & Constructor<DisabledHost>;
|
||||
|
||||
export type DisabledMixin = typeof DisabledMixinImplementation;
|
||||
|
|
|
|||
|
|
@ -1,14 +1,7 @@
|
|||
import { Constructor } from '@open-wc/dedupe-mixin';
|
||||
import { DisabledMixinHost } from './DisabledMixinTypes';
|
||||
import { DisabledHost } from './DisabledMixinTypes';
|
||||
import { LitElement } from 'lit-element';
|
||||
export declare class DisabledWithTabIndexMixinHost {
|
||||
static get properties(): {
|
||||
tabIndex: {
|
||||
type: NumberConstructor;
|
||||
reflect: boolean;
|
||||
attribute: string;
|
||||
};
|
||||
};
|
||||
export declare class DisabledWithTabIndexHost {
|
||||
tabIndex: number;
|
||||
/**
|
||||
* Makes request to make the element disabled and set the tabindex
|
||||
|
|
@ -27,6 +20,6 @@ export declare class DisabledWithTabIndexMixinHost {
|
|||
|
||||
export declare function DisabledWithTabIndexMixinImplementation<T extends Constructor<LitElement>>(
|
||||
superclass: T,
|
||||
): T & Constructor<DisabledWithTabIndexMixinHost> & Constructor<DisabledMixinHost>;
|
||||
): T & Constructor<DisabledWithTabIndexHost> & Constructor<DisabledHost>;
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
export declare class SlotMixinHost {
|
||||
export declare class SlotHost {
|
||||
/**
|
||||
* Obtains all the slots to create
|
||||
*/
|
||||
|
|
@ -50,6 +50,6 @@ export declare class SlotMixinHost {
|
|||
*/
|
||||
export declare function SlotMixinImplementation<T extends Constructor<HTMLElement>>(
|
||||
superclass: T,
|
||||
): T & Constructor<SlotMixinHost>;
|
||||
): T & Constructor<SlotHost>;
|
||||
|
||||
export type SlotMixin = typeof SlotMixinImplementation;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { Constructor } from '@open-wc/dedupe-mixin';
|
|||
export type StylesMap = {
|
||||
[key: string]: string;
|
||||
};
|
||||
export declare class UpdateStylesMixinHost {
|
||||
export declare class UpdateStylesHost {
|
||||
/**
|
||||
* @example
|
||||
* <my-element>
|
||||
|
|
@ -29,6 +29,6 @@ export declare class UpdateStylesMixinHost {
|
|||
*/
|
||||
declare function UpdateStylesMixinImplementation<T extends Constructor<HTMLElement>>(
|
||||
superclass: T,
|
||||
): T & Constructor<UpdateStylesMixinHost>;
|
||||
): T & Constructor<UpdateStylesHost>;
|
||||
|
||||
export type UpdateStylesMixin = typeof UpdateStylesMixinImplementation;
|
||||
|
|
|
|||
|
|
@ -27,9 +27,7 @@ describe('lion-dialog', () => {
|
|||
it('should show content on invoker click', async () => {
|
||||
const el = await fixture(html`
|
||||
<lion-dialog>
|
||||
<div slot="content" class="dialog">
|
||||
Hey there
|
||||
</div>
|
||||
<div slot="content" class="dialog">Hey there</div>
|
||||
<button slot="invoker">Popup button</button>
|
||||
</lion-dialog>
|
||||
`);
|
||||
|
|
@ -45,9 +43,7 @@ describe('lion-dialog', () => {
|
|||
<div slot="content">
|
||||
open nested overlay:
|
||||
<lion-dialog>
|
||||
<div slot="content">
|
||||
Nested content
|
||||
</div>
|
||||
<div slot="content">Nested content</div>
|
||||
<button slot="invoker">nested invoker button</button>
|
||||
</lion-dialog>
|
||||
</div>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,11 +1,13 @@
|
|||
import { dedupeMixin } from '@lion/core';
|
||||
import { FormControlMixin } from './FormControlMixin.js';
|
||||
/**
|
||||
* @typedef {import('../types/FocusMixinTypes').FocusMixin} FocusMixin
|
||||
* @type {FocusMixin}
|
||||
* @param {import('@open-wc/dedupe-mixin').Constructor<import('@lion/core').LitElement>} superclass
|
||||
*/
|
||||
const FocusMixinImplementation = superclass =>
|
||||
// eslint-disable-next-line no-unused-vars, max-len, no-shadow
|
||||
class FocusMixin extends superclass {
|
||||
class FocusMixin extends FormControlMixin(superclass) {
|
||||
static get properties() {
|
||||
return {
|
||||
focused: {
|
||||
|
|
@ -21,16 +23,12 @@ const FocusMixinImplementation = superclass =>
|
|||
}
|
||||
|
||||
connectedCallback() {
|
||||
if (super.connectedCallback) {
|
||||
super.connectedCallback();
|
||||
}
|
||||
this.__registerEventsForFocusMixin();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
if (super.disconnectedCallback) {
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
this.__teardownEventsForFocusMixin();
|
||||
}
|
||||
|
||||
|
|
@ -101,10 +99,22 @@ const FocusMixinImplementation = superclass =>
|
|||
}
|
||||
|
||||
__teardownEventsForFocusMixin() {
|
||||
this._inputNode.removeEventListener('focus', this.__redispatchFocus);
|
||||
this._inputNode.removeEventListener('blur', this.__redispatchBlur);
|
||||
this._inputNode.removeEventListener('focusin', this.__redispatchFocusin);
|
||||
this._inputNode.removeEventListener('focusout', this.__redispatchFocusout);
|
||||
this._inputNode.removeEventListener(
|
||||
'focus',
|
||||
/** @type {EventListenerOrEventListenerObject} */ (this.__redispatchFocus),
|
||||
);
|
||||
this._inputNode.removeEventListener(
|
||||
'blur',
|
||||
/** @type {EventListenerOrEventListenerObject} */ (this.__redispatchBlur),
|
||||
);
|
||||
this._inputNode.removeEventListener(
|
||||
'focusin',
|
||||
/** @type {EventListenerOrEventListenerObject} */ (this.__redispatchFocusin),
|
||||
);
|
||||
this._inputNode.removeEventListener(
|
||||
'focusout',
|
||||
/** @type {EventListenerOrEventListenerObject} */ (this.__redispatchFocusout),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { css, dedupeMixin, html, nothing, SlotMixin } from '@lion/core';
|
||||
import { Unparseable } from './validate/Unparseable.js';
|
||||
import { DisabledMixin } from '@lion/core/src/DisabledMixin.js';
|
||||
import { FormRegisteringMixin } from './registration/FormRegisteringMixin.js';
|
||||
import { getAriaElementsInRightDomOrder } from './utils/getAriaElementsInRightDomOrder.js';
|
||||
import { Unparseable } from './validate/Unparseable.js';
|
||||
|
||||
/**
|
||||
* Generates random unique identifier (for dom elements)
|
||||
|
|
@ -17,16 +18,17 @@ function uuid(prefix) {
|
|||
* This Mixin is a shared fundament for all form components, it's applied on:
|
||||
* - LionField (which is extended to LionInput, LionTextarea, LionSelect etc. etc.)
|
||||
* - LionFieldset (which is extended to LionRadioGroup, LionCheckboxGroup, LionForm)
|
||||
* @typedef {import('lit-html').TemplateResult} TemplateResult
|
||||
* @typedef {import('lit-element').CSSResult} CSSResult
|
||||
* @typedef {import('lit-html').nothing} nothing
|
||||
* @typedef {import('@lion/core').TemplateResult} TemplateResult
|
||||
* @typedef {import('@lion/core').CSSResult} CSSResult
|
||||
* @typedef {import('@lion/core').nothing} nothing
|
||||
* @typedef {import('@lion/core/types/SlotMixinTypes').SlotsMap} SlotsMap
|
||||
* @typedef {import('../types/FormControlMixinTypes.js').FormControlMixin} FormControlMixin
|
||||
* @type {FormControlMixin}
|
||||
* @param {import('@open-wc/dedupe-mixin').Constructor<import('@lion/core').LitElement>} superclass
|
||||
*/
|
||||
const FormControlMixinImplementation = superclass =>
|
||||
// eslint-disable-next-line no-shadow, no-unused-vars
|
||||
class FormControlMixin extends FormRegisteringMixin(SlotMixin(superclass)) {
|
||||
class FormControlMixin extends FormRegisteringMixin(DisabledMixin(SlotMixin(superclass))) {
|
||||
static get properties() {
|
||||
return {
|
||||
/**
|
||||
|
|
@ -48,6 +50,21 @@ const FormControlMixinImplementation = superclass =>
|
|||
type: String,
|
||||
attribute: 'help-text',
|
||||
},
|
||||
|
||||
/**
|
||||
* The model value is the result of the parser function(when available).
|
||||
* It should be considered as the internal value used for validation and reasoning/logic.
|
||||
* The model value is 'ready for consumption' by the outside world (think of a Date
|
||||
* object or a float). The modelValue can(and is recommended to) be used as both input
|
||||
* value and output value of the `LionField`.
|
||||
*
|
||||
* Examples:
|
||||
* - For a date input: a String '20/01/1999' will be converted to new Date('1999/01/20')
|
||||
* - For a number input: a formatted String '1.234,56' will be converted to a Number:
|
||||
* 1234.56
|
||||
*/
|
||||
modelValue: { attribute: false },
|
||||
|
||||
/**
|
||||
* Contains all elements that should end up in aria-labelledby of `._inputNode`
|
||||
*/
|
||||
|
|
@ -112,7 +129,8 @@ const FormControlMixinImplementation = superclass =>
|
|||
* @return {string}
|
||||
*/
|
||||
get fieldName() {
|
||||
return this.__fieldName || this.label || this.name;
|
||||
// @ts-expect-error
|
||||
return this.__fieldName || this.label || this.name; // FIXME: when LionField is typed we can inherit this prop
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -184,7 +202,9 @@ const FormControlMixinImplementation = superclass =>
|
|||
}
|
||||
|
||||
get _feedbackNode() {
|
||||
return this.__getDirectSlotChild('feedback');
|
||||
return /** @type {import('./validate/LionValidationFeedback').LionValidationFeedback | undefined} */ (this.__getDirectSlotChild(
|
||||
'feedback',
|
||||
));
|
||||
}
|
||||
|
||||
constructor() {
|
||||
|
|
@ -197,7 +217,11 @@ const FormControlMixinImplementation = superclass =>
|
|||
this._ariaDescribedNodes = [];
|
||||
/** @type {'child' | 'choice-group' | 'fieldset'} */
|
||||
this._repropagationRole = 'child';
|
||||
this.addEventListener('model-value-changed', this.__repropagateChildrenValues);
|
||||
this._isRepropagationEndpoint = false;
|
||||
this.addEventListener(
|
||||
'model-value-changed',
|
||||
/** @type {EventListenerOrEventListenerObject} */ (this.__repropagateChildrenValues),
|
||||
);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
|
|
@ -339,12 +363,8 @@ const FormControlMixinImplementation = superclass =>
|
|||
*/
|
||||
render() {
|
||||
return html`
|
||||
<div class="form-field__group-one">
|
||||
${this._groupOneTemplate()}
|
||||
</div>
|
||||
<div class="form-field__group-two">
|
||||
${this._groupTwoTemplate()}
|
||||
</div>
|
||||
<div class="form-field__group-one">${this._groupOneTemplate()}</div>
|
||||
<div class="form-field__group-two">${this._groupTwoTemplate()}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
|
|
@ -479,10 +499,15 @@ const FormControlMixinImplementation = superclass =>
|
|||
/**
|
||||
* @param {?} modelValue
|
||||
* @return {boolean}
|
||||
*
|
||||
* FIXME: Move to FormatMixin? Since there we have access to modelValue prop
|
||||
*/
|
||||
// @ts-expect-error
|
||||
_isEmpty(modelValue = this.modelValue) {
|
||||
let value = modelValue;
|
||||
// @ts-expect-error
|
||||
if (this.modelValue instanceof Unparseable) {
|
||||
// @ts-expect-error
|
||||
value = this.modelValue.viewValue;
|
||||
}
|
||||
|
||||
|
|
@ -629,7 +654,7 @@ const FormControlMixinImplementation = superclass =>
|
|||
}
|
||||
|
||||
/**
|
||||
* @return {HTMLElement[]}
|
||||
* @return {Array.<HTMLElement|undefined>}
|
||||
*/
|
||||
// Returns dom references to all elements that should be referred to by field(s)
|
||||
_getAriaDescriptionElements() {
|
||||
|
|
@ -681,10 +706,12 @@ const FormControlMixinImplementation = superclass =>
|
|||
|
||||
/**
|
||||
* @param {string} slotName
|
||||
* @return {HTMLElement}
|
||||
* @return {HTMLElement | undefined}
|
||||
*/
|
||||
__getDirectSlotChild(slotName) {
|
||||
return [...this.children].find(el => el.slot === slotName);
|
||||
return /** @type {HTMLElement[]} */ (Array.from(this.children)).find(
|
||||
el => el.slot === slotName,
|
||||
);
|
||||
}
|
||||
|
||||
__dispatchInitialModelValueChangedEvent() {
|
||||
|
|
@ -756,6 +783,7 @@ const FormControlMixinImplementation = superclass =>
|
|||
// We only send the checked changed up (not the unchecked). In this way a choice group
|
||||
// (radio-group, checkbox-group, select/listbox) acts as an 'endpoint' (a single Field)
|
||||
// just like the native <select>
|
||||
// @ts-expect-error multipleChoice is not directly available but only as side effect
|
||||
if (this._repropagationRole === 'choice-group' && !this.multipleChoice && !target.checked) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { dedupeMixin } from '@lion/core';
|
||||
import { FormControlMixin } from './FormControlMixin.js';
|
||||
import { Unparseable } from './validate/Unparseable.js';
|
||||
import { ValidateMixin } from './validate/ValidateMixin.js';
|
||||
|
||||
/**
|
||||
* @typedef {import('../types/FormatMixinTypes').FormatMixin} FormatMixin
|
||||
|
|
@ -52,25 +54,12 @@ import { Unparseable } from './validate/Unparseable.js';
|
|||
* Flow: serializedValue (deserializer) -> `.modelValue` (formatter) -> `.formattedValue` -> `._inputNode.value`
|
||||
*
|
||||
* @type {FormatMixin}
|
||||
* @param {import('@open-wc/dedupe-mixin').Constructor<import('@lion/core').LitElement>} superclass
|
||||
*/
|
||||
const FormatMixinImplementation = superclass =>
|
||||
class FormatMixin extends superclass {
|
||||
class FormatMixin extends ValidateMixin(FormControlMixin(superclass)) {
|
||||
static get properties() {
|
||||
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 result will be stored in the native _inputNode (usually an input[type=text]).
|
||||
|
|
@ -296,7 +285,7 @@ const FormatMixinImplementation = superclass =>
|
|||
*/
|
||||
_onModelValueChanged(...args) {
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -405,7 +394,8 @@ const FormatMixinImplementation = superclass =>
|
|||
this._inputNode.removeEventListener('input', this._proxyInputEvent);
|
||||
this._inputNode.removeEventListener(
|
||||
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
|
||||
* field is left non-empty
|
||||
* - on keyup (actually, on the model-value-changed event) -> 'dirty' will be set to true
|
||||
* @param {HTMLElement} superclass
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {InteractionStateMixin}
|
||||
* @param {import('@open-wc/dedupe-mixin').Constructor<import('@lion/core').LitElement>} superclass
|
||||
*/
|
||||
const InteractionStateMixinImplementation = superclass =>
|
||||
class InteractionStateMixin extends FormControlMixin(superclass) {
|
||||
|
|
@ -105,18 +103,14 @@ const InteractionStateMixinImplementation = superclass =>
|
|||
* Register event handlers and validate prefilled inputs
|
||||
*/
|
||||
connectedCallback() {
|
||||
if (super.connectedCallback) {
|
||||
super.connectedCallback();
|
||||
}
|
||||
this.addEventListener(this._leaveEvent, this._iStateOnLeave);
|
||||
this.addEventListener(this._valueChangedEvent, this._iStateOnValueChange);
|
||||
this.initInteractionState();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
if (super.disconnectedCallback) {
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
this.removeEventListener(this._leaveEvent, this._iStateOnLeave);
|
||||
this.removeEventListener(this._valueChangedEvent, this._iStateOnValueChange);
|
||||
}
|
||||
|
|
@ -169,6 +163,27 @@ const InteractionStateMixinImplementation = superclass =>
|
|||
_onDirtyChanged() {
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -1,14 +1,10 @@
|
|||
import { LitElement, SlotMixin } from '@lion/core';
|
||||
import { DisabledMixin } from '@lion/core/src/DisabledMixin.js';
|
||||
import { ValidateMixin } from './validate/ValidateMixin.js';
|
||||
import { FocusMixin } from './FocusMixin.js';
|
||||
import { FormatMixin } from './FormatMixin.js';
|
||||
import { FormControlMixin } from './FormControlMixin.js';
|
||||
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.
|
||||
* 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
|
||||
*/
|
||||
// @ts-expect-error base constructors same return type
|
||||
export class LionField extends FormControlMixin(
|
||||
InteractionStateMixin(
|
||||
FocusMixin(FormatMixin(ValidateMixin(DisabledMixin(SlotMixin(LitElement))))),
|
||||
),
|
||||
InteractionStateMixin(FocusMixin(FormatMixin(ValidateMixin(SlotMixin(LitElement))))),
|
||||
) {
|
||||
static get properties() {
|
||||
return {
|
||||
submitted: {
|
||||
// make sure validation can be triggered based on observer
|
||||
type: Boolean,
|
||||
},
|
||||
autocomplete: {
|
||||
type: String,
|
||||
reflect: true,
|
||||
|
|
@ -47,6 +38,10 @@ export class LionField extends FormControlMixin(
|
|||
};
|
||||
}
|
||||
|
||||
get _inputNode() {
|
||||
return /** @type {HTMLInputElement} */ (super._inputNode); // casts type
|
||||
}
|
||||
|
||||
/** @type {number} */
|
||||
get selectionStart() {
|
||||
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 (this._inputNode) {
|
||||
this._setValueAndPreserveCaret(value);
|
||||
/** @type {string | undefined} */
|
||||
this.__value = undefined;
|
||||
} else {
|
||||
this.__value = value;
|
||||
|
|
@ -98,11 +94,16 @@ export class LionField extends FormControlMixin(
|
|||
constructor() {
|
||||
super();
|
||||
this.name = '';
|
||||
this.submitted = false;
|
||||
/** @type {string | undefined} */
|
||||
this.autocomplete = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('lit-element').PropertyValues } changedProperties
|
||||
*/
|
||||
firstUpdated(changedProperties) {
|
||||
super.firstUpdated(changedProperties);
|
||||
/** @type {any} */
|
||||
this._initialModelValue = this.modelValue;
|
||||
}
|
||||
|
||||
|
|
@ -118,6 +119,9 @@ export class LionField extends FormControlMixin(
|
|||
this._inputNode.removeEventListener('change', this._onChange);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('lit-element').PropertyValues } changedProperties
|
||||
*/
|
||||
updated(changedProperties) {
|
||||
super.updated(changedProperties);
|
||||
|
||||
|
|
@ -131,14 +135,12 @@ export class LionField extends FormControlMixin(
|
|||
}
|
||||
|
||||
if (changedProperties.has('autocomplete')) {
|
||||
this._inputNode.autocomplete = this.autocomplete;
|
||||
this._inputNode.autocomplete = /** @type {string} */ (this.autocomplete);
|
||||
}
|
||||
}
|
||||
|
||||
resetInteractionState() {
|
||||
if (super.resetInteractionState) {
|
||||
super.resetInteractionState();
|
||||
}
|
||||
this.submitted = false;
|
||||
}
|
||||
|
||||
|
|
@ -147,19 +149,15 @@ export class LionField extends FormControlMixin(
|
|||
this.resetInteractionState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears modelValue.
|
||||
* Interaction states are not cleared (use resetInteractionState for this)
|
||||
*/
|
||||
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
|
||||
}
|
||||
|
||||
_onChange() {
|
||||
if (super._onChange) {
|
||||
super._onChange();
|
||||
}
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('user-input-changed', {
|
||||
bubbles: true,
|
||||
|
|
|
|||
|
|
@ -2,9 +2,18 @@ import { dedupeMixin } from '@lion/core';
|
|||
import { FormRegistrarMixin } from '../registration/FormRegistrarMixin.js';
|
||||
import { InteractionStateMixin } from '../InteractionStateMixin.js';
|
||||
|
||||
export const ChoiceGroupMixin = dedupeMixin(
|
||||
superclass =>
|
||||
// eslint-disable-next-line
|
||||
/**
|
||||
* @typedef {import('../../types/choice-group/ChoiceGroupMixinTypes').ChoiceGroupMixin} ChoiceGroupMixin
|
||||
* @typedef {import('../../types/FormControlMixinTypes').FormControlHost} FormControlHost
|
||||
* @typedef {import('../../types/registration/FormRegistrarMixinTypes').ElementWithParentFormGroup} ElementWithParentFormGroup
|
||||
* @typedef {FormControlHost & HTMLElement & {__parentFormGroup?:HTMLElement, checked?:boolean}} FormControl
|
||||
*/
|
||||
|
||||
/**
|
||||
* @type {ChoiceGroupMixin}
|
||||
* @param {import('@open-wc/dedupe-mixin').Constructor<import('@lion/core').LitElement>} superclass
|
||||
*/
|
||||
const ChoiceGroupMixinImplementation = superclass =>
|
||||
class ChoiceGroupMixin extends FormRegistrarMixin(InteractionStateMixin(superclass)) {
|
||||
static get properties() {
|
||||
return {
|
||||
|
|
@ -12,7 +21,6 @@ export const ChoiceGroupMixin = dedupeMixin(
|
|||
* @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.
|
||||
* @type {boolean}
|
||||
*/
|
||||
multipleChoice: {
|
||||
type: Boolean,
|
||||
|
|
@ -30,13 +38,19 @@ export const ChoiceGroupMixin = dedupeMixin(
|
|||
}
|
||||
|
||||
set modelValue(value) {
|
||||
/**
|
||||
* @param {{ modelValue: { value: any; }; }} el
|
||||
* @param {any} val
|
||||
*/
|
||||
const checkCondition = (el, val) => el.modelValue.value === val;
|
||||
|
||||
if (this.__isInitialModelValue) {
|
||||
this.__isInitialModelValue = false;
|
||||
this.registrationComplete.then(() => {
|
||||
this._setCheckedElements(value, (el, val) => el.modelValue.value === val);
|
||||
this._setCheckedElements(value, checkCondition);
|
||||
});
|
||||
} else {
|
||||
this._setCheckedElements(value, (el, val) => el.modelValue.value === val);
|
||||
this._setCheckedElements(value, checkCondition);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -57,13 +71,19 @@ export const ChoiceGroupMixin = dedupeMixin(
|
|||
}
|
||||
|
||||
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, (el, val) => el.serializedValue.value === val);
|
||||
this._setCheckedElements(value, checkCondition);
|
||||
});
|
||||
} else {
|
||||
this._setCheckedElements(value, (el, val) => el.serializedValue.value === val);
|
||||
this._setCheckedElements(value, checkCondition);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -76,13 +96,19 @@ export const ChoiceGroupMixin = dedupeMixin(
|
|||
}
|
||||
|
||||
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, (el, val) => el.formattedValue === val);
|
||||
this._setCheckedElements(value, checkCondition);
|
||||
});
|
||||
} else {
|
||||
this._setCheckedElements(value, (el, val) => el.formattedValue === val);
|
||||
this._setCheckedElements(value, checkCondition);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -94,6 +120,7 @@ export const ChoiceGroupMixin = dedupeMixin(
|
|||
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;
|
||||
|
|
@ -124,9 +151,7 @@ export const ChoiceGroupMixin = dedupeMixin(
|
|||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
if (super.disconnectedCallback) {
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
if (this.registrationComplete.done === false) {
|
||||
this.__rejectRegistrationComplete();
|
||||
|
|
@ -135,6 +160,8 @@ export const ChoiceGroupMixin = dedupeMixin(
|
|||
|
||||
/**
|
||||
* @override from FormRegistrarMixin
|
||||
* @param {FormControl} child
|
||||
* @param {number} indexToInsertAt
|
||||
*/
|
||||
addFormElement(child, indexToInsertAt) {
|
||||
this._throwWhenInvalidChildModelValue(child);
|
||||
|
|
@ -153,6 +180,7 @@ export const ChoiceGroupMixin = dedupeMixin(
|
|||
|
||||
/**
|
||||
* @override
|
||||
* @param {string} property
|
||||
*/
|
||||
_getFromAllFormElements(property, filterCondition = () => true) {
|
||||
// For modelValue, serializedValue and formattedValue, an exception should be made,
|
||||
|
|
@ -167,8 +195,12 @@ export const ChoiceGroupMixin = dedupeMixin(
|
|||
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')
|
||||
) {
|
||||
|
|
@ -196,6 +228,9 @@ export const ChoiceGroupMixin = dedupeMixin(
|
|||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {CustomEvent & {target:FormControl}} ev
|
||||
*/
|
||||
_checkSingleChoiceElements(ev) {
|
||||
const { target } = ev;
|
||||
if (target.checked === false) return;
|
||||
|
|
@ -208,7 +243,7 @@ export const ChoiceGroupMixin = dedupeMixin(
|
|||
choice.checked = false; // eslint-disable-line no-param-reassign
|
||||
}
|
||||
});
|
||||
this.__triggerCheckedValueChanged();
|
||||
// this.__triggerCheckedValueChanged();
|
||||
}
|
||||
|
||||
_getCheckedElements() {
|
||||
|
|
@ -216,6 +251,10 @@ export const ChoiceGroupMixin = dedupeMixin(
|
|||
return this.formElements.filter(el => el.checked && !el.disabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string | any[]} value
|
||||
* @param {Function} check
|
||||
*/
|
||||
_setCheckedElements(value, check) {
|
||||
for (let i = 0; i < this.formElements.length; i += 1) {
|
||||
if (this.multipleChoice) {
|
||||
|
|
@ -236,6 +275,9 @@ export const ChoiceGroupMixin = dedupeMixin(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {FormControl} child
|
||||
*/
|
||||
__delegateNameAttribute(child) {
|
||||
if (!child.name || child.name === this.name) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
|
|
@ -253,6 +295,7 @@ export const ChoiceGroupMixin = dedupeMixin(
|
|||
|
||||
/**
|
||||
* @override FormControlMixin
|
||||
* @param {CustomEvent} ev
|
||||
*/
|
||||
_onBeforeRepropagateChildrenValues(ev) {
|
||||
// Normalize target, since we might receive 'portal events' (from children in a modal,
|
||||
|
|
@ -269,5 +312,6 @@ export const ChoiceGroupMixin = dedupeMixin(
|
|||
this.__setChoiceGroupTouched();
|
||||
this.requestUpdate('modelValue');
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const ChoiceGroupMixin = dedupeMixin(ChoiceGroupMixinImplementation);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,29 @@
|
|||
/* 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';
|
||||
|
||||
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) {
|
||||
static get properties() {
|
||||
return {
|
||||
|
|
@ -25,7 +44,7 @@ export const ChoiceInputMixin = superclass =>
|
|||
*/
|
||||
modelValue: {
|
||||
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
|
||||
|
|
@ -44,10 +63,15 @@ export const ChoiceInputMixin = superclass =>
|
|||
set choiceValue(value) {
|
||||
this.requestUpdate('choiceValue', this.choiceValue);
|
||||
if (this.modelValue.value !== value) {
|
||||
/** @type {ChoiceInputModelValue} */
|
||||
this.modelValue = { value, checked: this.modelValue.checked };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {any} oldValue
|
||||
*/
|
||||
requestUpdateInternal(name, oldValue) {
|
||||
super.requestUpdateInternal(name, oldValue);
|
||||
|
||||
|
|
@ -62,6 +86,9 @@ export const ChoiceInputMixin = superclass =>
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('lit-element').PropertyValues } changedProperties
|
||||
*/
|
||||
firstUpdated(changedProperties) {
|
||||
super.firstUpdated(changedProperties);
|
||||
if (changedProperties.has('checked')) {
|
||||
|
|
@ -71,6 +98,9 @@ export const ChoiceInputMixin = superclass =>
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('lit-element').PropertyValues } changedProperties
|
||||
*/
|
||||
updated(changedProperties) {
|
||||
super.updated(changedProperties);
|
||||
if (changedProperties.has('modelValue')) {
|
||||
|
|
@ -118,9 +148,7 @@ export const ChoiceInputMixin = superclass =>
|
|||
render() {
|
||||
return html`
|
||||
<slot name="input"></slot>
|
||||
<div class="choice-field__graphic-container">
|
||||
${this._choiceGraphicTemplate()}
|
||||
</div>
|
||||
<div class="choice-field__graphic-container">${this._choiceGraphicTemplate()}</div>
|
||||
<div class="choice-field__label">
|
||||
<slot name="label"></slot>
|
||||
</div>
|
||||
|
|
@ -148,10 +176,16 @@ export const ChoiceInputMixin = superclass =>
|
|||
this.checked = !this.checked;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} checked
|
||||
*/
|
||||
__syncModelCheckedToChecked(checked) {
|
||||
this.checked = checked;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} checked
|
||||
*/
|
||||
__syncCheckedToModel(checked) {
|
||||
this.modelValue = { value: this.choiceValue, checked };
|
||||
}
|
||||
|
|
@ -160,7 +194,8 @@ export const ChoiceInputMixin = superclass =>
|
|||
// ._inputNode might not be available yet(slot content)
|
||||
// or at all (no reliance on platform construct, in case of [role=option])
|
||||
if (this._inputNode) {
|
||||
this._inputNode.checked = this.checked;
|
||||
/** @type {HTMLInputElement} */
|
||||
(this._inputNode).checked = this.checked;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -178,8 +213,12 @@ export const ChoiceInputMixin = superclass =>
|
|||
* @override
|
||||
* hasChanged is designed for async (updated) callback, also check for sync
|
||||
* (requestUpdateInternal) callback
|
||||
* @param {{ modelValue:unknown }} newV
|
||||
* @param {{ modelValue:unknown }} [oldV]
|
||||
*/
|
||||
// @ts-expect-error
|
||||
_onModelValueChanged({ modelValue }, { modelValue: old }) {
|
||||
// @ts-expect-error
|
||||
if (this.constructor._classProperties.get('modelValue').hasChanged(modelValue, old)) {
|
||||
super._onModelValueChanged({ modelValue });
|
||||
}
|
||||
|
|
@ -195,8 +234,8 @@ export const ChoiceInputMixin = superclass =>
|
|||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
* Overridden from FormatMixin, since a different modelValue is used for choice inputs.
|
||||
* @override Overridden from FormatMixin, since a different modelValue is used for choice inputs.
|
||||
* @param {ChoiceInputModelValue } modelValue
|
||||
*/
|
||||
formatter(modelValue) {
|
||||
return modelValue && modelValue.value !== undefined ? modelValue.value : modelValue;
|
||||
|
|
@ -216,3 +255,5 @@ export const ChoiceInputMixin = superclass =>
|
|||
*/
|
||||
_syncValueUpwards() {}
|
||||
};
|
||||
|
||||
export const ChoiceInputMixin = dedupeMixin(ChoiceInputMixinImplementation);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,11 @@ export class FormElementsHaveNoError extends Validator {
|
|||
return 'FormElementsHaveNoError';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {unknown} [value]
|
||||
* @param {string | undefined} [options]
|
||||
* @param {{ node: any }} config
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
execute(value, options, config) {
|
||||
const hasError = config.node._anyFormElementHasFeedbackFor('error');
|
||||
|
|
|
|||
|
|
@ -7,6 +7,15 @@ import { ValidateMixin } from '../validate/ValidateMixin.js';
|
|||
import { getAriaElementsInRightDomOrder } from '../utils/getAriaElementsInRightDomOrder.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
|
||||
* elements with [role="group|radiogroup"] (think of checkbox-group, radio-group, fieldset).
|
||||
|
|
@ -15,10 +24,11 @@ import { FormElementsHaveNoError } from './FormElementsHaveNoError.js';
|
|||
* 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
|
||||
* 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(
|
||||
superclass =>
|
||||
// eslint-disable-next-line no-shadow
|
||||
const FormGroupMixinImplementation = superclass =>
|
||||
class FormGroupMixin extends FormRegistrarMixin(
|
||||
FormControlMixin(ValidateMixin(DisabledMixin(SlotMixin(superclass)))),
|
||||
) {
|
||||
|
|
@ -132,7 +142,7 @@ export const FormGroupMixin = dedupeMixin(
|
|||
this.addEventListener('validate-performed', this.__onChildValidatePerformed);
|
||||
|
||||
this.defaultValidators = [new FormElementsHaveNoError()];
|
||||
|
||||
/** @type {Promise<any> & {done?:boolean}} */
|
||||
this.registrationComplete = new Promise((resolve, reject) => {
|
||||
this.__resolveRegistrationComplete = resolve;
|
||||
this.__rejectRegistrationComplete = reject;
|
||||
|
|
@ -164,9 +174,7 @@ export const FormGroupMixin = dedupeMixin(
|
|||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
if (super.disconnectedCallback) {
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
if (this.__hasActiveOutsideClickHandling) {
|
||||
document.removeEventListener('click', this._checkForOutsideClick);
|
||||
|
|
@ -194,6 +202,9 @@ export const FormGroupMixin = dedupeMixin(
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('lit-element').PropertyValues } changedProperties
|
||||
*/
|
||||
updated(changedProperties) {
|
||||
super.updated(changedProperties);
|
||||
|
||||
|
|
@ -219,8 +230,11 @@ export const FormGroupMixin = dedupeMixin(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Event} event
|
||||
*/
|
||||
_checkForOutsideClick(event) {
|
||||
const outsideGroupClicked = !this.contains(event.target);
|
||||
const outsideGroupClicked = !this.contains(/** @type {Node} */ (event.target));
|
||||
if (outsideGroupClicked) {
|
||||
this.touched = true;
|
||||
}
|
||||
|
|
@ -301,15 +315,18 @@ export const FormGroupMixin = dedupeMixin(
|
|||
});
|
||||
}
|
||||
|
||||
_getFromAllFormElements(property, filterCondition = el => !el.disabled) {
|
||||
/**
|
||||
* @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 => filterCondition(el)).map(el => el[property]);
|
||||
} else if (filterCondition(elem)) {
|
||||
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, filterCondition);
|
||||
result[name] = elem._getFromAllFormElements(property, filterFn);
|
||||
} else {
|
||||
result[name] = elem[property];
|
||||
}
|
||||
|
|
@ -318,17 +335,28 @@ export const FormGroupMixin = dedupeMixin(
|
|||
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((el, index) => {
|
||||
this.formElements[name].forEach((
|
||||
/** @type {FormControl} */ el,
|
||||
/** @type {number} */ index,
|
||||
) => {
|
||||
el[property] = values[name][index]; // eslint-disable-line no-param-reassign
|
||||
});
|
||||
}
|
||||
|
|
@ -339,19 +367,25 @@ export const FormGroupMixin = dedupeMixin(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} property
|
||||
*/
|
||||
_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].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(el => {
|
||||
return this.formElements[name].some((/** @type {FormControl} */ el) => {
|
||||
return Boolean(el.hasFeedbackFor && el.hasFeedbackFor.includes(state));
|
||||
});
|
||||
}
|
||||
|
|
@ -362,10 +396,13 @@ export const FormGroupMixin = dedupeMixin(
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} property
|
||||
*/
|
||||
_everyFormElementHas(property) {
|
||||
return Object.keys(this.formElements).every(name => {
|
||||
if (Array.isArray(this.formElements[name])) {
|
||||
return this.formElements[name].every(el => !!el[property]);
|
||||
return this.formElements[name].every((/** @type {FormControl} */ el) => !!el[property]);
|
||||
}
|
||||
return !!this.formElements[name][property];
|
||||
});
|
||||
|
|
@ -376,9 +413,10 @@ export const FormGroupMixin = dedupeMixin(
|
|||
* - 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(ev.target)) {
|
||||
if (ev && this.isRegisteredFormElement(/** @type {FormControl} */ (ev.target))) {
|
||||
this.validate();
|
||||
}
|
||||
}
|
||||
|
|
@ -387,6 +425,9 @@ export const FormGroupMixin = dedupeMixin(
|
|||
this.focused = this._anyFormElementHas('focused');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Event} ev
|
||||
*/
|
||||
_onFocusOut(ev) {
|
||||
const lastEl = this.formElements[this.formElements.length - 1];
|
||||
if (ev.target === lastEl) {
|
||||
|
|
@ -399,14 +440,16 @@ export const FormGroupMixin = dedupeMixin(
|
|||
this.dirty = this._anyFormElementHas('dirty');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {FormControl} child
|
||||
*/
|
||||
__linkChildrenMessagesToParent(child) {
|
||||
// aria-describedby of (nested) children
|
||||
let parent = this;
|
||||
const unTypedThis = /** @type {unknown} */ (this);
|
||||
let parent = /** @type {FormControlHost & { __parentFormGroup:any }} */ (unTypedThis);
|
||||
const ctor = /** @type {typeof FormGroupMixin} */ (this.constructor);
|
||||
while (parent) {
|
||||
this.constructor._addDescriptionElementIdsToField(
|
||||
child,
|
||||
parent._getAriaDescriptionElements(),
|
||||
);
|
||||
ctor._addDescriptionElementIdsToField(child, parent._getAriaDescriptionElements());
|
||||
// Also check if the newly added child needs to refer grandparents
|
||||
parent = parent.__parentFormGroup;
|
||||
}
|
||||
|
|
@ -416,11 +459,12 @@ export const FormGroupMixin = dedupeMixin(
|
|||
* @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) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
child.makeRequestToBeDisabled();
|
||||
}
|
||||
// TODO: Unlink in removeFormElement
|
||||
|
|
@ -439,8 +483,8 @@ export const FormGroupMixin = dedupeMixin(
|
|||
/**
|
||||
* Add aria-describedby to child element(field), so that it points to feedback/help-text of
|
||||
* parent(fieldset)
|
||||
* @param {LionField} field - the child: lion-field/lion-input/lion-textarea
|
||||
* @param {array} descriptionElements - description elements like feedback and help-text
|
||||
* @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 });
|
||||
|
|
@ -453,10 +497,12 @@ export const FormGroupMixin = dedupeMixin(
|
|||
|
||||
/**
|
||||
* @override of FormRegistrarMixin. Connects ValidateMixin
|
||||
* @param {FormRegisteringHost} el
|
||||
*/
|
||||
removeFormElement(...args) {
|
||||
super.removeFormElement(...args);
|
||||
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/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
|
||||
*
|
||||
* @type {FormRegisteringMixin}
|
||||
* @param {import('@open-wc/dedupe-mixin').Constructor<HTMLElement>} superclass
|
||||
*/
|
||||
const FormRegisteringMixinImplementation = superclass =>
|
||||
class FormRegisteringMixin extends superclass {
|
||||
class extends superclass {
|
||||
/** @type {FormRegistrarHost | undefined} */
|
||||
__parentFormGroup;
|
||||
|
||||
connectedCallback() {
|
||||
// @ts-expect-error check it anyway, because could be lit-element extension
|
||||
if (super.connectedCallback) {
|
||||
// @ts-expect-error check it anyway, because could be lit-element extension
|
||||
super.connectedCallback();
|
||||
}
|
||||
this.dispatchEvent(
|
||||
|
|
@ -26,7 +34,9 @@ const FormRegisteringMixinImplementation = superclass =>
|
|||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
// @ts-expect-error check it anyway, because could be lit-element extension
|
||||
if (super.disconnectedCallback) {
|
||||
// @ts-expect-error check it anyway, because could be lit-element extension
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
if (this.__parentFormGroup) {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,17 @@
|
|||
// eslint-disable-next-line max-classes-per-file
|
||||
import { dedupeMixin } from '@lion/core';
|
||||
import { FormRegisteringMixin } from './FormRegisteringMixin.js';
|
||||
import { FormControlsCollection } from './FormControlsCollection.js';
|
||||
|
||||
// TODO: rename .formElements to .formControls? (or .$controls ?)
|
||||
import { FormRegisteringMixin } from './FormRegisteringMixin.js';
|
||||
|
||||
/**
|
||||
* @typedef {import('../../types/registration/FormRegistrarMixinTypes').FormRegistrarMixin} FormRegistrarMixin
|
||||
* @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.
|
||||
* See FormControlsCollection for more information
|
||||
* @type {FormRegistrarMixin}
|
||||
* @param {import('@open-wc/dedupe-mixin').Constructor<import('@lion/core').LitElement>} superclass
|
||||
*/
|
||||
const FormRegistrarMixinImplementation = superclass =>
|
||||
// eslint-disable-next-line no-shadow, no-unused-vars
|
||||
class FormRegistrarMixin extends FormRegisteringMixin(superclass) {
|
||||
class extends FormRegisteringMixin(superclass) {
|
||||
static get properties() {
|
||||
return {
|
||||
/**
|
||||
|
|
@ -45,7 +50,11 @@ const FormRegistrarMixinImplementation = superclass =>
|
|||
this._isFormOrFieldset = false;
|
||||
|
||||
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
|
||||
*/
|
||||
addFormElement(child, indexToInsertAt) {
|
||||
|
|
@ -74,12 +83,12 @@ const FormRegistrarMixinImplementation = superclass =>
|
|||
|
||||
// 2. Add children as object key
|
||||
if (this._isFormOrFieldset) {
|
||||
// @ts-ignore
|
||||
const { name } = child; // FIXME: <-- ElementWithParentFormGroup should become LionFieldWithParentFormGroup so that "name" exists
|
||||
const { name } = child;
|
||||
if (!name) {
|
||||
console.info('Error Node:', child); // eslint-disable-line no-console
|
||||
throw new TypeError('You need to define a name');
|
||||
}
|
||||
// @ts-expect-error
|
||||
if (name === this.name) {
|
||||
console.info('Error Node:', child); // eslint-disable-line no-console
|
||||
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) {
|
||||
// 1. Handle array based children
|
||||
|
|
@ -117,7 +126,7 @@ const FormRegistrarMixinImplementation = superclass =>
|
|||
|
||||
// 2. Handle name based object keys
|
||||
if (this._isFormOrFieldset) {
|
||||
// @ts-ignore
|
||||
// @ts-expect-error
|
||||
const { name } = child; // FIXME: <-- ElementWithParentFormGroup should become LionFieldWithParentFormGroup so that "name" exists
|
||||
if (name.substr(-2) === '[]' && this.formElements[name]) {
|
||||
const idx = this.formElements[name].indexOf(child);
|
||||
|
|
|
|||
|
|
@ -17,10 +17,11 @@ import { dedupeMixin } from '@lion/core';
|
|||
* </my-portal>
|
||||
* // my-field will be registered within my-form
|
||||
* @type {FormRegistrarPortalMixin}
|
||||
* @param {import('@open-wc/dedupe-mixin').Constructor<HTMLElement>} superclass
|
||||
*/
|
||||
const FormRegistrarPortalMixinImplementation = superclass =>
|
||||
// eslint-disable-next-line no-shadow, no-unused-vars
|
||||
class FormRegistrarPortalMixin extends superclass {
|
||||
class extends superclass {
|
||||
constructor() {
|
||||
super();
|
||||
/** @type {(FormRegistrarPortalHost & HTMLElement) | undefined} */
|
||||
|
|
@ -30,7 +31,8 @@ const FormRegistrarPortalMixinImplementation = superclass =>
|
|||
);
|
||||
this.addEventListener(
|
||||
'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
|
||||
* `requestUpdate`) we only have to change our abstraction instead of all our components
|
||||
* @type {SyncUpdatableMixin}
|
||||
* @param {import('@open-wc/dedupe-mixin').Constructor<import('@lion/core').LitElement>} superclass
|
||||
*/
|
||||
const SyncUpdatableMixinImplementation = superclass =>
|
||||
class SyncUpdatable extends superclass {
|
||||
class extends superclass {
|
||||
constructor() {
|
||||
super();
|
||||
// Namespace for this mixin that guarantees naming clashes will not occur...
|
||||
|
|
@ -52,6 +53,7 @@ const SyncUpdatableMixinImplementation = superclass =>
|
|||
* @param {*} oldValue
|
||||
*/
|
||||
static __syncUpdatableHasChanged(name, newValue, oldValue) {
|
||||
// @ts-expect-error FIXME: Typescript bug, superclass static method not availabe from static context
|
||||
const properties = this._classProperties;
|
||||
if (properties.get(name) && properties.get(name).hasChanged) {
|
||||
return properties.get(name).hasChanged(newValue, oldValue);
|
||||
|
|
@ -61,7 +63,8 @@ const SyncUpdatableMixinImplementation = superclass =>
|
|||
|
||||
__syncUpdatableInitialize() {
|
||||
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;
|
||||
// Empty queue...
|
||||
|
|
@ -84,7 +87,8 @@ const SyncUpdatableMixinImplementation = superclass =>
|
|||
this.__SyncUpdatableNamespace = 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
|
||||
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';
|
||||
|
||||
/**
|
||||
* @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
|
||||
* 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 {
|
||||
static get properties() {
|
||||
return {
|
||||
/**
|
||||
* @property {FeedbackData} feedbackData
|
||||
*/
|
||||
feedbackData: Array,
|
||||
feedbackData: { attribute: false },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
_messageTemplate({ message }) {
|
||||
return message;
|
||||
}
|
||||
|
||||
updated() {
|
||||
super.updated();
|
||||
/**
|
||||
* @param {import('lit-element').PropertyValues } changedProperties
|
||||
*/
|
||||
updated(changedProperties) {
|
||||
super.updated(changedProperties);
|
||||
if (this.feedbackData && this.feedbackData[0]) {
|
||||
this.setAttribute('type', this.feedbackData[0].type);
|
||||
this.currentType = this.feedbackData[0].type;
|
||||
|
|
@ -31,7 +44,8 @@ export class LionValidationFeedback extends LitElement {
|
|||
if (this.currentType === 'success') {
|
||||
this.removeMessage = window.setTimeout(() => {
|
||||
this.removeAttribute('type');
|
||||
this.feedbackData = '';
|
||||
/** @type {messageMap[]} */
|
||||
this.feedbackData = [];
|
||||
}, 3000);
|
||||
}
|
||||
} else if (this.currentType !== 'success') {
|
||||
|
|
|
|||
|
|
@ -8,11 +8,14 @@ import { Validator } from './Validator.js';
|
|||
*/
|
||||
export class ResultValidator extends Validator {
|
||||
/**
|
||||
* @param {object} context
|
||||
* @param {Validator[]} context.validationResult
|
||||
* @param {Validator[]} context.prevValidationResult
|
||||
* @param {Validator[]} context.validators
|
||||
* @returns {Feedback[]}
|
||||
* @param {Object} context
|
||||
* @param {Validator[]} context.regularValidationResult
|
||||
* @param {Validator[] | undefined} context.prevValidationResult
|
||||
* @param {Validator[]} [context.validators]
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,25 @@
|
|||
/* eslint-disable class-methods-use-this, camelcase, no-param-reassign, max-classes-per-file */
|
||||
|
||||
import { dedupeMixin, ScopedElementsMixin, SlotMixin } from '@lion/core';
|
||||
import { dedupeMixin, ScopedElementsMixin, SlotMixin, DisabledMixin } from '@lion/core';
|
||||
// TODO: make form-core independent from localize
|
||||
import { localize } from '@lion/localize';
|
||||
import { LionValidationFeedback } from './LionValidationFeedback.js';
|
||||
import { ResultValidator } from './ResultValidator.js';
|
||||
import { Unparseable } from './Unparseable.js';
|
||||
import { AsyncQueue } from '../utils/AsyncQueue.js';
|
||||
import { pascalCase } from '../utils/pascalCase.js';
|
||||
import { SyncUpdatableMixin } from '../utils/SyncUpdatableMixin.js';
|
||||
import { LionValidationFeedback } from './LionValidationFeedback.js';
|
||||
import { ResultValidator } from './ResultValidator.js';
|
||||
import { Unparseable } from './Unparseable.js';
|
||||
import { Validator } from './Validator.js';
|
||||
import { Required } from './validators/Required.js';
|
||||
import { FormControlMixin } from '../FormControlMixin.js';
|
||||
|
||||
/**
|
||||
* @typedef {import('../../types/validate/ValidateMixinTypes').ValidateMixin} ValidateMixin
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {any[]} array1
|
||||
* @param {any[]} array2
|
||||
*/
|
||||
function arrayDiff(array1 = [], array2 = []) {
|
||||
return array1.filter(x => !array2.includes(x)).concat(array2.filter(x => !array1.includes(x)));
|
||||
}
|
||||
|
|
@ -20,52 +28,42 @@ function arrayDiff(array1 = [], array2 = []) {
|
|||
* @desc Handles all validation, based on modelValue changes. It has no knowledge about dom and
|
||||
* UI. All error visibility, dom interaction and accessibility are handled in FeedbackMixin.
|
||||
*
|
||||
* @event error-state-changed fires when FormControl goes from non-error to error state and vice versa
|
||||
* @event error-changed fires when the Validator(s) leading to the error state, change
|
||||
* @type {ValidateMixin}
|
||||
* @param {import('@open-wc/dedupe-mixin').Constructor<import('@lion/core').LitElement>} superclass
|
||||
*/
|
||||
export const ValidateMixin = dedupeMixin(
|
||||
superclass =>
|
||||
export const ValidateMixinImplementation = superclass =>
|
||||
// eslint-disable-next-line no-unused-vars, no-shadow
|
||||
class ValidateMixin extends SyncUpdatableMixin(SlotMixin(ScopedElementsMixin(superclass))) {
|
||||
class extends FormControlMixin(
|
||||
SyncUpdatableMixin(DisabledMixin(SlotMixin(ScopedElementsMixin(superclass)))),
|
||||
) {
|
||||
static get scopedElements() {
|
||||
const scopedElementsCtor = /** @type {typeof import('@open-wc/scoped-elements/src/types').ScopedElementsHost} */ (super
|
||||
.constructor);
|
||||
return {
|
||||
...super.scopedElements,
|
||||
...scopedElementsCtor.scopedElements,
|
||||
'lion-validation-feedback': LionValidationFeedback,
|
||||
};
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
/**
|
||||
* @desc List of all Validator instances applied to FormControl
|
||||
* @type {Validator[]}
|
||||
* @example
|
||||
* FormControl.validators = [new Required(), new MinLength(3, { type: 'warning' })];
|
||||
*/
|
||||
validators: Array,
|
||||
validators: { attribute: false },
|
||||
|
||||
hasFeedbackFor: {
|
||||
type: Array,
|
||||
},
|
||||
hasFeedbackFor: { attribute: false },
|
||||
|
||||
shouldShowFeedbackFor: {
|
||||
type: Array,
|
||||
},
|
||||
shouldShowFeedbackFor: { attribute: false },
|
||||
|
||||
showsFeedbackFor: {
|
||||
type: Array,
|
||||
attribute: 'shows-feedback-for',
|
||||
reflect: true,
|
||||
converter: {
|
||||
fromAttribute: value => value.split(','),
|
||||
toAttribute: value => value.join(','),
|
||||
fromAttribute: /** @param {string} value */ value => value.split(','),
|
||||
toAttribute: /** @param {[]} value */ value => value.join(','),
|
||||
},
|
||||
},
|
||||
|
||||
validationStates: {
|
||||
type: Object,
|
||||
// hasChanged: this._hasObjectChanged,
|
||||
},
|
||||
validationStates: { attribute: false },
|
||||
|
||||
/**
|
||||
* @desc flag that indicates whether async validation is pending
|
||||
|
|
@ -76,30 +74,17 @@ export const ValidateMixin = dedupeMixin(
|
|||
reflect: true,
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc value that al validation revolves around: once changed (usually triggered by
|
||||
* end user entering input), it will automatically trigger validation.
|
||||
*/
|
||||
modelValue: Object,
|
||||
|
||||
/**
|
||||
* @desc specialized fields (think of input-date and input-email) can have preconfigured
|
||||
* validators.
|
||||
*/
|
||||
defaultValidators: Array,
|
||||
defaultValidators: { attribute: false },
|
||||
|
||||
/**
|
||||
* Subclassers can enable this to show multiple feedback messages at the same time
|
||||
* By default, just like the platform, only one message (with highest prio) is visible.
|
||||
*/
|
||||
_visibleMessagesAmount: Number,
|
||||
|
||||
/**
|
||||
* @type {Promise<string>|string} will be passed as an argument to the `.getMessage`
|
||||
* method of a Validator. When filled in, this field name can be used to enhance
|
||||
* error messages.
|
||||
*/
|
||||
fieldName: String,
|
||||
_visibleMessagesAmount: { attribute: false },
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -115,36 +100,22 @@ export const ValidateMixin = dedupeMixin(
|
|||
* Adds "._feedbackNode" as described below
|
||||
*/
|
||||
get slots() {
|
||||
/**
|
||||
* FIXME: Ugly workaround https://github.com/microsoft/TypeScript/issues/40110
|
||||
* @callback getScopedTagName
|
||||
* @param {string} tagName
|
||||
* @returns {string}
|
||||
*
|
||||
* @typedef {Object} ScopedElementsObj
|
||||
* @property {getScopedTagName} getScopedTagName
|
||||
*/
|
||||
const ctor = /** @type {typeof ValidateMixin & ScopedElementsObj} */ (this.constructor);
|
||||
return {
|
||||
...super.slots,
|
||||
feedback: () =>
|
||||
document.createElement(this.constructor.getScopedTagName('lion-validation-feedback')),
|
||||
feedback: () => document.createElement(ctor.getScopedTagName('lion-validation-feedback')),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @overridable
|
||||
* @type {Element} _feedbackNode:
|
||||
* Gets a `FeedbackData` object as its input.
|
||||
* This element can be a custom made (web) component that renders messages in accordance with
|
||||
* the implemented Design System. For instance, it could add an icon in front of a message.
|
||||
* The _feedbackNode is only responsible for the visual rendering part, it should NOT contain
|
||||
* state. All state will be determined by the outcome of `FormControl.filterFeeback()`.
|
||||
* FormControl delegates to individual sub elements and decides who renders what.
|
||||
* For instance, FormControl itself is responsible for reflecting error-state and error-show
|
||||
* to its host element.
|
||||
* This means filtering out messages should happen in FormControl and NOT in `_feedbackNode`
|
||||
*
|
||||
* - gets a FeedbackData object as input
|
||||
* - should know about the FeedbackMessage types('error', 'success' etc.) that the FormControl
|
||||
* (having ValidateMixin applied) returns
|
||||
* - renders result and
|
||||
*
|
||||
*/
|
||||
get _feedbackNode() {
|
||||
return this.querySelector('[slot=feedback]');
|
||||
}
|
||||
|
||||
get _allValidators() {
|
||||
return [...this.validators, ...this.defaultValidators];
|
||||
}
|
||||
|
|
@ -152,14 +123,22 @@ export const ValidateMixin = dedupeMixin(
|
|||
constructor() {
|
||||
super();
|
||||
|
||||
/** @type {string[]} */
|
||||
this.hasFeedbackFor = [];
|
||||
|
||||
/** @type {string[]} */
|
||||
this.shouldShowFeedbackFor = [];
|
||||
|
||||
/** @type {string[]} */
|
||||
this.showsFeedbackFor = [];
|
||||
|
||||
/** @type {Object.<string, Object.<string, boolean>>} */
|
||||
this.validationStates = {};
|
||||
|
||||
this._visibleMessagesAmount = 1;
|
||||
|
||||
this.isPending = false;
|
||||
|
||||
/** @type {Validator[]} */
|
||||
this.validators = [];
|
||||
/** @type {Validator[]} */
|
||||
|
|
@ -191,12 +170,19 @@ export const ValidateMixin = dedupeMixin(
|
|||
localize.removeEventListener('localeChanged', this._updateFeedbackComponent);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('lit-element').PropertyValues} changedProperties
|
||||
*/
|
||||
firstUpdated(changedProperties) {
|
||||
super.firstUpdated(changedProperties);
|
||||
this.__validateInitialized = true;
|
||||
this.validate();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {?} oldValue
|
||||
*/
|
||||
updateSync(name, oldValue) {
|
||||
super.updateSync(name, oldValue);
|
||||
if (name === 'validators') {
|
||||
|
|
@ -215,7 +201,7 @@ export const ValidateMixin = dedupeMixin(
|
|||
// This can't be reflected asynchronously in Safari
|
||||
// Screen reader output should be in sync with visibility of error messages
|
||||
if (this._inputNode) {
|
||||
this._inputNode.setAttribute('aria-invalid', this._hasFeedbackVisibleFor('error'));
|
||||
this._inputNode.setAttribute('aria-invalid', `${this._hasFeedbackVisibleFor('error')}`);
|
||||
// this._inputNode.setCustomValidity(this._validationMessage || '');
|
||||
}
|
||||
|
||||
|
|
@ -261,6 +247,8 @@ export const ValidateMixin = dedupeMixin(
|
|||
*
|
||||
* Situations A.2 and A.3 are not mutually exclusive and can be triggered within one validate()
|
||||
* call. Situation B will occur after every call.
|
||||
*
|
||||
* @param {{ clearCurrentResult?: boolean }} [opts]
|
||||
*/
|
||||
async validate({ clearCurrentResult } = {}) {
|
||||
if (this.disabled) {
|
||||
|
|
@ -300,7 +288,7 @@ export const ValidateMixin = dedupeMixin(
|
|||
const value =
|
||||
this.modelValue instanceof Unparseable ? this.modelValue.viewValue : this.modelValue;
|
||||
|
||||
/** @type {Validator} */
|
||||
/** @type {Validator | undefined} */
|
||||
const requiredValidator = this._allValidators.find(v => v instanceof Required);
|
||||
|
||||
/**
|
||||
|
|
@ -327,12 +315,14 @@ export const ValidateMixin = dedupeMixin(
|
|||
const /** @type {Validator[]} */ filteredValidators = this._allValidators.filter(
|
||||
v => !(v instanceof ResultValidator) && !(v instanceof Required),
|
||||
);
|
||||
const /** @type {Validator[]} */ syncValidators = filteredValidators.filter(
|
||||
v => !v.constructor.async,
|
||||
);
|
||||
const /** @type {Validator[]} */ asyncValidators = filteredValidators.filter(
|
||||
v => v.constructor.async,
|
||||
);
|
||||
const /** @type {Validator[]} */ syncValidators = filteredValidators.filter(v => {
|
||||
const vCtor = /** @type {typeof Validator} */ (v.constructor);
|
||||
return !vCtor.async;
|
||||
});
|
||||
const /** @type {Validator[]} */ asyncValidators = filteredValidators.filter(v => {
|
||||
const vCtor = /** @type {typeof Validator} */ (v.constructor);
|
||||
return vCtor.async;
|
||||
});
|
||||
|
||||
/**
|
||||
* 2. Synchronous validators
|
||||
|
|
@ -350,6 +340,8 @@ export const ValidateMixin = dedupeMixin(
|
|||
/**
|
||||
* @desc step A2, calls __finishValidation
|
||||
* @param {Validator[]} syncValidators
|
||||
* @param {unknown} value
|
||||
* @param {{ hasAsync: boolean }} opts
|
||||
*/
|
||||
__executeSyncValidators(syncValidators, value, { hasAsync }) {
|
||||
if (syncValidators.length) {
|
||||
|
|
@ -362,14 +354,13 @@ export const ValidateMixin = dedupeMixin(
|
|||
|
||||
/**
|
||||
* @desc step A3, calls __finishValidation
|
||||
* @param {Validator[]} filteredValidators all Validators except required and ResultValidators
|
||||
* @param {Validator[]} asyncValidators all Validators except required and ResultValidators
|
||||
* @param {?} value
|
||||
*/
|
||||
async __executeAsyncValidators(asyncValidators, value) {
|
||||
if (asyncValidators.length) {
|
||||
this.isPending = true;
|
||||
const resultPromises = asyncValidators.map(v =>
|
||||
v.execute(value, v.param, { node: this }),
|
||||
);
|
||||
const resultPromises = asyncValidators.map(v => v.execute(value, v.param, { node: this }));
|
||||
const booleanResults = await Promise.all(resultPromises);
|
||||
this.__asyncValidationResult = booleanResults
|
||||
.map((r, i) => asyncValidators[i]) // Create an array of Validators
|
||||
|
|
@ -384,10 +375,10 @@ export const ValidateMixin = dedupeMixin(
|
|||
* @param {Validator[]} regularValidationResult result of steps 1-3
|
||||
*/
|
||||
__executeResultValidators(regularValidationResult) {
|
||||
/** @type {ResultValidator[]} */
|
||||
const resultValidators = this._allValidators.filter(
|
||||
v => !v.constructor.async && v instanceof ResultValidator,
|
||||
);
|
||||
const resultValidators = /** @type {ResultValidator[]} */ (this._allValidators.filter(v => {
|
||||
const vCtor = /** @type {typeof Validator} */ (v.constructor);
|
||||
return !vCtor.async && v instanceof ResultValidator;
|
||||
}));
|
||||
|
||||
return resultValidators.filter(v =>
|
||||
v.executeOnResults({
|
||||
|
|
@ -404,18 +395,18 @@ export const ValidateMixin = dedupeMixin(
|
|||
* If not, we have nothing left to wait for.
|
||||
*/
|
||||
__finishValidation({ source, hasAsync }) {
|
||||
const /** @type {Validator[]} */ syncAndAsyncOutcome = [
|
||||
...this.__syncValidationResult,
|
||||
...this.__asyncValidationResult,
|
||||
];
|
||||
const syncAndAsyncOutcome = [...this.__syncValidationResult, ...this.__asyncValidationResult];
|
||||
// if we have any ResultValidators left, now is the time to run them...
|
||||
const resultOutCome = this.__executeResultValidators(syncAndAsyncOutcome);
|
||||
|
||||
/** @typedef {Validator[]} TotalValidationResult */
|
||||
this.__validationResult = [...resultOutCome, ...syncAndAsyncOutcome];
|
||||
// this._storeResultsOnInstance(this.__validationResult);
|
||||
|
||||
const validationStates = this.constructor.validationTypes.reduce(
|
||||
const ctor = /** @type {typeof import('../../types/validate/ValidateMixinTypes').ValidateHost} */ (this
|
||||
.constructor);
|
||||
|
||||
/** @type {Object.<string, Object.<string, boolean>>} */
|
||||
const validationStates = ctor.validationTypes.reduce(
|
||||
(acc, type) => ({ ...acc, [type]: {} }),
|
||||
{},
|
||||
);
|
||||
|
|
@ -423,7 +414,8 @@ export const ValidateMixin = dedupeMixin(
|
|||
if (!validationStates[v.type]) {
|
||||
validationStates[v.type] = {};
|
||||
}
|
||||
validationStates[v.type][v.constructor.validatorName] = true;
|
||||
const vCtor = /** @type {typeof Validator} */ (v.constructor);
|
||||
validationStates[v.type][vCtor.validatorName] = true;
|
||||
});
|
||||
this.validationStates = validationStates;
|
||||
|
||||
|
|
@ -432,15 +424,20 @@ export const ValidateMixin = dedupeMixin(
|
|||
/** private event that should be listened to by LionFieldSet */
|
||||
this.dispatchEvent(new Event('validate-performed', { bubbles: true }));
|
||||
if (source === 'async' || !hasAsync) {
|
||||
if (this.__validateCompleteResolve) {
|
||||
this.__validateCompleteResolve();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
__clearValidationResults() {
|
||||
this.__syncValidationResult = [];
|
||||
this.__asyncValidationResult = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Event|CustomEvent} e
|
||||
*/
|
||||
__onValidatorUpdated(e) {
|
||||
if (e.type === 'param-changed' || e.type === 'config-changed') {
|
||||
this.validate();
|
||||
|
|
@ -451,7 +448,11 @@ export const ValidateMixin = dedupeMixin(
|
|||
const events = ['param-changed', 'config-changed'];
|
||||
if (this.__prevValidators) {
|
||||
this.__prevValidators.forEach(v => {
|
||||
events.forEach(e => v.removeEventListener(e, this.__onValidatorUpdated));
|
||||
events.forEach(e => {
|
||||
if (v.removeEventListener) {
|
||||
v.removeEventListener(e, this.__onValidatorUpdated);
|
||||
}
|
||||
});
|
||||
v.onFormControlDisconnect(this);
|
||||
});
|
||||
}
|
||||
|
|
@ -464,31 +465,35 @@ export const ValidateMixin = dedupeMixin(
|
|||
console.error(errorMessage, this);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
if (this.constructor.validationTypes.indexOf(v.type) === -1) {
|
||||
const ctor = /** @type {typeof import('../../types/validate/ValidateMixinTypes').ValidateHost} */ (this
|
||||
.constructor);
|
||||
if (ctor.validationTypes.indexOf(v.type) === -1) {
|
||||
const vCtor = /** @type {typeof Validator} */ (v.constructor);
|
||||
// throws in constructor are not visible to end user so we do both
|
||||
const errorMessage = `This component does not support the validator type "${v.type}" used in "${v.constructor.validatorName}". You may change your validators type or add it to the components "static get validationTypes() {}".`;
|
||||
const errorMessage = `This component does not support the validator type "${v.type}" used in "${vCtor.validatorName}". You may change your validators type or add it to the components "static get validationTypes() {}".`;
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(errorMessage, this);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
events.forEach(e => v.addEventListener(e, this.__onValidatorUpdated));
|
||||
events.forEach(e => {
|
||||
if (v.addEventListener) {
|
||||
v.addEventListener(e, this.__onValidatorUpdated);
|
||||
}
|
||||
});
|
||||
v.onFormControlConnect(this);
|
||||
});
|
||||
this.__prevValidators = this._allValidators;
|
||||
}
|
||||
|
||||
static _hasObjectChanged(result, prevResult) {
|
||||
return JSON.stringify(result) !== JSON.stringify(prevResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {?} v
|
||||
*/
|
||||
__isEmpty(v) {
|
||||
if (typeof this._isEmpty === 'function') {
|
||||
return this._isEmpty(v);
|
||||
}
|
||||
return (
|
||||
this.modelValue === null ||
|
||||
typeof this.modelValue === 'undefined' ||
|
||||
this.modelValue === ''
|
||||
this.modelValue === null || typeof this.modelValue === 'undefined' || this.modelValue === ''
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -498,7 +503,7 @@ export const ValidateMixin = dedupeMixin(
|
|||
|
||||
/**
|
||||
* @typedef {object} FeedbackMessage
|
||||
* @property {string} message this
|
||||
* @property {string | Node} message this
|
||||
* @property {string} type will be 'error' for messages from default Validators. Could be
|
||||
* 'warning', 'info' etc. for Validators with custom types. Needed as a directive for
|
||||
* feedbackNode how to render a message of a certain type
|
||||
|
|
@ -510,7 +515,7 @@ export const ValidateMixin = dedupeMixin(
|
|||
|
||||
/**
|
||||
* @param {Validator[]} validators list of objects having a .getMessage method
|
||||
* @return {FeedbackMessage[]}
|
||||
* @return {Promise.<FeedbackMessage[]>}
|
||||
*/
|
||||
async __getFeedbackMessages(validators) {
|
||||
let fieldName = await this.fieldName;
|
||||
|
|
@ -544,7 +549,8 @@ export const ValidateMixin = dedupeMixin(
|
|||
* - we set aria-invalid="true" in case hasErrorVisible is true
|
||||
*/
|
||||
_updateFeedbackComponent() {
|
||||
if (!this._feedbackNode) {
|
||||
const { _feedbackNode } = this;
|
||||
if (!_feedbackNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -560,37 +566,28 @@ export const ValidateMixin = dedupeMixin(
|
|||
});
|
||||
const messageMap = await this.__getFeedbackMessages(this.__prioritizedResult);
|
||||
|
||||
this._feedbackNode.feedbackData = messageMap.length ? messageMap : [];
|
||||
_feedbackNode.feedbackData = messageMap.length ? messageMap : [];
|
||||
});
|
||||
} else {
|
||||
this.__feedbackQueue.add(async () => {
|
||||
this._feedbackNode.feedbackData = [];
|
||||
_feedbackNode.feedbackData = [];
|
||||
});
|
||||
}
|
||||
this.feedbackComplete = this.__feedbackQueue.complete;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Show the validity feedback when returning true, don't show when false
|
||||
* @param {string} type
|
||||
*/
|
||||
_showFeedbackConditionFor(/* type */) {
|
||||
return (this.touched && this.dirty) || this.prefilled || this.submitted;
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
_showFeedbackConditionFor(type) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} type
|
||||
*/
|
||||
_hasFeedbackVisibleFor(type) {
|
||||
return (
|
||||
this.hasFeedbackFor &&
|
||||
|
|
@ -600,6 +597,7 @@ export const ValidateMixin = dedupeMixin(
|
|||
);
|
||||
}
|
||||
|
||||
/** @param {import('lit-element').PropertyValues} changedProperties */
|
||||
updated(changedProperties) {
|
||||
super.updated(changedProperties);
|
||||
|
||||
|
|
@ -607,31 +605,41 @@ export const ValidateMixin = dedupeMixin(
|
|||
changedProperties.has('shouldShowFeedbackFor') ||
|
||||
changedProperties.has('hasFeedbackFor')
|
||||
) {
|
||||
this.showsFeedbackFor = this.constructor.validationTypes
|
||||
const ctor = /** @type {typeof import('../../types/validate/ValidateMixinTypes').ValidateHost} */ (this
|
||||
.constructor);
|
||||
// Necessary typecast because types aren't smart enough to understand that we filter out undefined
|
||||
this.showsFeedbackFor = /** @type {string[]} */ (ctor.validationTypes
|
||||
.map(type => (this._hasFeedbackVisibleFor(type) ? type : undefined))
|
||||
.filter(_ => !!_);
|
||||
.filter(_ => !!_));
|
||||
this._updateFeedbackComponent();
|
||||
}
|
||||
}
|
||||
|
||||
_updateShouldShowFeedbackFor() {
|
||||
this.shouldShowFeedbackFor = this.constructor.validationTypes
|
||||
const ctor = /** @type {typeof import('../../types/validate/ValidateMixinTypes').ValidateHost} */ (this
|
||||
.constructor);
|
||||
// Necessary typecast because types aren't smart enough to understand that we filter out undefined
|
||||
this.shouldShowFeedbackFor = /** @type {string[]} */ (ctor.validationTypes
|
||||
.map(type => (this._showFeedbackConditionFor(type) ? type : undefined))
|
||||
.filter(_ => !!_);
|
||||
.filter(_ => !!_));
|
||||
}
|
||||
|
||||
/**
|
||||
* @overridable
|
||||
* @desc Orders all active validators in this.__validationResult. Can
|
||||
* also filter out occurrences (based on interaction states)
|
||||
* @returns {Validator[]} ordered list of Validators with feedback messages visible to the
|
||||
* @param {{ validationResult: Validator[] }} opts
|
||||
* @return {Validator[]} ordered list of Validators with feedback messages visible to the
|
||||
* end user
|
||||
*/
|
||||
_prioritizeAndFilterFeedback({ validationResult }) {
|
||||
const types = this.constructor.validationTypes;
|
||||
const ctor = /** @type {typeof import('../../types/validate/ValidateMixinTypes').ValidateHost} */ (this
|
||||
.constructor);
|
||||
const types = ctor.validationTypes;
|
||||
// Sort all validators based on the type provided.
|
||||
const res = validationResult.sort((a, b) => types.indexOf(a.type) - types.indexOf(b.type));
|
||||
return res.slice(0, this._visibleMessagesAmount);
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const ValidateMixin = dedupeMixin(ValidateMixinImplementation);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
import { fakeExtendsEventTarget } from '../utils/fakeExtendsEventTarget.js';
|
||||
|
||||
export class Validator {
|
||||
/**
|
||||
*
|
||||
* @param {?} [param]
|
||||
* @param {Object.<string,?>} [config]
|
||||
*/
|
||||
constructor(param, config) {
|
||||
fakeExtendsEventTarget(this);
|
||||
this.__fakeExtendsEventTarget();
|
||||
|
||||
/** @type {?} */
|
||||
this.__param = param;
|
||||
|
||||
/** @type {Object.<string,?>} */
|
||||
this.__config = config || {};
|
||||
this.type = (config && config.type) || 'error'; // Default type supported by ValidateMixin
|
||||
}
|
||||
|
|
@ -19,22 +25,28 @@ export class Validator {
|
|||
|
||||
/**
|
||||
* @desc The function that returns a Boolean
|
||||
* @param {string|Date|Number|object} modelValue
|
||||
* @param {object} param
|
||||
* @param {?} [modelValue]
|
||||
* @param {?} [param]
|
||||
* @param {{}} [config]
|
||||
* @returns {Boolean|Promise<Boolean>}
|
||||
*/
|
||||
execute(/* modelValue, param */) {
|
||||
if (!this.validatorName) {
|
||||
// eslint-disable-next-line no-unused-vars, class-methods-use-this
|
||||
execute(modelValue, param, config) {
|
||||
const ctor = /** @type {typeof Validator} */ (this.constructor);
|
||||
if (!ctor.validatorName) {
|
||||
throw new Error(
|
||||
'A validator needs to have a name! Please set it via "static get validatorName() { return \'IsCat\'; }"',
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
set param(p) {
|
||||
this.__param = p;
|
||||
if (this.dispatchEvent) {
|
||||
this.dispatchEvent(new Event('param-changed'));
|
||||
}
|
||||
}
|
||||
|
||||
get param() {
|
||||
return this.__param;
|
||||
|
|
@ -42,8 +54,10 @@ export class Validator {
|
|||
|
||||
set config(c) {
|
||||
this.__config = c;
|
||||
if (this.dispatchEvent) {
|
||||
this.dispatchEvent(new Event('config-changed'));
|
||||
}
|
||||
}
|
||||
|
||||
get config() {
|
||||
return this.__config;
|
||||
|
|
@ -51,16 +65,18 @@ export class Validator {
|
|||
|
||||
/**
|
||||
* @overridable
|
||||
* @param {object} data
|
||||
* @param {*} data.modelValue
|
||||
* @param {string} data.fieldName
|
||||
* @param {*} data.params
|
||||
* @param {string} data.type
|
||||
* @returns {string|Node|Promise<stringOrNode>|() => stringOrNode)}
|
||||
* @param {object} [data]
|
||||
* @param {*} [data.modelValue]
|
||||
* @param {string} [data.fieldName]
|
||||
* @param {HTMLElement} [data.formControl]
|
||||
* @param {*} [data.params]
|
||||
* @param {string|undefined} [data.type]
|
||||
* @returns {Promise<string|Node>}
|
||||
*/
|
||||
async _getMessage(data) {
|
||||
const ctor = /** @type {typeof Validator} */ (this.constructor);
|
||||
const composedData = {
|
||||
name: this.constructor.validatorName,
|
||||
name: ctor.validatorName,
|
||||
type: this.type,
|
||||
params: this.param,
|
||||
config: this.config,
|
||||
|
|
@ -75,29 +91,32 @@ export class Validator {
|
|||
.config.getMessage}`,
|
||||
);
|
||||
}
|
||||
return this.constructor.getMessage(composedData);
|
||||
return ctor.getMessage(composedData);
|
||||
}
|
||||
|
||||
/**
|
||||
* @overridable
|
||||
* @param {object} data
|
||||
* @param {*} data.modelValue
|
||||
* @param {string} data.fieldName
|
||||
* @param {*} data.params
|
||||
* @param {string} data.type
|
||||
* @returns {string|Node|Promise<stringOrNode>|() => stringOrNode)}
|
||||
* @param {object} [data]
|
||||
* @param {*} [data.modelValue]
|
||||
* @param {string} [data.fieldName]
|
||||
* @param {*} [data.params]
|
||||
* @param {string} [data.type]
|
||||
* @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()"`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {FormControl} formControl
|
||||
* @param {HTMLElement} formControl
|
||||
*/
|
||||
onFormControlConnect(formControl) {} // eslint-disable-line
|
||||
|
||||
/**
|
||||
* @param {FormControl} formControl
|
||||
* @param {HTMLElement} formControl
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
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:
|
||||
|
|
|
|||
|
|
@ -1,14 +1,29 @@
|
|||
import { ResultValidator } from '../ResultValidator.js';
|
||||
|
||||
/**
|
||||
* @typedef {import('../Validator').Validator} Validator
|
||||
*/
|
||||
|
||||
export class DefaultSuccess extends ResultValidator {
|
||||
/**
|
||||
* @param {...any} args
|
||||
*/
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this.type = 'success';
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Object} context
|
||||
* @param {Validator[]} context.regularValidationResult
|
||||
* @param {Validator[]} context.prevValidationResult
|
||||
* @returns {boolean}
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
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 prevHadErrorOrWarning = !!prevValidationResult.filter(errorOrWarning).length;
|
||||
return !hasErrorOrWarning && prevHadErrorOrWarning;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@
|
|||
import { normalizeDateTime } from '@lion/localize';
|
||||
import { Validator } from '../Validator.js';
|
||||
|
||||
/**
|
||||
* @param {?} value
|
||||
*/
|
||||
function isDate(value) {
|
||||
return (
|
||||
Object.prototype.toString.call(value) === '[object Date]' && !Number.isNaN(value.getTime())
|
||||
|
|
@ -14,6 +17,9 @@ export class IsDate extends Validator {
|
|||
return 'IsDate';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {?} value
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
execute(value) {
|
||||
let hasError = false;
|
||||
|
|
@ -29,6 +35,9 @@ export class MinDate extends Validator {
|
|||
return 'MinDate';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {?} value
|
||||
*/
|
||||
execute(value, min = this.param) {
|
||||
let hasError = false;
|
||||
if (!isDate(value) || value < normalizeDateTime(min)) {
|
||||
|
|
@ -43,6 +52,9 @@ export class MaxDate extends Validator {
|
|||
return 'MaxDate';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {?} value
|
||||
*/
|
||||
execute(value, max = this.param) {
|
||||
let hasError = false;
|
||||
if (!isDate(value) || value > normalizeDateTime(max)) {
|
||||
|
|
@ -57,6 +69,9 @@ export class MinMaxDate extends Validator {
|
|||
return 'MinMaxDate';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {?} value
|
||||
*/
|
||||
execute(value, { min = 0, max = 0 } = this.param) {
|
||||
let hasError = false;
|
||||
if (!isDate(value) || value < normalizeDateTime(min) || value > normalizeDateTime(max)) {
|
||||
|
|
@ -71,6 +86,9 @@ export class IsDateDisabled extends Validator {
|
|||
return 'IsDateDisabled';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {?} value
|
||||
*/
|
||||
execute(value, isDisabledFn = this.param) {
|
||||
let hasError = false;
|
||||
if (!isDate(value) || isDisabledFn(value)) {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ export class IsNumber extends Validator {
|
|||
return 'IsNumber';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {?} value
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
execute(value) {
|
||||
let isEnabled = false;
|
||||
|
|
@ -30,6 +33,9 @@ export class MinNumber extends Validator {
|
|||
return 'MinNumber';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {?} value
|
||||
*/
|
||||
execute(value, min = this.param) {
|
||||
let isEnabled = false;
|
||||
if (!isNumber(value) || value < min) {
|
||||
|
|
@ -44,6 +50,9 @@ export class MaxNumber extends Validator {
|
|||
return 'MaxNumber';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {?} value
|
||||
*/
|
||||
execute(value, max = this.param) {
|
||||
let isEnabled = false;
|
||||
if (!isNumber(value) || value > max) {
|
||||
|
|
@ -58,6 +67,9 @@ export class MinMaxNumber extends Validator {
|
|||
return 'MinMaxNumber';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {?} value
|
||||
*/
|
||||
execute(value, { min = 0, max = 0 } = this.param) {
|
||||
let isEnabled = false;
|
||||
if (!isNumber(value) || value < min || value > max) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
import { Validator } from '../Validator.js';
|
||||
|
||||
/**
|
||||
* @typedef {import('../../../types/FormControlMixinTypes.js').FormControlHost} FormControlHost
|
||||
*/
|
||||
|
||||
export class Required extends Validator {
|
||||
static get validatorName() {
|
||||
return 'Required';
|
||||
|
|
@ -11,6 +15,9 @@ export class Required extends Validator {
|
|||
* FormControl.__isEmpty / FormControl._isEmpty.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {FormControlHost & HTMLElement} formControl
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
onFormControlConnect(formControl) {
|
||||
if (formControl._inputNode) {
|
||||
|
|
@ -18,6 +25,9 @@ export class Required extends Validator {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {FormControlHost & HTMLElement} formControl
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
onFormControlDisconnect(formControl) {
|
||||
if (formControl._inputNode) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
/* eslint-disable max-classes-per-file */
|
||||
import { Validator } from '../Validator.js';
|
||||
|
||||
/**
|
||||
* @param {?} value
|
||||
*/
|
||||
const isString = value => typeof value === 'string';
|
||||
|
||||
export class IsString extends Validator {
|
||||
|
|
@ -8,6 +11,9 @@ export class IsString extends Validator {
|
|||
return 'IsString';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {?} value
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
execute(value) {
|
||||
let hasError = false;
|
||||
|
|
@ -23,6 +29,9 @@ export class EqualsLength extends Validator {
|
|||
return 'EqualsLength';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {?} value
|
||||
*/
|
||||
execute(value, length = this.param) {
|
||||
let hasError = false;
|
||||
if (!isString(value) || value.length !== length) {
|
||||
|
|
@ -37,6 +46,9 @@ export class MinLength extends Validator {
|
|||
return 'MinLength';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {?} value
|
||||
*/
|
||||
execute(value, min = this.param) {
|
||||
let hasError = false;
|
||||
if (!isString(value) || value.length < min) {
|
||||
|
|
@ -51,6 +63,9 @@ export class MaxLength extends Validator {
|
|||
return 'MaxLength';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {?} value
|
||||
*/
|
||||
execute(value, max = this.param) {
|
||||
let hasError = false;
|
||||
if (!isString(value) || value.length > max) {
|
||||
|
|
@ -65,6 +80,9 @@ export class MinMaxLength extends Validator {
|
|||
return 'MinMaxLength';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {?} value
|
||||
*/
|
||||
execute(value, { min = 0, max = 0 } = this.param) {
|
||||
let hasError = false;
|
||||
if (!isString(value) || value.length < min || value.length > max) {
|
||||
|
|
@ -80,6 +98,9 @@ export class IsEmail extends Validator {
|
|||
return 'IsEmail';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {?} value
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
execute(value) {
|
||||
let hasError = false;
|
||||
|
|
@ -90,12 +111,19 @@ export class IsEmail extends Validator {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {?} value
|
||||
* @param {RegExp} pattern
|
||||
*/
|
||||
const hasPattern = (value, pattern) => pattern.test(value);
|
||||
export class Pattern extends Validator {
|
||||
static get validatorName() {
|
||||
return 'Pattern';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {?} value
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
execute(value, pattern = this.param) {
|
||||
if (!(pattern instanceof RegExp)) {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@ export class AlwaysValid extends Validator {
|
|||
return 'AlwaysValid';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Promise<boolean> | boolean}
|
||||
*/
|
||||
execute() {
|
||||
const showMessage = false;
|
||||
return showMessage;
|
||||
|
|
@ -28,7 +31,10 @@ export class AsyncAlwaysValid extends AlwaysValid {
|
|||
return true;
|
||||
}
|
||||
|
||||
execute() {
|
||||
/**
|
||||
* @return {Promise<boolean>}
|
||||
*/
|
||||
async execute() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -38,6 +44,9 @@ export class AsyncAlwaysInvalid extends AlwaysValid {
|
|||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Promise<boolean>}
|
||||
*/
|
||||
async execute() {
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,28 +6,35 @@ import { FormRegistrarPortalMixin } from '../src/registration/FormRegistrarPorta
|
|||
|
||||
/**
|
||||
* @typedef {import('../types/registration/FormRegistrarMixinTypes').FormRegistrarHost} FormRegistrarHost
|
||||
* @typedef {import('../types/registration/FormRegistrarMixinTypes').FormRegistrarMixin} FormRegistrarMixin
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Object} customConfig
|
||||
* @param {string} customConfig.suffix
|
||||
* @param {string} customConfig.parentTagString
|
||||
* @param {string} customConfig.childTagString
|
||||
* @param {string} customConfig.portalTagString
|
||||
* @typedef {Object} customConfig
|
||||
* @property {typeof HTMLElement | typeof import('@lion/core').UpdatingElement | typeof LitElement} [baseElement]
|
||||
* @property {string} [customConfig.suffix]
|
||||
* @property {string} [customConfig.parentTagString]
|
||||
* @property {string} [customConfig.childTagString]
|
||||
* @property {string} [customConfig.portalTagString]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {customConfig} customConfig
|
||||
*/
|
||||
export const runRegistrationSuite = customConfig => {
|
||||
const cfg = {
|
||||
// @ts-expect-error https://github.com/microsoft/TypeScript/issues/38535
|
||||
baseElement: HTMLElement,
|
||||
...customConfig,
|
||||
};
|
||||
|
||||
describe(`FormRegistrationMixins ${cfg.suffix}`, () => {
|
||||
// @ts-expect-error base constructors same return type & type cannot be assigned like this
|
||||
class RegistrarClass extends FormRegistrarMixin(cfg.baseElement) {}
|
||||
cfg.parentTagString = defineCE(RegistrarClass);
|
||||
// @ts-expect-error base constructors same return type & type cannot be assigned like this
|
||||
class RegisteringClass extends FormRegisteringMixin(cfg.baseElement) {}
|
||||
cfg.childTagString = defineCE(RegisteringClass);
|
||||
// @ts-expect-error base constructors same return type & type cannot be assigned like this
|
||||
class PortalClass extends FormRegistrarPortalMixin(cfg.baseElement) {}
|
||||
cfg.portalTagString = defineCE(PortalClass);
|
||||
|
||||
|
|
@ -84,6 +91,7 @@ export const runRegistrationSuite = customConfig => {
|
|||
});
|
||||
|
||||
it('works for components that have a delayed render', async () => {
|
||||
// @ts-expect-error base constructors same return type
|
||||
class PerformUpdate extends FormRegistrarMixin(LitElement) {
|
||||
async performUpdate() {
|
||||
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 sinon from 'sinon';
|
||||
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
|
||||
*/
|
||||
|
||||
// @ts-expect-error base constructor same return type
|
||||
class FormatClass extends FormatMixin(LitElement) {
|
||||
get _inputNode() {
|
||||
return /** @type {HTMLInputElement} */ (super._inputNode); // casts type
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`<slot name="input"></slot>`;
|
||||
}
|
||||
|
|
@ -29,15 +30,10 @@ class FormatClass extends FormatMixin(LitElement) {
|
|||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
get _inputNode() {
|
||||
return this.querySelector('input');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {FormatClass & inputNodeHost} formControl
|
||||
* @param {FormatClass} formControl
|
||||
* @param {?} 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) {
|
||||
const cfg = {
|
||||
|
|
@ -95,7 +91,7 @@ export function runFormatMixinSuite(customConfig) {
|
|||
let elem;
|
||||
/** @type {FormatClass} */
|
||||
let nonFormat;
|
||||
/** @type {FormatClass & inputNodeHost} */
|
||||
/** @type {FormatClass} */
|
||||
let fooFormat;
|
||||
|
||||
before(async () => {
|
||||
|
|
@ -128,7 +124,7 @@ export function runFormatMixinSuite(customConfig) {
|
|||
});
|
||||
|
||||
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}>`,
|
||||
));
|
||||
|
||||
|
|
@ -215,7 +211,7 @@ export function runFormatMixinSuite(customConfig) {
|
|||
|
||||
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>
|
||||
const formatElem = /** @type {FormatClass & inputNodeHost} */ (await fixture(html`
|
||||
const formatElem = /** @type {FormatClass} */ (await fixture(html`
|
||||
<${elem}
|
||||
value="string"
|
||||
.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 () => {
|
||||
const formatEl = /** @type {FormatClass & inputNodeHost} */ (await fixture(html`
|
||||
const formatEl = /** @type {FormatClass} */ (await fixture(html`
|
||||
<${elem} .formatter="${/** @param {string} value */ value => `foo: ${value}`}">
|
||||
<input slot="input" />
|
||||
</${elem}>
|
||||
|
|
@ -255,14 +251,14 @@ export function runFormatMixinSuite(customConfig) {
|
|||
});
|
||||
|
||||
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}`}">
|
||||
<input slot="input" />
|
||||
</${elem}>
|
||||
`));
|
||||
// The FormatMixin can be used in conjunction with the ValidateMixin, in which case
|
||||
// it can hold errorState (affecting the formatting)
|
||||
el.errorState = true;
|
||||
el.hasFeedbackFor = ['error'];
|
||||
|
||||
// users types value 'test'
|
||||
mimicUserInput(el, 'test');
|
||||
|
|
@ -274,6 +270,7 @@ export function runFormatMixinSuite(customConfig) {
|
|||
});
|
||||
|
||||
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 tagNoInput = unsafeStatic(tagNoInputString);
|
||||
expect(async () => {
|
||||
|
|
@ -300,7 +297,9 @@ export function runFormatMixinSuite(customConfig) {
|
|||
|
||||
it('should have formatOptions available in formatter', async () => {
|
||||
const formatterSpy = sinon.spy(value => `foo: ${value}`);
|
||||
const generatedViewValue = generateValueBasedOnType({ viewValue: true });
|
||||
const generatedViewValue = /** @type {string} */ (generateValueBasedOnType({
|
||||
viewValue: true,
|
||||
}));
|
||||
await fixture(html`
|
||||
<${elem} value="${generatedViewValue}" .formatter="${formatterSpy}"
|
||||
.formatOptions="${{ locale: 'en-GB', decimalSeparator: '-' }}">
|
||||
|
|
@ -319,7 +318,7 @@ export function runFormatMixinSuite(customConfig) {
|
|||
/** @type {?} */
|
||||
const generatedValue = generateValueBasedOnType();
|
||||
const parserSpy = sinon.spy();
|
||||
const el = /** @type {FormatClass & inputNodeHost} */ (await fixture(html`
|
||||
const el = /** @type {FormatClass} */ (await fixture(html`
|
||||
<${elem} .parser="${parserSpy}">
|
||||
<input slot="input" value="${generatedValue}">
|
||||
</${elem}>
|
||||
|
|
@ -335,7 +334,7 @@ export function runFormatMixinSuite(customConfig) {
|
|||
});
|
||||
|
||||
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}>
|
||||
<input slot="input" value="string">
|
||||
</${elem}>
|
||||
|
|
@ -359,7 +358,7 @@ export function runFormatMixinSuite(customConfig) {
|
|||
toggleValue: true,
|
||||
});
|
||||
|
||||
const el = /** @type {FormatClass & inputNodeHost & validateHost} */ (await fixture(html`
|
||||
const el = /** @type {FormatClass} */ (await fixture(html`
|
||||
<${elem} .formatter=${formatterSpy}>
|
||||
<input slot="input" value="${generatedViewValue}">
|
||||
</${elem}>
|
||||
|
|
@ -371,7 +370,7 @@ export function runFormatMixinSuite(customConfig) {
|
|||
// 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.
|
||||
|
||||
/* const AlwaysInvalid = class extends Validator {
|
||||
const AlwaysInvalid = class extends Validator {
|
||||
static get validatorName() {
|
||||
return 'AlwaysInvalid';
|
||||
}
|
||||
|
|
@ -379,9 +378,9 @@ export function runFormatMixinSuite(customConfig) {
|
|||
execute() {
|
||||
return true;
|
||||
}
|
||||
}; */
|
||||
// @ts-ignore // FIXME: remove this ignore after Validator class is typed
|
||||
// el.validators = [new AlwaysInvalid()];
|
||||
};
|
||||
|
||||
el.validators = [new AlwaysInvalid()];
|
||||
mimicUserInput(el, generatedViewValueAlt);
|
||||
|
||||
expect(formatterSpy.callCount).to.equal(1);
|
||||
|
|
@ -398,19 +397,21 @@ export function runFormatMixinSuite(customConfig) {
|
|||
});
|
||||
|
||||
describe('Unparseable values', () => {
|
||||
// it('should convert to Unparseable when wrong value inputted by user', async () => {
|
||||
// const el = /** @type {FormatClass & inputNodeHost} */ (await fixture(html`
|
||||
// <${elem} .parser=${viewValue => Number(viewValue) || undefined}
|
||||
// >
|
||||
// <input slot="input">
|
||||
// </${elem}>
|
||||
// `));
|
||||
// mimicUserInput(el, 'test');
|
||||
// expect(el.modelValue).to.be.an.instanceof(Unparseable);
|
||||
// });
|
||||
it('should convert to Unparseable when wrong value inputted by user', async () => {
|
||||
const el = /** @type {FormatClass} */ (await fixture(html`
|
||||
<${elem} .parser=${
|
||||
/** @param {string} viewValue */ viewValue => Number(viewValue) || undefined
|
||||
}
|
||||
>
|
||||
<input slot="input">
|
||||
</${elem}>
|
||||
`));
|
||||
mimicUserInput(el, 'test');
|
||||
expect(el.modelValue).to.be.an.instanceof(Unparseable);
|
||||
});
|
||||
|
||||
it('should preserve the viewValue when not parseable', async () => {
|
||||
const el = /** @type {FormatClass & inputNodeHost} */ (await fixture(html`
|
||||
const el = /** @type {FormatClass} */ (await fixture(html`
|
||||
<${elem}
|
||||
.parser=${/** @param {string} viewValue */ viewValue => Number(viewValue) || undefined}
|
||||
>
|
||||
|
|
@ -422,17 +423,17 @@ export function runFormatMixinSuite(customConfig) {
|
|||
expect(el.value).to.equal('test');
|
||||
});
|
||||
|
||||
// it('should display the viewValue when modelValue is of type Unparseable', async () => {
|
||||
// const el = /** @type {FormatClass} */ (await fixture(html`
|
||||
// <${elem}
|
||||
// .parser=${/** @param {string} viewValue */ viewValue => Number(viewValue) || undefined}
|
||||
// >
|
||||
// <input slot="input">
|
||||
// </${elem}>
|
||||
// `));
|
||||
// el.modelValue = new Unparseable('foo');
|
||||
// expect(el.value).to.equal('foo');
|
||||
// });
|
||||
it('should display the viewValue when modelValue is of type Unparseable', async () => {
|
||||
const el = /** @type {FormatClass} */ (await fixture(html`
|
||||
<${elem}
|
||||
.parser=${/** @param {string} viewValue */ viewValue => Number(viewValue) || undefined}
|
||||
>
|
||||
<input slot="input">
|
||||
</${elem}>
|
||||
`));
|
||||
el.modelValue = new Unparseable('foo');
|
||||
expect(el.value).to.equal('foo');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import {
|
|||
} from '@open-wc/testing';
|
||||
import sinon from 'sinon';
|
||||
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]
|
||||
|
|
@ -22,7 +24,8 @@ export function runInteractionStateMixinSuite(customConfig) {
|
|||
};
|
||||
|
||||
describe(`InteractionStateMixin`, async () => {
|
||||
class IState extends InteractionStateMixin(LitElement) {
|
||||
// @ts-expect-error base constructors same return type
|
||||
class IState extends InteractionStateMixin(ValidateMixin(LitElement)) {
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.tabIndex = 0;
|
||||
|
|
@ -207,8 +210,41 @@ export function runInteractionStateMixinSuite(customConfig) {
|
|||
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', () => {
|
||||
it('can override the `_leaveEvent`', async () => {
|
||||
// @ts-expect-error base constructor same return type
|
||||
class IStateCustomBlur extends InteractionStateMixin(LitElement) {
|
||||
constructor() {
|
||||
super();
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@ import {
|
|||
AsyncAlwaysValid,
|
||||
} from '../test-helpers.js';
|
||||
|
||||
/**
|
||||
* @param {{tagString?: string | null, lightDom?: string}} [customConfig]
|
||||
*/
|
||||
export function runValidateMixinSuite(customConfig) {
|
||||
const cfg = {
|
||||
tagString: null,
|
||||
|
|
@ -24,32 +27,19 @@ export function runValidateMixinSuite(customConfig) {
|
|||
};
|
||||
|
||||
const lightDom = cfg.lightDom || '';
|
||||
const tagString =
|
||||
cfg.tagString ||
|
||||
defineCE(
|
||||
class extends ValidateMixin(LitElement) {
|
||||
static get properties() {
|
||||
return { modelValue: String };
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const tag = unsafeStatic(tagString);
|
||||
const withInputTagString =
|
||||
cfg.tagString ||
|
||||
defineCE(
|
||||
class extends ValidateMixin(LitElement) {
|
||||
// @ts-expect-error base constructor same return type
|
||||
class ValidateElement extends ValidateMixin(LitElement) {
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.appendChild(document.createElement('input'));
|
||||
const inputNode = document.createElement('input');
|
||||
inputNode.slot = 'input';
|
||||
this.appendChild(inputNode);
|
||||
}
|
||||
}
|
||||
|
||||
get _inputNode() {
|
||||
return this.querySelector('input');
|
||||
}
|
||||
},
|
||||
);
|
||||
const withInputTag = unsafeStatic(withInputTagString);
|
||||
const tagString = cfg.tagString || defineCE(ValidateElement);
|
||||
const tag = unsafeStatic(tagString);
|
||||
|
||||
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 () => {
|
||||
// we throw and console error as constructor throw are not visible to the end user
|
||||
const stub = sinon.stub(console, 'error');
|
||||
const el = await fixture(html`<${tag}></${tag}>`);
|
||||
const el = /** @type {ValidateElement} */ (await fixture(html`<${tag}></${tag}>`));
|
||||
const errorMessage =
|
||||
'Validators array only accepts class instances of Validator. Type "array" found.';
|
||||
expect(() => {
|
||||
// @ts-expect-error putting the wrong value on purpose
|
||||
el.validators = [[new Required()]];
|
||||
}).to.throw(errorMessage);
|
||||
expect(stub.args[0][0]).to.equal(errorMessage);
|
||||
|
|
@ -89,6 +80,7 @@ export function runValidateMixinSuite(customConfig) {
|
|||
const errorMessage2 =
|
||||
'Validators array only accepts class instances of Validator. Type "string" found.';
|
||||
expect(() => {
|
||||
// @ts-expect-error because we purposely put a wrong type
|
||||
el.validators = ['required'];
|
||||
}).to.throw(errorMessage2);
|
||||
expect(stub.args[1][0]).to.equal(errorMessage2);
|
||||
|
|
@ -110,7 +102,7 @@ export function runValidateMixinSuite(customConfig) {
|
|||
return 'MajorValidator';
|
||||
}
|
||||
}
|
||||
const el = await fixture(html`<${tag}></${tag}>`);
|
||||
const el = /** @type {ValidateElement} */ (await fixture(html`<${tag}></${tag}>`));
|
||||
expect(() => {
|
||||
el.validators = [new MajorValidator()];
|
||||
}).to.throw(errorMessage);
|
||||
|
|
@ -120,21 +112,21 @@ export function runValidateMixinSuite(customConfig) {
|
|||
});
|
||||
|
||||
it('validates on initialization (once form field has bootstrapped/initialized)', async () => {
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||
<${tag}
|
||||
.validators=${[new Required()]}
|
||||
>${lightDom}</${tag}>
|
||||
`);
|
||||
`));
|
||||
expect(el.hasFeedbackFor).to.deep.equal(['error']);
|
||||
});
|
||||
|
||||
it('revalidates when ".modelValue" changes', async () => {
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||
<${tag}
|
||||
.validators=${[new AlwaysValid()]}
|
||||
.modelValue=${'myValue'}
|
||||
>${lightDom}</${tag}>
|
||||
`);
|
||||
`));
|
||||
|
||||
const validateSpy = sinon.spy(el, 'validate');
|
||||
el.modelValue = 'x';
|
||||
|
|
@ -142,12 +134,12 @@ export function runValidateMixinSuite(customConfig) {
|
|||
});
|
||||
|
||||
it('revalidates when ".validators" changes', async () => {
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||
<${tag}
|
||||
.validators=${[new AlwaysValid()]}
|
||||
.modelValue=${'myValue'}
|
||||
>${lightDom}</${tag}>
|
||||
`);
|
||||
`));
|
||||
|
||||
const validateSpy = sinon.spy(el, 'validate');
|
||||
el.validators = [new MinLength(3)];
|
||||
|
|
@ -155,12 +147,12 @@ export function runValidateMixinSuite(customConfig) {
|
|||
});
|
||||
|
||||
it('clears current results when ".modelValue" changes', async () => {
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||
<${tag}
|
||||
.validators=${[new AlwaysValid()]}
|
||||
.modelValue=${'myValue'}
|
||||
>${lightDom}</${tag}>
|
||||
`);
|
||||
`));
|
||||
|
||||
const clearSpy = sinon.spy(el, '__clearValidationResults');
|
||||
const validateSpy = sinon.spy(el, 'validate');
|
||||
|
|
@ -180,9 +172,9 @@ export function runValidateMixinSuite(customConfig) {
|
|||
it('firstly checks for empty values', async () => {
|
||||
const alwaysValid = new AlwaysValid();
|
||||
const alwaysValidExecuteSpy = sinon.spy(alwaysValid, 'execute');
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||
<${tag} .validators=${[alwaysValid]}>${lightDom}</${tag}>
|
||||
`);
|
||||
`));
|
||||
const isEmptySpy = sinon.spy(el, '__isEmpty');
|
||||
const validateSpy = sinon.spy(el, 'validate');
|
||||
el.modelValue = '';
|
||||
|
|
@ -197,9 +189,9 @@ export function runValidateMixinSuite(customConfig) {
|
|||
});
|
||||
|
||||
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}>
|
||||
`);
|
||||
`));
|
||||
const isEmptySpy = sinon.spy(el, '__isEmpty');
|
||||
const syncSpy = sinon.spy(el, '__executeSyncValidators');
|
||||
el.modelValue = 'nonEmpty';
|
||||
|
|
@ -207,11 +199,11 @@ export function runValidateMixinSuite(customConfig) {
|
|||
});
|
||||
|
||||
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()]}>
|
||||
${lightDom}
|
||||
</${tag}>
|
||||
`);
|
||||
`));
|
||||
const syncSpy = sinon.spy(el, '__executeSyncValidators');
|
||||
const asyncSpy = sinon.spy(el, '__executeAsyncValidators');
|
||||
el.modelValue = 'nonEmpty';
|
||||
|
|
@ -225,12 +217,12 @@ export function runValidateMixinSuite(customConfig) {
|
|||
}
|
||||
}
|
||||
|
||||
let el = await fixture(html`
|
||||
let el = /** @type {ValidateElement} */ (await fixture(html`
|
||||
<${tag}
|
||||
.validators=${[new AlwaysValid(), new MyResult()]}>
|
||||
${lightDom}
|
||||
</${tag}>
|
||||
`);
|
||||
`));
|
||||
|
||||
const syncSpy = sinon.spy(el, '__executeSyncValidators');
|
||||
const resultSpy2 = sinon.spy(el, '__executeResultValidators');
|
||||
|
|
@ -257,11 +249,11 @@ export function runValidateMixinSuite(customConfig) {
|
|||
|
||||
describe('Finalization', () => {
|
||||
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()]}>
|
||||
${lightDom}
|
||||
</${tag}>
|
||||
`);
|
||||
`));
|
||||
const cbSpy = sinon.spy();
|
||||
el.addEventListener('validate-performed', cbSpy);
|
||||
el.modelValue = 'nonEmpty';
|
||||
|
|
@ -269,11 +261,11 @@ export function runValidateMixinSuite(customConfig) {
|
|||
});
|
||||
|
||||
it('resolves ".validateComplete" Promise', async () => {
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||
<${tag} .validators=${[new AsyncAlwaysInvalid()]}>
|
||||
${lightDom}
|
||||
</${tag}>
|
||||
`);
|
||||
`));
|
||||
el.modelValue = 'nonEmpty';
|
||||
const validateResolveSpy = sinon.spy(el, '__validateCompleteResolve');
|
||||
await el.validateComplete;
|
||||
|
|
@ -284,8 +276,16 @@ export function runValidateMixinSuite(customConfig) {
|
|||
|
||||
describe('Validator Integration', () => {
|
||||
class IsCat extends Validator {
|
||||
/**
|
||||
* @param {...any} args
|
||||
*/
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
/**
|
||||
*
|
||||
* @param {string} modelValue
|
||||
* @param {{number: number}} [param]
|
||||
*/
|
||||
this.execute = (modelValue, param) => {
|
||||
const validateString = param && param.number ? `cat${param.number}` : 'cat';
|
||||
const showError = modelValue !== validateString;
|
||||
|
|
@ -299,6 +299,9 @@ export function runValidateMixinSuite(customConfig) {
|
|||
}
|
||||
|
||||
class OtherValidator extends Validator {
|
||||
/**
|
||||
* @param {...any} args
|
||||
*/
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this.execute = () => true;
|
||||
|
|
@ -318,6 +321,7 @@ export function runValidateMixinSuite(customConfig) {
|
|||
.modelValue=${'model'}
|
||||
>${lightDom}</${tag}>
|
||||
`);
|
||||
// @ts-expect-error weird sinon type error..
|
||||
expect(otherValidatorSpy.calledWith('model')).to.be.true;
|
||||
});
|
||||
|
||||
|
|
@ -330,6 +334,7 @@ export function runValidateMixinSuite(customConfig) {
|
|||
.modelValue=${new Unparseable('view')}
|
||||
>${lightDom}</${tag}>
|
||||
`);
|
||||
// @ts-expect-error weird sinon type error..
|
||||
expect(otherValidatorSpy.calledWith('view')).to.be.true;
|
||||
});
|
||||
|
||||
|
|
@ -355,13 +360,14 @@ export function runValidateMixinSuite(customConfig) {
|
|||
.modelValue=${'cat'}
|
||||
>${lightDom}</${tag}>
|
||||
`);
|
||||
// @ts-expect-error another sinon type problem
|
||||
expect(executeSpy.args[0][2].node).to.equal(el);
|
||||
});
|
||||
|
||||
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}>
|
||||
`);
|
||||
`));
|
||||
|
||||
el.modelValue = 'cat';
|
||||
expect(el.validationStates.error.IsCat).to.be.undefined;
|
||||
|
|
@ -374,12 +380,12 @@ export function runValidateMixinSuite(customConfig) {
|
|||
it('Validators get retriggered on parameter change', async () => {
|
||||
const isCatValidator = new IsCat('Felix');
|
||||
const catSpy = sinon.spy(isCatValidator, 'execute');
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||
<${tag}
|
||||
.validators=${[isCatValidator]}
|
||||
.modelValue=${'cat'}
|
||||
>${lightDom}</${tag}>
|
||||
`);
|
||||
`));
|
||||
el.modelValue = 'cat';
|
||||
expect(catSpy.callCount).to.equal(1);
|
||||
isCatValidator.param = 'Garfield';
|
||||
|
|
@ -388,7 +394,9 @@ export function runValidateMixinSuite(customConfig) {
|
|||
});
|
||||
|
||||
describe('Async Validator Integration', () => {
|
||||
/** @type {Promise<any>} */
|
||||
let asyncVPromise;
|
||||
/** @type {function} */
|
||||
let asyncVResolve;
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
@ -421,36 +429,36 @@ export function runValidateMixinSuite(customConfig) {
|
|||
// default execution trigger is keyup (think of password availability backend)
|
||||
// can configure execution trigger (blur, etc?)
|
||||
it('handles "execute" functions returning promises', async () => {
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||
<${tag}
|
||||
.modelValue=${'dog'}
|
||||
.validators=${[new IsAsyncCat()]}>
|
||||
${lightDom}
|
||||
</${tag}>
|
||||
`);
|
||||
`));
|
||||
|
||||
const validator = el.validators[0];
|
||||
expect(validator instanceof Validator).to.be.true;
|
||||
expect(el.hasFeedbackFor).to.deep.equal([]);
|
||||
asyncVResolve();
|
||||
await aTimeout();
|
||||
await aTimeout(0);
|
||||
expect(el.hasFeedbackFor).to.deep.equal(['error']);
|
||||
});
|
||||
|
||||
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}>
|
||||
`);
|
||||
`));
|
||||
expect(el.isPending).to.be.false;
|
||||
expect(el.hasAttribute('is-pending')).to.be.false;
|
||||
|
||||
el.validators = [new IsAsyncCat()];
|
||||
expect(el.isPending).to.be.true;
|
||||
await aTimeout();
|
||||
await aTimeout(0);
|
||||
expect(el.hasAttribute('is-pending')).to.be.true;
|
||||
|
||||
asyncVResolve();
|
||||
await aTimeout();
|
||||
await aTimeout(0);
|
||||
expect(el.isPending).to.be.false;
|
||||
expect(el.hasAttribute('is-pending')).to.be.false;
|
||||
});
|
||||
|
|
@ -460,11 +468,11 @@ export function runValidateMixinSuite(customConfig) {
|
|||
const asyncV = new IsAsyncCat();
|
||||
const asyncVExecuteSpy = sinon.spy(asyncV, 'execute');
|
||||
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||
<${tag} .modelValue=${'dog'}>
|
||||
${lightDom}
|
||||
</${tag}>
|
||||
`);
|
||||
`));
|
||||
// debounce started
|
||||
el.validators = [asyncV];
|
||||
expect(asyncVExecuteSpy.called).to.equal(0);
|
||||
|
|
@ -473,7 +481,7 @@ export function runValidateMixinSuite(customConfig) {
|
|||
expect(asyncVExecuteSpy.called).to.equal(1);
|
||||
|
||||
// New validation cycle. Now change modelValue inbetween, so validation is retriggered.
|
||||
asyncVExecuteSpy.reset();
|
||||
asyncVExecuteSpy.resetHistory();
|
||||
el.modelValue = 'dogger';
|
||||
expect(asyncVExecuteSpy.called).to.equal(0);
|
||||
el.modelValue = 'doggerer';
|
||||
|
|
@ -488,13 +496,13 @@ export function runValidateMixinSuite(customConfig) {
|
|||
|
||||
it.skip('cancels and reschedules async validation on ".modelValue" change', async () => {
|
||||
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'}>
|
||||
${lightDom}
|
||||
</${tag}>
|
||||
`);
|
||||
`));
|
||||
// debounce started
|
||||
el.validators = [asyncV];
|
||||
expect(asyncVAbortSpy.called).to.equal(0);
|
||||
|
|
@ -508,16 +516,19 @@ export function runValidateMixinSuite(customConfig) {
|
|||
const asyncV = new IsAsyncCat();
|
||||
const asyncVExecuteSpy = sinon.spy(asyncV, 'execute');
|
||||
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {ValidateElement & { isFocused: boolean }} */ (await fixture(html`
|
||||
<${tag}
|
||||
.isFocused=${true}
|
||||
.modelValue=${'dog'}
|
||||
.validators=${[asyncV]}
|
||||
.asyncValidateOn=${({ formControl }) => !formControl.isFocused}
|
||||
.asyncValidateOn=${
|
||||
/** @param {{ formControl: { isFocused: boolean } }} opts */ ({ formControl }) =>
|
||||
!formControl.isFocused
|
||||
}
|
||||
>
|
||||
${lightDom}
|
||||
</${tag}>
|
||||
`);
|
||||
`));
|
||||
|
||||
expect(asyncVExecuteSpy.called).to.equal(0);
|
||||
el.isFocused = false;
|
||||
|
|
@ -527,35 +538,38 @@ export function runValidateMixinSuite(customConfig) {
|
|||
});
|
||||
|
||||
describe('ResultValidator Integration', () => {
|
||||
let MySuccessResultValidator;
|
||||
let withSuccessTagString;
|
||||
let withSuccessTag;
|
||||
|
||||
before(() => {
|
||||
MySuccessResultValidator = class extends ResultValidator {
|
||||
const MySuccessResultValidator = class extends ResultValidator {
|
||||
/**
|
||||
* @param {...any} args
|
||||
*/
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this.type = 'success';
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {{ regularValidationResult: Validator[], prevValidationResult: Validator[]}} param0
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
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 prevHadErrorOrWarning = !!prevValidationResult.filter(errorOrWarning).length;
|
||||
return !hasErrorOrWarning && prevHadErrorOrWarning;
|
||||
}
|
||||
};
|
||||
|
||||
withSuccessTagString = defineCE(
|
||||
const withSuccessTagString = defineCE(
|
||||
// @ts-expect-error
|
||||
class extends ValidateMixin(LitElement) {
|
||||
static get validationTypes() {
|
||||
return [...super.validationTypes, 'success'];
|
||||
}
|
||||
},
|
||||
);
|
||||
withSuccessTag = unsafeStatic(withSuccessTagString);
|
||||
});
|
||||
const withSuccessTag = unsafeStatic(withSuccessTagString);
|
||||
|
||||
it('calls ResultValidators after regular validators', async () => {
|
||||
const resultValidator = new MySuccessResultValidator();
|
||||
|
|
@ -589,12 +603,12 @@ export function runValidateMixinSuite(customConfig) {
|
|||
const resultValidator = new MySuccessResultValidator();
|
||||
const resultValidateSpy = sinon.spy(resultValidator, 'executeOnResults');
|
||||
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||
<${withSuccessTag}
|
||||
.validators=${[new MinLength(3), resultValidator]}
|
||||
.modelValue=${'myValue'}
|
||||
>${lightDom}</${withSuccessTag}>
|
||||
`);
|
||||
`));
|
||||
const prevValidationResult = el.__prevValidationResult;
|
||||
const regularValidationResult = [
|
||||
...el.__syncValidationResult,
|
||||
|
|
@ -619,26 +633,26 @@ export function runValidateMixinSuite(customConfig) {
|
|||
const validator = new AlwaysInvalid();
|
||||
const resultV = new AlwaysInvalidResult();
|
||||
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||
<${tag}
|
||||
.validators=${[validator, resultV]}
|
||||
.modelValue=${'myValue'}
|
||||
>${lightDom}</${tag}>
|
||||
`);
|
||||
`));
|
||||
|
||||
const /** @type {TotalValidationResult} */ totalValidationResult = el.__validationResult;
|
||||
const totalValidationResult = el.__validationResult;
|
||||
expect(totalValidationResult).to.eql([resultV, validator]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Required Validator integration', () => {
|
||||
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}
|
||||
.validators=${[new Required()]}
|
||||
.modelValue=${''}
|
||||
>${lightDom}</${tag}>
|
||||
`);
|
||||
`));
|
||||
expect(el.validationStates.error.Required).to.be.true;
|
||||
expect(el.hasFeedbackFor).to.deep.equal(['error']);
|
||||
|
||||
|
|
@ -648,13 +662,13 @@ export function runValidateMixinSuite(customConfig) {
|
|||
});
|
||||
|
||||
it('calls private ".__isEmpty" by default', async () => {
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||
<${tag}
|
||||
.validators=${[new Required()]}
|
||||
.modelValue=${''}
|
||||
>${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 privateIsEmptySpy = sinon.spy(el, '__isEmpty');
|
||||
el.modelValue = null;
|
||||
|
|
@ -663,21 +677,22 @@ export function runValidateMixinSuite(customConfig) {
|
|||
});
|
||||
|
||||
it('calls "._isEmpty" when provided (useful for different modelValues)', async () => {
|
||||
const customRequiredTagString = defineCE(
|
||||
class extends ValidateMixin(LitElement) {
|
||||
// @ts-expect-error base constructor same return type
|
||||
class _isEmptyValidate extends ValidateMixin(LitElement) {
|
||||
_isEmpty() {
|
||||
// @ts-expect-error
|
||||
return this.modelValue.model === '';
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
const customRequiredTagString = defineCE(_isEmptyValidate);
|
||||
const customRequiredTag = unsafeStatic(customRequiredTagString);
|
||||
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {_isEmptyValidate} */ (await fixture(html`
|
||||
<${customRequiredTag}
|
||||
.validators=${[new Required()]}
|
||||
.modelValue=${{ model: 'foo' }}
|
||||
>${lightDom}</${customRequiredTag}>
|
||||
`);
|
||||
`));
|
||||
|
||||
const providedIsEmptySpy = sinon.spy(el, '_isEmpty');
|
||||
el.modelValue = { model: '' };
|
||||
|
|
@ -688,32 +703,34 @@ export function runValidateMixinSuite(customConfig) {
|
|||
it('prevents other Validators from being called when input is empty', async () => {
|
||||
const alwaysInvalid = new AlwaysInvalid();
|
||||
const alwaysInvalidSpy = sinon.spy(alwaysInvalid, 'execute');
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||
<${tag}
|
||||
.validators=${[new Required(), alwaysInvalid]}
|
||||
.modelValue=${''}
|
||||
>${lightDom}</${tag}>
|
||||
`);
|
||||
`));
|
||||
expect(alwaysInvalidSpy.callCount).to.equal(0); // __isRequired returned false (invalid)
|
||||
el.modelValue = 'foo';
|
||||
expect(alwaysInvalidSpy.callCount).to.equal(1); // __isRequired returned true (valid)
|
||||
});
|
||||
|
||||
it('adds [aria-required="true"] to "._inputNode"', async () => {
|
||||
const el = await fixture(html`
|
||||
<${withInputTag}
|
||||
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||
<${tag}
|
||||
.validators=${[new Required()]}
|
||||
.modelValue=${''}
|
||||
>${lightDom}</${withInputTag}>
|
||||
`);
|
||||
expect(el._inputNode.getAttribute('aria-required')).to.equal('true');
|
||||
>${lightDom}</${tag}>
|
||||
`));
|
||||
console.log(el._inputNode);
|
||||
expect(el._inputNode?.getAttribute('aria-required')).to.equal('true');
|
||||
el.validators = [];
|
||||
expect(el._inputNode.getAttribute('aria-required')).to.be.null;
|
||||
expect(el._inputNode?.getAttribute('aria-required')).to.be.null;
|
||||
});
|
||||
});
|
||||
|
||||
describe('Default (preconfigured) Validators', () => {
|
||||
const preconfTagString = defineCE(
|
||||
// @ts-expect-error base constructor same return type
|
||||
class extends ValidateMixin(LitElement) {
|
||||
constructor() {
|
||||
super();
|
||||
|
|
@ -724,11 +741,11 @@ export function runValidateMixinSuite(customConfig) {
|
|||
const preconfTag = unsafeStatic(preconfTagString);
|
||||
|
||||
it('can be stored for custom inputs', async () => {
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||
<${preconfTag}
|
||||
.validators=${[new MinLength(3)]}
|
||||
.modelValue=${'12'}
|
||||
></${preconfTag}>`);
|
||||
></${preconfTag}>`));
|
||||
|
||||
expect(el.validationStates.error.AlwaysInvalid).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 () => {
|
||||
const altPreconfTagString = defineCE(
|
||||
// @ts-expect-error base constructor same return type
|
||||
class extends ValidateMixin(LitElement) {
|
||||
constructor() {
|
||||
super();
|
||||
|
|
@ -745,10 +763,10 @@ export function runValidateMixinSuite(customConfig) {
|
|||
);
|
||||
const altPreconfTag = unsafeStatic(altPreconfTagString);
|
||||
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||
<${altPreconfTag}
|
||||
.modelValue=${'12'}
|
||||
></${altPreconfTag}>`);
|
||||
></${altPreconfTag}>`));
|
||||
|
||||
expect(el.validationStates.error.MinLength).to.be.true;
|
||||
el.defaultValidators[0].param = 2;
|
||||
|
|
@ -756,10 +774,10 @@ export function runValidateMixinSuite(customConfig) {
|
|||
});
|
||||
|
||||
it('can be requested via "._allValidators" getter', async () => {
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||
<${preconfTag}
|
||||
.validators=${[new MinLength(3)]}
|
||||
></${preconfTag}>`);
|
||||
></${preconfTag}>`));
|
||||
|
||||
expect(el.validators.length).to.equal(1);
|
||||
expect(el.defaultValidators.length).to.equal(1);
|
||||
|
|
@ -776,11 +794,11 @@ export function runValidateMixinSuite(customConfig) {
|
|||
|
||||
describe('State storage and reflection', () => {
|
||||
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}
|
||||
.modelValue=${'a'}
|
||||
.validators=${[new MinLength(3), new AlwaysInvalid()]}
|
||||
>${lightDom}</${tag}>`);
|
||||
>${lightDom}</${tag}>`));
|
||||
|
||||
expect(el.validationStates.error.MinLength).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 () => {
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||
<${tag}
|
||||
.validators=${[new MinLength(3)]}
|
||||
>${lightDom}</${tag}>
|
||||
`);
|
||||
`));
|
||||
el.modelValue = 'a';
|
||||
expect(el.hasFeedbackFor).to.deep.equal(['error']);
|
||||
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 () => {
|
||||
const validators = [new Required()];
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||
<${tag}
|
||||
.validators=${validators}
|
||||
>${lightDom}</${tag}>
|
||||
`);
|
||||
`));
|
||||
expect(el.hasFeedbackFor).to.deep.equal(['error']);
|
||||
expect(el.validationStates.error).to.eql({ Required: true });
|
||||
|
||||
|
|
@ -827,13 +845,13 @@ export function runValidateMixinSuite(customConfig) {
|
|||
describe('Events', () => {
|
||||
it('fires "showsFeedbackForChanged" event async after feedbackData got synced to feedbackElement', async () => {
|
||||
const spy = sinon.spy();
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||
<${tag}
|
||||
.submitted=${true}
|
||||
.validators=${[new MinLength(7)]}
|
||||
@showsFeedbackForChanged=${spy};
|
||||
>${lightDom}</${tag}>
|
||||
`);
|
||||
`));
|
||||
el.modelValue = 'a';
|
||||
await el.updateComplete;
|
||||
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 () => {
|
||||
const spy = sinon.spy();
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||
<${tag}
|
||||
.submitted=${true}
|
||||
.validators=${[new MinLength(7)]}
|
||||
@showsFeedbackForErrorChanged=${spy};
|
||||
>${lightDom}</${tag}>
|
||||
`);
|
||||
`));
|
||||
el.modelValue = 'a';
|
||||
await el.updateComplete;
|
||||
expect(spy).to.have.callCount(1);
|
||||
|
|
@ -873,19 +891,25 @@ export function runValidateMixinSuite(customConfig) {
|
|||
|
||||
describe('Accessibility', () => {
|
||||
it.skip('calls "._inputNode.setCustomValidity(errorMessage)"', async () => {
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||
<${tag}
|
||||
.modelValue=${'123'}
|
||||
.validators=${[new MinLength(3, { message: 'foo' })]}>
|
||||
<input slot="input">
|
||||
</${tag}>`);
|
||||
const spy = sinon.spy(el.inputElement, 'setCustomValidity');
|
||||
</${tag}>`));
|
||||
|
||||
if (el._inputNode) {
|
||||
// @ts-expect-error
|
||||
const spy = sinon.spy(el._inputNode, 'setCustomValidity');
|
||||
el.modelValue = '';
|
||||
expect(spy.callCount).to.be(1);
|
||||
expect(spy.callCount).to.equal(1);
|
||||
// @ts-expect-error needs to be rewritten to new API
|
||||
expect(el.validationMessage).to.be('foo');
|
||||
el.modelValue = '123';
|
||||
expect(spy.callCount).to.be(2);
|
||||
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
|
||||
|
|
@ -895,6 +919,7 @@ export function runValidateMixinSuite(customConfig) {
|
|||
|
||||
describe('Extensibility: Custom Validator types', () => {
|
||||
const customTypeTagString = defineCE(
|
||||
// @ts-expect-error base constructor same return type
|
||||
class extends ValidateMixin(LitElement) {
|
||||
static get validationTypes() {
|
||||
return [...super.validationTypes, 'x', 'y'];
|
||||
|
|
@ -904,7 +929,7 @@ export function runValidateMixinSuite(customConfig) {
|
|||
const customTypeTag = unsafeStatic(customTypeTagString);
|
||||
|
||||
it('supports additional validationTypes in .hasFeedbackFor', async () => {
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||
<${customTypeTag}
|
||||
.validators=${[
|
||||
new MinLength(2, { type: 'x' }),
|
||||
|
|
@ -913,7 +938,7 @@ export function runValidateMixinSuite(customConfig) {
|
|||
]}
|
||||
.modelValue=${'1234'}
|
||||
>${lightDom}</${customTypeTag}>
|
||||
`);
|
||||
`));
|
||||
expect(el.hasFeedbackFor).to.deep.equal([]);
|
||||
|
||||
el.modelValue = '123'; // triggers y
|
||||
|
|
@ -927,7 +952,7 @@ export function runValidateMixinSuite(customConfig) {
|
|||
});
|
||||
|
||||
it('supports additional validationTypes in .validationStates', async () => {
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||
<${customTypeTag}
|
||||
.validators=${[
|
||||
new MinLength(2, { type: 'x' }),
|
||||
|
|
@ -936,7 +961,7 @@ export function runValidateMixinSuite(customConfig) {
|
|||
]}
|
||||
.modelValue=${'1234'}
|
||||
>${lightDom}</${customTypeTag}>
|
||||
`);
|
||||
`));
|
||||
expect(el.validationStates).to.eql({
|
||||
x: {},
|
||||
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 () => {
|
||||
// we set submitted to always show error message in the test
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||
<${customTypeTag}
|
||||
.submitted=${true}
|
||||
._visibleMessagesAmount=${Infinity}
|
||||
|
|
@ -1002,16 +1003,17 @@ export function runValidateMixinSuite(customConfig) {
|
|||
]}
|
||||
.modelValue=${'1'}
|
||||
>${lightDom}</${customTypeTag}>
|
||||
`);
|
||||
`));
|
||||
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']);
|
||||
|
||||
el.modelValue = '12';
|
||||
await el.updateComplete;
|
||||
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']);
|
||||
});
|
||||
|
||||
|
|
@ -1025,8 +1027,8 @@ export function runValidateMixinSuite(customConfig) {
|
|||
describe('Subclassers', () => {
|
||||
describe('Adding new Validator types', () => {
|
||||
it('can add helpers for validation types', async () => {
|
||||
const elTagString = defineCE(
|
||||
class extends ValidateMixin(LitElement) {
|
||||
// @ts-expect-error base constructor same return type
|
||||
class ValidateHasX extends ValidateMixin(LitElement) {
|
||||
static get validationTypes() {
|
||||
return [...super.validationTypes, 'x'];
|
||||
}
|
||||
|
|
@ -1038,18 +1040,18 @@ export function runValidateMixinSuite(customConfig) {
|
|||
get hasXVisible() {
|
||||
return this.showsFeedbackFor.includes('x');
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
const elTagString = defineCE(ValidateHasX);
|
||||
const elTag = unsafeStatic(elTagString);
|
||||
|
||||
// we set submitted to always show errors
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {ValidateHasX} */ (await fixture(html`
|
||||
<${elTag}
|
||||
.submitted=${true}
|
||||
.validators=${[new MinLength(2, { type: 'x' })]}
|
||||
.modelValue=${'1'}
|
||||
>${lightDom}</${elTag}>
|
||||
`);
|
||||
`));
|
||||
await el.feedbackComplete;
|
||||
expect(el.hasX).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 () => {
|
||||
/**
|
||||
*
|
||||
* @param {string[]} array1
|
||||
* @param {string[]} array2
|
||||
*/
|
||||
function arrayDiff(array1 = [], array2 = []) {
|
||||
return array1
|
||||
.filter(x => !array2.includes(x))
|
||||
.concat(array2.filter(x => !array1.includes(x)));
|
||||
}
|
||||
const elTagString = defineCE(
|
||||
// @ts-expect-error base constructor same return type
|
||||
class extends ValidateMixin(LitElement) {
|
||||
static get validationTypes() {
|
||||
return [...super.validationTypes, 'x'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {?} oldValue
|
||||
*/
|
||||
updateSync(name, oldValue) {
|
||||
super.updateSync(name, oldValue);
|
||||
if (name === 'hasFeedbackFor') {
|
||||
|
|
@ -1088,14 +1100,14 @@ export function runValidateMixinSuite(customConfig) {
|
|||
|
||||
const spy = sinon.spy();
|
||||
// we set prefilled to always show errors
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||
<${elTag}
|
||||
.prefilled=${true}
|
||||
@hasFeedbackForXChanged=${spy}
|
||||
.validators=${[new MinLength(2, { type: 'x' })]}
|
||||
.modelValue=${'1'}
|
||||
>${lightDom}</${elTag}>
|
||||
`);
|
||||
`));
|
||||
expect(spy).to.have.callCount(1);
|
||||
el.modelValue = '1';
|
||||
expect(spy).to.have.callCount(1);
|
||||
|
|
@ -1112,6 +1124,7 @@ export function runValidateMixinSuite(customConfig) {
|
|||
// TODO: add this test on FormControl layer
|
||||
it('reconsiders feedback visibility when interaction states changed', async () => {
|
||||
const elTagString = defineCE(
|
||||
// @ts-expect-error base constructor same return type
|
||||
class extends ValidateMixin(LitElement) {
|
||||
static get properties() {
|
||||
return {
|
||||
|
|
@ -1129,12 +1142,12 @@ export function runValidateMixinSuite(customConfig) {
|
|||
},
|
||||
);
|
||||
const elTag = unsafeStatic(elTagString);
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||
<${elTag}
|
||||
.validators=${[new AlwaysInvalid()]}
|
||||
.modelValue=${'myValue'}
|
||||
>${lightDom}</${elTag}>
|
||||
`);
|
||||
`));
|
||||
|
||||
const spy = sinon.spy(el, '_updateFeedbackComponent');
|
||||
let counter = 0;
|
||||
|
|
@ -1153,43 +1166,6 @@ export function runValidateMixinSuite(customConfig) {
|
|||
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', () => {
|
||||
|
|
|
|||
|
|
@ -8,84 +8,67 @@ import { AlwaysInvalid } from '../test-helpers.js';
|
|||
|
||||
export function runValidateMixinFeedbackPart() {
|
||||
describe('Validity Feedback', () => {
|
||||
let tagString;
|
||||
let tag;
|
||||
let ContainsLowercaseA;
|
||||
const lightDom = '';
|
||||
|
||||
beforeEach(() => {
|
||||
localizeTearDown();
|
||||
});
|
||||
|
||||
before(() => {
|
||||
tagString = defineCE(
|
||||
class extends ValidateMixin(LitElement) {
|
||||
static get properties() {
|
||||
return {
|
||||
modelValue: { type: String },
|
||||
submitted: { type: Boolean },
|
||||
};
|
||||
}
|
||||
|
||||
// @ts-expect-error base constructor same return type
|
||||
class ValidateElement extends ValidateMixin(LitElement) {
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.appendChild(document.createElement('input'));
|
||||
const inputNode = document.createElement('input');
|
||||
inputNode.slot = 'input';
|
||||
this.appendChild(inputNode);
|
||||
}
|
||||
}
|
||||
|
||||
get _inputNode() {
|
||||
return this.querySelector('input');
|
||||
}
|
||||
},
|
||||
);
|
||||
tag = unsafeStatic(tagString);
|
||||
const tagString = defineCE(ValidateElement);
|
||||
const tag = unsafeStatic(tagString);
|
||||
|
||||
ContainsLowercaseA = class extends Validator {
|
||||
class ContainsLowercaseA extends Validator {
|
||||
static get validatorName() {
|
||||
return 'ContainsLowercaseA';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {?} modelValue
|
||||
*/
|
||||
execute(modelValue) {
|
||||
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 = () => 'Message for AlwaysInvalid';
|
||||
MinLength.getMessage = () =>
|
||||
AlwaysInvalid.getMessage = async () => 'Message for AlwaysInvalid';
|
||||
MinLength.getMessage = async () =>
|
||||
localize.locale === 'de-DE' ? 'Nachricht für MinLength' : 'Message for MinLength';
|
||||
ContainsLowercaseA.getMessage = () => 'Message for ContainsLowercaseA';
|
||||
ContainsCat.getMessage = () => 'Message for ContainsCat';
|
||||
});
|
||||
ContainsLowercaseA.getMessage = async () => 'Message for ContainsLowercaseA';
|
||||
ContainsCat.getMessage = async () => 'Message for ContainsCat';
|
||||
|
||||
const lightDom = '';
|
||||
|
||||
afterEach(() => {
|
||||
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 () => {
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||
<${tag} submitted .validators=${[new MinLength(3)]}>${lightDom}</${tag}>
|
||||
`);
|
||||
`));
|
||||
|
||||
el.modelValue = 'a';
|
||||
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 () => {
|
||||
const elTagString = defineCE(
|
||||
class extends ValidateMixin(LitElement) {
|
||||
// @ts-expect-error base constructors same return type
|
||||
class ValidateElementCustomTypes extends ValidateMixin(LitElement) {
|
||||
static get validationTypes() {
|
||||
return [...super.validationTypes, 'x'];
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
const elTagString = defineCE(ValidateElementCustomTypes);
|
||||
const elTag = unsafeStatic(elTagString);
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {ValidateElementCustomTypes} */ (await fixture(html`
|
||||
<${elTag}
|
||||
.submitted=${true}
|
||||
.validators=${[
|
||||
new MinLength(2, { type: 'x' }),
|
||||
new MinLength(3, { type: 'error' }),
|
||||
]}>${lightDom}</${elTag}>
|
||||
`);
|
||||
`));
|
||||
|
||||
el.modelValue = '1';
|
||||
await el.updateComplete;
|
||||
|
|
@ -134,30 +117,30 @@ export function runValidateMixinFeedbackPart() {
|
|||
});
|
||||
|
||||
it('passes a message to the "._feedbackNode"', async () => {
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||
<${tag}
|
||||
.submitted=${true}
|
||||
.modelValue=${'cat'}
|
||||
>${lightDom}</${tag}>
|
||||
`);
|
||||
`));
|
||||
expect(el._feedbackNode.feedbackData).to.deep.equal([]);
|
||||
el.validators = [new AlwaysInvalid()];
|
||||
await el.updateComplete;
|
||||
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 () => {
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||
<${tag}
|
||||
.submitted=${true}
|
||||
.modelValue=${'cat'}
|
||||
.validators=${[new AlwaysInvalid()]}
|
||||
>${lightDom}</${tag}>
|
||||
`);
|
||||
`));
|
||||
await el.updateComplete;
|
||||
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
|
||||
await el.validate();
|
||||
await el.updateComplete;
|
||||
|
|
@ -166,20 +149,21 @@ export function runValidateMixinFeedbackPart() {
|
|||
});
|
||||
|
||||
it('writes prioritized result to "._feedbackNode" based on Validator order', async () => {
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||
<${tag}
|
||||
.submitted=${true}
|
||||
.modelValue=${'cat'}
|
||||
.validators=${[new AlwaysInvalid(), new MinLength(4)]}
|
||||
>${lightDom}</${tag}>
|
||||
`);
|
||||
`));
|
||||
await el.updateComplete;
|
||||
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 () => {
|
||||
let unlockMessage;
|
||||
/** @type {function} FIXME: find better way to type this kind of pattern */
|
||||
let unlockMessage = () => {};
|
||||
const messagePromise = new Promise(resolve => {
|
||||
unlockMessage = resolve;
|
||||
});
|
||||
|
|
@ -189,23 +173,26 @@ export function runValidateMixinFeedbackPart() {
|
|||
return 'this ends up in "._feedbackNode"';
|
||||
};
|
||||
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||
<${tag}
|
||||
.submitted=${true}
|
||||
.modelValue=${'cat'}
|
||||
.validators=${[new AlwaysInvalid()]}
|
||||
>${lightDom}</${tag}>
|
||||
`);
|
||||
`));
|
||||
expect(el._feedbackNode.feedbackData).to.be.undefined;
|
||||
unlockMessage();
|
||||
await el.updateComplete;
|
||||
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...
|
||||
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 => {
|
||||
unlockMessage = resolve;
|
||||
});
|
||||
|
|
@ -215,47 +202,63 @@ export function runValidateMixinFeedbackPart() {
|
|||
return 'this ends up in "._feedbackNode"';
|
||||
};
|
||||
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||
<${tag}
|
||||
.submitted=${true}
|
||||
.modelValue=${'cat'}
|
||||
.validators=${[new AlwaysInvalid()]}
|
||||
>${lightDom}</${tag}>
|
||||
`);
|
||||
`));
|
||||
|
||||
expect(el._feedbackNode.feedbackData).to.be.undefined;
|
||||
unlockMessage();
|
||||
await el.updateComplete;
|
||||
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 () => {
|
||||
const customFeedbackTagString = defineCE(
|
||||
class extends LitElement {
|
||||
class ValidateElementCustomRender extends LitElement {
|
||||
static get properties() {
|
||||
return {
|
||||
feedbackData: Array,
|
||||
feedbackData: { attribute: false },
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
/**
|
||||
* @typedef {Object} messageMap
|
||||
* @property {string | Node} message
|
||||
* @property {string} type
|
||||
* @property {Validator} [validator]
|
||||
*/
|
||||
|
||||
/** @type {messageMap[]} */
|
||||
this.feedbackData = [];
|
||||
}
|
||||
|
||||
render() {
|
||||
const name =
|
||||
this.feedbackData && this.feedbackData[0]
|
||||
? this.feedbackData[0].validator.constructor.validatorName
|
||||
: '';
|
||||
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 el = await fixture(html`
|
||||
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||
<${tag}
|
||||
.submitted=${true}
|
||||
.validators=${[new ContainsLowercaseA(), new AlwaysInvalid()]}>
|
||||
<${customFeedbackTag} slot="feedback"><${customFeedbackTag}>
|
||||
</${tag}>
|
||||
`);
|
||||
`));
|
||||
|
||||
expect(el._feedbackNode.localName).to.equal(customFeedbackTagString);
|
||||
|
||||
|
|
@ -273,66 +276,46 @@ export function runValidateMixinFeedbackPart() {
|
|||
});
|
||||
|
||||
it('supports custom messages in Validator instance configuration object', async () => {
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||
<${tag}
|
||||
.submitted=${true}
|
||||
.validators=${[new MinLength(3, { getMessage: () => 'custom via config' })]}
|
||||
>${lightDom}</${tag}>
|
||||
`);
|
||||
`));
|
||||
|
||||
el.modelValue = 'a';
|
||||
await el.updateComplete;
|
||||
await el.feedbackComplete;
|
||||
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);
|
||||
expect(el._feedbackNode.feedbackData?.[0].message).to.equal('custom via config');
|
||||
});
|
||||
|
||||
it('updates the feedback component when locale changes', async () => {
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||
<${tag}
|
||||
.submitted=${true}
|
||||
.validators=${[new MinLength(3)]}
|
||||
.modelValue=${'1'}
|
||||
>${lightDom}</${tag}>
|
||||
`);
|
||||
`));
|
||||
await el.feedbackComplete;
|
||||
expect(el._feedbackNode.feedbackData.length).to.equal(1);
|
||||
expect(el._feedbackNode.feedbackData[0].message).to.equal('Message for MinLength');
|
||||
expect(el._feedbackNode.feedbackData?.length).to.equal(1);
|
||||
expect(el._feedbackNode.feedbackData?.[0].message).to.equal('Message for MinLength');
|
||||
|
||||
localize.locale = 'de-DE';
|
||||
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 () => {
|
||||
const elTagString = defineCE(
|
||||
class extends ValidateMixin(LitElement) {
|
||||
// @ts-expect-error base constructor same return type
|
||||
class ValidateElementCustomTypes extends ValidateMixin(LitElement) {
|
||||
static get validationTypes() {
|
||||
return ['error', 'success'];
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
const elTagString = defineCE(ValidateElementCustomTypes);
|
||||
const elTag = unsafeStatic(elTagString);
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {ValidateElementCustomTypes} */ (await fixture(html`
|
||||
<${elTag}
|
||||
.submitted=${true}
|
||||
.validators=${[
|
||||
|
|
@ -340,28 +323,28 @@ export function runValidateMixinFeedbackPart() {
|
|||
new DefaultSuccess(null, { getMessage: () => 'This is a success message' }),
|
||||
]}
|
||||
>${lightDom}</${elTag}>
|
||||
`);
|
||||
`));
|
||||
|
||||
el.modelValue = 'a';
|
||||
await el.updateComplete;
|
||||
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';
|
||||
await el.updateComplete;
|
||||
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', () => {
|
||||
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}
|
||||
submitted
|
||||
.validators=${[new Required()]}
|
||||
.modelValue=${'a'}
|
||||
>${lightDom}</${tag}>
|
||||
`);
|
||||
`));
|
||||
const inputNode = el._inputNode;
|
||||
expect(inputNode.getAttribute('aria-invalid')).to.equal('false');
|
||||
|
||||
|
|
@ -379,25 +362,27 @@ export function runValidateMixinFeedbackPart() {
|
|||
|
||||
describe('Meta data', () => {
|
||||
it('".getMessage()" gets a reference to formControl, params, modelValue and type', async () => {
|
||||
const elTagString = defineCE(
|
||||
class extends ValidateMixin(LitElement) {
|
||||
// @ts-expect-error base constructor same return type
|
||||
class ValidateElementCustomTypes extends ValidateMixin(LitElement) {
|
||||
static get validationTypes() {
|
||||
return ['error', 'x'];
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const elTagString = defineCE(ValidateElementCustomTypes);
|
||||
const elTag = unsafeStatic(elTagString);
|
||||
let el;
|
||||
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}
|
||||
.submitted=${true}
|
||||
.validators=${[constructorValidator]}
|
||||
.modelValue=${'cat'}
|
||||
>${lightDom}</${elTag}>
|
||||
`);
|
||||
`));
|
||||
await el.updateComplete;
|
||||
await el.feedbackComplete;
|
||||
expect(constructorMessageSpy.args[0][0]).to.eql({
|
||||
|
|
@ -413,13 +398,13 @@ export function runValidateMixinFeedbackPart() {
|
|||
const instanceMessageSpy = sinon.spy();
|
||||
const instanceValidator = new MinLength(4, { getMessage: instanceMessageSpy });
|
||||
|
||||
el = await fixture(html`
|
||||
el = /** @type {ValidateElementCustomTypes} */ (await fixture(html`
|
||||
<${elTag}
|
||||
.submitted=${true}
|
||||
.validators=${[instanceValidator]}
|
||||
.modelValue=${'cat'}
|
||||
>${lightDom}</${elTag}>
|
||||
`);
|
||||
`));
|
||||
await el.updateComplete;
|
||||
await el.feedbackComplete;
|
||||
expect(instanceMessageSpy.args[0][0]).to.eql({
|
||||
|
|
@ -437,16 +422,17 @@ export function runValidateMixinFeedbackPart() {
|
|||
|
||||
it('".getMessage()" gets .fieldName defined on instance', async () => {
|
||||
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}
|
||||
.submitted=${true}
|
||||
.validators=${[constructorValidator]}
|
||||
.modelValue=${'cat'}
|
||||
.fieldName=${new Promise(resolve => resolve('myField'))}
|
||||
>${lightDom}</${tag}>
|
||||
`);
|
||||
`));
|
||||
await el.updateComplete;
|
||||
await el.feedbackComplete;
|
||||
expect(spy.args[0][0]).to.eql({
|
||||
|
|
@ -465,22 +451,23 @@ export function runValidateMixinFeedbackPart() {
|
|||
const constructorValidator = new MinLength(4, {
|
||||
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}
|
||||
.submitted=${true}
|
||||
.validators=${[constructorValidator]}
|
||||
.modelValue=${'cat'}
|
||||
.fieldName=${new Promise(resolve => resolve('myField'))}
|
||||
>${lightDom}</${tag}>
|
||||
`);
|
||||
`));
|
||||
await el.updateComplete;
|
||||
await el.feedbackComplete;
|
||||
|
||||
// ignore fieldName Promise as it will always be unique
|
||||
const compare = spy.args[0][0];
|
||||
delete compare.config.fieldName;
|
||||
delete compare?.config.fieldName;
|
||||
expect(compare).to.eql({
|
||||
config: {},
|
||||
params: 4,
|
||||
|
|
@ -503,13 +490,13 @@ export function runValidateMixinFeedbackPart() {
|
|||
* The Queue system solves this by queueing the updateFeedbackComponent tasks and
|
||||
* await them one by one.
|
||||
*/
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||
<${tag}
|
||||
.submitted=${true}
|
||||
.validators=${[new MinLength(3)]}
|
||||
.modelValue=${'1'}
|
||||
>${lightDom}</${tag}>
|
||||
`);
|
||||
`));
|
||||
|
||||
el.modelValue = '12345';
|
||||
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';
|
||||
|
||||
describe('FocusMixin', () => {
|
||||
// @ts-expect-error base constructors same return type
|
||||
class Focusable extends FocusMixin(LitElement) {
|
||||
render() {
|
||||
return html`<slot name="input"></slot>`;
|
||||
}
|
||||
|
||||
get _inputNode() {
|
||||
return this.querySelector('input');
|
||||
}
|
||||
}
|
||||
|
||||
const tagString = defineCE(Focusable);
|
||||
|
|
|
|||
|
|
@ -1,20 +1,14 @@
|
|||
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 { FormControlMixin } from '../src/FormControlMixin.js';
|
||||
import { FormRegistrarMixin } from '../src/registration/FormRegistrarMixin.js';
|
||||
|
||||
describe('FormControlMixin', () => {
|
||||
const inputSlot = '<input slot="input" />';
|
||||
class FormControlMixinClass extends FormControlMixin(SlotMixin(LitElement)) {
|
||||
static get properties() {
|
||||
return {
|
||||
modelValue: {
|
||||
type: String,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-expect-error base constructor same return type
|
||||
class FormControlMixinClass extends FormControlMixin(LitElement) {}
|
||||
|
||||
const tagString = defineCE(FormControlMixinClass);
|
||||
const tag = unsafeStatic(tagString);
|
||||
|
|
@ -207,17 +201,10 @@ describe('FormControlMixin', () => {
|
|||
});
|
||||
|
||||
describe('Model-value-changed event propagation', () => {
|
||||
// @ts-expect-error base constructor same return type
|
||||
const FormControlWithRegistrarMixinClass = class extends FormControlMixin(
|
||||
FormRegistrarMixin(SlotMixin(LitElement)),
|
||||
) {
|
||||
static get properties() {
|
||||
return {
|
||||
modelValue: {
|
||||
type: String,
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
FormRegistrarMixin(LitElement),
|
||||
) {};
|
||||
|
||||
const groupElem = defineCE(FormControlWithRegistrarMixinClass);
|
||||
const groupTag = unsafeStatic(groupElem);
|
||||
|
|
|
|||
|
|
@ -1,19 +1,30 @@
|
|||
import { html, LitElement } from '@lion/core';
|
||||
import '@lion/fieldset/lion-fieldset.js';
|
||||
// @ts-expect-error
|
||||
import { LionInput } from '@lion/input';
|
||||
import '@lion/fieldset/lion-fieldset.js';
|
||||
import { FormGroupMixin, Required } from '@lion/form-core';
|
||||
import { expect, fixture } from '@open-wc/testing';
|
||||
import { ChoiceGroupMixin } from '../../src/choice-group/ChoiceGroupMixin.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', () => {
|
||||
before(() => {
|
||||
class ChoiceInput extends ChoiceInputMixin(LionInput) {}
|
||||
// @ts-expect-error base constructors same return type
|
||||
customElements.define('choice-group-input', ChoiceInput);
|
||||
|
||||
// @ts-expect-error
|
||||
class ChoiceGroup extends ChoiceGroupMixin(FormGroupMixin(LitElement)) {}
|
||||
customElements.define('choice-group', ChoiceGroup);
|
||||
|
||||
// @ts-expect-error
|
||||
class ChoiceGroupMultiple extends ChoiceGroupMixin(FormGroupMixin(LitElement)) {
|
||||
constructor() {
|
||||
super();
|
||||
|
|
@ -21,16 +32,15 @@ describe('ChoiceGroupMixin', () => {
|
|||
}
|
||||
}
|
||||
customElements.define('choice-group-multiple', ChoiceGroupMultiple);
|
||||
});
|
||||
|
||||
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-input .choiceValue=${'male'}></choice-group-input>
|
||||
<choice-group-input .choiceValue=${'female'} checked></choice-group-input>
|
||||
<choice-group-input .choiceValue=${'other'}></choice-group-input>
|
||||
</choice-group>
|
||||
`);
|
||||
`));
|
||||
expect(el.modelValue).to.equal('female');
|
||||
el.formElements[0].checked = true;
|
||||
expect(el.modelValue).to.equal('male');
|
||||
|
|
@ -39,13 +49,13 @@ describe('ChoiceGroupMixin', () => {
|
|||
});
|
||||
|
||||
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-input .choiceValue=${'male'}></choice-group-input>
|
||||
<choice-group-input .choiceValue=${'female'} checked></choice-group-input>
|
||||
<choice-group-input .choiceValue=${'other'}></choice-group-input>
|
||||
</choice-group>
|
||||
`);
|
||||
`));
|
||||
expect(el.formattedValue).to.equal('female');
|
||||
el.formElements[0].checked = true;
|
||||
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 () => {
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {ChoiceGroup} */ (await fixture(html`
|
||||
<choice-group name="gender">
|
||||
<choice-group-input .choiceValue=${'male'}></choice-group-input>
|
||||
<choice-group-input .choiceValue=${'female'} checked></choice-group-input>
|
||||
<choice-group-input .choiceValue=${'other'}></choice-group-input>
|
||||
</choice-group>
|
||||
`);
|
||||
const invalidChild = await fixture(html`
|
||||
`));
|
||||
const invalidChild = /** @type {ChoiceGroup} */ (await fixture(html`
|
||||
<choice-group-input .modelValue=${'Lara'}></choice-group-input>
|
||||
`);
|
||||
`));
|
||||
|
||||
expect(() => {
|
||||
el.addFormElement(invalidChild);
|
||||
|
|
@ -73,35 +83,35 @@ describe('ChoiceGroupMixin', () => {
|
|||
});
|
||||
|
||||
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-input .choiceValue=${'female'} checked></choice-group-input>
|
||||
<choice-group-input .choiceValue=${'other'}></choice-group-input>
|
||||
</choice-group>
|
||||
`);
|
||||
`));
|
||||
|
||||
expect(el.formElements[0].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>
|
||||
`);
|
||||
`));
|
||||
el.appendChild(validChild);
|
||||
|
||||
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 () => {
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {ChoiceGroup} */ (await fixture(html`
|
||||
<choice-group name="gender">
|
||||
<choice-group-input .choiceValue=${'female'} checked></choice-group-input>
|
||||
<choice-group-input .choiceValue=${'other'}></choice-group-input>
|
||||
</choice-group>
|
||||
`);
|
||||
`));
|
||||
|
||||
const invalidChild = await fixture(html`
|
||||
const invalidChild = /** @type {ChoiceGroup} */ (await fixture(html`
|
||||
<choice-group-input name="foo" .choiceValue=${'male'}></choice-group-input>
|
||||
`);
|
||||
`));
|
||||
|
||||
expect(() => {
|
||||
el.addFormElement(invalidChild);
|
||||
|
|
@ -111,39 +121,39 @@ describe('ChoiceGroupMixin', () => {
|
|||
});
|
||||
|
||||
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-input .choiceValue=${'male'}></choice-group-input>
|
||||
<choice-group-input .choiceValue=${'female'}></choice-group-input>
|
||||
<choice-group-input .choiceValue=${'other'}></choice-group-input>
|
||||
</choice-group>
|
||||
`);
|
||||
`));
|
||||
|
||||
expect(el.modelValue).to.equal('other');
|
||||
expect(el.formElements[2].checked).to.be.true;
|
||||
});
|
||||
|
||||
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-input .choiceValue=${'male'}></choice-group-input>
|
||||
<choice-group-input .choiceValue=${'female'}></choice-group-input>
|
||||
<choice-group-input .choiceValue=${'other'}></choice-group-input>
|
||||
</choice-group>
|
||||
`);
|
||||
`));
|
||||
|
||||
expect(el.serializedValue).to.equal('other');
|
||||
expect(el.formElements[2].checked).to.be.true;
|
||||
});
|
||||
|
||||
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-input .choiceValue=${'male'}></choice-group-input>
|
||||
<choice-group-input .choiceValue=${'female'}></choice-group-input>
|
||||
<choice-group-input .choiceValue=${'other'}></choice-group-input>
|
||||
</choice-group>
|
||||
`);
|
||||
`));
|
||||
|
||||
expect(el.formattedValue).to.equal('other');
|
||||
expect(el.formElements[2].checked).to.be.true;
|
||||
|
|
@ -152,12 +162,12 @@ describe('ChoiceGroupMixin', () => {
|
|||
it('can handle complex data via choiceValue', async () => {
|
||||
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-input .choiceValue=${{ some: 'data' }}></choice-group-input>
|
||||
<choice-group-input .choiceValue=${date} checked></choice-group-input>
|
||||
</choice-group>
|
||||
`);
|
||||
`));
|
||||
|
||||
expect(el.modelValue).to.equal(date);
|
||||
el.formElements[0].checked = true;
|
||||
|
|
@ -165,12 +175,12 @@ describe('ChoiceGroupMixin', () => {
|
|||
});
|
||||
|
||||
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-input .choiceValue=${0} checked></choice-group-input>
|
||||
<choice-group-input .choiceValue=${''}></choice-group-input>
|
||||
</choice-group>
|
||||
`);
|
||||
`));
|
||||
|
||||
expect(el.modelValue).to.equal(0);
|
||||
el.formElements[1].checked = true;
|
||||
|
|
@ -178,7 +188,7 @@ describe('ChoiceGroupMixin', () => {
|
|||
});
|
||||
|
||||
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-input .modelValue="${{ value: 'male', checked: false }}"></choice-group-input>
|
||||
<choice-group-input
|
||||
|
|
@ -188,7 +198,7 @@ describe('ChoiceGroupMixin', () => {
|
|||
.modelValue="${{ value: 'other', checked: false }}"
|
||||
></choice-group-input>
|
||||
</choice-group>
|
||||
`);
|
||||
`));
|
||||
|
||||
expect(el.modelValue).to.equal('female');
|
||||
el.modelValue = 'other';
|
||||
|
|
@ -197,7 +207,7 @@ describe('ChoiceGroupMixin', () => {
|
|||
|
||||
it('expect child nodes to only fire one model-value-changed event per instance', async () => {
|
||||
let counter = 0;
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {ChoiceGroup} */ (await fixture(html`
|
||||
<choice-group
|
||||
name="gender"
|
||||
@model-value-changed=${() => {
|
||||
|
|
@ -208,7 +218,7 @@ describe('ChoiceGroupMixin', () => {
|
|||
<choice-group-input .modelValue=${{ value: 'female', checked: true }}></choice-group-input>
|
||||
<choice-group-input .choiceValue=${'other'}></choice-group-input>
|
||||
</choice-group>
|
||||
`);
|
||||
`));
|
||||
|
||||
counter = 0; // reset after setup which may result in different results
|
||||
|
||||
|
|
@ -231,60 +241,60 @@ describe('ChoiceGroupMixin', () => {
|
|||
});
|
||||
|
||||
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-input .choiceValue=${'male'}></choice-group-input>
|
||||
<choice-group-input
|
||||
.choiceValue=${{ subObject: 'satisfies required' }}
|
||||
></choice-group-input>
|
||||
</choice-group>
|
||||
`);
|
||||
`));
|
||||
expect(el.hasFeedbackFor).to.include('error');
|
||||
expect(el.validationStates).to.have.a.property('error');
|
||||
expect(el.validationStates.error).to.have.a.property('Required');
|
||||
expect(el.validationStates.error).to.exist;
|
||||
expect(el.validationStates.error.Required).to.exist;
|
||||
|
||||
el.formElements[0].checked = true;
|
||||
expect(el.hasFeedbackFor).not.to.include('error');
|
||||
expect(el.validationStates).to.have.a.property('error');
|
||||
expect(el.validationStates.error).not.to.have.a.property('Required');
|
||||
expect(el.validationStates.error).to.exist;
|
||||
expect(el.validationStates.error.Required).to.not.exist;
|
||||
|
||||
el.formElements[1].checked = true;
|
||||
expect(el.hasFeedbackFor).not.to.include('error');
|
||||
expect(el.validationStates).to.have.a.property('error');
|
||||
expect(el.validationStates.error).not.to.have.a.property('Required');
|
||||
expect(el.validationStates.error).to.exist;
|
||||
expect(el.validationStates.error.Required).to.not.exist;
|
||||
});
|
||||
|
||||
it('returns serialized value', async () => {
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {ChoiceGroup} */ (await fixture(html`
|
||||
<choice-group name="gender">
|
||||
<choice-group-input .choiceValue=${'male'}></choice-group-input>
|
||||
<choice-group-input .choiceValue=${'female'}></choice-group-input>
|
||||
</choice-group>
|
||||
`);
|
||||
`));
|
||||
el.formElements[0].checked = true;
|
||||
expect(el.serializedValue).to.deep.equal('male');
|
||||
});
|
||||
|
||||
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-input .choiceValue=${'male'}></choice-group-input>
|
||||
<choice-group-input .choiceValue=${'female'}></choice-group-input>
|
||||
</choice-group>
|
||||
`);
|
||||
`));
|
||||
|
||||
expect(el.serializedValue).to.deep.equal('');
|
||||
});
|
||||
|
||||
describe('multipleChoice', () => {
|
||||
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-input .choiceValue=${'male'}></choice-group-input>
|
||||
<choice-group-input .choiceValue=${'female'} checked></choice-group-input>
|
||||
<choice-group-input .choiceValue=${'other'}></choice-group-input>
|
||||
</choice-group-multiple>
|
||||
`);
|
||||
`));
|
||||
|
||||
expect(el.modelValue).to.eql(['female']);
|
||||
el.formElements[0].checked = true;
|
||||
|
|
@ -294,13 +304,13 @@ describe('ChoiceGroupMixin', () => {
|
|||
});
|
||||
|
||||
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-input .choiceValue=${'male'}></choice-group-input>
|
||||
<choice-group-input .choiceValue=${'female'} checked></choice-group-input>
|
||||
<choice-group-input .choiceValue=${'other'}></choice-group-input>
|
||||
</choice-group-multiple>
|
||||
`);
|
||||
`));
|
||||
|
||||
expect(el.serializedValue).to.eql(['female']);
|
||||
el.formElements[0].checked = true;
|
||||
|
|
@ -310,13 +320,13 @@ describe('ChoiceGroupMixin', () => {
|
|||
});
|
||||
|
||||
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-input .choiceValue=${'male'}></choice-group-input>
|
||||
<choice-group-input .choiceValue=${'female'} checked></choice-group-input>
|
||||
<choice-group-input .choiceValue=${'other'}></choice-group-input>
|
||||
</choice-group-multiple>
|
||||
`);
|
||||
`));
|
||||
|
||||
expect(el.formattedValue).to.eql(['female']);
|
||||
el.formElements[0].checked = true;
|
||||
|
|
@ -326,13 +336,13 @@ describe('ChoiceGroupMixin', () => {
|
|||
});
|
||||
|
||||
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-input .choiceValue=${'male'}></choice-group-input>
|
||||
<choice-group-input .choiceValue=${'female'}></choice-group-input>
|
||||
<choice-group-input .choiceValue=${'other'}></choice-group-input>
|
||||
</choice-group-multiple>
|
||||
`);
|
||||
`));
|
||||
|
||||
el.modelValue = ['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 () => {
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {ChoiceGroup} */ (await fixture(html`
|
||||
<choice-group-multiple name="gender[]">
|
||||
<choice-group-input .choiceValue=${'male'} checked></choice-group-input>
|
||||
<choice-group-input .choiceValue=${'female'}></choice-group-input>
|
||||
<choice-group-input .choiceValue=${'other'} checked></choice-group-input>
|
||||
</choice-group-multiple>
|
||||
`);
|
||||
`));
|
||||
|
||||
expect(el.modelValue).to.eql(['male', 'other']);
|
||||
expect(el.formElements[0].checked).to.be.true;
|
||||
|
|
@ -362,7 +372,7 @@ describe('ChoiceGroupMixin', () => {
|
|||
|
||||
describe('Integration with a parent form/fieldset', () => {
|
||||
it('will serialize all children with their serializedValue', async () => {
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {ChoiceGroup} */ (await fixture(html`
|
||||
<lion-fieldset>
|
||||
<choice-group name="gender">
|
||||
<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>
|
||||
</lion-fieldset>
|
||||
`);
|
||||
`));
|
||||
|
||||
expect(el.serializedValue).to.eql({
|
||||
gender: 'female',
|
||||
});
|
||||
|
||||
const choiceGroupEl = el.querySelector('[name="gender"]');
|
||||
const choiceGroupEl = /** @type {ChoiceGroup} */ (el.querySelector('[name="gender"]'));
|
||||
choiceGroupEl.multipleChoice = true;
|
||||
expect(el.serializedValue).to.eql({
|
||||
gender: ['female'],
|
||||
|
|
|
|||
|
|
@ -1,28 +1,46 @@
|
|||
import { html } from '@lion/core';
|
||||
// @ts-expect-error
|
||||
import { LionInput } from '@lion/input';
|
||||
import { Required } from '@lion/form-core';
|
||||
import { expect, fixture } from '@open-wc/testing';
|
||||
import sinon from 'sinon';
|
||||
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', () => {
|
||||
before(() => {
|
||||
class ChoiceInput extends ChoiceInputMixin(LionInput) {
|
||||
connectedCallback() {
|
||||
if (super.connectedCallback) super.connectedCallback();
|
||||
/** @typedef {Element & ChoiceClass} ChoiceInput */
|
||||
class ChoiceClass extends ChoiceInputMixin(LionInput) {
|
||||
constructor() {
|
||||
super();
|
||||
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 () => {
|
||||
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;
|
||||
});
|
||||
|
||||
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.modelValue).to.deep.equal({
|
||||
|
|
@ -34,7 +52,9 @@ describe('ChoiceInputMixin', () => {
|
|||
it('can handle complex data via choiceValue', async () => {
|
||||
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.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 () => {
|
||||
let counter = 0;
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {ChoiceInput} */ (await fixture(html`
|
||||
<choice-input
|
||||
@model-value-changed=${() => {
|
||||
counter += 1;
|
||||
}}
|
||||
.choiceValue=${'foo'}
|
||||
></choice-input>
|
||||
`);
|
||||
`));
|
||||
expect(counter).to.equal(1); // undefined to set value
|
||||
|
||||
el.checked = true;
|
||||
|
|
@ -67,7 +87,7 @@ describe('ChoiceInputMixin', () => {
|
|||
|
||||
it('fires one "user-input-changed" event after user interaction', async () => {
|
||||
let counter = 0;
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {ChoiceInput} */ (await fixture(html`
|
||||
<choice-input
|
||||
@user-input-changed="${() => {
|
||||
counter += 1;
|
||||
|
|
@ -75,7 +95,7 @@ describe('ChoiceInputMixin', () => {
|
|||
>
|
||||
<input slot="input" />
|
||||
</choice-input>
|
||||
`);
|
||||
`));
|
||||
expect(counter).to.equal(0);
|
||||
// Here we try to mimic user interaction by firing browser events
|
||||
const nativeInput = el._inputNode;
|
||||
|
|
@ -86,31 +106,31 @@ describe('ChoiceInputMixin', () => {
|
|||
});
|
||||
|
||||
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>
|
||||
`);
|
||||
`));
|
||||
expect(el.hasFeedbackFor).to.include('error');
|
||||
expect(el.validationStates).to.have.a.property('error');
|
||||
expect(el.validationStates.error).to.have.a.property('Required');
|
||||
expect(el.validationStates.error).to.exist;
|
||||
expect(el.validationStates.error.Required).to.exist;
|
||||
el.checked = true;
|
||||
expect(el.hasFeedbackFor).not.to.include('error');
|
||||
expect(el.validationStates).to.have.a.property('error');
|
||||
expect(el.validationStates.error).not.to.have.a.property('Required');
|
||||
expect(el.validationStates.error).to.exist;
|
||||
expect(el.validationStates.error.Required).not.to.exist;
|
||||
});
|
||||
|
||||
describe('Checked state synchronization', () => {
|
||||
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');
|
||||
|
||||
const precheckedElementAttr = await fixture(html`
|
||||
const precheckedElementAttr = /** @type {ChoiceInput} */ (await fixture(html`
|
||||
<choice-input .checked=${true}></choice-input>
|
||||
`);
|
||||
`));
|
||||
expect(precheckedElementAttr.checked).to.equal(true, 'initially checked via attribute');
|
||||
});
|
||||
|
||||
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;
|
||||
el.checked = true;
|
||||
expect(el.checked).to.be.true;
|
||||
|
|
@ -120,7 +140,7 @@ describe('ChoiceInputMixin', () => {
|
|||
});
|
||||
|
||||
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();
|
||||
expect(el.checked).to.be.true;
|
||||
el._inputNode.click();
|
||||
|
|
@ -128,7 +148,9 @@ describe('ChoiceInputMixin', () => {
|
|||
});
|
||||
|
||||
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.modelValue).to.deep.equal({
|
||||
checked: false,
|
||||
|
|
@ -145,7 +167,9 @@ describe('ChoiceInputMixin', () => {
|
|||
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
|
||||
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;
|
||||
|
||||
const spyModelCheckedToChecked = sinon.spy(el, '__syncModelCheckedToChecked');
|
||||
|
|
@ -168,13 +192,14 @@ describe('ChoiceInputMixin', () => {
|
|||
});
|
||||
|
||||
it('synchronizes checked state to [checked] attribute for styling purposes', async () => {
|
||||
/** @param {ChoiceInput} el */
|
||||
const hasAttr = el => el.hasAttribute('checked');
|
||||
const el = await fixture(`<choice-input></choice-input>`);
|
||||
const elChecked = await fixture(html`
|
||||
const el = /** @type {ChoiceInput} */ (await fixture(`<choice-input></choice-input>`));
|
||||
const elChecked = /** @type {ChoiceInput} */ (await fixture(html`
|
||||
<choice-input .checked=${true}>
|
||||
<input slot="input" />
|
||||
</choice-input>
|
||||
`);
|
||||
`));
|
||||
|
||||
// Initial values
|
||||
expect(hasAttr(el)).to.equal(false, 'initial unchecked element');
|
||||
|
|
@ -214,32 +239,38 @@ describe('ChoiceInputMixin', () => {
|
|||
|
||||
describe('Format/parse/serialize loop', () => {
|
||||
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 });
|
||||
|
||||
const elChecked = await fixture(html`
|
||||
const elChecked = /** @type {ChoiceInput} */ (await fixture(html`
|
||||
<choice-input .choiceValue=${'foo'} .checked=${true}></choice-input>
|
||||
`);
|
||||
`));
|
||||
expect(elChecked.modelValue).deep.equal({ value: 'foo', checked: true });
|
||||
});
|
||||
|
||||
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('');
|
||||
|
||||
const elementWithValue = await fixture(html`
|
||||
const elementWithValue = /** @type {ChoiceInput} */ (await fixture(html`
|
||||
<choice-input .choiceValue=${'foo'}></choice-input>
|
||||
`);
|
||||
`));
|
||||
expect(elementWithValue.formattedValue).to.equal('foo');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Interaction states', () => {
|
||||
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');
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { defineCE } from '@open-wc/testing';
|
||||
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';
|
||||
|
||||
const fieldTagString = defineCE(
|
||||
class extends customElements.get('lion-field') {
|
||||
class extends LionField {
|
||||
get slots() {
|
||||
return {
|
||||
...super.slots,
|
||||
|
|
@ -18,7 +18,6 @@ const fieldTagString = defineCE(
|
|||
describe('<lion-field> integrations', () => {
|
||||
runInteractionStateMixinSuite({
|
||||
tagString: fieldTagString,
|
||||
suffix: 'lion-field',
|
||||
});
|
||||
|
||||
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('../types/FormControlMixinTypes').FormControlHost} FormControlHost
|
||||
* @typedef {FormControlHost & HTMLElement & {__parentFormGroup?:HTMLElement, checked?:boolean}} FormControl
|
||||
*/
|
||||
|
||||
/** @typedef {HTMLElement & {shadowRoot: HTMLElement, assignedNodes: Function}} ShadowHTMLElement */
|
||||
|
||||
const tagString = 'lion-field';
|
||||
const tag = unsafeStatic(tagString);
|
||||
const inputSlotString = '<input slot="input" />';
|
||||
const inputSlot = unsafeHTML(inputSlotString);
|
||||
|
||||
/**
|
||||
* @param {import("../index.js").LionField} formControl
|
||||
* @param {string} newViewValue
|
||||
*/
|
||||
function mimicUserInput(formControl, newViewValue) {
|
||||
formControl.value = newViewValue; // eslint-disable-line no-param-reassign
|
||||
formControl._inputNode.dispatchEvent(new CustomEvent('input', { bubbles: true }));
|
||||
|
|
@ -32,10 +40,19 @@ beforeEach(() => {
|
|||
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>', () => {
|
||||
it(`puts a unique id "${tagString}-[hash]" on the native input`, async () => {
|
||||
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 () => {
|
||||
|
|
@ -148,15 +165,15 @@ describe('<lion-field>', () => {
|
|||
const el = /** @type {LionField} */ (await fixture(
|
||||
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 () => {
|
||||
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';
|
||||
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'
|
||||
|
|
@ -189,7 +206,7 @@ describe('<lion-field>', () => {
|
|||
|
||||
el.disabled = true;
|
||||
await el.updateComplete;
|
||||
await aTimeout();
|
||||
await aTimeout(0);
|
||||
|
||||
expect(el._inputNode.hasAttribute('disabled')).to.equal(true);
|
||||
const disabledel = /** @type {LionField} */ (await fixture(
|
||||
|
|
@ -220,7 +237,7 @@ describe('<lion-field>', () => {
|
|||
<span slot="feedback">No name entered</span>
|
||||
</${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-describedby')).to.contain(`help-text-${el._inputId}`);
|
||||
|
|
@ -238,7 +255,7 @@ describe('<lion-field>', () => {
|
|||
</${tag}>
|
||||
`));
|
||||
|
||||
const nativeInput = Array.from(el.children).find(child => child.slot === 'input');
|
||||
const nativeInput = getSlot(el, 'input');
|
||||
expect(nativeInput.getAttribute('aria-labelledby')).to.contain(
|
||||
`before-${el._inputId} after-${el._inputId}`,
|
||||
);
|
||||
|
|
@ -250,7 +267,7 @@ describe('<lion-field>', () => {
|
|||
// TODO: Move test below to FormControlMixin.test.js.
|
||||
it(`allows to add to aria description or label via addToAriaLabelledBy() and
|
||||
addToAriaDescribedBy()`, async () => {
|
||||
const wrapper = /** @type {LionField} */ (await fixture(html`
|
||||
const wrapper = /** @type {HTMLElement} */ (await fixture(html`
|
||||
<div id="wrapper">
|
||||
<${tag}>
|
||||
${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="additionalDescription"> Same for this </div>
|
||||
</div>`));
|
||||
const el = wrapper.querySelector(tagString);
|
||||
const el = /** @type {LionField} */ (wrapper.querySelector(tagString));
|
||||
// wait until the field element is done rendering
|
||||
await el.updateComplete;
|
||||
await el.updateComplete;
|
||||
|
|
@ -270,25 +287,33 @@ describe('<lion-field>', () => {
|
|||
// 1. addToAriaLabel()
|
||||
// Check if the aria attr is filled initially
|
||||
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)
|
||||
expect(_inputNode.getAttribute('aria-labelledby')).to.contain(`label-${el._inputId}`);
|
||||
expect(labelledbyAttr).to.contain(`label-${el._inputId}`);
|
||||
// Should be placed in the end
|
||||
expect(
|
||||
_inputNode.getAttribute('aria-labelledby').indexOf(`label-${el._inputId}`) <
|
||||
_inputNode.getAttribute('aria-labelledby').indexOf('additionalLabel'),
|
||||
labelledbyAttr.indexOf(`label-${el._inputId}`) < labelledbyAttr.indexOf('additionalLabel'),
|
||||
);
|
||||
|
||||
// 2. addToAriaDescription()
|
||||
// Check if the aria attr is filled initially
|
||||
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)
|
||||
expect(_inputNode.getAttribute('aria-describedby')).to.contain(`feedback-${el._inputId}`);
|
||||
expect(describedbyAttr).to.contain(`feedback-${el._inputId}`);
|
||||
// Should be placed in the end
|
||||
expect(
|
||||
_inputNode.getAttribute('aria-describedby').indexOf(`feedback-${el._inputId}`) <
|
||||
_inputNode.getAttribute('aria-describedby').indexOf('additionalDescription'),
|
||||
describedbyAttr.indexOf(`feedback-${el._inputId}`) <
|
||||
describedbyAttr.indexOf('additionalDescription'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -310,6 +335,9 @@ describe('<lion-field>', () => {
|
|||
return 'HasX';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} value
|
||||
*/
|
||||
execute(value) {
|
||||
const result = value.indexOf('x') === -1;
|
||||
return result;
|
||||
|
|
@ -324,6 +352,10 @@ describe('<lion-field>', () => {
|
|||
</${tag}>
|
||||
`));
|
||||
|
||||
/**
|
||||
* @param {import("../index.js").LionField} _sceneEl
|
||||
* @param {{ index?: number; el: any; wantedShowsFeedbackFor: any; }} scenario
|
||||
*/
|
||||
const executeScenario = async (_sceneEl, scenario) => {
|
||||
const sceneEl = _sceneEl;
|
||||
sceneEl.resetInteractionState();
|
||||
|
|
@ -372,6 +404,9 @@ describe('<lion-field>', () => {
|
|||
return 'HasX';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} value
|
||||
*/
|
||||
execute(value) {
|
||||
const result = value.indexOf('x') === -1;
|
||||
return result;
|
||||
|
|
@ -396,7 +431,7 @@ describe('<lion-field>', () => {
|
|||
`));
|
||||
|
||||
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.validationStates.error).to.deep.equal({});
|
||||
|
|
@ -408,6 +443,9 @@ describe('<lion-field>', () => {
|
|||
return 'HasX';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} value
|
||||
*/
|
||||
execute(value) {
|
||||
const result = value.indexOf('x') === -1;
|
||||
return result;
|
||||
|
|
@ -422,7 +460,7 @@ describe('<lion-field>', () => {
|
|||
</${tag}>
|
||||
`));
|
||||
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;
|
||||
await el.updateComplete;
|
||||
|
|
@ -437,10 +475,10 @@ describe('<lion-field>', () => {
|
|||
>${inputSlot}</${tag}>
|
||||
`));
|
||||
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';
|
||||
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 () => {
|
||||
|
|
@ -450,6 +488,9 @@ describe('<lion-field>', () => {
|
|||
return 'Bar';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} value
|
||||
*/
|
||||
execute(value) {
|
||||
const hasError = value !== 'bar';
|
||||
return hasError;
|
||||
|
|
@ -502,8 +543,12 @@ describe('<lion-field>', () => {
|
|||
'feedback',
|
||||
];
|
||||
names.forEach(slotName => {
|
||||
el.querySelector(`[slot="${slotName}"]`).setAttribute('test-me', 'ok');
|
||||
const slot = el.shadowRoot.querySelector(`slot[name="${slotName}"]`);
|
||||
const slotLight = /** @type {HTMLElement} */ (el.querySelector(`[slot="${slotName}"]`));
|
||||
slotLight.setAttribute('test-me', 'ok');
|
||||
// @ts-expect-error
|
||||
const slot = /** @type {ShadowHTMLElement} */ (el.shadowRoot.querySelector(
|
||||
`slot[name="${slotName}"]`,
|
||||
));
|
||||
const assignedNodes = slot.assignedNodes();
|
||||
expect(assignedNodes.length).to.equal(1);
|
||||
expect(assignedNodes[0].getAttribute('test-me')).to.equal('ok');
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ describe('SyncUpdatableMixin', () => {
|
|||
it('initializes all properties', async () => {
|
||||
let hasCalledFirstUpdated = false;
|
||||
let hasCalledUpdateSync = false;
|
||||
// @ts-expect-error base constructors same return type
|
||||
class UpdatableImplementation extends SyncUpdatableMixin(LitElement) {
|
||||
static get properties() {
|
||||
return {
|
||||
|
|
@ -64,6 +65,7 @@ describe('SyncUpdatableMixin', () => {
|
|||
it('guarantees Member Order Independence', async () => {
|
||||
let hasCalledRunPropertyEffect = false;
|
||||
|
||||
// @ts-expect-error base constructors same return type
|
||||
class UpdatableImplementation extends SyncUpdatableMixin(LitElement) {
|
||||
static get properties() {
|
||||
return {
|
||||
|
|
@ -134,7 +136,7 @@ describe('SyncUpdatableMixin', () => {
|
|||
let propChangedCount = 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) {
|
||||
static get properties() {
|
||||
return {
|
||||
|
|
@ -152,7 +154,6 @@ describe('SyncUpdatableMixin', () => {
|
|||
* @param {*} oldValue
|
||||
*/
|
||||
requestUpdateInternal(name, oldValue) {
|
||||
// @ts-ignore the private override is on purpose
|
||||
super.requestUpdateInternal(name, oldValue);
|
||||
if (name === 'prop') {
|
||||
propChangedCount += 1;
|
||||
|
|
@ -188,6 +189,7 @@ describe('SyncUpdatableMixin', () => {
|
|||
|
||||
describe('After firstUpdated', () => {
|
||||
it('calls "updateSync" immediately when the observed property is changed (newValue !== oldValue)', async () => {
|
||||
// @ts-expect-error
|
||||
class UpdatableImplementation extends SyncUpdatableMixin(LitElement) {
|
||||
static get properties() {
|
||||
return {
|
||||
|
|
@ -243,6 +245,7 @@ describe('SyncUpdatableMixin', () => {
|
|||
describe('Features', () => {
|
||||
// See: https://lit-element.polymer-project.org/guide/lifecycle#haschanged
|
||||
it('supports "hasChanged" from UpdatingElement', async () => {
|
||||
// @ts-expect-error base constructors same return type
|
||||
class UpdatableImplementation extends SyncUpdatableMixin(LitElement) {
|
||||
static get properties() {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ describe('Date Validation', () => {
|
|||
it('provides new isDate() to allow only dates', () => {
|
||||
let isEnabled;
|
||||
const validator = new IsDate();
|
||||
expect(validator.constructor.validatorName).to.equal('IsDate');
|
||||
expect(IsDate.validatorName).to.equal('IsDate');
|
||||
|
||||
isEnabled = validator.execute(new Date());
|
||||
expect(isEnabled).to.be.false;
|
||||
|
|
@ -28,7 +28,7 @@ describe('Date Validation', () => {
|
|||
it('provides new minDate(x) to allow only dates after min', () => {
|
||||
let isEnabled;
|
||||
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'));
|
||||
expect(isEnabled).to.be.false;
|
||||
|
|
@ -46,7 +46,7 @@ describe('Date Validation', () => {
|
|||
it('provides maxDate() to allow only dates before max', () => {
|
||||
let isEnabled;
|
||||
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'));
|
||||
expect(isEnabled).to.be.false;
|
||||
|
|
@ -67,7 +67,7 @@ describe('Date Validation', () => {
|
|||
min: new Date('2018/02/02'),
|
||||
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'));
|
||||
expect(isEnabled).to.be.false;
|
||||
|
|
@ -87,8 +87,8 @@ describe('Date Validation', () => {
|
|||
|
||||
it('provides new IsDateDisabled() to disable dates matching specified condition', () => {
|
||||
let isDisabled;
|
||||
const validator = new IsDateDisabled(d => d.getDate() === 3);
|
||||
expect(validator.constructor.validatorName).to.equal('IsDateDisabled');
|
||||
const validator = new IsDateDisabled(/** @param {Date} d */ d => d.getDate() === 3);
|
||||
expect(IsDateDisabled.validatorName).to.equal('IsDateDisabled');
|
||||
|
||||
isDisabled = validator.execute(new Date('2018/02/04'));
|
||||
expect(isDisabled).to.be.false;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ describe('Number Validation', () => {
|
|||
it('provides new IsNumber() to allow only numbers', () => {
|
||||
let isEnabled;
|
||||
const validator = new IsNumber();
|
||||
expect(validator.constructor.validatorName).to.equal('IsNumber');
|
||||
expect(IsNumber.validatorName).to.equal('IsNumber');
|
||||
|
||||
isEnabled = validator.execute(4);
|
||||
expect(isEnabled).to.be.false;
|
||||
|
|
@ -26,7 +26,7 @@ describe('Number Validation', () => {
|
|||
it('provides new MinNumber(x) to allow only numbers longer then min', () => {
|
||||
let isEnabled;
|
||||
const validator = new MinNumber(3);
|
||||
expect(validator.constructor.validatorName).to.equal('MinNumber');
|
||||
expect(MinNumber.validatorName).to.equal('MinNumber');
|
||||
|
||||
isEnabled = validator.execute(3);
|
||||
expect(isEnabled).to.be.false;
|
||||
|
|
@ -38,7 +38,7 @@ describe('Number Validation', () => {
|
|||
it('provides new MaxNumber(x) to allow only number shorter then max', () => {
|
||||
let isEnabled;
|
||||
const validator = new MaxNumber(3);
|
||||
expect(validator.constructor.validatorName).to.equal('MaxNumber');
|
||||
expect(MaxNumber.validatorName).to.equal('MaxNumber');
|
||||
|
||||
isEnabled = validator.execute(3);
|
||||
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', () => {
|
||||
let isEnabled;
|
||||
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);
|
||||
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 { MinLength } from '../../src/validate/validators/StringValidators.js';
|
||||
|
||||
/**
|
||||
* @typedef {import('../../src/validate/Validator').Validator} Validator
|
||||
*/
|
||||
|
||||
describe('ResultValidator', () => {
|
||||
it('has an "executeOnResults" function returning active state', async () => {
|
||||
// This test shows the best practice of creating executeOnResults method
|
||||
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(
|
||||
new MyResultValidator().executeOnResults({
|
||||
regularValidateResult: [new Required(), new MinLength(3)],
|
||||
regularValidationResult: [new Required(), new MinLength(3)],
|
||||
prevValidationResult: [],
|
||||
}),
|
||||
).to.be.true;
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ describe('String Validation', () => {
|
|||
it('provides new IsString() to allow only strings', () => {
|
||||
let isEnabled;
|
||||
const validator = new IsString();
|
||||
expect(validator.constructor.validatorName).to.equal('IsString');
|
||||
expect(IsString.validatorName).to.equal('IsString');
|
||||
|
||||
isEnabled = validator.execute('foo');
|
||||
expect(isEnabled).to.be.false;
|
||||
|
|
@ -29,7 +29,7 @@ describe('String Validation', () => {
|
|||
it('provides new EqualsLength(x) to allow only a specific string length', () => {
|
||||
let isEnabled;
|
||||
const validator = new EqualsLength(3);
|
||||
expect(validator.constructor.validatorName).to.equal('EqualsLength');
|
||||
expect(EqualsLength.validatorName).to.equal('EqualsLength');
|
||||
|
||||
isEnabled = validator.execute('foo');
|
||||
expect(isEnabled).to.be.false;
|
||||
|
|
@ -44,7 +44,7 @@ describe('String Validation', () => {
|
|||
it('provides new MinLength(x) to allow only strings longer then min', () => {
|
||||
let isEnabled;
|
||||
const validator = new MinLength(3);
|
||||
expect(validator.constructor.validatorName).to.equal('MinLength');
|
||||
expect(MinLength.validatorName).to.equal('MinLength');
|
||||
|
||||
isEnabled = validator.execute('foo');
|
||||
expect(isEnabled).to.be.false;
|
||||
|
|
@ -56,7 +56,7 @@ describe('String Validation', () => {
|
|||
it('provides new MaxLength(x) to allow only strings shorter then max', () => {
|
||||
let isEnabled;
|
||||
const validator = new MaxLength(3);
|
||||
expect(validator.constructor.validatorName).to.equal('MaxLength');
|
||||
expect(MaxLength.validatorName).to.equal('MaxLength');
|
||||
|
||||
isEnabled = validator.execute('foo');
|
||||
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', () => {
|
||||
let isEnabled;
|
||||
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');
|
||||
expect(isEnabled).to.be.false;
|
||||
|
|
@ -83,7 +83,7 @@ describe('String Validation', () => {
|
|||
it('provides new IsEmail() to allow only valid email formats', () => {
|
||||
let isEnabled;
|
||||
const validator = new IsEmail();
|
||||
expect(validator.constructor.validatorName).to.equal('IsEmail');
|
||||
expect(IsEmail.validatorName).to.equal('IsEmail');
|
||||
|
||||
isEnabled = validator.execute('foo@bar.com');
|
||||
expect(isEnabled).to.be.false;
|
||||
|
|
@ -116,7 +116,7 @@ describe('String Validation', () => {
|
|||
it('provides new Pattern() to allow only valid patterns', () => {
|
||||
let isEnabled;
|
||||
let validator = new Pattern(/#LionRocks/);
|
||||
expect(validator.constructor.validatorName).to.equal('Pattern');
|
||||
expect(Pattern.validatorName).to.equal('Pattern');
|
||||
|
||||
isEnabled = validator.execute('#LionRocks');
|
||||
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 { defineCE, expect, fixture, html, unsafeStatic } from '@open-wc/testing';
|
||||
import sinon from 'sinon';
|
||||
import { ValidateMixin } from '../../src/validate/ValidateMixin.js';
|
||||
import { Validator } from '../../src/validate/Validator.js';
|
||||
|
||||
/**
|
||||
* @param {function} method
|
||||
* @param {string} errorMessage
|
||||
*/
|
||||
async function expectThrowsAsync(method, errorMessage) {
|
||||
let error = null;
|
||||
try {
|
||||
|
|
@ -20,6 +24,10 @@ async function expectThrowsAsync(method, errorMessage) {
|
|||
describe('Validator', () => {
|
||||
it('has an "execute" function returning "shown" state', async () => {
|
||||
class MyValidator extends Validator {
|
||||
/**
|
||||
* @param {string} [modelValue]
|
||||
* @param {string} [param]
|
||||
*/
|
||||
execute(modelValue, param) {
|
||||
const hasError = modelValue === 'test' && param === 'me';
|
||||
return hasError;
|
||||
|
|
@ -79,20 +87,24 @@ describe('Validator', () => {
|
|||
});
|
||||
|
||||
it('has access to name, type, params, config in static get getMessage', () => {
|
||||
let staticArgs;
|
||||
let data;
|
||||
class MyValidator extends Validator {
|
||||
static get validatorName() {
|
||||
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' });
|
||||
vali._getMessage();
|
||||
|
||||
expect(staticArgs[0]).to.deep.equal({
|
||||
expect(data).to.deep.equal({
|
||||
name: 'MyValidator',
|
||||
type: 'error',
|
||||
params: 'myParam',
|
||||
|
|
@ -103,7 +115,9 @@ describe('Validator', () => {
|
|||
it('fires "param-changed" event on param change', async () => {
|
||||
const vali = new Validator('foo');
|
||||
const cb = sinon.spy(() => {});
|
||||
if (vali.addEventListener) {
|
||||
vali.addEventListener('param-changed', cb);
|
||||
}
|
||||
vali.param = 'bar';
|
||||
expect(cb.callCount).to.equal(1);
|
||||
});
|
||||
|
|
@ -111,47 +125,41 @@ describe('Validator', () => {
|
|||
it('fires "config-changed" event on config change', async () => {
|
||||
const vali = new Validator('foo', { foo: 'bar' });
|
||||
const cb = sinon.spy(() => {});
|
||||
if (vali.addEventListener) {
|
||||
vali.addEventListener('config-changed', cb);
|
||||
}
|
||||
vali.config = { bar: 'foo' };
|
||||
expect(cb.callCount).to.equal(1);
|
||||
});
|
||||
|
||||
it('has access to FormControl', async () => {
|
||||
const lightDom = '';
|
||||
const tagString = defineCE(
|
||||
class extends ValidateMixin(LitElement) {
|
||||
// @ts-expect-error base constructors same return type
|
||||
class ValidateElement extends ValidateMixin(LitElement) {
|
||||
static get properties() {
|
||||
return { modelValue: String };
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
const tagString = defineCE(ValidateElement);
|
||||
const tag = unsafeStatic(tagString);
|
||||
|
||||
class MyValidator extends Validator {
|
||||
/**
|
||||
* @param {string} modelValue
|
||||
* @param {string} param
|
||||
*/
|
||||
execute(modelValue, param) {
|
||||
const hasError = modelValue === 'forbidden' && param === 'values';
|
||||
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 connectSpy = sinon.spy(myVal, 'onFormControlConnect');
|
||||
const disconnectSpy = sinon.spy(myVal, 'onFormControlDisconnect');
|
||||
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||
<${tag} .validators=${[myVal]}>${lightDom}</${tag}>
|
||||
`);
|
||||
`));
|
||||
|
||||
expect(connectSpy.callCount).to.equal(1);
|
||||
expect(connectSpy.calledWith(el)).to.equal(true);
|
||||
|
|
@ -171,6 +179,9 @@ describe('Validator', () => {
|
|||
it('supports customized types', async () => {
|
||||
// This test shows the best practice of adding custom types
|
||||
class MyValidator extends Validator {
|
||||
/**
|
||||
* @param {...any} args
|
||||
*/
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this.type = 'my-type';
|
||||
|
|
|
|||
|
|
@ -4,9 +4,15 @@ import sinon from 'sinon';
|
|||
import '../../lion-validation-feedback.js';
|
||||
import { AlwaysInvalid, AlwaysValid } from '../../test-helpers.js';
|
||||
|
||||
/**
|
||||
* @typedef {import('../../src/validate/LionValidationFeedback').LionValidationFeedback} LionValidationFeedback
|
||||
*/
|
||||
|
||||
describe('lion-validation-feedback', () => {
|
||||
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('');
|
||||
el.feedbackData = [{ message: 'hello', type: 'error', validator: new AlwaysInvalid() }];
|
||||
await el.updateComplete;
|
||||
|
|
@ -14,7 +20,9 @@ describe('lion-validation-feedback', () => {
|
|||
});
|
||||
|
||||
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() }];
|
||||
await el.updateComplete;
|
||||
expect(el.getAttribute('type')).to.equal('error');
|
||||
|
|
@ -25,7 +33,9 @@ describe('lion-validation-feedback', () => {
|
|||
});
|
||||
|
||||
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();
|
||||
|
||||
|
|
@ -45,7 +55,9 @@ describe('lion-validation-feedback', () => {
|
|||
});
|
||||
|
||||
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();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,8 @@
|
|||
import { Constructor } from '@open-wc/dedupe-mixin';
|
||||
import { LitElement } from '@lion/core';
|
||||
import { FormControlHost } from './FormControlMixinTypes';
|
||||
|
||||
export declare class FocusHost {
|
||||
static properties: {
|
||||
focused: {
|
||||
type: BooleanConstructor;
|
||||
reflect: boolean;
|
||||
};
|
||||
};
|
||||
focused: boolean;
|
||||
|
||||
connectedCallback(): void;
|
||||
|
|
@ -23,6 +18,6 @@ export declare class FocusHost {
|
|||
|
||||
export declare function FocusImplementation<T extends Constructor<LitElement>>(
|
||||
superclass: T,
|
||||
): T & Constructor<FocusHost> & FocusHost;
|
||||
): T & Constructor<FocusHost> & FocusHost & Constructor<FormControlHost> & typeof FormControlHost;
|
||||
|
||||
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 { SlotsMap } from '@lion/core/types/SlotMixinTypes';
|
||||
import { LitElement, CSSResult, TemplateResult, nothing } from '@lion/core';
|
||||
import { DisabledHost } from '@lion/core/types/DisabledMixinTypes';
|
||||
|
||||
export class FormControlMixinHost {
|
||||
static get properties(): {
|
||||
name: {
|
||||
type: StringConstructor;
|
||||
reflect: boolean;
|
||||
};
|
||||
label: {
|
||||
attribute: boolean;
|
||||
};
|
||||
helpText: {
|
||||
type: StringConstructor;
|
||||
attribute: string;
|
||||
};
|
||||
_ariaLabelledNodes: {
|
||||
attribute: boolean;
|
||||
};
|
||||
_ariaDescribedNodes: {
|
||||
attribute: boolean;
|
||||
};
|
||||
_repropagationRole: {
|
||||
attribute: boolean;
|
||||
};
|
||||
_isRepropagationEndpoint: {
|
||||
attribute: boolean;
|
||||
};
|
||||
};
|
||||
import { LionValidationFeedback } from '../src/validate/LionValidationFeedback';
|
||||
import { FormRegisteringHost } from './registration/FormRegisteringMixinTypes';
|
||||
|
||||
export class FormControlHost {
|
||||
static get styles(): CSSResult | CSSResult[];
|
||||
|
||||
name: string;
|
||||
modelValue: unknown;
|
||||
set label(arg: string);
|
||||
get label(): string;
|
||||
__label: string | undefined;
|
||||
|
|
@ -43,11 +24,12 @@ export class FormControlMixinHost {
|
|||
get _inputNode(): HTMLElement;
|
||||
get _labelNode(): HTMLElement;
|
||||
get _helpTextNode(): HTMLElement;
|
||||
get _feedbackNode(): HTMLElement;
|
||||
get _feedbackNode(): LionValidationFeedback | undefined;
|
||||
_inputId: string;
|
||||
_ariaLabelledNodes: HTMLElement[];
|
||||
_ariaDescribedNodes: HTMLElement[];
|
||||
_repropagationRole: 'child' | 'choice-group' | 'fieldset';
|
||||
_repropagationRole: string; // 'child' | 'choice-group' | 'fieldset';
|
||||
_isRepropagationEndpoint: boolean;
|
||||
|
||||
connectedCallback(): void;
|
||||
updated(changedProperties: import('lit-element').PropertyValues): void;
|
||||
|
|
@ -99,6 +81,14 @@ export class FormControlMixinHost {
|
|||
|
||||
export declare function FormControlImplementation<T extends Constructor<LitElement>>(
|
||||
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;
|
||||
|
|
|
|||
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 { LitElement } from '@lion/core';
|
||||
import { ValidateHost } from './validate/ValidateMixinTypes';
|
||||
import { FormControlHost } from './FormControlMixinTypes';
|
||||
|
||||
export declare interface FormatOptions {
|
||||
locale?: string;
|
||||
|
|
@ -7,15 +9,6 @@ export declare interface FormatOptions {
|
|||
}
|
||||
|
||||
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;
|
||||
serializedValue: string;
|
||||
formatOn: string;
|
||||
|
|
@ -32,7 +25,7 @@ export declare class FormatHost {
|
|||
_calculateValues(opts: { source: 'model' | 'serialized' | 'formatted' | null }): void;
|
||||
__callParser(value: string | undefined): object;
|
||||
__callFormatter(): string;
|
||||
_onModelValueChanged(args: { modelValue: unknown }[]): void;
|
||||
_onModelValueChanged(arg: { modelValue: unknown }): void;
|
||||
_dispatchModelValueChangedEvent(): void;
|
||||
_syncValueUpwards(): void;
|
||||
_reflectBackFormattedValueToUser(): void;
|
||||
|
|
@ -47,6 +40,12 @@ export declare class FormatHost {
|
|||
|
||||
export declare function FormatImplementation<T extends Constructor<LitElement>>(
|
||||
superclass: T,
|
||||
): T & Constructor<FormatHost> & FormatHost;
|
||||
): T &
|
||||
Constructor<FormatHost> &
|
||||
FormatHost &
|
||||
Constructor<ValidateHost> &
|
||||
typeof ValidateHost &
|
||||
Constructor<FormControlHost> &
|
||||
typeof FormControlHost;
|
||||
|
||||
export type FormatMixin = typeof FormatImplementation;
|
||||
|
|
|
|||
|
|
@ -1,27 +1,8 @@
|
|||
import { Constructor } from '@open-wc/dedupe-mixin';
|
||||
import { LitElement } from '@lion/core';
|
||||
import { FormControlHost } from './FormControlMixinTypes';
|
||||
|
||||
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;
|
||||
filled: boolean;
|
||||
touched: boolean;
|
||||
|
|
@ -43,6 +24,10 @@ export declare class InteractionStateHost {
|
|||
|
||||
export declare function InteractionStateImplementation<T extends Constructor<LitElement>>(
|
||||
superclass: T,
|
||||
): T & Constructor<InteractionStateHost> & InteractionStateHost;
|
||||
): T &
|
||||
Constructor<InteractionStateHost> &
|
||||
InteractionStateHost &
|
||||
Constructor<FormControlHost> &
|
||||
typeof FormControlHost;
|
||||
|
||||
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 { FormRegistrarHost } from './FormRegistrarMixinTypes';
|
||||
import { LitElement } from '@lion/core';
|
||||
|
||||
export declare class FormRegisteringHost {
|
||||
connectedCallback(): void;
|
||||
disconnectedCallback(): void;
|
||||
__parentFormGroup?: FormRegistrarHost;
|
||||
}
|
||||
|
||||
export declare function FormRegisteringImplementation<T extends Constructor<HTMLElement>>(
|
||||
export declare function FormRegisteringImplementation<T extends Constructor<LitElement>>(
|
||||
superclass: T,
|
||||
): T & Constructor<FormRegisteringHost> & FormRegisteringHost;
|
||||
): T & Constructor<FormRegisteringHost> & typeof FormRegisteringHost;
|
||||
|
||||
export type FormRegisteringMixin = typeof FormRegisteringImplementation;
|
||||
|
|
|
|||
|
|
@ -1,27 +1,35 @@
|
|||
import { Constructor } from '@open-wc/dedupe-mixin';
|
||||
|
||||
export declare class FormControlsCollection {
|
||||
_keys(): string[];
|
||||
}
|
||||
import { FormControlsCollection } from '../../src/registration/FormControlsCollection';
|
||||
import { FormRegisteringHost } from '../../types/registration/FormRegisteringMixinTypes';
|
||||
import { FormControlHost } from '../../types/FormControlMixinTypes';
|
||||
import { LitElement } from '@lion/core';
|
||||
|
||||
export declare class ElementWithParentFormGroup {
|
||||
__parentFormGroup: FormRegistrarHost;
|
||||
}
|
||||
|
||||
export declare class FormRegistrarHost {
|
||||
static get properties(): {
|
||||
_isFormOrFieldset: {
|
||||
type: BooleanConstructor;
|
||||
reflect: boolean;
|
||||
};
|
||||
};
|
||||
_isFormOrFieldset: boolean;
|
||||
formElements: FormControlsCollection;
|
||||
addFormElement(child: HTMLElement & ElementWithParentFormGroup, indexToInsertAt: number): void;
|
||||
formElements: FormControlsCollection & { [x: string]: any };
|
||||
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,
|
||||
): T & Constructor<FormRegistrarHost> & FormRegistrarHost;
|
||||
): T &
|
||||
Constructor<FormRegistrarHost> &
|
||||
typeof FormRegistrarHost &
|
||||
Constructor<FormRegisteringHost> &
|
||||
typeof FormRegisteringHost;
|
||||
|
||||
export type FormRegistrarMixin = typeof FormRegistrarImplementation;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { Constructor } from '@open-wc/dedupe-mixin';
|
||||
import { LitElement } from '@lion/core';
|
||||
|
||||
export declare class FormRegistrarPortalHost {
|
||||
__redispatchEventForFormRegistrarPortalMixin(ev: CustomEvent): void;
|
||||
}
|
||||
|
||||
export declare function FormRegistrarPortalImplementation<T extends Constructor<HTMLElement>>(
|
||||
export declare function FormRegistrarPortalImplementation<T extends Constructor<LitElement>>(
|
||||
superclass: T,
|
||||
): T & Constructor<FormRegistrarPortalHost> & FormRegistrarPortalHost;
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,6 @@ export type SyncUpdatableHostType = typeof SyncUpdatableHost;
|
|||
|
||||
export declare function SyncUpdatableImplementation<T extends Constructor<LitElement>>(
|
||||
superclass: T,
|
||||
): T & Constructor<SyncUpdatableHost> & SyncUpdatableHost;
|
||||
): T & Constructor<SyncUpdatableHost> & typeof SyncUpdatableHost;
|
||||
|
||||
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>
|
||||
</lion-form>
|
||||
<h-output .field="${fieldElement}" .show="${[...props, 'hasFeedbackFor']}"> </h-output>
|
||||
<h3>
|
||||
Set conditions for validation feedback visibility
|
||||
</h3>
|
||||
<h3>Set conditions for validation feedback visibility</h3>
|
||||
<lion-checkbox-group name="props[]" @model-value-changed="${fetchConditionsAndReevaluate}">
|
||||
${props.map(p => html` <lion-checkbox .label="${p}" .choiceValue="${p}"> </lion-checkbox> `)}
|
||||
</lion-checkbox-group>
|
||||
|
|
|
|||
|
|
@ -53,9 +53,7 @@ export const disabled = () => {
|
|||
<lion-input name="LastName2" label="Last Name" .modelValue=${'Bar'}></lion-input>
|
||||
</lion-fieldset>
|
||||
</lion-fieldset>
|
||||
<button @click=${toggleDisabled}>
|
||||
Toggle disabled
|
||||
</button>
|
||||
<button @click=${toggleDisabled}>Toggle disabled</button>
|
||||
`;
|
||||
};
|
||||
```
|
||||
|
|
|
|||
|
|
@ -113,9 +113,7 @@ describe(`Submitting/Resetting Form`, async () => {
|
|||
></lion-textarea>
|
||||
<div class="buttons">
|
||||
<lion-button id="submit_button" type="submit" raised>Submit</lion-button>
|
||||
<lion-button id="reset_button" type="reset" raised>
|
||||
Reset
|
||||
</lion-button>
|
||||
<lion-button id="reset_button" type="reset" raised> Reset </lion-button>
|
||||
</div>
|
||||
</form>
|
||||
</lion-form>
|
||||
|
|
|
|||
|
|
@ -21,9 +21,7 @@ export class SbLocaleSwitcher extends LitElement {
|
|||
return html`
|
||||
${this.showLocales.map(
|
||||
showLocale => html`
|
||||
<button @click=${() => this.callback(showLocale)}>
|
||||
${showLocale}
|
||||
</button>
|
||||
<button @click=${() => this.callback(showLocale)}>${showLocale}</button>
|
||||
`,
|
||||
)}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -225,9 +225,7 @@ export class LionInputDatepicker extends ScopedElementsMixin(OverlayMixin(LionIn
|
|||
|
||||
render() {
|
||||
return html`
|
||||
<div class="form-field__group-one">
|
||||
${this._groupOneTemplate()}
|
||||
</div>
|
||||
<div class="form-field__group-one">${this._groupOneTemplate()}</div>
|
||||
<div class="form-field__group-two">
|
||||
${this._groupTwoTemplate()} ${this._overlayTemplate()}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -79,11 +79,7 @@ class MyHelloComponent extends LocalizeMixin(LitElement) {
|
|||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div>
|
||||
${this.msgLit('my-hello-component:greeting')}
|
||||
</div>
|
||||
`;
|
||||
return html` <div>${this.msgLit('my-hello-component:greeting')}</div> `;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -112,9 +108,7 @@ import { localize } from '@lion/localize';
|
|||
|
||||
export function myTemplate(someData) {
|
||||
return html`
|
||||
<div>
|
||||
${localize.msg('my-hello-component:feeling', { feeling: someData.feeling })}
|
||||
</div>
|
||||
<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 isLocalizeESModule from './isLocalizeESModule.js';
|
||||
|
||||
|
|
|
|||
|
|
@ -19,9 +19,9 @@ export function getDateFormatBasedOnLocale() {
|
|||
function getPartByIndex(index) {
|
||||
/** @type {Object.<string,string>} */
|
||||
const template = {
|
||||
'2012': 'year',
|
||||
'12': 'month',
|
||||
'20': 'day',
|
||||
2012: 'year',
|
||||
12: 'month',
|
||||
20: 'day',
|
||||
};
|
||||
const key = dateParts[index];
|
||||
return template[key];
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { expect, oneEvent, aTimeout } from '@open-wc/testing';
|
||||
import sinon from 'sinon';
|
||||
// @ts-expect-error no types for this package
|
||||
import { fetchMock } from '@bundled-es-modules/fetch-mock';
|
||||
import { setupFakeImport, resetFakeImport, fakeImport } from '../test-helpers.js';
|
||||
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ describe('LocalizeMixin', () => {
|
|||
'child-element': loc => fakeImport(`./child-element/${loc}.js`),
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
// @ts-expect-error
|
||||
class ParentElement extends LocalizeMixin(LitElement) {
|
||||
static get localizeNamespaces() {
|
||||
return [parentElementNs, defaultNs, ...super.localizeNamespaces];
|
||||
|
|
@ -79,7 +79,7 @@ describe('LocalizeMixin', () => {
|
|||
}
|
||||
|
||||
const tagString = defineCE(
|
||||
// @ts-ignore
|
||||
// @ts-expect-error
|
||||
class ChildElement extends LocalizeMixin(ParentElement) {
|
||||
static get localizeNamespaces() {
|
||||
return [childElementNs, defaultNs, ...super.localizeNamespaces];
|
||||
|
|
|
|||
|
|
@ -91,9 +91,7 @@ export const isTooltip = () => {
|
|||
<button slot="invoker" @mouseenter="${showTooltip}" @mouseleave="${hideTooltip}">
|
||||
Hover me to open the tooltip!
|
||||
</button>
|
||||
<div slot="content" class="demo-overlay">
|
||||
Hello!
|
||||
</div>
|
||||
<div slot="content" class="demo-overlay">Hello!</div>
|
||||
</demo-overlay-system>
|
||||
`;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -167,11 +167,7 @@ export function runOverlayMixinSuite({ tagString, tag, suffix = '' }) {
|
|||
function sendCloseEvent(e) {
|
||||
e.target.dispatchEvent(new Event('close-overlay', { bubbles: true }));
|
||||
}
|
||||
const closeBtn = await fixture(html`
|
||||
<button @click=${sendCloseEvent}>
|
||||
close
|
||||
</button>
|
||||
`);
|
||||
const closeBtn = await fixture(html` <button @click=${sendCloseEvent}>close</button> `);
|
||||
|
||||
const el = await fixture(html`
|
||||
<${tag} opened>
|
||||
|
|
|
|||
|
|
@ -67,11 +67,7 @@ describe('OverlayController', () => {
|
|||
`);
|
||||
}
|
||||
if (mode === 'inline') {
|
||||
contentNode = await fixture(html`
|
||||
<div>
|
||||
I should be on top
|
||||
</div>
|
||||
`);
|
||||
contentNode = await fixture(html` <div>I should be on top</div> `);
|
||||
contentNode.style.zIndex = zIndexVal;
|
||||
}
|
||||
return contentNode;
|
||||
|
|
|
|||
|
|
@ -103,9 +103,7 @@ describe('Local Positioning', () => {
|
|||
},
|
||||
});
|
||||
await fixture(html`
|
||||
<div style="position: absolute; top: 0;">
|
||||
${ctrl.invokerNode}${ctrl.content}
|
||||
</div>
|
||||
<div style="position: absolute; top: 0;">${ctrl.invokerNode}${ctrl.content}</div>
|
||||
`);
|
||||
|
||||
await ctrl.show();
|
||||
|
|
|
|||
|
|
@ -35,9 +35,7 @@ const interactionElementsNode = renderLitAsNode(html`
|
|||
const lightDomTemplate = html`
|
||||
<div>
|
||||
<button id="outside-1">outside 1</button>
|
||||
<div id="rootElement">
|
||||
${interactionElementsNode}
|
||||
</div>
|
||||
<div id="rootElement">${interactionElementsNode}</div>
|
||||
<button id="outside-2">outside 2</button>
|
||||
</div>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -94,17 +94,11 @@ export const methods = () => {
|
|||
<button @click=${() => document.getElementById('pagination-method').previous()}>
|
||||
Previous
|
||||
</button>
|
||||
<button @click=${() => document.getElementById('pagination-method').next()}>
|
||||
Next
|
||||
</button>
|
||||
<button @click=${() => document.getElementById('pagination-method').next()}>Next</button>
|
||||
<br />
|
||||
<br />
|
||||
<button @click=${() => document.getElementById('pagination-method').first()}>
|
||||
First
|
||||
</button>
|
||||
<button @click=${() => document.getElementById('pagination-method').last()}>
|
||||
Last
|
||||
</button>
|
||||
<button @click=${() => document.getElementById('pagination-method').first()}>First</button>
|
||||
<button @click=${() => document.getElementById('pagination-method').last()}>Last</button>
|
||||
<br />
|
||||
<br />
|
||||
<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>
|
||||
<div class="u-ml2">
|
||||
${this._activeAnalyzerSelectTemplate()}
|
||||
<button @click="${() => downloadFile('data.csv', this._createCsv())}">
|
||||
get csv
|
||||
</button>
|
||||
<button @click="${() => downloadFile('data.csv', this._createCsv())}">get csv</button>
|
||||
</div>
|
||||
</div>
|
||||
${this._selectionMenuTemplate(this.__menuData)}
|
||||
|
|
|
|||
|
|
@ -367,9 +367,7 @@ You can use this `selectedElement` to then render the content to your own invoke
|
|||
```html
|
||||
<lion-select-rich>
|
||||
<my-invoker-button slot="invoker"></my-invoker-button>
|
||||
<lion-options slot="input">
|
||||
...
|
||||
</lion-options>
|
||||
<lion-options slot="input"> ... </lion-options>
|
||||
</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() {
|
||||
super();
|
||||
this.active = false;
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ export class LionSelectInvoker extends LionButton {
|
|||
|
||||
_contentTemplate() {
|
||||
if (this.selectedElement) {
|
||||
const labelNodes = Array.from(this.selectedElement.querySelectorAll('*'));
|
||||
const labelNodes = Array.from(this.selectedElement.childNodes);
|
||||
if (labelNodes.length > 0) {
|
||||
return labelNodes.map(node => node.cloneNode(true));
|
||||
}
|
||||
|
|
@ -113,11 +113,7 @@ export class LionSelectInvoker extends LionButton {
|
|||
}
|
||||
|
||||
_beforeTemplate() {
|
||||
return html`
|
||||
<div id="content-wrapper">
|
||||
${this._contentTemplate()}
|
||||
</div>
|
||||
`;
|
||||
return html` <div id="content-wrapper">${this._contentTemplate()}</div> `;
|
||||
}
|
||||
|
||||
// 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 () => {
|
||||
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;
|
||||
|
||||
expect(el._contentWrapperNode).lightDom.to.equal(
|
||||
`
|
||||
Textnode
|
||||
<h2>I am</h2>
|
||||
<p>2 lines</p>
|
||||
`,
|
||||
|
|
|
|||
|
|
@ -38,12 +38,8 @@ export class LionSwitch extends ScopedElementsMixin(ChoiceInputMixin(LionField))
|
|||
*/
|
||||
render() {
|
||||
return html`
|
||||
<div class="form-field__group-one">
|
||||
${this._groupOneTemplate()}
|
||||
</div>
|
||||
<div class="form-field__group-two">
|
||||
${this._groupTwoTemplate()}
|
||||
</div>
|
||||
<div class="form-field__group-one">${this._groupOneTemplate()}</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