lion/packages/ui/components/form/src/LionForm.js
2024-07-23 17:14:49 +02:00

125 lines
3.2 KiB
JavaScript

import { LionFieldset } from '@lion/ui/fieldset.js';
/**
* @typedef {import('../../form-core/types/registration/FormRegistrarMixinTypes.js').FormRegistrarHost} FormRegistrarHost
*/
const throwFormNodeError = () => {
throw new Error(
'No form node found. Did you put a <form> element inside your custom-form element?',
);
};
/**
* @param {FormRegistrarHost} formEl
* @returns {boolean}
*/
function hasFocusableChildren(formEl) {
// this implies all children have the same type (either all of them are focusable or none of them are)
return formEl.formElements?.some(child => child._focusableNode);
}
/**
* LionForm: form wrapper providing extra features and integration with lion-field elements.
*
* @customElement lion-form
*/
// eslint-disable-next-line no-unused-vars
export class LionForm extends LionFieldset {
constructor() {
super();
/** @protected */
this._submit = this._submit.bind(this);
/** @protected */
this._reset = this._reset.bind(this);
}
connectedCallback() {
super.connectedCallback();
this.__registerEventsForLionForm();
// @override LionFieldset: makes sure a11y is handled by ._formNode
this.removeAttribute('role');
}
disconnectedCallback() {
super.disconnectedCallback();
this.__teardownEventsForLionForm();
}
get _formNode() {
return /** @type {HTMLFormElement} */ (this.querySelector('form'));
}
submit() {
if (this._formNode) {
// Firefox requires cancelable flag, otherwise we cannot preventDefault
// Firefox still runs default handlers for untrusted events :\
this._formNode.dispatchEvent(new Event('submit', { cancelable: true }));
} else {
throwFormNodeError();
}
}
/**
* @param {Event} ev
* @protected
*/
_submit(ev) {
ev.preventDefault();
ev.stopPropagation();
this.submitGroup();
this.dispatchEvent(new Event('submit', { bubbles: true }));
if (this.hasFeedbackFor?.includes('error')) {
this._setFocusOnFirstErroneousFormElement(/** @type { * & FormRegistrarHost } */ (this));
}
}
reset() {
if (this._formNode) {
this._formNode.reset();
} else {
throwFormNodeError();
}
}
/**
* @param {Event} ev
* @protected
*/
_reset(ev) {
ev.preventDefault();
ev.stopPropagation();
this.resetGroup();
this.dispatchEvent(new Event('reset', { bubbles: true }));
}
/**
* @param {FormRegistrarHost} element
* @protected
*/
_setFocusOnFirstErroneousFormElement(element) {
const firstFormElWithError =
element.formElements.find(child => child.hasFeedbackFor.includes('error')) ||
element.formElements[0];
if (hasFocusableChildren(firstFormElWithError)) {
this._setFocusOnFirstErroneousFormElement(firstFormElWithError);
} else {
firstFormElWithError._focusableNode.focus();
}
}
/** @private */
__registerEventsForLionForm() {
this._formNode.addEventListener('submit', this._submit);
this._formNode.addEventListener('reset', this._reset);
}
/** @private */
__teardownEventsForLionForm() {
this._formNode.removeEventListener('submit', this._submit);
this._formNode.removeEventListener('reset', this._reset);
}
}