diff --git a/ui/src/javascript/ui-init.ts b/ui/src/javascript/ui-init.ts index e726c30..fc42bca 100644 --- a/ui/src/javascript/ui-init.ts +++ b/ui/src/javascript/ui-init.ts @@ -1,14 +1,8 @@ // Dependencies. -// 1. Modules. -import FormValidate from './modules/form-validate'; -import Search from './modules/search'; -import Slider from './modules/slider'; -// 2. For DEMO purposes only. -import demoAjaxFetchHTML from './modules/demo-ajax-fetch-html'; - -// 3. Web components. +// Web components. import WebUIDisclosure from './web-components/webui-disclosure'; +import WebUIFormValidate from './web-components/webui-form-validate'; import WebUIMakeClickable from './web-components/webui-make-clickable'; import WebUIModal from './web-components/webui-modal'; import WebUINotify from './web-components/webui-notify'; @@ -19,17 +13,19 @@ import WebUIToggle from './web-components/webui-toggle'; import WebUITabs from './web-components/webui-tabs'; import WebUIVideoPlayer from './web-components/webui-video-player'; -export const uiInit = (): void => { - FormValidate.start(); - Search.start(); - Slider.start(); +// Modules. +import Search from './modules/search'; +import Slider from './modules/slider'; - // For DEMO purposes only. - demoAjaxFetchHTML.start(); +// For DEMO purposes only. +import demoAjaxFetchHTML from './modules/demo-ajax-fetch-html'; +export const uiInit = (): void => { // Define Web Components !customElements.get('webui-disclosure') && customElements.define('webui-disclosure', WebUIDisclosure); + !customElements.get('webui-form-validate') && + customElements.define('webui-form-validate', WebUIFormValidate); !customElements.get('webui-make-clickable') && customElements.define('webui-make-clickable', WebUIMakeClickable); !customElements.get('webui-modal') && @@ -48,4 +44,10 @@ export const uiInit = (): void => { customElements.define('webui-toggle', WebUIToggle); !customElements.get('webui-video-player') && customElements.define('webui-video-player', WebUIVideoPlayer); + + // For DEMO purposes only. + demoAjaxFetchHTML.start(); + + Search.start(); + Slider.start(); }; diff --git a/ui/src/javascript/web-components/webui-disclosure.ts b/ui/src/javascript/web-components/webui-disclosure.ts index 3487676..e6198d7 100644 --- a/ui/src/javascript/web-components/webui-disclosure.ts +++ b/ui/src/javascript/web-components/webui-disclosure.ts @@ -14,12 +14,12 @@ export default class WebUIDisclosure extends HTMLElement { if (!this.trigger || !this.content) return; - this.a11ySetup(); + this.setupA11y(); this.trigger?.addEventListener('click', this); } - private a11ySetup(): void { + private setupA11y(): void { this.trigger?.removeAttribute('hidden'); this.trigger?.setAttribute('aria-expanded', 'false'); this.content?.setAttribute('hidden', ''); diff --git a/ui/src/javascript/modules/form-validate.ts b/ui/src/javascript/web-components/webui-form-validate.ts similarity index 62% rename from ui/src/javascript/modules/form-validate.ts rename to ui/src/javascript/web-components/webui-form-validate.ts index 2a49554..89c7a05 100644 --- a/ui/src/javascript/modules/form-validate.ts +++ b/ui/src/javascript/web-components/webui-form-validate.ts @@ -1,60 +1,36 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ - -export default class FormValidate { - private form: HTMLFormElement; +export default class FormValidate extends HTMLElement { + private form: HTMLFormElement | null; private errorFieldClass: string; private errorMsgClass: string; - constructor(form: HTMLFormElement) { - this.form = form; + constructor() { + super(); + + this.form = this.querySelector('form'); this.errorFieldClass = 'form__field--has-error'; this.errorMsgClass = 'form__error'; - this.init(); - } - - public static start(): void { - const forms = document.querySelectorAll( - '[data-module="form-validate"]', - ); - - [...(forms as any)].forEach((form) => { - const instance = new FormValidate(form); - return instance; - }); - } + if (!this.form) return; - private init(): void { - this.initFormValidate(); - this.form.addEventListener('submit', (e: Event) => - this.handleSubmit(e), - ); - this.form.addEventListener( - 'blur', - (e: Event) => { - this.handleBlur(e); - }, - true, - ); - } - - private initFormValidate(): void { // Prevent native HTML5 validation. this.form.noValidate = true; + + this.form.addEventListener('submit', this); + this.form.addEventListener('blur', this, true); } private handleSubmit(e: Event): void { - if (!this.form.checkValidity()) { + if (!this.form?.checkValidity()) { e.preventDefault(); - [...(this.form.elements as any)].forEach((field) => { + [...(this.form?.elements as any)].forEach((field) => { if (!field.checkValidity()) { this.showError(field); } }); // Focus on 1st error. - const firstError = this.form.querySelector( + const firstError = this.form?.querySelector( '[aria-invalid]', ) as HTMLElement; firstError?.focus(); @@ -84,9 +60,25 @@ export default class FormValidate { errorMsg.textContent = field.validationMessage; fieldWrapper?.classList.add(this.errorFieldClass); + // Only add 1 error msg per field (e.g. a group of radio buttons). if (!fieldWrapper?.querySelector(`#${errorMsg.id}`)) { - fieldWrapper?.appendChild(errorMsg); + let target; + + if (field.type === 'checkbox') { + target = fieldWrapper.querySelector('.checkbox'); + fieldWrapper?.insertBefore(errorMsg, target); + return; + } + + if (field.type === 'radio') { + const fieldset = fieldWrapper.querySelector('fieldset'); + target = fieldWrapper.querySelector('.radio'); + fieldset?.insertBefore(errorMsg, target); + return; + } + + fieldWrapper?.insertBefore(errorMsg, field); } field.setAttribute('aria-invalid', 'true'); @@ -102,4 +94,15 @@ export default class FormValidate { fieldWrapper?.classList.remove(this.errorFieldClass); errorMsg?.remove(); } + + // Handle constructor() event listeners. + handleEvent(e: MouseEvent) { + if (e.type === 'submit') { + this.handleSubmit(e); + } + + if (e.type === 'blur') { + this.handleBlur(e); + } + } } diff --git a/ui/stories/4. Forms/Form/Form.js b/ui/stories/4. Forms/Form/Form.js index 9362548..da3044c 100644 --- a/ui/stories/4. Forms/Form/Form.js +++ b/ui/stories/4. Forms/Form/Form.js @@ -151,6 +151,9 @@ export const FormServerValidationHtml = () => ` Telephone input label UK number, between 9 and 13 digits, can include spaces + + Error text... + ` aria-invalid="true" aria-describedby="input-tel-error" /> - - Error text... -