Merge pull request #1370 from ing-bank/feat/lit2Update

feat: update to lit 2
This commit is contained in:
Thijs Louisse 2021-05-20 23:15:35 +02:00 committed by GitHub
commit 2b73c50f6e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
158 changed files with 5448 additions and 4148 deletions

View file

@ -0,0 +1,40 @@
---
'babel-plugin-extend-docs': minor
'providence-analytics': minor
'publish-docs': minor
'remark-extend': minor
'@lion/accordion': minor
'@lion/button': minor
'@lion/calendar': minor
'@lion/checkbox-group': minor
'@lion/collapsible': minor
'@lion/combobox': minor
'@lion/core': minor
'@lion/dialog': minor
'@lion/form': minor
'@lion/form-core': minor
'@lion/form-integrations': minor
'@lion/helpers': minor
'@lion/icon': minor
'@lion/input': minor
'@lion/input-amount': minor
'@lion/input-datepicker': minor
'@lion/input-iban': minor
'@lion/input-stepper': minor
'@lion/listbox': minor
'@lion/localize': minor
'@lion/overlays': minor
'@lion/pagination': minor
'@lion/progress-indicator': minor
'@lion/radio-group': minor
'@lion/select': minor
'@lion/select-rich': minor
'@lion/steps': minor
'@lion/switch': minor
'@lion/tabs': minor
'@lion/textarea': minor
'@lion/tooltip': minor
'@lion/validate-messages': minor
---
**BREAKING** Upgrade to lit version 2

View file

@ -16,3 +16,11 @@ trim_trailing_whitespace = false
block_comment_start = /** block_comment_start = /**
block_comment = * block_comment = *
block_comment_end = */ block_comment_end = */
[*.{d.ts,patch,editorconfig}]
charset = unset
indent_style = unset
indent_size = unset
end_of_line = unset
insert_final_newline = unset
trim_trailing_whitespace = unset

View file

@ -7,3 +7,4 @@ storybook-static/
_site-dev _site-dev
_site _site
docs/_merged_* docs/_merged_*
patches/

View file

@ -72,7 +72,7 @@ jobs:
strategy: strategy:
matrix: matrix:
node-version: [12.x, 14.x] node-version: [12.x, 14.x]
os: [ubuntu-latest, windows-latest] os: [ubuntu-latest]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2

View file

@ -24,7 +24,10 @@ import { lazyRender } from './src/lazyRender.js';
export const main = () => html` export const main = () => html`
<lion-combobox name="combo" label="Default"> <lion-combobox name="combo" label="Default">
${lazyRender( ${lazyRender(
listboxData.map(entry => html` <lion-option .choiceValue="${entry}">${entry}</lion-option> `), listboxData.map(
(entry, i) =>
html` <lion-option .checked="${i === 0}" .choiceValue="${entry}">${entry}</lion-option> `,
),
)} )}
</lion-combobox> </lion-combobox>
`; `;

View file

@ -1,4 +1,5 @@
import { directive } from '@lion/core'; import { directive } from 'lit/directive.js';
import { AsyncDirective } from 'lit/async-directive.js';
/** /**
* In order to speed up the first meaningful paint, use this directive * In order to speed up the first meaningful paint, use this directive
@ -15,9 +16,14 @@ import { directive } from '@lion/core';
* )} * )}
* </lion-combobox> * </lion-combobox>
*/ */
export const lazyRender = directive(tplResult => part => { export const lazyRender = directive(
setTimeout(() => { class extends AsyncDirective {
part.setValue(tplResult); render(tplResult) {
part.commit(); setTimeout(() => {
}); this.setValue(tplResult);
}); });
}
},
);
// export const lazyRender = () => {};

View file

@ -1,4 +1,4 @@
/* eslint-disable*/ /* eslint-disable */
// https://github.com/gustf/js-levenshtein/blob/master/index.js // https://github.com/gustf/js-levenshtein/blob/master/index.js
function _min(d0, d1, d2, bx, ay) { function _min(d0, d1, d2, bx, ay) {

View file

@ -36,9 +36,8 @@ There are the following methods available to control the pagination.
```js preview-story ```js preview-story
export const methods = ({ shadowRoot }) => { export const methods = ({ shadowRoot }) => {
setTimeout(() => { setTimeout(() => {
shadowRoot.getElementById('pagination-method-demo').innerText = shadowRoot.getElementById( shadowRoot.getElementById('pagination-method-demo').innerText =
'pagination-method', shadowRoot.getElementById('pagination-method').current;
).current;
}); });
return html` return html`
@ -80,9 +79,8 @@ export const methods = ({ shadowRoot }) => {
```js preview-story ```js preview-story
export const event = ({ shadowRoot }) => { export const event = ({ shadowRoot }) => {
setTimeout(() => { setTimeout(() => {
shadowRoot.getElementById('pagination-event-demo-text').innerText = shadowRoot.getElementById( shadowRoot.getElementById('pagination-event-demo-text').innerText =
'pagination-event-demo', shadowRoot.getElementById('pagination-event-demo').current;
).current;
}); });
return html` return html`

View file

@ -18,9 +18,9 @@ import '@lion/button/define';
export class UmbrellaForm extends LitElement { export class UmbrellaForm extends LitElement {
get _lionFormNode() { get _lionFormNode() {
return /** @type {import('@lion/form').LionForm} */ (this.shadowRoot?.querySelector( return /** @type {import('@lion/form').LionForm} */ (
'lion-form', this.shadowRoot?.querySelector('lion-form')
)); );
} }
render() { render() {

View file

@ -14,7 +14,7 @@ import {
import './assets/demo-overlay-system.js'; import './assets/demo-overlay-system.js';
import './assets/demo-overlay-backdrop.js'; import './assets/demo-overlay-backdrop.js';
import './assets/applyDemoOverlayStyles.js'; import './assets/applyDemoOverlayStyles.js';
import { ref as r } from './assets/ref.js'; import { ref, createRef } from 'lit/directives/ref.js';
``` ```
The overlay system allows to create different types of overlays like dialogs, toasts, tooltips, dropdown, etc. The overlay system allows to create different types of overlays like dialogs, toasts, tooltips, dropdown, etc.
@ -388,14 +388,21 @@ export const openedState = () => {
const appState = { const appState = {
opened: false, opened: false,
}; };
const refs = {}; const myRefs = {
overlay: createRef(),
openedState: createRef(),
};
function onOpenClosed(ev) { function onOpenClosed(ev) {
appState.opened = ev.target.opened; appState.opened = ev.target.opened;
refs.openedState.innerText = appState.opened; myRefs.openedState.value.innerText = appState.opened;
} }
return html` return html`
appState.opened: <span #openedState=${r(refs)}>${appState.opened}</span> appState.opened: <span ${ref(myRefs.openedState)}>${appState.opened}</span>
<demo-overlay-system .opened="${appState.opened}" @opened-changed=${onOpenClosed}> <demo-overlay-system
${ref(myRefs.overlay)}
.opened="${appState.opened}"
@opened-changed=${onOpenClosed}
>
<button slot="invoker">Overlay</button> <button slot="invoker">Overlay</button>
<div slot="content" class="demo-overlay"> <div slot="content" class="demo-overlay">
Hello! You can close this notification here: Hello! You can close this notification here:
@ -419,7 +426,10 @@ the `before-close` or `before-open` events.
export const interceptingOpenClose = () => { export const interceptingOpenClose = () => {
// Application code // Application code
let blockOverlay = true; let blockOverlay = true;
const refs = {}; const myRefs = {
statusButton: createRef(),
overlay: createRef(),
};
function intercept(ev) { function intercept(ev) {
if (blockOverlay) { if (blockOverlay) {
ev.preventDefault(); ev.preventDefault();
@ -428,28 +438,29 @@ export const interceptingOpenClose = () => {
return html` return html`
Overlay blocked state: Overlay blocked state:
<button <button
#statusButton=${r(refs)} ${ref(myRefs.statusButton)}
@click="${() => { @click="${() => {
blockOverlay = !blockOverlay; blockOverlay = !blockOverlay;
refs.statusButton.textContent = blockOverlay; myRefs.statusButton.value.textContent = blockOverlay;
}}" }}"
> >
${blockOverlay} ${blockOverlay}
</button> </button>
<demo-overlay-system <demo-overlay-system
#overlay=${r(refs)} ${ref(myRefs.overlay)}
@before-closed=${intercept} @before-closed=${intercept}
@before-opened=${intercept} @before-opened=${intercept}
> >
<button <button
slot="invoker" slot="invoker"
@click=${() => console.log('blockOverlay', blockOverlay, 'opened', refs.overlay.opened)} @click=${() =>
console.log('blockOverlay', blockOverlay, 'opened', myRefs.overlay.value.opened)}
> >
Overlay Overlay
</button> </button>
<div slot="content" class="demo-overlay"> <div slot="content" class="demo-overlay">
Hello! You can close this notification here: Hello! You can close this notification here:
<button @click=${() => (refs.overlay.opened = false)}></button> <button @click=${() => (myRefs.overlay.value.opened = false)}></button>
</div> </div>
</demo-overlay-system> </demo-overlay-system>
`; `;

View file

@ -2,10 +2,6 @@
"private": true, "private": true,
"name": "@lion/root", "name": "@lion/root",
"license": "MIT", "license": "MIT",
"workspaces": [
"packages/*",
"packages-node/*"
],
"scripts": { "scripts": {
"build": "rocket build", "build": "rocket build",
"build:types": "tsc -p tsconfig.build.types.json", "build:types": "tsc -p tsconfig.build.types.json",
@ -17,7 +13,7 @@
"format": "npm run format:eslint && npm run format:prettier", "format": "npm run format:eslint && npm run format:prettier",
"format:eslint": "eslint --ext .js,.html . --fix", "format:eslint": "eslint --ext .js,.html . --fix",
"format:prettier": "prettier \"**/*.{js,md}\" \"packages/*/package.json\" \"package.json\" --write", "format:prettier": "prettier \"**/*.{js,md}\" \"packages/*/package.json\" \"package.json\" --write",
"postinstall": "npm run custom-elements-manifest", "postinstall": "npm run custom-elements-manifest && patch-package",
"lint": "run-p lint:*", "lint": "run-p lint:*",
"lint:eclint": "git ls-files | xargs eclint check", "lint:eclint": "git ls-files | xargs eclint check",
"lint:eslint": "eslint --ext .js,.html .", "lint:eslint": "eslint --ext .js,.html .",
@ -36,6 +32,10 @@
"test:screenshots": "rimraf screenshots/.diff/ && rimraf screenshots/.current/ && mocha --require scripts/screenshots/bootstrap.js --exit --timeout 10000 \"packages/**/test/*.screenshots-test.js\"", "test:screenshots": "rimraf screenshots/.diff/ && rimraf screenshots/.current/ && mocha --require scripts/screenshots/bootstrap.js --exit --timeout 10000 \"packages/**/test/*.screenshots-test.js\"",
"test:screenshots:update": "cross-env UPDATE_SCREENSHOTS=true npm run test:screenshots" "test:screenshots:update": "cross-env UPDATE_SCREENSHOTS=true npm run test:screenshots"
}, },
"workspaces": [
"packages/*",
"packages-node/*"
],
"devDependencies": { "devDependencies": {
"@babel/core": "^7.10.1", "@babel/core": "^7.10.1",
"@bundled-es-modules/fetch-mock": "^6.5.2", "@bundled-es-modules/fetch-mock": "^6.5.2",
@ -45,8 +45,8 @@
"@custom-elements-manifest/analyzer": "^0.1.8", "@custom-elements-manifest/analyzer": "^0.1.8",
"@open-wc/building-rollup": "^1.2.1", "@open-wc/building-rollup": "^1.2.1",
"@open-wc/eslint-config": "^4.2.0", "@open-wc/eslint-config": "^4.2.0",
"@open-wc/testing": "^2.5.18", "@open-wc/testing": "^3.0.0-next.1",
"@open-wc/testing-helpers": "^1.0.0", "@open-wc/testing-helpers": "^2.0.0-next.0",
"@rocket/blog": "^0.3.0", "@rocket/blog": "^0.3.0",
"@rocket/cli": "^0.6.2", "@rocket/cli": "^0.6.2",
"@rocket/launch": "^0.4.0", "@rocket/launch": "^0.4.0",
@ -59,9 +59,9 @@
"@types/prettier": "^2.2.1", "@types/prettier": "^2.2.1",
"@web/dev-server": "^0.1.8", "@web/dev-server": "^0.1.8",
"@web/dev-server-legacy": "^0.1.7", "@web/dev-server-legacy": "^0.1.7",
"@web/test-runner": "^0.12.15", "@web/test-runner": "^0.13.4",
"@web/test-runner-browserstack": "^0.4.2", "@web/test-runner-browserstack": "^0.4.2",
"@web/test-runner-playwright": "^0.8.4", "@web/test-runner-playwright": "^0.8.6",
"babel-polyfill": "^6.26.0", "babel-polyfill": "^6.26.0",
"bundlesize": "^1.0.0-beta.2", "bundlesize": "^1.0.0-beta.2",
"chai": "^4.2.0", "chai": "^4.2.0",
@ -87,7 +87,9 @@
"mock-fs": "^4.10.1", "mock-fs": "^4.10.1",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"nyc": "^15.0.0", "nyc": "^15.0.0",
"patch-package": "^6.4.7",
"playwright": "^1.7.1", "playwright": "^1.7.1",
"postinstall-postinstall": "^2.1.0",
"prettier": "^2.0.5", "prettier": "^2.0.5",
"prettier-package-json": "^2.1.3", "prettier-package-json": "^2.1.3",
"remark-html": "^11.0.1", "remark-html": "^11.0.1",

View file

@ -1,3 +1,3 @@
const babelPlugin = require('./src/babelPluginExtendDocs'); const babelPlugin = require('./src/babelPluginExtendDocs.js');
module.exports = babelPlugin; module.exports = babelPlugin;

View file

@ -111,9 +111,10 @@ class PBoard extends DecorateMixin(LitElement) {
checked checked
@change="${({ target }) => { @change="${({ target }) => {
// TODO: of course, logic depending on dom is never a good idea // TODO: of course, logic depending on dom is never a good idea
const groupBoxes = target.parentElement.nextElementSibling.querySelectorAll( const groupBoxes =
'input[type=checkbox]', target.parentElement.nextElementSibling.querySelectorAll(
); 'input[type=checkbox]',
);
const { checked } = target; const { checked } = target;
Array.from(groupBoxes).forEach(box => { Array.from(groupBoxes).forEach(box => {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign

View file

@ -56,7 +56,7 @@ async function findMembersPerAstEntry(ast, fullCurrentFilePath, projectPath) {
// // Handle methods // // Handle methods
// const mBlacklistPlatform = ['constructor', 'connectedCallback', 'disconnectedCallback']; // const mBlacklistPlatform = ['constructor', 'connectedCallback', 'disconnectedCallback'];
// const mBlacklistLitEl = [ // const mBlacklistLitEl = [
// 'requestUpdateInternal', // 'requestUpdate',
// 'createRenderRoot', // 'createRenderRoot',
// 'render', // 'render',
// 'updated', // 'updated',

View file

@ -179,7 +179,7 @@
"accessType": "public" "accessType": "public"
}, },
{ {
"name": "requestUpdateInternal", "name": "requestUpdate",
"accessType": "protected" "accessType": "protected"
}, },
{ {

View file

@ -30,7 +30,7 @@ export class ExtendedComp extends MyCompMixin(RefClass) {
static get properties() {} static get properties() {}
static get styles() {} static get styles() {}
get updateComplete() {} get updateComplete() {}
requestUpdateInternal() {} requestUpdate() {}
createRenderRoot() {} createRenderRoot() {}
render() {} render() {}
updated() {} updated() {}

View file

@ -25,11 +25,8 @@ const promptAnalyzerModule = require('../../src/cli/prompt-analyzer-menu.js');
const { toPosixPath } = require('../../src/program/utils/to-posix-path.js'); const { toPosixPath } = require('../../src/program/utils/to-posix-path.js');
const { getExtendDocsResults } = require('../../src/cli/launch-providence-with-extend-docs.js'); const { getExtendDocsResults } = require('../../src/cli/launch-providence-with-extend-docs.js');
const { const { pathsArrayFromCs, pathsArrayFromCollectionName, appendProjectDependencyPaths } =
pathsArrayFromCs, cliHelpersModule;
pathsArrayFromCollectionName,
appendProjectDependencyPaths,
} = cliHelpersModule;
const queryResults = []; const queryResults = [];

View file

@ -213,7 +213,7 @@ describe('Analyzer "find-classes"', () => {
static get properties() {} static get properties() {}
static get styles() {} static get styles() {}
get updateComplete() {} get updateComplete() {}
requestUpdateInternal() {} requestUpdate() {}
createRenderRoot() {} createRenderRoot() {}
render() {} render() {}
updated() {} updated() {}

View file

@ -15,9 +15,8 @@ const {
restoreSuppressNonCriticalLogs, restoreSuppressNonCriticalLogs,
} = require('../../../test-helpers/mock-log-service-helpers.js'); } = require('../../../test-helpers/mock-log-service-helpers.js');
const findCustomelementsQueryConfig = QueryService.getQueryConfigFromAnalyzer( const findCustomelementsQueryConfig =
'find-customelements', QueryService.getQueryConfigFromAnalyzer('find-customelements');
);
const _providenceCfg = { const _providenceCfg = {
targetProjectPaths: ['/fictional/project'], // defined in mockProject targetProjectPaths: ['/fictional/project'], // defined in mockProject
}; };

8
packages-node/publish-docs/index.d.ts vendored Normal file
View file

@ -0,0 +1,8 @@
export { PublishDocs } from "./src/PublishDocs.js";
export type PublishDocsOptions = {
projectDir: string;
gitHubUrl: string;
gitRootDir: string;
copyDir: string;
copyTarget: string;
};

View file

@ -149,7 +149,8 @@ describe('remarkExtend', () => {
it('throws if an import file does not exist', async () => { it('throws if an import file does not exist', async () => {
await expectThrowsAsync(() => execute("```js ::import('./fixtures/not-available.md')\n```"), { await expectThrowsAsync(() => execute("```js ::import('./fixtures/not-available.md')\n```"), {
errorMatch: /The import "\.\/fixtures\/not-available.md" in "test-file.md" does not exist\. Resolved to ".*"\.$/, errorMatch:
/The import "\.\/fixtures\/not-available.md" in "test-file.md" does not exist\. Resolved to ".*"\.$/,
}); });
}); });
@ -157,7 +158,8 @@ describe('remarkExtend', () => {
const input = const input =
"```js ::import('./fixtures/three-sections-red.md', 'heading:has([value=Does not exit])')\n```"; "```js ::import('./fixtures/three-sections-red.md', 'heading:has([value=Does not exit])')\n```";
await expectThrowsAsync(() => execute(input), { await expectThrowsAsync(() => execute(input), {
errorMatch: /The start selector "heading:has\(\[value=Does not exit\]\)" could not find a matching node in ".*"\.$/, errorMatch:
/The start selector "heading:has\(\[value=Does not exit\]\)" could not find a matching node in ".*"\.$/,
}); });
}); });
@ -165,7 +167,8 @@ describe('remarkExtend', () => {
const input = const input =
"```js ::import('./fixtures/three-sections-red.md', 'heading:has([value=More Red])', 'heading:has([value=Does not exit])')\n```"; "```js ::import('./fixtures/three-sections-red.md', 'heading:has([value=More Red])', 'heading:has([value=Does not exit])')\n```";
await expectThrowsAsync(() => execute(input), { await expectThrowsAsync(() => execute(input), {
errorMatch: /The end selector "heading:has\(\[value=Does not exit\]\)" could not find a matching node in ".*"\./, errorMatch:
/The end selector "heading:has\(\[value=Does not exit\]\)" could not find a matching node in ".*"\./,
}); });
}); });

View file

@ -226,12 +226,12 @@ export class LionAccordion extends LitElement {
* @private * @private
*/ */
__setupStore() { __setupStore() {
const invokers = /** @type {HTMLElement[]} */ (Array.from( const invokers = /** @type {HTMLElement[]} */ (
this.querySelectorAll('[slot="invoker"]'), Array.from(this.querySelectorAll('[slot="invoker"]'))
)); );
const contents = /** @type {HTMLElement[]} */ (Array.from( const contents = /** @type {HTMLElement[]} */ (
this.querySelectorAll('[slot="content"]'), Array.from(this.querySelectorAll('[slot="content"]'))
)); );
if (invokers.length !== contents.length) { if (invokers.length !== contents.length) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.warn( console.warn(
@ -356,9 +356,11 @@ export class LionAccordion extends LitElement {
if (!(this.__store && this.__store[this.focusedIndex])) { if (!(this.__store && this.__store[this.focusedIndex])) {
return; return;
} }
const previousInvoker = /** @type {HTMLElement | undefined} */ (Array.from(this.children).find( const previousInvoker = /** @type {HTMLElement | undefined} */ (
child => child.slot === 'invoker' && child.firstElementChild?.hasAttribute('focused'), Array.from(this.children).find(
)); child => child.slot === 'invoker' && child.firstElementChild?.hasAttribute('focused'),
)
);
if (previousInvoker) { if (previousInvoker) {
unfocusInvoker(previousInvoker); unfocusInvoker(previousInvoker);
} }

View file

@ -1,4 +1,5 @@
import { expect, fixture, html } from '@open-wc/testing'; import { expect, fixture } from '@open-wc/testing';
import { html } from 'lit/static-html.js';
import sinon from 'sinon'; import sinon from 'sinon';
import '../lion-accordion.js'; import '../lion-accordion.js';
@ -25,14 +26,16 @@ describe('<lion-accordion>', () => {
}); });
it('can programmatically set expanded', async () => { it('can programmatically set expanded', async () => {
const el = /** @type {LionAccordion} */ (await fixture(html` const el = /** @type {LionAccordion} */ (
<lion-accordion .expanded=${[1]}> await fixture(html`
<h2 slot="invoker"><button>invoker 1</button></h2> <lion-accordion .expanded=${[1]}>
<div slot="content">content 1</div> <h2 slot="invoker"><button>invoker 1</button></h2>
<h2 slot="invoker"><button>invoker 2</button></h2> <div slot="content">content 1</div>
<div slot="content">content 2</div> <h2 slot="invoker"><button>invoker 2</button></h2>
</lion-accordion> <div slot="content">content 2</div>
`)); </lion-accordion>
`)
);
expect(el.expanded).to.deep.equal([1]); expect(el.expanded).to.deep.equal([1]);
expect( expect(
Array.from(el.children).find( Array.from(el.children).find(
@ -103,14 +106,16 @@ describe('<lion-accordion>', () => {
}); });
it('can programmatically set focusedIndex', async () => { it('can programmatically set focusedIndex', async () => {
const el = /** @type {LionAccordion} */ (await fixture(html` const el = /** @type {LionAccordion} */ (
<lion-accordion .focusedIndex=${1}> await fixture(html`
<h2 slot="invoker"><button>invoker 1</button></h2> <lion-accordion .focusedIndex=${1}>
<div slot="content">content 1</div> <h2 slot="invoker"><button>invoker 1</button></h2>
<h2 slot="invoker"><button>invoker 2</button></h2> <div slot="content">content 1</div>
<div slot="content">content 2</div> <h2 slot="invoker"><button>invoker 2</button></h2>
</lion-accordion> <div slot="content">content 2</div>
`)); </lion-accordion>
`)
);
expect(el.focusedIndex).to.equal(1); expect(el.focusedIndex).to.equal(1);
expect( expect(
Array.from(el.children).find( Array.from(el.children).find(
@ -214,16 +219,18 @@ describe('<lion-accordion>', () => {
}); });
it('selects previous invoker on [arrow-left] and [arrow-up]', async () => { it('selects previous invoker on [arrow-left] and [arrow-up]', async () => {
const el = /** @type {LionAccordion} */ (await fixture(html` const el = /** @type {LionAccordion} */ (
<lion-accordion .focusedIndex=${1}> await fixture(html`
<h2 slot="invoker"><button>invoker 1</button></h2> <lion-accordion .focusedIndex=${1}>
<div slot="content">content 1</div> <h2 slot="invoker"><button>invoker 1</button></h2>
<h2 slot="invoker"><button>invoker 2</button></h2> <div slot="content">content 1</div>
<div slot="content">content 2</div> <h2 slot="invoker"><button>invoker 2</button></h2>
<h2 slot="invoker"><button>invoker 3</button></h2> <div slot="content">content 2</div>
<div slot="content">content 3</div> <h2 slot="invoker"><button>invoker 3</button></h2>
</lion-accordion> <div slot="content">content 3</div>
`)); </lion-accordion>
`)
);
const invokers = el.querySelectorAll('[slot=invoker]'); const invokers = el.querySelectorAll('[slot=invoker]');
el.focusedIndex = 2; el.focusedIndex = 2;
invokers[2].firstElementChild?.dispatchEvent( invokers[2].firstElementChild?.dispatchEvent(
@ -237,14 +244,16 @@ describe('<lion-accordion>', () => {
}); });
it('selects first invoker on [home]', async () => { it('selects first invoker on [home]', async () => {
const el = /** @type {LionAccordion} */ (await fixture(html` const el = /** @type {LionAccordion} */ (
<lion-accordion .focusedIndex=${1}> await fixture(html`
<h2 slot="invoker"><button>invoker 1</button></h2> <lion-accordion .focusedIndex=${1}>
<div slot="content">content 1</div> <h2 slot="invoker"><button>invoker 1</button></h2>
<h2 slot="invoker"><button>invoker 2</button></h2> <div slot="content">content 1</div>
<div slot="content">content 2</div> <h2 slot="invoker"><button>invoker 2</button></h2>
</lion-accordion> <div slot="content">content 2</div>
`)); </lion-accordion>
`)
);
const invokers = el.querySelectorAll('[slot=invoker]'); const invokers = el.querySelectorAll('[slot=invoker]');
invokers[1].firstElementChild?.dispatchEvent(new KeyboardEvent('keydown', { key: 'Home' })); invokers[1].firstElementChild?.dispatchEvent(new KeyboardEvent('keydown', { key: 'Home' }));
expect(el.focusedIndex).to.equal(0); expect(el.focusedIndex).to.equal(0);
@ -258,16 +267,18 @@ describe('<lion-accordion>', () => {
}); });
it('stays on last invoker on [arrow-right]', async () => { it('stays on last invoker on [arrow-right]', async () => {
const el = /** @type {LionAccordion} */ (await fixture(html` const el = /** @type {LionAccordion} */ (
<lion-accordion focusedIndex="2"> await fixture(html`
<h2 slot="invoker"><button>invoker 1</button></h2> <lion-accordion focusedIndex="2">
<div slot="content">content 1</div> <h2 slot="invoker"><button>invoker 1</button></h2>
<h2 slot="invoker"><button>invoker 2</button></h2> <div slot="content">content 1</div>
<div slot="content">content 2</div> <h2 slot="invoker"><button>invoker 2</button></h2>
<h2 slot="invoker"><button>invoker 3</button></h2> <div slot="content">content 2</div>
<div slot="content">content 3</div> <h2 slot="invoker"><button>invoker 3</button></h2>
</lion-accordion> <div slot="content">content 3</div>
`)); </lion-accordion>
`)
);
const invokers = el.querySelectorAll('[slot=invoker]'); const invokers = el.querySelectorAll('[slot=invoker]');
invokers[2].firstElementChild?.dispatchEvent( invokers[2].firstElementChild?.dispatchEvent(
new KeyboardEvent('keydown', { key: 'ArrowRight' }), new KeyboardEvent('keydown', { key: 'ArrowRight' }),
@ -276,16 +287,18 @@ describe('<lion-accordion>', () => {
}); });
it('stays on first invoker on [arrow-left]', async () => { it('stays on first invoker on [arrow-left]', async () => {
const el = /** @type {LionAccordion} */ (await fixture(html` const el = /** @type {LionAccordion} */ (
<lion-accordion> await fixture(html`
<h2 slot="invoker"><button>invoker 1</button></h2> <lion-accordion>
<div slot="content">content 1</div> <h2 slot="invoker"><button>invoker 1</button></h2>
<h2 slot="invoker"><button>invoker 2</button></h2> <div slot="content">content 1</div>
<div slot="content">content 2</div> <h2 slot="invoker"><button>invoker 2</button></h2>
<h2 slot="invoker"><button>invoker 3</button></h2> <div slot="content">content 2</div>
<div slot="content">content 3</div> <h2 slot="invoker"><button>invoker 3</button></h2>
</lion-accordion> <div slot="content">content 3</div>
`)); </lion-accordion>
`)
);
const invokers = el.querySelectorAll('[slot=invoker]'); const invokers = el.querySelectorAll('[slot=invoker]');
invokers[0].firstElementChild?.dispatchEvent( invokers[0].firstElementChild?.dispatchEvent(
new KeyboardEvent('keydown', { key: 'ArrowLeft' }), new KeyboardEvent('keydown', { key: 'ArrowLeft' }),
@ -338,12 +351,12 @@ describe('<lion-accordion>', () => {
el.append(content); el.append(content);
} }
await el.updateComplete; await el.updateComplete;
const invokers = /** @type {HTMLElement[]} */ (Array.from( const invokers = /** @type {HTMLElement[]} */ (
el.querySelectorAll('[slot=invoker]'), Array.from(el.querySelectorAll('[slot=invoker]'))
)); );
const contents = /** @type {HTMLElement[]} */ (Array.from( const contents = /** @type {HTMLElement[]} */ (
el.querySelectorAll('[slot=content]'), Array.from(el.querySelectorAll('[slot=content]'))
)); );
invokers.forEach((invoker, index) => { invokers.forEach((invoker, index) => {
const content = contents[index]; const content = contents[index];
expect(invoker.style.getPropertyValue('order')).to.equal(`${index + 1}`); expect(invoker.style.getPropertyValue('order')).to.equal(`${index + 1}`);
@ -403,12 +416,14 @@ describe('<lion-accordion>', () => {
}); });
it('adds aria-expanded="true" to invoker when its content is expanded', async () => { it('adds aria-expanded="true" to invoker when its content is expanded', async () => {
const el = /** @type {LionAccordion} */ (await fixture(html` const el = /** @type {LionAccordion} */ (
<lion-accordion> await fixture(html`
<h2 slot="invoker"><button>invoker</button></h2> <lion-accordion>
<div slot="content">content</div> <h2 slot="invoker"><button>invoker</button></h2>
</lion-accordion> <div slot="content">content</div>
`)); </lion-accordion>
`)
);
el.expanded = [0]; el.expanded = [0];
expect( expect(
Array.from(el.children).find(child => child.slot === 'invoker')?.firstElementChild, Array.from(el.children).find(child => child.slot === 'invoker')?.firstElementChild,

View file

@ -162,9 +162,9 @@ export class LionButton extends DisabledWithTabIndexMixin(SlotMixin(LitElement))
* @protected * @protected
*/ */
get _nativeButtonNode() { get _nativeButtonNode() {
return /** @type {HTMLButtonElement} */ (Array.from(this.children).find( return /** @type {HTMLButtonElement} */ (
child => child.slot === '_button', Array.from(this.children).find(child => child.slot === '_button')
)); );
} }
get slots() { get slots() {

View file

@ -1,6 +1,7 @@
/* eslint-disable lit-a11y/click-events-have-key-events */ /* eslint-disable lit-a11y/click-events-have-key-events */
import { browserDetection } from '@lion/core'; import { browserDetection } from '@lion/core';
import { aTimeout, expect, fixture, html, oneEvent, unsafeStatic } from '@open-wc/testing'; import { aTimeout, expect, fixture, oneEvent } from '@open-wc/testing';
import { unsafeStatic, html } from 'lit/static-html.js';
import sinon from 'sinon'; import sinon from 'sinon';
import '@lion/core/differentKeyEventNamesShimIE'; import '@lion/core/differentKeyEventNamesShimIE';
import '@lion/button/define'; import '@lion/button/define';
@ -37,9 +38,9 @@ describe('lion-button', () => {
}); });
it('sync type down to the native button', async () => { it('sync type down to the native button', async () => {
const el = /** @type {LionButton} */ (await fixture( const el = /** @type {LionButton} */ (
`<lion-button type="button">foo</lion-button>`, await fixture(`<lion-button type="button">foo</lion-button>`)
)); );
const { nativeButtonNode } = getProtectedMembers(el); const { nativeButtonNode } = getProtectedMembers(el);
expect(el.type).to.equal('button'); expect(el.type).to.equal('button');
@ -175,9 +176,9 @@ describe('lion-button', () => {
}); });
it('does not override user provided role', async () => { it('does not override user provided role', async () => {
const el = /** @type {LionButton} */ (await fixture( const el = /** @type {LionButton} */ (
`<lion-button role="foo">foo</lion-button>`, await fixture(`<lion-button role="foo">foo</lion-button>`)
)); );
expect(el.getAttribute('role')).to.equal('foo'); expect(el.getAttribute('role')).to.equal('foo');
}); });
@ -187,9 +188,9 @@ describe('lion-button', () => {
}); });
it('has a tabindex="-1" when disabled', async () => { it('has a tabindex="-1" when disabled', async () => {
const el = /** @type {LionButton} */ (await fixture( const el = /** @type {LionButton} */ (
`<lion-button disabled>foo</lion-button>`, await fixture(`<lion-button disabled>foo</lion-button>`)
)); );
expect(el.getAttribute('tabindex')).to.equal('-1'); expect(el.getAttribute('tabindex')).to.equal('-1');
el.disabled = false; el.disabled = false;
await el.updateComplete; await el.updateComplete;
@ -200,16 +201,16 @@ describe('lion-button', () => {
}); });
it('does not override user provided tabindex', async () => { it('does not override user provided tabindex', async () => {
const el = /** @type {LionButton} */ (await fixture( const el = /** @type {LionButton} */ (
`<lion-button tabindex="5">foo</lion-button>`, await fixture(`<lion-button tabindex="5">foo</lion-button>`)
)); );
expect(el.getAttribute('tabindex')).to.equal('5'); expect(el.getAttribute('tabindex')).to.equal('5');
}); });
it('disabled does not override user provided tabindex', async () => { it('disabled does not override user provided tabindex', async () => {
const el = /** @type {LionButton} */ (await fixture( const el = /** @type {LionButton} */ (
`<lion-button tabindex="5" disabled>foo</lion-button>`, await fixture(`<lion-button tabindex="5" disabled>foo</lion-button>`)
)); );
expect(el.getAttribute('tabindex')).to.equal('-1'); expect(el.getAttribute('tabindex')).to.equal('-1');
el.disabled = false; el.disabled = false;
await el.updateComplete; await el.updateComplete;
@ -230,9 +231,9 @@ describe('lion-button', () => {
it('does not override aria-labelledby when provided by user', async () => { it('does not override aria-labelledby when provided by user', async () => {
const browserDetectionStub = sinon.stub(browserDetection, 'isIE11').value(true); const browserDetectionStub = sinon.stub(browserDetection, 'isIE11').value(true);
const el = /** @type {LionButton} */ (await fixture( const el = /** @type {LionButton} */ (
`<lion-button aria-labelledby="some-id another-id">foo</lion-button>`, await fixture(`<lion-button aria-labelledby="some-id another-id">foo</lion-button>`)
)); );
expect(el.getAttribute('aria-labelledby')).to.equal('some-id another-id'); expect(el.getAttribute('aria-labelledby')).to.equal('some-id another-id');
browserDetectionStub.restore(); browserDetectionStub.restore();
}); });
@ -244,15 +245,17 @@ describe('lion-button', () => {
expect(nativeButtonNode.getAttribute('aria-hidden')).to.equal('true'); expect(nativeButtonNode.getAttribute('aria-hidden')).to.equal('true');
}); });
it('is accessible', async () => { // TODO: enable when native button is not a child anymore
it.skip('is accessible', async () => {
const el = /** @type {LionButton} */ (await fixture(`<lion-button>foo</lion-button>`)); const el = /** @type {LionButton} */ (await fixture(`<lion-button>foo</lion-button>`));
await expect(el).to.be.accessible(); await expect(el).to.be.accessible();
}); });
it('is accessible when disabled', async () => { // TODO: enable when native button is not a child anymore
const el = /** @type {LionButton} */ (await fixture( it.skip('is accessible when disabled', async () => {
`<lion-button disabled>foo</lion-button>`, const el = /** @type {LionButton} */ (
)); await fixture(`<lion-button disabled>foo</lion-button>`)
);
await expect(el).to.be.accessible({ ignoredRules: ['color-contrast'] }); await expect(el).to.be.accessible({ ignoredRules: ['color-contrast'] });
}); });
}); });
@ -266,9 +269,9 @@ describe('lion-button', () => {
<lion-button type="submit">foo</lion-button> <lion-button type="submit">foo</lion-button>
</form> </form>
`); `);
const button /** @type {LionButton} */ = /** @type {LionButton} */ (form.querySelector( const button /** @type {LionButton} */ = /** @type {LionButton} */ (
'lion-button', form.querySelector('lion-button')
)); );
button.click(); button.click();
expect(formSubmitSpy).to.have.been.calledOnce; expect(formSubmitSpy).to.have.been.calledOnce;
}); });
@ -280,9 +283,9 @@ describe('lion-button', () => {
<lion-button type="submit">foo</lion-button> <lion-button type="submit">foo</lion-button>
</form> </form>
`); `);
const button /** @type {LionButton} */ = /** @type {LionButton} */ (form.querySelector( const button /** @type {LionButton} */ = /** @type {LionButton} */ (
'lion-button', form.querySelector('lion-button')
)); );
button.dispatchEvent(new KeyboardEvent('keyup', { key: ' ' })); button.dispatchEvent(new KeyboardEvent('keyup', { key: ' ' }));
await aTimeout(0); await aTimeout(0);
await aTimeout(0); await aTimeout(0);
@ -313,15 +316,15 @@ describe('lion-button', () => {
<lion-button type="reset">reset</lion-button> <lion-button type="reset">reset</lion-button>
</form> </form>
`); `);
const btn /** @type {LionButton} */ = /** @type {LionButton} */ (form.querySelector( const btn /** @type {LionButton} */ = /** @type {LionButton} */ (
'lion-button', form.querySelector('lion-button')
)); );
const firstName = /** @type {HTMLInputElement} */ (form.querySelector( const firstName = /** @type {HTMLInputElement} */ (
'input[name=firstName]', form.querySelector('input[name=firstName]')
)); );
const lastName = /** @type {HTMLInputElement} */ (form.querySelector( const lastName = /** @type {HTMLInputElement} */ (
'input[name=lastName]', form.querySelector('input[name=lastName]')
)); );
firstName.value = 'Foo'; firstName.value = 'Foo';
lastName.value = 'Bar'; lastName.value = 'Bar';
@ -435,9 +438,9 @@ describe('lion-button', () => {
it('is fired once', async () => { it('is fired once', async () => {
const clickSpy = /** @type {EventListener} */ (sinon.spy()); const clickSpy = /** @type {EventListener} */ (sinon.spy());
const el = /** @type {LionButton} */ (await fixture( const el = /** @type {LionButton} */ (
html` <lion-button @click="${clickSpy}">foo</lion-button> `, await fixture(html` <lion-button @click="${clickSpy}">foo</lion-button> `)
)); );
el.click(); el.click();
@ -454,17 +457,19 @@ describe('lion-button', () => {
const formSpyEarly = /** @type {EventListener} */ (sinon.spy(e => e.preventDefault())); const formSpyEarly = /** @type {EventListener} */ (sinon.spy(e => e.preventDefault()));
const formSpyLater = /** @type {EventListener} */ (sinon.spy(e => e.preventDefault())); const formSpyLater = /** @type {EventListener} */ (sinon.spy(e => e.preventDefault()));
const el = /** @type {HTMLDivElement} */ (await fixture( const el = /** @type {HTMLDivElement} */ (
html` await fixture(
<div @click="${outsideSpy}"> html`
<form @click="${formSpyEarly}"> <div @click="${outsideSpy}">
<div @click="${insideSpy}"> <form @click="${formSpyEarly}">
<lion-button>foo</lion-button> <div @click="${insideSpy}">
</div> <lion-button>foo</lion-button>
</form> </div>
</div> </form>
`, </div>
)); `,
)
);
const lionButton = /** @type {LionButton} */ (el.querySelector('lion-button')); const lionButton = /** @type {LionButton} */ (el.querySelector('lion-button'));
const form = /** @type {HTMLFormElement} */ (el.querySelector('form')); const form = /** @type {HTMLFormElement} */ (el.querySelector('form'));
form.addEventListener('click', formSpyLater); form.addEventListener('click', formSpyLater);
@ -482,13 +487,15 @@ describe('lion-button', () => {
}); });
it('works when connected to different form', async () => { it('works when connected to different form', async () => {
const form1El = /** @type {HTMLFormElement} */ (await fixture( const form1El = /** @type {HTMLFormElement} */ (
html` await fixture(
<form> html`
<lion-button>foo</lion-button> <form>
</form> <lion-button>foo</lion-button>
`, </form>
)); `,
)
);
const lionButton = /** @type {LionButton} */ (form1El.querySelector('lion-button')); const lionButton = /** @type {LionButton} */ (form1El.querySelector('lion-button'));
expect(lionButton._form).to.equal(form1El); expect(lionButton._form).to.equal(form1El);
@ -500,15 +507,17 @@ describe('lion-button', () => {
const formSpyEarly = /** @type {EventListener} */ (sinon.spy(e => e.preventDefault())); const formSpyEarly = /** @type {EventListener} */ (sinon.spy(e => e.preventDefault()));
const formSpyLater = /** @type {EventListener} */ (sinon.spy(e => e.preventDefault())); const formSpyLater = /** @type {EventListener} */ (sinon.spy(e => e.preventDefault()));
const form2El = /** @type {HTMLFormElement} */ (await fixture( const form2El = /** @type {HTMLFormElement} */ (
html` await fixture(
<div @click="${outsideSpy}"> html`
<form @click="${formSpyEarly}"> <div @click="${outsideSpy}">
<div @click="${insideSpy}">${lionButton}</div> <form @click="${formSpyEarly}">
</form> <div @click="${insideSpy}">${lionButton}</div>
</div> </form>
`, </div>
)); `,
)
);
const form2Node = /** @type {HTMLFormElement} */ (form2El.querySelector('form')); const form2Node = /** @type {HTMLFormElement} */ (form2El.querySelector('form'));
expect(lionButton._form).to.equal(form2Node); expect(lionButton._form).to.equal(form2Node);
@ -534,9 +543,9 @@ describe('lion-button', () => {
before(async () => { before(async () => {
const nativeButtonEl = /** @type {LionButton} */ (await fixture('<button>foo</button>')); const nativeButtonEl = /** @type {LionButton} */ (await fixture('<button>foo</button>'));
const lionButtonEl = /** @type {LionButton} */ (await fixture( const lionButtonEl = /** @type {LionButton} */ (
'<lion-button>foo</lion-button>', await fixture('<lion-button>foo</lion-button>')
)); );
nativeButtonEvent = await prepareClickEvent(nativeButtonEl); nativeButtonEvent = await prepareClickEvent(nativeButtonEl);
lionButtonEvent = await prepareClickEvent(lionButtonEl); lionButtonEvent = await prepareClickEvent(lionButtonEl);
}); });
@ -578,9 +587,9 @@ describe('lion-button', () => {
const targetName = 'host'; const targetName = 'host';
it(`is ${targetName} with type ${type} and it is inside a ${container}`, async () => { it(`is ${targetName} with type ${type} and it is inside a ${container}`, async () => {
const clickSpy = /** @type {EventListener} */ (sinon.spy(e => e.preventDefault())); const clickSpy = /** @type {EventListener} */ (sinon.spy(e => e.preventDefault()));
const el = /** @type {LionButton} */ (await fixture( const el = /** @type {LionButton} */ (
`<lion-button type="${type}">foo</lion-button>`, await fixture(`<lion-button type="${type}">foo</lion-button>`)
)); );
const tag = unsafeStatic(container); const tag = unsafeStatic(container);
await fixture(html`<${tag} @click="${clickSpy}">${el}</${tag}>`); await fixture(html`<${tag} @click="${clickSpy}">${el}</${tag}>`);
const event = await prepareClickEvent(el); const event = await prepareClickEvent(el);

2
packages/calendar/index.d.ts vendored Normal file
View file

@ -0,0 +1,2 @@
export { isSameDate } from "./src/utils/isSameDate.js";
export { LionCalendar } from "./src/LionCalendar.js";

View file

@ -1,3 +1,4 @@
/* eslint-disable import/no-extraneous-dependencies */
import { html, LitElement } from '@lion/core'; import { html, LitElement } from '@lion/core';
import { import {
getMonthNames, getMonthNames,
@ -224,9 +225,9 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
} }
focusCentralDate() { focusCentralDate() {
const button = /** @type {HTMLElement} */ (this.shadowRoot?.querySelector( const button = /** @type {HTMLElement} */ (
'button[tabindex="0"]', this.shadowRoot?.querySelector('button[tabindex="0"]')
)); );
button.focus(); button.focus();
this.__focusedDate = this.centralDate; this.__focusedDate = this.centralDate;
} }
@ -267,9 +268,9 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
* we can guard against adding events twice * we can guard against adding events twice
*/ */
if (!this.__eventsAdded) { if (!this.__eventsAdded) {
this.__contentWrapperElement = /** @type {HTMLButtonElement} */ (this.shadowRoot?.getElementById( this.__contentWrapperElement = /** @type {HTMLButtonElement} */ (
'js-content-wrapper', this.shadowRoot?.getElementById('js-content-wrapper')
)); );
this.__contentWrapperElement.addEventListener('click', this.__boundClickDateDelegation); this.__contentWrapperElement.addEventListener('click', this.__boundClickDateDelegation);
this.__contentWrapperElement.addEventListener('focus', this.__boundFocusDateDelegation); this.__contentWrapperElement.addEventListener('focus', this.__boundFocusDateDelegation);
this.__contentWrapperElement.addEventListener('blur', this.__boundBlurDateDelegation); this.__contentWrapperElement.addEventListener('blur', this.__boundBlurDateDelegation);
@ -305,8 +306,8 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
* @param {string} name * @param {string} name
* @param {?} oldValue * @param {?} oldValue
*/ */
requestUpdateInternal(name, oldValue) { requestUpdate(name, oldValue) {
super.requestUpdateInternal(name, oldValue); super.requestUpdate(name, oldValue);
const map = { const map = {
disableDates: () => this.__disableDatesChanged(), disableDates: () => this.__disableDatesChanged(),
@ -740,8 +741,9 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
!this.__focusedDate && !this.__focusedDate &&
isDayButton(/** @type {HTMLElement} el */ (this.shadowRoot?.activeElement)) isDayButton(/** @type {HTMLElement} el */ (this.shadowRoot?.activeElement))
) { ) {
this.__focusedDate = /** @type {HTMLButtonElement & { date: Date }} */ (this.shadowRoot this.__focusedDate = /** @type {HTMLButtonElement & { date: Date }} */ (
?.activeElement).date; this.shadowRoot?.activeElement
).date;
} }
} }

View file

@ -59,10 +59,16 @@ export function dayTemplate(day, { weekdays, monthsLabels = defaultMonthLabels }
<button <button
.date=${day.date} .date=${day.date}
class="calendar__day-button" class="calendar__day-button"
tabindex=${ifDefined(day.tabindex)} tabindex=${ifDefined(Number(day.tabindex))}
aria-label=${`${dayNumber} ${monthName} ${year} ${weekdayName}`} aria-label=${`${dayNumber} ${monthName} ${year} ${weekdayName}`}
aria-pressed=${ifDefined(day.ariaPressed)} aria-pressed=${
aria-current=${ifDefined(day.ariaCurrent)} /** @type {'true'|'false'|'mixed'|'undefined'} */ (ifDefined(day.ariaPressed))
}
aria-current=${
/** @type {'page'|'step'|'location'|'date'|'time'|'true'|'false'} */ (
ifDefined(day.ariaCurrent)
)
}
?disabled=${day.disabled} ?disabled=${day.disabled}
?selected=${day.selected} ?selected=${day.selected}
?past=${day.past} ?past=${day.past}

View file

@ -33,15 +33,15 @@ export class CalendarObject {
} }
get nextYearButtonEl() { get nextYearButtonEl() {
return /** @type {HTMLElement & { ariaLabel: string }} */ (this.el.shadowRoot?.querySelectorAll( return /** @type {HTMLElement & { ariaLabel: string }} */ (
'.calendar__next-button', this.el.shadowRoot?.querySelectorAll('.calendar__next-button')[0]
)[0]); );
} }
get previousYearButtonEl() { get previousYearButtonEl() {
return /** @type {HTMLElement & { ariaLabel: string }} */ (this.el.shadowRoot?.querySelectorAll( return /** @type {HTMLElement & { ariaLabel: string }} */ (
'.calendar__previous-button', this.el.shadowRoot?.querySelectorAll('.calendar__previous-button')[0]
)[0]); );
} }
get nextMonthButtonEl() { get nextMonthButtonEl() {
@ -57,33 +57,43 @@ export class CalendarObject {
} }
get weekdayHeaderEls() { get weekdayHeaderEls() {
return /** @type {HTMLElement[]} */ (Array.from( return /** @type {HTMLElement[]} */ (
/** @type {ShadowRoot} */ (this.el.shadowRoot).querySelectorAll('.calendar__weekday-header'), Array.from(
)); /** @type {ShadowRoot} */ (this.el.shadowRoot).querySelectorAll(
'.calendar__weekday-header',
),
)
);
} }
get dayEls() { get dayEls() {
return /** @type {HTMLElement[]} */ (Array.from( return /** @type {HTMLElement[]} */ (
/** @type {ShadowRoot} */ (this.el.shadowRoot).querySelectorAll( Array.from(
'.calendar__day-button[current-month]', /** @type {ShadowRoot} */ (this.el.shadowRoot).querySelectorAll(
), '.calendar__day-button[current-month]',
)); ),
)
);
} }
get previousMonthDayEls() { get previousMonthDayEls() {
return /** @type {HTMLElement[]} */ (Array.from( return /** @type {HTMLElement[]} */ (
/** @type {ShadowRoot} */ (this.el.shadowRoot).querySelectorAll( Array.from(
'.calendar__day-button[previous-month]', /** @type {ShadowRoot} */ (this.el.shadowRoot).querySelectorAll(
), '.calendar__day-button[previous-month]',
)); ),
)
);
} }
get nextMonthDayEls() { get nextMonthDayEls() {
return /** @type {HTMLElement[]} */ (Array.from( return /** @type {HTMLElement[]} */ (
/** @type {ShadowRoot} */ (this.el.shadowRoot).querySelectorAll( Array.from(
'.calendar__day-button[next-month]', /** @type {ShadowRoot} */ (this.el.shadowRoot).querySelectorAll(
), '.calendar__day-button[next-month]',
)); ),
)
);
} }
get dayObjs() { get dayObjs() {
@ -103,9 +113,11 @@ export class CalendarObject {
*/ */
getDayEl(monthDayNumber) { getDayEl(monthDayNumber) {
// Relies on the fact that empty cells don't have .calendar__day-button[current-month] // Relies on the fact that empty cells don't have .calendar__day-button[current-month]
return /** @type {HTMLElement} */ (this.el.shadowRoot?.querySelectorAll( return /** @type {HTMLElement} */ (
'.calendar__day-button[current-month]', this.el.shadowRoot?.querySelectorAll('.calendar__day-button[current-month]')[
)[monthDayNumber - 1]); monthDayNumber - 1
]
);
} }
/** /**

View file

@ -813,12 +813,10 @@ describe('<lion-calendar>', () => {
`); `);
const elObj = new CalendarObject(el); const elObj = new CalendarObject(el);
expect( expect(
elObj.checkForAllDayObjs(/** @param {DayObject} d */ d => d.el.hasAttribute('disabled'), [ elObj.checkForAllDayObjs(
1, /** @param {DayObject} d */ d => d.el.hasAttribute('disabled'),
2, [1, 2, 30, 31],
30, ),
31,
]),
).to.equal(true); ).to.equal(true);
clock.restore(); clock.restore();

View file

@ -11,9 +11,8 @@ function compareMultipleMonth(obj) {
week.days.forEach((day, dayi) => { week.days.forEach((day, dayi) => {
// @ts-expect-error since we are converting Date to ISO string, but that's okay for our test Date comparisons // @ts-expect-error since we are converting Date to ISO string, but that's okay for our test Date comparisons
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
obj.months[monthi].weeks[weeki].days[dayi].date = obj.months[monthi].weeks[weeki].days[ obj.months[monthi].weeks[weeki].days[dayi].date =
dayi obj.months[monthi].weeks[weeki].days[dayi].date.toISOString();
].date.toISOString();
}); });
}); });
}); });

3
packages/checkbox-group/index.d.ts vendored Normal file
View file

@ -0,0 +1,3 @@
export { LionCheckboxGroup } from "./src/LionCheckboxGroup.js";
export { LionCheckboxIndeterminate } from "./src/LionCheckboxIndeterminate.js";
export { LionCheckbox } from "./src/LionCheckbox.js";

View file

@ -1,5 +1,6 @@
import { localizeTearDown } from '@lion/localize/test-helpers'; import { localizeTearDown } from '@lion/localize/test-helpers';
import { expect, fixture as _fixture, html } from '@open-wc/testing'; import { expect, fixture as _fixture } from '@open-wc/testing';
import { html } from 'lit/static-html.js';
import '@lion/checkbox-group/define'; import '@lion/checkbox-group/define';
/** /**

View file

@ -1,4 +1,5 @@
import { expect, fixture, html } from '@open-wc/testing'; import { expect, fixture } from '@open-wc/testing';
import { html } from 'lit/static-html.js';
import { getFormControlMembers } from '@lion/form-core/test-helpers'; import { getFormControlMembers } from '@lion/form-core/test-helpers';
import '@lion/checkbox-group/define'; import '@lion/checkbox-group/define';
@ -46,9 +47,9 @@ describe('<lion-checkbox-indeterminate>', () => {
</lion-checkbox-indeterminate> </lion-checkbox-indeterminate>
</lion-checkbox-group> </lion-checkbox-group>
`); `);
const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector( const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (
'lion-checkbox-indeterminate', el.querySelector('lion-checkbox-indeterminate')
)); );
// Assert // Assert
expect(elIndeterminate?.hasAttribute('indeterminate')).to.be.false; expect(elIndeterminate?.hasAttribute('indeterminate')).to.be.false;
@ -65,9 +66,9 @@ describe('<lion-checkbox-indeterminate>', () => {
</lion-checkbox-indeterminate> </lion-checkbox-indeterminate>
</lion-checkbox-group> </lion-checkbox-group>
`); `);
const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector( const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (
'lion-checkbox-indeterminate', el.querySelector('lion-checkbox-indeterminate')
)); );
// Assert // Assert
expect(elIndeterminate?.hasAttribute('indeterminate')).to.be.true; expect(elIndeterminate?.hasAttribute('indeterminate')).to.be.true;
@ -75,18 +76,20 @@ describe('<lion-checkbox-indeterminate>', () => {
it('should be checked if all children are checked', async () => { it('should be checked if all children are checked', async () => {
// Arrange // Arrange
const el = /** @type {LionCheckboxGroup} */ (await fixture(html` const el = /** @type {LionCheckboxGroup} */ (
<lion-checkbox-group name="scientists[]"> await fixture(html`
<lion-checkbox-indeterminate label="Favorite scientists"> <lion-checkbox-group name="scientists[]">
<lion-checkbox slot="checkbox" label="Archimedes" checked></lion-checkbox> <lion-checkbox-indeterminate label="Favorite scientists">
<lion-checkbox slot="checkbox" label="Francis Bacon" checked></lion-checkbox> <lion-checkbox slot="checkbox" label="Archimedes" checked></lion-checkbox>
<lion-checkbox slot="checkbox" label="Marie Curie" checked></lion-checkbox> <lion-checkbox slot="checkbox" label="Francis Bacon" checked></lion-checkbox>
</lion-checkbox-indeterminate> <lion-checkbox slot="checkbox" label="Marie Curie" checked></lion-checkbox>
</lion-checkbox-group> </lion-checkbox-indeterminate>
`)); </lion-checkbox-group>
const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector( `)
'lion-checkbox-indeterminate', );
)); const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (
el.querySelector('lion-checkbox-indeterminate')
);
// Assert // Assert
expect(elIndeterminate?.hasAttribute('indeterminate')).to.be.false; expect(elIndeterminate?.hasAttribute('indeterminate')).to.be.false;
@ -95,18 +98,20 @@ describe('<lion-checkbox-indeterminate>', () => {
it('should become indeterminate if one child is checked', async () => { it('should become indeterminate if one child is checked', async () => {
// Arrange // Arrange
const el = /** @type {LionCheckboxGroup} */ (await fixture(html` const el = /** @type {LionCheckboxGroup} */ (
<lion-checkbox-group name="scientists[]"> await fixture(html`
<lion-checkbox-indeterminate label="Favorite scientists"> <lion-checkbox-group name="scientists[]">
<lion-checkbox slot="checkbox" label="Archimedes"></lion-checkbox> <lion-checkbox-indeterminate label="Favorite scientists">
<lion-checkbox slot="checkbox" label="Francis Bacon"></lion-checkbox> <lion-checkbox slot="checkbox" label="Archimedes"></lion-checkbox>
<lion-checkbox slot="checkbox" label="Marie Curie"></lion-checkbox> <lion-checkbox slot="checkbox" label="Francis Bacon"></lion-checkbox>
</lion-checkbox-indeterminate> <lion-checkbox slot="checkbox" label="Marie Curie"></lion-checkbox>
</lion-checkbox-group> </lion-checkbox-indeterminate>
`)); </lion-checkbox-group>
const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector( `)
'lion-checkbox-indeterminate', );
)); const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (
el.querySelector('lion-checkbox-indeterminate')
);
const { _subCheckboxes } = getCheckboxIndeterminateMembers(elIndeterminate); const { _subCheckboxes } = getCheckboxIndeterminateMembers(elIndeterminate);
@ -120,18 +125,20 @@ describe('<lion-checkbox-indeterminate>', () => {
it('should become checked if all children are checked', async () => { it('should become checked if all children are checked', async () => {
// Arrange // Arrange
const el = /** @type {LionCheckboxGroup} */ (await fixture(html` const el = /** @type {LionCheckboxGroup} */ (
<lion-checkbox-group name="scientists[]"> await fixture(html`
<lion-checkbox-indeterminate label="Favorite scientists"> <lion-checkbox-group name="scientists[]">
<lion-checkbox slot="checkbox" label="Archimedes"></lion-checkbox> <lion-checkbox-indeterminate label="Favorite scientists">
<lion-checkbox slot="checkbox" label="Francis Bacon"></lion-checkbox> <lion-checkbox slot="checkbox" label="Archimedes"></lion-checkbox>
<lion-checkbox slot="checkbox" label="Marie Curie"></lion-checkbox> <lion-checkbox slot="checkbox" label="Francis Bacon"></lion-checkbox>
</lion-checkbox-indeterminate> <lion-checkbox slot="checkbox" label="Marie Curie"></lion-checkbox>
</lion-checkbox-group> </lion-checkbox-indeterminate>
`)); </lion-checkbox-group>
const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector( `)
'lion-checkbox-indeterminate', );
)); const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (
el.querySelector('lion-checkbox-indeterminate')
);
const { _subCheckboxes } = getCheckboxIndeterminateMembers(elIndeterminate); const { _subCheckboxes } = getCheckboxIndeterminateMembers(elIndeterminate);
// Act // Act
@ -147,18 +154,20 @@ describe('<lion-checkbox-indeterminate>', () => {
it('should sync all children when parent is checked (from indeterminate to checked)', async () => { it('should sync all children when parent is checked (from indeterminate to checked)', async () => {
// Arrange // Arrange
const el = /** @type {LionCheckboxGroup} */ (await fixture(html` const el = /** @type {LionCheckboxGroup} */ (
<lion-checkbox-group name="scientists[]"> await fixture(html`
<lion-checkbox-indeterminate label="Favorite scientists"> <lion-checkbox-group name="scientists[]">
<lion-checkbox slot="checkbox" label="Archimedes"></lion-checkbox> <lion-checkbox-indeterminate label="Favorite scientists">
<lion-checkbox slot="checkbox" label="Francis Bacon" checked></lion-checkbox> <lion-checkbox slot="checkbox" label="Archimedes"></lion-checkbox>
<lion-checkbox slot="checkbox" label="Marie Curie"></lion-checkbox> <lion-checkbox slot="checkbox" label="Francis Bacon" checked></lion-checkbox>
</lion-checkbox-indeterminate> <lion-checkbox slot="checkbox" label="Marie Curie"></lion-checkbox>
</lion-checkbox-group> </lion-checkbox-indeterminate>
`)); </lion-checkbox-group>
const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector( `)
'lion-checkbox-indeterminate', );
)); const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (
el.querySelector('lion-checkbox-indeterminate')
);
const { _subCheckboxes, _inputNode } = getCheckboxIndeterminateMembers(elIndeterminate); const { _subCheckboxes, _inputNode } = getCheckboxIndeterminateMembers(elIndeterminate);
// Act // Act
@ -174,18 +183,20 @@ describe('<lion-checkbox-indeterminate>', () => {
it('should sync all children when parent is checked (from unchecked to checked)', async () => { it('should sync all children when parent is checked (from unchecked to checked)', async () => {
// Arrange // Arrange
const el = /** @type {LionCheckboxGroup} */ (await fixture(html` const el = /** @type {LionCheckboxGroup} */ (
<lion-checkbox-group name="scientists[]"> await fixture(html`
<lion-checkbox-indeterminate label="Favorite scientists"> <lion-checkbox-group name="scientists[]">
<lion-checkbox slot="checkbox" label="Archimedes"></lion-checkbox> <lion-checkbox-indeterminate label="Favorite scientists">
<lion-checkbox slot="checkbox" label="Francis Bacon"></lion-checkbox> <lion-checkbox slot="checkbox" label="Archimedes"></lion-checkbox>
<lion-checkbox slot="checkbox" label="Marie Curie"></lion-checkbox> <lion-checkbox slot="checkbox" label="Francis Bacon"></lion-checkbox>
</lion-checkbox-indeterminate> <lion-checkbox slot="checkbox" label="Marie Curie"></lion-checkbox>
</lion-checkbox-group> </lion-checkbox-indeterminate>
`)); </lion-checkbox-group>
const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector( `)
'lion-checkbox-indeterminate', );
)); const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (
el.querySelector('lion-checkbox-indeterminate')
);
const { _subCheckboxes, _inputNode } = getCheckboxIndeterminateMembers(elIndeterminate); const { _subCheckboxes, _inputNode } = getCheckboxIndeterminateMembers(elIndeterminate);
// Act // Act
@ -201,18 +212,20 @@ describe('<lion-checkbox-indeterminate>', () => {
it('should sync all children when parent is checked (from checked to unchecked)', async () => { it('should sync all children when parent is checked (from checked to unchecked)', async () => {
// Arrange // Arrange
const el = /** @type {LionCheckboxGroup} */ (await fixture(html` const el = /** @type {LionCheckboxGroup} */ (
<lion-checkbox-group name="scientists[]"> await fixture(html`
<lion-checkbox-indeterminate label="Favorite scientists"> <lion-checkbox-group name="scientists[]">
<lion-checkbox slot="checkbox" label="Archimedes" checked></lion-checkbox> <lion-checkbox-indeterminate label="Favorite scientists">
<lion-checkbox slot="checkbox" label="Francis Bacon" checked></lion-checkbox> <lion-checkbox slot="checkbox" label="Archimedes" checked></lion-checkbox>
<lion-checkbox slot="checkbox" label="Marie Curie" checked></lion-checkbox> <lion-checkbox slot="checkbox" label="Francis Bacon" checked></lion-checkbox>
</lion-checkbox-indeterminate> <lion-checkbox slot="checkbox" label="Marie Curie" checked></lion-checkbox>
</lion-checkbox-group> </lion-checkbox-indeterminate>
`)); </lion-checkbox-group>
const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector( `)
'lion-checkbox-indeterminate', );
)); const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (
el.querySelector('lion-checkbox-indeterminate')
);
const { _subCheckboxes, _inputNode } = getCheckboxIndeterminateMembers(elIndeterminate); const { _subCheckboxes, _inputNode } = getCheckboxIndeterminateMembers(elIndeterminate);
// Act // Act
@ -228,45 +241,50 @@ describe('<lion-checkbox-indeterminate>', () => {
it('should work as expected with siblings checkbox-indeterminate', async () => { it('should work as expected with siblings checkbox-indeterminate', async () => {
// Arrange // Arrange
const el = /** @type {LionCheckboxGroup} */ (await fixture(html` const el = /** @type {LionCheckboxGroup} */ (
<lion-checkbox-group name="scientists[]" label="Favorite scientists"> await fixture(html`
<lion-checkbox-indeterminate label="Old Greek scientists" id="first-checkbox-indeterminate"> <lion-checkbox-group name="scientists[]" label="Favorite scientists">
<lion-checkbox <lion-checkbox-indeterminate
slot="checkbox" label="Old Greek scientists"
label="Archimedes" id="first-checkbox-indeterminate"
.choiceValue=${'Archimedes'} >
></lion-checkbox> <lion-checkbox
<lion-checkbox slot="checkbox" label="Plato" .choiceValue=${'Plato'}></lion-checkbox> slot="checkbox"
<lion-checkbox label="Archimedes"
slot="checkbox" .choiceValue=${'Archimedes'}
label="Pythagoras" ></lion-checkbox>
.choiceValue=${'Pythagoras'} <lion-checkbox slot="checkbox" label="Plato" .choiceValue=${'Plato'}></lion-checkbox>
></lion-checkbox> <lion-checkbox
</lion-checkbox-indeterminate> slot="checkbox"
<lion-checkbox-indeterminate label="Pythagoras"
label="17th Century scientists" .choiceValue=${'Pythagoras'}
id="second-checkbox-indeterminate" ></lion-checkbox>
> </lion-checkbox-indeterminate>
<lion-checkbox <lion-checkbox-indeterminate
slot="checkbox" label="17th Century scientists"
label="Isaac Newton" id="second-checkbox-indeterminate"
.choiceValue=${'Isaac Newton'} >
></lion-checkbox> <lion-checkbox
<lion-checkbox slot="checkbox"
slot="checkbox" label="Isaac Newton"
label="Galileo Galilei" .choiceValue=${'Isaac Newton'}
.choiceValue=${'Galileo Galilei'} ></lion-checkbox>
></lion-checkbox> <lion-checkbox
</lion-checkbox-indeterminate> slot="checkbox"
</lion-checkbox-group> label="Galileo Galilei"
`)); .choiceValue=${'Galileo Galilei'}
const elFirstIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector( ></lion-checkbox>
'#first-checkbox-indeterminate', </lion-checkbox-indeterminate>
)); </lion-checkbox-group>
`)
);
const elFirstIndeterminate = /** @type {LionCheckboxIndeterminate} */ (
el.querySelector('#first-checkbox-indeterminate')
);
const elSecondIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector( const elSecondIndeterminate = /** @type {LionCheckboxIndeterminate} */ (
'#second-checkbox-indeterminate', el.querySelector('#second-checkbox-indeterminate')
)); );
const elFirstSubCheckboxes = getCheckboxIndeterminateMembers(elFirstIndeterminate); const elFirstSubCheckboxes = getCheckboxIndeterminateMembers(elFirstIndeterminate);
const elSecondSubCheckboxes = getCheckboxIndeterminateMembers(elSecondIndeterminate); const elSecondSubCheckboxes = getCheckboxIndeterminateMembers(elSecondIndeterminate);
@ -289,45 +307,47 @@ describe('<lion-checkbox-indeterminate>', () => {
it('should work as expected with nested indeterminate checkboxes', async () => { it('should work as expected with nested indeterminate checkboxes', async () => {
// Arrange // Arrange
const el = /** @type {LionCheckboxGroup} */ (await fixture(html` const el = /** @type {LionCheckboxGroup} */ (
<lion-checkbox-group name="scientists[]" label="Favorite scientists"> await fixture(html`
<lion-checkbox-indeterminate label="Scientists" id="parent-checkbox-indeterminate"> <lion-checkbox-group name="scientists[]" label="Favorite scientists">
<lion-checkbox <lion-checkbox-indeterminate label="Scientists" id="parent-checkbox-indeterminate">
slot="checkbox"
label="Isaac Newton"
.choiceValue=${'Isaac Newton'}
></lion-checkbox>
<lion-checkbox
slot="checkbox"
label="Galileo Galilei"
.choiceValue=${'Galileo Galilei'}
></lion-checkbox>
<lion-checkbox-indeterminate
slot="checkbox"
label="Old Greek scientists"
id="nested-checkbox-indeterminate"
>
<lion-checkbox <lion-checkbox
slot="checkbox" slot="checkbox"
label="Archimedes" label="Isaac Newton"
.choiceValue=${'Archimedes'} .choiceValue=${'Isaac Newton'}
></lion-checkbox> ></lion-checkbox>
<lion-checkbox slot="checkbox" label="Plato" .choiceValue=${'Plato'}></lion-checkbox>
<lion-checkbox <lion-checkbox
slot="checkbox" slot="checkbox"
label="Pythagoras" label="Galileo Galilei"
.choiceValue=${'Pythagoras'} .choiceValue=${'Galileo Galilei'}
></lion-checkbox> ></lion-checkbox>
<lion-checkbox-indeterminate
slot="checkbox"
label="Old Greek scientists"
id="nested-checkbox-indeterminate"
>
<lion-checkbox
slot="checkbox"
label="Archimedes"
.choiceValue=${'Archimedes'}
></lion-checkbox>
<lion-checkbox slot="checkbox" label="Plato" .choiceValue=${'Plato'}></lion-checkbox>
<lion-checkbox
slot="checkbox"
label="Pythagoras"
.choiceValue=${'Pythagoras'}
></lion-checkbox>
</lion-checkbox-indeterminate>
</lion-checkbox-indeterminate> </lion-checkbox-indeterminate>
</lion-checkbox-indeterminate> </lion-checkbox-group>
</lion-checkbox-group> `)
`)); );
const elNestedIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector( const elNestedIndeterminate = /** @type {LionCheckboxIndeterminate} */ (
'#nested-checkbox-indeterminate', el.querySelector('#nested-checkbox-indeterminate')
)); );
const elParentIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector( const elParentIndeterminate = /** @type {LionCheckboxIndeterminate} */ (
'#parent-checkbox-indeterminate', el.querySelector('#parent-checkbox-indeterminate')
)); );
const elNestedSubCheckboxes = getCheckboxIndeterminateMembers(elNestedIndeterminate); const elNestedSubCheckboxes = getCheckboxIndeterminateMembers(elNestedIndeterminate);
const elParentSubCheckboxes = getCheckboxIndeterminateMembers(elParentIndeterminate); const elParentSubCheckboxes = getCheckboxIndeterminateMembers(elParentIndeterminate);
@ -375,25 +395,27 @@ describe('<lion-checkbox-indeterminate>', () => {
it('should work as expected if extra html', async () => { it('should work as expected if extra html', async () => {
// Arrange // Arrange
const el = /** @type {LionCheckboxGroup} */ (await fixture(html` const el = /** @type {LionCheckboxGroup} */ (
<lion-checkbox-group name="scientists[]"> await fixture(html`
<div> <lion-checkbox-group name="scientists[]">
Let's have some fun <div>
<div>Hello I'm a div</div> Let's have some fun
<lion-checkbox-indeterminate label="Favorite scientists"> <div>Hello I'm a div</div>
<div>useless div</div> <lion-checkbox-indeterminate label="Favorite scientists">
<lion-checkbox slot="checkbox" label="Archimedes"></lion-checkbox> <div>useless div</div>
<lion-checkbox slot="checkbox" label="Francis Bacon"></lion-checkbox> <lion-checkbox slot="checkbox" label="Archimedes"></lion-checkbox>
<div>absolutely useless</div> <lion-checkbox slot="checkbox" label="Francis Bacon"></lion-checkbox>
<lion-checkbox slot="checkbox" label="Marie Curie"></lion-checkbox> <div>absolutely useless</div>
</lion-checkbox-indeterminate> <lion-checkbox slot="checkbox" label="Marie Curie"></lion-checkbox>
</div> </lion-checkbox-indeterminate>
<div>Too much fun, stop it !</div> </div>
</lion-checkbox-group> <div>Too much fun, stop it !</div>
`)); </lion-checkbox-group>
const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector( `)
'lion-checkbox-indeterminate', );
)); const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (
el.querySelector('lion-checkbox-indeterminate')
);
const { _subCheckboxes } = getCheckboxIndeterminateMembers(elIndeterminate); const { _subCheckboxes } = getCheckboxIndeterminateMembers(elIndeterminate);
// Act // Act

View file

@ -1,4 +1,5 @@
import { expect, fixture, html } from '@open-wc/testing'; import { expect, fixture } from '@open-wc/testing';
import { html } from 'lit/static-html.js';
import '@lion/checkbox-group/define-checkbox'; import '@lion/checkbox-group/define-checkbox';
/** /**
@ -14,9 +15,9 @@ describe('<lion-checkbox>', () => {
}); });
it('can be reset when unchecked by default', async () => { it('can be reset when unchecked by default', async () => {
const el = /** @type {LionCheckbox} */ (await fixture(html` const el = /** @type {LionCheckbox} */ (
<lion-checkbox name="checkbox" .choiceValue=${'male'}></lion-checkbox> await fixture(html` <lion-checkbox name="checkbox" .choiceValue=${'male'}></lion-checkbox> `)
`)); );
expect(el._initialModelValue).to.deep.equal({ value: 'male', checked: false }); expect(el._initialModelValue).to.deep.equal({ value: 'male', checked: false });
el.checked = true; el.checked = true;
expect(el.modelValue).to.deep.equal({ value: 'male', checked: true }); expect(el.modelValue).to.deep.equal({ value: 'male', checked: true });
@ -26,9 +27,11 @@ describe('<lion-checkbox>', () => {
}); });
it('can be reset when checked by default', async () => { it('can be reset when checked by default', async () => {
const el = /** @type {LionCheckbox} */ (await fixture(html` const el = /** @type {LionCheckbox} */ (
<lion-checkbox name="checkbox" .choiceValue=${'male'} checked></lion-checkbox> await fixture(html`
`)); <lion-checkbox name="checkbox" .choiceValue=${'male'} checked></lion-checkbox>
`)
);
expect(el._initialModelValue).to.deep.equal({ value: 'male', checked: true }); expect(el._initialModelValue).to.deep.equal({ value: 'male', checked: true });
el.checked = false; el.checked = false;
expect(el.modelValue).to.deep.equal({ value: 'male', checked: false }); expect(el.modelValue).to.deep.equal({ value: 'male', checked: false });

1
packages/collapsible/index.d.ts vendored Normal file
View file

@ -0,0 +1 @@
export { LionCollapsible } from "./src/LionCollapsible.js";

View file

@ -1,4 +1,5 @@
import { expect, fixture as _fixture, html } from '@open-wc/testing'; import { expect, fixture as _fixture } from '@open-wc/testing';
import { html } from 'lit/static-html.js';
import '@lion/collapsible/define'; import '@lion/collapsible/define';
@ -53,7 +54,7 @@ describe('<lion-collapsible>', () => {
it('has [opened] on current expanded invoker which serves as styling hook', async () => { it('has [opened] on current expanded invoker which serves as styling hook', async () => {
const collapsible = await fixture(defaultCollapsible); const collapsible = await fixture(defaultCollapsible);
collapsible.opened = true; collapsible.opened = true;
await collapsible.requestUpdate(); await collapsible.updateComplete;
expect(collapsible).to.have.attribute('opened'); expect(collapsible).to.have.attribute('opened');
}); });
@ -62,7 +63,7 @@ describe('<lion-collapsible>', () => {
const collHeight1 = getProtectedMembers(collapsible); const collHeight1 = getProtectedMembers(collapsible);
expect(collHeight1.contentHeight).to.equal('0px'); expect(collHeight1.contentHeight).to.equal('0px');
collapsible.show(); collapsible.show();
await collapsible.requestUpdate(); await collapsible.updateComplete;
const collHeight2 = getProtectedMembers(collapsible); const collHeight2 = getProtectedMembers(collapsible);
expect(collHeight2.contentHeight).to.equal('32px'); expect(collHeight2.contentHeight).to.equal('32px');
}); });
@ -93,10 +94,10 @@ describe('<lion-collapsible>', () => {
it('should listen to the open and close state change', async () => { it('should listen to the open and close state change', async () => {
const collapsible = await fixture(collapsibleWithEvents); const collapsible = await fixture(collapsibleWithEvents);
collapsible.show(); collapsible.show();
await collapsible.requestUpdate(); await collapsible.updateComplete;
expect(isCollapsibleOpen).to.equal(true); expect(isCollapsibleOpen).to.equal(true);
collapsible.hide(); collapsible.hide();
await collapsible.requestUpdate(); await collapsible.updateComplete;
expect(isCollapsibleOpen).to.equal(false); expect(isCollapsibleOpen).to.equal(false);
}); });
}); });
@ -131,7 +132,7 @@ describe('<lion-collapsible>', () => {
const collapsibleElement = await fixture(defaultCollapsible); const collapsibleElement = await fixture(defaultCollapsible);
const invoker = collapsibleElement.querySelector('[slot=invoker]'); const invoker = collapsibleElement.querySelector('[slot=invoker]');
collapsibleElement.opened = true; collapsibleElement.opened = true;
await collapsibleElement.requestUpdate(); await collapsibleElement.updateComplete;
expect(invoker).to.have.attribute('aria-expanded', 'true'); expect(invoker).to.have.attribute('aria-expanded', 'true');
}); });
}); });

1
packages/combobox/index.d.ts vendored Normal file
View file

@ -0,0 +1 @@
export { LionCombobox } from "./src/LionCombobox.js";

View file

@ -1,3 +1,3 @@
import { LionCombobox } from './src/LionCombobox.js'; import { /** @type{HTMLElement} */ LionCombobox } from './src/LionCombobox.js';
customElements.define('lion-combobox', LionCombobox); customElements.define('lion-combobox', LionCombobox);

View file

@ -218,8 +218,10 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
* @protected * @protected
*/ */
get _listboxNode() { get _listboxNode() {
return /** @type {LionOptions} */ ((this._overlayCtrl && this._overlayCtrl.contentNode) || return /** @type {LionOptions} */ (
Array.from(this.children).find(child => child.slot === 'listbox')); (this._overlayCtrl && this._overlayCtrl.contentNode) ||
Array.from(this.children).find(child => child.slot === 'listbox')
);
} }
/** /**
@ -310,8 +312,8 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
* @param {'disabled'|'modelValue'|'readOnly'|'focused'} name * @param {'disabled'|'modelValue'|'readOnly'|'focused'} name
* @param {unknown} oldValue * @param {unknown} oldValue
*/ */
requestUpdateInternal(name, oldValue) { requestUpdate(name, oldValue) {
super.requestUpdateInternal(name, oldValue); super.requestUpdate(name, oldValue);
if (name === 'disabled' || name === 'readOnly') { if (name === 'disabled' || name === 'readOnly') {
this.__setComboboxDisabledAndReadOnly(); this.__setComboboxDisabledAndReadOnly();
} }
@ -514,9 +516,8 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
phase: 'overlay-close', phase: 'overlay-close',
}) })
) { ) {
this._inputNode.value = this.formElements[ this._inputNode.value =
/** @type {number} */ (this.checkedIndex) this.formElements[/** @type {number} */ (this.checkedIndex)].choiceValue;
].choiceValue;
} }
} else { } else {
this._syncToTextboxMultiple(this.modelValue, this._oldModelValue); this._syncToTextboxMultiple(this.modelValue, this._oldModelValue);
@ -703,7 +704,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
}); });
// [7]. If no autofill took place, we are left with the previously matched option; correct this // [7]. If no autofill took place, we are left with the previously matched option; correct this
if (!hasAutoFilled && autoselect && !this.multipleChoice) { if (autoselect && !hasAutoFilled && !this.multipleChoice) {
// This means there is no match for checkedIndex // This means there is no match for checkedIndex
this.checkedIndex = -1; this.checkedIndex = -1;
} }
@ -771,7 +772,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
*/ */
_setupOverlayCtrl() { _setupOverlayCtrl() {
super._setupOverlayCtrl(); super._setupOverlayCtrl();
this.__initFilterListbox(); this.__shouldAutocompleteNextUpdate = true;
this.__setupCombobox(); this.__setupCombobox();
} }
@ -863,13 +864,6 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
} }
} }
/**
* @private
*/
__initFilterListbox() {
this._handleAutocompletion();
}
/** /**
* @private * @private
*/ */

File diff suppressed because it is too large Load diff

View file

@ -1,16 +1,82 @@
export { asyncAppend } from 'lit-html/directives/async-append.js'; export {
export { asyncReplace } from 'lit-html/directives/async-replace.js'; html,
export { cache } from 'lit-html/directives/cache.js'; CSSResult,
export { classMap } from 'lit-html/directives/class-map.js'; adoptStyles,
export { guard } from 'lit-html/directives/guard.js'; css,
export { ifDefined } from 'lit-html/directives/if-defined.js'; getCompatibleStyle,
export { repeat } from 'lit-html/directives/repeat.js'; supportsAdoptingStyleSheets,
export { styleMap } from 'lit-html/directives/style-map.js'; unsafeCSS,
export { unsafeHTML } from 'lit-html/directives/unsafe-html.js'; UpdatingElement,
export { until } from 'lit-html/directives/until.js'; notEqual,
export { render as renderShady } from 'lit-html/lib/shady-render.js'; ReactiveElement,
svg,
noChange,
nothing,
render,
RenderOptions,
LitElement,
defaultConverter,
CSSResultArray,
PropertyValues,
TemplateResult,
} from 'lit';
export {
customElement,
property,
state,
eventOptions,
query,
queryAll,
queryAsync,
queryAssignedNodes,
} from 'lit/decorators.js';
export {
AttributePart,
BooleanAttributePart,
ChildPart,
ElementPart,
EventPart,
Part,
PartType,
directive,
Directive,
DirectiveResult,
} from 'lit/directive.js';
export { AsyncDirective } from 'lit/async-directive.js';
export {
isPrimitive,
TemplateResultType,
isTemplateResult,
isDirectiveResult,
getDirectiveClass,
isSingleExpression,
insertPart,
setChildPartValue,
setCommittedValue,
getCommittedValue,
removePart,
clearPart,
} from 'lit/directive-helpers.js';
export { asyncAppend } from 'lit/directives/async-append.js';
export { asyncReplace } from 'lit/directives/async-replace.js';
export { cache } from 'lit/directives/cache.js';
export { classMap } from 'lit/directives/class-map.js';
export { guard } from 'lit/directives/guard.js';
export { ifDefined } from 'lit/directives/if-defined.js';
export { repeat } from 'lit/directives/repeat.js';
export { styleMap } from 'lit/directives/style-map.js';
export { unsafeHTML } from 'lit/directives/unsafe-html.js';
export { until } from 'lit/directives/until.js';
// open-wc
export { ScopedElementsMixin } from '@open-wc/scoped-elements'; export { ScopedElementsMixin } from '@open-wc/scoped-elements';
export { dedupeMixin } from '@open-wc/dedupe-mixin'; export { dedupeMixin } from '@open-wc/dedupe-mixin';
// ours
export { DelegateMixin } from './src/DelegateMixin.js'; export { DelegateMixin } from './src/DelegateMixin.js';
export { DisabledMixin } from './src/DisabledMixin.js'; export { DisabledMixin } from './src/DisabledMixin.js';
export { DisabledWithTabIndexMixin } from './src/DisabledWithTabIndexMixin.js'; export { DisabledWithTabIndexMixin } from './src/DisabledWithTabIndexMixin.js';
@ -18,39 +84,3 @@ export { SlotMixin } from './src/SlotMixin.js';
export { UpdateStylesMixin } from './src/UpdateStylesMixin.js'; export { UpdateStylesMixin } from './src/UpdateStylesMixin.js';
export { browserDetection } from './src/browserDetection.js'; export { browserDetection } from './src/browserDetection.js';
export { EventTargetShim } from './src/EventTargetShim.js'; export { EventTargetShim } from './src/EventTargetShim.js';
export {
css,
CSSResult,
CSSResultArray,
customElement,
defaultConverter,
eventOptions,
LitElement,
notEqual,
property,
PropertyValues,
query,
queryAll,
supportsAdoptingStyleSheets,
unsafeCSS,
UpdatingElement,
} from 'lit-element';
export {
AttributePart,
BooleanAttributePart,
directive,
EventPart,
html,
isDirective,
isPrimitive,
noChange,
NodePart,
nothing,
PropertyPart,
render,
svg,
SVGTemplateResult,
TemplateResult,
removeNodes,
reparentNodes,
} from 'lit-html';

View file

@ -1,53 +1,63 @@
// lit-element
export { export {
css, html,
CSSResult, CSSResult,
// decorators.js adoptStyles,
customElement, css,
// updating-element.js getCompatibleStyle,
defaultConverter,
eventOptions,
LitElement,
notEqual,
property,
query,
queryAll,
// css-tag.js
supportsAdoptingStyleSheets, supportsAdoptingStyleSheets,
unsafeCSS, unsafeCSS,
UpdatingElement, UpdatingElement,
} from 'lit-element'; notEqual,
// lit-html ReactiveElement,
export {
AttributePart,
BooleanAttributePart,
directive,
EventPart,
html,
isDirective,
isPrimitive,
noChange,
NodePart,
nothing,
PropertyPart,
render,
svg, svg,
SVGTemplateResult, noChange,
TemplateResult, nothing,
reparentNodes, render,
removeNodes, LitElement,
} from 'lit-html'; defaultConverter,
export { asyncAppend } from 'lit-html/directives/async-append.js'; } from 'lit';
export { asyncReplace } from 'lit-html/directives/async-replace.js';
export { cache } from 'lit-html/directives/cache.js'; export {
export { classMap } from 'lit-html/directives/class-map.js'; customElement,
export { guard } from 'lit-html/directives/guard.js'; property,
export { ifDefined } from 'lit-html/directives/if-defined.js'; state,
export { repeat } from 'lit-html/directives/repeat.js'; eventOptions,
export { styleMap } from 'lit-html/directives/style-map.js'; query,
export { unsafeHTML } from 'lit-html/directives/unsafe-html.js'; queryAll,
export { until } from 'lit-html/directives/until.js'; queryAsync,
export { render as renderShady } from 'lit-html/lib/shady-render.js'; queryAssignedNodes,
} from 'lit/decorators.js';
export { directive, Directive } from 'lit/directive.js';
export { AsyncDirective } from 'lit/async-directive.js';
export {
isPrimitive,
TemplateResultType,
isTemplateResult,
isDirectiveResult,
getDirectiveClass,
isSingleExpression,
insertPart,
setChildPartValue,
setCommittedValue,
getCommittedValue,
removePart,
clearPart,
} from 'lit/directive-helpers.js';
export { asyncAppend } from 'lit/directives/async-append.js';
export { asyncReplace } from 'lit/directives/async-replace.js';
export { cache } from 'lit/directives/cache.js';
export { classMap } from 'lit/directives/class-map.js';
export { guard } from 'lit/directives/guard.js';
export { ifDefined } from 'lit/directives/if-defined.js';
export { repeat } from 'lit/directives/repeat.js';
export { styleMap } from 'lit/directives/style-map.js';
export { unsafeHTML } from 'lit/directives/unsafe-html.js';
export { until } from 'lit/directives/until.js';
// open-wc // open-wc
export { ScopedElementsMixin } from '@open-wc/scoped-elements'; export { ScopedElementsMixin } from '@open-wc/scoped-elements';
export { dedupeMixin } from '@open-wc/dedupe-mixin'; export { dedupeMixin } from '@open-wc/dedupe-mixin';

View file

@ -37,9 +37,8 @@
], ],
"dependencies": { "dependencies": {
"@open-wc/dedupe-mixin": "^1.2.18", "@open-wc/dedupe-mixin": "^1.2.18",
"@open-wc/scoped-elements": "^1.3.3", "@open-wc/scoped-elements": "^2.0.0-next.3",
"lit-element": "~2.4.0", "lit": "^2.0.0-rc.2"
"lit-html": "^1.3.0"
}, },
"keywords": [ "keywords": [
"lion", "lion",

View file

@ -60,8 +60,8 @@ const DisabledMixinImplementation = superclass =>
* @param {PropertyKey} name * @param {PropertyKey} name
* @param {?} oldValue * @param {?} oldValue
*/ */
requestUpdateInternal(name, oldValue) { requestUpdate(name, oldValue) {
super.requestUpdateInternal(name, oldValue); super.requestUpdate(name, oldValue);
if (name === 'disabled') { if (name === 'disabled') {
if (this.__isUserSettingDisabled) { if (this.__isUserSettingDisabled) {
this.__restoreDisabledTo = this.disabled; this.__restoreDisabledTo = this.disabled;

View file

@ -62,8 +62,8 @@ const DisabledWithTabIndexMixinImplementation = superclass =>
* @param {PropertyKey} name * @param {PropertyKey} name
* @param {?} oldValue * @param {?} oldValue
*/ */
requestUpdateInternal(name, oldValue) { requestUpdate(name, oldValue) {
super.requestUpdateInternal(name, oldValue); super.requestUpdate(name, oldValue);
if (name === 'disabled') { if (name === 'disabled') {
if (this.disabled) { if (this.disabled) {

View file

@ -1,4 +1,5 @@
import { defineCE, expect, fixture, html, unsafeStatic } from '@open-wc/testing'; import { defineCE, expect, fixture } from '@open-wc/testing';
import { html, unsafeStatic } from 'lit/static-html.js';
import sinon from 'sinon'; import sinon from 'sinon';
import { LitElement } from '../index.js'; import { LitElement } from '../index.js';
import { DelegateMixin } from '../src/DelegateMixin.js'; import { DelegateMixin } from '../src/DelegateMixin.js';
@ -83,9 +84,9 @@ describe('DelegateMixin', () => {
const element = await fixture(`<${tag}><button slot="button">click me</button></${tag}>`); const element = await fixture(`<${tag}><button slot="button">click me</button></${tag}>`);
const cb = sinon.spy(); const cb = sinon.spy();
element.addEventListener('click', cb); element.addEventListener('click', cb);
const childEl = /** @type {HTMLElement} */ (Array.from(element.children)?.find( const childEl = /** @type {HTMLElement} */ (
child => child.slot === 'button', Array.from(element.children)?.find(child => child.slot === 'button')
)); );
childEl?.click(); childEl?.click();
expect(cb.callCount).to.equal(1); expect(cb.callCount).to.equal(1);
}); });
@ -343,14 +344,14 @@ describe('DelegateMixin', () => {
const tagName = unsafeStatic(tag); const tagName = unsafeStatic(tag);
// Here, the Application Developerd tries to set the type via attribute // Here, the Application Developerd tries to set the type via attribute
const elementAttr = /** @type {ScheduledElement} */ (await fixture( const elementAttr = /** @type {ScheduledElement} */ (
`<${tag} type="radio"></${tag}>`, await fixture(`<${tag} type="radio"></${tag}>`)
)); );
expect(elementAttr.scheduledElement?.type).to.equal('radio'); expect(elementAttr.scheduledElement?.type).to.equal('radio');
// Here, the Application Developer tries to set the type via property // Here, the Application Developer tries to set the type via property
const elementProp = /** @type {ScheduledElement} */ (await fixture( const elementProp = /** @type {ScheduledElement} */ (
html`<${tagName} .type=${'radio'}></${tagName}>`, await fixture(html`<${tagName} .type=${'radio'}></${tagName}>`)
)); );
expect(elementProp.scheduledElement?.type).to.equal('radio'); expect(elementProp.scheduledElement?.type).to.equal('radio');
}); });

View file

@ -1,4 +1,5 @@
import { expect, fixture, html } from '@open-wc/testing'; import { expect, fixture } from '@open-wc/testing';
import { html } from 'lit/static-html.js';
import { LitElement } from '../index.js'; import { LitElement } from '../index.js';
import { DisabledMixin } from '../src/DisabledMixin.js'; import { DisabledMixin } from '../src/DisabledMixin.js';
@ -9,9 +10,9 @@ describe('DisabledMixin', () => {
}); });
it('reflects disabled to attribute', async () => { it('reflects disabled to attribute', async () => {
const el = /** @type {CanBeDisabled} */ (await fixture( const el = /** @type {CanBeDisabled} */ (
html`<can-be-disabled></can-be-disabled>`, await fixture(html`<can-be-disabled></can-be-disabled>`)
)); );
expect(el.hasAttribute('disabled')).to.be.false; expect(el.hasAttribute('disabled')).to.be.false;
el.makeRequestToBeDisabled(); el.makeRequestToBeDisabled();
el.disabled = true; el.disabled = true;
@ -20,9 +21,9 @@ describe('DisabledMixin', () => {
}); });
it('can be requested to be disabled', async () => { it('can be requested to be disabled', async () => {
const el = /** @type {CanBeDisabled} */ (await fixture( const el = /** @type {CanBeDisabled} */ (
html`<can-be-disabled></can-be-disabled>`, await fixture(html`<can-be-disabled></can-be-disabled>`)
)); );
el.makeRequestToBeDisabled(); el.makeRequestToBeDisabled();
expect(el.disabled).to.be.true; expect(el.disabled).to.be.true;
await el.updateComplete; await el.updateComplete;
@ -30,9 +31,9 @@ describe('DisabledMixin', () => {
}); });
it('will not allow to become enabled after makeRequestToBeDisabled()', async () => { it('will not allow to become enabled after makeRequestToBeDisabled()', async () => {
const el = /** @type {CanBeDisabled} */ (await fixture( const el = /** @type {CanBeDisabled} */ (
html`<can-be-disabled></can-be-disabled>`, await fixture(html`<can-be-disabled></can-be-disabled>`)
)); );
el.makeRequestToBeDisabled(); el.makeRequestToBeDisabled();
expect(el.disabled).to.be.true; expect(el.disabled).to.be.true;
@ -41,18 +42,18 @@ describe('DisabledMixin', () => {
}); });
it('will stay disabled after retractRequestToBeDisabled() if it was disabled before', async () => { it('will stay disabled after retractRequestToBeDisabled() if it was disabled before', async () => {
const el = /** @type {CanBeDisabled} */ (await fixture( const el = /** @type {CanBeDisabled} */ (
html`<can-be-disabled disabled></can-be-disabled>`, await fixture(html`<can-be-disabled disabled></can-be-disabled>`)
)); );
el.makeRequestToBeDisabled(); el.makeRequestToBeDisabled();
el.retractRequestToBeDisabled(); el.retractRequestToBeDisabled();
expect(el.disabled).to.be.true; expect(el.disabled).to.be.true;
}); });
it('will become enabled after retractRequestToBeDisabled() if it was enabled before', async () => { it('will become enabled after retractRequestToBeDisabled() if it was enabled before', async () => {
const el = /** @type {CanBeDisabled} */ (await fixture( const el = /** @type {CanBeDisabled} */ (
html`<can-be-disabled></can-be-disabled>`, await fixture(html`<can-be-disabled></can-be-disabled>`)
)); );
el.makeRequestToBeDisabled(); el.makeRequestToBeDisabled();
expect(el.disabled).to.be.true; expect(el.disabled).to.be.true;
el.retractRequestToBeDisabled(); el.retractRequestToBeDisabled();
@ -60,9 +61,9 @@ describe('DisabledMixin', () => {
}); });
it('may allow multiple calls to makeRequestToBeDisabled()', async () => { it('may allow multiple calls to makeRequestToBeDisabled()', async () => {
const el = /** @type {CanBeDisabled} */ (await fixture( const el = /** @type {CanBeDisabled} */ (
html`<can-be-disabled></can-be-disabled>`, await fixture(html`<can-be-disabled></can-be-disabled>`)
)); );
el.makeRequestToBeDisabled(); el.makeRequestToBeDisabled();
el.makeRequestToBeDisabled(); el.makeRequestToBeDisabled();
el.retractRequestToBeDisabled(); el.retractRequestToBeDisabled();
@ -70,9 +71,9 @@ describe('DisabledMixin', () => {
}); });
it('will restore last state after retractRequestToBeDisabled()', async () => { it('will restore last state after retractRequestToBeDisabled()', async () => {
const el = /** @type {CanBeDisabled} */ (await fixture( const el = /** @type {CanBeDisabled} */ (
html`<can-be-disabled></can-be-disabled>`, await fixture(html`<can-be-disabled></can-be-disabled>`)
)); );
el.makeRequestToBeDisabled(); el.makeRequestToBeDisabled();
el.disabled = true; el.disabled = true;
el.retractRequestToBeDisabled(); el.retractRequestToBeDisabled();

View file

@ -1,6 +1,6 @@
/* eslint-disable lit-a11y/tabindex-no-positive */ /* eslint-disable lit-a11y/tabindex-no-positive */
import { expect, fixture, html } from '@open-wc/testing'; import { expect, fixture } from '@open-wc/testing';
import { html } from 'lit/static-html.js';
import { LitElement } from '../index.js'; import { LitElement } from '../index.js';
import { DisabledWithTabIndexMixin } from '../src/DisabledWithTabIndexMixin.js'; import { DisabledWithTabIndexMixin } from '../src/DisabledWithTabIndexMixin.js';
@ -11,17 +11,17 @@ describe('DisabledWithTabIndexMixin', () => {
}); });
it('has an initial tabIndex of 0', async () => { it('has an initial tabIndex of 0', async () => {
const el = /** @type {WithTabIndex} */ (await fixture(html` const el = /** @type {WithTabIndex} */ (
<can-be-disabled-with-tab-index></can-be-disabled-with-tab-index> await fixture(html` <can-be-disabled-with-tab-index></can-be-disabled-with-tab-index> `)
`)); );
expect(el.tabIndex).to.equal(0); expect(el.tabIndex).to.equal(0);
expect(el.getAttribute('tabindex')).to.equal('0'); expect(el.getAttribute('tabindex')).to.equal('0');
}); });
it('sets tabIndex to -1 if disabled', async () => { it('sets tabIndex to -1 if disabled', async () => {
const el = /** @type {WithTabIndex} */ (await fixture(html` const el = /** @type {WithTabIndex} */ (
<can-be-disabled-with-tab-index></can-be-disabled-with-tab-index> await fixture(html` <can-be-disabled-with-tab-index></can-be-disabled-with-tab-index> `)
`)); );
el.disabled = true; el.disabled = true;
expect(el.tabIndex).to.equal(-1); expect(el.tabIndex).to.equal(-1);
await el.updateComplete; await el.updateComplete;
@ -29,9 +29,11 @@ describe('DisabledWithTabIndexMixin', () => {
}); });
it('disabled does not override user provided tabindex', async () => { it('disabled does not override user provided tabindex', async () => {
const el = /** @type {WithTabIndex} */ (await fixture(html` const el = /** @type {WithTabIndex} */ (
<can-be-disabled-with-tab-index tabindex="5" disabled></can-be-disabled-with-tab-index> await fixture(html`
`)); <can-be-disabled-with-tab-index tabindex="5" disabled></can-be-disabled-with-tab-index>
`)
);
expect(el.getAttribute('tabindex')).to.equal('-1'); expect(el.getAttribute('tabindex')).to.equal('-1');
el.disabled = false; el.disabled = false;
await el.updateComplete; await el.updateComplete;
@ -39,9 +41,11 @@ describe('DisabledWithTabIndexMixin', () => {
}); });
it('can be disabled imperatively', async () => { it('can be disabled imperatively', async () => {
const el = /** @type {WithTabIndex} */ (await fixture(html` const el = /** @type {WithTabIndex} */ (
<can-be-disabled-with-tab-index disabled></can-be-disabled-with-tab-index> await fixture(html`
`)); <can-be-disabled-with-tab-index disabled></can-be-disabled-with-tab-index>
`)
);
expect(el.getAttribute('tabindex')).to.equal('-1'); expect(el.getAttribute('tabindex')).to.equal('-1');
el.disabled = false; el.disabled = false;
@ -56,9 +60,9 @@ describe('DisabledWithTabIndexMixin', () => {
}); });
it('will not allow to change tabIndex after makeRequestToBeDisabled()', async () => { it('will not allow to change tabIndex after makeRequestToBeDisabled()', async () => {
const el = /** @type {WithTabIndex} */ (await fixture(html` const el = /** @type {WithTabIndex} */ (
<can-be-disabled-with-tab-index></can-be-disabled-with-tab-index> await fixture(html` <can-be-disabled-with-tab-index></can-be-disabled-with-tab-index> `)
`)); );
el.makeRequestToBeDisabled(); el.makeRequestToBeDisabled();
el.tabIndex = 5; el.tabIndex = 5;
@ -68,9 +72,11 @@ describe('DisabledWithTabIndexMixin', () => {
}); });
it('will restore last tabIndex after retractRequestToBeDisabled()', async () => { it('will restore last tabIndex after retractRequestToBeDisabled()', async () => {
const el = /** @type {WithTabIndex} */ (await fixture(html` const el = /** @type {WithTabIndex} */ (
<can-be-disabled-with-tab-index tabindex="5"></can-be-disabled-with-tab-index> await fixture(html`
`)); <can-be-disabled-with-tab-index tabindex="5"></can-be-disabled-with-tab-index>
`)
);
el.makeRequestToBeDisabled(); el.makeRequestToBeDisabled();
expect(el.tabIndex).to.equal(-1); expect(el.tabIndex).to.equal(-1);
await el.updateComplete; await el.updateComplete;
@ -97,9 +103,11 @@ describe('DisabledWithTabIndexMixin', () => {
}); });
it('may allow multiple calls to retractRequestToBeDisabled', async () => { it('may allow multiple calls to retractRequestToBeDisabled', async () => {
const el = /** @type {WithTabIndex} */ (await fixture(html` const el = /** @type {WithTabIndex} */ (
<can-be-disabled-with-tab-index disabled></can-be-disabled-with-tab-index> await fixture(html`
`)); <can-be-disabled-with-tab-index disabled></can-be-disabled-with-tab-index>
`)
);
el.retractRequestToBeDisabled(); el.retractRequestToBeDisabled();
el.retractRequestToBeDisabled(); el.retractRequestToBeDisabled();
expect(el.disabled).to.be.true; expect(el.disabled).to.be.true;

View file

@ -108,9 +108,9 @@ describe('SlotMixin', () => {
const tag = defineCE(SlotPrivateText); const tag = defineCE(SlotPrivateText);
const el = /** @type {SlotPrivateText} */ (await fixture(`<${tag}><${tag}>`)); const el = /** @type {SlotPrivateText} */ (await fixture(`<${tag}><${tag}>`));
expect(el.didCreateConditionalSlot()).to.be.true; expect(el.didCreateConditionalSlot()).to.be.true;
const elUserSlot = /** @type {SlotPrivateText} */ (await fixture( const elUserSlot = /** @type {SlotPrivateText} */ (
`<${tag}><p slot="conditional">foo</p><${tag}>`, await fixture(`<${tag}><p slot="conditional">foo</p><${tag}>`)
)); );
expect(elUserSlot.didCreateConditionalSlot()).to.be.false; expect(elUserSlot.didCreateConditionalSlot()).to.be.false;
renderSlot = false; renderSlot = false;
const elNoSlot = /** @type {SlotPrivateText} */ (await fixture(`<${tag}><${tag}>`)); const elNoSlot = /** @type {SlotPrivateText} */ (await fixture(`<${tag}><${tag}>`));

View file

@ -1,4 +1,5 @@
import { defineCE, expect, fixture, html } from '@open-wc/testing'; import { defineCE, expect, fixture } from '@open-wc/testing';
import { html } from 'lit/static-html.js';
import { css, LitElement } from '../index.js'; import { css, LitElement } from '../index.js';
import { UpdateStylesMixin } from '../src/UpdateStylesMixin.js'; import { UpdateStylesMixin } from '../src/UpdateStylesMixin.js';

View file

@ -1,4 +1,5 @@
import { expect, fixture as _fixture, html, unsafeStatic } from '@open-wc/testing'; import { expect, fixture as _fixture } from '@open-wc/testing';
import { html, unsafeStatic } from 'lit/static-html.js';
import { runOverlayMixinSuite } from '../../overlays/test-suites/OverlayMixin.suite.js'; import { runOverlayMixinSuite } from '../../overlays/test-suites/OverlayMixin.suite.js';
import '@lion/dialog/define'; import '@lion/dialog/define';
@ -62,9 +63,9 @@ describe('lion-dialog', () => {
el._overlayInvokerNode.click(); el._overlayInvokerNode.click();
expect(el.opened).to.be.true; expect(el.opened).to.be.true;
const overlaysContainer = /** @type {HTMLElement} */ (document.querySelector( const overlaysContainer = /** @type {HTMLElement} */ (
'.global-overlays', document.querySelector('.global-overlays')
)); );
const wrapperNode = Array.from(overlaysContainer.children)[1]; const wrapperNode = Array.from(overlaysContainer.children)[1];
const nestedDialog = /** @type {LionDialog} */ (wrapperNode.querySelector('lion-dialog')); const nestedDialog = /** @type {LionDialog} */ (wrapperNode.querySelector('lion-dialog'));
// @ts-expect-error you're not allowed to call protected _overlayInvokerNode in public context, but for testing it's okay // @ts-expect-error you're not allowed to call protected _overlayInvokerNode in public context, but for testing it's okay

View file

@ -1,6 +1,7 @@
import { dedupeMixin } from '@lion/core'; import { dedupeMixin } from '@lion/core';
const windowWithOptionalPolyfill = /** @type {Window & typeof globalThis & {applyFocusVisiblePolyfill?: function}} */ (window); const windowWithOptionalPolyfill =
/** @type {Window & typeof globalThis & {applyFocusVisiblePolyfill?: function}} */ (window);
const polyfilledNodes = new WeakMap(); const polyfilledNodes = new WeakMap();
/** /**

View file

@ -7,7 +7,6 @@ import { FormRegisteringMixin } from './registration/FormRegisteringMixin.js';
* @typedef {import('@lion/core').TemplateResult} TemplateResult * @typedef {import('@lion/core').TemplateResult} TemplateResult
* @typedef {import('@lion/core').CSSResult} CSSResult * @typedef {import('@lion/core').CSSResult} CSSResult
* @typedef {import('@lion/core').CSSResultArray} CSSResultArray * @typedef {import('@lion/core').CSSResultArray} CSSResultArray
* @typedef {import('@lion/core').nothing} nothing
* @typedef {import('@lion/core/types/SlotMixinTypes').SlotsMap} SlotsMap * @typedef {import('@lion/core/types/SlotMixinTypes').SlotsMap} SlotsMap
* @typedef {import('./validate/LionValidationFeedback').LionValidationFeedback} LionValidationFeedback * @typedef {import('./validate/LionValidationFeedback').LionValidationFeedback} LionValidationFeedback
* @typedef {import('../types/choice-group/ChoiceInputMixinTypes').ChoiceInputHost} ChoiceInputHost * @typedef {import('../types/choice-group/ChoiceInputMixinTypes').ChoiceInputHost} ChoiceInputHost
@ -765,7 +764,6 @@ const FormControlMixinImplementation = superclass =>
if (this._ariaLabelledNodes.includes(element)) { if (this._ariaLabelledNodes.includes(element)) {
this._ariaLabelledNodes.splice(this._ariaLabelledNodes.indexOf(element), 1); this._ariaLabelledNodes.splice(this._ariaLabelledNodes.indexOf(element), 1);
this._ariaLabelledNodes = [...this._ariaLabelledNodes]; this._ariaLabelledNodes = [...this._ariaLabelledNodes];
// This value will be read when we need to reflect to attr // This value will be read when we need to reflect to attr
/** @type {boolean} */ /** @type {boolean} */
this.__reorderAriaLabelledNodes = false; this.__reorderAriaLabelledNodes = false;

View file

@ -72,8 +72,8 @@ const FormatMixinImplementation = superclass =>
* @param {string} name * @param {string} name
* @param {any} oldVal * @param {any} oldVal
*/ */
requestUpdateInternal(name, oldVal) { requestUpdate(name, oldVal) {
super.requestUpdateInternal(name, oldVal); super.requestUpdate(name, oldVal);
if (name === 'modelValue' && this.modelValue !== oldVal) { if (name === 'modelValue' && this.modelValue !== oldVal) {
this._onModelValueChanged({ modelValue: this.modelValue }, { modelValue: oldVal }); this._onModelValueChanged({ modelValue: this.modelValue }, { modelValue: oldVal });
@ -525,8 +525,9 @@ const FormatMixinImplementation = superclass =>
this._inputNode.removeEventListener('input', this._proxyInputEvent); this._inputNode.removeEventListener('input', this._proxyInputEvent);
this._inputNode.removeEventListener( this._inputNode.removeEventListener(
this.formatOn, this.formatOn,
/** @type {EventListenerOrEventListenerObject} */ (this /** @type {EventListenerOrEventListenerObject} */ (
._reflectBackFormattedValueDebounced), this._reflectBackFormattedValueDebounced
),
); );
this._inputNode.removeEventListener('compositionstart', this.__onCompositionEvent); this._inputNode.removeEventListener('compositionstart', this.__onCompositionEvent);
this._inputNode.removeEventListener('compositionend', this.__onCompositionEvent); this._inputNode.removeEventListener('compositionend', this.__onCompositionEvent);

View file

@ -35,14 +35,14 @@ const InteractionStateMixinImplementation = superclass =>
* @param {PropertyKey} name * @param {PropertyKey} name
* @param {*} oldVal * @param {*} oldVal
*/ */
requestUpdateInternal(name, oldVal) { requestUpdate(name, oldVal) {
super.requestUpdateInternal(name, oldVal); super.requestUpdate(name, oldVal);
if (name === 'touched' && this.touched !== oldVal) { if (name === 'touched' && this.touched !== oldVal) {
this._onTouchedChanged(); this._onTouchedChanged();
} }
if (name === 'modelValue') { if (name === 'modelValue') {
// We do this in requestUpdateInternal because we don't want to fire another re-render (e.g. when doing this in updated) // We do this in requestUpdate because we don't want to fire another re-render (e.g. when doing this in updated)
// Furthermore, we cannot do it on model-value-changed event because it isn't fired initially. // Furthermore, we cannot do it on model-value-changed event because it isn't fired initially.
this.filled = !this._isEmpty(); this.filled = !this._isEmpty();
} }

View file

@ -53,8 +53,8 @@ const ChoiceInputMixinImplementation = superclass =>
* @param {string} name * @param {string} name
* @param {any} oldValue * @param {any} oldValue
*/ */
requestUpdateInternal(name, oldValue) { requestUpdate(name, oldValue) {
super.requestUpdateInternal(name, oldValue); super.requestUpdate(name, oldValue);
if (name === 'modelValue') { if (name === 'modelValue') {
if (this.modelValue.checked !== this.checked) { if (this.modelValue.checked !== this.checked) {
@ -298,7 +298,7 @@ const ChoiceInputMixinImplementation = superclass =>
/** /**
* @override * @override
* hasChanged is designed for async (updated) callback, also check for sync * hasChanged is designed for async (updated) callback, also check for sync
* (requestUpdateInternal) callback * (requestUpdate) callback
* @param {{ modelValue:unknown }} newV * @param {{ modelValue:unknown }} newV
* @param {{ modelValue:unknown }} [old] * @param {{ modelValue:unknown }} [old]
* @protected * @protected
@ -309,7 +309,7 @@ const ChoiceInputMixinImplementation = superclass =>
_old = old.modelValue; _old = old.modelValue;
} }
// @ts-expect-error [external]: lit private property // @ts-expect-error [external]: lit private property
if (this.constructor._classProperties.get('modelValue').hasChanged(modelValue, _old)) { if (this.constructor.elementProperties.get('modelValue').hasChanged(modelValue, _old)) {
super._onModelValueChanged({ modelValue }); super._onModelValueChanged({ modelValue });
} }
} }

View file

@ -360,12 +360,11 @@ const FormGroupMixinImplementation = superclass =>
if (values && typeof values === 'object') { if (values && typeof values === 'object') {
Object.keys(values).forEach(name => { Object.keys(values).forEach(name => {
if (Array.isArray(this.formElements[name])) { if (Array.isArray(this.formElements[name])) {
this.formElements[name].forEach(( this.formElements[name].forEach(
/** @type {FormControl} */ el, (/** @type {FormControl} */ el, /** @type {number} */ index) => {
/** @type {number} */ index, el[property] = values[name][index]; // eslint-disable-line no-param-reassign
) => { },
el[property] = values[name][index]; // eslint-disable-line no-param-reassign );
});
} }
if (this.formElements[name]) { if (this.formElements[name]) {
this.formElements[name][property] = values[name]; this.formElements[name][property] = values[name];

View file

@ -31,13 +31,13 @@ const FormRegistrarPortalMixinImplementation = superclass =>
* @type {(FormRegistrarPortalHost & HTMLElement) | undefined} * @type {(FormRegistrarPortalHost & HTMLElement) | undefined}
*/ */
this.registrationTarget = undefined; this.registrationTarget = undefined;
this.__redispatchEventForFormRegistrarPortalMixin = this.__redispatchEventForFormRegistrarPortalMixin.bind( this.__redispatchEventForFormRegistrarPortalMixin =
this, this.__redispatchEventForFormRegistrarPortalMixin.bind(this);
);
this.addEventListener( this.addEventListener(
'form-element-register', 'form-element-register',
/** @type {EventListenerOrEventListenerObject} */ (this /** @type {EventListenerOrEventListenerObject} */ (
.__redispatchEventForFormRegistrarPortalMixin), this.__redispatchEventForFormRegistrarPortalMixin
),
); );
} }

View file

@ -18,7 +18,7 @@ import { dedupeMixin } from '@lion/core';
* `updateSync` will only be called when new value differs from old value. * `updateSync` will only be called when new value differs from old value.
* See: https://lit-element.polymer-project.org/guide/lifecycle#haschanged * See: https://lit-element.polymer-project.org/guide/lifecycle#haschanged
* - it is a stable abstraction on top of a protected/non official lifecycle LitElement api. * - it is a stable abstraction on top of a protected/non official lifecycle LitElement api.
* Whenever the implementation of `requestUpdateInternal` changes (this happened in the past for * Whenever the implementation of `requestUpdate` changes (this happened in the past for
* `requestUpdate`) we only have to change our abstraction instead of all our components * `requestUpdate`) we only have to change our abstraction instead of all our components
* @type {SyncUpdatableMixin} * @type {SyncUpdatableMixin}
* @param {import('@open-wc/dedupe-mixin').Constructor<import('@lion/core').LitElement>} superclass * @param {import('@open-wc/dedupe-mixin').Constructor<import('@lion/core').LitElement>} superclass
@ -64,7 +64,7 @@ const SyncUpdatableMixinImplementation = superclass =>
*/ */
static __syncUpdatableHasChanged(name, newValue, oldValue) { static __syncUpdatableHasChanged(name, newValue, oldValue) {
// @ts-expect-error [external]: accessing private lit property // @ts-expect-error [external]: accessing private lit property
const properties = this._classProperties; const properties = this.elementProperties;
if (properties.get(name) && properties.get(name).hasChanged) { if (properties.get(name) && properties.get(name).hasChanged) {
return properties.get(name).hasChanged(newValue, oldValue); return properties.get(name).hasChanged(newValue, oldValue);
} }
@ -74,8 +74,10 @@ const SyncUpdatableMixinImplementation = superclass =>
/** @private */ /** @private */
__syncUpdatableInitialize() { __syncUpdatableInitialize() {
const ns = this.__SyncUpdatableNamespace; const ns = this.__SyncUpdatableNamespace;
const ctor = /** @type {typeof SyncUpdatableMixin & typeof import('../../types/utils/SyncUpdatableMixinTypes').SyncUpdatableHost} */ (this const ctor =
.constructor); /** @type {typeof SyncUpdatableMixin & typeof import('../../types/utils/SyncUpdatableMixinTypes').SyncUpdatableHost} */ (
this.constructor
);
ns.initialized = true; ns.initialized = true;
// Empty queue... // Empty queue...
@ -93,14 +95,16 @@ const SyncUpdatableMixinImplementation = superclass =>
* @param {string} name * @param {string} name
* @param {*} oldValue * @param {*} oldValue
*/ */
requestUpdateInternal(name, oldValue) { requestUpdate(name, oldValue) {
super.requestUpdateInternal(name, oldValue); super.requestUpdate(name, oldValue);
this.__SyncUpdatableNamespace = this.__SyncUpdatableNamespace || {}; this.__SyncUpdatableNamespace = this.__SyncUpdatableNamespace || {};
const ns = this.__SyncUpdatableNamespace; const ns = this.__SyncUpdatableNamespace;
const ctor = /** @type {typeof SyncUpdatableMixin & typeof import('../../types/utils/SyncUpdatableMixinTypes').SyncUpdatableHost} */ (this const ctor =
.constructor); /** @type {typeof SyncUpdatableMixin & typeof import('../../types/utils/SyncUpdatableMixinTypes').SyncUpdatableHost} */ (
this.constructor
);
// Before connectedCallback: queue // Before connectedCallback: queue
if (!ns.initialized) { if (!ns.initialized) {
ns.queue = ns.queue || new Set(); ns.queue = ns.queue || new Set();
@ -114,7 +118,7 @@ const SyncUpdatableMixinImplementation = superclass =>
} }
/** /**
* An abstraction that has the exact same api as `requestUpdateInternal`, but taking * An abstraction that has the exact same api as `requestUpdate`, but taking
* into account: * into account:
* - [member order independence](https://github.com/webcomponents/gold-standard/wiki/Member-Order-Independence) * - [member order independence](https://github.com/webcomponents/gold-standard/wiki/Member-Order-Independence)
* - property effects start when all (light) dom has initialized (on firstUpdated) * - property effects start when all (light) dom has initialized (on firstUpdated)
@ -122,7 +126,7 @@ const SyncUpdatableMixinImplementation = superclass =>
* - compatible with propertyAccessor.`hasChanged`: no manual checks needed or accidentally * - compatible with propertyAccessor.`hasChanged`: no manual checks needed or accidentally
* run property effects / events when no change happened * run property effects / events when no change happened
* effects when values didn't change * effects when values didn't change
* All code previously present in requestUpdateInternal can be placed in this method. * All code previously present in requestUpdate can be placed in this method.
* @param {string} name * @param {string} name
* @param {*} oldValue * @param {*} oldValue
*/ */

View file

@ -39,8 +39,10 @@ export const ValidateMixinImplementation = superclass =>
SyncUpdatableMixin(DisabledMixin(SlotMixin(ScopedElementsMixin(superclass)))), SyncUpdatableMixin(DisabledMixin(SlotMixin(ScopedElementsMixin(superclass)))),
) { ) {
static get scopedElements() { static get scopedElements() {
const scopedElementsCtor = /** @type {typeof import('@open-wc/scoped-elements/src/types').ScopedElementsHost} */ (super const scopedElementsCtor =
.constructor); /** @type {typeof import('@open-wc/scoped-elements/src/types').ScopedElementsHost} */ (
super.constructor
);
return { return {
...scopedElementsCtor.scopedElements, ...scopedElementsCtor.scopedElements,
'lion-validation-feedback': LionValidationFeedback, 'lion-validation-feedback': LionValidationFeedback,
@ -482,10 +484,12 @@ export const ValidateMixinImplementation = superclass =>
* @private * @private
*/ */
__executeResultValidators(regularValidationResult) { __executeResultValidators(regularValidationResult) {
const resultValidators = /** @type {ResultValidator[]} */ (this._allValidators.filter(v => { const resultValidators = /** @type {ResultValidator[]} */ (
const vCtor = /** @type {typeof Validator} */ (v.constructor); this._allValidators.filter(v => {
return !vCtor.async && v instanceof ResultValidator; const vCtor = /** @type {typeof Validator} */ (v.constructor);
})); return !vCtor.async && v instanceof ResultValidator;
})
);
return resultValidators.filter(v => return resultValidators.filter(v =>
v.executeOnResults({ v.executeOnResults({
@ -511,8 +515,10 @@ export const ValidateMixinImplementation = superclass =>
this.__validationResult = [...resultOutCome, ...syncAndAsyncOutcome]; this.__validationResult = [...resultOutCome, ...syncAndAsyncOutcome];
// this._storeResultsOnInstance(this.__validationResult); // this._storeResultsOnInstance(this.__validationResult);
const ctor = /** @type {typeof import('../../types/validate/ValidateMixinTypes').ValidateHost} */ (this const ctor =
.constructor); /** @type {typeof import('../../types/validate/ValidateMixinTypes').ValidateHost} */ (
this.constructor
);
/** @type {Object.<string, Object.<string, boolean>>} */ /** @type {Object.<string, Object.<string, boolean>>} */
const validationStates = ctor.validationTypes.reduce( const validationStates = ctor.validationTypes.reduce(
@ -582,8 +588,10 @@ export const ValidateMixinImplementation = superclass =>
console.error(errorMessage, this); console.error(errorMessage, this);
throw new Error(errorMessage); throw new Error(errorMessage);
} }
const ctor = /** @type {typeof import('../../types/validate/ValidateMixinTypes').ValidateHost} */ (this const ctor =
.constructor); /** @type {typeof import('../../types/validate/ValidateMixinTypes').ValidateHost} */ (
this.constructor
);
if (ctor.validationTypes.indexOf(v.type) === -1) { if (ctor.validationTypes.indexOf(v.type) === -1) {
const vCtor = /** @type {typeof Validator} */ (v.constructor); const vCtor = /** @type {typeof Validator} */ (v.constructor);
// throws in constructor are not visible to end user so we do both // throws in constructor are not visible to end user so we do both
@ -776,12 +784,16 @@ export const ValidateMixinImplementation = superclass =>
changedProperties.has('shouldShowFeedbackFor') || changedProperties.has('shouldShowFeedbackFor') ||
changedProperties.has('hasFeedbackFor') changedProperties.has('hasFeedbackFor')
) { ) {
const ctor = /** @type {typeof import('../../types/validate/ValidateMixinTypes').ValidateHost} */ (this const ctor =
.constructor); /** @type {typeof import('../../types/validate/ValidateMixinTypes').ValidateHost} */ (
this.constructor
);
// Necessary typecast because types aren't smart enough to understand that we filter out undefined // Necessary typecast because types aren't smart enough to understand that we filter out undefined
this.showsFeedbackFor = /** @type {string[]} */ (ctor.validationTypes this.showsFeedbackFor = /** @type {string[]} */ (
.map(type => (this._hasFeedbackVisibleFor(type) ? type : undefined)) ctor.validationTypes
.filter(Boolean)); .map(type => (this._hasFeedbackVisibleFor(type) ? type : undefined))
.filter(Boolean)
);
this._updateFeedbackComponent(); this._updateFeedbackComponent();
} }
@ -791,9 +803,9 @@ export const ValidateMixinImplementation = superclass =>
} }
if (changedProperties.has('validationStates')) { if (changedProperties.has('validationStates')) {
const prevStates = /** @type {{[key: string]: object;}} */ (changedProperties.get( const prevStates = /** @type {{[key: string]: object;}} */ (
'validationStates', changedProperties.get('validationStates')
)); );
if (prevStates) { if (prevStates) {
Object.entries(this.validationStates).forEach(([type, feedbackObj]) => { Object.entries(this.validationStates).forEach(([type, feedbackObj]) => {
if ( if (
@ -811,21 +823,25 @@ export const ValidateMixinImplementation = superclass =>
* @protected * @protected
*/ */
_updateShouldShowFeedbackFor() { _updateShouldShowFeedbackFor() {
const ctor = /** @type {typeof import('../../types/validate/ValidateMixinTypes').ValidateHost} */ (this const ctor =
.constructor); /** @type {typeof import('../../types/validate/ValidateMixinTypes').ValidateHost} */ (
this.constructor
);
// Necessary typecast because types aren't smart enough to understand that we filter out undefined // Necessary typecast because types aren't smart enough to understand that we filter out undefined
const newShouldShowFeedbackFor = /** @type {string[]} */ (ctor.validationTypes const newShouldShowFeedbackFor = /** @type {string[]} */ (
.map(type => ctor.validationTypes
this.feedbackCondition( .map(type =>
type, this.feedbackCondition(
this._feedbackConditionMeta, type,
this._showFeedbackConditionFor.bind(this), this._feedbackConditionMeta,
this._showFeedbackConditionFor.bind(this),
)
? type
: undefined,
) )
? type .filter(Boolean)
: undefined, );
)
.filter(Boolean));
if (JSON.stringify(this.shouldShowFeedbackFor) !== JSON.stringify(newShouldShowFeedbackFor)) { if (JSON.stringify(this.shouldShowFeedbackFor) !== JSON.stringify(newShouldShowFeedbackFor)) {
this.shouldShowFeedbackFor = newShouldShowFeedbackFor; this.shouldShowFeedbackFor = newShouldShowFeedbackFor;
@ -841,8 +857,10 @@ export const ValidateMixinImplementation = superclass =>
* @protected * @protected
*/ */
_prioritizeAndFilterFeedback({ validationResult }) { _prioritizeAndFilterFeedback({ validationResult }) {
const ctor = /** @type {typeof import('../../types/validate/ValidateMixinTypes').ValidateHost} */ (this const ctor =
.constructor); /** @type {typeof import('../../types/validate/ValidateMixinTypes').ValidateHost} */ (
this.constructor
);
const types = ctor.validationTypes; const types = ctor.validationTypes;
// Sort all validators based on the type provided. // Sort all validators based on the type provided.
const res = validationResult const res = validationResult

View file

@ -92,7 +92,8 @@ export class MinMaxLength extends Validator {
} }
} }
const isEmailRegex = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; const isEmailRegex =
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
export class IsEmail extends Validator { export class IsEmail extends Validator {
static get validatorName() { static get validatorName() {
return 'IsEmail'; return 'IsEmail';

View file

@ -36,11 +36,13 @@ export const runRegistrationSuite = customConfig => {
const { parentTagString, childTagString } = cfg; const { parentTagString, childTagString } = cfg;
it('can register a formElement', async () => { it('can register a formElement', async () => {
const el = /** @type {RegistrarClass} */ (await fixture(html` const el = /** @type {RegistrarClass} */ (
await fixture(html`
<${parentTag}> <${parentTag}>
<${childTag}></${childTag}> <${childTag}></${childTag}>
</${parentTag}> </${parentTag}>
`)); `)
);
expect(el.formElements.length).to.equal(1); expect(el.formElements.length).to.equal(1);
}); });
@ -57,25 +59,29 @@ export const runRegistrationSuite = customConfig => {
}); });
it('can register a formElement with arbitrary dom tree in between registrar and registering', async () => { it('can register a formElement with arbitrary dom tree in between registrar and registering', async () => {
const el = /** @type {RegistrarClass} */ (await fixture(html` const el = /** @type {RegistrarClass} */ (
await fixture(html`
<${parentTag}> <${parentTag}>
<div> <div>
<${childTag}></${childTag}> <${childTag}></${childTag}>
</div> </div>
</${parentTag}> </${parentTag}>
`)); `)
);
expect(el.formElements.length).to.equal(1); expect(el.formElements.length).to.equal(1);
}); });
it('supports nested registration parents', async () => { it('supports nested registration parents', async () => {
const el = /** @type {RegistrarClass} */ (await fixture(html` const el = /** @type {RegistrarClass} */ (
await fixture(html`
<${parentTag}> <${parentTag}>
<${parentTag} class="sub-group"> <${parentTag} class="sub-group">
<${childTag}></${childTag}> <${childTag}></${childTag}>
<${childTag}></${childTag}> <${childTag}></${childTag}>
</${parentTag}> </${parentTag}>
</${parentTag}> </${parentTag}>
`)); `)
);
expect(el.formElements.length).to.equal(1); expect(el.formElements.length).to.equal(1);
const subGroup = /** @type {RegistrarClass} */ (el.querySelector('.sub-group')); const subGroup = /** @type {RegistrarClass} */ (el.querySelector('.sub-group'));
@ -95,20 +101,24 @@ export const runRegistrationSuite = customConfig => {
} }
const tagWrapperString = defineCE(PerformUpdate); const tagWrapperString = defineCE(PerformUpdate);
const tagWrapper = unsafeStatic(tagWrapperString); const tagWrapper = unsafeStatic(tagWrapperString);
const el = /** @type {PerformUpdate} */ (await fixture(html` const el = /** @type {PerformUpdate} */ (
await fixture(html`
<${tagWrapper}> <${tagWrapper}>
<${childTag}></${childTag}> <${childTag}></${childTag}>
</${tagWrapper}> </${tagWrapper}>
`)); `)
);
expect(el.formElements.length).to.equal(1); expect(el.formElements.length).to.equal(1);
}); });
it('can dynamically add/remove elements', async () => { it('can dynamically add/remove elements', async () => {
const el = /** @type {RegistrarClass} */ (await fixture(html` const el = /** @type {RegistrarClass} */ (
await fixture(html`
<${parentTag}> <${parentTag}>
<${childTag}></${childTag}> <${childTag}></${childTag}>
</${parentTag}> </${parentTag}>
`)); `)
);
const newField = await fixture(html` const newField = await fixture(html`
<${childTag}></${childTag}> <${childTag}></${childTag}>
`); `);
@ -122,20 +132,24 @@ export const runRegistrationSuite = customConfig => {
}); });
it('adds elements to formElements in the right order (DOM)', async () => { it('adds elements to formElements in the right order (DOM)', async () => {
const el = /** @type {RegistrarClass} */ (await fixture(html` const el = /** @type {RegistrarClass} */ (
await fixture(html`
<${parentTag}> <${parentTag}>
<${childTag} pos="0"></${childTag}> <${childTag} pos="0"></${childTag}>
<${childTag} pos="1"></${childTag}> <${childTag} pos="1"></${childTag}>
<${childTag} pos="2"></${childTag}> <${childTag} pos="2"></${childTag}>
</${parentTag}> </${parentTag}>
`)); `)
);
/** INSERT field before the pos=1 */ /** INSERT field before the pos=1 */
/** /**
* @typedef {Object.<string, string>} prop * @typedef {Object.<string, string>} prop
*/ */
const newField = /** @type {RegisteringClass & prop} */ (await fixture(html` const newField = /** @type {RegisteringClass & prop} */ (
await fixture(html`
<${childTag}></${childTag}> <${childTag}></${childTag}>
`)); `)
);
newField.setAttribute('pos', 'inserted-before-1'); newField.setAttribute('pos', 'inserted-before-1');
el.insertBefore(newField, el.children[1]); el.insertBefore(newField, el.children[1]);
@ -145,9 +159,11 @@ export const runRegistrationSuite = customConfig => {
expect(el.formElements[1].getAttribute('pos')).to.equal('inserted-before-1'); expect(el.formElements[1].getAttribute('pos')).to.equal('inserted-before-1');
/** INSERT field before the pos=0 (e.g. at the top) */ /** INSERT field before the pos=0 (e.g. at the top) */
const topField = /** @type {RegisteringClass & prop} */ (await fixture(html` const topField = /** @type {RegisteringClass & prop} */ (
await fixture(html`
<${childTag}></${childTag}> <${childTag}></${childTag}>
`)); `)
);
topField.setAttribute('pos', 'inserted-before-0'); topField.setAttribute('pos', 'inserted-before-0');
el.insertBefore(topField, el.children[0]); el.insertBefore(topField, el.children[0]);
@ -159,9 +175,9 @@ export const runRegistrationSuite = customConfig => {
describe('FormRegistrarPortalMixin', () => { describe('FormRegistrarPortalMixin', () => {
it('forwards registrations to the .registrationTarget', async () => { it('forwards registrations to the .registrationTarget', async () => {
const el = /** @type {RegistrarClass} */ (await fixture( const el = /** @type {RegistrarClass} */ (
html`<${parentTag}></${parentTag}>`, await fixture(html`<${parentTag}></${parentTag}>`)
)); );
await fixture(html` await fixture(html`
<${portalTag} .registrationTarget=${el}> <${portalTag} .registrationTarget=${el}>
<${childTag}></${childTag}> <${childTag}></${childTag}>
@ -172,9 +188,9 @@ export const runRegistrationSuite = customConfig => {
}); });
it('can dynamically add/remove elements', async () => { it('can dynamically add/remove elements', async () => {
const el = /** @type {RegistrarClass} */ (await fixture( const el = /** @type {RegistrarClass} */ (
html`<${parentTag}></${parentTag}>`, await fixture(html`<${parentTag}></${parentTag}>`)
)); );
const portal = await fixture(html` const portal = await fixture(html`
<${portalTag} .registrationTarget=${el}> <${portalTag} .registrationTarget=${el}>
<${childTag}></${childTag}> <${childTag}></${childTag}>
@ -194,13 +210,15 @@ export const runRegistrationSuite = customConfig => {
}); });
it('adds elements to formElements in the right order', async () => { it('adds elements to formElements in the right order', async () => {
const el = /** @type {RegistrarClass} */ (await fixture(html` const el = /** @type {RegistrarClass} */ (
await fixture(html`
<${parentTag}> <${parentTag}>
<${childTag}></${childTag}> <${childTag}></${childTag}>
<${childTag}></${childTag}> <${childTag}></${childTag}>
<${childTag}></${childTag}> <${childTag}></${childTag}>
</${parentTag}> </${parentTag}>
`)); `)
);
expect(el.formElements.length).to.equal(3); expect(el.formElements.length).to.equal(3);
@ -232,9 +250,9 @@ export const runRegistrationSuite = customConfig => {
}); });
it('keeps working if moving the portal itself', async () => { it('keeps working if moving the portal itself', async () => {
const el = /** @type {RegistrarClass} */ (await fixture( const el = /** @type {RegistrarClass} */ (
html`<${parentTag}></${parentTag}>`, await fixture(html`<${parentTag}></${parentTag}>`)
)); );
const portal = await fixture(html` const portal = await fixture(html`
<${portalTag} .registrationTarget=${el}> <${portalTag} .registrationTarget=${el}>
<${childTag}></${childTag}> <${childTag}></${childTag}>
@ -270,9 +288,9 @@ export const runRegistrationSuite = customConfig => {
); );
const delayedPortalTag = unsafeStatic(delayedPortalString); const delayedPortalTag = unsafeStatic(delayedPortalString);
const el = /** @type {RegistrarClass} */ (await fixture( const el = /** @type {RegistrarClass} */ (
html`<${parentTag}></${parentTag}>`, await fixture(html`<${parentTag}></${parentTag}>`)
)); );
await fixture(html` await fixture(html`
<${delayedPortalTag} .registrationTarget=${el}> <${delayedPortalTag} .registrationTarget=${el}>
<${childTag}></${childTag}> <${childTag}></${childTag}>

View file

@ -1,6 +1,8 @@
import { LitElement } from '@lion/core'; import { LitElement } from '@lion/core';
import { parseDate } from '@lion/localize'; import { parseDate } from '@lion/localize';
import { aTimeout, defineCE, expect, fixture, html, unsafeStatic } from '@open-wc/testing'; import { aTimeout, defineCE, expect, fixture } from '@open-wc/testing';
import { html, unsafeStatic } from 'lit/static-html.js';
import sinon from 'sinon'; import sinon from 'sinon';
import { FormatMixin } from '../src/FormatMixin.js'; import { FormatMixin } from '../src/FormatMixin.js';
import { Unparseable, Validator } from '../index.js'; import { Unparseable, Validator } from '../index.js';
@ -95,7 +97,7 @@ export function runFormatMixinSuite(customConfig) {
} }
describe('FormatMixin', async () => { describe('FormatMixin', async () => {
/** @type {{d: any}} */ /** @type {{_$litStatic$: any}} */
let tag; let tag;
/** @type {FormatClass} */ /** @type {FormatClass} */
let nonFormat; let nonFormat;
@ -148,9 +150,9 @@ export function runFormatMixinSuite(customConfig) {
*/ */
describe('ModelValue', () => { describe('ModelValue', () => {
it('fires `model-value-changed` for every programmatic modelValue change', async () => { it('fires `model-value-changed` for every programmatic modelValue change', async () => {
const el = /** @type {FormatClass} */ (await fixture( const el = /** @type {FormatClass} */ (
html`<${tag}><input slot="input"></${tag}>`, await fixture(html`<${tag}><input slot="input"></${tag}>`)
)); );
let counter = 0; let counter = 0;
let isTriggeredByUser = false; let isTriggeredByUser = false;
@ -172,18 +174,19 @@ export function runFormatMixinSuite(customConfig) {
}); });
it('fires `model-value-changed` for every user input, adding `isTriggeredByUser` in event detail', async () => { it('fires `model-value-changed` for every user input, adding `isTriggeredByUser` in event detail', async () => {
const formatEl = /** @type {FormatClass} */ (await fixture( const formatEl = /** @type {FormatClass} */ (
html`<${tag}><input slot="input"></${tag}>`, await fixture(html`<${tag}><input slot="input"></${tag}>`)
)); );
let counter = 0; let counter = 0;
let isTriggeredByUser = false; let isTriggeredByUser = false;
formatEl.addEventListener('model-value-changed', ( formatEl.addEventListener(
/** @param {CustomEvent} event */ event, 'model-value-changed',
) => { (/** @param {CustomEvent} event */ event) => {
counter += 1; counter += 1;
isTriggeredByUser = /** @type {CustomEvent} */ (event).detail.isTriggeredByUser; isTriggeredByUser = /** @type {CustomEvent} */ (event).detail.isTriggeredByUser;
}); },
);
mimicUserInput(formatEl, generateValueBasedOnType()); mimicUserInput(formatEl, generateValueBasedOnType());
expect(counter).to.equal(1); expect(counter).to.equal(1);
@ -205,7 +208,8 @@ export function runFormatMixinSuite(customConfig) {
it('synchronizes _inputNode.value as a fallback mechanism on init (when no modelValue provided)', async () => { it('synchronizes _inputNode.value as a fallback mechanism on init (when no modelValue provided)', async () => {
// Note that in lion-field, the attribute would be put on <lion-field>, not on <input> // Note that in lion-field, the attribute would be put on <lion-field>, not on <input>
const formatElem = /** @type {FormatClass} */ (await fixture(html` const formatElem = /** @type {FormatClass} */ (
await fixture(html`
<${tag} <${tag}
value="string" value="string"
.formatter=${/** @param {string} value */ value => `foo: ${value}`} .formatter=${/** @param {string} value */ value => `foo: ${value}`}
@ -215,7 +219,8 @@ export function runFormatMixinSuite(customConfig) {
> >
<input slot="input" value="string" /> <input slot="input" value="string" />
</${tag}> </${tag}>
`)); `)
);
// Now check if the format/parse/serialize loop has been triggered // Now check if the format/parse/serialize loop has been triggered
await formatElem.updateComplete; await formatElem.updateComplete;
expect(formatElem.formattedValue).to.equal('foo: string'); expect(formatElem.formattedValue).to.equal('foo: string');
@ -228,20 +233,23 @@ export function runFormatMixinSuite(customConfig) {
describe('Unparseable values', () => { describe('Unparseable values', () => {
it('converts to Unparseable when wrong value inputted by user', async () => { it('converts to Unparseable when wrong value inputted by user', async () => {
const el = /** @type {FormatClass} */ (await fixture(html` const el = /** @type {FormatClass} */ (
await fixture(html`
<${tag} .parser=${ <${tag} .parser=${
/** @param {string} viewValue */ viewValue => Number(viewValue) || undefined /** @param {string} viewValue */ viewValue => Number(viewValue) || undefined
} }
> >
<input slot="input"> <input slot="input">
</${tag}> </${tag}>
`)); `)
);
mimicUserInput(el, 'test'); mimicUserInput(el, 'test');
expect(el.modelValue).to.be.an.instanceof(Unparseable); expect(el.modelValue).to.be.an.instanceof(Unparseable);
}); });
it('preserves the viewValue when unparseable', async () => { it('preserves the viewValue when unparseable', async () => {
const el = /** @type {FormatClass} */ (await fixture(html` const el = /** @type {FormatClass} */ (
await fixture(html`
<${tag} <${tag}
.parser=${ .parser=${
/** @param {string} viewValue */ viewValue => Number(viewValue) || undefined /** @param {string} viewValue */ viewValue => Number(viewValue) || undefined
@ -249,14 +257,16 @@ export function runFormatMixinSuite(customConfig) {
> >
<input slot="input"> <input slot="input">
</${tag}> </${tag}>
`)); `)
);
mimicUserInput(el, 'test'); mimicUserInput(el, 'test');
expect(el.formattedValue).to.equal('test'); expect(el.formattedValue).to.equal('test');
expect(el.value).to.equal('test'); expect(el.value).to.equal('test');
}); });
it('displays the viewValue when modelValue is of type Unparseable', async () => { it('displays the viewValue when modelValue is of type Unparseable', async () => {
const el = /** @type {FormatClass} */ (await fixture(html` const el = /** @type {FormatClass} */ (
await fixture(html`
<${tag} <${tag}
.parser=${ .parser=${
/** @param {string} viewValue */ viewValue => Number(viewValue) || undefined /** @param {string} viewValue */ viewValue => Number(viewValue) || undefined
@ -264,17 +274,20 @@ export function runFormatMixinSuite(customConfig) {
> >
<input slot="input"> <input slot="input">
</${tag}> </${tag}>
`)); `)
);
el.modelValue = new Unparseable('foo'); el.modelValue = new Unparseable('foo');
expect(el.value).to.equal('foo'); expect(el.value).to.equal('foo');
}); });
it('empty strings are not Unparseable', async () => { it('empty strings are not Unparseable', async () => {
const el = /** @type {FormatClass} */ (await fixture(html` const el = /** @type {FormatClass} */ (
await fixture(html`
<${tag}> <${tag}>
<input slot="input" value="string"> <input slot="input" value="string">
</${tag}> </${tag}>
`)); `)
);
// This could happen when the user erases the input value // This could happen when the user erases the input value
mimicUserInput(el, ''); mimicUserInput(el, '');
// For backwards compatibility, we keep the modelValue an empty string here. // For backwards compatibility, we keep the modelValue an empty string here.
@ -303,11 +316,13 @@ export function runFormatMixinSuite(customConfig) {
describe('Presenting value to end user', () => { describe('Presenting value to end user', () => {
it('reflects back formatted value to user on leave', async () => { it('reflects back formatted value to user on leave', async () => {
const formatEl = /** @type {FormatClass} */ (await fixture(html` const formatEl = /** @type {FormatClass} */ (
await fixture(html`
<${tag} .formatter="${/** @param {string} value */ value => `foo: ${value}`}"> <${tag} .formatter="${/** @param {string} value */ value => `foo: ${value}`}">
<input slot="input" /> <input slot="input" />
</${tag}> </${tag}>
`)); `)
);
const { _inputNode } = getFormControlMembers(formatEl); const { _inputNode } = getFormControlMembers(formatEl);
const generatedViewValue = generateValueBasedOnType({ viewValue: true }); const generatedViewValue = generateValueBasedOnType({ viewValue: true });
@ -322,11 +337,13 @@ export function runFormatMixinSuite(customConfig) {
}); });
it('reflects back .formattedValue immediately when .modelValue changed imperatively', async () => { it('reflects back .formattedValue immediately when .modelValue changed imperatively', async () => {
const el = /** @type {FormatClass} */ (await fixture(html` const el = /** @type {FormatClass} */ (
await fixture(html`
<${tag} .formatter="${/** @param {string} value */ value => `foo: ${value}`}"> <${tag} .formatter="${/** @param {string} value */ value => `foo: ${value}`}">
<input slot="input" /> <input slot="input" />
</${tag}> </${tag}>
`)); `)
);
const { _inputNode } = getFormControlMembers(el); const { _inputNode } = getFormControlMembers(el);
@ -351,7 +368,8 @@ export function runFormatMixinSuite(customConfig) {
const parserSpy = sinon.spy(value => value.replace('foo: ', '')); const parserSpy = sinon.spy(value => value.replace('foo: ', ''));
const serializerSpy = sinon.spy(value => `[foo] ${value}`); const serializerSpy = sinon.spy(value => `[foo] ${value}`);
const preprocessorSpy = sinon.spy(value => value.replace('bar', '')); const preprocessorSpy = sinon.spy(value => value.replace('bar', ''));
const el = /** @type {FormatClass} */ (await fixture(html` const el = /** @type {FormatClass} */ (
await fixture(html`
<${tag} <${tag}
.formatter=${formatterSpy} .formatter=${formatterSpy}
.parser=${parserSpy} .parser=${parserSpy}
@ -361,7 +379,8 @@ export function runFormatMixinSuite(customConfig) {
> >
<input slot="input"> <input slot="input">
</${tag}> </${tag}>
`)); `)
);
expect(formatterSpy.called).to.be.true; expect(formatterSpy.called).to.be.true;
expect(serializerSpy.called).to.be.true; expect(serializerSpy.called).to.be.true;
@ -407,11 +426,13 @@ export function runFormatMixinSuite(customConfig) {
toggleValue: true, toggleValue: true,
}); });
const el = /** @type {FormatClass} */ (await fixture(html` const el = /** @type {FormatClass} */ (
await fixture(html`
<${tag} .formatter=${formatterSpy}> <${tag} .formatter=${formatterSpy}>
<input slot="input" .value="${generatedViewValue}"> <input slot="input" .value="${generatedViewValue}">
</${tag}> </${tag}>
`)); `)
);
expect(formatterSpy.callCount).to.equal(1); expect(formatterSpy.callCount).to.equal(1);
el.hasFeedbackFor.push('error'); el.hasFeedbackFor.push('error');
@ -446,9 +467,11 @@ export function runFormatMixinSuite(customConfig) {
it('has formatOptions available in formatter', async () => { it('has formatOptions available in formatter', async () => {
const formatterSpy = sinon.spy(value => `foo: ${value}`); const formatterSpy = sinon.spy(value => `foo: ${value}`);
const generatedViewValue = /** @type {string} */ (generateValueBasedOnType({ const generatedViewValue = /** @type {string} */ (
viewValue: true, generateValueBasedOnType({
})); viewValue: true,
})
);
await fixture(html` await fixture(html`
<${tag} value="${generatedViewValue}" .formatter="${formatterSpy}" <${tag} value="${generatedViewValue}" .formatter="${formatterSpy}"
.formatOptions="${{ locale: 'en-GB', decimalSeparator: '-' }}"> .formatOptions="${{ locale: 'en-GB', decimalSeparator: '-' }}">
@ -483,9 +506,11 @@ export function runFormatMixinSuite(customConfig) {
} }
it('sets formatOptions.mode to "pasted" (and restores to "auto")', async () => { it('sets formatOptions.mode to "pasted" (and restores to "auto")', async () => {
const el = /** @type {FormatClass} */ (await fixture(html` const el = /** @type {FormatClass} */ (
await fixture(html`
<${reflectingTag}><input slot="input"></${reflectingTag}> <${reflectingTag}><input slot="input"></${reflectingTag}>
`)); `)
);
const formatterSpy = sinon.spy(el, 'formatter'); const formatterSpy = sinon.spy(el, 'formatter');
paste(el); paste(el);
expect(formatterSpy).to.be.called; expect(formatterSpy).to.be.called;
@ -496,9 +521,11 @@ export function runFormatMixinSuite(customConfig) {
}); });
it('sets protected value "_isPasting" for Subclassers', async () => { it('sets protected value "_isPasting" for Subclassers', async () => {
const el = /** @type {FormatClass} */ (await fixture(html` const el = /** @type {FormatClass} */ (
await fixture(html`
<${reflectingTag}><input slot="input"></${reflectingTag}> <${reflectingTag}><input slot="input"></${reflectingTag}>
`)); `)
);
const formatterSpy = sinon.spy(el, 'formatter'); const formatterSpy = sinon.spy(el, 'formatter');
paste(el); paste(el);
expect(formatterSpy).to.have.been.called; expect(formatterSpy).to.have.been.called;
@ -510,9 +537,11 @@ export function runFormatMixinSuite(customConfig) {
}); });
it('calls formatter and "_reflectBackOn()"', async () => { it('calls formatter and "_reflectBackOn()"', async () => {
const el = /** @type {FormatClass} */ (await fixture(html` const el = /** @type {FormatClass} */ (
await fixture(html`
<${tag}><input slot="input"></${tag}> <${tag}><input slot="input"></${tag}>
`)); `)
);
// @ts-ignore [allow-protected] in test // @ts-ignore [allow-protected] in test
const reflectBackSpy = sinon.spy(el, '_reflectBackOn'); const reflectBackSpy = sinon.spy(el, '_reflectBackOn');
paste(el); paste(el);
@ -520,9 +549,11 @@ export function runFormatMixinSuite(customConfig) {
}); });
it(`updates viewValue when "_reflectBackOn()" configured to reflect`, async () => { it(`updates viewValue when "_reflectBackOn()" configured to reflect`, async () => {
const el = /** @type {FormatClass} */ (await fixture(html` const el = /** @type {FormatClass} */ (
await fixture(html`
<${reflectingTag}><input slot="input"></${reflectingTag}> <${reflectingTag}><input slot="input"></${reflectingTag}>
`)); `)
);
// @ts-ignore [allow-protected] in test // @ts-ignore [allow-protected] in test
const reflectBackSpy = sinon.spy(el, '_reflectBackOn'); const reflectBackSpy = sinon.spy(el, '_reflectBackOn');
paste(el); paste(el);
@ -536,11 +567,13 @@ export function runFormatMixinSuite(customConfig) {
/** @type {?} */ /** @type {?} */
const generatedValue = generateValueBasedOnType(); const generatedValue = generateValueBasedOnType();
const parserSpy = sinon.spy(); const parserSpy = sinon.spy();
const el = /** @type {FormatClass} */ (await fixture(html` const el = /** @type {FormatClass} */ (
await fixture(html`
<${tag} .parser="${parserSpy}"> <${tag} .parser="${parserSpy}">
<input slot="input" .value="${generatedValue}"> <input slot="input" .value="${generatedValue}">
</${tag}> </${tag}>
`)); `)
);
expect(parserSpy.callCount).to.equal(1); expect(parserSpy.callCount).to.equal(1);
// This could happen for instance in a reset // This could happen for instance in a reset
@ -562,11 +595,13 @@ export function runFormatMixinSuite(customConfig) {
const toBeCorrectedVal = `${val}$`; const toBeCorrectedVal = `${val}$`;
const preprocessorSpy = sinon.spy(v => v.replace(/\$$/g, '')); const preprocessorSpy = sinon.spy(v => v.replace(/\$$/g, ''));
const el = /** @type {FormatClass} */ (await fixture(html` const el = /** @type {FormatClass} */ (
await fixture(html`
<${tag} .preprocessor=${preprocessorSpy}> <${tag} .preprocessor=${preprocessorSpy}>
<input slot="input"> <input slot="input">
</${tag}> </${tag}>
`)); `)
);
const { _inputNode } = getFormControlMembers(el); const { _inputNode } = getFormControlMembers(el);
@ -581,11 +616,13 @@ export function runFormatMixinSuite(customConfig) {
}); });
it('does not preprocess during composition', async () => { it('does not preprocess during composition', async () => {
const el = /** @type {FormatClass} */ (await fixture(html` const el = /** @type {FormatClass} */ (
await fixture(html`
<${tag} .preprocessor=${(/** @type {string} */ v) => v.replace(/\$$/g, '')}> <${tag} .preprocessor=${(/** @type {string} */ v) => v.replace(/\$$/g, '')}>
<input slot="input"> <input slot="input">
</${tag}> </${tag}>
`)); `)
);
const { _inputNode } = getFormControlMembers(el); const { _inputNode } = getFormControlMembers(el);

View file

@ -82,9 +82,9 @@ export function runInteractionStateMixinSuite(customConfig) {
}); });
it('sets an attribute "filled" if the input has a non-empty modelValue', async () => { it('sets an attribute "filled" if the input has a non-empty modelValue', async () => {
const el = /** @type {IState} */ (await fixture( const el = /** @type {IState} */ (
html`<${tag} .modelValue=${'hello'}></${tag}>`, await fixture(html`<${tag} .modelValue=${'hello'}></${tag}>`)
)); );
expect(el.hasAttribute('filled')).to.equal(true); expect(el.hasAttribute('filled')).to.equal(true);
el.modelValue = ''; el.modelValue = '';
await el.updateComplete; await el.updateComplete;
@ -97,9 +97,11 @@ export function runInteractionStateMixinSuite(customConfig) {
it('fires "(touched|dirty)-state-changed" event when state changes', async () => { it('fires "(touched|dirty)-state-changed" event when state changes', async () => {
const touchedSpy = sinon.spy(); const touchedSpy = sinon.spy();
const dirtySpy = sinon.spy(); const dirtySpy = sinon.spy();
const el = /** @type {IState} */ (await fixture( const el = /** @type {IState} */ (
html`<${tag} @touched-changed=${touchedSpy} @dirty-changed=${dirtySpy}></${tag}>`, await fixture(
)); html`<${tag} @touched-changed=${touchedSpy} @dirty-changed=${dirtySpy}></${tag}>`,
)
);
el.touched = true; el.touched = true;
expect(touchedSpy.callCount).to.equal(1); expect(touchedSpy.callCount).to.equal(1);
@ -109,14 +111,18 @@ export function runInteractionStateMixinSuite(customConfig) {
}); });
it('sets prefilled once instantiated', async () => { it('sets prefilled once instantiated', async () => {
const el = /** @type {IState} */ (await fixture(html` const el = /** @type {IState} */ (
await fixture(html`
<${tag} .modelValue=${'prefilled'}></${tag}> <${tag} .modelValue=${'prefilled'}></${tag}>
`)); `)
);
expect(el.prefilled).to.be.true; expect(el.prefilled).to.be.true;
const nonPrefilled = /** @type {IState} */ (await fixture(html` const nonPrefilled = /** @type {IState} */ (
await fixture(html`
<${tag} .modelValue=${''}></${tag}> <${tag} .modelValue=${''}></${tag}>
`)); `)
);
expect(nonPrefilled.prefilled).to.be.false; expect(nonPrefilled.prefilled).to.be.false;
}); });
@ -125,9 +131,9 @@ export function runInteractionStateMixinSuite(customConfig) {
(${cfg.allowedModelValueTypes.map(t => t.name).join(', ')})`, async () => { (${cfg.allowedModelValueTypes.map(t => t.name).join(', ')})`, async () => {
/** @typedef {{_inputNode: HTMLElement}} inputNodeInterface */ /** @typedef {{_inputNode: HTMLElement}} inputNodeInterface */
const el = /** @type {IState & inputNodeInterface} */ (await fixture( const el = /** @type {IState & inputNodeInterface} */ (
html`<${tag}></${tag}>`, await fixture(html`<${tag}></${tag}>`)
)); );
/** /**
* @param {*} modelValue * @param {*} modelValue
@ -213,9 +219,11 @@ export function runInteractionStateMixinSuite(customConfig) {
describe('Validation integration with states', () => { describe('Validation integration with states', () => {
it('has .shouldShowFeedbackFor indicating for which type to show messages', async () => { it('has .shouldShowFeedbackFor indicating for which type to show messages', async () => {
const el = /** @type {IState} */ (await fixture(html` const el = /** @type {IState} */ (
await fixture(html`
<${tag}></${tag}> <${tag}></${tag}>
`)); `)
);
// @ts-ignore [allow-private] in test // @ts-ignore [allow-private] in test
expect(el.shouldShowFeedbackFor).to.deep.equal([]); expect(el.shouldShowFeedbackFor).to.deep.equal([]);
el.submitted = true; el.submitted = true;
@ -225,9 +233,11 @@ export function runInteractionStateMixinSuite(customConfig) {
}); });
it('keeps the feedback component in sync', async () => { it('keeps the feedback component in sync', async () => {
const el = /** @type {IState} */ (await fixture(html` const el = /** @type {IState} */ (
await fixture(html`
<${tag} .validators=${[new MinLength(3)]}></${tag}> <${tag} .validators=${[new MinLength(3)]}></${tag}>
`)); `)
);
const { _feedbackNode } = getFormControlMembers(el); const { _feedbackNode } = getFormControlMembers(el);
await el.updateComplete; await el.updateComplete;
@ -257,9 +267,9 @@ export function runInteractionStateMixinSuite(customConfig) {
} }
const tagLeaveString = defineCE(IStateCustomBlur); const tagLeaveString = defineCE(IStateCustomBlur);
const tagLeave = unsafeStatic(tagLeaveString); const tagLeave = unsafeStatic(tagLeaveString);
const el = /** @type {IStateCustomBlur} */ (await fixture( const el = /** @type {IStateCustomBlur} */ (
html`<${tagLeave}></${tagLeave}>`, await fixture(html`<${tagLeave}></${tagLeave}>`)
)); );
el.dispatchEvent(new Event('custom-blur')); el.dispatchEvent(new Event('custom-blur'));
expect(el.touched).to.be.true; expect(el.touched).to.be.true;
}); });

View file

@ -117,21 +117,25 @@ export function runValidateMixinSuite(customConfig) {
}); });
it('validates on initialization (once form field has bootstrapped/initialized)', async () => { it('validates on initialization (once form field has bootstrapped/initialized)', async () => {
const el = /** @type {ValidateElement} */ (await fixture(html` const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag} <${tag}
.validators=${[new Required()]} .validators=${[new Required()]}
>${lightDom}</${tag}> >${lightDom}</${tag}>
`)); `)
);
expect(el.hasFeedbackFor).to.deep.equal(['error']); expect(el.hasFeedbackFor).to.deep.equal(['error']);
}); });
it('revalidates when ".modelValue" changes', async () => { it('revalidates when ".modelValue" changes', async () => {
const el = /** @type {ValidateElement} */ (await fixture(html` const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag} <${tag}
.validators=${[new AlwaysValid()]} .validators=${[new AlwaysValid()]}
.modelValue=${'myValue'} .modelValue=${'myValue'}
>${lightDom}</${tag}> >${lightDom}</${tag}>
`)); `)
);
const validateSpy = sinon.spy(el, 'validate'); const validateSpy = sinon.spy(el, 'validate');
el.modelValue = 'x'; el.modelValue = 'x';
@ -139,13 +143,15 @@ export function runValidateMixinSuite(customConfig) {
}); });
it('revalidates when child ".modelValue" changes', async () => { it('revalidates when child ".modelValue" changes', async () => {
const el = /** @type {ValidateElement} */ (await fixture(html` const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag} <${tag}
._repropagationRole="${'fieldset'}" ._repropagationRole="${'fieldset'}"
.validators=${[new AlwaysValid()]} .validators=${[new AlwaysValid()]}
.modelValue=${'myValue'} .modelValue=${'myValue'}
><lion-field id="child"><input slot="input"></lion-field></${tag}> ><lion-field id="child"><input slot="input"></lion-field></${tag}>
`)); `)
);
const validateSpy = sinon.spy(el, 'validate'); const validateSpy = sinon.spy(el, 'validate');
/** @type {LionField} */ (el.querySelector('#child')).modelValue = 'test'; /** @type {LionField} */ (el.querySelector('#child')).modelValue = 'test';
await el.updateComplete; await el.updateComplete;
@ -153,12 +159,14 @@ export function runValidateMixinSuite(customConfig) {
}); });
it('revalidates when ".validators" changes', async () => { it('revalidates when ".validators" changes', async () => {
const el = /** @type {ValidateElement} */ (await fixture(html` const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag} <${tag}
.validators=${[new AlwaysValid()]} .validators=${[new AlwaysValid()]}
.modelValue=${'myValue'} .modelValue=${'myValue'}
>${lightDom}</${tag}> >${lightDom}</${tag}>
`)); `)
);
const validateSpy = sinon.spy(el, 'validate'); const validateSpy = sinon.spy(el, 'validate');
el.validators = [new MinLength(3)]; el.validators = [new MinLength(3)];
@ -166,12 +174,14 @@ export function runValidateMixinSuite(customConfig) {
}); });
it('clears current results when ".modelValue" changes', async () => { it('clears current results when ".modelValue" changes', async () => {
const el = /** @type {ValidateElement} */ (await fixture(html` const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag} <${tag}
.validators=${[new AlwaysValid()]} .validators=${[new AlwaysValid()]}
.modelValue=${'myValue'} .modelValue=${'myValue'}
>${lightDom}</${tag}> >${lightDom}</${tag}>
`)); `)
);
// @ts-ignore [allow-private] in test // @ts-ignore [allow-private] in test
const clearSpy = sinon.spy(el, '__clearValidationResults'); const clearSpy = sinon.spy(el, '__clearValidationResults');
@ -192,9 +202,11 @@ export function runValidateMixinSuite(customConfig) {
it('firstly checks for empty values', async () => { it('firstly checks for empty values', async () => {
const alwaysValid = new AlwaysValid(); const alwaysValid = new AlwaysValid();
const alwaysValidExecuteSpy = sinon.spy(alwaysValid, 'execute'); const alwaysValidExecuteSpy = sinon.spy(alwaysValid, 'execute');
const el = /** @type {ValidateElement} */ (await fixture(html` const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag} .validators=${[alwaysValid]}>${lightDom}</${tag}> <${tag} .validators=${[alwaysValid]}>${lightDom}</${tag}>
`)); `)
);
// @ts-ignore [allow-private] in test // @ts-ignore [allow-private] in test
const isEmptySpy = sinon.spy(el, '__isEmpty'); const isEmptySpy = sinon.spy(el, '__isEmpty');
const validateSpy = sinon.spy(el, 'validate'); const validateSpy = sinon.spy(el, 'validate');
@ -210,9 +222,11 @@ export function runValidateMixinSuite(customConfig) {
}); });
it('secondly checks for synchronous Validators: creates RegularValidationResult', async () => { it('secondly checks for synchronous Validators: creates RegularValidationResult', async () => {
const el = /** @type {ValidateElement} */ (await fixture(html` const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag} .validators=${[new AlwaysValid()]}>${lightDom}</${tag}> <${tag} .validators=${[new AlwaysValid()]}>${lightDom}</${tag}>
`)); `)
);
// @ts-ignore [allow-private] in test // @ts-ignore [allow-private] in test
const isEmptySpy = sinon.spy(el, '__isEmpty'); const isEmptySpy = sinon.spy(el, '__isEmpty');
// @ts-ignore [allow-private] in test // @ts-ignore [allow-private] in test
@ -222,11 +236,13 @@ export function runValidateMixinSuite(customConfig) {
}); });
it('thirdly schedules asynchronous Validators: creates RegularValidationResult', async () => { it('thirdly schedules asynchronous Validators: creates RegularValidationResult', async () => {
const el = /** @type {ValidateElement} */ (await fixture(html` const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag} .validators=${[new AlwaysValid(), new AsyncAlwaysValid()]}> <${tag} .validators=${[new AlwaysValid(), new AsyncAlwaysValid()]}>
${lightDom} ${lightDom}
</${tag}> </${tag}>
`)); `)
);
// @ts-ignore [allow-private] in test // @ts-ignore [allow-private] in test
const syncSpy = sinon.spy(el, '__executeSyncValidators'); const syncSpy = sinon.spy(el, '__executeSyncValidators');
// @ts-ignore [allow-private] in test // @ts-ignore [allow-private] in test
@ -242,12 +258,14 @@ export function runValidateMixinSuite(customConfig) {
} }
} }
let el = /** @type {ValidateElement} */ (await fixture(html` let el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag} <${tag}
.validators=${[new AlwaysValid(), new MyResult()]}> .validators=${[new AlwaysValid(), new MyResult()]}>
${lightDom} ${lightDom}
</${tag}> </${tag}>
`)); `)
);
// @ts-ignore [allow-private] in test // @ts-ignore [allow-private] in test
const syncSpy = sinon.spy(el, '__executeSyncValidators'); const syncSpy = sinon.spy(el, '__executeSyncValidators');
@ -278,11 +296,13 @@ export function runValidateMixinSuite(customConfig) {
describe('Finalization', () => { describe('Finalization', () => {
it('fires private "validate-performed" event on every cycle', async () => { it('fires private "validate-performed" event on every cycle', async () => {
const el = /** @type {ValidateElement} */ (await fixture(html` const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag} .validators=${[new AlwaysValid(), new AsyncAlwaysInvalid()]}> <${tag} .validators=${[new AlwaysValid(), new AsyncAlwaysInvalid()]}>
${lightDom} ${lightDom}
</${tag}> </${tag}>
`)); `)
);
const cbSpy = sinon.spy(); const cbSpy = sinon.spy();
el.addEventListener('validate-performed', cbSpy); el.addEventListener('validate-performed', cbSpy);
el.modelValue = 'nonEmpty'; el.modelValue = 'nonEmpty';
@ -290,11 +310,13 @@ export function runValidateMixinSuite(customConfig) {
}); });
it('resolves ".validateComplete" Promise', async () => { it('resolves ".validateComplete" Promise', async () => {
const el = /** @type {ValidateElement} */ (await fixture(html` const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag} .validators=${[new AsyncAlwaysInvalid()]}> <${tag} .validators=${[new AsyncAlwaysInvalid()]}>
${lightDom} ${lightDom}
</${tag}> </${tag}>
`)); `)
);
el.modelValue = 'nonEmpty'; el.modelValue = 'nonEmpty';
// @ts-ignore [allow-private] in test // @ts-ignore [allow-private] in test
const validateResolveSpy = sinon.spy(el, '__validateCompleteResolve'); const validateResolveSpy = sinon.spy(el, '__validateCompleteResolve');
@ -395,9 +417,11 @@ export function runValidateMixinSuite(customConfig) {
}); });
it('Validators will not be called on empty values', async () => { it('Validators will not be called on empty values', async () => {
const el = /** @type {ValidateElement} */ (await fixture(html` const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag} .validators=${[new IsCat()]}>${lightDom}</${tag}> <${tag} .validators=${[new IsCat()]}>${lightDom}</${tag}>
`)); `)
);
el.modelValue = 'cat'; el.modelValue = 'cat';
expect(el.validationStates.error.IsCat).to.be.undefined; expect(el.validationStates.error.IsCat).to.be.undefined;
@ -410,12 +434,14 @@ export function runValidateMixinSuite(customConfig) {
it('Validators get retriggered on parameter change', async () => { it('Validators get retriggered on parameter change', async () => {
const isCatValidator = new IsCat('Felix'); const isCatValidator = new IsCat('Felix');
const catSpy = sinon.spy(isCatValidator, 'execute'); const catSpy = sinon.spy(isCatValidator, 'execute');
const el = /** @type {ValidateElement} */ (await fixture(html` const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag} <${tag}
.validators=${[isCatValidator]} .validators=${[isCatValidator]}
.modelValue=${'cat'} .modelValue=${'cat'}
>${lightDom}</${tag}> >${lightDom}</${tag}>
`)); `)
);
el.modelValue = 'cat'; el.modelValue = 'cat';
expect(catSpy.callCount).to.equal(1); expect(catSpy.callCount).to.equal(1);
isCatValidator.param = 'Garfield'; isCatValidator.param = 'Garfield';
@ -459,13 +485,15 @@ export function runValidateMixinSuite(customConfig) {
// default execution trigger is keyup (think of password availability backend) // default execution trigger is keyup (think of password availability backend)
// can configure execution trigger (blur, etc?) // can configure execution trigger (blur, etc?)
it('handles "execute" functions returning promises', async () => { it('handles "execute" functions returning promises', async () => {
const el = /** @type {ValidateElement} */ (await fixture(html` const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag} <${tag}
.modelValue=${'dog'} .modelValue=${'dog'}
.validators=${[new IsAsyncCat()]}> .validators=${[new IsAsyncCat()]}>
${lightDom} ${lightDom}
</${tag}> </${tag}>
`)); `)
);
const validator = el.validators[0]; const validator = el.validators[0];
expect(validator instanceof Validator).to.be.true; expect(validator instanceof Validator).to.be.true;
@ -476,9 +504,11 @@ export function runValidateMixinSuite(customConfig) {
}); });
it('sets ".isPending/[is-pending]" when validation is in progress', async () => { it('sets ".isPending/[is-pending]" when validation is in progress', async () => {
const el = /** @type {ValidateElement} */ (await fixture(html` const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag} .modelValue=${'dog'}>${lightDom}</${tag}> <${tag} .modelValue=${'dog'}>${lightDom}</${tag}>
`)); `)
);
expect(el.isPending).to.be.false; expect(el.isPending).to.be.false;
expect(el.hasAttribute('is-pending')).to.be.false; expect(el.hasAttribute('is-pending')).to.be.false;
@ -498,11 +528,13 @@ export function runValidateMixinSuite(customConfig) {
const asyncV = new IsAsyncCat(); const asyncV = new IsAsyncCat();
const asyncVExecuteSpy = sinon.spy(asyncV, 'execute'); const asyncVExecuteSpy = sinon.spy(asyncV, 'execute');
const el = /** @type {ValidateElement} */ (await fixture(html` const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag} .modelValue=${'dog'}> <${tag} .modelValue=${'dog'}>
${lightDom} ${lightDom}
</${tag}> </${tag}>
`)); `)
);
// debounce started // debounce started
el.validators = [asyncV]; el.validators = [asyncV];
expect(asyncVExecuteSpy.called).to.equal(0); expect(asyncVExecuteSpy.called).to.equal(0);
@ -528,11 +560,13 @@ export function runValidateMixinSuite(customConfig) {
const asyncV = new IsAsyncCat(); const asyncV = new IsAsyncCat();
const asyncVAbortSpy = sinon.spy(asyncV, 'abortExecution'); const asyncVAbortSpy = sinon.spy(asyncV, 'abortExecution');
const el = /** @type {ValidateElement} */ (await fixture(html` const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag} .modelValue=${'dog'}> <${tag} .modelValue=${'dog'}>
${lightDom} ${lightDom}
</${tag}> </${tag}>
`)); `)
);
// debounce started // debounce started
el.validators = [asyncV]; el.validators = [asyncV];
expect(asyncVAbortSpy.called).to.equal(0); expect(asyncVAbortSpy.called).to.equal(0);
@ -546,7 +580,8 @@ export function runValidateMixinSuite(customConfig) {
const asyncV = new IsAsyncCat(); const asyncV = new IsAsyncCat();
const asyncVExecuteSpy = sinon.spy(asyncV, 'execute'); const asyncVExecuteSpy = sinon.spy(asyncV, 'execute');
const el = /** @type {ValidateElement & { isFocused: boolean }} */ (await fixture(html` const el = /** @type {ValidateElement & { isFocused: boolean }} */ (
await fixture(html`
<${tag} <${tag}
.isFocused=${true} .isFocused=${true}
.modelValue=${'dog'} .modelValue=${'dog'}
@ -558,7 +593,8 @@ export function runValidateMixinSuite(customConfig) {
> >
${lightDom} ${lightDom}
</${tag}> </${tag}>
`)); `)
);
expect(asyncVExecuteSpy.called).to.equal(0); expect(asyncVExecuteSpy.called).to.equal(0);
el.isFocused = false; el.isFocused = false;
@ -635,12 +671,14 @@ export function runValidateMixinSuite(customConfig) {
const resultValidator = new MySuccessResultValidator(); const resultValidator = new MySuccessResultValidator();
const resultValidateSpy = sinon.spy(resultValidator, 'executeOnResults'); const resultValidateSpy = sinon.spy(resultValidator, 'executeOnResults');
const el = /** @type {ValidateElement} */ (await fixture(html` const el = /** @type {ValidateElement} */ (
await fixture(html`
<${withSuccessTag} <${withSuccessTag}
.validators=${[new MinLength(3), resultValidator]} .validators=${[new MinLength(3), resultValidator]}
.modelValue=${'myValue'} .modelValue=${'myValue'}
>${lightDom}</${withSuccessTag}> >${lightDom}</${withSuccessTag}>
`)); `)
);
// @ts-ignore [allow-private] in test // @ts-ignore [allow-private] in test
const prevValidationResult = el.__prevValidationResult; const prevValidationResult = el.__prevValidationResult;
// @ts-ignore [allow-private] in test // @ts-ignore [allow-private] in test
@ -671,12 +709,14 @@ export function runValidateMixinSuite(customConfig) {
const validator = new AlwaysInvalid(); const validator = new AlwaysInvalid();
const resultV = new AlwaysInvalidResult(); const resultV = new AlwaysInvalidResult();
const el = /** @type {ValidateElement} */ (await fixture(html` const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag} <${tag}
.validators=${[validator, resultV]} .validators=${[validator, resultV]}
.modelValue=${'myValue'} .modelValue=${'myValue'}
>${lightDom}</${tag}> >${lightDom}</${tag}>
`)); `)
);
// @ts-ignore [allow-private] in test // @ts-ignore [allow-private] in test
const totalValidationResult = el.__validationResult; const totalValidationResult = el.__validationResult;
@ -686,12 +726,14 @@ export function runValidateMixinSuite(customConfig) {
describe('Required Validator integration', () => { describe('Required Validator integration', () => {
it('will result in erroneous state when form control is empty', async () => { it('will result in erroneous state when form control is empty', async () => {
const el = /** @type {ValidateElement} */ (await fixture(html` const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag} <${tag}
.validators=${[new Required()]} .validators=${[new Required()]}
.modelValue=${''} .modelValue=${''}
>${lightDom}</${tag}> >${lightDom}</${tag}>
`)); `)
);
expect(el.validationStates.error.Required).to.be.true; expect(el.validationStates.error.Required).to.be.true;
expect(el.hasFeedbackFor).to.deep.equal(['error']); expect(el.hasFeedbackFor).to.deep.equal(['error']);
@ -701,12 +743,14 @@ export function runValidateMixinSuite(customConfig) {
}); });
it('calls private ".__isEmpty" by default', async () => { it('calls private ".__isEmpty" by default', async () => {
const el = /** @type {ValidateElement} */ (await fixture(html` const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag} <${tag}
.validators=${[new Required()]} .validators=${[new Required()]}
.modelValue=${''} .modelValue=${''}
>${lightDom}</${tag}> >${lightDom}</${tag}>
`)); `)
);
const validator = /** @type {Validator} */ (el.validators.find(v => v instanceof Required)); const validator = /** @type {Validator} */ (el.validators.find(v => v instanceof Required));
const executeSpy = sinon.spy(validator, 'execute'); const executeSpy = sinon.spy(validator, 'execute');
// @ts-ignore [allow-private] in test // @ts-ignore [allow-private] in test
@ -725,12 +769,14 @@ export function runValidateMixinSuite(customConfig) {
const customRequiredTagString = defineCE(_isEmptyValidate); const customRequiredTagString = defineCE(_isEmptyValidate);
const customRequiredTag = unsafeStatic(customRequiredTagString); const customRequiredTag = unsafeStatic(customRequiredTagString);
const el = /** @type {_isEmptyValidate} */ (await fixture(html` const el = /** @type {_isEmptyValidate} */ (
await fixture(html`
<${customRequiredTag} <${customRequiredTag}
.validators=${[new Required()]} .validators=${[new Required()]}
.modelValue=${{ model: 'foo' }} .modelValue=${{ model: 'foo' }}
>${lightDom}</${customRequiredTag}> >${lightDom}</${customRequiredTag}>
`)); `)
);
const providedIsEmptySpy = sinon.spy(el, '_isEmpty'); const providedIsEmptySpy = sinon.spy(el, '_isEmpty');
el.modelValue = { model: '' }; el.modelValue = { model: '' };
@ -741,24 +787,28 @@ export function runValidateMixinSuite(customConfig) {
it('prevents other Validators from being called when input is empty', async () => { it('prevents other Validators from being called when input is empty', async () => {
const alwaysInvalid = new AlwaysInvalid(); const alwaysInvalid = new AlwaysInvalid();
const alwaysInvalidSpy = sinon.spy(alwaysInvalid, 'execute'); const alwaysInvalidSpy = sinon.spy(alwaysInvalid, 'execute');
const el = /** @type {ValidateElement} */ (await fixture(html` const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag} <${tag}
.validators=${[new Required(), alwaysInvalid]} .validators=${[new Required(), alwaysInvalid]}
.modelValue=${''} .modelValue=${''}
>${lightDom}</${tag}> >${lightDom}</${tag}>
`)); `)
);
expect(alwaysInvalidSpy.callCount).to.equal(0); // __isRequired returned false (invalid) expect(alwaysInvalidSpy.callCount).to.equal(0); // __isRequired returned false (invalid)
el.modelValue = 'foo'; el.modelValue = 'foo';
expect(alwaysInvalidSpy.callCount).to.equal(1); // __isRequired returned true (valid) expect(alwaysInvalidSpy.callCount).to.equal(1); // __isRequired returned true (valid)
}); });
it('adds [aria-required="true"] to "._inputNode"', async () => { it('adds [aria-required="true"] to "._inputNode"', async () => {
const el = /** @type {ValidateElement} */ (await fixture(html` const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag} <${tag}
.validators=${[new Required()]} .validators=${[new Required()]}
.modelValue=${''} .modelValue=${''}
>${lightDom}</${tag}> >${lightDom}</${tag}>
`)); `)
);
const { _inputNode } = getFormControlMembers(el); const { _inputNode } = getFormControlMembers(el);
expect(_inputNode?.getAttribute('aria-required')).to.equal('true'); expect(_inputNode?.getAttribute('aria-required')).to.equal('true');
@ -779,11 +829,13 @@ export function runValidateMixinSuite(customConfig) {
const preconfTag = unsafeStatic(preconfTagString); const preconfTag = unsafeStatic(preconfTagString);
it('can be stored for custom inputs', async () => { it('can be stored for custom inputs', async () => {
const el = /** @type {ValidateElement} */ (await fixture(html` const el = /** @type {ValidateElement} */ (
await fixture(html`
<${preconfTag} <${preconfTag}
.validators=${[new MinLength(3)]} .validators=${[new MinLength(3)]}
.modelValue=${'12'} .modelValue=${'12'}
></${preconfTag}>`)); ></${preconfTag}>`)
);
expect(el.validationStates.error.AlwaysInvalid).to.be.true; expect(el.validationStates.error.AlwaysInvalid).to.be.true;
expect(el.validationStates.error.MinLength).to.be.true; expect(el.validationStates.error.MinLength).to.be.true;
@ -800,10 +852,12 @@ export function runValidateMixinSuite(customConfig) {
); );
const altPreconfTag = unsafeStatic(altPreconfTagString); const altPreconfTag = unsafeStatic(altPreconfTagString);
const el = /** @type {ValidateElement} */ (await fixture(html` const el = /** @type {ValidateElement} */ (
await fixture(html`
<${altPreconfTag} <${altPreconfTag}
.modelValue=${'12'} .modelValue=${'12'}
></${altPreconfTag}>`)); ></${altPreconfTag}>`)
);
expect(el.validationStates.error.MinLength).to.be.true; expect(el.validationStates.error.MinLength).to.be.true;
el.defaultValidators[0].param = 2; el.defaultValidators[0].param = 2;
@ -811,10 +865,12 @@ export function runValidateMixinSuite(customConfig) {
}); });
it('can be requested via "._allValidators" getter', async () => { it('can be requested via "._allValidators" getter', async () => {
const el = /** @type {ValidateElement} */ (await fixture(html` const el = /** @type {ValidateElement} */ (
await fixture(html`
<${preconfTag} <${preconfTag}
.validators=${[new MinLength(3)]} .validators=${[new MinLength(3)]}
></${preconfTag}>`)); ></${preconfTag}>`)
);
const { _allValidators } = getFormControlMembers(el); const { _allValidators } = getFormControlMembers(el);
expect(el.validators.length).to.equal(1); expect(el.validators.length).to.equal(1);
@ -834,11 +890,13 @@ export function runValidateMixinSuite(customConfig) {
describe('State storage and reflection', () => { describe('State storage and reflection', () => {
it('stores validity of individual Validators in ".validationStates.error[validator.validatorName]"', async () => { it('stores validity of individual Validators in ".validationStates.error[validator.validatorName]"', async () => {
const el = /** @type {ValidateElement} */ (await fixture(html` const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag} <${tag}
.modelValue=${'a'} .modelValue=${'a'}
.validators=${[new MinLength(3), new AlwaysInvalid()]} .validators=${[new MinLength(3), new AlwaysInvalid()]}
>${lightDom}</${tag}>`)); >${lightDom}</${tag}>`)
);
expect(el.validationStates.error.MinLength).to.be.true; expect(el.validationStates.error.MinLength).to.be.true;
expect(el.validationStates.error.AlwaysInvalid).to.be.true; expect(el.validationStates.error.AlwaysInvalid).to.be.true;
@ -849,11 +907,13 @@ export function runValidateMixinSuite(customConfig) {
}); });
it('removes "non active" states whenever modelValue becomes undefined', async () => { it('removes "non active" states whenever modelValue becomes undefined', async () => {
const el = /** @type {ValidateElement} */ (await fixture(html` const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag} <${tag}
.validators=${[new MinLength(3)]} .validators=${[new MinLength(3)]}
>${lightDom}</${tag}> >${lightDom}</${tag}>
`)); `)
);
el.modelValue = 'a'; el.modelValue = 'a';
expect(el.hasFeedbackFor).to.deep.equal(['error']); expect(el.hasFeedbackFor).to.deep.equal(['error']);
expect(el.validationStates.error).to.not.eql({}); expect(el.validationStates.error).to.not.eql({});
@ -865,11 +925,13 @@ export function runValidateMixinSuite(customConfig) {
it('clears current validation results when validators array updated', async () => { it('clears current validation results when validators array updated', async () => {
const validators = [new Required()]; const validators = [new Required()];
const el = /** @type {ValidateElement} */ (await fixture(html` const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag} <${tag}
.validators=${validators} .validators=${validators}
>${lightDom}</${tag}> >${lightDom}</${tag}>
`)); `)
);
expect(el.hasFeedbackFor).to.deep.equal(['error']); expect(el.hasFeedbackFor).to.deep.equal(['error']);
expect(el.validationStates.error).to.eql({ Required: true }); expect(el.validationStates.error).to.eql({ Required: true });
@ -883,7 +945,8 @@ export function runValidateMixinSuite(customConfig) {
}); });
it('can be configured to change visibility conditions per type', async () => { it('can be configured to change visibility conditions per type', async () => {
const el = /** @type {ValidateElement} */ (await fixture(html` const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag} <${tag}
.validators="${[new Required({}, { type: 'error' })]}" .validators="${[new Required({}, { type: 'error' })]}"
.feedbackCondition="${( .feedbackCondition="${(
@ -897,7 +960,8 @@ export function runValidateMixinSuite(customConfig) {
return defaultCondition(type); return defaultCondition(type);
}}" }}"
>${lightDom}</${tag}> >${lightDom}</${tag}>
`)); `)
);
expect(el.showsFeedbackFor).to.eql(['error']); expect(el.showsFeedbackFor).to.eql(['error']);
}); });
@ -905,13 +969,15 @@ export function runValidateMixinSuite(customConfig) {
describe('Events', () => { describe('Events', () => {
it('fires "showsFeedbackForChanged" event async after feedbackData got synced to feedbackElement', async () => { it('fires "showsFeedbackForChanged" event async after feedbackData got synced to feedbackElement', async () => {
const spy = sinon.spy(); const spy = sinon.spy();
const el = /** @type {ValidateElement} */ (await fixture(html` const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag} <${tag}
.submitted=${true} .submitted=${true}
.validators=${[new MinLength(7)]} .validators=${[new MinLength(7)]}
@showsFeedbackForChanged=${spy}; @showsFeedbackForChanged=${spy}
>${lightDom}</${tag}> >${lightDom}</${tag}>
`)); `)
);
el.modelValue = 'a'; el.modelValue = 'a';
await el.updateComplete; await el.updateComplete;
expect(spy).to.have.callCount(1); expect(spy).to.have.callCount(1);
@ -927,13 +993,15 @@ export function runValidateMixinSuite(customConfig) {
it('fires "showsFeedbackFor{type}Changed" event async when type visibility changed', async () => { it('fires "showsFeedbackFor{type}Changed" event async when type visibility changed', async () => {
const spy = sinon.spy(); const spy = sinon.spy();
const el = /** @type {ValidateElement} */ (await fixture(html` const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag} <${tag}
.submitted=${true} .submitted=${true}
.validators=${[new MinLength(7)]} .validators=${[new MinLength(7)]}
@showsFeedbackForErrorChanged=${spy}; @showsFeedbackForErrorChanged=${spy}
>${lightDom}</${tag}> >${lightDom}</${tag}>
`)); `)
);
el.modelValue = 'a'; el.modelValue = 'a';
await el.updateComplete; await el.updateComplete;
expect(spy).to.have.callCount(1); expect(spy).to.have.callCount(1);
@ -949,13 +1017,15 @@ export function runValidateMixinSuite(customConfig) {
it('fires "{type}StateChanged" event async when type validity changed', async () => { it('fires "{type}StateChanged" event async when type validity changed', async () => {
const spy = sinon.spy(); const spy = sinon.spy();
const el = /** @type {ValidateElement} */ (await fixture(html` const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag} <${tag}
.submitted=${true} .submitted=${true}
.validators=${[new MinLength(7)]} .validators=${[new MinLength(7)]}
@errorStateChanged=${spy}; @errorStateChanged=${spy}
>${lightDom}</${tag}> >${lightDom}</${tag}>
`)); `)
);
expect(spy).to.have.callCount(0); expect(spy).to.have.callCount(0);
el.modelValue = 'a'; el.modelValue = 'a';
@ -975,12 +1045,14 @@ export function runValidateMixinSuite(customConfig) {
describe('Accessibility', () => { describe('Accessibility', () => {
it.skip('calls "._inputNode.setCustomValidity(errorMessage)"', async () => { it.skip('calls "._inputNode.setCustomValidity(errorMessage)"', async () => {
const el = /** @type {ValidateElement} */ (await fixture(html` const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag} <${tag}
.modelValue=${'123'} .modelValue=${'123'}
.validators=${[new MinLength(3, { message: 'foo' })]}> .validators=${[new MinLength(3, { message: 'foo' })]}>
<input slot="input"> <input slot="input">
</${tag}>`)); </${tag}>`)
);
const { _inputNode } = getFormControlMembers(el); const { _inputNode } = getFormControlMembers(el);
if (_inputNode) { if (_inputNode) {
@ -1013,7 +1085,8 @@ export function runValidateMixinSuite(customConfig) {
const customTypeTag = unsafeStatic(customTypeTagString); const customTypeTag = unsafeStatic(customTypeTagString);
it('supports additional validationTypes in .hasFeedbackFor', async () => { it('supports additional validationTypes in .hasFeedbackFor', async () => {
const el = /** @type {ValidateElement} */ (await fixture(html` const el = /** @type {ValidateElement} */ (
await fixture(html`
<${customTypeTag} <${customTypeTag}
.validators=${[ .validators=${[
new MinLength(2, { type: 'x' }), new MinLength(2, { type: 'x' }),
@ -1022,7 +1095,8 @@ export function runValidateMixinSuite(customConfig) {
]} ]}
.modelValue=${'1234'} .modelValue=${'1234'}
>${lightDom}</${customTypeTag}> >${lightDom}</${customTypeTag}>
`)); `)
);
expect(el.hasFeedbackFor).to.deep.equal([]); expect(el.hasFeedbackFor).to.deep.equal([]);
el.modelValue = '123'; // triggers y el.modelValue = '123'; // triggers y
@ -1036,7 +1110,8 @@ export function runValidateMixinSuite(customConfig) {
}); });
it('supports additional validationTypes in .validationStates', async () => { it('supports additional validationTypes in .validationStates', async () => {
const el = /** @type {ValidateElement} */ (await fixture(html` const el = /** @type {ValidateElement} */ (
await fixture(html`
<${customTypeTag} <${customTypeTag}
.validators=${[ .validators=${[
new MinLength(2, { type: 'x' }), new MinLength(2, { type: 'x' }),
@ -1045,7 +1120,8 @@ export function runValidateMixinSuite(customConfig) {
]} ]}
.modelValue=${'1234'} .modelValue=${'1234'}
>${lightDom}</${customTypeTag}> >${lightDom}</${customTypeTag}>
`)); `)
);
expect(el.validationStates).to.eql({ expect(el.validationStates).to.eql({
x: {}, x: {},
error: {}, error: {},
@ -1076,7 +1152,8 @@ export function runValidateMixinSuite(customConfig) {
it('orders feedback based on provided "validationTypes"', async () => { it('orders feedback based on provided "validationTypes"', async () => {
// we set submitted to always show error message in the test // we set submitted to always show error message in the test
const el = /** @type {ValidateElement} */ (await fixture(html` const el = /** @type {ValidateElement} */ (
await fixture(html`
<${customTypeTag} <${customTypeTag}
.submitted=${true} .submitted=${true}
._visibleMessagesAmount=${Infinity} ._visibleMessagesAmount=${Infinity}
@ -1087,7 +1164,8 @@ export function runValidateMixinSuite(customConfig) {
]} ]}
.modelValue=${'1'} .modelValue=${'1'}
>${lightDom}</${customTypeTag}> >${lightDom}</${customTypeTag}>
`)); `)
);
const { _feedbackNode } = getFormControlMembers(el); const { _feedbackNode } = getFormControlMembers(el);
await el.feedbackComplete; await el.feedbackComplete;
@ -1132,13 +1210,15 @@ export function runValidateMixinSuite(customConfig) {
const elTag = unsafeStatic(elTagString); const elTag = unsafeStatic(elTagString);
// we set submitted to always show errors // we set submitted to always show errors
const el = /** @type {ValidateHasX} */ (await fixture(html` const el = /** @type {ValidateHasX} */ (
await fixture(html`
<${elTag} <${elTag}
.submitted=${true} .submitted=${true}
.validators=${[new MinLength(2, { type: 'x' })]} .validators=${[new MinLength(2, { type: 'x' })]}
.modelValue=${'1'} .modelValue=${'1'}
>${lightDom}</${elTag}> >${lightDom}</${elTag}>
`)); `)
);
await el.feedbackComplete; await el.feedbackComplete;
expect(el.hasX).to.be.true; expect(el.hasX).to.be.true;
expect(el.hasXVisible).to.be.true; expect(el.hasXVisible).to.be.true;
@ -1186,14 +1266,16 @@ export function runValidateMixinSuite(customConfig) {
const spy = sinon.spy(); const spy = sinon.spy();
// we set prefilled to always show errors // we set prefilled to always show errors
const el = /** @type {ValidateElement} */ (await fixture(html` const el = /** @type {ValidateElement} */ (
await fixture(html`
<${elTag} <${elTag}
.prefilled=${true} .prefilled=${true}
@hasFeedbackForXChanged=${spy} @hasFeedbackForXChanged=${spy}
.validators=${[new MinLength(2, { type: 'x' })]} .validators=${[new MinLength(2, { type: 'x' })]}
.modelValue=${'1'} .modelValue=${'1'}
>${lightDom}</${elTag}> >${lightDom}</${elTag}>
`)); `)
);
expect(spy).to.have.callCount(1); expect(spy).to.have.callCount(1);
el.modelValue = '1'; el.modelValue = '1';
expect(spy).to.have.callCount(1); expect(spy).to.have.callCount(1);
@ -1228,12 +1310,14 @@ export function runValidateMixinSuite(customConfig) {
}, },
); );
const elTag = unsafeStatic(elTagString); const elTag = unsafeStatic(elTagString);
const el = /** @type {ValidateElement} */ (await fixture(html` const el = /** @type {ValidateElement} */ (
await fixture(html`
<${elTag} <${elTag}
.validators=${[new AlwaysInvalid()]} .validators=${[new AlwaysInvalid()]}
.modelValue=${'myValue'} .modelValue=${'myValue'}
>${lightDom}</${elTag}> >${lightDom}</${elTag}>
`)); `)
);
// @ts-ignore [allow-protected] in test // @ts-ignore [allow-protected] in test
const spy = sinon.spy(el, '_updateShouldShowFeedbackFor'); const spy = sinon.spy(el, '_updateShouldShowFeedbackFor');
@ -1282,14 +1366,16 @@ export function runValidateMixinSuite(customConfig) {
}, },
); );
const elTag = unsafeStatic(elTagString); const elTag = unsafeStatic(elTagString);
const el = /** @type {ValidateElement} */ (await fixture(html` const el = /** @type {ValidateElement} */ (
await fixture(html`
<${elTag} <${elTag}
.validators=${[ .validators=${[
new AlwaysInvalid({}, { type: 'error' }), new AlwaysInvalid({}, { type: 'error' }),
new AlwaysInvalid({}, { type: 'info' }), new AlwaysInvalid({}, { type: 'info' }),
]} ]}
>${lightDom}</${elTag}> >${lightDom}</${elTag}>
`)); `)
);
for (const [modelValue, expected] of [ for (const [modelValue, expected] of [
['A', ['error']], ['A', ['error']],

View file

@ -66,9 +66,11 @@ export function runValidateMixinFeedbackPart() {
}); });
it('has .showsFeedbackFor indicating for which type it actually shows messages', async () => { it('has .showsFeedbackFor indicating for which type it actually shows messages', async () => {
const el = /** @type {ValidateElement} */ (await fixture(html` const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag} submitted .validators=${[new MinLength(3)]}>${lightDom}</${tag}> <${tag} submitted .validators=${[new MinLength(3)]}>${lightDom}</${tag}>
`)); `)
);
el.modelValue = 'a'; el.modelValue = 'a';
await el.feedbackComplete; await el.feedbackComplete;
@ -87,14 +89,16 @@ export function runValidateMixinFeedbackPart() {
} }
const elTagString = defineCE(ValidateElementCustomTypes); const elTagString = defineCE(ValidateElementCustomTypes);
const elTag = unsafeStatic(elTagString); const elTag = unsafeStatic(elTagString);
const el = /** @type {ValidateElementCustomTypes} */ (await fixture(html` const el = /** @type {ValidateElementCustomTypes} */ (
await fixture(html`
<${elTag} <${elTag}
.submitted=${true} .submitted=${true}
.validators=${[ .validators=${[
new MinLength(2, { type: 'x' }), new MinLength(2, { type: 'x' }),
new MinLength(3, { type: 'error' }), new MinLength(3, { type: 'error' }),
]}>${lightDom}</${elTag}> ]}>${lightDom}</${elTag}>
`)); `)
);
el.modelValue = '1'; el.modelValue = '1';
await el.updateComplete; await el.updateComplete;
@ -116,12 +120,14 @@ export function runValidateMixinFeedbackPart() {
}); });
it('passes a message to the "._feedbackNode"', async () => { it('passes a message to the "._feedbackNode"', async () => {
const el = /** @type {ValidateElement} */ (await fixture(html` const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag} <${tag}
.submitted=${true} .submitted=${true}
.modelValue=${'cat'} .modelValue=${'cat'}
>${lightDom}</${tag}> >${lightDom}</${tag}>
`)); `)
);
const { _feedbackNode } = getFormControlMembers(el); const { _feedbackNode } = getFormControlMembers(el);
expect(_feedbackNode.feedbackData).to.deep.equal([]); expect(_feedbackNode.feedbackData).to.deep.equal([]);
@ -132,13 +138,15 @@ export function runValidateMixinFeedbackPart() {
}); });
it('has configurable feedback visibility hook', async () => { it('has configurable feedback visibility hook', async () => {
const el = /** @type {ValidateElement} */ (await fixture(html` const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag} <${tag}
.submitted=${true} .submitted=${true}
.modelValue=${'cat'} .modelValue=${'cat'}
.validators=${[new AlwaysInvalid()]} .validators=${[new AlwaysInvalid()]}
>${lightDom}</${tag}> >${lightDom}</${tag}>
`)); `)
);
const { _feedbackNode } = getFormControlMembers(el); const { _feedbackNode } = getFormControlMembers(el);
await el.updateComplete; await el.updateComplete;
@ -153,13 +161,15 @@ export function runValidateMixinFeedbackPart() {
}); });
it('writes prioritized result to "._feedbackNode" based on Validator order', async () => { it('writes prioritized result to "._feedbackNode" based on Validator order', async () => {
const el = /** @type {ValidateElement} */ (await fixture(html` const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag} <${tag}
.submitted=${true} .submitted=${true}
.modelValue=${'cat'} .modelValue=${'cat'}
.validators=${[new AlwaysInvalid(), new MinLength(4)]} .validators=${[new AlwaysInvalid(), new MinLength(4)]}
>${lightDom}</${tag}> >${lightDom}</${tag}>
`)); `)
);
const { _feedbackNode } = getFormControlMembers(el); const { _feedbackNode } = getFormControlMembers(el);
await el.updateComplete; await el.updateComplete;
@ -179,13 +189,15 @@ export function runValidateMixinFeedbackPart() {
return 'this ends up in "._feedbackNode"'; return 'this ends up in "._feedbackNode"';
}; };
const el = /** @type {ValidateElement} */ (await fixture(html` const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag} <${tag}
.submitted=${true} .submitted=${true}
.modelValue=${'cat'} .modelValue=${'cat'}
.validators=${[new AlwaysInvalid()]} .validators=${[new AlwaysInvalid()]}
>${lightDom}</${tag}> >${lightDom}</${tag}>
`)); `)
);
const { _feedbackNode } = getFormControlMembers(el); const { _feedbackNode } = getFormControlMembers(el);
expect(_feedbackNode.feedbackData).to.be.undefined; expect(_feedbackNode.feedbackData).to.be.undefined;
@ -208,13 +220,15 @@ export function runValidateMixinFeedbackPart() {
return 'this ends up in "._feedbackNode"'; return 'this ends up in "._feedbackNode"';
}; };
const el = /** @type {ValidateElement} */ (await fixture(html` const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag} <${tag}
.submitted=${true} .submitted=${true}
.modelValue=${'cat'} .modelValue=${'cat'}
.validators=${[new AlwaysInvalid()]} .validators=${[new AlwaysInvalid()]}
>${lightDom}</${tag}> >${lightDom}</${tag}>
`)); `)
);
const { _feedbackNode } = getFormControlMembers(el); const { _feedbackNode } = getFormControlMembers(el);
expect(_feedbackNode.feedbackData).to.be.undefined; expect(_feedbackNode.feedbackData).to.be.undefined;
@ -248,8 +262,9 @@ export function runValidateMixinFeedbackPart() {
render() { render() {
let name = ''; let name = '';
if (this.feedbackData && this.feedbackData.length > 0) { if (this.feedbackData && this.feedbackData.length > 0) {
const ctor = /** @type {typeof Validator} */ (this.feedbackData[0]?.validator const ctor = /** @type {typeof Validator} */ (
?.constructor); this.feedbackData[0]?.validator?.constructor
);
name = ctor.validatorName; name = ctor.validatorName;
} }
return html`Custom for ${name}`; return html`Custom for ${name}`;
@ -257,13 +272,15 @@ export function runValidateMixinFeedbackPart() {
} }
const customFeedbackTagString = defineCE(ValidateElementCustomRender); const customFeedbackTagString = defineCE(ValidateElementCustomRender);
const customFeedbackTag = unsafeStatic(customFeedbackTagString); const customFeedbackTag = unsafeStatic(customFeedbackTagString);
const el = /** @type {ValidateElement} */ (await fixture(html` const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag} <${tag}
.submitted=${true} .submitted=${true}
.validators=${[new ContainsLowercaseA(), new AlwaysInvalid()]}> .validators=${[new ContainsLowercaseA(), new AlwaysInvalid()]}>
<${customFeedbackTag} slot="feedback"><${customFeedbackTag}> <${customFeedbackTag} slot="feedback"><${customFeedbackTag}>
</${tag}> </${tag}>
`)); `)
);
const { _feedbackNode } = getFormControlMembers(el); const { _feedbackNode } = getFormControlMembers(el);
expect(_feedbackNode.localName).to.equal(customFeedbackTagString); expect(_feedbackNode.localName).to.equal(customFeedbackTagString);
@ -282,12 +299,14 @@ export function runValidateMixinFeedbackPart() {
}); });
it('supports custom messages in Validator instance configuration object', async () => { it('supports custom messages in Validator instance configuration object', async () => {
const el = /** @type {ValidateElement} */ (await fixture(html` const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag} <${tag}
.submitted=${true} .submitted=${true}
.validators=${[new MinLength(3, { getMessage: () => 'custom via config' })]} .validators=${[new MinLength(3, { getMessage: () => 'custom via config' })]}
>${lightDom}</${tag}> >${lightDom}</${tag}>
`)); `)
);
const { _feedbackNode } = getFormControlMembers(el); const { _feedbackNode } = getFormControlMembers(el);
el.modelValue = 'a'; el.modelValue = 'a';
@ -297,13 +316,15 @@ export function runValidateMixinFeedbackPart() {
}); });
it('updates the feedback component when locale changes', async () => { it('updates the feedback component when locale changes', async () => {
const el = /** @type {ValidateElement} */ (await fixture(html` const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag} <${tag}
.submitted=${true} .submitted=${true}
.validators=${[new MinLength(3)]} .validators=${[new MinLength(3)]}
.modelValue=${'1'} .modelValue=${'1'}
>${lightDom}</${tag}> >${lightDom}</${tag}>
`)); `)
);
const { _feedbackNode } = getFormControlMembers(el); const { _feedbackNode } = getFormControlMembers(el);
await el.feedbackComplete; await el.feedbackComplete;
@ -323,7 +344,8 @@ export function runValidateMixinFeedbackPart() {
} }
const elTagString = defineCE(ValidateElementCustomTypes); const elTagString = defineCE(ValidateElementCustomTypes);
const elTag = unsafeStatic(elTagString); const elTag = unsafeStatic(elTagString);
const el = /** @type {ValidateElementCustomTypes} */ (await fixture(html` const el = /** @type {ValidateElementCustomTypes} */ (
await fixture(html`
<${elTag} <${elTag}
.submitted=${true} .submitted=${true}
.validators=${[ .validators=${[
@ -331,7 +353,8 @@ export function runValidateMixinFeedbackPart() {
new DefaultSuccess(null, { getMessage: () => 'This is a success message' }), new DefaultSuccess(null, { getMessage: () => 'This is a success message' }),
]} ]}
>${lightDom}</${elTag}> >${lightDom}</${elTag}>
`)); `)
);
const { _feedbackNode } = getFormControlMembers(el); const { _feedbackNode } = getFormControlMembers(el);
el.modelValue = 'a'; el.modelValue = 'a';
@ -347,13 +370,15 @@ export function runValidateMixinFeedbackPart() {
describe('Accessibility', () => { describe('Accessibility', () => {
it('sets [aria-invalid="true"] to "._inputNode" when there is an error', async () => { it('sets [aria-invalid="true"] to "._inputNode" when there is an error', async () => {
const el = /** @type {ValidateElement} */ (await fixture(html` const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag} <${tag}
submitted submitted
.validators=${[new Required()]} .validators=${[new Required()]}
.modelValue=${'a'} .modelValue=${'a'}
>${lightDom}</${tag}> >${lightDom}</${tag}>
`)); `)
);
const { _inputNode } = getFormControlMembers(el); const { _inputNode } = getFormControlMembers(el);
const inputNode = _inputNode; const inputNode = _inputNode;
@ -386,13 +411,15 @@ export function runValidateMixinFeedbackPart() {
const ctorValidator = /** @type {typeof MinLength} */ (constructorValidator.constructor); const ctorValidator = /** @type {typeof MinLength} */ (constructorValidator.constructor);
const constructorMessageSpy = sinon.spy(ctorValidator, 'getMessage'); const constructorMessageSpy = sinon.spy(ctorValidator, 'getMessage');
el = /** @type {ValidateElementCustomTypes} */ (await fixture(html` el = /** @type {ValidateElementCustomTypes} */ (
await fixture(html`
<${elTag} <${elTag}
.submitted=${true} .submitted=${true}
.validators=${[constructorValidator]} .validators=${[constructorValidator]}
.modelValue=${'cat'} .modelValue=${'cat'}
>${lightDom}</${elTag}> >${lightDom}</${elTag}>
`)); `)
);
await el.updateComplete; await el.updateComplete;
await el.feedbackComplete; await el.feedbackComplete;
expect(constructorMessageSpy.args[0][0]).to.eql({ expect(constructorMessageSpy.args[0][0]).to.eql({
@ -408,13 +435,15 @@ export function runValidateMixinFeedbackPart() {
const instanceMessageSpy = sinon.spy(); const instanceMessageSpy = sinon.spy();
const instanceValidator = new MinLength(4, { getMessage: instanceMessageSpy }); const instanceValidator = new MinLength(4, { getMessage: instanceMessageSpy });
el = /** @type {ValidateElementCustomTypes} */ (await fixture(html` el = /** @type {ValidateElementCustomTypes} */ (
await fixture(html`
<${elTag} <${elTag}
.submitted=${true} .submitted=${true}
.validators=${[instanceValidator]} .validators=${[instanceValidator]}
.modelValue=${'cat'} .modelValue=${'cat'}
>${lightDom}</${elTag}> >${lightDom}</${elTag}>
`)); `)
);
await el.updateComplete; await el.updateComplete;
await el.feedbackComplete; await el.feedbackComplete;
expect(instanceMessageSpy.args[0][0]).to.eql({ expect(instanceMessageSpy.args[0][0]).to.eql({
@ -435,14 +464,16 @@ export function runValidateMixinFeedbackPart() {
const ctorValidator = /** @type {typeof MinLength} */ (constructorValidator.constructor); const ctorValidator = /** @type {typeof MinLength} */ (constructorValidator.constructor);
const spy = sinon.spy(ctorValidator, 'getMessage'); const spy = sinon.spy(ctorValidator, 'getMessage');
const el = /** @type {ValidateElement} */ (await fixture(html` const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag} <${tag}
.submitted=${true} .submitted=${true}
.validators=${[constructorValidator]} .validators=${[constructorValidator]}
.modelValue=${'cat'} .modelValue=${'cat'}
.fieldName=${new Promise(resolve => resolve('myField'))} .fieldName=${new Promise(resolve => resolve('myField'))}
>${lightDom}</${tag}> >${lightDom}</${tag}>
`)); `)
);
await el.updateComplete; await el.updateComplete;
await el.feedbackComplete; await el.feedbackComplete;
expect(spy.args[0][0]).to.eql({ expect(spy.args[0][0]).to.eql({
@ -464,14 +495,16 @@ export function runValidateMixinFeedbackPart() {
const ctorValidator = /** @type {typeof MinLength} */ (constructorValidator.constructor); const ctorValidator = /** @type {typeof MinLength} */ (constructorValidator.constructor);
const spy = sinon.spy(ctorValidator, 'getMessage'); const spy = sinon.spy(ctorValidator, 'getMessage');
const el = /** @type {ValidateElement} */ (await fixture(html` const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag} <${tag}
.submitted=${true} .submitted=${true}
.validators=${[constructorValidator]} .validators=${[constructorValidator]}
.modelValue=${'cat'} .modelValue=${'cat'}
.fieldName=${new Promise(resolve => resolve('myField'))} .fieldName=${new Promise(resolve => resolve('myField'))}
>${lightDom}</${tag}> >${lightDom}</${tag}>
`)); `)
);
await el.updateComplete; await el.updateComplete;
await el.feedbackComplete; await el.feedbackComplete;
@ -500,13 +533,15 @@ export function runValidateMixinFeedbackPart() {
* The Queue system solves this by queueing the updateFeedbackComponent tasks and * The Queue system solves this by queueing the updateFeedbackComponent tasks and
* await them one by one. * await them one by one.
*/ */
const el = /** @type {ValidateElement} */ (await fixture(html` const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag} <${tag}
.submitted=${true} .submitted=${true}
.validators=${[new MinLength(3)]} .validators=${[new MinLength(3)]}
.modelValue=${'1'} .modelValue=${'1'}
>${lightDom}</${tag}> >${lightDom}</${tag}>
`)); `)
);
const { _feedbackNode } = getFormControlMembers(el); const { _feedbackNode } = getFormControlMembers(el);
el.modelValue = '12345'; el.modelValue = '12345';

View file

@ -2,7 +2,9 @@ import { LitElement } from '@lion/core';
import { LionInput } from '@lion/input'; import { LionInput } from '@lion/input';
import '@lion/fieldset/define'; import '@lion/fieldset/define';
import { FormGroupMixin, Required } from '@lion/form-core'; import { FormGroupMixin, Required } from '@lion/form-core';
import { expect, html, fixture, fixtureSync, unsafeStatic } from '@open-wc/testing'; import { expect, fixture, fixtureSync } from '@open-wc/testing';
import { html, unsafeStatic } from 'lit/static-html.js';
import sinon from 'sinon'; import sinon from 'sinon';
import { ChoiceGroupMixin } from '../../src/choice-group/ChoiceGroupMixin.js'; import { ChoiceGroupMixin } from '../../src/choice-group/ChoiceGroupMixin.js';
import { ChoiceInputMixin } from '../../src/choice-group/ChoiceInputMixin.js'; import { ChoiceInputMixin } from '../../src/choice-group/ChoiceInputMixin.js';
@ -41,13 +43,15 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi
describe(`ChoiceGroupMixin: ${cfg.parentTagString}`, () => { describe(`ChoiceGroupMixin: ${cfg.parentTagString}`, () => {
if (cfg.choiceType === 'single') { if (cfg.choiceType === 'single') {
it('has a single modelValue representing the currently checked radio value', async () => { it('has a single modelValue representing the currently checked radio value', async () => {
const el = /** @type {ChoiceInputGroup} */ (await fixture(html` const el = /** @type {ChoiceInputGroup} */ (
await fixture(html`
<${parentTag} name="gender[]"> <${parentTag} name="gender[]">
<${childTag} .choiceValue=${'male'}></${childTag}> <${childTag} .choiceValue=${'male'}></${childTag}>
<${childTag} .choiceValue=${'female'} checked></${childTag}> <${childTag} .choiceValue=${'female'} checked></${childTag}>
<${childTag} .choiceValue=${'other'}></${childTag}> <${childTag} .choiceValue=${'other'}></${childTag}>
</${parentTag}> </${parentTag}>
`)); `)
);
expect(el.modelValue).to.equal('female'); expect(el.modelValue).to.equal('female');
el.formElements[0].checked = true; el.formElements[0].checked = true;
expect(el.modelValue).to.equal('male'); expect(el.modelValue).to.equal('male');
@ -56,13 +60,15 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi
}); });
it('has a single formattedValue representing the currently checked radio value', async () => { it('has a single formattedValue representing the currently checked radio value', async () => {
const el = /** @type {ChoiceInputGroup} */ (await fixture(html` const el = /** @type {ChoiceInputGroup} */ (
await fixture(html`
<${parentTag} name="gender"> <${parentTag} name="gender">
<${childTag} .choiceValue=${'male'}></${childTag}> <${childTag} .choiceValue=${'male'}></${childTag}>
<${childTag} .choiceValue=${'female'} checked></${childTag}> <${childTag} .choiceValue=${'female'} checked></${childTag}>
<${childTag} .choiceValue=${'other'}></${childTag}> <${childTag} .choiceValue=${'other'}></${childTag}>
</${parentTag}> </${parentTag}>
`)); `)
);
expect(el.formattedValue).to.equal('female'); expect(el.formattedValue).to.equal('female');
el.formElements[0].checked = true; el.formElements[0].checked = true;
expect(el.formattedValue).to.equal('male'); expect(el.formattedValue).to.equal('male');
@ -72,16 +78,20 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi
} }
it('throws if a child element without a modelValue like { value: "foo", checked: false } tries to register', async () => { it('throws if a child element without a modelValue like { value: "foo", checked: false } tries to register', async () => {
const el = /** @type {ChoiceInputGroup} */ (await fixture(html` const el = /** @type {ChoiceInputGroup} */ (
await fixture(html`
<${parentTag} name="gender[]"> <${parentTag} name="gender[]">
<${childTag} .choiceValue=${'male'}></${childTag}> <${childTag} .choiceValue=${'male'}></${childTag}>
<${childTag} .choiceValue=${'female'} checked></${childTag}> <${childTag} .choiceValue=${'female'} checked></${childTag}>
<${childTag} .choiceValue=${'other'}></${childTag}> <${childTag} .choiceValue=${'other'}></${childTag}>
</${parentTag}> </${parentTag}>
`)); `)
const invalidChild = /** @type {ChoiceInputGroup} */ (await fixture(html` );
const invalidChild = /** @type {ChoiceInputGroup} */ (
await fixture(html`
<${childTag} .modelValue=${'Lara'}></${childTag}> <${childTag} .modelValue=${'Lara'}></${childTag}>
`)); `)
);
expect(() => { expect(() => {
el.addFormElement(invalidChild); el.addFormElement(invalidChild);
@ -91,31 +101,37 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi
}); });
it('automatically sets the name property of child fields to its own name', async () => { it('automatically sets the name property of child fields to its own name', async () => {
const el = /** @type {ChoiceInputGroup} */ (await fixture(html` const el = /** @type {ChoiceInputGroup} */ (
await fixture(html`
<${parentTag} name="gender[]"> <${parentTag} name="gender[]">
<${childTag} .choiceValue=${'female'} checked></${childTag}> <${childTag} .choiceValue=${'female'} checked></${childTag}>
<${childTag} .choiceValue=${'other'}></${childTag}> <${childTag} .choiceValue=${'other'}></${childTag}>
</${parentTag}> </${parentTag}>
`)); `)
);
expect(el.formElements[0].name).to.equal('gender[]'); expect(el.formElements[0].name).to.equal('gender[]');
expect(el.formElements[1].name).to.equal('gender[]'); expect(el.formElements[1].name).to.equal('gender[]');
const validChild = /** @type {ChoiceInputGroup} */ (await fixture(html` const validChild = /** @type {ChoiceInputGroup} */ (
await fixture(html`
<${childTag} .choiceValue=${'male'}></${childTag}> <${childTag} .choiceValue=${'male'}></${childTag}>
`)); `)
);
el.appendChild(validChild); el.appendChild(validChild);
expect(el.formElements[2].name).to.equal('gender[]'); expect(el.formElements[2].name).to.equal('gender[]');
}); });
it('automatically updates the name property of child fields to its own name', async () => { it('automatically updates the name property of child fields to its own name', async () => {
const el = /** @type {ChoiceInputGroup} */ (await fixture(html` const el = /** @type {ChoiceInputGroup} */ (
await fixture(html`
<${parentTag} name="gender[]"> <${parentTag} name="gender[]">
<${childTag}></${childTag}> <${childTag}></${childTag}>
<${childTag}></${childTag}> <${childTag}></${childTag}>
</${parentTag}> </${parentTag}>
`)); `)
);
expect(el.formElements[0].name).to.equal('gender[]'); expect(el.formElements[0].name).to.equal('gender[]');
expect(el.formElements[1].name).to.equal('gender[]'); expect(el.formElements[1].name).to.equal('gender[]');
@ -129,12 +145,14 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi
}); });
it('prevents updating the name property of a child if it is different from its parent', async () => { it('prevents updating the name property of a child if it is different from its parent', async () => {
const el = /** @type {ChoiceInputGroup} */ (await fixture(html` const el = /** @type {ChoiceInputGroup} */ (
await fixture(html`
<${parentTag} name="gender[]"> <${parentTag} name="gender[]">
<${childTag}></${childTag}> <${childTag}></${childTag}>
<${childTag}></${childTag}> <${childTag}></${childTag}>
</${parentTag}> </${parentTag}>
`)); `)
);
expect(el.formElements[0].name).to.equal('gender[]'); expect(el.formElements[0].name).to.equal('gender[]');
expect(el.formElements[1].name).to.equal('gender[]'); expect(el.formElements[1].name).to.equal('gender[]');
@ -146,12 +164,14 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi
}); });
it('allows updating the name property of a child if parent tagName does not include childTagname', async () => { it('allows updating the name property of a child if parent tagName does not include childTagname', async () => {
const el = /** @type {ChoiceInputGroup} */ (await fixture(html` const el = /** @type {ChoiceInputGroup} */ (
await fixture(html`
<${parentTag} name="gender[]"> <${parentTag} name="gender[]">
<${childTagFoo}></${childTagFoo}> <${childTagFoo}></${childTagFoo}>
<${childTagFoo}></${childTagFoo}> <${childTagFoo}></${childTagFoo}>
</${parentTag}> </${parentTag}>
`)); `)
);
expect(el.formElements[0].name).to.equal('gender[]'); expect(el.formElements[0].name).to.equal('gender[]');
expect(el.formElements[1].name).to.equal('gender[]'); expect(el.formElements[1].name).to.equal('gender[]');
@ -163,12 +183,14 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi
}); });
it('allows setting the condition for syncing the name property of a child to parent', async () => { it('allows setting the condition for syncing the name property of a child to parent', async () => {
const el = /** @type {ChoiceInputGroup} */ (await fixture(html` const el = /** @type {ChoiceInputGroup} */ (
await fixture(html`
<${parentTag} name="gender[]"> <${parentTag} name="gender[]">
<${childTagBar}></${childTagBar}> <${childTagBar}></${childTagBar}>
<${childTagBar}></${childTagBar}> <${childTagBar}></${childTagBar}>
</${parentTag}> </${parentTag}>
`)); `)
);
expect(el.formElements[0].name).to.equal('gender[]'); expect(el.formElements[0].name).to.equal('gender[]');
expect(el.formElements[1].name).to.equal('gender[]'); expect(el.formElements[1].name).to.equal('gender[]');
@ -180,29 +202,35 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi
}); });
it('adjusts the name of a child element if it has a different name than the group', async () => { it('adjusts the name of a child element if it has a different name than the group', async () => {
const el = /** @type {ChoiceInputGroup} */ (await fixture(html` const el = /** @type {ChoiceInputGroup} */ (
await fixture(html`
<${parentTag} name="gender[]"> <${parentTag} name="gender[]">
<${childTag} .choiceValue=${'female'} checked></${childTag}> <${childTag} .choiceValue=${'female'} checked></${childTag}>
<${childTag} .choiceValue=${'other'}></${childTag}> <${childTag} .choiceValue=${'other'}></${childTag}>
</${parentTag}> </${parentTag}>
`)); `)
);
const invalidChild = /** @type {ChoiceInputGroup} */ (await fixture(html` const invalidChild = /** @type {ChoiceInputGroup} */ (
await fixture(html`
<${childTag} name="foo" .choiceValue=${'male'}></${childTag}> <${childTag} name="foo" .choiceValue=${'male'}></${childTag}>
`)); `)
);
el.addFormElement(invalidChild); el.addFormElement(invalidChild);
await invalidChild.updateComplete; await invalidChild.updateComplete;
expect(invalidChild.name).to.equal('gender[]'); expect(invalidChild.name).to.equal('gender[]');
}); });
it('can set initial modelValue on creation', async () => { it('can set initial modelValue on creation', async () => {
const el = /** @type {ChoiceInputGroup} */ (await fixture(html` const el = /** @type {ChoiceInputGroup} */ (
await fixture(html`
<${parentTag} name="gender[]" .modelValue=${'other'}> <${parentTag} name="gender[]" .modelValue=${'other'}>
<${childTag} .choiceValue=${'male'}></${childTag}> <${childTag} .choiceValue=${'male'}></${childTag}>
<${childTag} .choiceValue=${'female'}></${childTag}> <${childTag} .choiceValue=${'female'}></${childTag}>
<${childTag} .choiceValue=${'other'}></${childTag}> <${childTag} .choiceValue=${'other'}></${childTag}>
</${parentTag}> </${parentTag}>
`)); `)
);
if (cfg.choiceType === 'single') { if (cfg.choiceType === 'single') {
expect(el.modelValue).to.equal('other'); expect(el.modelValue).to.equal('other');
@ -213,13 +241,15 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi
}); });
it('can set initial serializedValue on creation', async () => { it('can set initial serializedValue on creation', async () => {
const el = /** @type {ChoiceInputGroup} */ (await fixture(html` const el = /** @type {ChoiceInputGroup} */ (
await fixture(html`
<${parentTag} name="gender[]" .serializedValue=${'other'}> <${parentTag} name="gender[]" .serializedValue=${'other'}>
<${childTag} .choiceValue=${'male'}></${childTag}> <${childTag} .choiceValue=${'male'}></${childTag}>
<${childTag} .choiceValue=${'female'}></${childTag}> <${childTag} .choiceValue=${'female'}></${childTag}>
<${childTag} .choiceValue=${'other'}></${childTag}> <${childTag} .choiceValue=${'other'}></${childTag}>
</${parentTag}> </${parentTag}>
`)); `)
);
if (cfg.choiceType === 'single') { if (cfg.choiceType === 'single') {
expect(el.serializedValue).to.equal('other'); expect(el.serializedValue).to.equal('other');
@ -230,13 +260,15 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi
}); });
it('can set initial formattedValue on creation', async () => { it('can set initial formattedValue on creation', async () => {
const el = /** @type {ChoiceInputGroup} */ (await fixture(html` const el = /** @type {ChoiceInputGroup} */ (
await fixture(html`
<${parentTag} name="gender[]" .formattedValue=${'other'}> <${parentTag} name="gender[]" .formattedValue=${'other'}>
<${childTag} .choiceValue=${'male'}></${childTag}> <${childTag} .choiceValue=${'male'}></${childTag}>
<${childTag} .choiceValue=${'female'}></${childTag}> <${childTag} .choiceValue=${'female'}></${childTag}>
<${childTag} .choiceValue=${'other'}></${childTag}> <${childTag} .choiceValue=${'other'}></${childTag}>
</${parentTag}> </${parentTag}>
`)); `)
);
if (cfg.choiceType === 'single') { if (cfg.choiceType === 'single') {
expect(el.formattedValue).to.equal('other'); expect(el.formattedValue).to.equal('other');
@ -247,13 +279,15 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi
}); });
it('correctly handles modelValue being set before registrationComplete', async () => { it('correctly handles modelValue being set before registrationComplete', async () => {
const el = /** @type {ChoiceInputGroup} */ (fixtureSync(html` const el = /** @type {ChoiceInputGroup} */ (
fixtureSync(html`
<${parentTag} name="gender[]" .modelValue=${null}> <${parentTag} name="gender[]" .modelValue=${null}>
<${childTag} .choiceValue=${'male'}></${childTag}> <${childTag} .choiceValue=${'male'}></${childTag}>
<${childTag} .choiceValue=${'female'}></${childTag}> <${childTag} .choiceValue=${'female'}></${childTag}>
<${childTag} .choiceValue=${'other'}></${childTag}> <${childTag} .choiceValue=${'other'}></${childTag}>
</${parentTag}> </${parentTag}>
`)); `)
);
if (cfg.choiceType === 'single') { if (cfg.choiceType === 'single') {
el.modelValue = 'other'; el.modelValue = 'other';
@ -267,13 +301,15 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi
}); });
it('correctly handles serializedValue being set before registrationComplete', async () => { it('correctly handles serializedValue being set before registrationComplete', async () => {
const el = /** @type {ChoiceInputGroup} */ (fixtureSync(html` const el = /** @type {ChoiceInputGroup} */ (
fixtureSync(html`
<${parentTag} name="gender[]" .serializedValue=${null}> <${parentTag} name="gender[]" .serializedValue=${null}>
<${childTag} .choiceValue=${'male'}></${childTag}> <${childTag} .choiceValue=${'male'}></${childTag}>
<${childTag} .choiceValue=${'female'}></${childTag}> <${childTag} .choiceValue=${'female'}></${childTag}>
<${childTag} .choiceValue=${'other'}></${childTag}> <${childTag} .choiceValue=${'other'}></${childTag}>
</${parentTag}> </${parentTag}>
`)); `)
);
if (cfg.choiceType === 'single') { if (cfg.choiceType === 'single') {
// @ts-expect-error // @ts-expect-error
@ -289,13 +325,15 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi
}); });
it('can handle null and undefined modelValues', async () => { it('can handle null and undefined modelValues', async () => {
const el = /** @type {ChoiceInputGroup} */ (await fixture(html` const el = /** @type {ChoiceInputGroup} */ (
await fixture(html`
<${parentTag} name="gender[]" .modelValue=${null}> <${parentTag} name="gender[]" .modelValue=${null}>
<${childTag} .choiceValue=${'male'}></${childTag}> <${childTag} .choiceValue=${'male'}></${childTag}>
<${childTag} .choiceValue=${'female'}></${childTag}> <${childTag} .choiceValue=${'female'}></${childTag}>
<${childTag} .choiceValue=${'other'}></${childTag}> <${childTag} .choiceValue=${'other'}></${childTag}>
</${parentTag}> </${parentTag}>
`)); `)
);
if (cfg.choiceType === 'single') { if (cfg.choiceType === 'single') {
expect(el.modelValue).to.equal(''); expect(el.modelValue).to.equal('');
@ -315,12 +353,14 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi
it('can handle complex data via choiceValue', async () => { it('can handle complex data via choiceValue', async () => {
const date = new Date(2018, 11, 24, 10, 33, 30, 0); const date = new Date(2018, 11, 24, 10, 33, 30, 0);
const el = /** @type {ChoiceInputGroup} */ (await fixture(html` const el = /** @type {ChoiceInputGroup} */ (
await fixture(html`
<${parentTag} name="data[]"> <${parentTag} name="data[]">
<${childTag} .choiceValue=${{ some: 'data' }}></${childTag}> <${childTag} .choiceValue=${{ some: 'data' }}></${childTag}>
<${childTag} .choiceValue=${date} checked></${childTag}> <${childTag} .choiceValue=${date} checked></${childTag}>
</${parentTag}> </${parentTag}>
`)); `)
);
if (cfg.choiceType === 'single') { if (cfg.choiceType === 'single') {
expect(el.modelValue).to.equal(date); expect(el.modelValue).to.equal(date);
@ -334,12 +374,14 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi
}); });
it('can handle 0 and empty string as valid values', async () => { it('can handle 0 and empty string as valid values', async () => {
const el = /** @type {ChoiceInputGroup} */ (await fixture(html` const el = /** @type {ChoiceInputGroup} */ (
await fixture(html`
<${parentTag} name="data[]"> <${parentTag} name="data[]">
<${childTag} .choiceValue=${0} checked></${childTag}> <${childTag} .choiceValue=${0} checked></${childTag}>
<${childTag} .choiceValue=${''}></${childTag}> <${childTag} .choiceValue=${''}></${childTag}>
</${parentTag}> </${parentTag}>
`)); `)
);
if (cfg.choiceType === 'single') { if (cfg.choiceType === 'single') {
expect(el.modelValue).to.equal(0); expect(el.modelValue).to.equal(0);
@ -353,7 +395,8 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi
}); });
it('can check a choice by supplying an available modelValue', async () => { it('can check a choice by supplying an available modelValue', async () => {
const el = /** @type {ChoiceInputGroup} */ (await fixture(html` const el = /** @type {ChoiceInputGroup} */ (
await fixture(html`
<${parentTag} name="gender[]"> <${parentTag} name="gender[]">
<${childTag} <${childTag}
.modelValue="${{ value: 'male', checked: false }}" .modelValue="${{ value: 'male', checked: false }}"
@ -365,7 +408,8 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi
.modelValue="${{ value: 'other', checked: false }}" .modelValue="${{ value: 'other', checked: false }}"
></${childTag}> ></${childTag}>
</${parentTag}> </${parentTag}>
`)); `)
);
if (cfg.choiceType === 'single') { if (cfg.choiceType === 'single') {
expect(el.modelValue).to.equal('female'); expect(el.modelValue).to.equal('female');
@ -377,7 +421,8 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi
}); });
it('can check a choice by supplying an available modelValue even if this modelValue is an array or object', async () => { it('can check a choice by supplying an available modelValue even if this modelValue is an array or object', async () => {
const el = /** @type {ChoiceInputGroup} */ (await fixture(html` const el = /** @type {ChoiceInputGroup} */ (
await fixture(html`
<${parentTag} name="gender[]"> <${parentTag} name="gender[]">
<${childTag} <${childTag}
.modelValue="${{ value: { v: 'male' }, checked: false }}" .modelValue="${{ value: { v: 'male' }, checked: false }}"
@ -389,7 +434,8 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi
.modelValue="${{ value: { v: 'other' }, checked: false }}" .modelValue="${{ value: { v: 'other' }, checked: false }}"
></${childTag}> ></${childTag}>
</${parentTag}> </${parentTag}>
`)); `)
);
if (cfg.choiceType === 'single') { if (cfg.choiceType === 'single') {
expect(el.modelValue).to.eql({ v: 'female' }); expect(el.modelValue).to.eql({ v: 'female' });
@ -407,7 +453,8 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi
it('expect child nodes to only fire one model-value-changed event per instance', async () => { it('expect child nodes to only fire one model-value-changed event per instance', async () => {
let counter = 0; let counter = 0;
const el = /** @type {ChoiceInputGroup} */ (await fixture(html` const el = /** @type {ChoiceInputGroup} */ (
await fixture(html`
<${parentTag} <${parentTag}
name="gender[]" name="gender[]"
@model-value-changed=${() => { @model-value-changed=${() => {
@ -420,7 +467,8 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi
></${childTag}> ></${childTag}>
<${childTag} .choiceValue=${'other'}></${childTag}> <${childTag} .choiceValue=${'other'}></${childTag}>
</${parentTag}> </${parentTag}>
`)); `)
);
counter = 0; // reset after setup which may result in different results counter = 0; // reset after setup which may result in different results
@ -454,14 +502,16 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi
}); });
it('can be required', async () => { it('can be required', async () => {
const el = /** @type {ChoiceInputGroup} */ (await fixture(html` const el = /** @type {ChoiceInputGroup} */ (
await fixture(html`
<${parentTag} name="gender[]" .validators=${[new Required()]}> <${parentTag} name="gender[]" .validators=${[new Required()]}>
<${childTag} .choiceValue=${'male'}></${childTag}> <${childTag} .choiceValue=${'male'}></${childTag}>
<${childTag} <${childTag}
.choiceValue=${{ subObject: 'satisfies required' }} .choiceValue=${{ subObject: 'satisfies required' }}
></${childTag}> ></${childTag}>
</${parentTag}> </${parentTag}>
`)); `)
);
expect(el.hasFeedbackFor).to.include('error'); expect(el.hasFeedbackFor).to.include('error');
expect(el.validationStates.error).to.exist; expect(el.validationStates.error).to.exist;
expect(el.validationStates.error.Required).to.exist; expect(el.validationStates.error.Required).to.exist;
@ -478,12 +528,14 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi
}); });
it('returns serialized value', async () => { it('returns serialized value', async () => {
const el = /** @type {ChoiceInputGroup} */ (await fixture(html` const el = /** @type {ChoiceInputGroup} */ (
await fixture(html`
<${parentTag} name="gender[]"> <${parentTag} name="gender[]">
<${childTag} .choiceValue=${'male'}></${childTag}> <${childTag} .choiceValue=${'male'}></${childTag}>
<${childTag} .choiceValue=${'female'}></${childTag}> <${childTag} .choiceValue=${'female'}></${childTag}>
</${parentTag}> </${parentTag}>
`)); `)
);
el.formElements[0].checked = true; el.formElements[0].checked = true;
if (cfg.choiceType === 'single') { if (cfg.choiceType === 'single') {
expect(el.serializedValue).to.deep.equal('male'); expect(el.serializedValue).to.deep.equal('male');
@ -493,12 +545,14 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi
}); });
it('returns serialized value on unchecked state', async () => { it('returns serialized value on unchecked state', async () => {
const el = /** @type {ChoiceInputGroup} */ (await fixture(html` const el = /** @type {ChoiceInputGroup} */ (
await fixture(html`
<${parentTag} name="gender[]"> <${parentTag} name="gender[]">
<${childTag} .choiceValue=${'male'}></${childTag}> <${childTag} .choiceValue=${'male'}></${childTag}>
<${childTag} .choiceValue=${'female'}></${childTag}> <${childTag} .choiceValue=${'female'}></${childTag}>
</${parentTag}> </${parentTag}>
`)); `)
);
if (cfg.choiceType === 'single') { if (cfg.choiceType === 'single') {
expect(el.serializedValue).to.deep.equal(''); expect(el.serializedValue).to.deep.equal('');
@ -508,12 +562,14 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi
}); });
it('can be cleared', async () => { it('can be cleared', async () => {
const el = /** @type {ChoiceInputGroup} */ (await fixture(html` const el = /** @type {ChoiceInputGroup} */ (
await fixture(html`
<${parentTag} name="gender[]"> <${parentTag} name="gender[]">
<${childTag} .choiceValue=${'male'}></${childTag}> <${childTag} .choiceValue=${'male'}></${childTag}>
<${childTag} .choiceValue=${'female'}></${childTag}> <${childTag} .choiceValue=${'female'}></${childTag}>
</${parentTag}> </${parentTag}>
`)); `)
);
el.formElements[0].checked = true; el.formElements[0].checked = true;
el.clear(); el.clear();
@ -526,13 +582,15 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi
describe('multipleChoice', () => { describe('multipleChoice', () => {
it('has a single modelValue representing all currently checked values', async () => { it('has a single modelValue representing all currently checked values', async () => {
const el = /** @type {ChoiceInputGroup} */ (await fixture(html` const el = /** @type {ChoiceInputGroup} */ (
await fixture(html`
<${parentTag} multiple-choice name="gender[]"> <${parentTag} multiple-choice name="gender[]">
<${childTag} .choiceValue=${'male'}></${childTag}> <${childTag} .choiceValue=${'male'}></${childTag}>
<${childTag} .choiceValue=${'female'} checked></${childTag}> <${childTag} .choiceValue=${'female'} checked></${childTag}>
<${childTag} .choiceValue=${'other'}></${childTag}> <${childTag} .choiceValue=${'other'}></${childTag}>
</${parentTag}> </${parentTag}>
`)); `)
);
expect(el.modelValue).to.eql(['female']); expect(el.modelValue).to.eql(['female']);
el.formElements[0].checked = true; el.formElements[0].checked = true;
@ -542,13 +600,15 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi
}); });
it('has a single serializedValue representing all currently checked values', async () => { it('has a single serializedValue representing all currently checked values', async () => {
const el = /** @type {ChoiceInputGroup} */ (await fixture(html` const el = /** @type {ChoiceInputGroup} */ (
await fixture(html`
<${parentTag} multiple-choice name="gender[]"> <${parentTag} multiple-choice name="gender[]">
<${childTag} .choiceValue=${'male'}></${childTag}> <${childTag} .choiceValue=${'male'}></${childTag}>
<${childTag} .choiceValue=${'female'} checked></${childTag}> <${childTag} .choiceValue=${'female'} checked></${childTag}>
<${childTag} .choiceValue=${'other'}></${childTag}> <${childTag} .choiceValue=${'other'}></${childTag}>
</${parentTag}> </${parentTag}>
`)); `)
);
expect(el.serializedValue).to.eql(['female']); expect(el.serializedValue).to.eql(['female']);
el.formElements[0].checked = true; el.formElements[0].checked = true;
@ -558,13 +618,15 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi
}); });
it('has a single formattedValue representing all currently checked values', async () => { it('has a single formattedValue representing all currently checked values', async () => {
const el = /** @type {ChoiceInputGroup} */ (await fixture(html` const el = /** @type {ChoiceInputGroup} */ (
await fixture(html`
<${parentTag} multiple-choice name="gender[]"> <${parentTag} multiple-choice name="gender[]">
<${childTag} .choiceValue=${'male'}></${childTag}> <${childTag} .choiceValue=${'male'}></${childTag}>
<${childTag} .choiceValue=${'female'} checked></${childTag}> <${childTag} .choiceValue=${'female'} checked></${childTag}>
<${childTag} .choiceValue=${'other'}></${childTag}> <${childTag} .choiceValue=${'other'}></${childTag}>
</${parentTag}> </${parentTag}>
`)); `)
);
expect(el.formattedValue).to.eql(['female']); expect(el.formattedValue).to.eql(['female']);
el.formElements[0].checked = true; el.formElements[0].checked = true;
@ -574,13 +636,15 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi
}); });
it('can check multiple checkboxes by setting the modelValue', async () => { it('can check multiple checkboxes by setting the modelValue', async () => {
const el = /** @type {ChoiceInputGroup} */ (await fixture(html` const el = /** @type {ChoiceInputGroup} */ (
await fixture(html`
<${parentTag} multiple-choice name="gender[]"> <${parentTag} multiple-choice name="gender[]">
<${childTag} .choiceValue=${'male'}></${childTag}> <${childTag} .choiceValue=${'male'}></${childTag}>
<${childTag} .choiceValue=${'female'}></${childTag}> <${childTag} .choiceValue=${'female'}></${childTag}>
<${childTag} .choiceValue=${'other'}></${childTag}> <${childTag} .choiceValue=${'other'}></${childTag}>
</${parentTag}> </${parentTag}>
`)); `)
);
el.modelValue = ['male', 'other']; el.modelValue = ['male', 'other'];
expect(el.modelValue).to.eql(['male', 'other']); expect(el.modelValue).to.eql(['male', 'other']);
@ -589,13 +653,15 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi
}); });
it('unchecks non-matching checkboxes when setting the modelValue', async () => { it('unchecks non-matching checkboxes when setting the modelValue', async () => {
const el = /** @type {ChoiceInputGroup} */ (await fixture(html` const el = /** @type {ChoiceInputGroup} */ (
await fixture(html`
<${parentTag} multiple-choice name="gender[]"> <${parentTag} multiple-choice name="gender[]">
<${childTag} .choiceValue=${'male'} checked></${childTag}> <${childTag} .choiceValue=${'male'} checked></${childTag}>
<${childTag} .choiceValue=${'female'}></${childTag}> <${childTag} .choiceValue=${'female'}></${childTag}>
<${childTag} .choiceValue=${'other'} checked></${childTag}> <${childTag} .choiceValue=${'other'} checked></${childTag}>
</${parentTag}> </${parentTag}>
`)); `)
);
expect(el.modelValue).to.eql(['male', 'other']); expect(el.modelValue).to.eql(['male', 'other']);
expect(el.formElements[0].checked).to.be.true; expect(el.formElements[0].checked).to.be.true;
@ -610,7 +676,8 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi
describe('Integration with a parent form/fieldset', () => { describe('Integration with a parent form/fieldset', () => {
it('will serialize all children with their serializedValue', async () => { it('will serialize all children with their serializedValue', async () => {
const el = /** @type {ChoiceInputGroup} */ (await fixture(html` const el = /** @type {ChoiceInputGroup} */ (
await fixture(html`
<lion-fieldset> <lion-fieldset>
<${parentTag} name="gender[]"> <${parentTag} name="gender[]">
<${childTag} .choiceValue=${'male'} checked disabled></${childTag}> <${childTag} .choiceValue=${'male'} checked disabled></${childTag}>
@ -618,7 +685,8 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi
<${childTag} .choiceValue=${'other'}></${childTag}> <${childTag} .choiceValue=${'other'}></${childTag}>
</${parentTag}> </${parentTag}>
</lion-fieldset> </lion-fieldset>
`)); `)
);
if (cfg.choiceType === 'single') { if (cfg.choiceType === 'single') {
expect(el.serializedValue).to.deep.equal({ 'gender[]': ['female'] }); expect(el.serializedValue).to.deep.equal({ 'gender[]': ['female'] });
@ -641,19 +709,19 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi
</lion-fieldset> </lion-fieldset>
`); `);
const choiceGroupEl = /** @type {ChoiceInputGroup} */ (formEl.querySelector( const choiceGroupEl = /** @type {ChoiceInputGroup} */ (
'[name=choice-group]', formEl.querySelector('[name=choice-group]')
)); );
if (choiceGroupEl.multipleChoice) { if (choiceGroupEl.multipleChoice) {
return; return;
} }
/** @typedef {{ checked: boolean }} checkedInterface */ /** @typedef {{ checked: boolean }} checkedInterface */
const option1El = /** @type {HTMLElement & checkedInterface} */ (formEl.querySelector( const option1El = /** @type {HTMLElement & checkedInterface} */ (
'#option1', formEl.querySelector('#option1')
)); );
const option2El = /** @type {HTMLElement & checkedInterface} */ (formEl.querySelector( const option2El = /** @type {HTMLElement & checkedInterface} */ (
'#option2', formEl.querySelector('#option2')
)); );
formEl.addEventListener('model-value-changed', formSpy); formEl.addEventListener('model-value-changed', formSpy);
choiceGroupEl?.addEventListener('model-value-changed', choiceGroupSpy); choiceGroupEl?.addEventListener('model-value-changed', choiceGroupSpy);

View file

@ -1,6 +1,7 @@
import { Required } from '@lion/form-core'; import { Required } from '@lion/form-core';
import { LionInput } from '@lion/input'; import { LionInput } from '@lion/input';
import { expect, fixture, html, unsafeStatic } from '@open-wc/testing'; import { expect, fixture, html, unsafeStatic } from '@open-wc/testing';
import { getFormControlMembers } from '@lion/form-core/test-helpers'; import { getFormControlMembers } from '@lion/form-core/test-helpers';
import sinon from 'sinon'; import sinon from 'sinon';
import { ChoiceInputMixin } from '../../src/choice-group/ChoiceInputMixin.js'; import { ChoiceInputMixin } from '../../src/choice-group/ChoiceInputMixin.js';
@ -15,6 +16,7 @@ customElements.define('choice-group-input', ChoiceInput);
/** /**
* @param {{ tagString?:string, tagType?: string}} [config] * @param {{ tagString?:string, tagType?: string}} [config]
* @deprecated
*/ */
export function runChoiceInputMixinSuite({ tagString } = {}) { export function runChoiceInputMixinSuite({ tagString } = {}) {
const cfg = { const cfg = {
@ -29,9 +31,9 @@ export function runChoiceInputMixinSuite({ tagString } = {}) {
}); });
it('has choiceValue', async () => { it('has choiceValue', async () => {
const el = /** @type {ChoiceInput} */ (await fixture( const el = /** @type {ChoiceInput} */ (
html`<${tag} .choiceValue=${'foo'}></${tag}>`, await fixture(html`<${tag} .choiceValue=${'foo'}></${tag}>`)
)); );
expect(el.choiceValue).to.equal('foo'); expect(el.choiceValue).to.equal('foo');
expect(el.modelValue).to.deep.equal({ expect(el.modelValue).to.deep.equal({
@ -43,9 +45,9 @@ export function runChoiceInputMixinSuite({ tagString } = {}) {
it('can handle complex data via choiceValue', async () => { it('can handle complex data via choiceValue', async () => {
const date = new Date(2018, 11, 24, 10, 33, 30, 0); const date = new Date(2018, 11, 24, 10, 33, 30, 0);
const el = /** @type {ChoiceInput} */ (await fixture( const el = /** @type {ChoiceInput} */ (
html`<${tag} .choiceValue=${date}></${tag}>`, await fixture(html`<${tag} .choiceValue=${date}></${tag}>`)
)); );
expect(el.choiceValue).to.equal(date); expect(el.choiceValue).to.equal(date);
expect(el.modelValue.value).to.equal(date); expect(el.modelValue.value).to.equal(date);
@ -53,14 +55,16 @@ export function runChoiceInputMixinSuite({ tagString } = {}) {
it('fires one "model-value-changed" event if choiceValue or checked state or modelValue changed', async () => { it('fires one "model-value-changed" event if choiceValue or checked state or modelValue changed', async () => {
let counter = 0; let counter = 0;
const el = /** @type {ChoiceInput} */ (await fixture(html` const el = /** @type {ChoiceInput} */ (
await fixture(html`
<${tag} <${tag}
@model-value-changed=${() => { @model-value-changed=${() => {
counter += 1; counter += 1;
}} }}
.choiceValue=${'foo'} .choiceValue=${'foo'}
></${tag}> ></${tag}>
`)); `)
);
expect(counter).to.equal(1); // undefined to set value expect(counter).to.equal(1); // undefined to set value
el.checked = true; el.checked = true;
@ -78,7 +82,8 @@ export function runChoiceInputMixinSuite({ tagString } = {}) {
it('fires one "user-input-changed" event after user interaction', async () => { it('fires one "user-input-changed" event after user interaction', async () => {
let counter = 0; let counter = 0;
const el = /** @type {ChoiceInput} */ (await fixture(html` const el = /** @type {ChoiceInput} */ (
await fixture(html`
<${tag} <${tag}
@user-input-changed="${() => { @user-input-changed="${() => {
counter += 1; counter += 1;
@ -86,7 +91,8 @@ export function runChoiceInputMixinSuite({ tagString } = {}) {
> >
<input slot="input" /> <input slot="input" />
</${tag}> </${tag}>
`)); `)
);
const { _inputNode } = getFormControlMembers(el); const { _inputNode } = getFormControlMembers(el);
expect(counter).to.equal(0); expect(counter).to.equal(0);
@ -100,13 +106,15 @@ export function runChoiceInputMixinSuite({ tagString } = {}) {
it('fires one "click" event when clicking label or input, using the right target', async () => { it('fires one "click" event when clicking label or input, using the right target', async () => {
const spy = sinon.spy(); const spy = sinon.spy();
const el = /** @type {ChoiceInput} */ (await fixture(html` const el = /** @type {ChoiceInput} */ (
await fixture(html`
<${tag} <${tag}
@click="${spy}" @click="${spy}"
> >
<input slot="input" /> <input slot="input" />
</${tag}> </${tag}>
`)); `)
);
const { _inputNode, _labelNode } = getFormControlMembers(el); const { _inputNode, _labelNode } = getFormControlMembers(el);
el.click(); el.click();
@ -122,7 +130,8 @@ export function runChoiceInputMixinSuite({ tagString } = {}) {
it('adds "isTriggerByUser" flag on model-value-changed', async () => { it('adds "isTriggerByUser" flag on model-value-changed', async () => {
let isTriggeredByUser; let isTriggeredByUser;
const el = /** @type {ChoiceInput} */ (await fixture(html` const el = /** @type {ChoiceInput} */ (
await fixture(html`
<${tag} <${tag}
@model-value-changed="${(/** @type {CustomEvent} */ event) => { @model-value-changed="${(/** @type {CustomEvent} */ event) => {
isTriggeredByUser = event.detail.isTriggeredByUser; isTriggeredByUser = event.detail.isTriggeredByUser;
@ -130,7 +139,8 @@ export function runChoiceInputMixinSuite({ tagString } = {}) {
> >
<input slot="input" /> <input slot="input" />
</${tag}> </${tag}>
`)); `)
);
const { _inputNode } = getFormControlMembers(el); const { _inputNode } = getFormControlMembers(el);
_inputNode.dispatchEvent(new CustomEvent('change', { bubbles: true })); _inputNode.dispatchEvent(new CustomEvent('change', { bubbles: true }));
@ -138,9 +148,11 @@ export function runChoiceInputMixinSuite({ tagString } = {}) {
}); });
it('can be required', async () => { it('can be required', async () => {
const el = /** @type {ChoiceInput} */ (await fixture(html` const el = /** @type {ChoiceInput} */ (
await fixture(html`
<${tag} .choiceValue=${'foo'} .validators=${[new Required()]}></${tag}> <${tag} .choiceValue=${'foo'} .validators=${[new Required()]}></${tag}>
`)); `)
);
expect(el.hasFeedbackFor).to.include('error'); expect(el.hasFeedbackFor).to.include('error');
expect(el.validationStates.error).to.exist; expect(el.validationStates.error).to.exist;
@ -156,9 +168,11 @@ export function runChoiceInputMixinSuite({ tagString } = {}) {
const el = /** @type {ChoiceInput} */ (await fixture(html`<${tag}></${tag}>`)); const el = /** @type {ChoiceInput} */ (await fixture(html`<${tag}></${tag}>`));
expect(el.checked).to.equal(false, 'initially unchecked'); expect(el.checked).to.equal(false, 'initially unchecked');
const precheckedElementAttr = /** @type {ChoiceInput} */ (await fixture(html` const precheckedElementAttr = /** @type {ChoiceInput} */ (
await fixture(html`
<${tag} .checked=${true}></${tag}> <${tag} .checked=${true}></${tag}>
`)); `)
);
expect(precheckedElementAttr.checked).to.equal(true, 'initially checked via attribute'); expect(precheckedElementAttr.checked).to.equal(true, 'initially checked via attribute');
}); });
@ -196,9 +210,9 @@ export function runChoiceInputMixinSuite({ tagString } = {}) {
}); });
it('synchronizes modelValue to checked state and vice versa', async () => { it('synchronizes modelValue to checked state and vice versa', async () => {
const el = /** @type {ChoiceInput} */ (await fixture( const el = /** @type {ChoiceInput} */ (
html`<${tag} .choiceValue=${'foo'}></${tag}>`, await fixture(html`<${tag} .choiceValue=${'foo'}></${tag}>`)
)); );
expect(el.checked).to.be.false; expect(el.checked).to.be.false;
expect(el.modelValue).to.deep.equal({ expect(el.modelValue).to.deep.equal({
checked: false, checked: false,
@ -215,9 +229,9 @@ export function runChoiceInputMixinSuite({ tagString } = {}) {
it('ensures optimal synchronize performance by preventing redundant computation steps', async () => { it('ensures optimal synchronize performance by preventing redundant computation steps', async () => {
/* we are checking private apis here to make sure we do not have cyclical updates /* we are checking private apis here to make sure we do not have cyclical updates
which can be quite common for these type of connected data */ which can be quite common for these type of connected data */
const el = /** @type {ChoiceInput} */ (await fixture( const el = /** @type {ChoiceInput} */ (
html`<${tag} .choiceValue=${'foo'}></${tag}>`, await fixture(html`<${tag} .choiceValue=${'foo'}></${tag}>`)
)); );
expect(el.checked).to.be.false; expect(el.checked).to.be.false;
// @ts-ignore [allow-private] in test // @ts-ignore [allow-private] in test
@ -245,11 +259,13 @@ export function runChoiceInputMixinSuite({ tagString } = {}) {
/** @param {ChoiceInput} el */ /** @param {ChoiceInput} el */
const hasAttr = el => el.hasAttribute('checked'); const hasAttr = el => el.hasAttribute('checked');
const el = /** @type {ChoiceInput} */ (await fixture(html`<${tag}></${tag}>`)); const el = /** @type {ChoiceInput} */ (await fixture(html`<${tag}></${tag}>`));
const elChecked = /** @type {ChoiceInput} */ (await fixture(html` const elChecked = /** @type {ChoiceInput} */ (
await fixture(html`
<${tag} .checked=${true}> <${tag} .checked=${true}>
<input slot="input" /> <input slot="input" />
</${tag}> </${tag}>
`)); `)
);
const { _inputNode } = getFormControlMembers(el); const { _inputNode } = getFormControlMembers(el);
const { _inputNode: _inputNodeChecked } = getFormControlMembers(elChecked); const { _inputNode: _inputNodeChecked } = getFormControlMembers(elChecked);
@ -294,14 +310,16 @@ export function runChoiceInputMixinSuite({ tagString } = {}) {
describe('Format/parse/serialize loop', () => { describe('Format/parse/serialize loop', () => {
it('creates a modelValue object like { checked: true, value: foo } on init', async () => { it('creates a modelValue object like { checked: true, value: foo } on init', async () => {
const el = /** @type {ChoiceInput} */ (await fixture( const el = /** @type {ChoiceInput} */ (
html`<${tag} .choiceValue=${'foo'}></${tag}>`, await fixture(html`<${tag} .choiceValue=${'foo'}></${tag}>`)
)); );
expect(el.modelValue).deep.equal({ value: 'foo', checked: false }); expect(el.modelValue).deep.equal({ value: 'foo', checked: false });
const elChecked = /** @type {ChoiceInput} */ (await fixture(html` const elChecked = /** @type {ChoiceInput} */ (
await fixture(html`
<${tag} .choiceValue=${'foo'} .checked=${true}></${tag}> <${tag} .choiceValue=${'foo'} .checked=${true}></${tag}>
`)); `)
);
expect(elChecked.modelValue).deep.equal({ value: 'foo', checked: true }); expect(elChecked.modelValue).deep.equal({ value: 'foo', checked: true });
}); });
@ -309,9 +327,11 @@ export function runChoiceInputMixinSuite({ tagString } = {}) {
const el = /** @type {ChoiceInput} */ (await fixture(html`<${tag}></${tag}>`)); const el = /** @type {ChoiceInput} */ (await fixture(html`<${tag}></${tag}>`));
expect(el.formattedValue).to.equal(''); expect(el.formattedValue).to.equal('');
const elementWithValue = /** @type {ChoiceInput} */ (await fixture(html` const elementWithValue = /** @type {ChoiceInput} */ (
await fixture(html`
<${tag} .choiceValue=${'foo'}></${tag}> <${tag} .choiceValue=${'foo'}></${tag}>
`)); `)
);
expect(elementWithValue.formattedValue).to.equal('foo'); expect(elementWithValue.formattedValue).to.equal('foo');
}); });
@ -325,9 +345,9 @@ export function runChoiceInputMixinSuite({ tagString } = {}) {
describe('Interaction states', () => { describe('Interaction states', () => {
it('is considered prefilled when checked and not considered prefilled when unchecked', async () => { it('is considered prefilled when checked and not considered prefilled when unchecked', async () => {
const el = /** @type {ChoiceInput} */ (await fixture( const el = /** @type {ChoiceInput} */ (
html`<${tag} .checked=${true}></${tag}>`, await fixture(html`<${tag} .checked=${true}></${tag}>`)
)); );
expect(el.prefilled).equal(true, 'checked element not considered prefilled'); expect(el.prefilled).equal(true, 'checked element not considered prefilled');
const elUnchecked = /** @type {ChoiceInput} */ (await fixture(html`<${tag}></${tag}>`)); const elUnchecked = /** @type {ChoiceInput} */ (await fixture(html`<${tag}></${tag}>`));

View file

@ -1,6 +1,7 @@
import { LitElement } from '@lion/core'; import { LitElement } from '@lion/core';
import { localizeTearDown } from '@lion/localize/test-helpers'; import { localizeTearDown } from '@lion/localize/test-helpers';
import { defineCE, expect, html, unsafeStatic, fixture } from '@open-wc/testing'; import { html, unsafeStatic } from 'lit/static-html.js';
import { defineCE, expect, fixture } from '@open-wc/testing';
import { getFormControlMembers } from '@lion/form-core/test-helpers'; import { getFormControlMembers } from '@lion/form-core/test-helpers';
import { LionInput } from '@lion/input'; import { LionInput } from '@lion/input';
import '@lion/form-core/define'; import '@lion/form-core/define';
@ -47,12 +48,14 @@ export function runFormGroupMixinInputSuite(cfg = {}) {
describe('FormGroupMixin with LionField', () => { describe('FormGroupMixin with LionField', () => {
it('serializes undefined values as "" (nb radios/checkboxes are always serialized)', async () => { it('serializes undefined values as "" (nb radios/checkboxes are always serialized)', async () => {
const fieldset = /** @type {FormGroup} */ (await fixture(html` const fieldset = /** @type {FormGroup} */ (
await fixture(html`
<${tag}> <${tag}>
<${childTag} name="custom[]"></${childTag}> <${childTag} name="custom[]"></${childTag}>
<${childTag} name="custom[]"></${childTag}> <${childTag} name="custom[]"></${childTag}>
</${tag}> </${tag}>
`)); `)
);
fieldset.formElements['custom[]'][0].modelValue = 'custom 1'; fieldset.formElements['custom[]'][0].modelValue = 'custom 1';
fieldset.formElements['custom[]'][1].modelValue = undefined; fieldset.formElements['custom[]'][1].modelValue = undefined;
@ -62,12 +65,14 @@ export function runFormGroupMixinInputSuite(cfg = {}) {
}); });
it('suffixes child labels with group label, just like in <fieldset>', async () => { it('suffixes child labels with group label, just like in <fieldset>', async () => {
const el = /** @type {FormGroup} */ (await fixture(html` const el = /** @type {FormGroup} */ (
await fixture(html`
<${tag} label="set"> <${tag} label="set">
<${childTag} name="A" label="fieldA"></${childTag}> <${childTag} name="A" label="fieldA"></${childTag}>
<${childTag} name="B" label="fieldB"></${childTag}> <${childTag} name="B" label="fieldB"></${childTag}>
</${tag}> </${tag}>
`)); `)
);
const { _labelNode } = getFormControlMembers(el); const { _labelNode } = getFormControlMembers(el);
/** /**
@ -88,8 +93,10 @@ export function runFormGroupMixinInputSuite(cfg = {}) {
// Test the cleanup on disconnected // Test the cleanup on disconnected
el.removeChild(field1); el.removeChild(field1);
await field1.updateComplete;
expect(getLabels(field1)).to.eql([field1._labelNode.id]); // TODO: wait for updated on disconnected to be fixed: https://github.com/lit/lit/issues/1901
// await field1.updateComplete;
// expect(getLabels(field1)).to.eql([field1._labelNode.id]);
}); });
}); });
@ -110,7 +117,8 @@ export function runFormGroupMixinInputSuite(cfg = {}) {
childAriaFixture = async ( childAriaFixture = async (
msgSlotType = 'feedback', // eslint-disable-line no-shadow msgSlotType = 'feedback', // eslint-disable-line no-shadow
) => { ) => {
const dom = /** @type {FormGroup} */ (await fixture(html` const dom = /** @type {FormGroup} */ (
await fixture(html`
<${tag} name="l1_g"> <${tag} name="l1_g">
<${childTag} name="l1_fa"> <${childTag} name="l1_fa">
<div slot="${msgSlotType}" id="msg_l1_fa"></div> <div slot="${msgSlotType}" id="msg_l1_fa"></div>
@ -144,7 +152,8 @@ export function runFormGroupMixinInputSuite(cfg = {}) {
<div slot="${msgSlotType}" id="msg_l1_g"></div> <div slot="${msgSlotType}" id="msg_l1_g"></div>
<!-- group referred by: #msg_l1_g (local) --> <!-- group referred by: #msg_l1_g (local) -->
</${tag}> </${tag}>
`)); `)
);
return dom; return dom;
}; };
@ -163,18 +172,18 @@ export function runFormGroupMixinInputSuite(cfg = {}) {
const msg_l2_fb = /** @type {FormChild} */ (childAriaFixture.querySelector('#msg_l2_fb')); const msg_l2_fb = /** @type {FormChild} */ (childAriaFixture.querySelector('#msg_l2_fb'));
// Field elements: all inputs pointing to message elements // Field elements: all inputs pointing to message elements
const input_l1_fa = /** @type {HTMLInputElement} */ (childAriaFixture.querySelector( const input_l1_fa = /** @type {HTMLInputElement} */ (
'input[name=l1_fa]', childAriaFixture.querySelector('input[name=l1_fa]')
)); );
const input_l1_fb = /** @type {HTMLInputElement} */ (childAriaFixture.querySelector( const input_l1_fb = /** @type {HTMLInputElement} */ (
'input[name=l1_fb]', childAriaFixture.querySelector('input[name=l1_fb]')
)); );
const input_l2_fa = /** @type {HTMLInputElement} */ (childAriaFixture.querySelector( const input_l2_fa = /** @type {HTMLInputElement} */ (
'input[name=l2_fa]', childAriaFixture.querySelector('input[name=l2_fa]')
)); );
const input_l2_fb = /** @type {HTMLInputElement} */ (childAriaFixture.querySelector( const input_l2_fb = /** @type {HTMLInputElement} */ (
'input[name=l2_fb]', childAriaFixture.querySelector('input[name=l2_fb]')
)); );
if (!cleanupPhase) { if (!cleanupPhase) {
// 'L1' fields (inside lion-fieldset[name="l1_g"]) should point to l1(group) msg // 'L1' fields (inside lion-fieldset[name="l1_g"]) should point to l1(group) msg
@ -222,18 +231,18 @@ export function runFormGroupMixinInputSuite(cfg = {}) {
).to.equal(true, 'order of ids'); ).to.equal(true, 'order of ids');
} else { } else {
// cleanupPhase // cleanupPhase
const control_l1_fa = /** @type {LionField} */ (childAriaFixture.querySelector( const control_l1_fa = /** @type {LionField} */ (
'[name=l1_fa]', childAriaFixture.querySelector('[name=l1_fa]')
)); );
const control_l1_fb = /** @type {LionField} */ (childAriaFixture.querySelector( const control_l1_fb = /** @type {LionField} */ (
'[name=l1_fb]', childAriaFixture.querySelector('[name=l1_fb]')
)); );
const control_l2_fa = /** @type {LionField} */ (childAriaFixture.querySelector( const control_l2_fa = /** @type {LionField} */ (
'[name=l2_fa]', childAriaFixture.querySelector('[name=l2_fa]')
)); );
const control_l2_fb = /** @type {LionField} */ (childAriaFixture.querySelector( const control_l2_fb = /** @type {LionField} */ (
'[name=l2_fb]', childAriaFixture.querySelector('[name=l2_fb]')
)); );
// @ts-expect-error removeChild should always be inherited via LitElement? // @ts-expect-error removeChild should always be inherited via LitElement?
control_l1_fa._parentFormGroup.removeChild(control_l1_fa); control_l1_fa._parentFormGroup.removeChild(control_l1_fa);
@ -303,12 +312,14 @@ export function runFormGroupMixinInputSuite(cfg = {}) {
await childAriaTest(await childAriaFixture('help-text')); await childAriaTest(await childAriaFixture('help-text'));
}); });
it(`cleans up feedback message belonging to fieldset on disconnect`, async () => { // TODO: wait for updated on disconnected to be fixed: https://github.com/lit/lit/issues/1901
it.skip(`cleans up feedback message belonging to fieldset on disconnect`, async () => {
const el = await childAriaFixture('feedback'); const el = await childAriaFixture('feedback');
await childAriaTest(el, { cleanupPhase: true }); await childAriaTest(el, { cleanupPhase: true });
}); });
it(`cleans up help-text message belonging to fieldset on disconnect`, async () => { // TODO: wait for updated on disconnected to be fixed: https://github.com/lit/lit/issues/1901
it.skip(`cleans up help-text message belonging to fieldset on disconnect`, async () => {
const el = await childAriaFixture('help-text'); const el = await childAriaFixture('help-text');
await childAriaTest(el, { cleanupPhase: true }); await childAriaTest(el, { cleanupPhase: true });
}); });

View file

@ -1,14 +1,7 @@
import { LitElement, ifDefined } from '@lion/core'; import { LitElement, ifDefined } from '@lion/core';
import { html, unsafeStatic } from 'lit/static-html.js';
import { localizeTearDown } from '@lion/localize/test-helpers'; import { localizeTearDown } from '@lion/localize/test-helpers';
import { import { defineCE, expect, triggerFocusFor, fixture, aTimeout } from '@open-wc/testing';
defineCE,
expect,
html,
triggerFocusFor,
unsafeStatic,
fixture,
aTimeout,
} from '@open-wc/testing';
import sinon from 'sinon'; import sinon from 'sinon';
import { IsNumber, Validator, LionField } from '@lion/form-core'; import { IsNumber, Validator, LionField } from '@lion/form-core';
import '@lion/form-core/define'; import '@lion/form-core/define';
@ -59,30 +52,32 @@ export function runFormGroupMixinSuite(cfg = {}) {
describe('FormGroupMixin', () => { describe('FormGroupMixin', () => {
// TODO: Tests below belong to FormControlMixin. Preferably run suite integration test // TODO: Tests below belong to FormControlMixin. Preferably run suite integration test
it(`has a fieldName based on the label`, async () => { it(`has a fieldName based on the label`, async () => {
const el1 = /** @type {FormGroup} */ (await fixture( const el1 = /** @type {FormGroup} */ (
html`<${tag} label="foo">${inputSlots}</${tag}>`, await fixture(html`<${tag} label="foo">${inputSlots}</${tag}>`)
)); );
const { _labelNode: _labelNode1 } = getFormControlMembers(el1); const { _labelNode: _labelNode1 } = getFormControlMembers(el1);
expect(el1.fieldName).to.equal(_labelNode1.textContent); expect(el1.fieldName).to.equal(_labelNode1.textContent);
const el2 = /** @type {FormGroup} */ (await fixture( const el2 = /** @type {FormGroup} */ (
html`<${tag}><label slot="label">bar</label>${inputSlots}</${tag}>`, await fixture(html`<${tag}><label slot="label">bar</label>${inputSlots}</${tag}>`)
)); );
const { _labelNode: _labelNode2 } = getFormControlMembers(el2); const { _labelNode: _labelNode2 } = getFormControlMembers(el2);
expect(el2.fieldName).to.equal(_labelNode2.textContent); expect(el2.fieldName).to.equal(_labelNode2.textContent);
}); });
it(`has a fieldName based on the name if no label exists`, async () => { it(`has a fieldName based on the name if no label exists`, async () => {
const el = /** @type {FormGroup} */ (await fixture( const el = /** @type {FormGroup} */ (
html`<${tag} name="foo">${inputSlots}</${tag}>`, await fixture(html`<${tag} name="foo">${inputSlots}</${tag}>`)
)); );
expect(el.fieldName).to.equal(el.name); expect(el.fieldName).to.equal(el.name);
}); });
it(`can override fieldName`, async () => { it(`can override fieldName`, async () => {
const el = /** @type {FormGroup} */ (await fixture(html` const el = /** @type {FormGroup} */ (
await fixture(html`
<${tag} label="foo" .fieldName="${'bar'}">${inputSlots}</${tag}> <${tag} label="foo" .fieldName="${'bar'}">${inputSlots}</${tag}>
`)); `)
);
// @ts-ignore [allow-protected] in test // @ts-ignore [allow-protected] in test
expect(el.__fieldName).to.equal(el.fieldName); expect(el.__fieldName).to.equal(el.fieldName);
}); });
@ -100,13 +95,15 @@ export function runFormGroupMixinSuite(cfg = {}) {
}); });
it(`supports in html wrapped form elements`, async () => { it(`supports in html wrapped form elements`, async () => {
const el = /** @type {FormGroup} */ (await fixture(html` const el = /** @type {FormGroup} */ (
await fixture(html`
<${tag}> <${tag}>
<div> <div>
<${childTag} name="foo"></${childTag}> <${childTag} name="foo"></${childTag}>
</div> </div>
</${tag}> </${tag}>
`)); `)
);
expect(el.formElements.length).to.equal(1); expect(el.formElements.length).to.equal(1);
el.children[0].removeChild(el.formElements.foo); el.children[0].removeChild(el.formElements.foo);
expect(el.formElements.length).to.equal(0); expect(el.formElements.length).to.equal(0);
@ -206,9 +203,9 @@ export function runFormGroupMixinSuite(cfg = {}) {
it('can dynamically add/remove elements', async () => { it('can dynamically add/remove elements', async () => {
const el = /** @type {FormGroup} */ (await fixture(html`<${tag}>${inputSlots}</${tag}>`)); const el = /** @type {FormGroup} */ (await fixture(html`<${tag}>${inputSlots}</${tag}>`));
const newField = /** @type {FormGroup} */ (await fixture( const newField = /** @type {FormGroup} */ (
html`<${childTag} name="lastName"></${childTag}>`, await fixture(html`<${childTag} name="lastName"></${childTag}>`)
)); );
const { _inputNode } = getFormControlMembers(el); const { _inputNode } = getFormControlMembers(el);
// @ts-ignore [allow-protected] in test // @ts-ignore [allow-protected] in test
@ -226,12 +223,14 @@ export function runFormGroupMixinSuite(cfg = {}) {
// TODO: Tests below belong to FormGroupMixin. Preferably run suite integration test // TODO: Tests below belong to FormGroupMixin. Preferably run suite integration test
it('can read/write all values (of every input) via this.modelValue', async () => { it('can read/write all values (of every input) via this.modelValue', async () => {
const el = /** @type {FormGroup} */ (await fixture(html` const el = /** @type {FormGroup} */ (
await fixture(html`
<${tag}> <${tag}>
<${childTag} name="lastName"></${childTag}> <${childTag} name="lastName"></${childTag}>
<${tag} name="newfieldset">${inputSlots}</${tag}> <${tag} name="newfieldset">${inputSlots}</${tag}>
</${tag}> </${tag}>
`)); `)
);
const newFieldset = /** @type {FormGroup} */ (el.querySelector(tagString)); const newFieldset = /** @type {FormGroup} */ (el.querySelector(tagString));
el.formElements.lastName.modelValue = 'Bar'; el.formElements.lastName.modelValue = 'Bar';
newFieldset.formElements['hobbies[]'][0].modelValue = { checked: true, value: 'chess' }; newFieldset.formElements['hobbies[]'][0].modelValue = { checked: true, value: 'chess' };
@ -301,7 +300,8 @@ export function runFormGroupMixinSuite(cfg = {}) {
}); });
it('does not list disabled values in this.modelValue', async () => { it('does not list disabled values in this.modelValue', async () => {
const el = /** @type {FormGroup} */ (await fixture(html` const el = /** @type {FormGroup} */ (
await fixture(html`
<${tag}> <${tag}>
<${childTag} name="a" disabled .modelValue="${'x'}"></${childTag}> <${childTag} name="a" disabled .modelValue="${'x'}"></${childTag}>
<${childTag} name="b" .modelValue="${'x'}"></${childTag}> <${childTag} name="b" .modelValue="${'x'}"></${childTag}>
@ -313,7 +313,8 @@ export function runFormGroupMixinSuite(cfg = {}) {
<${childTag} name="e" .modelValue="${'x'}"></${childTag}> <${childTag} name="e" .modelValue="${'x'}"></${childTag}>
</${tag}> </${tag}>
</${tag}> </${tag}>
`)); `)
);
expect(el.modelValue).to.deep.equal({ expect(el.modelValue).to.deep.equal({
b: 'x', b: 'x',
newFieldset: { newFieldset: {
@ -323,12 +324,14 @@ export function runFormGroupMixinSuite(cfg = {}) {
}); });
it('does not throw if setter data of this.modelValue can not be handled', async () => { it('does not throw if setter data of this.modelValue can not be handled', async () => {
const el = /** @type {FormGroup} */ (await fixture(html` const el = /** @type {FormGroup} */ (
await fixture(html`
<${tag}> <${tag}>
<${childTag} name="firstName" .modelValue=${'foo'}></${childTag}> <${childTag} name="firstName" .modelValue=${'foo'}></${childTag}>
<${childTag} name="lastName" .modelValue=${'bar'}></${childTag}> <${childTag} name="lastName" .modelValue=${'bar'}></${childTag}>
</${tag}> </${tag}>
`)); `)
);
const initState = { const initState = {
firstName: 'foo', firstName: 'foo',
lastName: 'bar', lastName: 'bar',
@ -343,9 +346,9 @@ export function runFormGroupMixinSuite(cfg = {}) {
}); });
it('disables/enables all its formElements if it becomes disabled/enabled', async () => { it('disables/enables all its formElements if it becomes disabled/enabled', async () => {
const el = /** @type {FormGroup} */ (await fixture( const el = /** @type {FormGroup} */ (
html`<${tag} disabled>${inputSlots}</${tag}>`, await fixture(html`<${tag} disabled>${inputSlots}</${tag}>`)
)); );
expect(el.formElements.color.disabled).to.be.true; expect(el.formElements.color.disabled).to.be.true;
expect(el.formElements['hobbies[]'][0].disabled).to.be.true; expect(el.formElements['hobbies[]'][0].disabled).to.be.true;
expect(el.formElements['hobbies[]'][1].disabled).to.be.true; expect(el.formElements['hobbies[]'][1].disabled).to.be.true;
@ -358,11 +361,13 @@ export function runFormGroupMixinSuite(cfg = {}) {
}); });
it('does not propagate/override initial disabled value on nested form elements', async () => { it('does not propagate/override initial disabled value on nested form elements', async () => {
const el = /** @type {FormGroup} */ (await fixture(html` const el = /** @type {FormGroup} */ (
await fixture(html`
<${tag}> <${tag}>
<${tag} name="sub" disabled>${inputSlots}</${tag}> <${tag} name="sub" disabled>${inputSlots}</${tag}>
</${tag}> </${tag}>
`)); `)
);
expect(el.disabled).to.equal(false); expect(el.disabled).to.equal(false);
expect(el.formElements.sub.disabled).to.be.true; expect(el.formElements.sub.disabled).to.be.true;
@ -372,11 +377,13 @@ export function runFormGroupMixinSuite(cfg = {}) {
}); });
it('can set initial modelValue on creation', async () => { it('can set initial modelValue on creation', async () => {
const el = /** @type {FormGroup} */ (await fixture(html` const el = /** @type {FormGroup} */ (
await fixture(html`
<${tag} .modelValue=${{ lastName: 'Bar' }}> <${tag} .modelValue=${{ lastName: 'Bar' }}>
<${childTag} name="lastName"></${childTag}> <${childTag} name="lastName"></${childTag}>
</${tag}> </${tag}>
`)); `)
);
expect(el.modelValue).to.eql({ expect(el.modelValue).to.eql({
lastName: 'Bar', lastName: 'Bar',
@ -384,11 +391,13 @@ export function runFormGroupMixinSuite(cfg = {}) {
}); });
it('can set initial serializedValue on creation', async () => { it('can set initial serializedValue on creation', async () => {
const el = /** @type {FormGroup} */ (await fixture(html` const el = /** @type {FormGroup} */ (
await fixture(html`
<${tag} .modelValue=${{ lastName: 'Bar' }}> <${tag} .modelValue=${{ lastName: 'Bar' }}>
<${childTag} name="lastName"></${childTag}> <${childTag} name="lastName"></${childTag}>
</${tag}> </${tag}>
`)); `)
);
expect(el.modelValue).to.eql({ lastName: 'Bar' }); expect(el.modelValue).to.eql({ lastName: 'Bar' });
}); });
@ -409,13 +418,15 @@ export function runFormGroupMixinSuite(cfg = {}) {
} }
} }
const el = /** @type {FormGroup} */ (await fixture(html` const el = /** @type {FormGroup} */ (
await fixture(html`
<${tag}> <${tag}>
<${childTag} name="color" .validators=${[ <${childTag} name="color" .validators=${[
new IsCat(), new IsCat(),
]} .modelValue=${'blue'}></${childTag}> ]} .modelValue=${'blue'}></${childTag}>
</${tag}> </${tag}>
`)); `)
);
expect(el.formElements.color.validationStates.error.IsCat).to.be.true; expect(el.formElements.color.validationStates.error.IsCat).to.be.true;
}); });
@ -442,13 +453,15 @@ export function runFormGroupMixinSuite(cfg = {}) {
} }
} }
const el = /** @type {FormGroup} */ (await fixture(html` const el = /** @type {FormGroup} */ (
await fixture(html`
<${tag}> <${tag}>
<${childTag} name="color" .validators=${[ <${childTag} name="color" .validators=${[
new IsCat(), new IsCat(),
]} .modelValue=${'blue'}></${childTag}> ]} .modelValue=${'blue'}></${childTag}>
</${tag}> </${tag}>
`)); `)
);
expect(el.validationStates.error.FormElementsHaveNoError).to.be.true; expect(el.validationStates.error.FormElementsHaveNoError).to.be.true;
expect(el.formElements.color.validationStates.error.IsCat).to.be.true; expect(el.formElements.color.validationStates.error.IsCat).to.be.true;
@ -470,14 +483,18 @@ export function runFormGroupMixinSuite(cfg = {}) {
return hasError; return hasError;
} }
} }
const el = /** @type {FormGroup} */ (await fixture(html` const el = /** @type {FormGroup} */ (
await fixture(html`
<${tag} .validators=${[new HasEvenNumberOfChildren()]}> <${tag} .validators=${[new HasEvenNumberOfChildren()]}>
<${childTag} id="c1" name="c1"></${childTag}> <${childTag} id="c1" name="c1"></${childTag}>
</${tag}> </${tag}>
`)); `)
const child2 = /** @type {FormGroup} */ (await fixture(html` );
const child2 = /** @type {FormGroup} */ (
await fixture(html`
<${childTag} name="c2"></${childTag}> <${childTag} name="c2"></${childTag}>
`)); `)
);
expect(el.validationStates.error.HasEvenNumberOfChildren).to.be.true; expect(el.validationStates.error.HasEvenNumberOfChildren).to.be.true;
el.appendChild(child2); el.appendChild(child2);
@ -495,18 +512,18 @@ export function runFormGroupMixinSuite(cfg = {}) {
describe('Interaction states', () => { describe('Interaction states', () => {
it('has false states (dirty, touched, prefilled) on init', async () => { it('has false states (dirty, touched, prefilled) on init', async () => {
const fieldset = /** @type {FormGroup} */ (await fixture( const fieldset = /** @type {FormGroup} */ (
html`<${tag}>${inputSlots}</${tag}>`, await fixture(html`<${tag}>${inputSlots}</${tag}>`)
)); );
expect(fieldset.dirty).to.equal(false, 'dirty'); expect(fieldset.dirty).to.equal(false, 'dirty');
expect(fieldset.touched).to.equal(false, 'touched'); expect(fieldset.touched).to.equal(false, 'touched');
expect(fieldset.prefilled).to.equal(false, 'prefilled'); expect(fieldset.prefilled).to.equal(false, 'prefilled');
}); });
it('sets dirty when value changed', async () => { it('sets dirty when value changed', async () => {
const fieldset = /** @type {FormGroup} */ (await fixture( const fieldset = /** @type {FormGroup} */ (
html`<${tag}>${inputSlots}</${tag}>`, await fixture(html`<${tag}>${inputSlots}</${tag}>`)
)); );
fieldset.formElements['hobbies[]'][0].modelValue = { checked: true, value: 'football' }; fieldset.formElements['hobbies[]'][0].modelValue = { checked: true, value: 'football' };
expect(fieldset.dirty).to.be.true; expect(fieldset.dirty).to.be.true;
}); });
@ -540,32 +557,38 @@ export function runFormGroupMixinSuite(cfg = {}) {
}); });
it('becomes prefilled if all form elements are prefilled', async () => { it('becomes prefilled if all form elements are prefilled', async () => {
const el = /** @type {FormGroup} */ (await fixture(html` const el = /** @type {FormGroup} */ (
await fixture(html`
<${tag}> <${tag}>
<${childTag} name="input1" .modelValue="${'prefilled'}"></${childTag}> <${childTag} name="input1" .modelValue="${'prefilled'}"></${childTag}>
<${childTag} name="input2"></${childTag}> <${childTag} name="input2"></${childTag}>
</${tag}> </${tag}>
`)); `)
);
expect(el.prefilled).to.be.false; expect(el.prefilled).to.be.false;
const el2 = /** @type {FormGroup} */ (await fixture(html` const el2 = /** @type {FormGroup} */ (
await fixture(html`
<${tag}> <${tag}>
<${childTag} name="input1" .modelValue="${'prefilled'}"></${childTag}> <${childTag} name="input1" .modelValue="${'prefilled'}"></${childTag}>
<${childTag} name="input2" .modelValue="${'prefilled'}"></${childTag}> <${childTag} name="input2" .modelValue="${'prefilled'}"></${childTag}>
</${tag}> </${tag}>
`)); `)
);
expect(el2.prefilled).to.be.true; expect(el2.prefilled).to.be.true;
}); });
it(`becomes "touched" once the last element of a group becomes blurred by keyboard it(`becomes "touched" once the last element of a group becomes blurred by keyboard
interaction (e.g. tabbing through the checkbox-group)`, async () => { interaction (e.g. tabbing through the checkbox-group)`, async () => {
const el = /** @type {FormGroup} */ (await fixture(html` const el = /** @type {FormGroup} */ (
await fixture(html`
<${tag}> <${tag}>
<label slot="label">My group</label> <label slot="label">My group</label>
<${childTag} name="myGroup[]" label="Option 1" value="1"></${childTag}> <${childTag} name="myGroup[]" label="Option 1" value="1"></${childTag}>
<${childTag} name="myGroup[]" label="Option 2" value="2"></${childTag}> <${childTag} name="myGroup[]" label="Option 2" value="2"></${childTag}>
</${tag}> </${tag}>
`)); `)
);
const button = /** @type {HTMLButtonElement} */ (await fixture(`<button>Blur</button>`)); const button = /** @type {HTMLButtonElement} */ (await fixture(`<button>Blur</button>`));
@ -582,22 +605,26 @@ export function runFormGroupMixinSuite(cfg = {}) {
it(`becomes "touched" once the group as a whole becomes blurred via mouse interaction after it(`becomes "touched" once the group as a whole becomes blurred via mouse interaction after
keyboard interaction (e.g. focus is moved inside the group and user clicks somewhere outside keyboard interaction (e.g. focus is moved inside the group and user clicks somewhere outside
the group)`, async () => { the group)`, async () => {
const el = /** @type {FormGroup} */ (await fixture(html` const el = /** @type {FormGroup} */ (
await fixture(html`
<${tag}> <${tag}>
<${childTag} name="input1"></${childTag}> <${childTag} name="input1"></${childTag}>
<${childTag} name="input2"></${childTag}> <${childTag} name="input2"></${childTag}>
</${tag}> </${tag}>
`)); `)
const el2 = /** @type {FormGroup} */ (await fixture(html` );
const el2 = /** @type {FormGroup} */ (
await fixture(html`
<${tag}> <${tag}>
<${childTag} name="input1"></${childTag}> <${childTag} name="input1"></${childTag}>
<${childTag} name="input2"></${childTag}> <${childTag} name="input2"></${childTag}>
</${tag}> </${tag}>
`)); `)
);
const outside = /** @type {HTMLButtonElement} */ (await fixture( const outside = /** @type {HTMLButtonElement} */ (
html`<button>outside</button>`, await fixture(html`<button>outside</button>`)
)); );
outside.click(); outside.click();
expect(el.touched, 'unfocused fieldset should stay untouched').to.be.false; expect(el.touched, 'unfocused fieldset should stay untouched').to.be.false;
@ -627,14 +654,16 @@ export function runFormGroupMixinSuite(cfg = {}) {
} }
} }
const outSideButton = /** @type {FormGroup} */ (await fixture( const outSideButton = /** @type {FormGroup} */ (
html`<button>outside</button>`, await fixture(html`<button>outside</button>`)
)); );
const el = /** @type {FormGroup} */ (await fixture(html` const el = /** @type {FormGroup} */ (
await fixture(html`
<${tag} .validators=${[new Input1IsTen()]}> <${tag} .validators=${[new Input1IsTen()]}>
<${childTag} name="input1" .validators=${[new IsNumber()]}></${childTag}> <${childTag} name="input1" .validators=${[new IsNumber()]}></${childTag}>
</${tag}> </${tag}>
`)); `)
);
const input1 = /** @type {FormChild} */ (el.querySelector('[name=input1]')); const input1 = /** @type {FormChild} */ (el.querySelector('[name=input1]'));
input1.modelValue = 2; input1.modelValue = 2;
input1.focus(); input1.focus();
@ -657,15 +686,17 @@ export function runFormGroupMixinSuite(cfg = {}) {
return hasError; return hasError;
} }
} }
const outSideButton = /** @type {FormGroup} */ (await fixture( const outSideButton = /** @type {FormGroup} */ (
html`<button>outside</button>`, await fixture(html`<button>outside</button>`)
)); );
const el = /** @type {FormGroup} */ (await fixture(html` const el = /** @type {FormGroup} */ (
await fixture(html`
<${tag} .validators=${[new Input1IsTen()]}> <${tag} .validators=${[new Input1IsTen()]}>
<${childTag} name="input1" .validators=${[new IsNumber()]}></${childTag}> <${childTag} name="input1" .validators=${[new IsNumber()]}></${childTag}>
<${childTag} name="input2" .validators=${[new IsNumber()]}></${childTag}> <${childTag} name="input2" .validators=${[new IsNumber()]}></${childTag}>
</${tag}> </${tag}>
`)); `)
);
const inputs = /** @type {FormChild[]} */ (Array.from(el.querySelectorAll(childTagString))); const inputs = /** @type {FormChild[]} */ (Array.from(el.querySelectorAll(childTagString)));
inputs[1].modelValue = 2; // make it dirty inputs[1].modelValue = 2; // make it dirty
inputs[1].focus(); inputs[1].focus();
@ -677,20 +708,24 @@ export function runFormGroupMixinSuite(cfg = {}) {
}); });
it('does not become dirty when elements are prefilled', async () => { it('does not become dirty when elements are prefilled', async () => {
const el = /** @type {FormGroup} */ (await fixture(html` const el = /** @type {FormGroup} */ (
await fixture(html`
<${tag} .serializedValue="${{ input1: 'x', input2: 'y' }}"> <${tag} .serializedValue="${{ input1: 'x', input2: 'y' }}">
<${childTag} name="input1" ></${childTag}> <${childTag} name="input1" ></${childTag}>
<${childTag} name="input2"></${childTag}> <${childTag} name="input2"></${childTag}>
</${tag}> </${tag}>
`)); `)
);
expect(el.dirty).to.be.false; expect(el.dirty).to.be.false;
const el2 = /** @type {FormGroup} */ (await fixture(html` const el2 = /** @type {FormGroup} */ (
await fixture(html`
<${tag} .modelValue="${{ input1: 'x', input2: 'y' }}"> <${tag} .modelValue="${{ input1: 'x', input2: 'y' }}">
<${childTag} name="input1" ></${childTag}> <${childTag} name="input1" ></${childTag}>
<${childTag} name="input2"></${childTag}> <${childTag} name="input2"></${childTag}>
</${tag}> </${tag}>
`)); `)
);
expect(el2.dirty).to.be.false; expect(el2.dirty).to.be.false;
}); });
}); });
@ -698,9 +733,9 @@ export function runFormGroupMixinSuite(cfg = {}) {
// TODO: this should be tested in FormGroupMixin // TODO: this should be tested in FormGroupMixin
describe('serializedValue', () => { describe('serializedValue', () => {
it('use form elements serializedValue', async () => { it('use form elements serializedValue', async () => {
const fieldset = /** @type {FormGroup} */ (await fixture( const fieldset = /** @type {FormGroup} */ (
html`<${tag}>${inputSlots}</${tag}>`, await fixture(html`<${tag}>${inputSlots}</${tag}>`)
)); );
fieldset.formElements['hobbies[]'][0].serializer = /** @param {?} v */ v => fieldset.formElements['hobbies[]'][0].serializer = /** @param {?} v */ v =>
`${v.value}-serialized`; `${v.value}-serialized`;
fieldset.formElements['hobbies[]'][0].modelValue = { checked: false, value: 'Bar' }; fieldset.formElements['hobbies[]'][0].modelValue = { checked: false, value: 'Bar' };
@ -720,9 +755,9 @@ export function runFormGroupMixinSuite(cfg = {}) {
}); });
it('treats names with ending [] as arrays', async () => { it('treats names with ending [] as arrays', async () => {
const fieldset = /** @type {FormGroup} */ (await fixture( const fieldset = /** @type {FormGroup} */ (
html`<${tag}>${inputSlots}</${tag}>`, await fixture(html`<${tag}>${inputSlots}</${tag}>`)
)); );
fieldset.formElements['hobbies[]'][0].modelValue = { checked: false, value: 'chess' }; fieldset.formElements['hobbies[]'][0].modelValue = { checked: false, value: 'chess' };
fieldset.formElements['hobbies[]'][1].modelValue = { checked: false, value: 'rugby' }; fieldset.formElements['hobbies[]'][1].modelValue = { checked: false, value: 'rugby' };
fieldset.formElements['gender[]'][0].modelValue = { checked: false, value: 'male' }; fieldset.formElements['gender[]'][0].modelValue = { checked: false, value: 'male' };
@ -742,21 +777,25 @@ export function runFormGroupMixinSuite(cfg = {}) {
}); });
it('0 is a valid value to be serialized', async () => { it('0 is a valid value to be serialized', async () => {
const fieldset = /** @type {FormGroup} */ (await fixture(html` const fieldset = /** @type {FormGroup} */ (
await fixture(html`
<${tag}> <${tag}>
<${childTag} name="price"></${childTag}> <${childTag} name="price"></${childTag}>
</${tag}>`)); </${tag}>`)
);
fieldset.formElements.price.modelValue = 0; fieldset.formElements.price.modelValue = 0;
expect(fieldset.serializedValue).to.deep.equal({ price: 0 }); expect(fieldset.serializedValue).to.deep.equal({ price: 0 });
}); });
it('allows for nested fieldsets', async () => { it('allows for nested fieldsets', async () => {
const fieldset = /** @type {FormGroup} */ (await fixture(html` const fieldset = /** @type {FormGroup} */ (
await fixture(html`
<${tag} name="userData"> <${tag} name="userData">
<${childTag} name="comment"></${childTag}> <${childTag} name="comment"></${childTag}>
<${tag} name="newfieldset">${inputSlots}</${tag}> <${tag} name="newfieldset">${inputSlots}</${tag}>
</${tag}> </${tag}>
`)); `)
);
const newFieldset = /** @type {FormGroup} */ (fieldset.querySelector(tagString)); const newFieldset = /** @type {FormGroup} */ (fieldset.querySelector(tagString));
newFieldset.formElements['hobbies[]'][0].modelValue = { checked: false, value: 'chess' }; newFieldset.formElements['hobbies[]'][0].modelValue = { checked: false, value: 'chess' };
newFieldset.formElements['hobbies[]'][1].modelValue = { checked: false, value: 'rugby' }; newFieldset.formElements['hobbies[]'][1].modelValue = { checked: false, value: 'rugby' };
@ -785,12 +824,14 @@ export function runFormGroupMixinSuite(cfg = {}) {
}); });
it('does not serialize disabled values', async () => { it('does not serialize disabled values', async () => {
const fieldset = /** @type {FormGroup} */ (await fixture(html` const fieldset = /** @type {FormGroup} */ (
await fixture(html`
<${tag}> <${tag}>
<${childTag} name="custom[]"></${childTag}> <${childTag} name="custom[]"></${childTag}>
<${childTag} name="custom[]"></${childTag}> <${childTag} name="custom[]"></${childTag}>
</${tag}> </${tag}>
`)); `)
);
fieldset.formElements['custom[]'][0].modelValue = 'custom 1'; fieldset.formElements['custom[]'][0].modelValue = 'custom 1';
fieldset.formElements['custom[]'][1].disabled = true; fieldset.formElements['custom[]'][1].disabled = true;
@ -800,12 +841,14 @@ export function runFormGroupMixinSuite(cfg = {}) {
}); });
it('will exclude form elements within a disabled fieldset', async () => { it('will exclude form elements within a disabled fieldset', async () => {
const fieldset = /** @type {FormGroup} */ (await fixture(html` const fieldset = /** @type {FormGroup} */ (
await fixture(html`
<${tag} name="userData"> <${tag} name="userData">
<${childTag} name="comment"></${childTag}> <${childTag} name="comment"></${childTag}>
<${tag} name="newfieldset">${inputSlots}</${tag}> <${tag} name="newfieldset">${inputSlots}</${tag}>
</${tag}> </${tag}>
`)); `)
);
const newFieldset = /** @type {FormGroup} */ (fieldset.querySelector(tagString)); const newFieldset = /** @type {FormGroup} */ (fieldset.querySelector(tagString));
fieldset.formElements.comment.modelValue = 'Foo'; fieldset.formElements.comment.modelValue = 'Foo';
@ -848,11 +891,13 @@ export function runFormGroupMixinSuite(cfg = {}) {
}); });
it('updates the formElements keys when a name attribute changes', async () => { it('updates the formElements keys when a name attribute changes', async () => {
const fieldset = /** @type {FormGroup} */ (await fixture(html` const fieldset = /** @type {FormGroup} */ (
await fixture(html`
<${tag}> <${tag}>
<${childTag} name="foo" .modelValue=${'qux'}></${childTag}> <${childTag} name="foo" .modelValue=${'qux'}></${childTag}>
</${tag}> </${tag}>
`)); `)
);
expect(fieldset.serializedValue.foo).to.equal('qux'); expect(fieldset.serializedValue.foo).to.equal('qux');
fieldset.formElements[0].name = 'bar'; fieldset.formElements[0].name = 'bar';
await fieldset.updateComplete; await fieldset.updateComplete;
@ -863,11 +908,13 @@ export function runFormGroupMixinSuite(cfg = {}) {
describe('Reset', () => { describe('Reset', () => {
it('restores default values if changes were made', async () => { it('restores default values if changes were made', async () => {
const el = /** @type {FormGroup} */ (await fixture(html` const el = /** @type {FormGroup} */ (
await fixture(html`
<${tag}> <${tag}>
<${childTag} id="firstName" name="firstName" .modelValue="${'Foo'}"></${childTag}> <${childTag} id="firstName" name="firstName" .modelValue="${'Foo'}"></${childTag}>
</${tag}> </${tag}>
`)); `)
);
await /** @type {FormChild} */ (el.querySelector(childTagString)).updateComplete; await /** @type {FormChild} */ (el.querySelector(childTagString)).updateComplete;
const input = /** @type {FormChild} */ (el.querySelector('#firstName')); const input = /** @type {FormChild} */ (el.querySelector('#firstName'));
@ -882,11 +929,13 @@ export function runFormGroupMixinSuite(cfg = {}) {
}); });
it('restores default values of arrays if changes were made', async () => { it('restores default values of arrays if changes were made', async () => {
const el = /** @type {FormGroup} */ (await fixture(html` const el = /** @type {FormGroup} */ (
await fixture(html`
<${tag}> <${tag}>
<${childTag} id="firstName" name="firstName[]" .modelValue="${'Foo'}"></${childTag}> <${childTag} id="firstName" name="firstName[]" .modelValue="${'Foo'}"></${childTag}>
</${tag}> </${tag}>
`)); `)
);
await /** @type {FormChild} */ (el.querySelector(childTagString)).updateComplete; await /** @type {FormChild} */ (el.querySelector(childTagString)).updateComplete;
const input = /** @type {FormChild} */ (el.querySelector('#firstName')); const input = /** @type {FormChild} */ (el.querySelector('#firstName'));
@ -901,13 +950,15 @@ export function runFormGroupMixinSuite(cfg = {}) {
}); });
it('restores default values of a nested fieldset if changes were made', async () => { it('restores default values of a nested fieldset if changes were made', async () => {
const el = /** @type {FormGroup} */ (await fixture(html` const el = /** @type {FormGroup} */ (
await fixture(html`
<${tag}> <${tag}>
<${tag} id="name" name="name[]"> <${tag} id="name" name="name[]">
<${childTag} id="firstName" name="firstName" .modelValue="${'Foo'}"></${childTag}> <${childTag} id="firstName" name="firstName" .modelValue="${'Foo'}"></${childTag}>
</${tag}> </${tag}>
</${tag}> </${tag}>
`)); `)
);
await Promise.all([ await Promise.all([
/** @type {FormChild} */ (el.querySelector(tagString)).updateComplete, /** @type {FormChild} */ (el.querySelector(tagString)).updateComplete,
/** @type {FormChild} */ (el.querySelector(childTagString)).updateComplete, /** @type {FormChild} */ (el.querySelector(childTagString)).updateComplete,
@ -928,9 +979,9 @@ export function runFormGroupMixinSuite(cfg = {}) {
}); });
it('clears interaction state', async () => { it('clears interaction state', async () => {
const el = /** @type {FormGroup} */ (await fixture( const el = /** @type {FormGroup} */ (
html`<${tag} touched dirty>${inputSlots}</${tag}>`, await fixture(html`<${tag} touched dirty>${inputSlots}</${tag}>`)
)); );
// Safety check initially // Safety check initially
// @ts-ignore [allow-protected] in test // @ts-ignore [allow-protected] in test
el._setValueForAllFormElements('prefilled', true); el._setValueForAllFormElements('prefilled', true);
@ -957,9 +1008,9 @@ export function runFormGroupMixinSuite(cfg = {}) {
}); });
it('clears submitted state', async () => { it('clears submitted state', async () => {
const fieldset = /** @type {FormGroup} */ (await fixture( const fieldset = /** @type {FormGroup} */ (
html`<${tag}>${inputSlots}</${tag}>`, await fixture(html`<${tag}>${inputSlots}</${tag}>`)
)); );
fieldset.submitted = true; fieldset.submitted = true;
fieldset.resetGroup(); fieldset.resetGroup();
expect(fieldset.submitted).to.equal(false); expect(fieldset.submitted).to.equal(false);
@ -999,12 +1050,14 @@ export function runFormGroupMixinSuite(cfg = {}) {
} }
} }
const el = /** @type {FormGroup} */ (await fixture(html` const el = /** @type {FormGroup} */ (
await fixture(html`
<${tag} .validators=${[new ColorContainsA()]}> <${tag} .validators=${[new ColorContainsA()]}>
<${childTag} name="color" .validators=${[new IsCat()]}></${childTag}> <${childTag} name="color" .validators=${[new IsCat()]}></${childTag}>
<${childTag} name="color2"></${childTag}> <${childTag} name="color2"></${childTag}>
</${tag}> </${tag}>
`)); `)
);
expect(el.hasFeedbackFor).to.deep.equal(['error']); expect(el.hasFeedbackFor).to.deep.equal(['error']);
expect(el.validationStates.error.ColorContainsA).to.be.true; expect(el.validationStates.error.ColorContainsA).to.be.true;
expect(el.formElements.color.hasFeedbackFor).to.deep.equal([]); expect(el.formElements.color.hasFeedbackFor).to.deep.equal([]);
@ -1024,14 +1077,16 @@ export function runFormGroupMixinSuite(cfg = {}) {
}); });
it('has access to `_initialModelValue` based on initial children states', async () => { it('has access to `_initialModelValue` based on initial children states', async () => {
const el = /** @type {FormGroup} */ (await fixture(html` const el = /** @type {FormGroup} */ (
await fixture(html`
<${tag}> <${tag}>
<${childTag} name="child[]" .modelValue="${'foo1'}"> <${childTag} name="child[]" .modelValue="${'foo1'}">
</${childTag}> </${childTag}>
<${childTag} name="child[]" .modelValue="${'bar1'}"> <${childTag} name="child[]" .modelValue="${'bar1'}">
</${childTag}> </${childTag}>
</${tag}> </${tag}>
`)); `)
);
await el.updateComplete; await el.updateComplete;
el.modelValue['child[]'] = ['foo2', 'bar2']; el.modelValue['child[]'] = ['foo2', 'bar2'];
// @ts-ignore [allow-protected] in test // @ts-ignore [allow-protected] in test
@ -1039,17 +1094,21 @@ export function runFormGroupMixinSuite(cfg = {}) {
}); });
it('does not wrongly recompute `_initialModelValue` after dynamic changes of children', async () => { it('does not wrongly recompute `_initialModelValue` after dynamic changes of children', async () => {
const el = /** @type {FormGroup} */ (await fixture(html` const el = /** @type {FormGroup} */ (
await fixture(html`
<${tag}> <${tag}>
<${childTag} name="child[]" .modelValue="${'foo1'}"> <${childTag} name="child[]" .modelValue="${'foo1'}">
</${childTag}> </${childTag}>
</${tag}> </${tag}>
`)); `)
);
el.modelValue['child[]'] = ['foo2']; el.modelValue['child[]'] = ['foo2'];
const childEl = /** @type {FormGroup} */ (await fixture(html` const childEl = /** @type {FormGroup} */ (
await fixture(html`
<${childTag} name="child[]" .modelValue="${'bar1'}"> <${childTag} name="child[]" .modelValue="${'bar1'}">
</${childTag}> </${childTag}>
`)); `)
);
el.appendChild(childEl); el.appendChild(childEl);
// @ts-ignore [allow-protected] in test // @ts-ignore [allow-protected] in test
expect(el._initialModelValue['child[]']).to.eql(['foo1', 'bar1']); expect(el._initialModelValue['child[]']).to.eql(['foo1', 'bar1']);
@ -1057,14 +1116,16 @@ export function runFormGroupMixinSuite(cfg = {}) {
describe('resetGroup method', () => { describe('resetGroup method', () => {
it('calls resetGroup on children fieldsets', async () => { it('calls resetGroup on children fieldsets', async () => {
const el = /** @type {FormGroup} */ (await fixture(html` const el = /** @type {FormGroup} */ (
await fixture(html`
<${tag} name="parentFieldset"> <${tag} name="parentFieldset">
<${tag} name="childFieldset"> <${tag} name="childFieldset">
<${childTag} name="child[]" .modelValue="${'foo1'}"> <${childTag} name="child[]" .modelValue="${'foo1'}">
</${childTag}> </${childTag}>
</${tag}> </${tag}>
</${tag}> </${tag}>
`)); `)
);
const childFieldsetEl = el.querySelector(tagString); const childFieldsetEl = el.querySelector(tagString);
// @ts-expect-error // @ts-expect-error
const resetGroupSpy = sinon.spy(childFieldsetEl, 'resetGroup'); const resetGroupSpy = sinon.spy(childFieldsetEl, 'resetGroup');
@ -1073,14 +1134,16 @@ export function runFormGroupMixinSuite(cfg = {}) {
}); });
it('calls reset on children fields', async () => { it('calls reset on children fields', async () => {
const el = /** @type {FormGroup} */ (await fixture(html` const el = /** @type {FormGroup} */ (
await fixture(html`
<${tag} name="parentFieldset"> <${tag} name="parentFieldset">
<${tag} name="childFieldset"> <${tag} name="childFieldset">
<${childTag} name="child[]" .modelValue="${'foo1'}"> <${childTag} name="child[]" .modelValue="${'foo1'}">
</${childTag}> </${childTag}>
</${tag}> </${tag}>
</${tag}> </${tag}>
`)); `)
);
const childFieldsetEl = /** @type {FormChild} */ (el.querySelector(childTagString)); const childFieldsetEl = /** @type {FormChild} */ (el.querySelector(childTagString));
const resetSpy = sinon.spy(childFieldsetEl, 'reset'); const resetSpy = sinon.spy(childFieldsetEl, 'reset');
el.resetGroup(); el.resetGroup();
@ -1090,14 +1153,16 @@ export function runFormGroupMixinSuite(cfg = {}) {
describe('clearGroup method', () => { describe('clearGroup method', () => {
it('calls clearGroup on children fieldset', async () => { it('calls clearGroup on children fieldset', async () => {
const el = /** @type {FormGroup} */ (await fixture(html` const el = /** @type {FormGroup} */ (
await fixture(html`
<${tag} name="parentFieldset"> <${tag} name="parentFieldset">
<${tag} name="childFieldset"> <${tag} name="childFieldset">
<${childTag} name="child[]" .modelValue="${'foo1'}"> <${childTag} name="child[]" .modelValue="${'foo1'}">
</${childTag}> </${childTag}>
</${tag}> </${tag}>
</${tag}> </${tag}>
`)); `)
);
const childFieldsetEl = el.querySelector(tagString); const childFieldsetEl = el.querySelector(tagString);
// @ts-expect-error // @ts-expect-error
const clearGroupSpy = sinon.spy(childFieldsetEl, 'clearGroup'); const clearGroupSpy = sinon.spy(childFieldsetEl, 'clearGroup');
@ -1106,14 +1171,16 @@ export function runFormGroupMixinSuite(cfg = {}) {
}); });
it('calls clear on children fields', async () => { it('calls clear on children fields', async () => {
const el = /** @type {FormGroup} */ (await fixture(html` const el = /** @type {FormGroup} */ (
await fixture(html`
<${tag} name="parentFieldset"> <${tag} name="parentFieldset">
<${tag} name="childFieldset"> <${tag} name="childFieldset">
<${childTag} name="child[]" .modelValue="${'foo1'}"> <${childTag} name="child[]" .modelValue="${'foo1'}">
</${childTag}> </${childTag}>
</${tag}> </${tag}>
</${tag}> </${tag}>
`)); `)
);
const childFieldsetEl = /** @type {FormChild} */ (el.querySelector(childTagString)); const childFieldsetEl = /** @type {FormChild} */ (el.querySelector(childTagString));
const clearSpy = sinon.spy(childFieldsetEl, 'clear'); const clearSpy = sinon.spy(childFieldsetEl, 'clear');
el.clearGroup(); el.clearGroup();
@ -1121,14 +1188,16 @@ export function runFormGroupMixinSuite(cfg = {}) {
}); });
it('should clear the value of fields', async () => { it('should clear the value of fields', async () => {
const el = /** @type {FormGroup} */ (await fixture(html` const el = /** @type {FormGroup} */ (
await fixture(html`
<${tag} name="parentFieldset"> <${tag} name="parentFieldset">
<${tag} name="childFieldset"> <${tag} name="childFieldset">
<${childTag} name="child" .modelValue="${'foo1'}"> <${childTag} name="child" .modelValue="${'foo1'}">
</${childTag}> </${childTag}>
</${tag}> </${tag}>
</${tag}> </${tag}>
`)); `)
);
el.clearGroup(); el.clearGroup();
expect( expect(
/** @type {FormChild} */ (el.querySelector('[name="child"]')).modelValue, /** @type {FormChild} */ (el.querySelector('[name="child"]')).modelValue,
@ -1139,9 +1208,9 @@ export function runFormGroupMixinSuite(cfg = {}) {
describe('Accessibility', () => { describe('Accessibility', () => {
it('has role="group" set', async () => { it('has role="group" set', async () => {
const fieldset = /** @type {FormGroup} */ (await fixture( const fieldset = /** @type {FormGroup} */ (
html`<${tag}>${inputSlots}</${tag}>`, await fixture(html`<${tag}>${inputSlots}</${tag}>`)
)); );
fieldset.formElements['hobbies[]'][0].modelValue = { checked: false, value: 'chess' }; fieldset.formElements['hobbies[]'][0].modelValue = { checked: false, value: 'chess' };
fieldset.formElements['hobbies[]'][1].modelValue = { checked: false, value: 'rugby' }; fieldset.formElements['hobbies[]'][1].modelValue = { checked: false, value: 'rugby' };
fieldset.formElements['gender[]'][0].modelValue = { checked: false, value: 'male' }; fieldset.formElements['gender[]'][0].modelValue = { checked: false, value: 'male' };
@ -1152,15 +1221,17 @@ export function runFormGroupMixinSuite(cfg = {}) {
}); });
it('has an aria-labelledby from element with slot="label"', async () => { it('has an aria-labelledby from element with slot="label"', async () => {
const el = /** @type {FormGroup} */ (await fixture(html` const el = /** @type {FormGroup} */ (
await fixture(html`
<${tag}> <${tag}>
<label slot="label">My Label</label> <label slot="label">My Label</label>
${inputSlots} ${inputSlots}
</${tag}> </${tag}>
`)); `)
const label = /** @type {HTMLElement} */ (Array.from(el.children).find( );
child => child.slot === 'label', const label = /** @type {HTMLElement} */ (
)); Array.from(el.children).find(child => child.slot === 'label')
);
expect(el.hasAttribute('aria-labelledby')).to.equal(true); expect(el.hasAttribute('aria-labelledby')).to.equal(true);
expect(el.getAttribute('aria-labelledby')).contains(label.id); expect(el.getAttribute('aria-labelledby')).contains(label.id);
}); });
@ -1204,13 +1275,15 @@ export function runFormGroupMixinSuite(cfg = {}) {
it(`when rendering children right from the start, sets their values correctly it(`when rendering children right from the start, sets their values correctly
based on prefilled model/seriazedValue`, async () => { based on prefilled model/seriazedValue`, async () => {
const el = /** @type {DynamicCWrapper} */ (await fixture(html` const el = /** @type {DynamicCWrapper} */ (
await fixture(html`
<${dynamicChildrenTag} <${dynamicChildrenTag}
.fields="${['firstName', 'lastName']}" .fields="${['firstName', 'lastName']}"
.modelValue="${{ firstName: 'foo', lastName: 'bar' }}" .modelValue="${{ firstName: 'foo', lastName: 'bar' }}"
> >
</${dynamicChildrenTag}> </${dynamicChildrenTag}>
`)); `)
);
await el.updateComplete; await el.updateComplete;
const fieldset = /** @type {FormGroup} */ ( const fieldset = /** @type {FormGroup} */ (
/** @type {ShadowRoot} */ (el.shadowRoot).querySelector(tagString) /** @type {ShadowRoot} */ (el.shadowRoot).querySelector(tagString)
@ -1218,13 +1291,15 @@ export function runFormGroupMixinSuite(cfg = {}) {
expect(fieldset.formElements[0].modelValue).to.equal('foo'); expect(fieldset.formElements[0].modelValue).to.equal('foo');
expect(fieldset.formElements[1].modelValue).to.equal('bar'); expect(fieldset.formElements[1].modelValue).to.equal('bar');
const el2 = /** @type {DynamicCWrapper} */ (await fixture(html` const el2 = /** @type {DynamicCWrapper} */ (
await fixture(html`
<${dynamicChildrenTag} <${dynamicChildrenTag}
.fields="${['firstName', 'lastName']}" .fields="${['firstName', 'lastName']}"
.serializedValue="${{ firstName: 'foo', lastName: 'bar' }}" .serializedValue="${{ firstName: 'foo', lastName: 'bar' }}"
> >
</${dynamicChildrenTag}> </${dynamicChildrenTag}>
`)); `)
);
await el2.updateComplete; await el2.updateComplete;
const fieldset2 = /** @type {FormGroup} */ ( const fieldset2 = /** @type {FormGroup} */ (
/** @type {ShadowRoot} */ (el2.shadowRoot).querySelector(tagString) /** @type {ShadowRoot} */ (el2.shadowRoot).querySelector(tagString)
@ -1235,10 +1310,12 @@ export function runFormGroupMixinSuite(cfg = {}) {
it(`when rendering children delayed, sets their values it(`when rendering children delayed, sets their values
correctly based on prefilled model/seriazedValue`, async () => { correctly based on prefilled model/seriazedValue`, async () => {
const el = /** @type {DynamicCWrapper} */ (await fixture(html` const el = /** @type {DynamicCWrapper} */ (
await fixture(html`
<${dynamicChildrenTag} .modelValue="${{ firstName: 'foo', lastName: 'bar' }}"> <${dynamicChildrenTag} .modelValue="${{ firstName: 'foo', lastName: 'bar' }}">
</${dynamicChildrenTag}> </${dynamicChildrenTag}>
`)); `)
);
await el.updateComplete; await el.updateComplete;
const fieldset = /** @type {FormGroup} */ ( const fieldset = /** @type {FormGroup} */ (
/** @type {ShadowRoot} */ (el.shadowRoot).querySelector(tagString) /** @type {ShadowRoot} */ (el.shadowRoot).querySelector(tagString)
@ -1248,10 +1325,12 @@ export function runFormGroupMixinSuite(cfg = {}) {
expect(fieldset.formElements[0].modelValue).to.equal('foo'); expect(fieldset.formElements[0].modelValue).to.equal('foo');
expect(fieldset.formElements[1].modelValue).to.equal('bar'); expect(fieldset.formElements[1].modelValue).to.equal('bar');
const el2 = /** @type {DynamicCWrapper} */ (await fixture(html` const el2 = /** @type {DynamicCWrapper} */ (
await fixture(html`
<${dynamicChildrenTag} .serializedValue="${{ firstName: 'foo', lastName: 'bar' }}"> <${dynamicChildrenTag} .serializedValue="${{ firstName: 'foo', lastName: 'bar' }}">
</${dynamicChildrenTag}> </${dynamicChildrenTag}>
`)); `)
);
await el2.updateComplete; await el2.updateComplete;
const fieldset2 = /** @type {FormGroup} */ ( const fieldset2 = /** @type {FormGroup} */ (
/** @type {ShadowRoot} */ (el2.shadowRoot).querySelector(tagString) /** @type {ShadowRoot} */ (el2.shadowRoot).querySelector(tagString)
@ -1264,13 +1343,15 @@ export function runFormGroupMixinSuite(cfg = {}) {
it(`when rendering children partly delayed, sets their values correctly based on it(`when rendering children partly delayed, sets their values correctly based on
prefilled model/seriazedValue`, async () => { prefilled model/seriazedValue`, async () => {
const el = /** @type {DynamicCWrapper} */ (await fixture(html` const el = /** @type {DynamicCWrapper} */ (
await fixture(html`
<${dynamicChildrenTag} .fields="${['firstName']}" .modelValue="${{ <${dynamicChildrenTag} .fields="${['firstName']}" .modelValue="${{
firstName: 'foo', firstName: 'foo',
lastName: 'bar', lastName: 'bar',
}}"> }}">
</${dynamicChildrenTag}> </${dynamicChildrenTag}>
`)); `)
);
await el.updateComplete; await el.updateComplete;
const fieldset = /** @type {FormGroup} */ ( const fieldset = /** @type {FormGroup} */ (
/** @type {ShadowRoot} */ (el.shadowRoot).querySelector(tagString) /** @type {ShadowRoot} */ (el.shadowRoot).querySelector(tagString)
@ -1280,13 +1361,15 @@ export function runFormGroupMixinSuite(cfg = {}) {
expect(fieldset.formElements[0].modelValue).to.equal('foo'); expect(fieldset.formElements[0].modelValue).to.equal('foo');
expect(fieldset.formElements[1].modelValue).to.equal('bar'); expect(fieldset.formElements[1].modelValue).to.equal('bar');
const el2 = /** @type {DynamicCWrapper} */ (await fixture(html` const el2 = /** @type {DynamicCWrapper} */ (
await fixture(html`
<${dynamicChildrenTag} .fields="${['firstName']}" .serializedValue="${{ <${dynamicChildrenTag} .fields="${['firstName']}" .serializedValue="${{
firstName: 'foo', firstName: 'foo',
lastName: 'bar', lastName: 'bar',
}}"> }}">
</${dynamicChildrenTag}> </${dynamicChildrenTag}>
`)); `)
);
await el2.updateComplete; await el2.updateComplete;
const fieldset2 = /** @type {FormGroup} */ ( const fieldset2 = /** @type {FormGroup} */ (
/** @type {ShadowRoot} */ (el2.shadowRoot).querySelector(tagString) /** @type {ShadowRoot} */ (el2.shadowRoot).querySelector(tagString)
@ -1305,13 +1388,15 @@ export function runFormGroupMixinSuite(cfg = {}) {
expect(elm.prefilled).to.be.true; expect(elm.prefilled).to.be.true;
} }
const el = /** @type {DynamicCWrapper} */ (await fixture(html` const el = /** @type {DynamicCWrapper} */ (
await fixture(html`
<${dynamicChildrenTag} .fields="${['firstName']}" .modelValue="${{ <${dynamicChildrenTag} .fields="${['firstName']}" .modelValue="${{
firstName: 'foo', firstName: 'foo',
lastName: 'bar', lastName: 'bar',
}}"> }}">
</${dynamicChildrenTag}> </${dynamicChildrenTag}>
`)); `)
);
await el.updateComplete; await el.updateComplete;
const fieldset = /** @type {FormGroup} */ ( const fieldset = /** @type {FormGroup} */ (
/** @type {ShadowRoot} */ (el.shadowRoot).querySelector(tagString) /** @type {ShadowRoot} */ (el.shadowRoot).querySelector(tagString)
@ -1324,13 +1409,15 @@ export function runFormGroupMixinSuite(cfg = {}) {
expectInteractionStatesToBeCorrectFor(fieldset.formElements[1]); expectInteractionStatesToBeCorrectFor(fieldset.formElements[1]);
expectInteractionStatesToBeCorrectFor(fieldset); expectInteractionStatesToBeCorrectFor(fieldset);
const el2 = /** @type {DynamicCWrapper} */ (await fixture(html` const el2 = /** @type {DynamicCWrapper} */ (
await fixture(html`
<${dynamicChildrenTag} .fields="${['firstName']}" .serializedValue="${{ <${dynamicChildrenTag} .fields="${['firstName']}" .serializedValue="${{
firstName: 'foo', firstName: 'foo',
lastName: 'bar', lastName: 'bar',
}}"> }}">
</${dynamicChildrenTag}> </${dynamicChildrenTag}>
`)); `)
);
await el2.updateComplete; await el2.updateComplete;
const fieldset2 = /** @type {FormGroup} */ ( const fieldset2 = /** @type {FormGroup} */ (
/** @type {ShadowRoot} */ (el2.shadowRoot).querySelector(tagString) /** @type {ShadowRoot} */ (el2.shadowRoot).querySelector(tagString)
@ -1345,13 +1432,15 @@ export function runFormGroupMixinSuite(cfg = {}) {
}); });
it(`prefilled children values take precedence over parent values`, async () => { it(`prefilled children values take precedence over parent values`, async () => {
const el = /** @type {DynamicCWrapper} */ (await fixture(html` const el = /** @type {DynamicCWrapper} */ (
await fixture(html`
<${dynamicChildrenTag} .modelValue="${{ <${dynamicChildrenTag} .modelValue="${{
firstName: 'foo', firstName: 'foo',
lastName: 'bar', lastName: 'bar',
}}"> }}">
</${dynamicChildrenTag}> </${dynamicChildrenTag}>
`)); `)
);
await el.updateComplete; await el.updateComplete;
const fieldset = /** @type {FormGroup} */ ( const fieldset = /** @type {FormGroup} */ (
/** @type {ShadowRoot} */ (el.shadowRoot).querySelector(tagString) /** @type {ShadowRoot} */ (el.shadowRoot).querySelector(tagString)
@ -1364,13 +1453,15 @@ export function runFormGroupMixinSuite(cfg = {}) {
expect(fieldset.formElements[0].modelValue).to.equal('wins'); expect(fieldset.formElements[0].modelValue).to.equal('wins');
expect(fieldset.formElements[1].modelValue).to.equal('winsAsWell'); expect(fieldset.formElements[1].modelValue).to.equal('winsAsWell');
const el2 = /** @type {DynamicCWrapper} */ (await fixture(html` const el2 = /** @type {DynamicCWrapper} */ (
await fixture(html`
<${dynamicChildrenTag} .serializedValue="${{ <${dynamicChildrenTag} .serializedValue="${{
firstName: 'foo', firstName: 'foo',
lastName: 'bar', lastName: 'bar',
}}"> }}">
</${dynamicChildrenTag}> </${dynamicChildrenTag}>
`)); `)
);
await el2.updateComplete; await el2.updateComplete;
const fieldset2 = /** @type {FormGroup} */ ( const fieldset2 = /** @type {FormGroup} */ (
/** @type {ShadowRoot} */ (el2.shadowRoot).querySelector(tagString) /** @type {ShadowRoot} */ (el2.shadowRoot).querySelector(tagString)

View file

@ -1,9 +1,11 @@
import { LitElement } from '@lion/core'; import { LitElement } from '@lion/core';
import { defineCE, expect, fixture, html, oneEvent, unsafeStatic } from '@open-wc/testing'; import { defineCE, expect, fixture, oneEvent } from '@open-wc/testing';
import { html, unsafeStatic } from 'lit/static-html.js';
import sinon from 'sinon'; import sinon from 'sinon';
import { FocusMixin } from '../src/FocusMixin.js'; import { FocusMixin } from '../src/FocusMixin.js';
const windowWithOptionalPolyfill = /** @type {Window & typeof globalThis & {applyFocusVisiblePolyfill?: function}} */ (window); const windowWithOptionalPolyfill =
/** @type {Window & typeof globalThis & {applyFocusVisiblePolyfill?: function}} */ (window);
/** /**
* Checks two things: * Checks two things:
@ -74,9 +76,11 @@ describe('FocusMixin', () => {
const tag = unsafeStatic(tagString); const tag = unsafeStatic(tagString);
it('focuses/blurs the underlaying native element on .focus()/.blur()', async () => { it('focuses/blurs the underlaying native element on .focus()/.blur()', async () => {
const el = /** @type {Focusable} */ (await fixture(html` const el = /** @type {Focusable} */ (
await fixture(html`
<${tag}><input slot="input"></${tag}> <${tag}><input slot="input"></${tag}>
`)); `)
);
// @ts-ignore [allow-protected] in test // @ts-ignore [allow-protected] in test
const { _focusableNode } = el; const { _focusableNode } = el;
@ -87,9 +91,11 @@ describe('FocusMixin', () => {
}); });
it('has an attribute focused when focused', async () => { it('has an attribute focused when focused', async () => {
const el = /** @type {Focusable} */ (await fixture(html` const el = /** @type {Focusable} */ (
await fixture(html`
<${tag}><input slot="input"></${tag}> <${tag}><input slot="input"></${tag}>
`)); `)
);
el.focus(); el.focus();
await el.updateComplete; await el.updateComplete;
@ -101,9 +107,11 @@ describe('FocusMixin', () => {
}); });
it('becomes focused/blurred if the native element gets focused/blurred', async () => { it('becomes focused/blurred if the native element gets focused/blurred', async () => {
const el = /** @type {Focusable} */ (await fixture(html` const el = /** @type {Focusable} */ (
await fixture(html`
<${tag}><input slot="input"></${tag}> <${tag}><input slot="input"></${tag}>
`)); `)
);
// @ts-ignore [allow-protected] in test // @ts-ignore [allow-protected] in test
const { _focusableNode } = el; const { _focusableNode } = el;
@ -115,9 +123,11 @@ describe('FocusMixin', () => {
}); });
it('dispatches [focus, blur] events', async () => { it('dispatches [focus, blur] events', async () => {
const el = /** @type {Focusable} */ (await fixture(html` const el = /** @type {Focusable} */ (
await fixture(html`
<${tag}><input slot="input"></${tag}> <${tag}><input slot="input"></${tag}>
`)); `)
);
setTimeout(() => el.focus()); setTimeout(() => el.focus());
const focusEv = await oneEvent(el, 'focus'); const focusEv = await oneEvent(el, 'focus');
expect(focusEv).to.be.instanceOf(Event); expect(focusEv).to.be.instanceOf(Event);
@ -137,9 +147,11 @@ describe('FocusMixin', () => {
}); });
it('dispatches [focusin, focusout] events with { bubbles: true, composed: true }', async () => { it('dispatches [focusin, focusout] events with { bubbles: true, composed: true }', async () => {
const el = /** @type {Focusable} */ (await fixture(html` const el = /** @type {Focusable} */ (
await fixture(html`
<${tag}><input slot="input"></${tag}> <${tag}><input slot="input"></${tag}>
`)); `)
);
setTimeout(() => el.focus()); setTimeout(() => el.focus());
const focusinEv = await oneEvent(el, 'focusin'); const focusinEv = await oneEvent(el, 'focusin');
expect(focusinEv).to.be.instanceOf(Event); expect(focusinEv).to.be.instanceOf(Event);
@ -160,9 +172,11 @@ describe('FocusMixin', () => {
describe('Having :focus-visible within', () => { describe('Having :focus-visible within', () => {
it('sets focusedVisible to true when focusable element matches :focus-visible', async () => { it('sets focusedVisible to true when focusable element matches :focus-visible', async () => {
const el = /** @type {Focusable} */ (await fixture(html` const el = /** @type {Focusable} */ (
await fixture(html`
<${tag}><input slot="input"></${tag}> <${tag}><input slot="input"></${tag}>
`)); `)
);
// @ts-ignore [allow-protected] in test // @ts-ignore [allow-protected] in test
const { _focusableNode } = el; const { _focusableNode } = el;
@ -204,9 +218,11 @@ describe('FocusMixin', () => {
}); });
it('has an attribute focused-visible when focusedVisible is true', async () => { it('has an attribute focused-visible when focusedVisible is true', async () => {
const el = /** @type {Focusable} */ (await fixture(html` const el = /** @type {Focusable} */ (
await fixture(html`
<${tag}><input slot="input"></${tag}> <${tag}><input slot="input"></${tag}>
`)); `)
);
// @ts-ignore [allow-protected] in test // @ts-ignore [allow-protected] in test
const { _focusableNode } = el; const { _focusableNode } = el;
@ -251,9 +267,11 @@ describe('FocusMixin', () => {
}); });
it('sets focusedVisible to true when focusable element if :focus-visible polyfill is loaded', async () => { it('sets focusedVisible to true when focusable element if :focus-visible polyfill is loaded', async () => {
const el = /** @type {Focusable} */ (await fixture(html` const el = /** @type {Focusable} */ (
await fixture(html`
<${tag}><input slot="input"></${tag}> <${tag}><input slot="input"></${tag}>
`)); `)
);
// @ts-ignore [allow-protected] in test // @ts-ignore [allow-protected] in test
const { _focusableNode } = el; const { _focusableNode } = el;

View file

@ -1,4 +1,5 @@
import { expect, html, defineCE, unsafeStatic, fixture } from '@open-wc/testing'; import { expect, defineCE, fixture } from '@open-wc/testing';
import { html, unsafeStatic } from 'lit/static-html.js';
import { LitElement } from '@lion/core'; import { LitElement } from '@lion/core';
import { getFormControlMembers } from '@lion/form-core/test-helpers'; import { getFormControlMembers } from '@lion/form-core/test-helpers';
import sinon from 'sinon'; import sinon from 'sinon';
@ -30,108 +31,130 @@ describe('FormControlMixin', () => {
describe('Label and helpText api', () => { describe('Label and helpText api', () => {
it('has a label', async () => { it('has a label', async () => {
const elAttr = /** @type {FormControlMixinClass} */ (await fixture(html` const elAttr = /** @type {FormControlMixinClass} */ (
await fixture(html`
<${tag} label="Email address">${inputSlot}</${tag}> <${tag} label="Email address">${inputSlot}</${tag}>
`)); `)
);
expect(elAttr.label).to.equal('Email address', 'as an attribute'); expect(elAttr.label).to.equal('Email address', 'as an attribute');
const elProp = /** @type {FormControlMixinClass} */ (await fixture(html` const elProp = /** @type {FormControlMixinClass} */ (
await fixture(html`
<${tag} <${tag}
.label=${'Email address'} .label=${'Email address'}
>${inputSlot} >${inputSlot}
</${tag}>`)); </${tag}>`)
);
expect(elProp.label).to.equal('Email address', 'as a property'); expect(elProp.label).to.equal('Email address', 'as a property');
const elElem = /** @type {FormControlMixinClass} */ (await fixture(html` const elElem = /** @type {FormControlMixinClass} */ (
await fixture(html`
<${tag}> <${tag}>
<label slot="label">Email address</label> <label slot="label">Email address</label>
${inputSlot} ${inputSlot}
</${tag}>`)); </${tag}>`)
);
expect(elElem.label).to.equal('Email address', 'as an element'); expect(elElem.label).to.equal('Email address', 'as an element');
}); });
it('has a label that supports inner html', async () => { it('has a label that supports inner html', async () => {
const el = /** @type {FormControlMixinClass} */ (await fixture(html` const el = /** @type {FormControlMixinClass} */ (
await fixture(html`
<${tag}> <${tag}>
<label slot="label">Email <span>address</span></label> <label slot="label">Email <span>address</span></label>
${inputSlot} ${inputSlot}
</${tag}>`)); </${tag}>`)
);
expect(el.label).to.equal('Email address'); expect(el.label).to.equal('Email address');
}); });
it('only takes label of direct child', async () => { it('only takes label of direct child', async () => {
const el = /** @type {FormControlMixinClass} */ (await fixture(html` const el = /** @type {FormControlMixinClass} */ (
await fixture(html`
<${tag}> <${tag}>
<${tag} label="Email address"> <${tag} label="Email address">
${inputSlot} ${inputSlot}
</${tag}> </${tag}>
</${tag}>`)); </${tag}>`)
);
expect(el.label).to.equal(''); expect(el.label).to.equal('');
}); });
it('can have a help-text', async () => { it('can have a help-text', async () => {
const elAttr = /** @type {FormControlMixinClass} */ (await fixture(html` const elAttr = /** @type {FormControlMixinClass} */ (
await fixture(html`
<${tag} help-text="We will not send you any spam">${inputSlot}</${tag}> <${tag} help-text="We will not send you any spam">${inputSlot}</${tag}>
`)); `)
);
expect(elAttr.helpText).to.equal('We will not send you any spam', 'as an attribute'); expect(elAttr.helpText).to.equal('We will not send you any spam', 'as an attribute');
const elProp = /** @type {FormControlMixinClass} */ (await fixture(html` const elProp = /** @type {FormControlMixinClass} */ (
await fixture(html`
<${tag} <${tag}
.helpText=${'We will not send you any spam'} .helpText=${'We will not send you any spam'}
>${inputSlot} >${inputSlot}
</${tag}>`)); </${tag}>`)
);
expect(elProp.helpText).to.equal('We will not send you any spam', 'as a property'); expect(elProp.helpText).to.equal('We will not send you any spam', 'as a property');
const elElem = /** @type {FormControlMixinClass} */ (await fixture(html` const elElem = /** @type {FormControlMixinClass} */ (
await fixture(html`
<${tag}> <${tag}>
<div slot="help-text">We will not send you any spam</div> <div slot="help-text">We will not send you any spam</div>
${inputSlot} ${inputSlot}
</${tag}>`)); </${tag}>`)
);
expect(elElem.helpText).to.equal('We will not send you any spam', 'as an element'); expect(elElem.helpText).to.equal('We will not send you any spam', 'as an element');
}); });
it('can have a help-text that supports inner html', async () => { it('can have a help-text that supports inner html', async () => {
const el = /** @type {FormControlMixinClass} */ (await fixture(html` const el = /** @type {FormControlMixinClass} */ (
await fixture(html`
<${tag}> <${tag}>
<div slot="help-text">We will not send you any <span>spam</span></div> <div slot="help-text">We will not send you any <span>spam</span></div>
${inputSlot} ${inputSlot}
</${tag}>`)); </${tag}>`)
);
expect(el.helpText).to.equal('We will not send you any spam'); expect(el.helpText).to.equal('We will not send you any spam');
}); });
it('only takes help-text of direct child', async () => { it('only takes help-text of direct child', async () => {
const el = /** @type {FormControlMixinClass} */ (await fixture(html` const el = /** @type {FormControlMixinClass} */ (
await fixture(html`
<${tag}> <${tag}>
<${tag} help-text="We will not send you any spam"> <${tag} help-text="We will not send you any spam">
${inputSlot} ${inputSlot}
</${tag}> </${tag}>
</${tag}>`)); </${tag}>`)
);
expect(el.helpText).to.equal(''); expect(el.helpText).to.equal('');
}); });
}); });
describe('Accessibility', () => { describe('Accessibility', () => {
it('does not duplicate aria-describedby and aria-labelledby ids on reconnect', async () => { it('does not duplicate aria-describedby and aria-labelledby ids on reconnect', async () => {
const wrapper = /** @type {HTMLElement} */ (await fixture(html` const wrapper = /** @type {HTMLElement} */ (
await fixture(html`
<div id="wrapper"> <div id="wrapper">
<${tag} help-text="This element will be disconnected/reconnected">${inputSlot}</${tag}> <${tag} help-text="This element will be disconnected/reconnected">${inputSlot}</${tag}>
</div> </div>
`)); `)
);
const el = /** @type {FormControlMixinClass} */ (wrapper.querySelector(tagString)); const el = /** @type {FormControlMixinClass} */ (wrapper.querySelector(tagString));
const { _inputNode } = getFormControlMembers(el); const { _inputNode } = getFormControlMembers(el);
const labelIdsBefore = /** @type {string} */ (_inputNode.getAttribute('aria-labelledby')); const labelIdsBefore = /** @type {string} */ (_inputNode.getAttribute('aria-labelledby'));
const descriptionIdsBefore = /** @type {string} */ (_inputNode.getAttribute( const descriptionIdsBefore = /** @type {string} */ (
'aria-describedby', _inputNode.getAttribute('aria-describedby')
)); );
// Reconnect // Reconnect
wrapper.removeChild(el); wrapper.removeChild(el);
wrapper.appendChild(el); wrapper.appendChild(el);
const labelIdsAfter = /** @type {string} */ (_inputNode.getAttribute('aria-labelledby')); const labelIdsAfter = /** @type {string} */ (_inputNode.getAttribute('aria-labelledby'));
const descriptionIdsAfter = /** @type {string} */ (_inputNode.getAttribute( const descriptionIdsAfter = /** @type {string} */ (
'aria-describedby', _inputNode.getAttribute('aria-describedby')
)); );
expect(labelIdsBefore).to.equal(labelIdsAfter); expect(labelIdsBefore).to.equal(labelIdsAfter);
expect(descriptionIdsBefore).to.equal(descriptionIdsAfter); expect(descriptionIdsBefore).to.equal(descriptionIdsAfter);
@ -139,11 +162,13 @@ describe('FormControlMixin', () => {
it('clicking the label should call `_onLabelClick`', async () => { it('clicking the label should call `_onLabelClick`', async () => {
const spy = sinon.spy(); const spy = sinon.spy();
const el = /** @type {FormControlMixinClass} */ (await fixture(html` const el = /** @type {FormControlMixinClass} */ (
await fixture(html`
<${tag} ._onLabelClick="${spy}"> <${tag} ._onLabelClick="${spy}">
${inputSlot} ${inputSlot}
</${tag}> </${tag}>
`)); `)
);
const { _labelNode } = getFormControlMembers(el); const { _labelNode } = getFormControlMembers(el);
expect(spy).to.not.have.been.called; expect(spy).to.not.have.been.called;
@ -232,7 +257,8 @@ describe('FormControlMixin', () => {
describe('Adding extra labels and descriptions', () => { describe('Adding extra labels and descriptions', () => {
it(`supports centrally orchestrated labels/descriptions via addToAriaLabelledBy() / it(`supports centrally orchestrated labels/descriptions via addToAriaLabelledBy() /
removeFromAriaLabelledBy() / addToAriaDescribedBy() / removeFromAriaDescribedBy()`, async () => { removeFromAriaLabelledBy() / addToAriaDescribedBy() / removeFromAriaDescribedBy()`, async () => {
const wrapper = /** @type {HTMLElement} */ (await fixture(html` const wrapper = /** @type {HTMLElement} */ (
await fixture(html`
<div id="wrapper"> <div id="wrapper">
<${tag}> <${tag}>
${inputSlot} ${inputSlot}
@ -241,7 +267,8 @@ describe('FormControlMixin', () => {
</${tag}> </${tag}>
<div id="additionalLabel"> This also needs to be read whenever the input has focus</div> <div id="additionalLabel"> This also needs to be read whenever the input has focus</div>
<div id="additionalDescription"> Same for this </div> <div id="additionalDescription"> Same for this </div>
</div>`)); </div>`)
);
const el = /** @type {FormControlMixinClass} */ (wrapper.querySelector(tagString)); const el = /** @type {FormControlMixinClass} */ (wrapper.querySelector(tagString));
const { _inputNode } = getFormControlMembers(el); const { _inputNode } = getFormControlMembers(el);
@ -257,9 +284,9 @@ describe('FormControlMixin', () => {
expect(/** @type {string} */ (_inputNode.getAttribute('aria-labelledby'))).to.contain( expect(/** @type {string} */ (_inputNode.getAttribute('aria-labelledby'))).to.contain(
`label-${inputId}`, `label-${inputId}`,
); );
const additionalLabel = /** @type {HTMLElement} */ (wrapper.querySelector( const additionalLabel = /** @type {HTMLElement} */ (
'#additionalLabel', wrapper.querySelector('#additionalLabel')
)); );
el.addToAriaLabelledBy(additionalLabel); el.addToAriaLabelledBy(additionalLabel);
await el.updateComplete; await el.updateComplete;
let labelledbyAttr = /** @type {string} */ (_inputNode.getAttribute('aria-labelledby')); let labelledbyAttr = /** @type {string} */ (_inputNode.getAttribute('aria-labelledby'));
@ -392,13 +419,15 @@ describe('FormControlMixin', () => {
it('redispatches one event from host', async () => { it('redispatches one event from host', async () => {
const formSpy = sinon.spy(); const formSpy = sinon.spy();
const fieldsetSpy = sinon.spy(); const fieldsetSpy = sinon.spy();
const formEl = /** @type {FormControlMixinClass} */ (await fixture(html` const formEl = /** @type {FormControlMixinClass} */ (
await fixture(html`
<${groupTag} name="form" ._repropagationRole=${'form-group'} @model-value-changed=${formSpy}> <${groupTag} name="form" ._repropagationRole=${'form-group'} @model-value-changed=${formSpy}>
<${groupTag} name="fieldset" ._repropagationRole=${'form-group'} @model-value-changed=${fieldsetSpy}> <${groupTag} name="fieldset" ._repropagationRole=${'form-group'} @model-value-changed=${fieldsetSpy}>
<${tag} name="field"></${tag}> <${tag} name="field"></${tag}>
</${groupTag}> </${groupTag}>
</${groupTag}> </${groupTag}>
`)); `)
);
const fieldsetEl = formEl.querySelector('[name=fieldset]'); const fieldsetEl = formEl.querySelector('[name=fieldset]');
expect(fieldsetSpy.callCount).to.equal(1); expect(fieldsetSpy.callCount).to.equal(1);

View file

@ -1,5 +1,5 @@
import { LitElement } from '@lion/core'; import { LitElement } from '@lion/core';
import { html } from '@open-wc/testing'; import { html } from 'lit/static-html.js';
import { runRegistrationSuite } from '../test-suites/FormRegistrationMixins.suite.js'; import { runRegistrationSuite } from '../test-suites/FormRegistrationMixins.suite.js';
runRegistrationSuite({ runRegistrationSuite({

View file

@ -2,14 +2,8 @@ import { unsafeHTML } from '@lion/core';
import { localize } from '@lion/localize'; import { localize } from '@lion/localize';
import { localizeTearDown } from '@lion/localize/test-helpers'; import { localizeTearDown } from '@lion/localize/test-helpers';
import { Required, Validator } from '@lion/form-core'; import { Required, Validator } from '@lion/form-core';
import { import { expect, fixture, triggerBlurFor, triggerFocusFor } from '@open-wc/testing';
expect, import { html, unsafeStatic } from 'lit/static-html.js';
fixture,
html,
triggerBlurFor,
triggerFocusFor,
unsafeStatic,
} from '@open-wc/testing';
import { getFormControlMembers } from '@lion/form-core/test-helpers'; import { getFormControlMembers } from '@lion/form-core/test-helpers';
import sinon from 'sinon'; import sinon from 'sinon';
import '@lion/form-core/define-field'; import '@lion/form-core/define-field';
@ -60,31 +54,31 @@ describe('<lion-field>', () => {
}); });
it(`has a fieldName based on the label`, async () => { it(`has a fieldName based on the label`, async () => {
const el1 = /** @type {LionField} */ (await fixture( const el1 = /** @type {LionField} */ (
html`<${tag} label="foo">${inputSlot}</${tag}>`, await fixture(html`<${tag} label="foo">${inputSlot}</${tag}>`)
)); );
const { _labelNode: _labelNode1 } = getFormControlMembers(el1); const { _labelNode: _labelNode1 } = getFormControlMembers(el1);
expect(el1.fieldName).to.equal(_labelNode1.textContent); expect(el1.fieldName).to.equal(_labelNode1.textContent);
const el2 = /** @type {LionField} */ (await fixture( const el2 = /** @type {LionField} */ (
html`<${tag}><label slot="label">bar</label>${inputSlot}</${tag}>`, await fixture(html`<${tag}><label slot="label">bar</label>${inputSlot}</${tag}>`)
)); );
const { _labelNode: _labelNode2 } = getFormControlMembers(el2); const { _labelNode: _labelNode2 } = getFormControlMembers(el2);
expect(el2.fieldName).to.equal(_labelNode2.textContent); expect(el2.fieldName).to.equal(_labelNode2.textContent);
}); });
it(`has a fieldName based on the name if no label exists`, async () => { it(`has a fieldName based on the name if no label exists`, async () => {
const el = /** @type {LionField} */ (await fixture( const el = /** @type {LionField} */ (
html`<${tag} name="foo">${inputSlot}</${tag}>`, await fixture(html`<${tag} name="foo">${inputSlot}</${tag}>`)
)); );
expect(el.fieldName).to.equal(el.name); expect(el.fieldName).to.equal(el.name);
}); });
it(`can override fieldName`, async () => { it(`can override fieldName`, async () => {
const el = /** @type {LionField} */ (await fixture( const el = /** @type {LionField} */ (
html`<${tag} label="foo" .fieldName="${'bar'}">${inputSlot}</${tag}>`, await fixture(html`<${tag} label="foo" .fieldName="${'bar'}">${inputSlot}</${tag}>`)
)); );
// @ts-ignore [allow-protected] in test // @ts-ignore [allow-protected] in test
expect(el.__fieldName).to.equal(el.fieldName); expect(el.__fieldName).to.equal(el.fieldName);
}); });
@ -134,9 +128,9 @@ describe('<lion-field>', () => {
}); });
it('can be cleared which erases value, validation and interaction states', async () => { it('can be cleared which erases value, validation and interaction states', async () => {
const el = /** @type {LionField} */ (await fixture( const el = /** @type {LionField} */ (
html`<${tag} value="Some value from attribute">${inputSlot}</${tag}>`, await fixture(html`<${tag} value="Some value from attribute">${inputSlot}</${tag}>`)
)); );
el.clear(); el.clear();
expect(el.modelValue).to.equal(''); expect(el.modelValue).to.equal('');
el.modelValue = 'Some value from property'; el.modelValue = 'Some value from property';
@ -146,10 +140,12 @@ describe('<lion-field>', () => {
}); });
it('can be reset which restores original modelValue', async () => { it('can be reset which restores original modelValue', async () => {
const el = /** @type {LionField} */ (await fixture(html` const el = /** @type {LionField} */ (
await fixture(html`
<${tag} .modelValue="${'foo'}"> <${tag} .modelValue="${'foo'}">
${inputSlot} ${inputSlot}
</${tag}>`)); </${tag}>`)
);
expect(el._initialModelValue).to.equal('foo'); expect(el._initialModelValue).to.equal('foo');
el.modelValue = 'bar'; el.modelValue = 'bar';
el.reset(); el.reset();
@ -171,13 +167,15 @@ describe('<lion-field>', () => {
<div slot="feedback" id="feedback-[id]">[feedback] </span> <div slot="feedback" id="feedback-[id]">[feedback] </span>
</lion-field> </lion-field>
~~~`, async () => { ~~~`, async () => {
const el = /** @type {LionField} */ (await fixture(html`<${tag}> const el = /** @type {LionField} */ (
await fixture(html`<${tag}>
<label slot="label">My Name</label> <label slot="label">My Name</label>
${inputSlot} ${inputSlot}
<span slot="help-text">Enter your Name</span> <span slot="help-text">Enter your Name</span>
<span slot="feedback">No name entered</span> <span slot="feedback">No name entered</span>
</${tag}> </${tag}>
`)); `)
);
const nativeInput = getSlot(el, 'input'); const nativeInput = getSlot(el, 'input');
// @ts-ignore allow protected accessors in tests // @ts-ignore allow protected accessors in tests
const inputId = el._inputId; const inputId = el._inputId;
@ -188,14 +186,16 @@ describe('<lion-field>', () => {
it(`allows additional slots (prefix, suffix, before, after) to be included in labelledby it(`allows additional slots (prefix, suffix, before, after) to be included in labelledby
(via attribute data-label) and in describedby (via attribute data-description)`, async () => { (via attribute data-label) and in describedby (via attribute data-description)`, async () => {
const el = /** @type {LionField} */ (await fixture(html`<${tag}> const el = /** @type {LionField} */ (
await fixture(html`<${tag}>
${inputSlot} ${inputSlot}
<span slot="before" data-label>[before]</span> <span slot="before" data-label>[before]</span>
<span slot="after" data-label>[after]</span> <span slot="after" data-label>[after]</span>
<span slot="prefix" data-description>[prefix]</span> <span slot="prefix" data-description>[prefix]</span>
<span slot="suffix" data-description>[suffix]</span> <span slot="suffix" data-description>[suffix]</span>
</${tag}> </${tag}>
`)); `)
);
const nativeInput = getSlot(el, 'input'); const nativeInput = getSlot(el, 'input');
// @ts-ignore allow protected accessors in tests // @ts-ignore allow protected accessors in tests
@ -234,14 +234,16 @@ describe('<lion-field>', () => {
return result; return result;
} }
}; };
const el = /** @type {LionField} */ (await fixture(html` const el = /** @type {LionField} */ (
await fixture(html`
<${tag} <${tag}
.validators=${[new HasX()]} .validators=${[new HasX()]}
.modelValue=${'a@b.nl'} .modelValue=${'a@b.nl'}
> >
${inputSlot} ${inputSlot}
</${tag}> </${tag}>
`)); `)
);
/** /**
* @param {import("../index.js").LionField} _sceneEl * @param {import("../index.js").LionField} _sceneEl
@ -303,7 +305,8 @@ describe('<lion-field>', () => {
return result; return result;
} }
}; };
const disabledEl = /** @type {LionField} */ (await fixture(html` const disabledEl = /** @type {LionField} */ (
await fixture(html`
<${tag} <${tag}
disabled disabled
.validators=${[new HasX()]} .validators=${[new HasX()]}
@ -311,15 +314,18 @@ describe('<lion-field>', () => {
> >
${inputSlot} ${inputSlot}
</${tag}> </${tag}>
`)); `)
const el = /** @type {LionField} */ (await fixture(html` );
const el = /** @type {LionField} */ (
await fixture(html`
<${tag} <${tag}
.validators=${[new HasX()]} .validators=${[new HasX()]}
.modelValue=${'a@b.nl'} .modelValue=${'a@b.nl'}
> >
${inputSlot} ${inputSlot}
</${tag}> </${tag}>
`)); `)
);
expect(el.hasFeedbackFor).to.deep.equal(['error']); expect(el.hasFeedbackFor).to.deep.equal(['error']);
expect(el.validationStates.error.HasX).to.exist; expect(el.validationStates.error.HasX).to.exist;
@ -329,11 +335,13 @@ describe('<lion-field>', () => {
}); });
it('can be required', async () => { it('can be required', async () => {
const el = /** @type {LionField} */ (await fixture(html` const el = /** @type {LionField} */ (
await fixture(html`
<${tag} <${tag}
.validators=${[new Required()]} .validators=${[new Required()]}
>${inputSlot}</${tag}> >${inputSlot}</${tag}>
`)); `)
);
expect(el.hasFeedbackFor).to.deep.equal(['error']); expect(el.hasFeedbackFor).to.deep.equal(['error']);
expect(el.validationStates.error.Required).to.exist; expect(el.validationStates.error.Required).to.exist;
el.modelValue = 'cat'; el.modelValue = 'cat';
@ -356,13 +364,15 @@ describe('<lion-field>', () => {
return hasError; return hasError;
} }
}; };
const el = /** @type {LionField} */ (await fixture(html` const el = /** @type {LionField} */ (
await fixture(html`
<${tag} <${tag}
.modelValue=${'init-string'} .modelValue=${'init-string'}
.formatter=${formatterSpy} .formatter=${formatterSpy}
.validators=${[new Bar()]} .validators=${[new Bar()]}
>${inputSlot}</${tag}> >${inputSlot}</${tag}>
`)); `)
);
expect(formatterSpy.callCount).to.equal(0); expect(formatterSpy.callCount).to.equal(0);
expect(el.formattedValue).to.equal('init-string'); expect(el.formattedValue).to.equal('init-string');
@ -379,7 +389,8 @@ describe('<lion-field>', () => {
describe(`Content projection`, () => { describe(`Content projection`, () => {
it('renders correctly all slot elements in light DOM', async () => { it('renders correctly all slot elements in light DOM', async () => {
const el = /** @type {LionField} */ (await fixture(html` const el = /** @type {LionField} */ (
await fixture(html`
<${tag}> <${tag}>
<label slot="label">[label]</label> <label slot="label">[label]</label>
${inputSlot} ${inputSlot}
@ -390,7 +401,8 @@ describe('<lion-field>', () => {
<span slot="suffix">[suffix]</span> <span slot="suffix">[suffix]</span>
<span slot="feedback">[feedback]</span> <span slot="feedback">[feedback]</span>
</${tag}> </${tag}>
`)); `)
);
const names = [ const names = [
'label', 'label',
@ -405,10 +417,9 @@ describe('<lion-field>', () => {
names.forEach(slotName => { names.forEach(slotName => {
const slotLight = /** @type {HTMLElement} */ (el.querySelector(`[slot="${slotName}"]`)); const slotLight = /** @type {HTMLElement} */ (el.querySelector(`[slot="${slotName}"]`));
slotLight.setAttribute('test-me', 'ok'); slotLight.setAttribute('test-me', 'ok');
// @ts-expect-error const slot = /** @type {ShadowHTMLElement} */ (
const slot = /** @type {ShadowHTMLElement} */ (el.shadowRoot.querySelector( /** @type {ShadowRoot} */ (el.shadowRoot).querySelector(`slot[name="${slotName}"]`)
`slot[name="${slotName}"]`, );
));
const assignedNodes = slot.assignedNodes(); const assignedNodes = slot.assignedNodes();
expect(assignedNodes.length).to.equal(1); expect(assignedNodes.length).to.equal(1);
expect(assignedNodes[0].getAttribute('test-me')).to.equal('ok'); expect(assignedNodes[0].getAttribute('test-me')).to.equal('ok');

View file

@ -1,5 +1,6 @@
import { LitElement } from '@lion/core'; import { LitElement } from '@lion/core';
import { defineCE, expect, fixture, fixtureSync, html, unsafeStatic } from '@open-wc/testing'; import { defineCE, expect, fixture, fixtureSync } from '@open-wc/testing';
import { html, unsafeStatic } from 'lit/static-html.js';
import sinon from 'sinon'; import sinon from 'sinon';
import { SyncUpdatableMixin } from '../../src/utils/SyncUpdatableMixin.js'; import { SyncUpdatableMixin } from '../../src/utils/SyncUpdatableMixin.js';
@ -43,9 +44,9 @@ describe('SyncUpdatableMixin', () => {
const tagString = defineCE(UpdatableImplementation); const tagString = defineCE(UpdatableImplementation);
const tag = unsafeStatic(tagString); const tag = unsafeStatic(tagString);
const el = /** @type {UpdatableImplementation} */ (fixtureSync( const el = /** @type {UpdatableImplementation} */ (
html`<${tag} prop-b="b"></${tag}>`, fixtureSync(html`<${tag} prop-b="b"></${tag}>`)
)); );
// Getters setters work as expected, without running property effects // Getters setters work as expected, without running property effects
expect(el.propA).to.equal('init-a'); expect(el.propA).to.equal('init-a');
@ -102,9 +103,9 @@ describe('SyncUpdatableMixin', () => {
const tagString = defineCE(UpdatableImplementation); const tagString = defineCE(UpdatableImplementation);
const tag = unsafeStatic(tagString); const tag = unsafeStatic(tagString);
const el = /** @type {UpdatableImplementation} */ (fixtureSync( const el = /** @type {UpdatableImplementation} */ (
html`<${tag} prop-b="b" .propA="${'a'}"></${tag}>`, fixtureSync(html`<${tag} prop-b="b" .propA="${'a'}"></${tag}>`)
)); );
// Derived // Derived
expect(el.derived).to.be.undefined; expect(el.derived).to.be.undefined;
@ -114,19 +115,19 @@ describe('SyncUpdatableMixin', () => {
expect(el.derived).to.equal('ab'); expect(el.derived).to.equal('ab');
expect(hasCalledRunPropertyEffect).to.be.true; expect(hasCalledRunPropertyEffect).to.be.true;
const el2 = /** @type {UpdatableImplementation} */ (await fixture( const el2 = /** @type {UpdatableImplementation} */ (
html`<${tag} .propA="${'a'}"></${tag}>`, await fixture(html`<${tag} .propA="${'a'}"></${tag}>`)
)); );
expect(el2.derived).to.equal('ainit-b'); expect(el2.derived).to.equal('ainit-b');
const el3 = /** @type {UpdatableImplementation} */ (await fixture( const el3 = /** @type {UpdatableImplementation} */ (
html`<${tag} .propB="${'b'}"></${tag}>`, await fixture(html`<${tag} .propB="${'b'}"></${tag}>`)
)); );
expect(el3.derived).to.equal('init-ab'); expect(el3.derived).to.equal('init-ab');
const el4 = /** @type {UpdatableImplementation} */ (await fixture( const el4 = /** @type {UpdatableImplementation} */ (
html`<${tag} .propA=${'a'} .propB="${'b'}"></${tag}>`, await fixture(html`<${tag} .propA=${'a'} .propB="${'b'}"></${tag}>`)
)); );
expect(el4.derived).to.equal('ab'); expect(el4.derived).to.equal('ab');
}); });
@ -150,8 +151,8 @@ describe('SyncUpdatableMixin', () => {
* @param {string} name * @param {string} name
* @param {*} oldValue * @param {*} oldValue
*/ */
requestUpdateInternal(name, oldValue) { requestUpdate(name, oldValue) {
super.requestUpdateInternal(name, oldValue); super.requestUpdate(name, oldValue);
if (name === 'prop') { if (name === 'prop') {
propChangedCount += 1; propChangedCount += 1;
} }
@ -223,9 +224,9 @@ describe('SyncUpdatableMixin', () => {
const tagString = defineCE(UpdatableImplementation); const tagString = defineCE(UpdatableImplementation);
const tag = unsafeStatic(tagString); const tag = unsafeStatic(tagString);
const el = /** @type {UpdatableImplementation} */ (fixtureSync( const el = /** @type {UpdatableImplementation} */ (
html`<${tag} prop-b="b" .propA="${'a'}"></${tag}>`, fixtureSync(html`<${tag} prop-b="b" .propA="${'a'}"></${tag}>`)
)); );
const spy = sinon.spy(el, '_runPropertyEffect'); const spy = sinon.spy(el, '_runPropertyEffect');
expect(spy.callCount).to.equal(0); expect(spy.callCount).to.equal(0);

View file

@ -1,4 +1,5 @@
import { expect, fixture, html } from '@open-wc/testing'; import { expect, fixture } from '@open-wc/testing';
import { html } from 'lit/static-html.js';
import sinon from 'sinon'; import sinon from 'sinon';
import { browserDetection } from '@lion/core'; import { browserDetection } from '@lion/core';
import { getAriaElementsInRightDomOrder } from '../../src/utils/getAriaElementsInRightDomOrder.js'; import { getAriaElementsInRightDomOrder } from '../../src/utils/getAriaElementsInRightDomOrder.js';

View file

@ -1,4 +1,5 @@
import { expect, fixture, html, unsafeStatic, defineCE } from '@open-wc/testing'; import { expect, fixture, defineCE } from '@open-wc/testing';
import { html, unsafeStatic } from 'lit/static-html.js';
import { LionField } from '@lion/form-core'; import { LionField } from '@lion/form-core';
import { getFormControlMembers } from '@lion/form-core/test-helpers'; import { getFormControlMembers } from '@lion/form-core/test-helpers';
import { Required } from '../../src/validate/validators/Required.js'; import { Required } from '../../src/validate/validators/Required.js';
@ -31,9 +32,9 @@ describe('Required validation', async () => {
const validator = new Required(); const validator = new Required();
it('get aria-required attribute if element is part of the right tag names', async () => { it('get aria-required attribute if element is part of the right tag names', async () => {
const el = /** @type {FormControlHost & HTMLElement} */ (await fixture( const el = /** @type {FormControlHost & HTMLElement} */ (
html`<${tag}></${tag}>`, await fixture(html`<${tag}></${tag}>`)
)); );
Required._compatibleTags.forEach(tagName => { Required._compatibleTags.forEach(tagName => {
inputNodeTag = /** @type {HTMLElementWithValue} */ (document.createElement(tagName)); inputNodeTag = /** @type {HTMLElementWithValue} */ (document.createElement(tagName));
@ -53,9 +54,9 @@ describe('Required validation', async () => {
expect(_inputNode).to.not.have.attribute('aria-required'); expect(_inputNode).to.not.have.attribute('aria-required');
}); });
it('get aria-required attribute if element is part of the right roles', async () => { it('get aria-required attribute if element is part of the right roles', async () => {
const el = /** @type {FormControlHost & HTMLElement} */ (await fixture( const el = /** @type {FormControlHost & HTMLElement} */ (
html`<${tag}></${tag}>`, await fixture(html`<${tag}></${tag}>`)
)); );
Required._compatibleRoles.forEach(role => { Required._compatibleRoles.forEach(role => {
// @ts-ignore // @ts-ignore

View file

@ -1,5 +1,6 @@
import { LitElement } from '@lion/core'; import { LitElement } from '@lion/core';
import { defineCE, expect, fixture, html, unsafeStatic } from '@open-wc/testing'; import { defineCE, expect, fixture } from '@open-wc/testing';
import { html, unsafeStatic } from 'lit/static-html.js';
import sinon from 'sinon'; import sinon from 'sinon';
import { ValidateMixin } from '../../src/validate/ValidateMixin.js'; import { ValidateMixin } from '../../src/validate/ValidateMixin.js';
import { Validator } from '../../src/validate/Validator.js'; import { Validator } from '../../src/validate/Validator.js';
@ -171,9 +172,11 @@ describe('Validator', () => {
const connectSpy = sinon.spy(myVal, 'onFormControlConnect'); const connectSpy = sinon.spy(myVal, 'onFormControlConnect');
const disconnectSpy = sinon.spy(myVal, 'onFormControlDisconnect'); const disconnectSpy = sinon.spy(myVal, 'onFormControlDisconnect');
const el = /** @type {ValidateElement} */ (await fixture(html` const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag} .validators=${[myVal]}>${lightDom}</${tag}> <${tag} .validators=${[myVal]}>${lightDom}</${tag}>
`)); `)
);
expect(connectSpy.callCount).to.equal(1); expect(connectSpy.callCount).to.equal(1);
expect(connectSpy.calledWith(el)).to.equal(true); expect(connectSpy.calledWith(el)).to.equal(true);

View file

@ -1,5 +1,6 @@
/* eslint-disable no-unused-vars, no-param-reassign */ /* eslint-disable no-unused-vars, no-param-reassign */
import { expect, fixture, html } from '@open-wc/testing'; import { expect, fixture } from '@open-wc/testing';
import { html } from 'lit/static-html.js';
import sinon from 'sinon'; import sinon from 'sinon';
import '@lion/form-core/define-validation-feedback'; import '@lion/form-core/define-validation-feedback';
import { AlwaysInvalid, AlwaysValid } from '../../test-helpers/index.js'; import { AlwaysInvalid, AlwaysValid } from '../../test-helpers/index.js';
@ -10,9 +11,9 @@ import { AlwaysInvalid, AlwaysValid } from '../../test-helpers/index.js';
describe('lion-validation-feedback', () => { describe('lion-validation-feedback', () => {
it('renders a validation message', async () => { it('renders a validation message', async () => {
const el = /** @type {LionValidationFeedback} */ (await fixture( const el = /** @type {LionValidationFeedback} */ (
html`<lion-validation-feedback></lion-validation-feedback>`, await fixture(html`<lion-validation-feedback></lion-validation-feedback>`)
)); );
expect(el).shadowDom.to.equal(''); expect(el).shadowDom.to.equal('');
el.feedbackData = [{ message: 'hello', type: 'error', validator: new AlwaysInvalid() }]; el.feedbackData = [{ message: 'hello', type: 'error', validator: new AlwaysInvalid() }];
await el.updateComplete; await el.updateComplete;
@ -20,9 +21,9 @@ describe('lion-validation-feedback', () => {
}); });
it('renders the validation type attribute', async () => { it('renders the validation type attribute', async () => {
const el = /** @type {LionValidationFeedback} */ (await fixture( const el = /** @type {LionValidationFeedback} */ (
html`<lion-validation-feedback></lion-validation-feedback>`, await fixture(html`<lion-validation-feedback></lion-validation-feedback>`)
)); );
el.feedbackData = [{ message: 'hello', type: 'error', validator: new AlwaysInvalid() }]; el.feedbackData = [{ message: 'hello', type: 'error', validator: new AlwaysInvalid() }];
await el.updateComplete; await el.updateComplete;
expect(el.getAttribute('type')).to.equal('error'); expect(el.getAttribute('type')).to.equal('error');
@ -33,9 +34,9 @@ describe('lion-validation-feedback', () => {
}); });
it('success message clears after 3s', async () => { it('success message clears after 3s', async () => {
const el = /** @type {LionValidationFeedback} */ (await fixture( const el = /** @type {LionValidationFeedback} */ (
html`<lion-validation-feedback></lion-validation-feedback>`, await fixture(html`<lion-validation-feedback></lion-validation-feedback>`)
)); );
const clock = sinon.useFakeTimers(); const clock = sinon.useFakeTimers();
@ -55,9 +56,9 @@ describe('lion-validation-feedback', () => {
}); });
it('does not clear error messages', async () => { it('does not clear error messages', async () => {
const el = /** @type {LionValidationFeedback} */ (await fixture( const el = /** @type {LionValidationFeedback} */ (
html`<lion-validation-feedback></lion-validation-feedback>`, await fixture(html`<lion-validation-feedback></lion-validation-feedback>`)
)); );
const clock = sinon.useFakeTimers(); const clock = sinon.useFakeTimers();

View file

@ -1,5 +1,5 @@
import { Constructor } from '@open-wc/dedupe-mixin'; import { Constructor } from '@open-wc/dedupe-mixin';
import { BooleanAttributePart, LitElement } from '@lion/core'; import { LitElement } from '@lion/core';
import { FormatNumberOptions } from '@lion/localize/types/LocalizeMixinTypes'; import { FormatNumberOptions } from '@lion/localize/types/LocalizeMixinTypes';
import { ValidateHost } from './validate/ValidateMixinTypes'; import { ValidateHost } from './validate/ValidateMixinTypes';
import { FormControlHost } from './FormControlMixinTypes'; import { FormControlHost } from './FormControlMixinTypes';

View file

@ -34,7 +34,7 @@ export declare class ChoiceInputHost {
protected get _inputNode(): HTMLElement; protected get _inputNode(): HTMLElement;
protected _proxyInputEvent(): void; protected _proxyInputEvent(): void;
protected requestUpdateInternal(name: string, oldValue: any): void; protected requestUpdate(name: string, oldValue: any): void;
protected _choiceGraphicTemplate(): TemplateResult; protected _choiceGraphicTemplate(): TemplateResult;
protected _afterTemplate(): TemplateResult; protected _afterTemplate(): TemplateResult;
protected _preventDuplicateLabelClick(ev: Event): void; protected _preventDuplicateLabelClick(ev: Event): void;

View file

@ -10,7 +10,7 @@ export declare interface SyncUpdatableNamespace {
export declare class SyncUpdatableHost { export declare class SyncUpdatableHost {
/** /**
* An abstraction that has the exact same api as `requestUpdateInternal`, but taking * An abstraction that has the exact same api as `requestUpdate`, but taking
* into account: * into account:
* - [member order independence](https://github.com/webcomponents/gold-standard/wiki/Member-Order-Independence) * - [member order independence](https://github.com/webcomponents/gold-standard/wiki/Member-Order-Independence)
* - property effects start when all (light) dom has initialized (on firstUpdated) * - property effects start when all (light) dom has initialized (on firstUpdated)
@ -18,7 +18,7 @@ export declare class SyncUpdatableHost {
* - compatible with propertyAccessor.`hasChanged`: no manual checks needed or accidentally * - compatible with propertyAccessor.`hasChanged`: no manual checks needed or accidentally
* run property effects / events when no change happened * run property effects / events when no change happened
* effects when values didn't change * effects when values didn't change
* All code previously present in requestUpdateInternal can be placed in this method. * All code previously present in requestUpdate can be placed in this method.
* @param {string} name * @param {string} name
* @param {*} oldValue * @param {*} oldValue
*/ */

View file

@ -1,4 +1,5 @@
import { expect, fixture, html } from '@open-wc/testing'; import { expect, fixture } from '@open-wc/testing';
import { html } from 'lit/static-html.js';
import { getAllTagNames } from './helpers/helpers.js'; import { getAllTagNames } from './helpers/helpers.js';
import './helpers/umbrella-form.js'; import './helpers/umbrella-form.js';
import '@lion/dialog/define'; import '@lion/dialog/define';

View file

@ -1,4 +1,5 @@
import { elementUpdated, expect, fixture, html } from '@open-wc/testing'; import { elementUpdated, expect, fixture } from '@open-wc/testing';
import { html } from 'lit/static-html.js';
import './helpers/umbrella-form.js'; import './helpers/umbrella-form.js';
import { getAllFieldsAndFormGroups } from './helpers/helpers.js'; import { getAllFieldsAndFormGroups } from './helpers/helpers.js';
@ -81,9 +82,11 @@ describe(`Submitting/Resetting/Clearing Form`, async () => {
}); });
it('calling resetGroup() should reset all metadata (interaction states and initial values)', async () => { it('calling resetGroup() should reset all metadata (interaction states and initial values)', async () => {
const el = /** @type {UmbrellaForm} */ (await fixture( const el = /** @type {UmbrellaForm} */ (
html`<umbrella-form .serializedValue="${fullyPrefilledSerializedValue}"></umbrella-form>`, await fixture(
)); html`<umbrella-form .serializedValue="${fullyPrefilledSerializedValue}"></umbrella-form>`,
)
);
await el.updateComplete; await el.updateComplete;
const formEl = el._lionFormNode; const formEl = el._lionFormNode;
@ -125,9 +128,11 @@ describe(`Submitting/Resetting/Clearing Form`, async () => {
// Wait till ListboxMixin properly clears // Wait till ListboxMixin properly clears
it('calling clearGroup() should clear all fields', async () => { it('calling clearGroup() should clear all fields', async () => {
const el = /** @type {UmbrellaForm} */ (await fixture( const el = /** @type {UmbrellaForm} */ (
html`<umbrella-form .serializedValue="${fullyPrefilledSerializedValue}"></umbrella-form>`, await fixture(
)); html`<umbrella-form .serializedValue="${fullyPrefilledSerializedValue}"></umbrella-form>`,
)
);
await el.updateComplete; await el.updateComplete;
const formEl = el._lionFormNode; const formEl = el._lionFormNode;

View file

@ -1,4 +1,5 @@
import { expect, fixture, html } from '@open-wc/testing'; import { expect, fixture } from '@open-wc/testing';
import { html } from 'lit/static-html.js';
import { getAllTagNames } from './helpers/helpers.js'; import { getAllTagNames } from './helpers/helpers.js';
import './helpers/umbrella-form.js'; import './helpers/umbrella-form.js';
@ -64,28 +65,30 @@ describe('Form Integrations', () => {
describe('Form Integrations', () => { describe('Form Integrations', () => {
it('does not become dirty when elements are prefilled', async () => { it('does not become dirty when elements are prefilled', async () => {
const el = /** @type {UmbrellaForm} */ (await fixture( const el = /** @type {UmbrellaForm} */ (
html`<umbrella-form await fixture(
.serializedValue="${{ html`<umbrella-form
full_name: { first_name: '', last_name: '' }, .serializedValue="${{
date: '2000-12-12', full_name: { first_name: '', last_name: '' },
datepicker: '2020-12-12', date: '2000-12-12',
bio: '', datepicker: '2020-12-12',
money: '', bio: '',
iban: '', money: '',
email: '', iban: '',
checkers: ['foo', 'bar'], email: '',
dinosaurs: 'brontosaurus', checkers: ['foo', 'bar'],
favoriteFruit: 'Banana', dinosaurs: 'brontosaurus',
favoriteMovie: 'Rocky', favoriteFruit: 'Banana',
favoriteColor: 'hotpink', favoriteMovie: 'Rocky',
lyrics: '1', favoriteColor: 'hotpink',
range: 2.3, lyrics: '1',
terms: [], range: 2.3,
comments: '', terms: [],
}}" comments: '',
></umbrella-form>`, }}"
)); ></umbrella-form>`,
)
);
await el._lionFormNode.initComplete; await el._lionFormNode.initComplete;
expect(el._lionFormNode.dirty).to.be.false; expect(el._lionFormNode.dirty).to.be.false;

View file

@ -1,4 +1,5 @@
import { expect, fixture, html, defineCE, unsafeStatic } from '@open-wc/testing'; import { expect, fixture, defineCE } from '@open-wc/testing';
import { html, unsafeStatic } from 'lit/static-html.js';
import { Required, DefaultSuccess, Validator } from '@lion/form-core'; import { Required, DefaultSuccess, Validator } from '@lion/form-core';
import { loadDefaultFeedbackMessages } from '@lion/validate-messages'; import { loadDefaultFeedbackMessages } from '@lion/validate-messages';
import { LionInput } from '@lion/input'; import { LionInput } from '@lion/input';
@ -41,7 +42,8 @@ describe('Form Validation Integrations', () => {
} }
const elTagString = defineCE(ValidateElementCustomTypes); const elTagString = defineCE(ValidateElementCustomTypes);
const elTag = unsafeStatic(elTagString); const elTag = unsafeStatic(elTagString);
const el = /** @type {ValidateElementCustomTypes} */ (await fixture(html` const el = /** @type {ValidateElementCustomTypes} */ (
await fixture(html`
<${elTag} <${elTag}
.validators=${[ .validators=${[
new Required(null, { getMessage: () => 'error' }), new Required(null, { getMessage: () => 'error' }),
@ -49,7 +51,8 @@ describe('Form Validation Integrations', () => {
new DefaultSuccess(), new DefaultSuccess(),
]} ]}
>${lightDom}</${elTag}> >${lightDom}</${elTag}>
`)); `)
);
const { _feedbackNode } = getFormControlMembers(el); const { _feedbackNode } = getFormControlMembers(el);
expect(_feedbackNode.feedbackData?.length).to.equal(0); expect(_feedbackNode.feedbackData?.length).to.equal(0);

View file

@ -22,9 +22,9 @@ import '@lion/input-stepper/define';
export class UmbrellaForm extends LitElement { export class UmbrellaForm extends LitElement {
get _lionFormNode() { get _lionFormNode() {
return /** @type {import('@lion/form').LionForm} */ (this.shadowRoot?.querySelector( return /** @type {import('@lion/form').LionForm} */ (
'lion-form', this.shadowRoot?.querySelector('lion-form')
)); );
} }
/** /**

View file

@ -1,4 +1,5 @@
import { expect, html, unsafeStatic, fixture } from '@open-wc/testing'; import { expect, fixture } from '@open-wc/testing';
import { html, unsafeStatic } from 'lit/static-html.js';
// eslint-disable-next-line import/no-extraneous-dependencies // eslint-disable-next-line import/no-extraneous-dependencies
import sinon from 'sinon'; import sinon from 'sinon';
@ -111,9 +112,9 @@ const choiceDispatchesCountOnInteraction = (tagname, count) => {
const tag = unsafeStatic(tagname); const tag = unsafeStatic(tagname);
const spy = sinon.spy(); const spy = sinon.spy();
it(getInteractionTitle(count), async () => { it(getInteractionTitle(count), async () => {
const el = /** @type {HTMLElement & {checked: boolean}} */ (await fixture( const el = /** @type {HTMLElement & {checked: boolean}} */ (
html`<${tag} .choiceValue="${'option'}"></${tag}>`, await fixture(html`<${tag} .choiceValue="${'option'}"></${tag}>`)
)); );
el.addEventListener('model-value-changed', spy); el.addEventListener('model-value-changed', spy);
el.checked = true; el.checked = true;
expect(spy.callCount).to.equal(count); expect(spy.callCount).to.equal(count);
@ -161,17 +162,17 @@ const choiceGroupDispatchesCountOnInteraction = (groupTagname, itemTagname, coun
`); `);
el.addEventListener('model-value-changed', spy); el.addEventListener('model-value-changed', spy);
const option2 = /** @type {HTMLElement & {checked: boolean}} */ (el.querySelector( const option2 = /** @type {HTMLElement & {checked: boolean}} */ (
`${itemTagname}:nth-child(2)`, el.querySelector(`${itemTagname}:nth-child(2)`)
)); );
option2.checked = true; option2.checked = true;
expect(spy.callCount).to.equal(count); expect(spy.callCount).to.equal(count);
spy.resetHistory(); spy.resetHistory();
const option3 = /** @type {HTMLElement & {checked: boolean}} */ (el.querySelector( const option3 = /** @type {HTMLElement & {checked: boolean}} */ (
`${itemTagname}:nth-child(3)`, el.querySelector(`${itemTagname}:nth-child(3)`)
)); );
option3.checked = true; option3.checked = true;
expect(spy.callCount).to.equal(count); expect(spy.callCount).to.equal(count);
}); });
@ -233,15 +234,17 @@ describe('lion-select', () => {
it(getInteractionTitle(interactionCount), async () => { it(getInteractionTitle(interactionCount), async () => {
const spy = sinon.spy(); const spy = sinon.spy();
const el = /** @type {LionSelect} */ (await fixture(html` const el = /** @type {LionSelect} */ (
<lion-select> await fixture(html`
<select slot="input"> <lion-select>
<option value="option1"></option> <select slot="input">
<option value="option2"></option> <option value="option1"></option>
<option value="option3"></option> <option value="option2"></option>
</select> <option value="option3"></option>
</lion-select> </select>
`)); </lion-select>
`)
);
el.addEventListener('model-value-changed', spy); el.addEventListener('model-value-changed', spy);
const option2 = /** @type {HTMLOptionElement} */ (el.querySelector('option:nth-child(2)')); const option2 = /** @type {HTMLOptionElement} */ (el.querySelector('option:nth-child(2)'));
@ -464,9 +467,10 @@ describe('detail.isTriggeredByUser', () => {
} }
const name = controlName === 'checkbox-group' ? 'test[]' : 'test'; const name = controlName === 'checkbox-group' ? 'test[]' : 'test';
const el = /** @type {LitElement & FormControl & {value: string} & {registrationComplete: Promise<boolean>} & {formElements: Array.<FormControl & {value: string}>}} */ (await fixture( const el =
html`<${tag} name="${name}">${childrenEl}</${tag}>`, /** @type {LitElement & FormControl & {value: string} & {registrationComplete: Promise<boolean>} & {formElements: Array.<FormControl & {value: string}>}} */ (
)); await fixture(html`<${tag} name="${name}">${childrenEl}</${tag}>`)
);
await el.registrationComplete; await el.registrationComplete;
el.addEventListener('model-value-changed', spy); el.addEventListener('model-value-changed', spy);

View file

@ -1,6 +1,7 @@
import '@lion/fieldset/define'; import '@lion/fieldset/define';
import '@lion/input/define'; import '@lion/input/define';
import { expect, html, fixture } from '@open-wc/testing'; import { expect, fixture } from '@open-wc/testing';
import { html } from 'lit/static-html.js';
// eslint-disable-next-line import/no-extraneous-dependencies // eslint-disable-next-line import/no-extraneous-dependencies
import sinon from 'sinon'; import sinon from 'sinon';

View file

@ -1,12 +1,5 @@
import { import { expect, fixture as _fixture, oneEvent, aTimeout, defineCE } from '@open-wc/testing';
expect, import { html, unsafeStatic } from 'lit/static-html.js';
fixture as _fixture,
html,
oneEvent,
aTimeout,
unsafeStatic,
defineCE,
} from '@open-wc/testing';
import { spy } from 'sinon'; import { spy } from 'sinon';
import { LionField } from '@lion/form-core'; import { LionField } from '@lion/form-core';
import { LionFieldset } from '@lion/fieldset'; import { LionFieldset } from '@lion/fieldset';
@ -61,9 +54,9 @@ describe('<lion-form>', () => {
</form> </form>
</lion-form> </lion-form>
`); `);
const resetButton = /** @type {HTMLInputElement} */ (withDefaults.querySelector( const resetButton = /** @type {HTMLInputElement} */ (
'input[type=reset]', withDefaults.querySelector('input[type=reset]')
)); );
withDefaults.formElements.firstName.modelValue = 'updatedFoo'; withDefaults.formElements.firstName.modelValue = 'updatedFoo';
expect(withDefaults.modelValue).to.deep.equal({ expect(withDefaults.modelValue).to.deep.equal({

View file

@ -1,4 +1,5 @@
import { expect, fixture as _fixture, html } from '@open-wc/testing'; import { expect, fixture as _fixture } from '@open-wc/testing';
import { html } from 'lit/static-html.js';
import '@lion/helpers/define-sb-action-logger'; import '@lion/helpers/define-sb-action-logger';
/** /**
@ -53,9 +54,9 @@ describe('sb-action-logger', () => {
it('shows a visual cue whenever something is logged to the logger', async () => { it('shows a visual cue whenever something is logged to the logger', async () => {
const el = await fixture(html`<sb-action-logger></sb-action-logger>`); const el = await fixture(html`<sb-action-logger></sb-action-logger>`);
const cueEl = /** @type {HTMLElement} */ (el.shadowRoot?.querySelector( const cueEl = /** @type {HTMLElement} */ (
'.header__log-cue-overlay', el.shadowRoot?.querySelector('.header__log-cue-overlay')
)); );
expect(cueEl.classList.contains('header__log-cue-overlay--slide')).to.be.false; expect(cueEl.classList.contains('header__log-cue-overlay--slide')).to.be.false;
el.log('Hello, World!'); el.log('Hello, World!');
@ -65,9 +66,9 @@ describe('sb-action-logger', () => {
it('has a visual counter that counts the amount of total logs', async () => { it('has a visual counter that counts the amount of total logs', async () => {
const el = await fixture(html`<sb-action-logger></sb-action-logger>`); const el = await fixture(html`<sb-action-logger></sb-action-logger>`);
const cueEl = /** @type {HTMLElement} */ (el.shadowRoot?.querySelector( const cueEl = /** @type {HTMLElement} */ (
'.header__log-cue-overlay', el.shadowRoot?.querySelector('.header__log-cue-overlay')
)); );
expect(cueEl.classList.contains('.header__log-cue-overlay--slide')).to.be.false; expect(cueEl.classList.contains('.header__log-cue-overlay--slide')).to.be.false;
@ -100,12 +101,12 @@ describe('sb-action-logger', () => {
const loggerEl = /** @type {HTMLElement} */ (el.shadowRoot?.querySelector('.logger')); const loggerEl = /** @type {HTMLElement} */ (el.shadowRoot?.querySelector('.logger'));
const firstLogCount = /** @type {HTMLElement} */ (loggerEl.firstElementChild?.querySelector( const firstLogCount = /** @type {HTMLElement} */ (
'.logger__log-count', loggerEl.firstElementChild?.querySelector('.logger__log-count')
)); );
const lastLogCount = /** @type {HTMLElement} */ (loggerEl.lastElementChild?.querySelector( const lastLogCount = /** @type {HTMLElement} */ (
'.logger__log-count', loggerEl.lastElementChild?.querySelector('.logger__log-count')
)); );
expect(loggerEl.children.length).to.equal(4); expect(loggerEl.children.length).to.equal(4);
expect(firstLogCount.innerText).to.equal('3'); expect(firstLogCount.innerText).to.equal('3');

View file

@ -1,6 +1,6 @@
/** /**
* @typedef {import('lit-html').nothing} nothing
* @typedef {import('@lion/core').TemplateResult} TemplateResult * @typedef {import('@lion/core').TemplateResult} TemplateResult
* @typedef {import('@lion/core').nothing} nothing
*/ */
export class IconManager { export class IconManager {

View file

@ -1,6 +1,11 @@
import { css, html, LitElement, nothing, render, TemplateResult } from '@lion/core'; import { css, html, LitElement, nothing, render, isTemplateResult } from '@lion/core';
import { icons } from './icons.js'; import { icons } from './icons.js';
/**
* @typedef {import('@lion/core').TemplateResult} TemplateResult
* @typedef {(tag: (strings: TemplateStringsArray, ... expr: string[]) => string) => string} TagFunction
*/
/** /**
* @param {?} wrappedSvgObject * @param {?} wrappedSvgObject
*/ */
@ -14,7 +19,7 @@ function unwrapSvg(wrappedSvgObject) {
* @param {TemplateResult|nothing} svg * @param {TemplateResult|nothing} svg
*/ */
function validateSvg(svg) { function validateSvg(svg) {
if (!(svg === nothing || svg instanceof TemplateResult)) { if (!(svg === nothing || isTemplateResult(svg))) {
throw new Error( throw new Error(
'icon accepts only lit-html templates or functions like "tag => tag`<svg>...</svg>`"', 'icon accepts only lit-html templates or functions like "tag => tag`<svg>...</svg>`"',
); );
@ -98,7 +103,10 @@ export class LionIcon extends LitElement {
this.role = 'img'; this.role = 'img';
this.ariaLabel = ''; this.ariaLabel = '';
this.iconId = ''; this.iconId = '';
/** @private */ /**
* @private
* @type {TemplateResult|nothing|TagFunction}
*/
this.__svg = nothing; this.__svg = nothing;
} }
@ -127,7 +135,7 @@ export class LionIcon extends LitElement {
/** /**
* On IE11, svgs without focusable false appear in the tab order * On IE11, svgs without focusable false appear in the tab order
* so make sure to have <svg focusable="false"> in svg files * so make sure to have <svg focusable="false"> in svg files
* @param {TemplateResult|nothing} svg * @param {TemplateResult|nothing|TagFunction} svg
*/ */
set svg(svg) { set svg(svg) {
this.__svg = svg; this.__svg = svg;
@ -138,6 +146,9 @@ export class LionIcon extends LitElement {
} }
} }
/**
* @type {TemplateResult|nothing|TagFunction}
*/
get svg() { get svg() {
return this.__svg; return this.__svg;
} }

View file

@ -1,5 +1,5 @@
import { nothing, until } from '@lion/core'; import { nothing, until, html } from '@lion/core';
import { aTimeout, expect, fixture as _fixture, fixtureSync, html } from '@open-wc/testing'; import { aTimeout, expect, fixture as _fixture, fixtureSync } from '@open-wc/testing';
import '@lion/icon/define'; import '@lion/icon/define';
import { icons } from '../src/icons.js'; import { icons } from '../src/icons.js';
import hammerSvg from './hammer.svg.js'; import hammerSvg from './hammer.svg.js';
@ -145,7 +145,7 @@ describe('lion-icon', () => {
await el.updateComplete; await el.updateComplete;
el.svg = nothing; el.svg = nothing;
await el.updateComplete; await el.updateComplete;
expect(el.innerHTML).to.equal('<!----><!---->'); // don't use lightDom.to.equal(''), it gives false positives expect(el.innerHTML).to.equal('<!---->'); // don't use lightDom.to.equal(''), it gives false positives
}); });
it('does not render "null" if changed from valid input to null', async () => { it('does not render "null" if changed from valid input to null', async () => {
@ -153,7 +153,7 @@ describe('lion-icon', () => {
await el.updateComplete; await el.updateComplete;
el.svg = nothing; el.svg = nothing;
await el.updateComplete; await el.updateComplete;
expect(el.innerHTML).to.equal('<!----><!---->'); // don't use lightDom.to.equal(''), it gives false positives expect(el.innerHTML).to.equal('<!---->'); // don't use lightDom.to.equal(''), it gives false positives
}); });
it('supports icons using an icon id', async () => { it('supports icons using an icon id', async () => {

View file

@ -18,30 +18,34 @@ describe('<lion-input-amount>', () => {
}); });
it('uses formatAmount for formatting', async () => { it('uses formatAmount for formatting', async () => {
const el = /** @type {LionInputAmount} */ (await fixture( const el = /** @type {LionInputAmount} */ (
`<lion-input-amount></lion-input-amount>`, await fixture(`<lion-input-amount></lion-input-amount>`)
)); );
expect(el.formatter).to.equal(formatAmount); expect(el.formatter).to.equal(formatAmount);
}); });
it('formatAmount uses currency provided on webcomponent', async () => { it('formatAmount uses currency provided on webcomponent', async () => {
// JOD displays 3 fraction digits by default // JOD displays 3 fraction digits by default
localize.locale = 'fr-FR'; localize.locale = 'fr-FR';
const el = /** @type {LionInputAmount} */ (await fixture( const el = /** @type {LionInputAmount} */ (
html`<lion-input-amount currency="JOD" .modelValue="${123}"></lion-input-amount>`, await fixture(
)); html`<lion-input-amount currency="JOD" .modelValue="${123}"></lion-input-amount>`,
)
);
expect(el.formattedValue).to.equal('123,000'); expect(el.formattedValue).to.equal('123,000');
}); });
it('formatAmount uses locale provided in formatOptions', async () => { it('formatAmount uses locale provided in formatOptions', async () => {
let el = /** @type {LionInputAmount} */ (await fixture( let el = /** @type {LionInputAmount} */ (
html` await fixture(
<lion-input-amount html`
.formatOptions="${{ locale: 'en-GB' }}" <lion-input-amount
.modelValue="${123}" .formatOptions="${{ locale: 'en-GB' }}"
></lion-input-amount> .modelValue="${123}"
`, ></lion-input-amount>
)); `,
)
);
expect(el.formattedValue).to.equal('123.00'); expect(el.formattedValue).to.equal('123.00');
el = await fixture( el = await fixture(
html` html`
@ -55,9 +59,11 @@ describe('<lion-input-amount>', () => {
}); });
it('ignores global locale change if property is provided', async () => { it('ignores global locale change if property is provided', async () => {
const el = /** @type {LionInputAmount} */ (await fixture(html` const el = /** @type {LionInputAmount} */ (
<lion-input-amount .modelValue=${123456.78} .locale="${'en-GB'}"></lion-input-amount> await fixture(html`
`)); <lion-input-amount .modelValue=${123456.78} .locale="${'en-GB'}"></lion-input-amount>
`)
);
expect(el.formattedValue).to.equal('123,456.78'); // British expect(el.formattedValue).to.equal('123,456.78'); // British
localize.locale = 'nl-NL'; localize.locale = 'nl-NL';
await aTimeout(0); await aTimeout(0);
@ -65,24 +71,24 @@ describe('<lion-input-amount>', () => {
}); });
it('uses parseAmount for parsing', async () => { it('uses parseAmount for parsing', async () => {
const el = /** @type {LionInputAmount} */ (await fixture( const el = /** @type {LionInputAmount} */ (
`<lion-input-amount></lion-input-amount>`, await fixture(`<lion-input-amount></lion-input-amount>`)
)); );
expect(el.parser).to.equal(parseAmount); expect(el.parser).to.equal(parseAmount);
}); });
it('sets inputmode attribute to decimal', async () => { it('sets inputmode attribute to decimal', async () => {
const el = /** @type {LionInputAmount} */ (await fixture( const el = /** @type {LionInputAmount} */ (
`<lion-input-amount></lion-input-amount>`, await fixture(`<lion-input-amount></lion-input-amount>`)
)); );
const { _inputNode } = getInputMembers(/** @type {* & LionInput} */ (el)); const { _inputNode } = getInputMembers(/** @type {* & LionInput} */ (el));
expect(_inputNode.getAttribute('inputmode')).to.equal('decimal'); expect(_inputNode.getAttribute('inputmode')).to.equal('decimal');
}); });
it('has type="text" to activate default keyboard on mobile with all necessary symbols', async () => { it('has type="text" to activate default keyboard on mobile with all necessary symbols', async () => {
const el = /** @type {LionInputAmount} */ (await fixture( const el = /** @type {LionInputAmount} */ (
`<lion-input-amount></lion-input-amount>`, await fixture(`<lion-input-amount></lion-input-amount>`)
)); );
const { _inputNode } = getInputMembers(/** @type {* & LionInput} */ (el)); const { _inputNode } = getInputMembers(/** @type {* & LionInput} */ (el));
expect(_inputNode.type).to.equal('text'); expect(_inputNode.type).to.equal('text');
}); });
@ -93,9 +99,9 @@ describe('<lion-input-amount>', () => {
}); });
it('displays currency if provided', async () => { it('displays currency if provided', async () => {
const el = /** @type {LionInputAmount} */ (await fixture( const el = /** @type {LionInputAmount} */ (
`<lion-input-amount currency="EUR"></lion-input-amount>`, await fixture(`<lion-input-amount currency="EUR"></lion-input-amount>`)
)); );
expect( expect(
/** @type {HTMLElement[]} */ (Array.from(el.children)).find(child => child.slot === 'after') /** @type {HTMLElement[]} */ (Array.from(el.children)).find(child => child.slot === 'after')
?.innerText, ?.innerText,
@ -104,9 +110,9 @@ describe('<lion-input-amount>', () => {
it('displays correct currency for TRY if locale is tr-TR', async () => { it('displays correct currency for TRY if locale is tr-TR', async () => {
localize.locale = 'tr-TR'; localize.locale = 'tr-TR';
const el = /** @type {LionInputAmount} */ (await fixture( const el = /** @type {LionInputAmount} */ (
`<lion-input-amount currency="TRY"></lion-input-amount>`, await fixture(`<lion-input-amount currency="TRY"></lion-input-amount>`)
)); );
expect( expect(
/** @type {HTMLElement[]} */ (Array.from(el.children)).find(child => child.slot === 'after') /** @type {HTMLElement[]} */ (Array.from(el.children)).find(child => child.slot === 'after')
?.innerText, ?.innerText,
@ -114,9 +120,9 @@ describe('<lion-input-amount>', () => {
}); });
it('can update currency', async () => { it('can update currency', async () => {
const el = /** @type {LionInputAmount} */ (await fixture( const el = /** @type {LionInputAmount} */ (
`<lion-input-amount currency="EUR"></lion-input-amount>`, await fixture(`<lion-input-amount currency="EUR"></lion-input-amount>`)
)); );
el.currency = 'USD'; el.currency = 'USD';
await el.updateComplete; await el.updateComplete;
expect( expect(
@ -126,9 +132,11 @@ describe('<lion-input-amount>', () => {
}); });
it('ignores currency if a suffix is already present', async () => { it('ignores currency if a suffix is already present', async () => {
const el = /** @type {LionInputAmount} */ (await fixture( const el = /** @type {LionInputAmount} */ (
`<lion-input-amount currency="EUR"><span slot="suffix">my-currency</span></lion-input-amount>`, await fixture(
)); `<lion-input-amount currency="EUR"><span slot="suffix">my-currency</span></lion-input-amount>`,
)
);
expect( expect(
/** @type {HTMLElement[]} */ (Array.from(el.children)).find(child => child.slot === 'suffix') /** @type {HTMLElement[]} */ (Array.from(el.children)).find(child => child.slot === 'suffix')
?.innerText, ?.innerText,
@ -143,18 +151,18 @@ describe('<lion-input-amount>', () => {
describe('Accessibility', () => { describe('Accessibility', () => {
it('adds currency id to aria-labelledby of input', async () => { it('adds currency id to aria-labelledby of input', async () => {
const el = /** @type {LionInputAmount} */ (await fixture( const el = /** @type {LionInputAmount} */ (
`<lion-input-amount currency="EUR"></lion-input-amount>`, await fixture(`<lion-input-amount currency="EUR"></lion-input-amount>`)
)); );
expect(el._currencyDisplayNode?.getAttribute('data-label')).to.be.not.null; expect(el._currencyDisplayNode?.getAttribute('data-label')).to.be.not.null;
const { _inputNode } = getInputMembers(/** @type {* & LionInput} */ (el)); const { _inputNode } = getInputMembers(/** @type {* & LionInput} */ (el));
expect(_inputNode.getAttribute('aria-labelledby')).to.contain(el._currencyDisplayNode?.id); expect(_inputNode.getAttribute('aria-labelledby')).to.contain(el._currencyDisplayNode?.id);
}); });
it('adds an aria-label to currency slot', async () => { it('adds an aria-label to currency slot', async () => {
const el = /** @type {LionInputAmount} */ (await fixture( const el = /** @type {LionInputAmount} */ (
`<lion-input-amount currency="EUR"></lion-input-amount>`, await fixture(`<lion-input-amount currency="EUR"></lion-input-amount>`)
)); );
expect(el._currencyDisplayNode?.getAttribute('aria-label')).to.equal('euros'); expect(el._currencyDisplayNode?.getAttribute('aria-label')).to.equal('euros');
el.currency = 'USD'; el.currency = 'USD';
await el.updateComplete; await el.updateComplete;

View file

@ -1,5 +1,6 @@
/* eslint-disable import/no-extraneous-dependencies */
import { LionCalendar } from '@lion/calendar'; import { LionCalendar } from '@lion/calendar';
import { html, ifDefined, ScopedElementsMixin } from '@lion/core'; import { html, ScopedElementsMixin, ifDefined, render } from '@lion/core';
import { LionInputDate } from '@lion/input-date'; import { LionInputDate } from '@lion/input-date';
import { import {
OverlayMixin, OverlayMixin,
@ -9,6 +10,10 @@ import {
} from '@lion/overlays'; } from '@lion/overlays';
import { LionCalendarOverlayFrame } from './LionCalendarOverlayFrame.js'; import { LionCalendarOverlayFrame } from './LionCalendarOverlayFrame.js';
/**
* @typedef {import('@lion/core').RenderOptions} RenderOptions
*/
/** /**
* @customElement lion-input-datepicker * @customElement lion-input-datepicker
*/ */
@ -62,13 +67,13 @@ export class LionInputDatepicker extends ScopedElementsMixin(
...super.slots, ...super.slots,
[this._calendarInvokerSlot]: () => { [this._calendarInvokerSlot]: () => {
const renderParent = document.createElement('div'); const renderParent = document.createElement('div');
/** @type {typeof LionInputDatepicker} */ (this.constructor).render( render(
this._invokerTemplate(), this._invokerTemplate(),
renderParent, renderParent,
{ /** @type {RenderOptions} */ ({
scopeName: this.localName, scopeName: this.localName,
eventContext: this, eventContext: this,
}, }),
); );
return /** @type {HTMLElement} */ (renderParent.firstElementChild); return /** @type {HTMLElement} */ (renderParent.firstElementChild);
}, },
@ -169,9 +174,9 @@ export class LionInputDatepicker extends ScopedElementsMixin(
* @protected * @protected
*/ */
get _calendarNode() { get _calendarNode() {
return /** @type {LionCalendar} */ (this._overlayCtrl.contentNode.querySelector( return /** @type {LionCalendar} */ (
'[slot="content"]', this._overlayCtrl.contentNode.querySelector('[slot="content"]')
)); );
} }
constructor() { constructor() {
@ -204,8 +209,8 @@ export class LionInputDatepicker extends ScopedElementsMixin(
* @param {PropertyKey} name * @param {PropertyKey} name
* @param {?} oldValue * @param {?} oldValue
*/ */
requestUpdateInternal(name, oldValue) { requestUpdate(name, oldValue) {
super.requestUpdateInternal(name, oldValue); super.requestUpdate(name, oldValue);
if (name === 'disabled' || name === 'readOnly') { if (name === 'disabled' || name === 'readOnly') {
this.__toggleInvokerDisabled(); this.__toggleInvokerDisabled();

View file

@ -57,13 +57,15 @@ export class DatepickerInputObject {
} }
get overlayHeadingEl() { get overlayHeadingEl() {
return /** @type {HTMLElement} */ (this.overlayEl && return /** @type {HTMLElement} */ (
this.overlayEl.shadowRoot?.querySelector('.calendar-overlay__heading')); this.overlayEl && this.overlayEl.shadowRoot?.querySelector('.calendar-overlay__heading')
);
} }
get overlayCloseButtonEl() { get overlayCloseButtonEl() {
return /** @type {HTMLElement} */ (this.calendarEl && return /** @type {HTMLElement} */ (
this.overlayEl.shadowRoot?.querySelector('#close-button')); this.calendarEl && this.overlayEl.shadowRoot?.querySelector('#close-button')
);
} }
get calendarEl() { get calendarEl() {

View file

@ -74,9 +74,9 @@ describe('<lion-input-datepicker>', () => {
const elObj = new DatepickerInputObject(el); const elObj = new DatepickerInputObject(el);
await elObj.openCalendar(); await elObj.openCalendar();
expect( expect(
/** @type {HTMLSlotElement} */ (elObj.overlayHeadingEl.querySelector( /** @type {HTMLSlotElement} */ (
'slot[name="heading"]', elObj.overlayHeadingEl.querySelector('slot[name="heading"]')
)).assignedNodes()[0], ).assignedNodes()[0],
).lightDom.to.equal('Pick your date'); ).lightDom.to.equal('Pick your date');
}); });
@ -90,9 +90,9 @@ describe('<lion-input-datepicker>', () => {
const elObj = new DatepickerInputObject(el); const elObj = new DatepickerInputObject(el);
await elObj.openCalendar(); await elObj.openCalendar();
expect( expect(
/** @type {HTMLSlotElement} */ (elObj.overlayHeadingEl.querySelector( /** @type {HTMLSlotElement} */ (
'slot[name="heading"]', elObj.overlayHeadingEl.querySelector('slot[name="heading"]')
)).assignedNodes()[0], ).assignedNodes()[0],
).lightDom.to.equal('foo'); ).lightDom.to.equal('foo');
}); });
@ -315,9 +315,9 @@ describe('<lion-input-datepicker>', () => {
const el = await fixture(html` const el = await fixture(html`
<lion-input-datepicker calendar-heading="foo"></lion-input-datepicker> <lion-input-datepicker calendar-heading="foo"></lion-input-datepicker>
`); `);
const calendarEl = /** @type {LionCalendar} */ (el.shadowRoot?.querySelector( const calendarEl = /** @type {LionCalendar} */ (
'[data-tag-name="lion-calendar"]', el.shadowRoot?.querySelector('lion-calendar')
)); );
const { dateSelectedByUser } = getProtectedMembersCalendar(calendarEl); const { dateSelectedByUser } = getProtectedMembersCalendar(calendarEl);
// First set a fixed date as if selected by a user // First set a fixed date as if selected by a user
dateSelectedByUser(new Date('December 17, 2020 03:24:00 GMT+0000')); dateSelectedByUser(new Date('December 17, 2020 03:24:00 GMT+0000'));
@ -342,9 +342,9 @@ describe('<lion-input-datepicker>', () => {
const el = await fixture(html` const el = await fixture(html`
<lion-input-datepicker calendar-heading="foo"></lion-input-datepicker> <lion-input-datepicker calendar-heading="foo"></lion-input-datepicker>
`); `);
const calendarEl = /** @type {LionCalendar} */ (el.shadowRoot?.querySelector( const calendarEl = /** @type {LionCalendar} */ (
'[data-tag-name="lion-calendar"]', el.shadowRoot?.querySelector('lion-calendar')
)); );
const { dateSelectedByUser } = getProtectedMembersCalendar(calendarEl); const { dateSelectedByUser } = getProtectedMembersCalendar(calendarEl);
// First set a fixed date as if selected by a user // First set a fixed date as if selected by a user
@ -356,9 +356,9 @@ describe('<lion-input-datepicker>', () => {
await elObj.openCalendar(); await elObj.openCalendar();
// Select the first date button, which is 29th of previous month (November) // Select the first date button, which is 29th of previous month (November)
const firstDateBtn = /** @type {HTMLButtonElement} */ (calendarEl?.shadowRoot?.querySelector( const firstDateBtn = /** @type {HTMLButtonElement} */ (
'.calendar__day-button', calendarEl?.shadowRoot?.querySelector('.calendar__day-button')
)); );
firstDateBtn.click(); firstDateBtn.click();
expect(/** @type {Date} */ (el.modelValue).getTime()).to.equal( expect(/** @type {Date} */ (el.modelValue).getTime()).to.equal(

View file

@ -1,4 +1,4 @@
/* eslint-disable max-classes-per-file */ /* eslint-disable max-classes-per-file, import/no-extraneous-dependencies */
import { localize } from '@lion/localize'; import { localize } from '@lion/localize';
import { Unparseable, Validator } from '@lion/form-core'; import { Unparseable, Validator } from '@lion/form-core';

Some files were not shown because too many files have changed in this diff Show more