UNPKG

material-inspired-component-library

Version:

The Material-Inspired Component Library (MICL) offers a collection of beautifully crafted components leveraging native HTML markup, designed to align with the Material Design 3 guidelines.

245 lines (214 loc) 9.47 kB
// // Copyright © 2025 Hermana AS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. import form from '../../foundations/form'; export const stepperSelector = '.micl-stepper'; const ACTIONS_SELECTOR = '.micl-stepper__action-back,.micl-stepper__action-next'; const BUTTON_SELECTOR = '.micl-stepper__header button[role=tab][aria-controls]'; const STEP_CLASS = 'micl-stepper__step'; const STEP_SELECTOR = '.' + STEP_CLASS; export default (() => { const getSelectedStep = (stepper: HTMLElement): HTMLElement | null => { let step = stepper.querySelector(`${STEP_SELECTOR}[aria-current=step]`); if (step) { return step as HTMLElement; } return setSelectedStep(stepper, stepper.querySelector(STEP_SELECTOR)); }; const getStepNumber = (stepper: HTMLElement, step: HTMLElement): number => { const allSteps = Array.from(stepper.querySelectorAll(STEP_SELECTOR)); const index = allSteps.indexOf(step); return index + 1; }; const setSelectedStep = (stepper: HTMLElement, step: HTMLElement | null): HTMLElement | null => { if (!step) { return null; } let index = 0; stepper.querySelectorAll(STEP_SELECTOR).forEach((e, i) => { e.setAttribute('aria-current', e === step ? 'step' : 'false'); if (e === step) { index = i; } }); const button = stepper.querySelectorAll(BUTTON_SELECTOR).item(index); stepper.querySelectorAll(BUTTON_SELECTOR).forEach((e, i) => { e.setAttribute('aria-selected', e === button ? 'true' : 'false'); }); refresh(stepper, step); return step; }; const endTransitionSelected = (event: Event): void => { const target = event.currentTarget as Element; if ((event as TransitionEvent).propertyName !== 'transform' || !target) { return; } target.classList.remove( 'micl-stepper__step--fromselected', 'micl-stepper__step--toselected' ); target.removeEventListener('transitionend', endTransitionSelected); }; const goToSibling = (stepper: HTMLElement, selectedStep: HTMLElement, sibling: HTMLElement): void => { selectedStep.addEventListener('transitionend', endTransitionSelected); selectedStep.classList.add('micl-stepper__step--fromselected'); selectedStep.offsetHeight; sibling.addEventListener('transitionend', endTransitionSelected); sibling.classList.add('micl-stepper__step--toselected'); sibling.offsetHeight; setSelectedStep(stepper, sibling); }; const isBackAction = (action: HTMLElement): boolean => action.classList.contains('micl-stepper__action-back'); const showHideActions = (stepper: HTMLElement, step: HTMLElement): void => { Array.from(stepper.querySelectorAll<HTMLElement>(ACTIONS_SELECTOR)).forEach(action => { const siblingKey = isBackAction(action) ? 'previousElementSibling' : 'nextElementSibling'; const hasSibling = (step[siblingKey] as Element)?.classList.contains(STEP_CLASS); action.classList.toggle('micl-hidden', !hasSibling); }); }; const showHideElements = (stepper: HTMLElement, step: HTMLElement): void => { const selectedStep = getStepNumber(stepper, step); stepper.querySelectorAll<HTMLElement>('[data-step]').forEach(element => { const shouldHide = element.dataset.step != `${selectedStep}`; element.classList.toggle('micl-hidden', shouldHide); }); }; const updateProgress = (stepper: HTMLElement, step: HTMLElement): void => { const index = getStepNumber(stepper, step); const totalSteps = stepper.querySelectorAll(STEP_SELECTOR).length; const linear = !stepper.classList.contains('micl-stepper--nonlinear'); const setText = (selector: string, content: string): void => { stepper.querySelectorAll(selector).forEach(e => { e.textContent = content; }); }; setText('.micl-stepper__progress-current', `${index}`); setText('.micl-stepper__progress-total', `${totalSteps}`); stepper.querySelectorAll('.micl-stepper__progress-dots').forEach(dots => { const fragment = document.createDocumentFragment(); dots.innerHTML = ''; for (let i = 1; i <= totalSteps; i++) { let dot = document.createElement('span'); dot.classList.add('micl-stepper__progress-dot'); if ((linear && (i <= index)) || (!linear && (i === index))) { dot.classList.add('micl-stepper__progress--done'); } fragment.appendChild(dot); } dots.appendChild(fragment); }); stepper.querySelectorAll(BUTTON_SELECTOR).forEach((button, i) => { button.classList.toggle( 'micl-stepper__progress--done', linear ? i + 1 <= index : i + 1 === index ); }); }; const refresh = (stepper: HTMLElement, step: HTMLElement): void => { showHideActions(stepper, step); showHideElements(stepper, step); updateProgress(stepper, step); }; return { initialize: (stepper: HTMLElement): void => { if (!stepper.matches(stepperSelector) || stepper.dataset.miclinitialized) { return; } stepper.dataset.miclinitialized = '1'; const step = getSelectedStep(stepper); const header = stepper.querySelector('.micl-stepper__header'); if (step) { refresh(stepper, step); header?.querySelectorAll<HTMLButtonElement>( 'button[role=tab][aria-controls]' ).forEach(button => { button.addEventListener('click', () => { if ( ('ariaControlsElements' in Element.prototype) && button.ariaControlsElements ) { setSelectedStep(stepper, button.ariaControlsElements[0] as HTMLElement); } else { const id = button.getAttribute('aria-controls'); if (id) { setSelectedStep(stepper, document.getElementById(id)); } } }); }); } Array.from(stepper.querySelectorAll<HTMLElement>(ACTIONS_SELECTOR)).forEach(action => { action.addEventListener('click', function(event: Event) { const back = isBackAction(this); const selectedStep = getSelectedStep(stepper); if ( !selectedStep || (!back && selectedStep instanceof HTMLFieldSetElement && !form.validateFieldSet(selectedStep, true)) ) { if (!back) { event.stopImmediatePropagation(); } return; } const sibling = selectedStep[ back ? 'previousElementSibling' : 'nextElementSibling' ] as HTMLElement; if (sibling?.classList.contains(STEP_CLASS)) { goToSibling(stepper, selectedStep, sibling); } }, true); }); if (stepper instanceof HTMLFormElement) { stepper.addEventListener('submit', (event: SubmitEvent) => { if (!event.submitter?.classList.contains('micl-form--dosubmit')) { event.preventDefault(); } if (!form.validateForm(stepper, true)) { event.stopImmediatePropagation(); } }, true); } } }; })();