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. * cannot be accessed individually via object keys.
* *
* @customElement lion-fieldset * @customElement lion-fieldset
* @extends {LitElement}
*/ */
// @ts-expect-error https://github.com/microsoft/TypeScript/issues/40110
export class LionFieldset extends FormGroupMixin(LitElement) { export class LionFieldset extends FormGroupMixin(LitElement) {
constructor() { constructor() {
super(); super();
/** @override from FormRegistrarMixin */ /** @override FormRegistrarMixin */
this._isFormOrFieldset = true; this._isFormOrFieldset = true;
this._repropagationRole = 'fieldset'; // configures FormControlMixin /** @override FormControlMixin */
this._repropagationRole = 'fieldset';
} }
} }

View file

@ -1,17 +1,38 @@
import { LionFieldset } from '@lion/fieldset'; 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. * LionForm: form wrapper providing extra features and integration with lion-field elements.
* *
* @customElement lion-form * @customElement lion-form
* @extends {LionFieldset}
*/ */
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
export class LionForm extends LionFieldset { 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() { connectedCallback() {
if (super.connectedCallback) { super.connectedCallback();
super.connectedCallback();
}
this.__registerEventsForLionForm(); this.__registerEventsForLionForm();
// @override LionFieldset: makes sure a11y is handled by ._formNode // @override LionFieldset: makes sure a11y is handled by ._formNode
@ -19,39 +40,32 @@ export class LionForm extends LionFieldset {
} }
disconnectedCallback() { disconnectedCallback() {
if (super.disconnectedCallback) { super.disconnectedCallback();
super.disconnectedCallback();
}
this.__teardownEventsForLionForm(); this.__teardownEventsForLionForm();
} }
get _formNode() { get _formNode() {
return this.querySelector('form'); return /** @type {HTMLFormElement} */ (this.querySelector('form'));
} }
submit() { submit() {
this._formNode.submit(); if (this._formNode) {
this._formNode.submit();
} else {
throwFormNodeError();
}
} }
reset() { reset() {
this._formNode.reset(); if (this._formNode) {
this._formNode.reset();
} else {
throwFormNodeError();
}
} }
__registerEventsForLionForm() { __registerEventsForLionForm() {
this._submit = ev => {
ev.preventDefault();
ev.stopPropagation();
this.submitGroup();
this.dispatchEvent(new Event('submit', { bubbles: true }));
};
this._formNode.addEventListener('submit', this._submit); 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); this._formNode.addEventListener('reset', this._reset);
} }

View file

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

View file

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