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 = *
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
docs/_merged_*
patches/

View file

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

View file

@ -24,7 +24,10 @@ import { lazyRender } from './src/lazyRender.js';
export const main = () => html`
<lion-combobox name="combo" label="Default">
${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>
`;

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
@ -15,9 +16,14 @@ import { directive } from '@lion/core';
* )}
* </lion-combobox>
*/
export const lazyRender = directive(tplResult => part => {
export const lazyRender = directive(
class extends AsyncDirective {
render(tplResult) {
setTimeout(() => {
part.setValue(tplResult);
part.commit();
});
this.setValue(tplResult);
});
}
},
);
// export const lazyRender = () => {};

View file

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

View file

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

View file

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

View file

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

View file

@ -111,7 +111,8 @@ class PBoard extends DecorateMixin(LitElement) {
checked
@change="${({ target }) => {
// TODO: of course, logic depending on dom is never a good idea
const groupBoxes = target.parentElement.nextElementSibling.querySelectorAll(
const groupBoxes =
target.parentElement.nextElementSibling.querySelectorAll(
'input[type=checkbox]',
);
const { checked } = target;

View file

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

View file

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

View file

@ -30,7 +30,7 @@ export class ExtendedComp extends MyCompMixin(RefClass) {
static get properties() {}
static get styles() {}
get updateComplete() {}
requestUpdateInternal() {}
requestUpdate() {}
createRenderRoot() {}
render() {}
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 { getExtendDocsResults } = require('../../src/cli/launch-providence-with-extend-docs.js');
const {
pathsArrayFromCs,
pathsArrayFromCollectionName,
appendProjectDependencyPaths,
} = cliHelpersModule;
const { pathsArrayFromCs, pathsArrayFromCollectionName, appendProjectDependencyPaths } =
cliHelpersModule;
const queryResults = [];

View file

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

View file

@ -15,9 +15,8 @@ const {
restoreSuppressNonCriticalLogs,
} = require('../../../test-helpers/mock-log-service-helpers.js');
const findCustomelementsQueryConfig = QueryService.getQueryConfigFromAnalyzer(
'find-customelements',
);
const findCustomelementsQueryConfig =
QueryService.getQueryConfigFromAnalyzer('find-customelements');
const _providenceCfg = {
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 () => {
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 =
"```js ::import('./fixtures/three-sections-red.md', 'heading:has([value=Does not exit])')\n```";
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 =
"```js ::import('./fixtures/three-sections-red.md', 'heading:has([value=More Red])', 'heading:has([value=Does not exit])')\n```";
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
*/
__setupStore() {
const invokers = /** @type {HTMLElement[]} */ (Array.from(
this.querySelectorAll('[slot="invoker"]'),
));
const contents = /** @type {HTMLElement[]} */ (Array.from(
this.querySelectorAll('[slot="content"]'),
));
const invokers = /** @type {HTMLElement[]} */ (
Array.from(this.querySelectorAll('[slot="invoker"]'))
);
const contents = /** @type {HTMLElement[]} */ (
Array.from(this.querySelectorAll('[slot="content"]'))
);
if (invokers.length !== contents.length) {
// eslint-disable-next-line no-console
console.warn(
@ -356,9 +356,11 @@ export class LionAccordion extends LitElement {
if (!(this.__store && this.__store[this.focusedIndex])) {
return;
}
const previousInvoker = /** @type {HTMLElement | undefined} */ (Array.from(this.children).find(
const previousInvoker = /** @type {HTMLElement | undefined} */ (
Array.from(this.children).find(
child => child.slot === 'invoker' && child.firstElementChild?.hasAttribute('focused'),
));
)
);
if (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 '../lion-accordion.js';
@ -25,14 +26,16 @@ describe('<lion-accordion>', () => {
});
it('can programmatically set expanded', async () => {
const el = /** @type {LionAccordion} */ (await fixture(html`
const el = /** @type {LionAccordion} */ (
await fixture(html`
<lion-accordion .expanded=${[1]}>
<h2 slot="invoker"><button>invoker 1</button></h2>
<div slot="content">content 1</div>
<h2 slot="invoker"><button>invoker 2</button></h2>
<div slot="content">content 2</div>
</lion-accordion>
`));
`)
);
expect(el.expanded).to.deep.equal([1]);
expect(
Array.from(el.children).find(
@ -103,14 +106,16 @@ describe('<lion-accordion>', () => {
});
it('can programmatically set focusedIndex', async () => {
const el = /** @type {LionAccordion} */ (await fixture(html`
const el = /** @type {LionAccordion} */ (
await fixture(html`
<lion-accordion .focusedIndex=${1}>
<h2 slot="invoker"><button>invoker 1</button></h2>
<div slot="content">content 1</div>
<h2 slot="invoker"><button>invoker 2</button></h2>
<div slot="content">content 2</div>
</lion-accordion>
`));
`)
);
expect(el.focusedIndex).to.equal(1);
expect(
Array.from(el.children).find(
@ -214,7 +219,8 @@ describe('<lion-accordion>', () => {
});
it('selects previous invoker on [arrow-left] and [arrow-up]', async () => {
const el = /** @type {LionAccordion} */ (await fixture(html`
const el = /** @type {LionAccordion} */ (
await fixture(html`
<lion-accordion .focusedIndex=${1}>
<h2 slot="invoker"><button>invoker 1</button></h2>
<div slot="content">content 1</div>
@ -223,7 +229,8 @@ describe('<lion-accordion>', () => {
<h2 slot="invoker"><button>invoker 3</button></h2>
<div slot="content">content 3</div>
</lion-accordion>
`));
`)
);
const invokers = el.querySelectorAll('[slot=invoker]');
el.focusedIndex = 2;
invokers[2].firstElementChild?.dispatchEvent(
@ -237,14 +244,16 @@ describe('<lion-accordion>', () => {
});
it('selects first invoker on [home]', async () => {
const el = /** @type {LionAccordion} */ (await fixture(html`
const el = /** @type {LionAccordion} */ (
await fixture(html`
<lion-accordion .focusedIndex=${1}>
<h2 slot="invoker"><button>invoker 1</button></h2>
<div slot="content">content 1</div>
<h2 slot="invoker"><button>invoker 2</button></h2>
<div slot="content">content 2</div>
</lion-accordion>
`));
`)
);
const invokers = el.querySelectorAll('[slot=invoker]');
invokers[1].firstElementChild?.dispatchEvent(new KeyboardEvent('keydown', { key: 'Home' }));
expect(el.focusedIndex).to.equal(0);
@ -258,7 +267,8 @@ describe('<lion-accordion>', () => {
});
it('stays on last invoker on [arrow-right]', async () => {
const el = /** @type {LionAccordion} */ (await fixture(html`
const el = /** @type {LionAccordion} */ (
await fixture(html`
<lion-accordion focusedIndex="2">
<h2 slot="invoker"><button>invoker 1</button></h2>
<div slot="content">content 1</div>
@ -267,7 +277,8 @@ describe('<lion-accordion>', () => {
<h2 slot="invoker"><button>invoker 3</button></h2>
<div slot="content">content 3</div>
</lion-accordion>
`));
`)
);
const invokers = el.querySelectorAll('[slot=invoker]');
invokers[2].firstElementChild?.dispatchEvent(
new KeyboardEvent('keydown', { key: 'ArrowRight' }),
@ -276,7 +287,8 @@ describe('<lion-accordion>', () => {
});
it('stays on first invoker on [arrow-left]', async () => {
const el = /** @type {LionAccordion} */ (await fixture(html`
const el = /** @type {LionAccordion} */ (
await fixture(html`
<lion-accordion>
<h2 slot="invoker"><button>invoker 1</button></h2>
<div slot="content">content 1</div>
@ -285,7 +297,8 @@ describe('<lion-accordion>', () => {
<h2 slot="invoker"><button>invoker 3</button></h2>
<div slot="content">content 3</div>
</lion-accordion>
`));
`)
);
const invokers = el.querySelectorAll('[slot=invoker]');
invokers[0].firstElementChild?.dispatchEvent(
new KeyboardEvent('keydown', { key: 'ArrowLeft' }),
@ -338,12 +351,12 @@ describe('<lion-accordion>', () => {
el.append(content);
}
await el.updateComplete;
const invokers = /** @type {HTMLElement[]} */ (Array.from(
el.querySelectorAll('[slot=invoker]'),
));
const contents = /** @type {HTMLElement[]} */ (Array.from(
el.querySelectorAll('[slot=content]'),
));
const invokers = /** @type {HTMLElement[]} */ (
Array.from(el.querySelectorAll('[slot=invoker]'))
);
const contents = /** @type {HTMLElement[]} */ (
Array.from(el.querySelectorAll('[slot=content]'))
);
invokers.forEach((invoker, index) => {
const content = contents[index];
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 () => {
const el = /** @type {LionAccordion} */ (await fixture(html`
const el = /** @type {LionAccordion} */ (
await fixture(html`
<lion-accordion>
<h2 slot="invoker"><button>invoker</button></h2>
<div slot="content">content</div>
</lion-accordion>
`));
`)
);
el.expanded = [0];
expect(
Array.from(el.children).find(child => child.slot === 'invoker')?.firstElementChild,

View file

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

View file

@ -1,6 +1,7 @@
/* eslint-disable lit-a11y/click-events-have-key-events */
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 '@lion/core/differentKeyEventNamesShimIE';
import '@lion/button/define';
@ -37,9 +38,9 @@ describe('lion-button', () => {
});
it('sync type down to the native button', async () => {
const el = /** @type {LionButton} */ (await fixture(
`<lion-button type="button">foo</lion-button>`,
));
const el = /** @type {LionButton} */ (
await fixture(`<lion-button type="button">foo</lion-button>`)
);
const { nativeButtonNode } = getProtectedMembers(el);
expect(el.type).to.equal('button');
@ -175,9 +176,9 @@ describe('lion-button', () => {
});
it('does not override user provided role', async () => {
const el = /** @type {LionButton} */ (await fixture(
`<lion-button role="foo">foo</lion-button>`,
));
const el = /** @type {LionButton} */ (
await fixture(`<lion-button role="foo">foo</lion-button>`)
);
expect(el.getAttribute('role')).to.equal('foo');
});
@ -187,9 +188,9 @@ describe('lion-button', () => {
});
it('has a tabindex="-1" when disabled', async () => {
const el = /** @type {LionButton} */ (await fixture(
`<lion-button disabled>foo</lion-button>`,
));
const el = /** @type {LionButton} */ (
await fixture(`<lion-button disabled>foo</lion-button>`)
);
expect(el.getAttribute('tabindex')).to.equal('-1');
el.disabled = false;
await el.updateComplete;
@ -200,16 +201,16 @@ describe('lion-button', () => {
});
it('does not override user provided tabindex', async () => {
const el = /** @type {LionButton} */ (await fixture(
`<lion-button tabindex="5">foo</lion-button>`,
));
const el = /** @type {LionButton} */ (
await fixture(`<lion-button tabindex="5">foo</lion-button>`)
);
expect(el.getAttribute('tabindex')).to.equal('5');
});
it('disabled does not override user provided tabindex', async () => {
const el = /** @type {LionButton} */ (await fixture(
`<lion-button tabindex="5" disabled>foo</lion-button>`,
));
const el = /** @type {LionButton} */ (
await fixture(`<lion-button tabindex="5" disabled>foo</lion-button>`)
);
expect(el.getAttribute('tabindex')).to.equal('-1');
el.disabled = false;
await el.updateComplete;
@ -230,9 +231,9 @@ describe('lion-button', () => {
it('does not override aria-labelledby when provided by user', async () => {
const browserDetectionStub = sinon.stub(browserDetection, 'isIE11').value(true);
const el = /** @type {LionButton} */ (await fixture(
`<lion-button aria-labelledby="some-id another-id">foo</lion-button>`,
));
const el = /** @type {LionButton} */ (
await fixture(`<lion-button aria-labelledby="some-id another-id">foo</lion-button>`)
);
expect(el.getAttribute('aria-labelledby')).to.equal('some-id another-id');
browserDetectionStub.restore();
});
@ -244,15 +245,17 @@ describe('lion-button', () => {
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>`));
await expect(el).to.be.accessible();
});
it('is accessible when disabled', async () => {
const el = /** @type {LionButton} */ (await fixture(
`<lion-button disabled>foo</lion-button>`,
));
// TODO: enable when native button is not a child anymore
it.skip('is accessible when disabled', async () => {
const el = /** @type {LionButton} */ (
await fixture(`<lion-button disabled>foo</lion-button>`)
);
await expect(el).to.be.accessible({ ignoredRules: ['color-contrast'] });
});
});
@ -266,9 +269,9 @@ describe('lion-button', () => {
<lion-button type="submit">foo</lion-button>
</form>
`);
const button /** @type {LionButton} */ = /** @type {LionButton} */ (form.querySelector(
'lion-button',
));
const button /** @type {LionButton} */ = /** @type {LionButton} */ (
form.querySelector('lion-button')
);
button.click();
expect(formSubmitSpy).to.have.been.calledOnce;
});
@ -280,9 +283,9 @@ describe('lion-button', () => {
<lion-button type="submit">foo</lion-button>
</form>
`);
const button /** @type {LionButton} */ = /** @type {LionButton} */ (form.querySelector(
'lion-button',
));
const button /** @type {LionButton} */ = /** @type {LionButton} */ (
form.querySelector('lion-button')
);
button.dispatchEvent(new KeyboardEvent('keyup', { key: ' ' }));
await aTimeout(0);
await aTimeout(0);
@ -313,15 +316,15 @@ describe('lion-button', () => {
<lion-button type="reset">reset</lion-button>
</form>
`);
const btn /** @type {LionButton} */ = /** @type {LionButton} */ (form.querySelector(
'lion-button',
));
const firstName = /** @type {HTMLInputElement} */ (form.querySelector(
'input[name=firstName]',
));
const lastName = /** @type {HTMLInputElement} */ (form.querySelector(
'input[name=lastName]',
));
const btn /** @type {LionButton} */ = /** @type {LionButton} */ (
form.querySelector('lion-button')
);
const firstName = /** @type {HTMLInputElement} */ (
form.querySelector('input[name=firstName]')
);
const lastName = /** @type {HTMLInputElement} */ (
form.querySelector('input[name=lastName]')
);
firstName.value = 'Foo';
lastName.value = 'Bar';
@ -435,9 +438,9 @@ describe('lion-button', () => {
it('is fired once', async () => {
const clickSpy = /** @type {EventListener} */ (sinon.spy());
const el = /** @type {LionButton} */ (await fixture(
html` <lion-button @click="${clickSpy}">foo</lion-button> `,
));
const el = /** @type {LionButton} */ (
await fixture(html` <lion-button @click="${clickSpy}">foo</lion-button> `)
);
el.click();
@ -454,7 +457,8 @@ describe('lion-button', () => {
const formSpyEarly = /** @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} */ (
await fixture(
html`
<div @click="${outsideSpy}">
<form @click="${formSpyEarly}">
@ -464,7 +468,8 @@ describe('lion-button', () => {
</form>
</div>
`,
));
)
);
const lionButton = /** @type {LionButton} */ (el.querySelector('lion-button'));
const form = /** @type {HTMLFormElement} */ (el.querySelector('form'));
form.addEventListener('click', formSpyLater);
@ -482,13 +487,15 @@ describe('lion-button', () => {
});
it('works when connected to different form', async () => {
const form1El = /** @type {HTMLFormElement} */ (await fixture(
const form1El = /** @type {HTMLFormElement} */ (
await fixture(
html`
<form>
<lion-button>foo</lion-button>
</form>
`,
));
)
);
const lionButton = /** @type {LionButton} */ (form1El.querySelector('lion-button'));
expect(lionButton._form).to.equal(form1El);
@ -500,7 +507,8 @@ describe('lion-button', () => {
const formSpyEarly = /** @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} */ (
await fixture(
html`
<div @click="${outsideSpy}">
<form @click="${formSpyEarly}">
@ -508,7 +516,8 @@ describe('lion-button', () => {
</form>
</div>
`,
));
)
);
const form2Node = /** @type {HTMLFormElement} */ (form2El.querySelector('form'));
expect(lionButton._form).to.equal(form2Node);
@ -534,9 +543,9 @@ describe('lion-button', () => {
before(async () => {
const nativeButtonEl = /** @type {LionButton} */ (await fixture('<button>foo</button>'));
const lionButtonEl = /** @type {LionButton} */ (await fixture(
'<lion-button>foo</lion-button>',
));
const lionButtonEl = /** @type {LionButton} */ (
await fixture('<lion-button>foo</lion-button>')
);
nativeButtonEvent = await prepareClickEvent(nativeButtonEl);
lionButtonEvent = await prepareClickEvent(lionButtonEl);
});
@ -578,9 +587,9 @@ describe('lion-button', () => {
const targetName = 'host';
it(`is ${targetName} with type ${type} and it is inside a ${container}`, async () => {
const clickSpy = /** @type {EventListener} */ (sinon.spy(e => e.preventDefault()));
const el = /** @type {LionButton} */ (await fixture(
`<lion-button type="${type}">foo</lion-button>`,
));
const el = /** @type {LionButton} */ (
await fixture(`<lion-button type="${type}">foo</lion-button>`)
);
const tag = unsafeStatic(container);
await fixture(html`<${tag} @click="${clickSpy}">${el}</${tag}>`);
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 {
getMonthNames,
@ -224,9 +225,9 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
}
focusCentralDate() {
const button = /** @type {HTMLElement} */ (this.shadowRoot?.querySelector(
'button[tabindex="0"]',
));
const button = /** @type {HTMLElement} */ (
this.shadowRoot?.querySelector('button[tabindex="0"]')
);
button.focus();
this.__focusedDate = this.centralDate;
}
@ -267,9 +268,9 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
* we can guard against adding events twice
*/
if (!this.__eventsAdded) {
this.__contentWrapperElement = /** @type {HTMLButtonElement} */ (this.shadowRoot?.getElementById(
'js-content-wrapper',
));
this.__contentWrapperElement = /** @type {HTMLButtonElement} */ (
this.shadowRoot?.getElementById('js-content-wrapper')
);
this.__contentWrapperElement.addEventListener('click', this.__boundClickDateDelegation);
this.__contentWrapperElement.addEventListener('focus', this.__boundFocusDateDelegation);
this.__contentWrapperElement.addEventListener('blur', this.__boundBlurDateDelegation);
@ -305,8 +306,8 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
* @param {string} name
* @param {?} oldValue
*/
requestUpdateInternal(name, oldValue) {
super.requestUpdateInternal(name, oldValue);
requestUpdate(name, oldValue) {
super.requestUpdate(name, oldValue);
const map = {
disableDates: () => this.__disableDatesChanged(),
@ -740,8 +741,9 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
!this.__focusedDate &&
isDayButton(/** @type {HTMLElement} el */ (this.shadowRoot?.activeElement))
) {
this.__focusedDate = /** @type {HTMLButtonElement & { date: Date }} */ (this.shadowRoot
?.activeElement).date;
this.__focusedDate = /** @type {HTMLButtonElement & { date: Date }} */ (
this.shadowRoot?.activeElement
).date;
}
}

View file

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

View file

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

View file

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

View file

@ -11,9 +11,8 @@ function compareMultipleMonth(obj) {
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
// eslint-disable-next-line no-param-reassign
obj.months[monthi].weeks[weeki].days[dayi].date = obj.months[monthi].weeks[weeki].days[
dayi
].date.toISOString();
obj.months[monthi].weeks[weeki].days[dayi].date =
obj.months[monthi].weeks[weeki].days[dayi].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 { 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';
/**

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 '@lion/checkbox-group/define';
@ -46,9 +47,9 @@ describe('<lion-checkbox-indeterminate>', () => {
</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
expect(elIndeterminate?.hasAttribute('indeterminate')).to.be.false;
@ -65,9 +66,9 @@ describe('<lion-checkbox-indeterminate>', () => {
</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
expect(elIndeterminate?.hasAttribute('indeterminate')).to.be.true;
@ -75,7 +76,8 @@ describe('<lion-checkbox-indeterminate>', () => {
it('should be checked if all children are checked', async () => {
// Arrange
const el = /** @type {LionCheckboxGroup} */ (await fixture(html`
const el = /** @type {LionCheckboxGroup} */ (
await fixture(html`
<lion-checkbox-group name="scientists[]">
<lion-checkbox-indeterminate label="Favorite scientists">
<lion-checkbox slot="checkbox" label="Archimedes" checked></lion-checkbox>
@ -83,10 +85,11 @@ describe('<lion-checkbox-indeterminate>', () => {
<lion-checkbox slot="checkbox" label="Marie Curie" checked></lion-checkbox>
</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
expect(elIndeterminate?.hasAttribute('indeterminate')).to.be.false;
@ -95,7 +98,8 @@ describe('<lion-checkbox-indeterminate>', () => {
it('should become indeterminate if one child is checked', async () => {
// Arrange
const el = /** @type {LionCheckboxGroup} */ (await fixture(html`
const el = /** @type {LionCheckboxGroup} */ (
await fixture(html`
<lion-checkbox-group name="scientists[]">
<lion-checkbox-indeterminate label="Favorite scientists">
<lion-checkbox slot="checkbox" label="Archimedes"></lion-checkbox>
@ -103,10 +107,11 @@ describe('<lion-checkbox-indeterminate>', () => {
<lion-checkbox slot="checkbox" label="Marie Curie"></lion-checkbox>
</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);
@ -120,7 +125,8 @@ describe('<lion-checkbox-indeterminate>', () => {
it('should become checked if all children are checked', async () => {
// Arrange
const el = /** @type {LionCheckboxGroup} */ (await fixture(html`
const el = /** @type {LionCheckboxGroup} */ (
await fixture(html`
<lion-checkbox-group name="scientists[]">
<lion-checkbox-indeterminate label="Favorite scientists">
<lion-checkbox slot="checkbox" label="Archimedes"></lion-checkbox>
@ -128,10 +134,11 @@ describe('<lion-checkbox-indeterminate>', () => {
<lion-checkbox slot="checkbox" label="Marie Curie"></lion-checkbox>
</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);
// Act
@ -147,7 +154,8 @@ describe('<lion-checkbox-indeterminate>', () => {
it('should sync all children when parent is checked (from indeterminate to checked)', async () => {
// Arrange
const el = /** @type {LionCheckboxGroup} */ (await fixture(html`
const el = /** @type {LionCheckboxGroup} */ (
await fixture(html`
<lion-checkbox-group name="scientists[]">
<lion-checkbox-indeterminate label="Favorite scientists">
<lion-checkbox slot="checkbox" label="Archimedes"></lion-checkbox>
@ -155,10 +163,11 @@ describe('<lion-checkbox-indeterminate>', () => {
<lion-checkbox slot="checkbox" label="Marie Curie"></lion-checkbox>
</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);
// Act
@ -174,7 +183,8 @@ describe('<lion-checkbox-indeterminate>', () => {
it('should sync all children when parent is checked (from unchecked to checked)', async () => {
// Arrange
const el = /** @type {LionCheckboxGroup} */ (await fixture(html`
const el = /** @type {LionCheckboxGroup} */ (
await fixture(html`
<lion-checkbox-group name="scientists[]">
<lion-checkbox-indeterminate label="Favorite scientists">
<lion-checkbox slot="checkbox" label="Archimedes"></lion-checkbox>
@ -182,10 +192,11 @@ describe('<lion-checkbox-indeterminate>', () => {
<lion-checkbox slot="checkbox" label="Marie Curie"></lion-checkbox>
</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);
// Act
@ -201,7 +212,8 @@ describe('<lion-checkbox-indeterminate>', () => {
it('should sync all children when parent is checked (from checked to unchecked)', async () => {
// Arrange
const el = /** @type {LionCheckboxGroup} */ (await fixture(html`
const el = /** @type {LionCheckboxGroup} */ (
await fixture(html`
<lion-checkbox-group name="scientists[]">
<lion-checkbox-indeterminate label="Favorite scientists">
<lion-checkbox slot="checkbox" label="Archimedes" checked></lion-checkbox>
@ -209,10 +221,11 @@ describe('<lion-checkbox-indeterminate>', () => {
<lion-checkbox slot="checkbox" label="Marie Curie" checked></lion-checkbox>
</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);
// Act
@ -228,9 +241,13 @@ describe('<lion-checkbox-indeterminate>', () => {
it('should work as expected with siblings checkbox-indeterminate', async () => {
// Arrange
const el = /** @type {LionCheckboxGroup} */ (await fixture(html`
const el = /** @type {LionCheckboxGroup} */ (
await fixture(html`
<lion-checkbox-group name="scientists[]" label="Favorite scientists">
<lion-checkbox-indeterminate label="Old Greek scientists" id="first-checkbox-indeterminate">
<lion-checkbox-indeterminate
label="Old Greek scientists"
id="first-checkbox-indeterminate"
>
<lion-checkbox
slot="checkbox"
label="Archimedes"
@ -259,14 +276,15 @@ describe('<lion-checkbox-indeterminate>', () => {
></lion-checkbox>
</lion-checkbox-indeterminate>
</lion-checkbox-group>
`));
const elFirstIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector(
'#first-checkbox-indeterminate',
));
`)
);
const elFirstIndeterminate = /** @type {LionCheckboxIndeterminate} */ (
el.querySelector('#first-checkbox-indeterminate')
);
const elSecondIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector(
'#second-checkbox-indeterminate',
));
const elSecondIndeterminate = /** @type {LionCheckboxIndeterminate} */ (
el.querySelector('#second-checkbox-indeterminate')
);
const elFirstSubCheckboxes = getCheckboxIndeterminateMembers(elFirstIndeterminate);
const elSecondSubCheckboxes = getCheckboxIndeterminateMembers(elSecondIndeterminate);
@ -289,7 +307,8 @@ describe('<lion-checkbox-indeterminate>', () => {
it('should work as expected with nested indeterminate checkboxes', async () => {
// Arrange
const el = /** @type {LionCheckboxGroup} */ (await fixture(html`
const el = /** @type {LionCheckboxGroup} */ (
await fixture(html`
<lion-checkbox-group name="scientists[]" label="Favorite scientists">
<lion-checkbox-indeterminate label="Scientists" id="parent-checkbox-indeterminate">
<lion-checkbox
@ -321,13 +340,14 @@ describe('<lion-checkbox-indeterminate>', () => {
</lion-checkbox-indeterminate>
</lion-checkbox-indeterminate>
</lion-checkbox-group>
`));
const elNestedIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector(
'#nested-checkbox-indeterminate',
));
const elParentIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector(
'#parent-checkbox-indeterminate',
));
`)
);
const elNestedIndeterminate = /** @type {LionCheckboxIndeterminate} */ (
el.querySelector('#nested-checkbox-indeterminate')
);
const elParentIndeterminate = /** @type {LionCheckboxIndeterminate} */ (
el.querySelector('#parent-checkbox-indeterminate')
);
const elNestedSubCheckboxes = getCheckboxIndeterminateMembers(elNestedIndeterminate);
const elParentSubCheckboxes = getCheckboxIndeterminateMembers(elParentIndeterminate);
@ -375,7 +395,8 @@ describe('<lion-checkbox-indeterminate>', () => {
it('should work as expected if extra html', async () => {
// Arrange
const el = /** @type {LionCheckboxGroup} */ (await fixture(html`
const el = /** @type {LionCheckboxGroup} */ (
await fixture(html`
<lion-checkbox-group name="scientists[]">
<div>
Let's have some fun
@ -390,10 +411,11 @@ describe('<lion-checkbox-indeterminate>', () => {
</div>
<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);
// 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';
/**
@ -14,9 +15,9 @@ describe('<lion-checkbox>', () => {
});
it('can be reset when unchecked by default', async () => {
const el = /** @type {LionCheckbox} */ (await fixture(html`
<lion-checkbox name="checkbox" .choiceValue=${'male'}></lion-checkbox>
`));
const el = /** @type {LionCheckbox} */ (
await fixture(html` <lion-checkbox name="checkbox" .choiceValue=${'male'}></lion-checkbox> `)
);
expect(el._initialModelValue).to.deep.equal({ value: 'male', checked: false });
el.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 () => {
const el = /** @type {LionCheckbox} */ (await fixture(html`
const el = /** @type {LionCheckbox} */ (
await fixture(html`
<lion-checkbox name="checkbox" .choiceValue=${'male'} checked></lion-checkbox>
`));
`)
);
expect(el._initialModelValue).to.deep.equal({ value: 'male', checked: true });
el.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';
@ -53,7 +54,7 @@ describe('<lion-collapsible>', () => {
it('has [opened] on current expanded invoker which serves as styling hook', async () => {
const collapsible = await fixture(defaultCollapsible);
collapsible.opened = true;
await collapsible.requestUpdate();
await collapsible.updateComplete;
expect(collapsible).to.have.attribute('opened');
});
@ -62,7 +63,7 @@ describe('<lion-collapsible>', () => {
const collHeight1 = getProtectedMembers(collapsible);
expect(collHeight1.contentHeight).to.equal('0px');
collapsible.show();
await collapsible.requestUpdate();
await collapsible.updateComplete;
const collHeight2 = getProtectedMembers(collapsible);
expect(collHeight2.contentHeight).to.equal('32px');
});
@ -93,10 +94,10 @@ describe('<lion-collapsible>', () => {
it('should listen to the open and close state change', async () => {
const collapsible = await fixture(collapsibleWithEvents);
collapsible.show();
await collapsible.requestUpdate();
await collapsible.updateComplete;
expect(isCollapsibleOpen).to.equal(true);
collapsible.hide();
await collapsible.requestUpdate();
await collapsible.updateComplete;
expect(isCollapsibleOpen).to.equal(false);
});
});
@ -131,7 +132,7 @@ describe('<lion-collapsible>', () => {
const collapsibleElement = await fixture(defaultCollapsible);
const invoker = collapsibleElement.querySelector('[slot=invoker]');
collapsibleElement.opened = true;
await collapsibleElement.requestUpdate();
await collapsibleElement.updateComplete;
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);

View file

@ -218,8 +218,10 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
* @protected
*/
get _listboxNode() {
return /** @type {LionOptions} */ ((this._overlayCtrl && this._overlayCtrl.contentNode) ||
Array.from(this.children).find(child => child.slot === 'listbox'));
return /** @type {LionOptions} */ (
(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 {unknown} oldValue
*/
requestUpdateInternal(name, oldValue) {
super.requestUpdateInternal(name, oldValue);
requestUpdate(name, oldValue) {
super.requestUpdate(name, oldValue);
if (name === 'disabled' || name === 'readOnly') {
this.__setComboboxDisabledAndReadOnly();
}
@ -514,9 +516,8 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
phase: 'overlay-close',
})
) {
this._inputNode.value = this.formElements[
/** @type {number} */ (this.checkedIndex)
].choiceValue;
this._inputNode.value =
this.formElements[/** @type {number} */ (this.checkedIndex)].choiceValue;
}
} else {
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
if (!hasAutoFilled && autoselect && !this.multipleChoice) {
if (autoselect && !hasAutoFilled && !this.multipleChoice) {
// This means there is no match for checkedIndex
this.checkedIndex = -1;
}
@ -771,7 +772,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
*/
_setupOverlayCtrl() {
super._setupOverlayCtrl();
this.__initFilterListbox();
this.__shouldAutocompleteNextUpdate = true;
this.__setupCombobox();
}
@ -863,13 +864,6 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
}
}
/**
* @private
*/
__initFilterListbox() {
this._handleAutocompletion();
}
/**
* @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 { asyncReplace } from 'lit-html/directives/async-replace.js';
export { cache } from 'lit-html/directives/cache.js';
export { classMap } from 'lit-html/directives/class-map.js';
export { guard } from 'lit-html/directives/guard.js';
export { ifDefined } from 'lit-html/directives/if-defined.js';
export { repeat } from 'lit-html/directives/repeat.js';
export { styleMap } from 'lit-html/directives/style-map.js';
export { unsafeHTML } from 'lit-html/directives/unsafe-html.js';
export { until } from 'lit-html/directives/until.js';
export { render as renderShady } from 'lit-html/lib/shady-render.js';
export {
html,
CSSResult,
adoptStyles,
css,
getCompatibleStyle,
supportsAdoptingStyleSheets,
unsafeCSS,
UpdatingElement,
notEqual,
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 { dedupeMixin } from '@open-wc/dedupe-mixin';
// ours
export { DelegateMixin } from './src/DelegateMixin.js';
export { DisabledMixin } from './src/DisabledMixin.js';
export { DisabledWithTabIndexMixin } from './src/DisabledWithTabIndexMixin.js';
@ -18,39 +84,3 @@ export { SlotMixin } from './src/SlotMixin.js';
export { UpdateStylesMixin } from './src/UpdateStylesMixin.js';
export { browserDetection } from './src/browserDetection.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 {
css,
html,
CSSResult,
// decorators.js
customElement,
// updating-element.js
defaultConverter,
eventOptions,
LitElement,
notEqual,
property,
query,
queryAll,
// css-tag.js
adoptStyles,
css,
getCompatibleStyle,
supportsAdoptingStyleSheets,
unsafeCSS,
UpdatingElement,
} from 'lit-element';
// lit-html
export {
AttributePart,
BooleanAttributePart,
directive,
EventPart,
html,
isDirective,
isPrimitive,
noChange,
NodePart,
nothing,
PropertyPart,
render,
notEqual,
ReactiveElement,
svg,
SVGTemplateResult,
TemplateResult,
reparentNodes,
removeNodes,
} from 'lit-html';
export { asyncAppend } from 'lit-html/directives/async-append.js';
export { asyncReplace } from 'lit-html/directives/async-replace.js';
export { cache } from 'lit-html/directives/cache.js';
export { classMap } from 'lit-html/directives/class-map.js';
export { guard } from 'lit-html/directives/guard.js';
export { ifDefined } from 'lit-html/directives/if-defined.js';
export { repeat } from 'lit-html/directives/repeat.js';
export { styleMap } from 'lit-html/directives/style-map.js';
export { unsafeHTML } from 'lit-html/directives/unsafe-html.js';
export { until } from 'lit-html/directives/until.js';
export { render as renderShady } from 'lit-html/lib/shady-render.js';
noChange,
nothing,
render,
LitElement,
defaultConverter,
} from 'lit';
export {
customElement,
property,
state,
eventOptions,
query,
queryAll,
queryAsync,
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
export { ScopedElementsMixin } from '@open-wc/scoped-elements';
export { dedupeMixin } from '@open-wc/dedupe-mixin';

View file

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

View file

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

View file

@ -62,8 +62,8 @@ const DisabledWithTabIndexMixinImplementation = superclass =>
* @param {PropertyKey} name
* @param {?} oldValue
*/
requestUpdateInternal(name, oldValue) {
super.requestUpdateInternal(name, oldValue);
requestUpdate(name, oldValue) {
super.requestUpdate(name, oldValue);
if (name === '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 { LitElement } from '../index.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 cb = sinon.spy();
element.addEventListener('click', cb);
const childEl = /** @type {HTMLElement} */ (Array.from(element.children)?.find(
child => child.slot === 'button',
));
const childEl = /** @type {HTMLElement} */ (
Array.from(element.children)?.find(child => child.slot === 'button')
);
childEl?.click();
expect(cb.callCount).to.equal(1);
});
@ -343,14 +344,14 @@ describe('DelegateMixin', () => {
const tagName = unsafeStatic(tag);
// Here, the Application Developerd tries to set the type via attribute
const elementAttr = /** @type {ScheduledElement} */ (await fixture(
`<${tag} type="radio"></${tag}>`,
));
const elementAttr = /** @type {ScheduledElement} */ (
await fixture(`<${tag} type="radio"></${tag}>`)
);
expect(elementAttr.scheduledElement?.type).to.equal('radio');
// Here, the Application Developer tries to set the type via property
const elementProp = /** @type {ScheduledElement} */ (await fixture(
html`<${tagName} .type=${'radio'}></${tagName}>`,
));
const elementProp = /** @type {ScheduledElement} */ (
await fixture(html`<${tagName} .type=${'radio'}></${tagName}>`)
);
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 { DisabledMixin } from '../src/DisabledMixin.js';
@ -9,9 +10,9 @@ describe('DisabledMixin', () => {
});
it('reflects disabled to attribute', async () => {
const el = /** @type {CanBeDisabled} */ (await fixture(
html`<can-be-disabled></can-be-disabled>`,
));
const el = /** @type {CanBeDisabled} */ (
await fixture(html`<can-be-disabled></can-be-disabled>`)
);
expect(el.hasAttribute('disabled')).to.be.false;
el.makeRequestToBeDisabled();
el.disabled = true;
@ -20,9 +21,9 @@ describe('DisabledMixin', () => {
});
it('can be requested to be disabled', async () => {
const el = /** @type {CanBeDisabled} */ (await fixture(
html`<can-be-disabled></can-be-disabled>`,
));
const el = /** @type {CanBeDisabled} */ (
await fixture(html`<can-be-disabled></can-be-disabled>`)
);
el.makeRequestToBeDisabled();
expect(el.disabled).to.be.true;
await el.updateComplete;
@ -30,9 +31,9 @@ describe('DisabledMixin', () => {
});
it('will not allow to become enabled after makeRequestToBeDisabled()', async () => {
const el = /** @type {CanBeDisabled} */ (await fixture(
html`<can-be-disabled></can-be-disabled>`,
));
const el = /** @type {CanBeDisabled} */ (
await fixture(html`<can-be-disabled></can-be-disabled>`)
);
el.makeRequestToBeDisabled();
expect(el.disabled).to.be.true;
@ -41,18 +42,18 @@ describe('DisabledMixin', () => {
});
it('will stay disabled after retractRequestToBeDisabled() if it was disabled before', async () => {
const el = /** @type {CanBeDisabled} */ (await fixture(
html`<can-be-disabled disabled></can-be-disabled>`,
));
const el = /** @type {CanBeDisabled} */ (
await fixture(html`<can-be-disabled disabled></can-be-disabled>`)
);
el.makeRequestToBeDisabled();
el.retractRequestToBeDisabled();
expect(el.disabled).to.be.true;
});
it('will become enabled after retractRequestToBeDisabled() if it was enabled before', async () => {
const el = /** @type {CanBeDisabled} */ (await fixture(
html`<can-be-disabled></can-be-disabled>`,
));
const el = /** @type {CanBeDisabled} */ (
await fixture(html`<can-be-disabled></can-be-disabled>`)
);
el.makeRequestToBeDisabled();
expect(el.disabled).to.be.true;
el.retractRequestToBeDisabled();
@ -60,9 +61,9 @@ describe('DisabledMixin', () => {
});
it('may allow multiple calls to makeRequestToBeDisabled()', async () => {
const el = /** @type {CanBeDisabled} */ (await fixture(
html`<can-be-disabled></can-be-disabled>`,
));
const el = /** @type {CanBeDisabled} */ (
await fixture(html`<can-be-disabled></can-be-disabled>`)
);
el.makeRequestToBeDisabled();
el.makeRequestToBeDisabled();
el.retractRequestToBeDisabled();
@ -70,9 +71,9 @@ describe('DisabledMixin', () => {
});
it('will restore last state after retractRequestToBeDisabled()', async () => {
const el = /** @type {CanBeDisabled} */ (await fixture(
html`<can-be-disabled></can-be-disabled>`,
));
const el = /** @type {CanBeDisabled} */ (
await fixture(html`<can-be-disabled></can-be-disabled>`)
);
el.makeRequestToBeDisabled();
el.disabled = true;
el.retractRequestToBeDisabled();

View file

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

View file

@ -108,9 +108,9 @@ describe('SlotMixin', () => {
const tag = defineCE(SlotPrivateText);
const el = /** @type {SlotPrivateText} */ (await fixture(`<${tag}><${tag}>`));
expect(el.didCreateConditionalSlot()).to.be.true;
const elUserSlot = /** @type {SlotPrivateText} */ (await fixture(
`<${tag}><p slot="conditional">foo</p><${tag}>`,
));
const elUserSlot = /** @type {SlotPrivateText} */ (
await fixture(`<${tag}><p slot="conditional">foo</p><${tag}>`)
);
expect(elUserSlot.didCreateConditionalSlot()).to.be.false;
renderSlot = false;
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 { 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 '@lion/dialog/define';
@ -62,9 +63,9 @@ describe('lion-dialog', () => {
el._overlayInvokerNode.click();
expect(el.opened).to.be.true;
const overlaysContainer = /** @type {HTMLElement} */ (document.querySelector(
'.global-overlays',
));
const overlaysContainer = /** @type {HTMLElement} */ (
document.querySelector('.global-overlays')
);
const wrapperNode = Array.from(overlaysContainer.children)[1];
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

View file

@ -1,6 +1,7 @@
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();
/**

View file

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

View file

@ -72,8 +72,8 @@ const FormatMixinImplementation = superclass =>
* @param {string} name
* @param {any} oldVal
*/
requestUpdateInternal(name, oldVal) {
super.requestUpdateInternal(name, oldVal);
requestUpdate(name, oldVal) {
super.requestUpdate(name, oldVal);
if (name === 'modelValue' && this.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(
this.formatOn,
/** @type {EventListenerOrEventListenerObject} */ (this
._reflectBackFormattedValueDebounced),
/** @type {EventListenerOrEventListenerObject} */ (
this._reflectBackFormattedValueDebounced
),
);
this._inputNode.removeEventListener('compositionstart', this.__onCompositionEvent);
this._inputNode.removeEventListener('compositionend', this.__onCompositionEvent);

View file

@ -35,14 +35,14 @@ const InteractionStateMixinImplementation = superclass =>
* @param {PropertyKey} name
* @param {*} oldVal
*/
requestUpdateInternal(name, oldVal) {
super.requestUpdateInternal(name, oldVal);
requestUpdate(name, oldVal) {
super.requestUpdate(name, oldVal);
if (name === 'touched' && this.touched !== oldVal) {
this._onTouchedChanged();
}
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.
this.filled = !this._isEmpty();
}

View file

@ -53,8 +53,8 @@ const ChoiceInputMixinImplementation = superclass =>
* @param {string} name
* @param {any} oldValue
*/
requestUpdateInternal(name, oldValue) {
super.requestUpdateInternal(name, oldValue);
requestUpdate(name, oldValue) {
super.requestUpdate(name, oldValue);
if (name === 'modelValue') {
if (this.modelValue.checked !== this.checked) {
@ -298,7 +298,7 @@ const ChoiceInputMixinImplementation = superclass =>
/**
* @override
* hasChanged is designed for async (updated) callback, also check for sync
* (requestUpdateInternal) callback
* (requestUpdate) callback
* @param {{ modelValue:unknown }} newV
* @param {{ modelValue:unknown }} [old]
* @protected
@ -309,7 +309,7 @@ const ChoiceInputMixinImplementation = superclass =>
_old = old.modelValue;
}
// @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 });
}
}

View file

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

View file

@ -31,13 +31,13 @@ const FormRegistrarPortalMixinImplementation = superclass =>
* @type {(FormRegistrarPortalHost & HTMLElement) | undefined}
*/
this.registrationTarget = undefined;
this.__redispatchEventForFormRegistrarPortalMixin = this.__redispatchEventForFormRegistrarPortalMixin.bind(
this,
);
this.__redispatchEventForFormRegistrarPortalMixin =
this.__redispatchEventForFormRegistrarPortalMixin.bind(this);
this.addEventListener(
'form-element-register',
/** @type {EventListenerOrEventListenerObject} */ (this
.__redispatchEventForFormRegistrarPortalMixin),
/** @type {EventListenerOrEventListenerObject} */ (
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.
* 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.
* 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
* @type {SyncUpdatableMixin}
* @param {import('@open-wc/dedupe-mixin').Constructor<import('@lion/core').LitElement>} superclass
@ -64,7 +64,7 @@ const SyncUpdatableMixinImplementation = superclass =>
*/
static __syncUpdatableHasChanged(name, newValue, oldValue) {
// @ts-expect-error [external]: accessing private lit property
const properties = this._classProperties;
const properties = this.elementProperties;
if (properties.get(name) && properties.get(name).hasChanged) {
return properties.get(name).hasChanged(newValue, oldValue);
}
@ -74,8 +74,10 @@ const SyncUpdatableMixinImplementation = superclass =>
/** @private */
__syncUpdatableInitialize() {
const ns = this.__SyncUpdatableNamespace;
const ctor = /** @type {typeof SyncUpdatableMixin & typeof import('../../types/utils/SyncUpdatableMixinTypes').SyncUpdatableHost} */ (this
.constructor);
const ctor =
/** @type {typeof SyncUpdatableMixin & typeof import('../../types/utils/SyncUpdatableMixinTypes').SyncUpdatableHost} */ (
this.constructor
);
ns.initialized = true;
// Empty queue...
@ -93,14 +95,16 @@ const SyncUpdatableMixinImplementation = superclass =>
* @param {string} name
* @param {*} oldValue
*/
requestUpdateInternal(name, oldValue) {
super.requestUpdateInternal(name, oldValue);
requestUpdate(name, oldValue) {
super.requestUpdate(name, oldValue);
this.__SyncUpdatableNamespace = this.__SyncUpdatableNamespace || {};
const ns = this.__SyncUpdatableNamespace;
const ctor = /** @type {typeof SyncUpdatableMixin & typeof import('../../types/utils/SyncUpdatableMixinTypes').SyncUpdatableHost} */ (this
.constructor);
const ctor =
/** @type {typeof SyncUpdatableMixin & typeof import('../../types/utils/SyncUpdatableMixinTypes').SyncUpdatableHost} */ (
this.constructor
);
// Before connectedCallback: queue
if (!ns.initialized) {
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:
* - [member order independence](https://github.com/webcomponents/gold-standard/wiki/Member-Order-Independence)
* - 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
* run property effects / events when no change happened
* 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 {*} oldValue
*/

View file

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

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 {
static get validatorName() {
return 'IsEmail';

View file

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

View file

@ -1,6 +1,8 @@
import { LitElement } from '@lion/core';
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 { FormatMixin } from '../src/FormatMixin.js';
import { Unparseable, Validator } from '../index.js';
@ -95,7 +97,7 @@ export function runFormatMixinSuite(customConfig) {
}
describe('FormatMixin', async () => {
/** @type {{d: any}} */
/** @type {{_$litStatic$: any}} */
let tag;
/** @type {FormatClass} */
let nonFormat;
@ -148,9 +150,9 @@ export function runFormatMixinSuite(customConfig) {
*/
describe('ModelValue', () => {
it('fires `model-value-changed` for every programmatic modelValue change', async () => {
const el = /** @type {FormatClass} */ (await fixture(
html`<${tag}><input slot="input"></${tag}>`,
));
const el = /** @type {FormatClass} */ (
await fixture(html`<${tag}><input slot="input"></${tag}>`)
);
let counter = 0;
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 () => {
const formatEl = /** @type {FormatClass} */ (await fixture(
html`<${tag}><input slot="input"></${tag}>`,
));
const formatEl = /** @type {FormatClass} */ (
await fixture(html`<${tag}><input slot="input"></${tag}>`)
);
let counter = 0;
let isTriggeredByUser = false;
formatEl.addEventListener('model-value-changed', (
/** @param {CustomEvent} event */ event,
) => {
formatEl.addEventListener(
'model-value-changed',
(/** @param {CustomEvent} event */ event) => {
counter += 1;
isTriggeredByUser = /** @type {CustomEvent} */ (event).detail.isTriggeredByUser;
});
},
);
mimicUserInput(formatEl, generateValueBasedOnType());
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 () => {
// 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}
value="string"
.formatter=${/** @param {string} value */ value => `foo: ${value}`}
@ -215,7 +219,8 @@ export function runFormatMixinSuite(customConfig) {
>
<input slot="input" value="string" />
</${tag}>
`));
`)
);
// Now check if the format/parse/serialize loop has been triggered
await formatElem.updateComplete;
expect(formatElem.formattedValue).to.equal('foo: string');
@ -228,20 +233,23 @@ export function runFormatMixinSuite(customConfig) {
describe('Unparseable values', () => {
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=${
/** @param {string} viewValue */ viewValue => Number(viewValue) || undefined
}
>
<input slot="input">
</${tag}>
`));
`)
);
mimicUserInput(el, 'test');
expect(el.modelValue).to.be.an.instanceof(Unparseable);
});
it('preserves the viewValue when unparseable', async () => {
const el = /** @type {FormatClass} */ (await fixture(html`
const el = /** @type {FormatClass} */ (
await fixture(html`
<${tag}
.parser=${
/** @param {string} viewValue */ viewValue => Number(viewValue) || undefined
@ -249,14 +257,16 @@ export function runFormatMixinSuite(customConfig) {
>
<input slot="input">
</${tag}>
`));
`)
);
mimicUserInput(el, 'test');
expect(el.formattedValue).to.equal('test');
expect(el.value).to.equal('test');
});
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}
.parser=${
/** @param {string} viewValue */ viewValue => Number(viewValue) || undefined
@ -264,17 +274,20 @@ export function runFormatMixinSuite(customConfig) {
>
<input slot="input">
</${tag}>
`));
`)
);
el.modelValue = new Unparseable('foo');
expect(el.value).to.equal('foo');
});
it('empty strings are not Unparseable', async () => {
const el = /** @type {FormatClass} */ (await fixture(html`
const el = /** @type {FormatClass} */ (
await fixture(html`
<${tag}>
<input slot="input" value="string">
</${tag}>
`));
`)
);
// This could happen when the user erases the input value
mimicUserInput(el, '');
// 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', () => {
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}`}">
<input slot="input" />
</${tag}>
`));
`)
);
const { _inputNode } = getFormControlMembers(formatEl);
const generatedViewValue = generateValueBasedOnType({ viewValue: true });
@ -322,11 +337,13 @@ export function runFormatMixinSuite(customConfig) {
});
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}`}">
<input slot="input" />
</${tag}>
`));
`)
);
const { _inputNode } = getFormControlMembers(el);
@ -351,7 +368,8 @@ export function runFormatMixinSuite(customConfig) {
const parserSpy = sinon.spy(value => value.replace('foo: ', ''));
const serializerSpy = sinon.spy(value => `[foo] ${value}`);
const preprocessorSpy = sinon.spy(value => value.replace('bar', ''));
const el = /** @type {FormatClass} */ (await fixture(html`
const el = /** @type {FormatClass} */ (
await fixture(html`
<${tag}
.formatter=${formatterSpy}
.parser=${parserSpy}
@ -361,7 +379,8 @@ export function runFormatMixinSuite(customConfig) {
>
<input slot="input">
</${tag}>
`));
`)
);
expect(formatterSpy.called).to.be.true;
expect(serializerSpy.called).to.be.true;
@ -407,11 +426,13 @@ export function runFormatMixinSuite(customConfig) {
toggleValue: true,
});
const el = /** @type {FormatClass} */ (await fixture(html`
const el = /** @type {FormatClass} */ (
await fixture(html`
<${tag} .formatter=${formatterSpy}>
<input slot="input" .value="${generatedViewValue}">
</${tag}>
`));
`)
);
expect(formatterSpy.callCount).to.equal(1);
el.hasFeedbackFor.push('error');
@ -446,9 +467,11 @@ export function runFormatMixinSuite(customConfig) {
it('has formatOptions available in formatter', async () => {
const formatterSpy = sinon.spy(value => `foo: ${value}`);
const generatedViewValue = /** @type {string} */ (generateValueBasedOnType({
const generatedViewValue = /** @type {string} */ (
generateValueBasedOnType({
viewValue: true,
}));
})
);
await fixture(html`
<${tag} value="${generatedViewValue}" .formatter="${formatterSpy}"
.formatOptions="${{ locale: 'en-GB', decimalSeparator: '-' }}">
@ -483,9 +506,11 @@ export function runFormatMixinSuite(customConfig) {
}
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}>
`));
`)
);
const formatterSpy = sinon.spy(el, 'formatter');
paste(el);
expect(formatterSpy).to.be.called;
@ -496,9 +521,11 @@ export function runFormatMixinSuite(customConfig) {
});
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}>
`));
`)
);
const formatterSpy = sinon.spy(el, 'formatter');
paste(el);
expect(formatterSpy).to.have.been.called;
@ -510,9 +537,11 @@ export function runFormatMixinSuite(customConfig) {
});
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}>
`));
`)
);
// @ts-ignore [allow-protected] in test
const reflectBackSpy = sinon.spy(el, '_reflectBackOn');
paste(el);
@ -520,9 +549,11 @@ export function runFormatMixinSuite(customConfig) {
});
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}>
`));
`)
);
// @ts-ignore [allow-protected] in test
const reflectBackSpy = sinon.spy(el, '_reflectBackOn');
paste(el);
@ -536,11 +567,13 @@ export function runFormatMixinSuite(customConfig) {
/** @type {?} */
const generatedValue = generateValueBasedOnType();
const parserSpy = sinon.spy();
const el = /** @type {FormatClass} */ (await fixture(html`
const el = /** @type {FormatClass} */ (
await fixture(html`
<${tag} .parser="${parserSpy}">
<input slot="input" .value="${generatedValue}">
</${tag}>
`));
`)
);
expect(parserSpy.callCount).to.equal(1);
// This could happen for instance in a reset
@ -562,11 +595,13 @@ export function runFormatMixinSuite(customConfig) {
const toBeCorrectedVal = `${val}$`;
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}>
<input slot="input">
</${tag}>
`));
`)
);
const { _inputNode } = getFormControlMembers(el);
@ -581,11 +616,13 @@ export function runFormatMixinSuite(customConfig) {
});
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, '')}>
<input slot="input">
</${tag}>
`));
`)
);
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 () => {
const el = /** @type {IState} */ (await fixture(
html`<${tag} .modelValue=${'hello'}></${tag}>`,
));
const el = /** @type {IState} */ (
await fixture(html`<${tag} .modelValue=${'hello'}></${tag}>`)
);
expect(el.hasAttribute('filled')).to.equal(true);
el.modelValue = '';
await el.updateComplete;
@ -97,9 +97,11 @@ export function runInteractionStateMixinSuite(customConfig) {
it('fires "(touched|dirty)-state-changed" event when state changes', async () => {
const touchedSpy = sinon.spy();
const dirtySpy = sinon.spy();
const el = /** @type {IState} */ (await fixture(
const el = /** @type {IState} */ (
await fixture(
html`<${tag} @touched-changed=${touchedSpy} @dirty-changed=${dirtySpy}></${tag}>`,
));
)
);
el.touched = true;
expect(touchedSpy.callCount).to.equal(1);
@ -109,14 +111,18 @@ export function runInteractionStateMixinSuite(customConfig) {
});
it('sets prefilled once instantiated', async () => {
const el = /** @type {IState} */ (await fixture(html`
const el = /** @type {IState} */ (
await fixture(html`
<${tag} .modelValue=${'prefilled'}></${tag}>
`));
`)
);
expect(el.prefilled).to.be.true;
const nonPrefilled = /** @type {IState} */ (await fixture(html`
const nonPrefilled = /** @type {IState} */ (
await fixture(html`
<${tag} .modelValue=${''}></${tag}>
`));
`)
);
expect(nonPrefilled.prefilled).to.be.false;
});
@ -125,9 +131,9 @@ export function runInteractionStateMixinSuite(customConfig) {
(${cfg.allowedModelValueTypes.map(t => t.name).join(', ')})`, async () => {
/** @typedef {{_inputNode: HTMLElement}} inputNodeInterface */
const el = /** @type {IState & inputNodeInterface} */ (await fixture(
html`<${tag}></${tag}>`,
));
const el = /** @type {IState & inputNodeInterface} */ (
await fixture(html`<${tag}></${tag}>`)
);
/**
* @param {*} modelValue
@ -213,9 +219,11 @@ export function runInteractionStateMixinSuite(customConfig) {
describe('Validation integration with states', () => {
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}>
`));
`)
);
// @ts-ignore [allow-private] in test
expect(el.shouldShowFeedbackFor).to.deep.equal([]);
el.submitted = true;
@ -225,9 +233,11 @@ export function runInteractionStateMixinSuite(customConfig) {
});
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}>
`));
`)
);
const { _feedbackNode } = getFormControlMembers(el);
await el.updateComplete;
@ -257,9 +267,9 @@ export function runInteractionStateMixinSuite(customConfig) {
}
const tagLeaveString = defineCE(IStateCustomBlur);
const tagLeave = unsafeStatic(tagLeaveString);
const el = /** @type {IStateCustomBlur} */ (await fixture(
html`<${tagLeave}></${tagLeave}>`,
));
const el = /** @type {IStateCustomBlur} */ (
await fixture(html`<${tagLeave}></${tagLeave}>`)
);
el.dispatchEvent(new Event('custom-blur'));
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 () => {
const el = /** @type {ValidateElement} */ (await fixture(html`
const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag}
.validators=${[new Required()]}
>${lightDom}</${tag}>
`));
`)
);
expect(el.hasFeedbackFor).to.deep.equal(['error']);
});
it('revalidates when ".modelValue" changes', async () => {
const el = /** @type {ValidateElement} */ (await fixture(html`
const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag}
.validators=${[new AlwaysValid()]}
.modelValue=${'myValue'}
>${lightDom}</${tag}>
`));
`)
);
const validateSpy = sinon.spy(el, 'validate');
el.modelValue = 'x';
@ -139,13 +143,15 @@ export function runValidateMixinSuite(customConfig) {
});
it('revalidates when child ".modelValue" changes', async () => {
const el = /** @type {ValidateElement} */ (await fixture(html`
const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag}
._repropagationRole="${'fieldset'}"
.validators=${[new AlwaysValid()]}
.modelValue=${'myValue'}
><lion-field id="child"><input slot="input"></lion-field></${tag}>
`));
`)
);
const validateSpy = sinon.spy(el, 'validate');
/** @type {LionField} */ (el.querySelector('#child')).modelValue = 'test';
await el.updateComplete;
@ -153,12 +159,14 @@ export function runValidateMixinSuite(customConfig) {
});
it('revalidates when ".validators" changes', async () => {
const el = /** @type {ValidateElement} */ (await fixture(html`
const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag}
.validators=${[new AlwaysValid()]}
.modelValue=${'myValue'}
>${lightDom}</${tag}>
`));
`)
);
const validateSpy = sinon.spy(el, 'validate');
el.validators = [new MinLength(3)];
@ -166,12 +174,14 @@ export function runValidateMixinSuite(customConfig) {
});
it('clears current results when ".modelValue" changes', async () => {
const el = /** @type {ValidateElement} */ (await fixture(html`
const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag}
.validators=${[new AlwaysValid()]}
.modelValue=${'myValue'}
>${lightDom}</${tag}>
`));
`)
);
// @ts-ignore [allow-private] in test
const clearSpy = sinon.spy(el, '__clearValidationResults');
@ -192,9 +202,11 @@ export function runValidateMixinSuite(customConfig) {
it('firstly checks for empty values', async () => {
const alwaysValid = new AlwaysValid();
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}>
`));
`)
);
// @ts-ignore [allow-private] in test
const isEmptySpy = sinon.spy(el, '__isEmpty');
const validateSpy = sinon.spy(el, 'validate');
@ -210,9 +222,11 @@ export function runValidateMixinSuite(customConfig) {
});
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}>
`));
`)
);
// @ts-ignore [allow-private] in test
const isEmptySpy = sinon.spy(el, '__isEmpty');
// @ts-ignore [allow-private] in test
@ -222,11 +236,13 @@ export function runValidateMixinSuite(customConfig) {
});
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()]}>
${lightDom}
</${tag}>
`));
`)
);
// @ts-ignore [allow-private] in test
const syncSpy = sinon.spy(el, '__executeSyncValidators');
// @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}
.validators=${[new AlwaysValid(), new MyResult()]}>
${lightDom}
</${tag}>
`));
`)
);
// @ts-ignore [allow-private] in test
const syncSpy = sinon.spy(el, '__executeSyncValidators');
@ -278,11 +296,13 @@ export function runValidateMixinSuite(customConfig) {
describe('Finalization', () => {
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()]}>
${lightDom}
</${tag}>
`));
`)
);
const cbSpy = sinon.spy();
el.addEventListener('validate-performed', cbSpy);
el.modelValue = 'nonEmpty';
@ -290,11 +310,13 @@ export function runValidateMixinSuite(customConfig) {
});
it('resolves ".validateComplete" Promise', async () => {
const el = /** @type {ValidateElement} */ (await fixture(html`
const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag} .validators=${[new AsyncAlwaysInvalid()]}>
${lightDom}
</${tag}>
`));
`)
);
el.modelValue = 'nonEmpty';
// @ts-ignore [allow-private] in test
const validateResolveSpy = sinon.spy(el, '__validateCompleteResolve');
@ -395,9 +417,11 @@ export function runValidateMixinSuite(customConfig) {
});
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}>
`));
`)
);
el.modelValue = 'cat';
expect(el.validationStates.error.IsCat).to.be.undefined;
@ -410,12 +434,14 @@ export function runValidateMixinSuite(customConfig) {
it('Validators get retriggered on parameter change', async () => {
const isCatValidator = new IsCat('Felix');
const catSpy = sinon.spy(isCatValidator, 'execute');
const el = /** @type {ValidateElement} */ (await fixture(html`
const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag}
.validators=${[isCatValidator]}
.modelValue=${'cat'}
>${lightDom}</${tag}>
`));
`)
);
el.modelValue = 'cat';
expect(catSpy.callCount).to.equal(1);
isCatValidator.param = 'Garfield';
@ -459,13 +485,15 @@ export function runValidateMixinSuite(customConfig) {
// default execution trigger is keyup (think of password availability backend)
// can configure execution trigger (blur, etc?)
it('handles "execute" functions returning promises', async () => {
const el = /** @type {ValidateElement} */ (await fixture(html`
const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag}
.modelValue=${'dog'}
.validators=${[new IsAsyncCat()]}>
${lightDom}
</${tag}>
`));
`)
);
const validator = el.validators[0];
expect(validator instanceof Validator).to.be.true;
@ -476,9 +504,11 @@ export function runValidateMixinSuite(customConfig) {
});
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}>
`));
`)
);
expect(el.isPending).to.be.false;
expect(el.hasAttribute('is-pending')).to.be.false;
@ -498,11 +528,13 @@ export function runValidateMixinSuite(customConfig) {
const asyncV = new IsAsyncCat();
const asyncVExecuteSpy = sinon.spy(asyncV, 'execute');
const el = /** @type {ValidateElement} */ (await fixture(html`
const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag} .modelValue=${'dog'}>
${lightDom}
</${tag}>
`));
`)
);
// debounce started
el.validators = [asyncV];
expect(asyncVExecuteSpy.called).to.equal(0);
@ -528,11 +560,13 @@ export function runValidateMixinSuite(customConfig) {
const asyncV = new IsAsyncCat();
const asyncVAbortSpy = sinon.spy(asyncV, 'abortExecution');
const el = /** @type {ValidateElement} */ (await fixture(html`
const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag} .modelValue=${'dog'}>
${lightDom}
</${tag}>
`));
`)
);
// debounce started
el.validators = [asyncV];
expect(asyncVAbortSpy.called).to.equal(0);
@ -546,7 +580,8 @@ export function runValidateMixinSuite(customConfig) {
const asyncV = new IsAsyncCat();
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}
.isFocused=${true}
.modelValue=${'dog'}
@ -558,7 +593,8 @@ export function runValidateMixinSuite(customConfig) {
>
${lightDom}
</${tag}>
`));
`)
);
expect(asyncVExecuteSpy.called).to.equal(0);
el.isFocused = false;
@ -635,12 +671,14 @@ export function runValidateMixinSuite(customConfig) {
const resultValidator = new MySuccessResultValidator();
const resultValidateSpy = sinon.spy(resultValidator, 'executeOnResults');
const el = /** @type {ValidateElement} */ (await fixture(html`
const el = /** @type {ValidateElement} */ (
await fixture(html`
<${withSuccessTag}
.validators=${[new MinLength(3), resultValidator]}
.modelValue=${'myValue'}
>${lightDom}</${withSuccessTag}>
`));
`)
);
// @ts-ignore [allow-private] in test
const prevValidationResult = el.__prevValidationResult;
// @ts-ignore [allow-private] in test
@ -671,12 +709,14 @@ export function runValidateMixinSuite(customConfig) {
const validator = new AlwaysInvalid();
const resultV = new AlwaysInvalidResult();
const el = /** @type {ValidateElement} */ (await fixture(html`
const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag}
.validators=${[validator, resultV]}
.modelValue=${'myValue'}
>${lightDom}</${tag}>
`));
`)
);
// @ts-ignore [allow-private] in test
const totalValidationResult = el.__validationResult;
@ -686,12 +726,14 @@ export function runValidateMixinSuite(customConfig) {
describe('Required Validator integration', () => {
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}
.validators=${[new Required()]}
.modelValue=${''}
>${lightDom}</${tag}>
`));
`)
);
expect(el.validationStates.error.Required).to.be.true;
expect(el.hasFeedbackFor).to.deep.equal(['error']);
@ -701,12 +743,14 @@ export function runValidateMixinSuite(customConfig) {
});
it('calls private ".__isEmpty" by default', async () => {
const el = /** @type {ValidateElement} */ (await fixture(html`
const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag}
.validators=${[new Required()]}
.modelValue=${''}
>${lightDom}</${tag}>
`));
`)
);
const validator = /** @type {Validator} */ (el.validators.find(v => v instanceof Required));
const executeSpy = sinon.spy(validator, 'execute');
// @ts-ignore [allow-private] in test
@ -725,12 +769,14 @@ export function runValidateMixinSuite(customConfig) {
const customRequiredTagString = defineCE(_isEmptyValidate);
const customRequiredTag = unsafeStatic(customRequiredTagString);
const el = /** @type {_isEmptyValidate} */ (await fixture(html`
const el = /** @type {_isEmptyValidate} */ (
await fixture(html`
<${customRequiredTag}
.validators=${[new Required()]}
.modelValue=${{ model: 'foo' }}
>${lightDom}</${customRequiredTag}>
`));
`)
);
const providedIsEmptySpy = sinon.spy(el, '_isEmpty');
el.modelValue = { model: '' };
@ -741,24 +787,28 @@ export function runValidateMixinSuite(customConfig) {
it('prevents other Validators from being called when input is empty', async () => {
const alwaysInvalid = new AlwaysInvalid();
const alwaysInvalidSpy = sinon.spy(alwaysInvalid, 'execute');
const el = /** @type {ValidateElement} */ (await fixture(html`
const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag}
.validators=${[new Required(), alwaysInvalid]}
.modelValue=${''}
>${lightDom}</${tag}>
`));
`)
);
expect(alwaysInvalidSpy.callCount).to.equal(0); // __isRequired returned false (invalid)
el.modelValue = 'foo';
expect(alwaysInvalidSpy.callCount).to.equal(1); // __isRequired returned true (valid)
});
it('adds [aria-required="true"] to "._inputNode"', async () => {
const el = /** @type {ValidateElement} */ (await fixture(html`
const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag}
.validators=${[new Required()]}
.modelValue=${''}
>${lightDom}</${tag}>
`));
`)
);
const { _inputNode } = getFormControlMembers(el);
expect(_inputNode?.getAttribute('aria-required')).to.equal('true');
@ -779,11 +829,13 @@ export function runValidateMixinSuite(customConfig) {
const preconfTag = unsafeStatic(preconfTagString);
it('can be stored for custom inputs', async () => {
const el = /** @type {ValidateElement} */ (await fixture(html`
const el = /** @type {ValidateElement} */ (
await fixture(html`
<${preconfTag}
.validators=${[new MinLength(3)]}
.modelValue=${'12'}
></${preconfTag}>`));
></${preconfTag}>`)
);
expect(el.validationStates.error.AlwaysInvalid).to.be.true;
expect(el.validationStates.error.MinLength).to.be.true;
@ -800,10 +852,12 @@ export function runValidateMixinSuite(customConfig) {
);
const altPreconfTag = unsafeStatic(altPreconfTagString);
const el = /** @type {ValidateElement} */ (await fixture(html`
const el = /** @type {ValidateElement} */ (
await fixture(html`
<${altPreconfTag}
.modelValue=${'12'}
></${altPreconfTag}>`));
></${altPreconfTag}>`)
);
expect(el.validationStates.error.MinLength).to.be.true;
el.defaultValidators[0].param = 2;
@ -811,10 +865,12 @@ export function runValidateMixinSuite(customConfig) {
});
it('can be requested via "._allValidators" getter', async () => {
const el = /** @type {ValidateElement} */ (await fixture(html`
const el = /** @type {ValidateElement} */ (
await fixture(html`
<${preconfTag}
.validators=${[new MinLength(3)]}
></${preconfTag}>`));
></${preconfTag}>`)
);
const { _allValidators } = getFormControlMembers(el);
expect(el.validators.length).to.equal(1);
@ -834,11 +890,13 @@ export function runValidateMixinSuite(customConfig) {
describe('State storage and reflection', () => {
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}
.modelValue=${'a'}
.validators=${[new MinLength(3), new AlwaysInvalid()]}
>${lightDom}</${tag}>`));
>${lightDom}</${tag}>`)
);
expect(el.validationStates.error.MinLength).to.be.true;
expect(el.validationStates.error.AlwaysInvalid).to.be.true;
@ -849,11 +907,13 @@ export function runValidateMixinSuite(customConfig) {
});
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}
.validators=${[new MinLength(3)]}
>${lightDom}</${tag}>
`));
`)
);
el.modelValue = 'a';
expect(el.hasFeedbackFor).to.deep.equal(['error']);
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 () => {
const validators = [new Required()];
const el = /** @type {ValidateElement} */ (await fixture(html`
const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag}
.validators=${validators}
>${lightDom}</${tag}>
`));
`)
);
expect(el.hasFeedbackFor).to.deep.equal(['error']);
expect(el.validationStates.error).to.eql({ Required: true });
@ -883,7 +945,8 @@ export function runValidateMixinSuite(customConfig) {
});
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}
.validators="${[new Required({}, { type: 'error' })]}"
.feedbackCondition="${(
@ -897,7 +960,8 @@ export function runValidateMixinSuite(customConfig) {
return defaultCondition(type);
}}"
>${lightDom}</${tag}>
`));
`)
);
expect(el.showsFeedbackFor).to.eql(['error']);
});
@ -905,13 +969,15 @@ export function runValidateMixinSuite(customConfig) {
describe('Events', () => {
it('fires "showsFeedbackForChanged" event async after feedbackData got synced to feedbackElement', async () => {
const spy = sinon.spy();
const el = /** @type {ValidateElement} */ (await fixture(html`
const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag}
.submitted=${true}
.validators=${[new MinLength(7)]}
@showsFeedbackForChanged=${spy};
@showsFeedbackForChanged=${spy}
>${lightDom}</${tag}>
`));
`)
);
el.modelValue = 'a';
await el.updateComplete;
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 () => {
const spy = sinon.spy();
const el = /** @type {ValidateElement} */ (await fixture(html`
const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag}
.submitted=${true}
.validators=${[new MinLength(7)]}
@showsFeedbackForErrorChanged=${spy};
@showsFeedbackForErrorChanged=${spy}
>${lightDom}</${tag}>
`));
`)
);
el.modelValue = 'a';
await el.updateComplete;
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 () => {
const spy = sinon.spy();
const el = /** @type {ValidateElement} */ (await fixture(html`
const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag}
.submitted=${true}
.validators=${[new MinLength(7)]}
@errorStateChanged=${spy};
@errorStateChanged=${spy}
>${lightDom}</${tag}>
`));
`)
);
expect(spy).to.have.callCount(0);
el.modelValue = 'a';
@ -975,12 +1045,14 @@ export function runValidateMixinSuite(customConfig) {
describe('Accessibility', () => {
it.skip('calls "._inputNode.setCustomValidity(errorMessage)"', async () => {
const el = /** @type {ValidateElement} */ (await fixture(html`
const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag}
.modelValue=${'123'}
.validators=${[new MinLength(3, { message: 'foo' })]}>
<input slot="input">
</${tag}>`));
</${tag}>`)
);
const { _inputNode } = getFormControlMembers(el);
if (_inputNode) {
@ -1013,7 +1085,8 @@ export function runValidateMixinSuite(customConfig) {
const customTypeTag = unsafeStatic(customTypeTagString);
it('supports additional validationTypes in .hasFeedbackFor', async () => {
const el = /** @type {ValidateElement} */ (await fixture(html`
const el = /** @type {ValidateElement} */ (
await fixture(html`
<${customTypeTag}
.validators=${[
new MinLength(2, { type: 'x' }),
@ -1022,7 +1095,8 @@ export function runValidateMixinSuite(customConfig) {
]}
.modelValue=${'1234'}
>${lightDom}</${customTypeTag}>
`));
`)
);
expect(el.hasFeedbackFor).to.deep.equal([]);
el.modelValue = '123'; // triggers y
@ -1036,7 +1110,8 @@ export function runValidateMixinSuite(customConfig) {
});
it('supports additional validationTypes in .validationStates', async () => {
const el = /** @type {ValidateElement} */ (await fixture(html`
const el = /** @type {ValidateElement} */ (
await fixture(html`
<${customTypeTag}
.validators=${[
new MinLength(2, { type: 'x' }),
@ -1045,7 +1120,8 @@ export function runValidateMixinSuite(customConfig) {
]}
.modelValue=${'1234'}
>${lightDom}</${customTypeTag}>
`));
`)
);
expect(el.validationStates).to.eql({
x: {},
error: {},
@ -1076,7 +1152,8 @@ export function runValidateMixinSuite(customConfig) {
it('orders feedback based on provided "validationTypes"', async () => {
// 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}
.submitted=${true}
._visibleMessagesAmount=${Infinity}
@ -1087,7 +1164,8 @@ export function runValidateMixinSuite(customConfig) {
]}
.modelValue=${'1'}
>${lightDom}</${customTypeTag}>
`));
`)
);
const { _feedbackNode } = getFormControlMembers(el);
await el.feedbackComplete;
@ -1132,13 +1210,15 @@ export function runValidateMixinSuite(customConfig) {
const elTag = unsafeStatic(elTagString);
// we set submitted to always show errors
const el = /** @type {ValidateHasX} */ (await fixture(html`
const el = /** @type {ValidateHasX} */ (
await fixture(html`
<${elTag}
.submitted=${true}
.validators=${[new MinLength(2, { type: 'x' })]}
.modelValue=${'1'}
>${lightDom}</${elTag}>
`));
`)
);
await el.feedbackComplete;
expect(el.hasX).to.be.true;
expect(el.hasXVisible).to.be.true;
@ -1186,14 +1266,16 @@ export function runValidateMixinSuite(customConfig) {
const spy = sinon.spy();
// we set prefilled to always show errors
const el = /** @type {ValidateElement} */ (await fixture(html`
const el = /** @type {ValidateElement} */ (
await fixture(html`
<${elTag}
.prefilled=${true}
@hasFeedbackForXChanged=${spy}
.validators=${[new MinLength(2, { type: 'x' })]}
.modelValue=${'1'}
>${lightDom}</${elTag}>
`));
`)
);
expect(spy).to.have.callCount(1);
el.modelValue = '1';
expect(spy).to.have.callCount(1);
@ -1228,12 +1310,14 @@ export function runValidateMixinSuite(customConfig) {
},
);
const elTag = unsafeStatic(elTagString);
const el = /** @type {ValidateElement} */ (await fixture(html`
const el = /** @type {ValidateElement} */ (
await fixture(html`
<${elTag}
.validators=${[new AlwaysInvalid()]}
.modelValue=${'myValue'}
>${lightDom}</${elTag}>
`));
`)
);
// @ts-ignore [allow-protected] in test
const spy = sinon.spy(el, '_updateShouldShowFeedbackFor');
@ -1282,14 +1366,16 @@ export function runValidateMixinSuite(customConfig) {
},
);
const elTag = unsafeStatic(elTagString);
const el = /** @type {ValidateElement} */ (await fixture(html`
const el = /** @type {ValidateElement} */ (
await fixture(html`
<${elTag}
.validators=${[
new AlwaysInvalid({}, { type: 'error' }),
new AlwaysInvalid({}, { type: 'info' }),
]}
>${lightDom}</${elTag}>
`));
`)
);
for (const [modelValue, expected] of [
['A', ['error']],

View file

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

View file

@ -2,7 +2,9 @@ import { LitElement } from '@lion/core';
import { LionInput } from '@lion/input';
import '@lion/fieldset/define';
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 { ChoiceGroupMixin } from '../../src/choice-group/ChoiceGroupMixin.js';
import { ChoiceInputMixin } from '../../src/choice-group/ChoiceInputMixin.js';
@ -41,13 +43,15 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi
describe(`ChoiceGroupMixin: ${cfg.parentTagString}`, () => {
if (cfg.choiceType === 'single') {
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[]">
<${childTag} .choiceValue=${'male'}></${childTag}>
<${childTag} .choiceValue=${'female'} checked></${childTag}>
<${childTag} .choiceValue=${'other'}></${childTag}>
</${parentTag}>
`));
`)
);
expect(el.modelValue).to.equal('female');
el.formElements[0].checked = true;
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 () => {
const el = /** @type {ChoiceInputGroup} */ (await fixture(html`
const el = /** @type {ChoiceInputGroup} */ (
await fixture(html`
<${parentTag} name="gender">
<${childTag} .choiceValue=${'male'}></${childTag}>
<${childTag} .choiceValue=${'female'} checked></${childTag}>
<${childTag} .choiceValue=${'other'}></${childTag}>
</${parentTag}>
`));
`)
);
expect(el.formattedValue).to.equal('female');
el.formElements[0].checked = true;
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 () => {
const el = /** @type {ChoiceInputGroup} */ (await fixture(html`
const el = /** @type {ChoiceInputGroup} */ (
await fixture(html`
<${parentTag} name="gender[]">
<${childTag} .choiceValue=${'male'}></${childTag}>
<${childTag} .choiceValue=${'female'} checked></${childTag}>
<${childTag} .choiceValue=${'other'}></${childTag}>
</${parentTag}>
`));
const invalidChild = /** @type {ChoiceInputGroup} */ (await fixture(html`
`)
);
const invalidChild = /** @type {ChoiceInputGroup} */ (
await fixture(html`
<${childTag} .modelValue=${'Lara'}></${childTag}>
`));
`)
);
expect(() => {
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 () => {
const el = /** @type {ChoiceInputGroup} */ (await fixture(html`
const el = /** @type {ChoiceInputGroup} */ (
await fixture(html`
<${parentTag} name="gender[]">
<${childTag} .choiceValue=${'female'} checked></${childTag}>
<${childTag} .choiceValue=${'other'}></${childTag}>
</${parentTag}>
`));
`)
);
expect(el.formElements[0].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}>
`));
`)
);
el.appendChild(validChild);
expect(el.formElements[2].name).to.equal('gender[]');
});
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[]">
<${childTag}></${childTag}>
<${childTag}></${childTag}>
</${parentTag}>
`));
`)
);
expect(el.formElements[0].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 () => {
const el = /** @type {ChoiceInputGroup} */ (await fixture(html`
const el = /** @type {ChoiceInputGroup} */ (
await fixture(html`
<${parentTag} name="gender[]">
<${childTag}></${childTag}>
<${childTag}></${childTag}>
</${parentTag}>
`));
`)
);
expect(el.formElements[0].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 () => {
const el = /** @type {ChoiceInputGroup} */ (await fixture(html`
const el = /** @type {ChoiceInputGroup} */ (
await fixture(html`
<${parentTag} name="gender[]">
<${childTagFoo}></${childTagFoo}>
<${childTagFoo}></${childTagFoo}>
</${parentTag}>
`));
`)
);
expect(el.formElements[0].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 () => {
const el = /** @type {ChoiceInputGroup} */ (await fixture(html`
const el = /** @type {ChoiceInputGroup} */ (
await fixture(html`
<${parentTag} name="gender[]">
<${childTagBar}></${childTagBar}>
<${childTagBar}></${childTagBar}>
</${parentTag}>
`));
`)
);
expect(el.formElements[0].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 () => {
const el = /** @type {ChoiceInputGroup} */ (await fixture(html`
const el = /** @type {ChoiceInputGroup} */ (
await fixture(html`
<${parentTag} name="gender[]">
<${childTag} .choiceValue=${'female'} checked></${childTag}>
<${childTag} .choiceValue=${'other'}></${childTag}>
</${parentTag}>
`));
`)
);
const invalidChild = /** @type {ChoiceInputGroup} */ (await fixture(html`
const invalidChild = /** @type {ChoiceInputGroup} */ (
await fixture(html`
<${childTag} name="foo" .choiceValue=${'male'}></${childTag}>
`));
`)
);
el.addFormElement(invalidChild);
await invalidChild.updateComplete;
expect(invalidChild.name).to.equal('gender[]');
});
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'}>
<${childTag} .choiceValue=${'male'}></${childTag}>
<${childTag} .choiceValue=${'female'}></${childTag}>
<${childTag} .choiceValue=${'other'}></${childTag}>
</${parentTag}>
`));
`)
);
if (cfg.choiceType === 'single') {
expect(el.modelValue).to.equal('other');
@ -213,13 +241,15 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi
});
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'}>
<${childTag} .choiceValue=${'male'}></${childTag}>
<${childTag} .choiceValue=${'female'}></${childTag}>
<${childTag} .choiceValue=${'other'}></${childTag}>
</${parentTag}>
`));
`)
);
if (cfg.choiceType === 'single') {
expect(el.serializedValue).to.equal('other');
@ -230,13 +260,15 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi
});
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'}>
<${childTag} .choiceValue=${'male'}></${childTag}>
<${childTag} .choiceValue=${'female'}></${childTag}>
<${childTag} .choiceValue=${'other'}></${childTag}>
</${parentTag}>
`));
`)
);
if (cfg.choiceType === 'single') {
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 () => {
const el = /** @type {ChoiceInputGroup} */ (fixtureSync(html`
const el = /** @type {ChoiceInputGroup} */ (
fixtureSync(html`
<${parentTag} name="gender[]" .modelValue=${null}>
<${childTag} .choiceValue=${'male'}></${childTag}>
<${childTag} .choiceValue=${'female'}></${childTag}>
<${childTag} .choiceValue=${'other'}></${childTag}>
</${parentTag}>
`));
`)
);
if (cfg.choiceType === 'single') {
el.modelValue = 'other';
@ -267,13 +301,15 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi
});
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}>
<${childTag} .choiceValue=${'male'}></${childTag}>
<${childTag} .choiceValue=${'female'}></${childTag}>
<${childTag} .choiceValue=${'other'}></${childTag}>
</${parentTag}>
`));
`)
);
if (cfg.choiceType === 'single') {
// @ts-expect-error
@ -289,13 +325,15 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi
});
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}>
<${childTag} .choiceValue=${'male'}></${childTag}>
<${childTag} .choiceValue=${'female'}></${childTag}>
<${childTag} .choiceValue=${'other'}></${childTag}>
</${parentTag}>
`));
`)
);
if (cfg.choiceType === 'single') {
expect(el.modelValue).to.equal('');
@ -315,12 +353,14 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi
it('can handle complex data via choiceValue', async () => {
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[]">
<${childTag} .choiceValue=${{ some: 'data' }}></${childTag}>
<${childTag} .choiceValue=${date} checked></${childTag}>
</${parentTag}>
`));
`)
);
if (cfg.choiceType === 'single') {
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 () => {
const el = /** @type {ChoiceInputGroup} */ (await fixture(html`
const el = /** @type {ChoiceInputGroup} */ (
await fixture(html`
<${parentTag} name="data[]">
<${childTag} .choiceValue=${0} checked></${childTag}>
<${childTag} .choiceValue=${''}></${childTag}>
</${parentTag}>
`));
`)
);
if (cfg.choiceType === 'single') {
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 () => {
const el = /** @type {ChoiceInputGroup} */ (await fixture(html`
const el = /** @type {ChoiceInputGroup} */ (
await fixture(html`
<${parentTag} name="gender[]">
<${childTag}
.modelValue="${{ value: 'male', checked: false }}"
@ -365,7 +408,8 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi
.modelValue="${{ value: 'other', checked: false }}"
></${childTag}>
</${parentTag}>
`));
`)
);
if (cfg.choiceType === 'single') {
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 () => {
const el = /** @type {ChoiceInputGroup} */ (await fixture(html`
const el = /** @type {ChoiceInputGroup} */ (
await fixture(html`
<${parentTag} name="gender[]">
<${childTag}
.modelValue="${{ value: { v: 'male' }, checked: false }}"
@ -389,7 +434,8 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi
.modelValue="${{ value: { v: 'other' }, checked: false }}"
></${childTag}>
</${parentTag}>
`));
`)
);
if (cfg.choiceType === 'single') {
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 () => {
let counter = 0;
const el = /** @type {ChoiceInputGroup} */ (await fixture(html`
const el = /** @type {ChoiceInputGroup} */ (
await fixture(html`
<${parentTag}
name="gender[]"
@model-value-changed=${() => {
@ -420,7 +467,8 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi
></${childTag}>
<${childTag} .choiceValue=${'other'}></${childTag}>
</${parentTag}>
`));
`)
);
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 () => {
const el = /** @type {ChoiceInputGroup} */ (await fixture(html`
const el = /** @type {ChoiceInputGroup} */ (
await fixture(html`
<${parentTag} name="gender[]" .validators=${[new Required()]}>
<${childTag} .choiceValue=${'male'}></${childTag}>
<${childTag}
.choiceValue=${{ subObject: 'satisfies required' }}
></${childTag}>
</${parentTag}>
`));
`)
);
expect(el.hasFeedbackFor).to.include('error');
expect(el.validationStates.error).to.exist;
expect(el.validationStates.error.Required).to.exist;
@ -478,12 +528,14 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi
});
it('returns serialized value', async () => {
const el = /** @type {ChoiceInputGroup} */ (await fixture(html`
const el = /** @type {ChoiceInputGroup} */ (
await fixture(html`
<${parentTag} name="gender[]">
<${childTag} .choiceValue=${'male'}></${childTag}>
<${childTag} .choiceValue=${'female'}></${childTag}>
</${parentTag}>
`));
`)
);
el.formElements[0].checked = true;
if (cfg.choiceType === 'single') {
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 () => {
const el = /** @type {ChoiceInputGroup} */ (await fixture(html`
const el = /** @type {ChoiceInputGroup} */ (
await fixture(html`
<${parentTag} name="gender[]">
<${childTag} .choiceValue=${'male'}></${childTag}>
<${childTag} .choiceValue=${'female'}></${childTag}>
</${parentTag}>
`));
`)
);
if (cfg.choiceType === 'single') {
expect(el.serializedValue).to.deep.equal('');
@ -508,12 +562,14 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi
});
it('can be cleared', async () => {
const el = /** @type {ChoiceInputGroup} */ (await fixture(html`
const el = /** @type {ChoiceInputGroup} */ (
await fixture(html`
<${parentTag} name="gender[]">
<${childTag} .choiceValue=${'male'}></${childTag}>
<${childTag} .choiceValue=${'female'}></${childTag}>
</${parentTag}>
`));
`)
);
el.formElements[0].checked = true;
el.clear();
@ -526,13 +582,15 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi
describe('multipleChoice', () => {
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[]">
<${childTag} .choiceValue=${'male'}></${childTag}>
<${childTag} .choiceValue=${'female'} checked></${childTag}>
<${childTag} .choiceValue=${'other'}></${childTag}>
</${parentTag}>
`));
`)
);
expect(el.modelValue).to.eql(['female']);
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 () => {
const el = /** @type {ChoiceInputGroup} */ (await fixture(html`
const el = /** @type {ChoiceInputGroup} */ (
await fixture(html`
<${parentTag} multiple-choice name="gender[]">
<${childTag} .choiceValue=${'male'}></${childTag}>
<${childTag} .choiceValue=${'female'} checked></${childTag}>
<${childTag} .choiceValue=${'other'}></${childTag}>
</${parentTag}>
`));
`)
);
expect(el.serializedValue).to.eql(['female']);
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 () => {
const el = /** @type {ChoiceInputGroup} */ (await fixture(html`
const el = /** @type {ChoiceInputGroup} */ (
await fixture(html`
<${parentTag} multiple-choice name="gender[]">
<${childTag} .choiceValue=${'male'}></${childTag}>
<${childTag} .choiceValue=${'female'} checked></${childTag}>
<${childTag} .choiceValue=${'other'}></${childTag}>
</${parentTag}>
`));
`)
);
expect(el.formattedValue).to.eql(['female']);
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 () => {
const el = /** @type {ChoiceInputGroup} */ (await fixture(html`
const el = /** @type {ChoiceInputGroup} */ (
await fixture(html`
<${parentTag} multiple-choice name="gender[]">
<${childTag} .choiceValue=${'male'}></${childTag}>
<${childTag} .choiceValue=${'female'}></${childTag}>
<${childTag} .choiceValue=${'other'}></${childTag}>
</${parentTag}>
`));
`)
);
el.modelValue = ['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 () => {
const el = /** @type {ChoiceInputGroup} */ (await fixture(html`
const el = /** @type {ChoiceInputGroup} */ (
await fixture(html`
<${parentTag} multiple-choice name="gender[]">
<${childTag} .choiceValue=${'male'} checked></${childTag}>
<${childTag} .choiceValue=${'female'}></${childTag}>
<${childTag} .choiceValue=${'other'} checked></${childTag}>
</${parentTag}>
`));
`)
);
expect(el.modelValue).to.eql(['male', 'other']);
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', () => {
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>
<${parentTag} name="gender[]">
<${childTag} .choiceValue=${'male'} checked disabled></${childTag}>
@ -618,7 +685,8 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi
<${childTag} .choiceValue=${'other'}></${childTag}>
</${parentTag}>
</lion-fieldset>
`));
`)
);
if (cfg.choiceType === 'single') {
expect(el.serializedValue).to.deep.equal({ 'gender[]': ['female'] });
@ -641,19 +709,19 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi
</lion-fieldset>
`);
const choiceGroupEl = /** @type {ChoiceInputGroup} */ (formEl.querySelector(
'[name=choice-group]',
));
const choiceGroupEl = /** @type {ChoiceInputGroup} */ (
formEl.querySelector('[name=choice-group]')
);
if (choiceGroupEl.multipleChoice) {
return;
}
/** @typedef {{ checked: boolean }} checkedInterface */
const option1El = /** @type {HTMLElement & checkedInterface} */ (formEl.querySelector(
'#option1',
));
const option2El = /** @type {HTMLElement & checkedInterface} */ (formEl.querySelector(
'#option2',
));
const option1El = /** @type {HTMLElement & checkedInterface} */ (
formEl.querySelector('#option1')
);
const option2El = /** @type {HTMLElement & checkedInterface} */ (
formEl.querySelector('#option2')
);
formEl.addEventListener('model-value-changed', formSpy);
choiceGroupEl?.addEventListener('model-value-changed', choiceGroupSpy);

View file

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

View file

@ -1,6 +1,7 @@
import { LitElement } from '@lion/core';
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 { LionInput } from '@lion/input';
import '@lion/form-core/define';
@ -47,12 +48,14 @@ export function runFormGroupMixinInputSuite(cfg = {}) {
describe('FormGroupMixin with LionField', () => {
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}>
<${childTag} name="custom[]"></${childTag}>
<${childTag} name="custom[]"></${childTag}>
</${tag}>
`));
`)
);
fieldset.formElements['custom[]'][0].modelValue = 'custom 1';
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 () => {
const el = /** @type {FormGroup} */ (await fixture(html`
const el = /** @type {FormGroup} */ (
await fixture(html`
<${tag} label="set">
<${childTag} name="A" label="fieldA"></${childTag}>
<${childTag} name="B" label="fieldB"></${childTag}>
</${tag}>
`));
`)
);
const { _labelNode } = getFormControlMembers(el);
/**
@ -88,8 +93,10 @@ export function runFormGroupMixinInputSuite(cfg = {}) {
// Test the cleanup on disconnected
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 (
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">
<${childTag} name="l1_fa">
<div slot="${msgSlotType}" id="msg_l1_fa"></div>
@ -144,7 +152,8 @@ export function runFormGroupMixinInputSuite(cfg = {}) {
<div slot="${msgSlotType}" id="msg_l1_g"></div>
<!-- group referred by: #msg_l1_g (local) -->
</${tag}>
`));
`)
);
return dom;
};
@ -163,18 +172,18 @@ export function runFormGroupMixinInputSuite(cfg = {}) {
const msg_l2_fb = /** @type {FormChild} */ (childAriaFixture.querySelector('#msg_l2_fb'));
// Field elements: all inputs pointing to message elements
const input_l1_fa = /** @type {HTMLInputElement} */ (childAriaFixture.querySelector(
'input[name=l1_fa]',
));
const input_l1_fb = /** @type {HTMLInputElement} */ (childAriaFixture.querySelector(
'input[name=l1_fb]',
));
const input_l2_fa = /** @type {HTMLInputElement} */ (childAriaFixture.querySelector(
'input[name=l2_fa]',
));
const input_l2_fb = /** @type {HTMLInputElement} */ (childAriaFixture.querySelector(
'input[name=l2_fb]',
));
const input_l1_fa = /** @type {HTMLInputElement} */ (
childAriaFixture.querySelector('input[name=l1_fa]')
);
const input_l1_fb = /** @type {HTMLInputElement} */ (
childAriaFixture.querySelector('input[name=l1_fb]')
);
const input_l2_fa = /** @type {HTMLInputElement} */ (
childAriaFixture.querySelector('input[name=l2_fa]')
);
const input_l2_fb = /** @type {HTMLInputElement} */ (
childAriaFixture.querySelector('input[name=l2_fb]')
);
if (!cleanupPhase) {
// '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');
} else {
// cleanupPhase
const control_l1_fa = /** @type {LionField} */ (childAriaFixture.querySelector(
'[name=l1_fa]',
));
const control_l1_fb = /** @type {LionField} */ (childAriaFixture.querySelector(
'[name=l1_fb]',
));
const control_l2_fa = /** @type {LionField} */ (childAriaFixture.querySelector(
'[name=l2_fa]',
));
const control_l2_fb = /** @type {LionField} */ (childAriaFixture.querySelector(
'[name=l2_fb]',
));
const control_l1_fa = /** @type {LionField} */ (
childAriaFixture.querySelector('[name=l1_fa]')
);
const control_l1_fb = /** @type {LionField} */ (
childAriaFixture.querySelector('[name=l1_fb]')
);
const control_l2_fa = /** @type {LionField} */ (
childAriaFixture.querySelector('[name=l2_fa]')
);
const control_l2_fb = /** @type {LionField} */ (
childAriaFixture.querySelector('[name=l2_fb]')
);
// @ts-expect-error removeChild should always be inherited via LitElement?
control_l1_fa._parentFormGroup.removeChild(control_l1_fa);
@ -303,12 +312,14 @@ export function runFormGroupMixinInputSuite(cfg = {}) {
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');
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');
await childAriaTest(el, { cleanupPhase: true });
});

View file

@ -1,14 +1,7 @@
import { LitElement, ifDefined } from '@lion/core';
import { html, unsafeStatic } from 'lit/static-html.js';
import { localizeTearDown } from '@lion/localize/test-helpers';
import {
defineCE,
expect,
html,
triggerFocusFor,
unsafeStatic,
fixture,
aTimeout,
} from '@open-wc/testing';
import { defineCE, expect, triggerFocusFor, fixture, aTimeout } from '@open-wc/testing';
import sinon from 'sinon';
import { IsNumber, Validator, LionField } from '@lion/form-core';
import '@lion/form-core/define';
@ -59,30 +52,32 @@ export function runFormGroupMixinSuite(cfg = {}) {
describe('FormGroupMixin', () => {
// TODO: Tests below belong to FormControlMixin. Preferably run suite integration test
it(`has a fieldName based on the label`, async () => {
const el1 = /** @type {FormGroup} */ (await fixture(
html`<${tag} label="foo">${inputSlots}</${tag}>`,
));
const el1 = /** @type {FormGroup} */ (
await fixture(html`<${tag} label="foo">${inputSlots}</${tag}>`)
);
const { _labelNode: _labelNode1 } = getFormControlMembers(el1);
expect(el1.fieldName).to.equal(_labelNode1.textContent);
const el2 = /** @type {FormGroup} */ (await fixture(
html`<${tag}><label slot="label">bar</label>${inputSlots}</${tag}>`,
));
const el2 = /** @type {FormGroup} */ (
await fixture(html`<${tag}><label slot="label">bar</label>${inputSlots}</${tag}>`)
);
const { _labelNode: _labelNode2 } = getFormControlMembers(el2);
expect(el2.fieldName).to.equal(_labelNode2.textContent);
});
it(`has a fieldName based on the name if no label exists`, async () => {
const el = /** @type {FormGroup} */ (await fixture(
html`<${tag} name="foo">${inputSlots}</${tag}>`,
));
const el = /** @type {FormGroup} */ (
await fixture(html`<${tag} name="foo">${inputSlots}</${tag}>`)
);
expect(el.fieldName).to.equal(el.name);
});
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}>
`));
`)
);
// @ts-ignore [allow-protected] in test
expect(el.__fieldName).to.equal(el.fieldName);
});
@ -100,13 +95,15 @@ export function runFormGroupMixinSuite(cfg = {}) {
});
it(`supports in html wrapped form elements`, async () => {
const el = /** @type {FormGroup} */ (await fixture(html`
const el = /** @type {FormGroup} */ (
await fixture(html`
<${tag}>
<div>
<${childTag} name="foo"></${childTag}>
</div>
</${tag}>
`));
`)
);
expect(el.formElements.length).to.equal(1);
el.children[0].removeChild(el.formElements.foo);
expect(el.formElements.length).to.equal(0);
@ -206,9 +203,9 @@ export function runFormGroupMixinSuite(cfg = {}) {
it('can dynamically add/remove elements', async () => {
const el = /** @type {FormGroup} */ (await fixture(html`<${tag}>${inputSlots}</${tag}>`));
const newField = /** @type {FormGroup} */ (await fixture(
html`<${childTag} name="lastName"></${childTag}>`,
));
const newField = /** @type {FormGroup} */ (
await fixture(html`<${childTag} name="lastName"></${childTag}>`)
);
const { _inputNode } = getFormControlMembers(el);
// @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
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}>
<${childTag} name="lastName"></${childTag}>
<${tag} name="newfieldset">${inputSlots}</${tag}>
</${tag}>
`));
`)
);
const newFieldset = /** @type {FormGroup} */ (el.querySelector(tagString));
el.formElements.lastName.modelValue = 'Bar';
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 () => {
const el = /** @type {FormGroup} */ (await fixture(html`
const el = /** @type {FormGroup} */ (
await fixture(html`
<${tag}>
<${childTag} name="a" disabled .modelValue="${'x'}"></${childTag}>
<${childTag} name="b" .modelValue="${'x'}"></${childTag}>
@ -313,7 +313,8 @@ export function runFormGroupMixinSuite(cfg = {}) {
<${childTag} name="e" .modelValue="${'x'}"></${childTag}>
</${tag}>
</${tag}>
`));
`)
);
expect(el.modelValue).to.deep.equal({
b: 'x',
newFieldset: {
@ -323,12 +324,14 @@ export function runFormGroupMixinSuite(cfg = {}) {
});
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}>
<${childTag} name="firstName" .modelValue=${'foo'}></${childTag}>
<${childTag} name="lastName" .modelValue=${'bar'}></${childTag}>
</${tag}>
`));
`)
);
const initState = {
firstName: 'foo',
lastName: 'bar',
@ -343,9 +346,9 @@ export function runFormGroupMixinSuite(cfg = {}) {
});
it('disables/enables all its formElements if it becomes disabled/enabled', async () => {
const el = /** @type {FormGroup} */ (await fixture(
html`<${tag} disabled>${inputSlots}</${tag}>`,
));
const el = /** @type {FormGroup} */ (
await fixture(html`<${tag} disabled>${inputSlots}</${tag}>`)
);
expect(el.formElements.color.disabled).to.be.true;
expect(el.formElements['hobbies[]'][0].disabled).to.be.true;
expect(el.formElements['hobbies[]'][1].disabled).to.be.true;
@ -358,11 +361,13 @@ export function runFormGroupMixinSuite(cfg = {}) {
});
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} name="sub" disabled>${inputSlots}</${tag}>
</${tag}>
`));
`)
);
expect(el.disabled).to.equal(false);
expect(el.formElements.sub.disabled).to.be.true;
@ -372,11 +377,13 @@ export function runFormGroupMixinSuite(cfg = {}) {
});
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' }}>
<${childTag} name="lastName"></${childTag}>
</${tag}>
`));
`)
);
expect(el.modelValue).to.eql({
lastName: 'Bar',
@ -384,11 +391,13 @@ export function runFormGroupMixinSuite(cfg = {}) {
});
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' }}>
<${childTag} name="lastName"></${childTag}>
</${tag}>
`));
`)
);
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}>
<${childTag} name="color" .validators=${[
new IsCat(),
]} .modelValue=${'blue'}></${childTag}>
</${tag}>
`));
`)
);
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}>
<${childTag} name="color" .validators=${[
new IsCat(),
]} .modelValue=${'blue'}></${childTag}>
</${tag}>
`));
`)
);
expect(el.validationStates.error.FormElementsHaveNoError).to.be.true;
expect(el.formElements.color.validationStates.error.IsCat).to.be.true;
@ -470,14 +483,18 @@ export function runFormGroupMixinSuite(cfg = {}) {
return hasError;
}
}
const el = /** @type {FormGroup} */ (await fixture(html`
const el = /** @type {FormGroup} */ (
await fixture(html`
<${tag} .validators=${[new HasEvenNumberOfChildren()]}>
<${childTag} id="c1" name="c1"></${childTag}>
</${tag}>
`));
const child2 = /** @type {FormGroup} */ (await fixture(html`
`)
);
const child2 = /** @type {FormGroup} */ (
await fixture(html`
<${childTag} name="c2"></${childTag}>
`));
`)
);
expect(el.validationStates.error.HasEvenNumberOfChildren).to.be.true;
el.appendChild(child2);
@ -495,18 +512,18 @@ export function runFormGroupMixinSuite(cfg = {}) {
describe('Interaction states', () => {
it('has false states (dirty, touched, prefilled) on init', async () => {
const fieldset = /** @type {FormGroup} */ (await fixture(
html`<${tag}>${inputSlots}</${tag}>`,
));
const fieldset = /** @type {FormGroup} */ (
await fixture(html`<${tag}>${inputSlots}</${tag}>`)
);
expect(fieldset.dirty).to.equal(false, 'dirty');
expect(fieldset.touched).to.equal(false, 'touched');
expect(fieldset.prefilled).to.equal(false, 'prefilled');
});
it('sets dirty when value changed', async () => {
const fieldset = /** @type {FormGroup} */ (await fixture(
html`<${tag}>${inputSlots}</${tag}>`,
));
const fieldset = /** @type {FormGroup} */ (
await fixture(html`<${tag}>${inputSlots}</${tag}>`)
);
fieldset.formElements['hobbies[]'][0].modelValue = { checked: true, value: 'football' };
expect(fieldset.dirty).to.be.true;
});
@ -540,32 +557,38 @@ export function runFormGroupMixinSuite(cfg = {}) {
});
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}>
<${childTag} name="input1" .modelValue="${'prefilled'}"></${childTag}>
<${childTag} name="input2"></${childTag}>
</${tag}>
`));
`)
);
expect(el.prefilled).to.be.false;
const el2 = /** @type {FormGroup} */ (await fixture(html`
const el2 = /** @type {FormGroup} */ (
await fixture(html`
<${tag}>
<${childTag} name="input1" .modelValue="${'prefilled'}"></${childTag}>
<${childTag} name="input2" .modelValue="${'prefilled'}"></${childTag}>
</${tag}>
`));
`)
);
expect(el2.prefilled).to.be.true;
});
it(`becomes "touched" once the last element of a group becomes blurred by keyboard
interaction (e.g. tabbing through the checkbox-group)`, async () => {
const el = /** @type {FormGroup} */ (await fixture(html`
const el = /** @type {FormGroup} */ (
await fixture(html`
<${tag}>
<label slot="label">My group</label>
<${childTag} name="myGroup[]" label="Option 1" value="1"></${childTag}>
<${childTag} name="myGroup[]" label="Option 2" value="2"></${childTag}>
</${tag}>
`));
`)
);
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
keyboard interaction (e.g. focus is moved inside the group and user clicks somewhere outside
the group)`, async () => {
const el = /** @type {FormGroup} */ (await fixture(html`
const el = /** @type {FormGroup} */ (
await fixture(html`
<${tag}>
<${childTag} name="input1"></${childTag}>
<${childTag} name="input2"></${childTag}>
</${tag}>
`));
const el2 = /** @type {FormGroup} */ (await fixture(html`
`)
);
const el2 = /** @type {FormGroup} */ (
await fixture(html`
<${tag}>
<${childTag} name="input1"></${childTag}>
<${childTag} name="input2"></${childTag}>
</${tag}>
`));
`)
);
const outside = /** @type {HTMLButtonElement} */ (await fixture(
html`<button>outside</button>`,
));
const outside = /** @type {HTMLButtonElement} */ (
await fixture(html`<button>outside</button>`)
);
outside.click();
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(
html`<button>outside</button>`,
));
const el = /** @type {FormGroup} */ (await fixture(html`
const outSideButton = /** @type {FormGroup} */ (
await fixture(html`<button>outside</button>`)
);
const el = /** @type {FormGroup} */ (
await fixture(html`
<${tag} .validators=${[new Input1IsTen()]}>
<${childTag} name="input1" .validators=${[new IsNumber()]}></${childTag}>
</${tag}>
`));
`)
);
const input1 = /** @type {FormChild} */ (el.querySelector('[name=input1]'));
input1.modelValue = 2;
input1.focus();
@ -657,15 +686,17 @@ export function runFormGroupMixinSuite(cfg = {}) {
return hasError;
}
}
const outSideButton = /** @type {FormGroup} */ (await fixture(
html`<button>outside</button>`,
));
const el = /** @type {FormGroup} */ (await fixture(html`
const outSideButton = /** @type {FormGroup} */ (
await fixture(html`<button>outside</button>`)
);
const el = /** @type {FormGroup} */ (
await fixture(html`
<${tag} .validators=${[new Input1IsTen()]}>
<${childTag} name="input1" .validators=${[new IsNumber()]}></${childTag}>
<${childTag} name="input2" .validators=${[new IsNumber()]}></${childTag}>
</${tag}>
`));
`)
);
const inputs = /** @type {FormChild[]} */ (Array.from(el.querySelectorAll(childTagString)));
inputs[1].modelValue = 2; // make it dirty
inputs[1].focus();
@ -677,20 +708,24 @@ export function runFormGroupMixinSuite(cfg = {}) {
});
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' }}">
<${childTag} name="input1" ></${childTag}>
<${childTag} name="input2"></${childTag}>
</${tag}>
`));
`)
);
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' }}">
<${childTag} name="input1" ></${childTag}>
<${childTag} name="input2"></${childTag}>
</${tag}>
`));
`)
);
expect(el2.dirty).to.be.false;
});
});
@ -698,9 +733,9 @@ export function runFormGroupMixinSuite(cfg = {}) {
// TODO: this should be tested in FormGroupMixin
describe('serializedValue', () => {
it('use form elements serializedValue', async () => {
const fieldset = /** @type {FormGroup} */ (await fixture(
html`<${tag}>${inputSlots}</${tag}>`,
));
const fieldset = /** @type {FormGroup} */ (
await fixture(html`<${tag}>${inputSlots}</${tag}>`)
);
fieldset.formElements['hobbies[]'][0].serializer = /** @param {?} v */ v =>
`${v.value}-serialized`;
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 () => {
const fieldset = /** @type {FormGroup} */ (await fixture(
html`<${tag}>${inputSlots}</${tag}>`,
));
const fieldset = /** @type {FormGroup} */ (
await fixture(html`<${tag}>${inputSlots}</${tag}>`)
);
fieldset.formElements['hobbies[]'][0].modelValue = { checked: false, value: 'chess' };
fieldset.formElements['hobbies[]'][1].modelValue = { checked: false, value: 'rugby' };
fieldset.formElements['gender[]'][0].modelValue = { checked: false, value: 'male' };
@ -742,21 +777,25 @@ export function runFormGroupMixinSuite(cfg = {}) {
});
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}>
<${childTag} name="price"></${childTag}>
</${tag}>`));
</${tag}>`)
);
fieldset.formElements.price.modelValue = 0;
expect(fieldset.serializedValue).to.deep.equal({ price: 0 });
});
it('allows for nested fieldsets', async () => {
const fieldset = /** @type {FormGroup} */ (await fixture(html`
const fieldset = /** @type {FormGroup} */ (
await fixture(html`
<${tag} name="userData">
<${childTag} name="comment"></${childTag}>
<${tag} name="newfieldset">${inputSlots}</${tag}>
</${tag}>
`));
`)
);
const newFieldset = /** @type {FormGroup} */ (fieldset.querySelector(tagString));
newFieldset.formElements['hobbies[]'][0].modelValue = { checked: false, value: 'chess' };
newFieldset.formElements['hobbies[]'][1].modelValue = { checked: false, value: 'rugby' };
@ -785,12 +824,14 @@ export function runFormGroupMixinSuite(cfg = {}) {
});
it('does not serialize disabled values', async () => {
const fieldset = /** @type {FormGroup} */ (await fixture(html`
const fieldset = /** @type {FormGroup} */ (
await fixture(html`
<${tag}>
<${childTag} name="custom[]"></${childTag}>
<${childTag} name="custom[]"></${childTag}>
</${tag}>
`));
`)
);
fieldset.formElements['custom[]'][0].modelValue = 'custom 1';
fieldset.formElements['custom[]'][1].disabled = true;
@ -800,12 +841,14 @@ export function runFormGroupMixinSuite(cfg = {}) {
});
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">
<${childTag} name="comment"></${childTag}>
<${tag} name="newfieldset">${inputSlots}</${tag}>
</${tag}>
`));
`)
);
const newFieldset = /** @type {FormGroup} */ (fieldset.querySelector(tagString));
fieldset.formElements.comment.modelValue = 'Foo';
@ -848,11 +891,13 @@ export function runFormGroupMixinSuite(cfg = {}) {
});
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}>
<${childTag} name="foo" .modelValue=${'qux'}></${childTag}>
</${tag}>
`));
`)
);
expect(fieldset.serializedValue.foo).to.equal('qux');
fieldset.formElements[0].name = 'bar';
await fieldset.updateComplete;
@ -863,11 +908,13 @@ export function runFormGroupMixinSuite(cfg = {}) {
describe('Reset', () => {
it('restores default values if changes were made', async () => {
const el = /** @type {FormGroup} */ (await fixture(html`
const el = /** @type {FormGroup} */ (
await fixture(html`
<${tag}>
<${childTag} id="firstName" name="firstName" .modelValue="${'Foo'}"></${childTag}>
</${tag}>
`));
`)
);
await /** @type {FormChild} */ (el.querySelector(childTagString)).updateComplete;
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 () => {
const el = /** @type {FormGroup} */ (await fixture(html`
const el = /** @type {FormGroup} */ (
await fixture(html`
<${tag}>
<${childTag} id="firstName" name="firstName[]" .modelValue="${'Foo'}"></${childTag}>
</${tag}>
`));
`)
);
await /** @type {FormChild} */ (el.querySelector(childTagString)).updateComplete;
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 () => {
const el = /** @type {FormGroup} */ (await fixture(html`
const el = /** @type {FormGroup} */ (
await fixture(html`
<${tag}>
<${tag} id="name" name="name[]">
<${childTag} id="firstName" name="firstName" .modelValue="${'Foo'}"></${childTag}>
</${tag}>
</${tag}>
`));
`)
);
await Promise.all([
/** @type {FormChild} */ (el.querySelector(tagString)).updateComplete,
/** @type {FormChild} */ (el.querySelector(childTagString)).updateComplete,
@ -928,9 +979,9 @@ export function runFormGroupMixinSuite(cfg = {}) {
});
it('clears interaction state', async () => {
const el = /** @type {FormGroup} */ (await fixture(
html`<${tag} touched dirty>${inputSlots}</${tag}>`,
));
const el = /** @type {FormGroup} */ (
await fixture(html`<${tag} touched dirty>${inputSlots}</${tag}>`)
);
// Safety check initially
// @ts-ignore [allow-protected] in test
el._setValueForAllFormElements('prefilled', true);
@ -957,9 +1008,9 @@ export function runFormGroupMixinSuite(cfg = {}) {
});
it('clears submitted state', async () => {
const fieldset = /** @type {FormGroup} */ (await fixture(
html`<${tag}>${inputSlots}</${tag}>`,
));
const fieldset = /** @type {FormGroup} */ (
await fixture(html`<${tag}>${inputSlots}</${tag}>`)
);
fieldset.submitted = true;
fieldset.resetGroup();
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()]}>
<${childTag} name="color" .validators=${[new IsCat()]}></${childTag}>
<${childTag} name="color2"></${childTag}>
</${tag}>
`));
`)
);
expect(el.hasFeedbackFor).to.deep.equal(['error']);
expect(el.validationStates.error.ColorContainsA).to.be.true;
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 () => {
const el = /** @type {FormGroup} */ (await fixture(html`
const el = /** @type {FormGroup} */ (
await fixture(html`
<${tag}>
<${childTag} name="child[]" .modelValue="${'foo1'}">
</${childTag}>
<${childTag} name="child[]" .modelValue="${'bar1'}">
</${childTag}>
</${tag}>
`));
`)
);
await el.updateComplete;
el.modelValue['child[]'] = ['foo2', 'bar2'];
// @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 () => {
const el = /** @type {FormGroup} */ (await fixture(html`
const el = /** @type {FormGroup} */ (
await fixture(html`
<${tag}>
<${childTag} name="child[]" .modelValue="${'foo1'}">
</${childTag}>
</${tag}>
`));
`)
);
el.modelValue['child[]'] = ['foo2'];
const childEl = /** @type {FormGroup} */ (await fixture(html`
const childEl = /** @type {FormGroup} */ (
await fixture(html`
<${childTag} name="child[]" .modelValue="${'bar1'}">
</${childTag}>
`));
`)
);
el.appendChild(childEl);
// @ts-ignore [allow-protected] in test
expect(el._initialModelValue['child[]']).to.eql(['foo1', 'bar1']);
@ -1057,14 +1116,16 @@ export function runFormGroupMixinSuite(cfg = {}) {
describe('resetGroup method', () => {
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="childFieldset">
<${childTag} name="child[]" .modelValue="${'foo1'}">
</${childTag}>
</${tag}>
</${tag}>
`));
`)
);
const childFieldsetEl = el.querySelector(tagString);
// @ts-expect-error
const resetGroupSpy = sinon.spy(childFieldsetEl, 'resetGroup');
@ -1073,14 +1134,16 @@ export function runFormGroupMixinSuite(cfg = {}) {
});
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="childFieldset">
<${childTag} name="child[]" .modelValue="${'foo1'}">
</${childTag}>
</${tag}>
</${tag}>
`));
`)
);
const childFieldsetEl = /** @type {FormChild} */ (el.querySelector(childTagString));
const resetSpy = sinon.spy(childFieldsetEl, 'reset');
el.resetGroup();
@ -1090,14 +1153,16 @@ export function runFormGroupMixinSuite(cfg = {}) {
describe('clearGroup method', () => {
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="childFieldset">
<${childTag} name="child[]" .modelValue="${'foo1'}">
</${childTag}>
</${tag}>
</${tag}>
`));
`)
);
const childFieldsetEl = el.querySelector(tagString);
// @ts-expect-error
const clearGroupSpy = sinon.spy(childFieldsetEl, 'clearGroup');
@ -1106,14 +1171,16 @@ export function runFormGroupMixinSuite(cfg = {}) {
});
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="childFieldset">
<${childTag} name="child[]" .modelValue="${'foo1'}">
</${childTag}>
</${tag}>
</${tag}>
`));
`)
);
const childFieldsetEl = /** @type {FormChild} */ (el.querySelector(childTagString));
const clearSpy = sinon.spy(childFieldsetEl, 'clear');
el.clearGroup();
@ -1121,14 +1188,16 @@ export function runFormGroupMixinSuite(cfg = {}) {
});
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="childFieldset">
<${childTag} name="child" .modelValue="${'foo1'}">
</${childTag}>
</${tag}>
</${tag}>
`));
`)
);
el.clearGroup();
expect(
/** @type {FormChild} */ (el.querySelector('[name="child"]')).modelValue,
@ -1139,9 +1208,9 @@ export function runFormGroupMixinSuite(cfg = {}) {
describe('Accessibility', () => {
it('has role="group" set', async () => {
const fieldset = /** @type {FormGroup} */ (await fixture(
html`<${tag}>${inputSlots}</${tag}>`,
));
const fieldset = /** @type {FormGroup} */ (
await fixture(html`<${tag}>${inputSlots}</${tag}>`)
);
fieldset.formElements['hobbies[]'][0].modelValue = { checked: false, value: 'chess' };
fieldset.formElements['hobbies[]'][1].modelValue = { checked: false, value: 'rugby' };
fieldset.formElements['gender[]'][0].modelValue = { checked: false, value: 'male' };
@ -1152,15 +1221,17 @@ export function runFormGroupMixinSuite(cfg = {}) {
});
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}>
<label slot="label">My Label</label>
${inputSlots}
</${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.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
based on prefilled model/seriazedValue`, async () => {
const el = /** @type {DynamicCWrapper} */ (await fixture(html`
const el = /** @type {DynamicCWrapper} */ (
await fixture(html`
<${dynamicChildrenTag}
.fields="${['firstName', 'lastName']}"
.modelValue="${{ firstName: 'foo', lastName: 'bar' }}"
>
</${dynamicChildrenTag}>
`));
`)
);
await el.updateComplete;
const fieldset = /** @type {FormGroup} */ (
/** @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[1].modelValue).to.equal('bar');
const el2 = /** @type {DynamicCWrapper} */ (await fixture(html`
const el2 = /** @type {DynamicCWrapper} */ (
await fixture(html`
<${dynamicChildrenTag}
.fields="${['firstName', 'lastName']}"
.serializedValue="${{ firstName: 'foo', lastName: 'bar' }}"
>
</${dynamicChildrenTag}>
`));
`)
);
await el2.updateComplete;
const fieldset2 = /** @type {FormGroup} */ (
/** @type {ShadowRoot} */ (el2.shadowRoot).querySelector(tagString)
@ -1235,10 +1310,12 @@ export function runFormGroupMixinSuite(cfg = {}) {
it(`when rendering children delayed, sets their values
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}>
`));
`)
);
await el.updateComplete;
const fieldset = /** @type {FormGroup} */ (
/** @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[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}>
`));
`)
);
await el2.updateComplete;
const fieldset2 = /** @type {FormGroup} */ (
/** @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
prefilled model/seriazedValue`, async () => {
const el = /** @type {DynamicCWrapper} */ (await fixture(html`
const el = /** @type {DynamicCWrapper} */ (
await fixture(html`
<${dynamicChildrenTag} .fields="${['firstName']}" .modelValue="${{
firstName: 'foo',
lastName: 'bar',
}}">
</${dynamicChildrenTag}>
`));
`)
);
await el.updateComplete;
const fieldset = /** @type {FormGroup} */ (
/** @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[1].modelValue).to.equal('bar');
const el2 = /** @type {DynamicCWrapper} */ (await fixture(html`
const el2 = /** @type {DynamicCWrapper} */ (
await fixture(html`
<${dynamicChildrenTag} .fields="${['firstName']}" .serializedValue="${{
firstName: 'foo',
lastName: 'bar',
}}">
</${dynamicChildrenTag}>
`));
`)
);
await el2.updateComplete;
const fieldset2 = /** @type {FormGroup} */ (
/** @type {ShadowRoot} */ (el2.shadowRoot).querySelector(tagString)
@ -1305,13 +1388,15 @@ export function runFormGroupMixinSuite(cfg = {}) {
expect(elm.prefilled).to.be.true;
}
const el = /** @type {DynamicCWrapper} */ (await fixture(html`
const el = /** @type {DynamicCWrapper} */ (
await fixture(html`
<${dynamicChildrenTag} .fields="${['firstName']}" .modelValue="${{
firstName: 'foo',
lastName: 'bar',
}}">
</${dynamicChildrenTag}>
`));
`)
);
await el.updateComplete;
const fieldset = /** @type {FormGroup} */ (
/** @type {ShadowRoot} */ (el.shadowRoot).querySelector(tagString)
@ -1324,13 +1409,15 @@ export function runFormGroupMixinSuite(cfg = {}) {
expectInteractionStatesToBeCorrectFor(fieldset.formElements[1]);
expectInteractionStatesToBeCorrectFor(fieldset);
const el2 = /** @type {DynamicCWrapper} */ (await fixture(html`
const el2 = /** @type {DynamicCWrapper} */ (
await fixture(html`
<${dynamicChildrenTag} .fields="${['firstName']}" .serializedValue="${{
firstName: 'foo',
lastName: 'bar',
}}">
</${dynamicChildrenTag}>
`));
`)
);
await el2.updateComplete;
const fieldset2 = /** @type {FormGroup} */ (
/** @type {ShadowRoot} */ (el2.shadowRoot).querySelector(tagString)
@ -1345,13 +1432,15 @@ export function runFormGroupMixinSuite(cfg = {}) {
});
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="${{
firstName: 'foo',
lastName: 'bar',
}}">
</${dynamicChildrenTag}>
`));
`)
);
await el.updateComplete;
const fieldset = /** @type {FormGroup} */ (
/** @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[1].modelValue).to.equal('winsAsWell');
const el2 = /** @type {DynamicCWrapper} */ (await fixture(html`
const el2 = /** @type {DynamicCWrapper} */ (
await fixture(html`
<${dynamicChildrenTag} .serializedValue="${{
firstName: 'foo',
lastName: 'bar',
}}">
</${dynamicChildrenTag}>
`));
`)
);
await el2.updateComplete;
const fieldset2 = /** @type {FormGroup} */ (
/** @type {ShadowRoot} */ (el2.shadowRoot).querySelector(tagString)

View file

@ -1,9 +1,11 @@
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 { 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:
@ -74,9 +76,11 @@ describe('FocusMixin', () => {
const tag = unsafeStatic(tagString);
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}>
`));
`)
);
// @ts-ignore [allow-protected] in test
const { _focusableNode } = el;
@ -87,9 +91,11 @@ describe('FocusMixin', () => {
});
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}>
`));
`)
);
el.focus();
await el.updateComplete;
@ -101,9 +107,11 @@ describe('FocusMixin', () => {
});
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}>
`));
`)
);
// @ts-ignore [allow-protected] in test
const { _focusableNode } = el;
@ -115,9 +123,11 @@ describe('FocusMixin', () => {
});
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}>
`));
`)
);
setTimeout(() => el.focus());
const focusEv = await oneEvent(el, 'focus');
expect(focusEv).to.be.instanceOf(Event);
@ -137,9 +147,11 @@ describe('FocusMixin', () => {
});
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}>
`));
`)
);
setTimeout(() => el.focus());
const focusinEv = await oneEvent(el, 'focusin');
expect(focusinEv).to.be.instanceOf(Event);
@ -160,9 +172,11 @@ describe('FocusMixin', () => {
describe('Having :focus-visible within', () => {
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}>
`));
`)
);
// @ts-ignore [allow-protected] in test
const { _focusableNode } = el;
@ -204,9 +218,11 @@ describe('FocusMixin', () => {
});
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}>
`));
`)
);
// @ts-ignore [allow-protected] in test
const { _focusableNode } = el;
@ -251,9 +267,11 @@ describe('FocusMixin', () => {
});
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}>
`));
`)
);
// @ts-ignore [allow-protected] in test
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 { getFormControlMembers } from '@lion/form-core/test-helpers';
import sinon from 'sinon';
@ -30,108 +31,130 @@ describe('FormControlMixin', () => {
describe('Label and helpText api', () => {
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}>
`));
`)
);
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}
.label=${'Email address'}
>${inputSlot}
</${tag}>`));
</${tag}>`)
);
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}>
<label slot="label">Email address</label>
${inputSlot}
</${tag}>`));
</${tag}>`)
);
expect(elElem.label).to.equal('Email address', 'as an element');
});
it('has a label that supports inner html', async () => {
const el = /** @type {FormControlMixinClass} */ (await fixture(html`
const el = /** @type {FormControlMixinClass} */ (
await fixture(html`
<${tag}>
<label slot="label">Email <span>address</span></label>
${inputSlot}
</${tag}>`));
</${tag}>`)
);
expect(el.label).to.equal('Email address');
});
it('only takes label of direct child', async () => {
const el = /** @type {FormControlMixinClass} */ (await fixture(html`
const el = /** @type {FormControlMixinClass} */ (
await fixture(html`
<${tag}>
<${tag} label="Email address">
${inputSlot}
</${tag}>
</${tag}>`));
</${tag}>`)
);
expect(el.label).to.equal('');
});
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}>
`));
`)
);
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}
.helpText=${'We will not send you any spam'}
>${inputSlot}
</${tag}>`));
</${tag}>`)
);
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}>
<div slot="help-text">We will not send you any spam</div>
${inputSlot}
</${tag}>`));
</${tag}>`)
);
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 () => {
const el = /** @type {FormControlMixinClass} */ (await fixture(html`
const el = /** @type {FormControlMixinClass} */ (
await fixture(html`
<${tag}>
<div slot="help-text">We will not send you any <span>spam</span></div>
${inputSlot}
</${tag}>`));
</${tag}>`)
);
expect(el.helpText).to.equal('We will not send you any spam');
});
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} help-text="We will not send you any spam">
${inputSlot}
</${tag}>
</${tag}>`));
</${tag}>`)
);
expect(el.helpText).to.equal('');
});
});
describe('Accessibility', () => {
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">
<${tag} help-text="This element will be disconnected/reconnected">${inputSlot}</${tag}>
</div>
`));
`)
);
const el = /** @type {FormControlMixinClass} */ (wrapper.querySelector(tagString));
const { _inputNode } = getFormControlMembers(el);
const labelIdsBefore = /** @type {string} */ (_inputNode.getAttribute('aria-labelledby'));
const descriptionIdsBefore = /** @type {string} */ (_inputNode.getAttribute(
'aria-describedby',
));
const descriptionIdsBefore = /** @type {string} */ (
_inputNode.getAttribute('aria-describedby')
);
// Reconnect
wrapper.removeChild(el);
wrapper.appendChild(el);
const labelIdsAfter = /** @type {string} */ (_inputNode.getAttribute('aria-labelledby'));
const descriptionIdsAfter = /** @type {string} */ (_inputNode.getAttribute(
'aria-describedby',
));
const descriptionIdsAfter = /** @type {string} */ (
_inputNode.getAttribute('aria-describedby')
);
expect(labelIdsBefore).to.equal(labelIdsAfter);
expect(descriptionIdsBefore).to.equal(descriptionIdsAfter);
@ -139,11 +162,13 @@ describe('FormControlMixin', () => {
it('clicking the label should call `_onLabelClick`', async () => {
const spy = sinon.spy();
const el = /** @type {FormControlMixinClass} */ (await fixture(html`
const el = /** @type {FormControlMixinClass} */ (
await fixture(html`
<${tag} ._onLabelClick="${spy}">
${inputSlot}
</${tag}>
`));
`)
);
const { _labelNode } = getFormControlMembers(el);
expect(spy).to.not.have.been.called;
@ -232,7 +257,8 @@ describe('FormControlMixin', () => {
describe('Adding extra labels and descriptions', () => {
it(`supports centrally orchestrated labels/descriptions via addToAriaLabelledBy() /
removeFromAriaLabelledBy() / addToAriaDescribedBy() / removeFromAriaDescribedBy()`, async () => {
const wrapper = /** @type {HTMLElement} */ (await fixture(html`
const wrapper = /** @type {HTMLElement} */ (
await fixture(html`
<div id="wrapper">
<${tag}>
${inputSlot}
@ -241,7 +267,8 @@ describe('FormControlMixin', () => {
</${tag}>
<div id="additionalLabel"> This also needs to be read whenever the input has focus</div>
<div id="additionalDescription"> Same for this </div>
</div>`));
</div>`)
);
const el = /** @type {FormControlMixinClass} */ (wrapper.querySelector(tagString));
const { _inputNode } = getFormControlMembers(el);
@ -257,9 +284,9 @@ describe('FormControlMixin', () => {
expect(/** @type {string} */ (_inputNode.getAttribute('aria-labelledby'))).to.contain(
`label-${inputId}`,
);
const additionalLabel = /** @type {HTMLElement} */ (wrapper.querySelector(
'#additionalLabel',
));
const additionalLabel = /** @type {HTMLElement} */ (
wrapper.querySelector('#additionalLabel')
);
el.addToAriaLabelledBy(additionalLabel);
await el.updateComplete;
let labelledbyAttr = /** @type {string} */ (_inputNode.getAttribute('aria-labelledby'));
@ -392,13 +419,15 @@ describe('FormControlMixin', () => {
it('redispatches one event from host', async () => {
const formSpy = 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="fieldset" ._repropagationRole=${'form-group'} @model-value-changed=${fieldsetSpy}>
<${tag} name="field"></${tag}>
</${groupTag}>
</${groupTag}>
`));
`)
);
const fieldsetEl = formEl.querySelector('[name=fieldset]');
expect(fieldsetSpy.callCount).to.equal(1);

View file

@ -1,5 +1,5 @@
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';
runRegistrationSuite({

View file

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

View file

@ -1,5 +1,6 @@
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 { SyncUpdatableMixin } from '../../src/utils/SyncUpdatableMixin.js';
@ -43,9 +44,9 @@ describe('SyncUpdatableMixin', () => {
const tagString = defineCE(UpdatableImplementation);
const tag = unsafeStatic(tagString);
const el = /** @type {UpdatableImplementation} */ (fixtureSync(
html`<${tag} prop-b="b"></${tag}>`,
));
const el = /** @type {UpdatableImplementation} */ (
fixtureSync(html`<${tag} prop-b="b"></${tag}>`)
);
// Getters setters work as expected, without running property effects
expect(el.propA).to.equal('init-a');
@ -102,9 +103,9 @@ describe('SyncUpdatableMixin', () => {
const tagString = defineCE(UpdatableImplementation);
const tag = unsafeStatic(tagString);
const el = /** @type {UpdatableImplementation} */ (fixtureSync(
html`<${tag} prop-b="b" .propA="${'a'}"></${tag}>`,
));
const el = /** @type {UpdatableImplementation} */ (
fixtureSync(html`<${tag} prop-b="b" .propA="${'a'}"></${tag}>`)
);
// Derived
expect(el.derived).to.be.undefined;
@ -114,19 +115,19 @@ describe('SyncUpdatableMixin', () => {
expect(el.derived).to.equal('ab');
expect(hasCalledRunPropertyEffect).to.be.true;
const el2 = /** @type {UpdatableImplementation} */ (await fixture(
html`<${tag} .propA="${'a'}"></${tag}>`,
));
const el2 = /** @type {UpdatableImplementation} */ (
await fixture(html`<${tag} .propA="${'a'}"></${tag}>`)
);
expect(el2.derived).to.equal('ainit-b');
const el3 = /** @type {UpdatableImplementation} */ (await fixture(
html`<${tag} .propB="${'b'}"></${tag}>`,
));
const el3 = /** @type {UpdatableImplementation} */ (
await fixture(html`<${tag} .propB="${'b'}"></${tag}>`)
);
expect(el3.derived).to.equal('init-ab');
const el4 = /** @type {UpdatableImplementation} */ (await fixture(
html`<${tag} .propA=${'a'} .propB="${'b'}"></${tag}>`,
));
const el4 = /** @type {UpdatableImplementation} */ (
await fixture(html`<${tag} .propA=${'a'} .propB="${'b'}"></${tag}>`)
);
expect(el4.derived).to.equal('ab');
});
@ -150,8 +151,8 @@ describe('SyncUpdatableMixin', () => {
* @param {string} name
* @param {*} oldValue
*/
requestUpdateInternal(name, oldValue) {
super.requestUpdateInternal(name, oldValue);
requestUpdate(name, oldValue) {
super.requestUpdate(name, oldValue);
if (name === 'prop') {
propChangedCount += 1;
}
@ -223,9 +224,9 @@ describe('SyncUpdatableMixin', () => {
const tagString = defineCE(UpdatableImplementation);
const tag = unsafeStatic(tagString);
const el = /** @type {UpdatableImplementation} */ (fixtureSync(
html`<${tag} prop-b="b" .propA="${'a'}"></${tag}>`,
));
const el = /** @type {UpdatableImplementation} */ (
fixtureSync(html`<${tag} prop-b="b" .propA="${'a'}"></${tag}>`)
);
const spy = sinon.spy(el, '_runPropertyEffect');
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 { browserDetection } from '@lion/core';
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 { getFormControlMembers } from '@lion/form-core/test-helpers';
import { Required } from '../../src/validate/validators/Required.js';
@ -31,9 +32,9 @@ describe('Required validation', async () => {
const validator = new Required();
it('get aria-required attribute if element is part of the right tag names', async () => {
const el = /** @type {FormControlHost & HTMLElement} */ (await fixture(
html`<${tag}></${tag}>`,
));
const el = /** @type {FormControlHost & HTMLElement} */ (
await fixture(html`<${tag}></${tag}>`)
);
Required._compatibleTags.forEach(tagName => {
inputNodeTag = /** @type {HTMLElementWithValue} */ (document.createElement(tagName));
@ -53,9 +54,9 @@ describe('Required validation', async () => {
expect(_inputNode).to.not.have.attribute('aria-required');
});
it('get aria-required attribute if element is part of the right roles', async () => {
const el = /** @type {FormControlHost & HTMLElement} */ (await fixture(
html`<${tag}></${tag}>`,
));
const el = /** @type {FormControlHost & HTMLElement} */ (
await fixture(html`<${tag}></${tag}>`)
);
Required._compatibleRoles.forEach(role => {
// @ts-ignore

View file

@ -1,5 +1,6 @@
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 { ValidateMixin } from '../../src/validate/ValidateMixin.js';
import { Validator } from '../../src/validate/Validator.js';
@ -171,9 +172,11 @@ describe('Validator', () => {
const connectSpy = sinon.spy(myVal, 'onFormControlConnect');
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}>
`));
`)
);
expect(connectSpy.callCount).to.equal(1);
expect(connectSpy.calledWith(el)).to.equal(true);

View file

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

View file

@ -1,5 +1,5 @@
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 { ValidateHost } from './validate/ValidateMixinTypes';
import { FormControlHost } from './FormControlMixinTypes';

View file

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

View file

@ -10,7 +10,7 @@ export declare interface SyncUpdatableNamespace {
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:
* - [member order independence](https://github.com/webcomponents/gold-standard/wiki/Member-Order-Independence)
* - 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
* run property effects / events when no change happened
* 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 {*} 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 './helpers/umbrella-form.js';
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 { 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 () => {
const el = /** @type {UmbrellaForm} */ (await fixture(
const el = /** @type {UmbrellaForm} */ (
await fixture(
html`<umbrella-form .serializedValue="${fullyPrefilledSerializedValue}"></umbrella-form>`,
));
)
);
await el.updateComplete;
const formEl = el._lionFormNode;
@ -125,9 +128,11 @@ describe(`Submitting/Resetting/Clearing Form`, async () => {
// Wait till ListboxMixin properly clears
it('calling clearGroup() should clear all fields', async () => {
const el = /** @type {UmbrellaForm} */ (await fixture(
const el = /** @type {UmbrellaForm} */ (
await fixture(
html`<umbrella-form .serializedValue="${fullyPrefilledSerializedValue}"></umbrella-form>`,
));
)
);
await el.updateComplete;
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 './helpers/umbrella-form.js';
@ -64,7 +65,8 @@ describe('Form Integrations', () => {
describe('Form Integrations', () => {
it('does not become dirty when elements are prefilled', async () => {
const el = /** @type {UmbrellaForm} */ (await fixture(
const el = /** @type {UmbrellaForm} */ (
await fixture(
html`<umbrella-form
.serializedValue="${{
full_name: { first_name: '', last_name: '' },
@ -85,7 +87,8 @@ describe('Form Integrations', () => {
comments: '',
}}"
></umbrella-form>`,
));
)
);
await el._lionFormNode.initComplete;
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 { loadDefaultFeedbackMessages } from '@lion/validate-messages';
import { LionInput } from '@lion/input';
@ -41,7 +42,8 @@ describe('Form Validation Integrations', () => {
}
const elTagString = defineCE(ValidateElementCustomTypes);
const elTag = unsafeStatic(elTagString);
const el = /** @type {ValidateElementCustomTypes} */ (await fixture(html`
const el = /** @type {ValidateElementCustomTypes} */ (
await fixture(html`
<${elTag}
.validators=${[
new Required(null, { getMessage: () => 'error' }),
@ -49,7 +51,8 @@ describe('Form Validation Integrations', () => {
new DefaultSuccess(),
]}
>${lightDom}</${elTag}>
`));
`)
);
const { _feedbackNode } = getFormControlMembers(el);
expect(_feedbackNode.feedbackData?.length).to.equal(0);

View file

@ -22,9 +22,9 @@ import '@lion/input-stepper/define';
export class UmbrellaForm extends LitElement {
get _lionFormNode() {
return /** @type {import('@lion/form').LionForm} */ (this.shadowRoot?.querySelector(
'lion-form',
));
return /** @type {import('@lion/form').LionForm} */ (
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
import sinon from 'sinon';
@ -111,9 +112,9 @@ const choiceDispatchesCountOnInteraction = (tagname, count) => {
const tag = unsafeStatic(tagname);
const spy = sinon.spy();
it(getInteractionTitle(count), async () => {
const el = /** @type {HTMLElement & {checked: boolean}} */ (await fixture(
html`<${tag} .choiceValue="${'option'}"></${tag}>`,
));
const el = /** @type {HTMLElement & {checked: boolean}} */ (
await fixture(html`<${tag} .choiceValue="${'option'}"></${tag}>`)
);
el.addEventListener('model-value-changed', spy);
el.checked = true;
expect(spy.callCount).to.equal(count);
@ -161,17 +162,17 @@ const choiceGroupDispatchesCountOnInteraction = (groupTagname, itemTagname, coun
`);
el.addEventListener('model-value-changed', spy);
const option2 = /** @type {HTMLElement & {checked: boolean}} */ (el.querySelector(
`${itemTagname}:nth-child(2)`,
));
const option2 = /** @type {HTMLElement & {checked: boolean}} */ (
el.querySelector(`${itemTagname}:nth-child(2)`)
);
option2.checked = true;
expect(spy.callCount).to.equal(count);
spy.resetHistory();
const option3 = /** @type {HTMLElement & {checked: boolean}} */ (el.querySelector(
`${itemTagname}:nth-child(3)`,
));
const option3 = /** @type {HTMLElement & {checked: boolean}} */ (
el.querySelector(`${itemTagname}:nth-child(3)`)
);
option3.checked = true;
expect(spy.callCount).to.equal(count);
});
@ -233,7 +234,8 @@ describe('lion-select', () => {
it(getInteractionTitle(interactionCount), async () => {
const spy = sinon.spy();
const el = /** @type {LionSelect} */ (await fixture(html`
const el = /** @type {LionSelect} */ (
await fixture(html`
<lion-select>
<select slot="input">
<option value="option1"></option>
@ -241,7 +243,8 @@ describe('lion-select', () => {
<option value="option3"></option>
</select>
</lion-select>
`));
`)
);
el.addEventListener('model-value-changed', spy);
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 el = /** @type {LitElement & FormControl & {value: string} & {registrationComplete: Promise<boolean>} & {formElements: Array.<FormControl & {value: string}>}} */ (await fixture(
html`<${tag} name="${name}">${childrenEl}</${tag}>`,
));
const el =
/** @type {LitElement & FormControl & {value: string} & {registrationComplete: Promise<boolean>} & {formElements: Array.<FormControl & {value: string}>}} */ (
await fixture(html`<${tag} name="${name}">${childrenEl}</${tag}>`)
);
await el.registrationComplete;
el.addEventListener('model-value-changed', spy);

View file

@ -1,6 +1,7 @@
import '@lion/fieldset/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
import sinon from 'sinon';

View file

@ -1,12 +1,5 @@
import {
expect,
fixture as _fixture,
html,
oneEvent,
aTimeout,
unsafeStatic,
defineCE,
} from '@open-wc/testing';
import { expect, fixture as _fixture, oneEvent, aTimeout, defineCE } from '@open-wc/testing';
import { html, unsafeStatic } from 'lit/static-html.js';
import { spy } from 'sinon';
import { LionField } from '@lion/form-core';
import { LionFieldset } from '@lion/fieldset';
@ -61,9 +54,9 @@ describe('<lion-form>', () => {
</form>
</lion-form>
`);
const resetButton = /** @type {HTMLInputElement} */ (withDefaults.querySelector(
'input[type=reset]',
));
const resetButton = /** @type {HTMLInputElement} */ (
withDefaults.querySelector('input[type=reset]')
);
withDefaults.formElements.firstName.modelValue = 'updatedFoo';
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';
/**
@ -53,9 +54,9 @@ describe('sb-action-logger', () => {
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 cueEl = /** @type {HTMLElement} */ (el.shadowRoot?.querySelector(
'.header__log-cue-overlay',
));
const cueEl = /** @type {HTMLElement} */ (
el.shadowRoot?.querySelector('.header__log-cue-overlay')
);
expect(cueEl.classList.contains('header__log-cue-overlay--slide')).to.be.false;
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 () => {
const el = await fixture(html`<sb-action-logger></sb-action-logger>`);
const cueEl = /** @type {HTMLElement} */ (el.shadowRoot?.querySelector(
'.header__log-cue-overlay',
));
const cueEl = /** @type {HTMLElement} */ (
el.shadowRoot?.querySelector('.header__log-cue-overlay')
);
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 firstLogCount = /** @type {HTMLElement} */ (loggerEl.firstElementChild?.querySelector(
'.logger__log-count',
));
const lastLogCount = /** @type {HTMLElement} */ (loggerEl.lastElementChild?.querySelector(
'.logger__log-count',
));
const firstLogCount = /** @type {HTMLElement} */ (
loggerEl.firstElementChild?.querySelector('.logger__log-count')
);
const lastLogCount = /** @type {HTMLElement} */ (
loggerEl.lastElementChild?.querySelector('.logger__log-count')
);
expect(loggerEl.children.length).to.equal(4);
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').nothing} nothing
*/
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';
/**
* @typedef {import('@lion/core').TemplateResult} TemplateResult
* @typedef {(tag: (strings: TemplateStringsArray, ... expr: string[]) => string) => string} TagFunction
*/
/**
* @param {?} wrappedSvgObject
*/
@ -14,7 +19,7 @@ function unwrapSvg(wrappedSvgObject) {
* @param {TemplateResult|nothing} svg
*/
function validateSvg(svg) {
if (!(svg === nothing || svg instanceof TemplateResult)) {
if (!(svg === nothing || isTemplateResult(svg))) {
throw new Error(
'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.ariaLabel = '';
this.iconId = '';
/** @private */
/**
* @private
* @type {TemplateResult|nothing|TagFunction}
*/
this.__svg = nothing;
}
@ -127,7 +135,7 @@ export class LionIcon extends LitElement {
/**
* On IE11, svgs without focusable false appear in the tab order
* so make sure to have <svg focusable="false"> in svg files
* @param {TemplateResult|nothing} svg
* @param {TemplateResult|nothing|TagFunction} svg
*/
set svg(svg) {
this.__svg = svg;
@ -138,6 +146,9 @@ export class LionIcon extends LitElement {
}
}
/**
* @type {TemplateResult|nothing|TagFunction}
*/
get svg() {
return this.__svg;
}

View file

@ -1,5 +1,5 @@
import { nothing, until } from '@lion/core';
import { aTimeout, expect, fixture as _fixture, fixtureSync, html } from '@open-wc/testing';
import { nothing, until, html } from '@lion/core';
import { aTimeout, expect, fixture as _fixture, fixtureSync } from '@open-wc/testing';
import '@lion/icon/define';
import { icons } from '../src/icons.js';
import hammerSvg from './hammer.svg.js';
@ -145,7 +145,7 @@ describe('lion-icon', () => {
await el.updateComplete;
el.svg = nothing;
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 () => {
@ -153,7 +153,7 @@ describe('lion-icon', () => {
await el.updateComplete;
el.svg = nothing;
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 () => {

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,11 @@
import { html, css } from '@lion/core';
import { html, css, render } from '@lion/core';
import { LionInput } from '@lion/input';
import { IsNumber, MinNumber, MaxNumber } from '@lion/form-core';
/**
* @typedef {import('@lion/core').RenderOptions} RenderOptions
*/
/**
* `LionInputStepper` is a class for custom input-stepper element (`<lion-input-stepper>` web component).
*
@ -60,6 +64,9 @@ export class LionInputStepper extends LionInput {
min: this.min,
step: this.step,
};
this.__increment = this.__increment.bind(this);
this.__decrement = this.__decrement.bind(this);
}
connectedCallback() {
@ -69,6 +76,7 @@ export class LionInputStepper extends LionInput {
min: this.min,
step: this.step,
};
this.role = 'spinbutton';
this.addEventListener('keydown', this.__keyDownHandler);
this._inputNode.setAttribute('inputmode', 'decimal');
@ -122,9 +130,8 @@ export class LionInputStepper extends LionInput {
'aria-valuemin': this.values.min,
};
const minMaxValidators = /** @type {(MaxNumber | MinNumber)[]} */ (Object.entries(
ariaAttributes,
)
const minMaxValidators = /** @type {(MaxNumber | MinNumber)[]} */ (
Object.entries(ariaAttributes)
.map(([key, val]) => {
if (val !== Infinity) {
this.setAttribute(key, `${val}`);
@ -132,7 +139,8 @@ export class LionInputStepper extends LionInput {
}
return null;
})
.filter(validator => validator !== null));
.filter(validator => validator !== null)
);
const validators = [new IsNumber(), ...minMaxValidators];
this.defaultValidators.push(...validators);
}
@ -219,13 +227,13 @@ export class LionInputStepper extends LionInput {
*/
__getIncrementButtonNode() {
const renderParent = document.createElement('div');
/** @type {typeof LionInputStepper} */ (this.constructor).render(
render(
this._incrementorTemplate(),
renderParent,
{
/** @type {RenderOptions} */ ({
scopeName: this.localName,
eventContext: this,
},
}),
);
return renderParent.firstElementChild;
}
@ -237,13 +245,13 @@ export class LionInputStepper extends LionInput {
*/
__getDecrementButtonNode() {
const renderParent = document.createElement('div');
/** @type {typeof LionInputStepper} */ (this.constructor).render(
render(
this._decrementorTemplate(),
renderParent,
{
/** @type {RenderOptions} */ ({
scopeName: this.localName,
eventContext: this,
},
}),
);
return renderParent.firstElementChild;
}

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