@coreui/vue-pro 5.10.0 → 5.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/README.md +1 -1
  2. package/dist/cjs/components/button/CButton.js +1 -1
  3. package/dist/cjs/components/button/CButton.js.map +1 -1
  4. package/dist/cjs/components/calendar/CCalendar.js +4 -3
  5. package/dist/cjs/components/calendar/CCalendar.js.map +1 -1
  6. package/dist/cjs/components/calendar/utils.d.ts +8 -0
  7. package/dist/cjs/components/calendar/utils.js +16 -0
  8. package/dist/cjs/components/calendar/utils.js.map +1 -1
  9. package/dist/cjs/components/form/CFormControlWrapper.d.ts +2 -0
  10. package/dist/cjs/components/form/CFormControlWrapper.js +9 -1
  11. package/dist/cjs/components/form/CFormControlWrapper.js.map +1 -1
  12. package/dist/cjs/components/index.d.ts +2 -0
  13. package/dist/cjs/components/index.js +44 -36
  14. package/dist/cjs/components/index.js.map +1 -1
  15. package/dist/cjs/components/link/CLink.js.map +1 -1
  16. package/dist/cjs/components/loading-button/CLoadingButton.d.ts +2 -2
  17. package/dist/cjs/components/multi-select/CMultiSelect.d.ts +12 -0
  18. package/dist/cjs/components/multi-select/CMultiSelect.js +23 -1
  19. package/dist/cjs/components/multi-select/CMultiSelect.js.map +1 -1
  20. package/dist/cjs/components/nav/CNav.d.ts +2 -2
  21. package/dist/cjs/components/nav/CNav.js +3 -2
  22. package/dist/cjs/components/nav/CNav.js.map +1 -1
  23. package/dist/cjs/components/offcanvas/COffcanvas.js.map +1 -1
  24. package/dist/cjs/components/password-input/CPasswordInput.d.ts +190 -0
  25. package/dist/cjs/components/password-input/CPasswordInput.js +178 -0
  26. package/dist/cjs/components/password-input/CPasswordInput.js.map +1 -0
  27. package/dist/cjs/components/password-input/index.d.ts +6 -0
  28. package/dist/cjs/components/password-input/index.js +13 -0
  29. package/dist/cjs/components/password-input/index.js.map +1 -0
  30. package/dist/cjs/components/popover/CPopover.d.ts +1 -1
  31. package/dist/cjs/components/rating/CRating.d.ts +1 -1
  32. package/dist/cjs/components/smart-table/CSmartTable.js +2 -2
  33. package/dist/cjs/components/smart-table/CSmartTable.js.map +1 -1
  34. package/dist/cjs/components/smart-table/utils.js.map +1 -1
  35. package/dist/cjs/components/stepper/CStepper.d.ts +168 -0
  36. package/dist/cjs/components/stepper/CStepper.js +305 -0
  37. package/dist/cjs/components/stepper/CStepper.js.map +1 -0
  38. package/dist/cjs/components/stepper/index.d.ts +6 -0
  39. package/dist/cjs/components/stepper/index.js +13 -0
  40. package/dist/cjs/components/stepper/index.js.map +1 -0
  41. package/dist/cjs/components/stepper/types.d.ts +15 -0
  42. package/dist/cjs/components/tabs/CTab.js.map +1 -1
  43. package/dist/cjs/components/tabs/CTabList.d.ts +2 -2
  44. package/dist/cjs/components/tabs/CTabList.js +3 -2
  45. package/dist/cjs/components/tabs/CTabList.js.map +1 -1
  46. package/dist/cjs/components/time-picker/utils.d.ts +10 -0
  47. package/dist/cjs/components/time-picker/utils.js +25 -9
  48. package/dist/cjs/components/time-picker/utils.js.map +1 -1
  49. package/dist/cjs/components/tooltip/CTooltip.d.ts +1 -1
  50. package/dist/cjs/components/widgets/CWidgetStatsB.js +2 -2
  51. package/dist/cjs/components/widgets/CWidgetStatsB.js.map +1 -1
  52. package/dist/cjs/components/widgets/CWidgetStatsC.js +2 -2
  53. package/dist/cjs/components/widgets/CWidgetStatsC.js.map +1 -1
  54. package/dist/cjs/index.js +50 -42
  55. package/dist/cjs/index.js.map +1 -1
  56. package/dist/cjs/node_modules/vue-types/dist/index.js +567 -0
  57. package/dist/cjs/node_modules/vue-types/dist/index.js.map +1 -0
  58. package/dist/cjs/node_modules/vue-types/dist/shared/vue-types.8139b772.js +29 -0
  59. package/dist/cjs/node_modules/vue-types/dist/shared/vue-types.8139b772.js.map +1 -0
  60. package/dist/cjs/utils/getNextActiveElement.js.map +1 -1
  61. package/dist/esm/components/avatar/CAvatar.js +1 -1
  62. package/dist/esm/components/badge/CBadge.js +1 -1
  63. package/dist/esm/components/button/CButton.js +2 -2
  64. package/dist/esm/components/button/CButton.js.map +1 -1
  65. package/dist/esm/components/calendar/CCalendar.js +5 -4
  66. package/dist/esm/components/calendar/CCalendar.js.map +1 -1
  67. package/dist/esm/components/calendar/utils.d.ts +8 -0
  68. package/dist/esm/components/calendar/utils.js +16 -1
  69. package/dist/esm/components/calendar/utils.js.map +1 -1
  70. package/dist/esm/components/card/CCard.js +1 -1
  71. package/dist/esm/components/dropdown/CDropdownToggle.js +2 -2
  72. package/dist/esm/components/form/CFormControlWrapper.d.ts +2 -0
  73. package/dist/esm/components/form/CFormControlWrapper.js +9 -1
  74. package/dist/esm/components/form/CFormControlWrapper.js.map +1 -1
  75. package/dist/esm/components/index.d.ts +2 -0
  76. package/dist/esm/components/index.js +4 -0
  77. package/dist/esm/components/index.js.map +1 -1
  78. package/dist/esm/components/link/CLink.js.map +1 -1
  79. package/dist/esm/components/loading-button/CLoadingButton.d.ts +2 -2
  80. package/dist/esm/components/multi-select/CMultiSelect.d.ts +12 -0
  81. package/dist/esm/components/multi-select/CMultiSelect.js +24 -2
  82. package/dist/esm/components/multi-select/CMultiSelect.js.map +1 -1
  83. package/dist/esm/components/nav/CNav.d.ts +2 -2
  84. package/dist/esm/components/nav/CNav.js +3 -2
  85. package/dist/esm/components/nav/CNav.js.map +1 -1
  86. package/dist/esm/components/offcanvas/COffcanvas.js.map +1 -1
  87. package/dist/esm/components/password-input/CPasswordInput.d.ts +190 -0
  88. package/dist/esm/components/password-input/CPasswordInput.js +176 -0
  89. package/dist/esm/components/password-input/CPasswordInput.js.map +1 -0
  90. package/dist/esm/components/password-input/index.d.ts +6 -0
  91. package/dist/esm/components/password-input/index.js +10 -0
  92. package/dist/esm/components/password-input/index.js.map +1 -0
  93. package/dist/esm/components/popover/CPopover.d.ts +1 -1
  94. package/dist/esm/components/rating/CRating.d.ts +1 -1
  95. package/dist/esm/components/smart-table/CSmartTable.js +2 -2
  96. package/dist/esm/components/smart-table/CSmartTable.js.map +1 -1
  97. package/dist/esm/components/smart-table/CSmartTableBody.js +1 -1
  98. package/dist/esm/components/smart-table/utils.js.map +1 -1
  99. package/dist/esm/components/stepper/CStepper.d.ts +168 -0
  100. package/dist/esm/components/stepper/CStepper.js +303 -0
  101. package/dist/esm/components/stepper/CStepper.js.map +1 -0
  102. package/dist/esm/components/stepper/index.d.ts +6 -0
  103. package/dist/esm/components/stepper/index.js +10 -0
  104. package/dist/esm/components/stepper/index.js.map +1 -0
  105. package/dist/esm/components/stepper/types.d.ts +15 -0
  106. package/dist/esm/components/tabs/CTab.js.map +1 -1
  107. package/dist/esm/components/tabs/CTabList.d.ts +2 -2
  108. package/dist/esm/components/tabs/CTabList.js +3 -2
  109. package/dist/esm/components/tabs/CTabList.js.map +1 -1
  110. package/dist/esm/components/time-picker/utils.d.ts +10 -0
  111. package/dist/esm/components/time-picker/utils.js +25 -10
  112. package/dist/esm/components/time-picker/utils.js.map +1 -1
  113. package/dist/esm/components/tooltip/CTooltip.d.ts +1 -1
  114. package/dist/esm/components/widgets/CWidgetStatsB.js +2 -2
  115. package/dist/esm/components/widgets/CWidgetStatsB.js.map +1 -1
  116. package/dist/esm/components/widgets/CWidgetStatsC.js +2 -2
  117. package/dist/esm/components/widgets/CWidgetStatsC.js.map +1 -1
  118. package/dist/esm/index.js +4 -0
  119. package/dist/esm/index.js.map +1 -1
  120. package/dist/esm/node_modules/vue-types/dist/index.js +541 -0
  121. package/dist/esm/node_modules/vue-types/dist/index.js.map +1 -0
  122. package/dist/esm/node_modules/vue-types/dist/shared/vue-types.8139b772.js +25 -0
  123. package/dist/esm/node_modules/vue-types/dist/shared/vue-types.8139b772.js.map +1 -0
  124. package/dist/esm/utils/getNextActiveElement.js.map +1 -1
  125. package/package.json +9 -9
  126. package/src/components/button/CButton.ts +1 -1
  127. package/src/components/calendar/CCalendar.ts +47 -44
  128. package/src/components/calendar/utils.ts +33 -10
  129. package/src/components/form/CFormControlWrapper.ts +35 -21
  130. package/src/components/index.ts +2 -0
  131. package/src/components/multi-select/CMultiSelect.ts +48 -25
  132. package/src/components/nav/CNav.ts +3 -2
  133. package/src/components/password-input/CPasswordInput.ts +214 -0
  134. package/src/components/password-input/index.ts +10 -0
  135. package/src/components/stepper/CStepper.ts +384 -0
  136. package/src/components/stepper/__tests__/CStepper.spec.ts +175 -0
  137. package/src/components/stepper/index.ts +10 -0
  138. package/src/components/stepper/types.ts +18 -0
  139. package/src/components/tabs/CTabList.ts +3 -2
  140. package/src/components/time-picker/CTimePicker.ts +22 -22
  141. package/src/components/time-picker/CTimePickerRollCol.ts +3 -3
  142. package/src/components/time-picker/utils.ts +30 -13
  143. package/dist/cjs/node_modules/is-plain-object/dist/is-plain-object.js +0 -37
  144. package/dist/cjs/node_modules/is-plain-object/dist/is-plain-object.js.map +0 -1
  145. package/dist/cjs/node_modules/vue-types/dist/vue-types.modern.js +0 -33
  146. package/dist/cjs/node_modules/vue-types/dist/vue-types.modern.js.map +0 -1
  147. package/dist/esm/node_modules/is-plain-object/dist/is-plain-object.js +0 -35
  148. package/dist/esm/node_modules/is-plain-object/dist/is-plain-object.js.map +0 -1
  149. package/dist/esm/node_modules/vue-types/dist/vue-types.modern.js +0 -6
  150. package/dist/esm/node_modules/vue-types/dist/vue-types.modern.js.map +0 -1
@@ -0,0 +1,384 @@
1
+ import { defineComponent, h, ref, watch, computed, nextTick, shallowRef } from 'vue'
2
+ import { CCollapse } from '../collapse'
3
+ import type { StepperStepData } from './types'
4
+
5
+ export const CStepper = defineComponent({
6
+ name: 'CStepper',
7
+ inheritAttrs: false,
8
+ props: {
9
+ /**
10
+ * The default active step index when not using `v-model`.
11
+ */
12
+ activeStepNumber: {
13
+ type: Number,
14
+ default: 1,
15
+ },
16
+
17
+ /**
18
+ * Optional unique ID used for accessibility attributes like `aria-labelledby`.
19
+ */
20
+ id: String,
21
+
22
+ /**
23
+ * Sets the layout direction of the Vue Stepper component.
24
+ *
25
+ * - `'horizontal'` – Steps are placed side-by-side.
26
+ * - `'vertical'` – Steps are stacked vertically (ideal for mobile).
27
+ *
28
+ * This makes the Vue Form Wizard adaptable to various screen sizes.
29
+ */
30
+ layout: {
31
+ type: String as () => 'horizontal' | 'vertical',
32
+ default: 'horizontal',
33
+ },
34
+
35
+ /**
36
+ * Enables linear step progression in the Vue Form Wizard.
37
+ *
38
+ * - `true`: Steps must be completed in order.
39
+ * - `false`: Users can navigate freely between steps.
40
+ *
41
+ * Useful for enforcing validation and structured data entry in Vue multi-step forms.
42
+ */
43
+ linear: {
44
+ type: Boolean,
45
+ default: true,
46
+ },
47
+
48
+ /**
49
+ * The current active step index of the Vue Stepper (used for controlled mode with `v-model`).
50
+ *
51
+ * If this prop is not provided, the Vue Form Wizard will use `activeStepNumber` as its initial value.
52
+ */
53
+ modelValue: Number,
54
+
55
+ /**
56
+ * Defines the list of steps in the Vue Stepper.
57
+ *
58
+ * Each step should include:
59
+ * - `label`: The title displayed in the step.
60
+ * - `indicator` (optional): Custom icon, number, or marker.
61
+ * - `formRef` (optional): A reference to the `<form>` used for validation in Vue Form Wizard scenarios.
62
+ */
63
+ steps: {
64
+ type: Array as () => (StepperStepData | string)[],
65
+ required: true,
66
+ },
67
+
68
+ /**
69
+ * Controls the layout of the step indicator and label.
70
+ *
71
+ * - `'horizontal'`: Icon and label are side-by-side.
72
+ * - `'vertical'`: Label is shown below the icon.
73
+ *
74
+ * Applies only when `layout` is set to `'horizontal'`.
75
+ */
76
+ stepButtonLayout: {
77
+ type: String as () => 'horizontal' | 'vertical',
78
+ default: 'horizontal',
79
+ },
80
+
81
+ /**
82
+ * Enables validation of forms within each step of the Vue Form Wizard.
83
+ *
84
+ * When set to `true`, the user cannot proceed unless the current step's form passes `checkValidity()`.
85
+ * Each step must expose a native form element via the `formRef` slot binding.
86
+ */
87
+ validation: {
88
+ type: Boolean,
89
+ default: true,
90
+ },
91
+ },
92
+ emits: [
93
+ /**
94
+ * Emitted when the user successfully finishes all steps in the Vue Form Wizard.
95
+ */
96
+ 'finish',
97
+
98
+ /**
99
+ * Emitted when the stepper is reset to its initial state.
100
+ */
101
+ 'reset',
102
+
103
+ /**
104
+ * Emitted on any manual or programmatic step change.
105
+ */
106
+ 'stepChange',
107
+
108
+ /**
109
+ * Emitted after each step's form is validated (when `validation: true`).
110
+ */
111
+ 'stepValidationComplete',
112
+
113
+ /**
114
+ * Emitted when the current active step changes.
115
+ *
116
+ * Useful for syncing state with `v-model`.
117
+ */
118
+ 'update:modelValue',
119
+ ],
120
+ setup(props, { emit, slots, attrs, expose }) {
121
+ const activeStepNumber = ref<number>(props.modelValue ?? props.activeStepNumber)
122
+ const isControlled = computed(() => props.modelValue !== undefined)
123
+ const isFinished = ref(false)
124
+ const stepsRef = ref<HTMLOListElement | null>(null)
125
+ const stepButtonRefs = shallowRef<(HTMLButtonElement | null)[]>([])
126
+ const formRefs = shallowRef<(HTMLFormElement | null)[]>([])
127
+
128
+ const registerFormRef = (stepNumber: number) => (el: HTMLFormElement | null) => {
129
+ formRefs.value[stepNumber - 1] = el
130
+ }
131
+
132
+ watch(
133
+ () => props.modelValue,
134
+ (val) => {
135
+ if (val !== undefined) activeStepNumber.value = val
136
+ }
137
+ )
138
+
139
+ watch(activeStepNumber, (val) => {
140
+ if (isControlled.value) emit('update:modelValue', val)
141
+ })
142
+
143
+ const isStepValid = (stepNumber: number): boolean => {
144
+ if (!props.validation) {
145
+ return true
146
+ }
147
+
148
+ const form = formRefs.value[stepNumber - 1]
149
+
150
+ console.log(`Validating step ${stepNumber}:`, form)
151
+
152
+ if (form) {
153
+ const isValid = form.checkValidity()
154
+ emit('stepValidationComplete', { stepNumber: stepNumber, isValid: isValid })
155
+
156
+ if (form && !isValid) {
157
+ if (!form.noValidate) {
158
+ form.reportValidity()
159
+ }
160
+
161
+ return false
162
+ }
163
+ }
164
+
165
+ return true
166
+ }
167
+
168
+ const setActiveStep = (stepNumber: number, bypassValidation = false) => {
169
+ if (
170
+ stepNumber < 1 ||
171
+ stepNumber > props.steps.length ||
172
+ stepNumber === activeStepNumber.value
173
+ ) {
174
+ return
175
+ }
176
+
177
+ if (
178
+ !bypassValidation &&
179
+ stepNumber > activeStepNumber.value &&
180
+ !isStepValid(activeStepNumber.value)
181
+ )
182
+ return
183
+
184
+ activeStepNumber.value = stepNumber
185
+ emit('stepChange', stepNumber)
186
+ }
187
+
188
+ const next = () => {
189
+ if (activeStepNumber.value <= props.steps.length) {
190
+ setActiveStep(activeStepNumber.value + 1)
191
+ } else {
192
+ finish()
193
+ }
194
+ }
195
+
196
+ const prev = () => {
197
+ if (activeStepNumber.value > 1) {
198
+ setActiveStep(activeStepNumber.value - 1, true)
199
+ }
200
+ }
201
+
202
+ const finish = () => {
203
+ if (activeStepNumber.value === props.steps.length && isStepValid(activeStepNumber.value)) {
204
+ isFinished.value = true
205
+ emit('finish')
206
+ }
207
+ }
208
+
209
+ const reset = () => {
210
+ formRefs.value.forEach((form) => form?.reset?.())
211
+ activeStepNumber.value = props.activeStepNumber
212
+ isFinished.value = false
213
+ emit('reset')
214
+ emit('stepChange', props.activeStepNumber)
215
+ nextTick(() => {
216
+ stepButtonRefs.value[props.activeStepNumber - 1]?.focus()
217
+ })
218
+ }
219
+
220
+ const handleKeyDown = (event: KeyboardEvent) => {
221
+ const buttons = stepButtonRefs.value
222
+ const current = event.target as HTMLButtonElement
223
+ const index = buttons.indexOf(current)
224
+ if (index === -1) return
225
+
226
+ let nextIndex = index
227
+ switch (event.key) {
228
+ case 'ArrowRight':
229
+ case 'ArrowDown': {
230
+ nextIndex = (index + 1) % buttons.length
231
+ break
232
+ }
233
+ case 'ArrowLeft':
234
+ case 'ArrowUp': {
235
+ nextIndex = (index - 1 + buttons.length) % buttons.length
236
+ break
237
+ }
238
+ case 'Home': {
239
+ nextIndex = 0
240
+ break
241
+ }
242
+ case 'End': {
243
+ nextIndex = buttons.length - 1
244
+ break
245
+ }
246
+ default: {
247
+ return
248
+ }
249
+ }
250
+
251
+ event.preventDefault()
252
+ buttons[nextIndex]?.focus()
253
+ }
254
+
255
+ expose({ next, prev, finish, reset })
256
+
257
+ return () => {
258
+ const isVertical = props.layout === 'vertical'
259
+ stepButtonRefs.value = []
260
+
261
+ return h(
262
+ 'div',
263
+ {
264
+ ...attrs,
265
+ class: ['stepper', { 'stepper-vertical': isVertical }, attrs.class],
266
+ },
267
+ [
268
+ h(
269
+ 'ol',
270
+ {
271
+ class: 'stepper-steps',
272
+ role: 'tablist',
273
+ 'aria-orientation': isVertical ? 'vertical' : 'horizontal',
274
+ onKeydown: handleKeyDown,
275
+ ref: stepsRef,
276
+ },
277
+ props.steps.map((step, index) => {
278
+ const stepNumber = index + 1
279
+
280
+ const isActive = !isFinished.value && stepNumber === activeStepNumber.value
281
+ const isComplete = isFinished.value || stepNumber < activeStepNumber.value
282
+ const isDisabled =
283
+ isFinished.value || (props.linear && stepNumber > activeStepNumber.value + 1)
284
+ const stepId = `step-${props.id || 'stepper'}-${stepNumber}`
285
+ const panelId = `panel-${props.id || 'stepper'}-${stepNumber}`
286
+
287
+ return h(
288
+ 'li',
289
+ {
290
+ key: stepNumber,
291
+ class: ['stepper-step', props.stepButtonLayout],
292
+ role: 'presentation',
293
+ },
294
+ [
295
+ h(
296
+ 'button',
297
+ {
298
+ type: 'button',
299
+ class: ['stepper-step-button', { active: isActive, complete: isComplete }],
300
+ disabled: isDisabled,
301
+ id: stepId,
302
+ role: 'tab',
303
+ 'aria-selected': isActive,
304
+ tabindex: isActive ? 0 : -1,
305
+ 'aria-controls': panelId,
306
+ onClick: () =>
307
+ setActiveStep(
308
+ stepNumber,
309
+ !props.linear || stepNumber <= activeStepNumber.value
310
+ ),
311
+ ref: (el) => (stepButtonRefs.value[index] = el as HTMLButtonElement),
312
+ },
313
+ [
314
+ h('span', { class: 'stepper-step-indicator' }, [
315
+ isComplete
316
+ ? h('span', { class: 'stepper-step-indicator-icon' })
317
+ : h(
318
+ 'span',
319
+ { class: 'stepper-step-indicator-text' },
320
+ typeof step === 'object' && 'indicator' in step
321
+ ? step.indicator
322
+ : stepNumber
323
+ ),
324
+ ]),
325
+ h(
326
+ 'span',
327
+ { class: 'stepper-step-label' },
328
+ typeof step === 'object' && 'label' in step ? step.label : step
329
+ ),
330
+ ]
331
+ ),
332
+ stepNumber < props.steps.length && h('div', { class: 'stepper-step-connector' }),
333
+ isVertical &&
334
+ h(
335
+ CCollapse,
336
+ {
337
+ class: 'stepper-step-content',
338
+ id: panelId,
339
+ role: 'tabpanel',
340
+ visible: isActive,
341
+ 'aria-hidden': !isActive,
342
+ 'aria-labelledby': stepId,
343
+ 'aria-live': 'polite',
344
+ },
345
+ () => slots[`step-${stepNumber}`]?.({ formRef: registerFormRef(stepNumber) })
346
+ ),
347
+ ]
348
+ )
349
+ })
350
+ ),
351
+ !isVertical &&
352
+ h(
353
+ 'div',
354
+ { class: 'stepper-content' },
355
+ props.steps.map((_, index) => {
356
+ const stepNumber = index + 1
357
+
358
+ const isActive = !isFinished.value && stepNumber === activeStepNumber.value
359
+ const stepId = `step-${props.id || 'stepper'}-${stepNumber}`
360
+ const panelId = `panel-${props.id || 'stepper'}-${stepNumber}`
361
+
362
+ return h(
363
+ 'div',
364
+ {
365
+ key: stepNumber,
366
+ id: panelId,
367
+ role: 'tabpanel',
368
+ 'aria-hidden': !isActive,
369
+ 'aria-labelledby': stepId,
370
+ 'aria-live': 'polite',
371
+ class: ['stepper-pane', { active: isActive, show: isActive }],
372
+ },
373
+ {
374
+ default: () =>
375
+ slots[`step-${stepNumber}`]?.({ formRef: registerFormRef(stepNumber) }),
376
+ }
377
+ )
378
+ })
379
+ ),
380
+ ]
381
+ )
382
+ }
383
+ },
384
+ })
@@ -0,0 +1,175 @@
1
+ import { mount } from '@vue/test-utils'
2
+ import { CStepper as Component } from '../../../index'
3
+
4
+ const ComponentName = 'CStepper'
5
+
6
+ const stepsMock = [
7
+ { label: 'Step 1', content: 'Content 1' },
8
+ { label: 'Step 2', content: 'Content 2' },
9
+ { label: 'Step 3', content: 'Content 3' },
10
+ ]
11
+
12
+ const defaultWrapper = mount(Component, {
13
+ propsData: {
14
+ steps: stepsMock,
15
+ },
16
+ })
17
+
18
+ const verticalWrapper = mount(Component, {
19
+ propsData: {
20
+ steps: stepsMock,
21
+ layout: 'vertical',
22
+ },
23
+ })
24
+
25
+ const linearWrapper = mount(Component, {
26
+ propsData: {
27
+ steps: stepsMock,
28
+ linear: true,
29
+ },
30
+ })
31
+
32
+ describe(`Loads and display ${ComponentName} component`, () => {
33
+ it('has a name', () => {
34
+ expect(Component.name).toMatch(ComponentName)
35
+ })
36
+
37
+ it('renders correctly', () => {
38
+ expect(defaultWrapper.html()).toMatchSnapshot()
39
+ })
40
+
41
+ it('contains step labels and content', () => {
42
+ expect(defaultWrapper.text()).toContain('Step 1')
43
+ expect(defaultWrapper.text()).toContain('Content 1')
44
+ })
45
+ })
46
+
47
+ describe(`Vertical layout ${ComponentName} component`, () => {
48
+ it('renders correctly', () => {
49
+ expect(verticalWrapper.html()).toMatchSnapshot()
50
+ })
51
+
52
+ it('uses vertical layout class (if applicable)', () => {
53
+ expect(verticalWrapper.classes()).toContain('stepper-vertical') // optional
54
+ })
55
+ })
56
+
57
+ describe(`Linear mode ${ComponentName} component`, () => {
58
+ it('disables future steps', () => {
59
+ const tabs = linearWrapper.findAll('[role="tab"]')
60
+ expect(tabs[0].attributes('disabled')).toBeUndefined()
61
+ expect(tabs[1].attributes('disabled')).toBeDefined()
62
+ expect(tabs[2].attributes('disabled')).toBeDefined()
63
+ })
64
+ })
65
+
66
+ describe(`Non-linear mode ${ComponentName} component`, () => {
67
+ const wrapper = mount(Component, {
68
+ propsData: {
69
+ steps: stepsMock,
70
+ linear: false,
71
+ },
72
+ })
73
+
74
+ it('allows clicking all steps', () => {
75
+ const tabs = wrapper.findAll('[role="tab"]')
76
+ expect(tabs[0].attributes('disabled')).toBeUndefined()
77
+ expect(tabs[1].attributes('disabled')).toBeUndefined()
78
+ expect(tabs[2].attributes('disabled')).toBeUndefined()
79
+ })
80
+
81
+ it('calls onStepChange when step is clicked', async () => {
82
+ const onStepChange = jest.fn()
83
+ const customWrapper = mount(Component, {
84
+ propsData: {
85
+ steps: stepsMock,
86
+ linear: false,
87
+ onStepChange,
88
+ },
89
+ })
90
+ await customWrapper.findAll('[role="tab"]')[2].trigger('click')
91
+ expect(onStepChange).toHaveBeenCalledWith(3)
92
+ })
93
+ })
94
+
95
+ describe(`Active step content ${ComponentName}`, () => {
96
+ const wrapper = mount(Component, {
97
+ propsData: {
98
+ steps: stepsMock,
99
+ layout: 'horizontal',
100
+ modelValue: 1,
101
+ },
102
+ })
103
+
104
+ it('renders only first step content', () => {
105
+ expect(wrapper.text()).toContain('Content 1')
106
+ expect(wrapper.text()).not.toContain('Content 2')
107
+ expect(wrapper.text()).not.toContain('Content 3')
108
+ })
109
+ })
110
+
111
+ describe(`Finish and reset events ${ComponentName}`, () => {
112
+ it('calls onFinish when last step is clicked', async () => {
113
+ const onFinish = jest.fn()
114
+ const wrapper = mount(Component, {
115
+ propsData: {
116
+ steps: stepsMock,
117
+ onFinish,
118
+ linear: false,
119
+ modelValue: 3,
120
+ },
121
+ })
122
+
123
+ await wrapper.findAll('[role="tab"]')[2].trigger('click')
124
+ expect(onFinish).toHaveBeenCalledTimes(1)
125
+ })
126
+
127
+ it('resets and focuses first step', async () => {
128
+ const onReset = jest.fn()
129
+ const wrapper = mount(Component, {
130
+ propsData: {
131
+ steps: stepsMock,
132
+ onReset,
133
+ },
134
+ })
135
+
136
+ const instance = wrapper.vm as any
137
+ instance.next()
138
+ await wrapper.vm.$nextTick()
139
+ instance.reset()
140
+ await wrapper.vm.$nextTick()
141
+
142
+ expect(onReset).toHaveBeenCalled()
143
+ expect(wrapper.findAll('[role="tab"]')[0].element).toBe(document.activeElement)
144
+ })
145
+ })
146
+
147
+ describe(`Uncontrolled vs Controlled ${ComponentName}`, () => {
148
+ it('starts at defaultActiveStepNumber', () => {
149
+ const wrapper = mount(Component, {
150
+ propsData: {
151
+ steps: stepsMock,
152
+ defaultActiveStepNumber: 2,
153
+ },
154
+ })
155
+
156
+ expect(wrapper.text()).toContain('Content 2')
157
+ expect(wrapper.text()).not.toContain('Content 1')
158
+ })
159
+
160
+ it('reacts to modelValue change', async () => {
161
+ const wrapper = mount(Component, {
162
+ propsData: {
163
+ steps: stepsMock,
164
+ modelValue: 1,
165
+ },
166
+ })
167
+
168
+ expect(wrapper.text()).toContain('Content 1')
169
+
170
+ await wrapper.setProps({ modelValue: 3 })
171
+
172
+ expect(wrapper.text()).toContain('Content 3')
173
+ expect(wrapper.text()).not.toContain('Content 1')
174
+ })
175
+ })
@@ -0,0 +1,10 @@
1
+ import { App } from 'vue'
2
+ import { CStepper } from './CStepper'
3
+
4
+ const CStepperPlugin = {
5
+ install: (app: App): void => {
6
+ app.component(CStepper.name as string, CStepper)
7
+ },
8
+ }
9
+
10
+ export { CStepper, CStepperPlugin }
@@ -0,0 +1,18 @@
1
+ import type { VNode } from 'vue'
2
+
3
+ export interface StepperRef {
4
+ next: () => void
5
+ prev: () => void
6
+ finish: () => void
7
+ reset: () => void
8
+ }
9
+
10
+ export interface StepperStepData {
11
+ indicator?: VNode
12
+ label: VNode
13
+ }
14
+
15
+ export type StepperStepValidationResult = {
16
+ stepNumber: number
17
+ isValid: boolean
18
+ }
@@ -18,12 +18,12 @@ const CTabList = defineComponent({
18
18
  /**
19
19
  * Set the nav variant to tabs or pills.
20
20
  *
21
- * @values 'pills', 'tabs', 'underline', 'underline-border'
21
+ * @values 'enclosed', 'enclosed-pills', 'pills', 'tabs', 'underline', 'underline-border'
22
22
  */
23
23
  variant: {
24
24
  type: String,
25
25
  validator: (value: string) => {
26
- return ['pills', 'tabs', 'underline', 'underline-border'].includes(value)
26
+ return ['enclosed', 'enclosed-pills', 'pills', 'tabs', 'underline', 'underline-border'].includes(value)
27
27
  },
28
28
  },
29
29
  },
@@ -72,6 +72,7 @@ const CTabList = defineComponent({
72
72
  {
73
73
  class: [
74
74
  'nav',
75
+ props.variant === 'enclosed-pills' && 'nav-enclosed',
75
76
  {
76
77
  [`nav-${props.layout}`]: props.layout,
77
78
  [`nav-${props.variant}`]: props.variant,