feat: add types for fieldset and form

This commit is contained in:
Joren Broekema 2020-09-28 12:20:45 +02:00 committed by Thomas Allmer
parent 9a74bfdc6e
commit 6f08e9296b
5 changed files with 86 additions and 58 deletions

View file

@ -0,0 +1,6 @@
---
'@lion/fieldset': minor
'@lion/form': minor
---
Added types for form and fieldset packages.

View file

@ -18,13 +18,14 @@ import { FormGroupMixin } from '@lion/form-core';
* cannot be accessed individually via object keys.
*
* @customElement lion-fieldset
* @extends {LitElement}
*/
// @ts-expect-error https://github.com/microsoft/TypeScript/issues/40110
export class LionFieldset extends FormGroupMixin(LitElement) {
constructor() {
super();
/** @override from FormRegistrarMixin */
/** @override FormRegistrarMixin */
this._isFormOrFieldset = true;
this._repropagationRole = 'fieldset'; // configures FormControlMixin
/** @override FormControlMixin */
this._repropagationRole = 'fieldset';
}
}

View file

@ -1,17 +1,38 @@
import { LionFieldset } from '@lion/fieldset';
const throwFormNodeError = () => {
throw new Error(
'No form node found. Did you put a <form> element inside your custom-form element?',
);
};
/**
* LionForm: form wrapper providing extra features and integration with lion-field elements.
*
* @customElement lion-form
* @extends {LionFieldset}
*/
// eslint-disable-next-line no-unused-vars
export class LionForm extends LionFieldset {
constructor() {
super();
/** @param {Event} ev */
this._submit = ev => {
ev.preventDefault();
ev.stopPropagation();
this.submitGroup();
this.dispatchEvent(new Event('submit', { bubbles: true }));
};
/** @param {Event} ev */
this._reset = ev => {
ev.preventDefault();
ev.stopPropagation();
this.resetGroup();
this.dispatchEvent(new Event('reset', { bubbles: true }));
};
}
connectedCallback() {
if (super.connectedCallback) {
super.connectedCallback();
}
super.connectedCallback();
this.__registerEventsForLionForm();
// @override LionFieldset: makes sure a11y is handled by ._formNode
@ -19,39 +40,32 @@ export class LionForm extends LionFieldset {
}
disconnectedCallback() {
if (super.disconnectedCallback) {
super.disconnectedCallback();
}
super.disconnectedCallback();
this.__teardownEventsForLionForm();
}
get _formNode() {
return this.querySelector('form');
return /** @type {HTMLFormElement} */ (this.querySelector('form'));
}
submit() {
this._formNode.submit();
if (this._formNode) {
this._formNode.submit();
} else {
throwFormNodeError();
}
}
reset() {
this._formNode.reset();
if (this._formNode) {
this._formNode.reset();
} else {
throwFormNodeError();
}
}
__registerEventsForLionForm() {
this._submit = ev => {
ev.preventDefault();
ev.stopPropagation();
this.submitGroup();
this.dispatchEvent(new Event('submit', { bubbles: true }));
};
this._formNode.addEventListener('submit', this._submit);
this._reset = ev => {
ev.preventDefault();
ev.stopPropagation();
this.resetGroup();
this.dispatchEvent(new Event('reset', { bubbles: true }));
};
this._formNode.addEventListener('reset', this._reset);
}

View file

@ -1,6 +1,6 @@
import {
expect,
fixture,
fixture as _fixture,
html,
oneEvent,
aTimeout,
@ -15,6 +15,13 @@ import '@lion/fieldset/lion-fieldset.js';
import '../lion-form.js';
/**
* @typedef {import('../src/LionForm').LionForm} LionForm
* @typedef {import('lit-html').TemplateResult} TemplateResult
*/
const fixture = /** @type {(arg: TemplateResult) => Promise<LionForm>} */ (_fixture);
const childTagString = defineCE(
class extends LionField {
get slots() {
@ -25,40 +32,38 @@ const childTagString = defineCE(
},
);
const childTag = unsafeStatic(childTagString);
const formTagString = 'lion-form';
const formTag = unsafeStatic(formTagString);
describe('<lion-form>', () => {
it('is an instance of LionFieldSet', async () => {
const el = await fixture(html`
<${formTag}>
<form>
</form>
</${formTag}>
<lion-form>
<form></form>
</lion-form>
`);
expect(el).to.be.instanceOf(LionFieldset);
});
it('relies on the native form for its accessible role', async () => {
const el = await fixture(html`
<${formTag}>
<form>
</form>
</${formTag}>
<lion-form>
<form></form>
</lion-form>
`);
expect(el.getAttribute('role')).to.be.null;
});
it('has a custom reset that gets triggered by native reset', async () => {
const withDefaults = await fixture(html`
<${formTag}>
<lion-form>
<form>
<${childTag} name="firstName" .modelValue="${'Foo'}"></${childTag}>
<input type="reset" value="reset-button" />
</form>
</${formTag}>
</lion-form>
`);
const resetButton = 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({
@ -84,11 +89,11 @@ describe('<lion-form>', () => {
it('dispatches reset events', async () => {
const el = await fixture(html`
<${formTag}>
<lion-form>
<form>
<${childTag} name="firstName" .modelValue="${'Foo'}"></${childTag}>
</form>
</${formTag}>
</lion-form>
`);
setTimeout(() => el.reset());
@ -103,27 +108,27 @@ describe('<lion-form>', () => {
it('works with the native submit event (triggered via a button)', async () => {
const submitSpy = spy();
const el = await fixture(html`
<${formTag} @submit=${submitSpy}>
<lion-form @submit=${submitSpy}>
<form>
<button type="submit">submit</button>
</form>
</${formTag}>
</lion-form>
`);
const button = el.querySelector('button');
const button = /** @type {HTMLButtonElement} */ (el.querySelector('button'));
button.click();
expect(submitSpy.callCount).to.equal(1);
});
it('dispatches submit events', async () => {
const el = await fixture(html`
<${formTag}>
<lion-form>
<form>
<button type="submit">submit</button>
</form>
</${formTag}>
</lion-form>
`);
const button = el.querySelector('button');
const button = /** @type {HTMLButtonElement} */ (el.querySelector('button'));
setTimeout(() => button.click());
const submitEv = await oneEvent(el, 'submit');
expect(submitEv).to.be.instanceOf(Event);
@ -135,29 +140,29 @@ describe('<lion-form>', () => {
it('handles internal submit handler before dispatch', async () => {
const el = await fixture(html`
<${formTag}>
<lion-form>
<form>
<button type="submit">submit</button>
</form>
</${formTag}>
</lion-form>
`);
const button = el.querySelector('button');
const button = /** @type {HTMLButtonElement} */ (el.querySelector('button'));
const internalHandlerSpy = spy(el, 'submitGroup');
const dispatchSpy = spy(el, 'dispatchEvent');
await aTimeout();
await aTimeout(0);
button.click();
expect(internalHandlerSpy).to.be.calledBefore(dispatchSpy);
});
it('handles internal submit handler before dispatch', async () => {
const el = await fixture(html`
<${formTag}>
<lion-form>
<form>
<button type="submit">submit</button>
</form>
</${formTag}>
</lion-form>
`);
const button = el.querySelector('button');
const button = /** @type {HTMLButtonElement} */ (el.querySelector('button'));
const internalHandlerSpy = spy(el, 'submitGroup');
const dispatchSpy = spy(el, 'dispatchEvent');
button.click();
@ -167,13 +172,13 @@ describe('<lion-form>', () => {
it('handles internal reset handler before dispatch', async () => {
const el = await fixture(html`
<${formTag}>
<lion-form>
<form>
<button type="reset">submit</button>
</form>
</${formTag}>
</lion-form>
`);
const button = el.querySelector('button');
const button = /** @type {HTMLButtonElement} */ (el.querySelector('button'));
const internalHandlerSpy = spy(el, 'resetGroup');
const dispatchSpy = spy(el, 'dispatchEvent');
button.click();

View file

@ -19,6 +19,8 @@
"packages/button/src/**/*.js",
"packages/checkbox-group/**/*.js",
"packages/core/**/*.js",
"packages/fieldset/**/*.js",
"packages/form/**/*.js",
"packages/form-core/**/*.js",
"packages/input/**/*.js",
"packages/input-amount/**/*.js",