UNPKG

@coreui/vue-pro

Version:

UI Components Library for Vue.js

305 lines (301 loc) 12.3 kB
'use strict'; var vue = require('vue'); var CCollapse = require('../collapse/CCollapse.js'); const CStepper = vue.defineComponent({ name: 'CStepper', inheritAttrs: false, props: { /** * The default active step index when not using `v-model`. */ activeStepNumber: { type: Number, default: 1, }, /** * Optional unique ID used for accessibility attributes like `aria-labelledby`. */ id: String, /** * Sets the layout direction of the Vue Stepper component. * * - `'horizontal'` – Steps are placed side-by-side. * - `'vertical'` – Steps are stacked vertically (ideal for mobile). * * This makes the Vue Form Wizard adaptable to various screen sizes. */ layout: { type: String, default: 'horizontal', }, /** * Enables linear step progression in the Vue Form Wizard. * * - `true`: Steps must be completed in order. * - `false`: Users can navigate freely between steps. * * Useful for enforcing validation and structured data entry in Vue multi-step forms. */ linear: { type: Boolean, default: true, }, /** * The current active step index of the Vue Stepper (used for controlled mode with `v-model`). * * If this prop is not provided, the Vue Form Wizard will use `activeStepNumber` as its initial value. */ modelValue: Number, /** * Defines the list of steps in the Vue Stepper. * * Each step should include: * - `label`: The title displayed in the step. * - `indicator` (optional): Custom icon, number, or marker. * - `formRef` (optional): A reference to the `<form>` used for validation in Vue Form Wizard scenarios. */ steps: { type: Array, required: true, }, /** * Controls the layout of the step indicator and label. * * - `'horizontal'`: Icon and label are side-by-side. * - `'vertical'`: Label is shown below the icon. * * Applies only when `layout` is set to `'horizontal'`. */ stepButtonLayout: { type: String, default: 'horizontal', }, /** * Enables validation of forms within each step of the Vue Form Wizard. * * When set to `true`, the user cannot proceed unless the current step's form passes `checkValidity()`. * Each step must expose a native form element via the `formRef` slot binding. */ validation: { type: Boolean, default: true, }, }, emits: [ /** * Emitted when the user successfully finishes all steps in the Vue Form Wizard. */ 'finish', /** * Emitted when the stepper is reset to its initial state. */ 'reset', /** * Emitted on any manual or programmatic step change. */ 'stepChange', /** * Emitted after each step's form is validated (when `validation: true`). */ 'stepValidationComplete', /** * Emitted when the current active step changes. * * Useful for syncing state with `v-model`. */ 'update:modelValue', ], setup(props, { emit, slots, attrs, expose }) { const activeStepNumber = vue.ref(props.modelValue ?? props.activeStepNumber); const isControlled = vue.computed(() => props.modelValue !== undefined); const isFinished = vue.ref(false); const stepsRef = vue.ref(null); const stepButtonRefs = vue.shallowRef([]); const formRefs = vue.shallowRef([]); const registerFormRef = (stepNumber) => (el) => { formRefs.value[stepNumber - 1] = el; }; vue.watch(() => props.modelValue, (val) => { if (val !== undefined) activeStepNumber.value = val; }); vue.watch(activeStepNumber, (val) => { if (isControlled.value) emit('update:modelValue', val); }); const isStepValid = (stepNumber) => { if (!props.validation) { return true; } const form = formRefs.value[stepNumber - 1]; if (form) { const isValid = form.checkValidity(); emit('stepValidationComplete', { stepNumber: stepNumber, isValid: isValid }); if (form && !isValid) { if (!form.noValidate) { form.reportValidity(); } return false; } } return true; }; const setActiveStep = (stepNumber, bypassValidation = false) => { if (stepNumber < 1 || stepNumber > props.steps.length || stepNumber === activeStepNumber.value) { return; } if (!bypassValidation && stepNumber > activeStepNumber.value && !isStepValid(activeStepNumber.value)) return; activeStepNumber.value = stepNumber; emit('stepChange', stepNumber); }; const next = () => { if (activeStepNumber.value <= props.steps.length) { setActiveStep(activeStepNumber.value + 1); } else { finish(); } }; const prev = () => { if (activeStepNumber.value > 1) { setActiveStep(activeStepNumber.value - 1, true); } }; const finish = () => { if (activeStepNumber.value === props.steps.length && isStepValid(activeStepNumber.value)) { isFinished.value = true; emit('finish'); } }; const reset = () => { formRefs.value.forEach((form) => form?.reset?.()); activeStepNumber.value = props.activeStepNumber; isFinished.value = false; emit('reset'); emit('stepChange', props.activeStepNumber); vue.nextTick(() => { stepButtonRefs.value[props.activeStepNumber - 1]?.focus(); }); }; const handleKeyDown = (event) => { const buttons = stepButtonRefs.value; const current = event.target; const index = buttons.indexOf(current); if (index === -1) return; let nextIndex = index; switch (event.key) { case 'ArrowRight': case 'ArrowDown': { nextIndex = (index + 1) % buttons.length; break; } case 'ArrowLeft': case 'ArrowUp': { nextIndex = (index - 1 + buttons.length) % buttons.length; break; } case 'Home': { nextIndex = 0; break; } case 'End': { nextIndex = buttons.length - 1; break; } default: { return; } } event.preventDefault(); buttons[nextIndex]?.focus(); }; expose({ next, prev, finish, reset }); return () => { const isVertical = props.layout === 'vertical'; stepButtonRefs.value = []; return vue.h('div', { ...attrs, class: ['stepper', { 'stepper-vertical': isVertical }, attrs.class], }, [ vue.h('ol', { class: 'stepper-steps', role: 'tablist', 'aria-orientation': isVertical ? 'vertical' : 'horizontal', onKeydown: handleKeyDown, ref: stepsRef, }, props.steps.map((step, index) => { const stepNumber = index + 1; const isActive = !isFinished.value && stepNumber === activeStepNumber.value; const isComplete = isFinished.value || stepNumber < activeStepNumber.value; const isDisabled = isFinished.value || (props.linear && stepNumber > activeStepNumber.value + 1); const stepId = `step-${props.id || 'stepper'}-${stepNumber}`; const panelId = `panel-${props.id || 'stepper'}-${stepNumber}`; return vue.h('li', { key: stepNumber, class: ['stepper-step', props.stepButtonLayout], role: 'presentation', }, [ vue.h('button', { type: 'button', class: ['stepper-step-button', { active: isActive, complete: isComplete }], disabled: isDisabled, id: stepId, role: 'tab', 'aria-selected': isActive, tabindex: isActive ? 0 : -1, 'aria-controls': panelId, onClick: () => setActiveStep(stepNumber, !props.linear || stepNumber <= activeStepNumber.value), ref: (el) => (stepButtonRefs.value[index] = el), }, [ vue.h('span', { class: 'stepper-step-indicator' }, [ isComplete ? vue.h('span', { class: 'stepper-step-indicator-icon' }) : vue.h('span', { class: 'stepper-step-indicator-text' }, typeof step === 'object' && 'indicator' in step ? step.indicator : stepNumber), ]), vue.h('span', { class: 'stepper-step-label' }, typeof step === 'object' && 'label' in step ? step.label : step), ]), stepNumber < props.steps.length && vue.h('div', { class: 'stepper-step-connector' }), isVertical && vue.h(CCollapse.CCollapse, { class: 'stepper-step-content', id: panelId, role: 'tabpanel', visible: isActive, 'aria-hidden': !isActive, 'aria-labelledby': stepId, 'aria-live': 'polite', }, () => slots[`step-${stepNumber}`]?.({ formRef: registerFormRef(stepNumber) })), ]); })), !isVertical && vue.h('div', { class: 'stepper-content' }, props.steps.map((_, index) => { const stepNumber = index + 1; const isActive = !isFinished.value && stepNumber === activeStepNumber.value; const stepId = `step-${props.id || 'stepper'}-${stepNumber}`; const panelId = `panel-${props.id || 'stepper'}-${stepNumber}`; return vue.h('div', { key: stepNumber, id: panelId, role: 'tabpanel', 'aria-hidden': !isActive, 'aria-labelledby': stepId, 'aria-live': 'polite', class: ['stepper-pane', { active: isActive, show: isActive }], }, { default: () => slots[`step-${stepNumber}`]?.({ formRef: registerFormRef(stepNumber) }), }); })), ]); }; }, }); exports.CStepper = CStepper; //# sourceMappingURL=CStepper.js.map