@coreui/vue-pro
Version:
UI Components Library for Vue.js
305 lines (301 loc) • 12.3 kB
JavaScript
'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