@coreui/vue-pro 5.16.0 → 5.17.1

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 (51) hide show
  1. package/README.md +1 -1
  2. package/dist/cjs/components/autocomplete/CAutocomplete.js +17 -16
  3. package/dist/cjs/components/autocomplete/CAutocomplete.js.map +1 -1
  4. package/dist/cjs/components/calendar/utils.js +20 -7
  5. package/dist/cjs/components/calendar/utils.js.map +1 -1
  6. package/dist/cjs/components/index.d.ts +1 -0
  7. package/dist/cjs/components/index.js +4 -0
  8. package/dist/cjs/components/index.js.map +1 -1
  9. package/dist/cjs/components/one-time-password-input/COneTimePassword.d.ts +278 -0
  10. package/dist/cjs/components/one-time-password-input/COneTimePassword.js +393 -0
  11. package/dist/cjs/components/one-time-password-input/COneTimePassword.js.map +1 -0
  12. package/dist/cjs/components/one-time-password-input/COneTimePasswordInput.d.ts +4 -0
  13. package/dist/cjs/components/one-time-password-input/COneTimePasswordInput.js +19 -0
  14. package/dist/cjs/components/one-time-password-input/COneTimePasswordInput.js.map +1 -0
  15. package/dist/cjs/components/one-time-password-input/index.d.ts +3 -0
  16. package/dist/cjs/components/one-time-password-input/utils.d.ts +2 -0
  17. package/dist/cjs/components/one-time-password-input/utils.js +18 -0
  18. package/dist/cjs/components/one-time-password-input/utils.js.map +1 -0
  19. package/dist/cjs/components/stepper/CStepper.d.ts +1 -1
  20. package/dist/cjs/index.js +4 -0
  21. package/dist/cjs/index.js.map +1 -1
  22. package/dist/esm/components/autocomplete/CAutocomplete.js +17 -16
  23. package/dist/esm/components/autocomplete/CAutocomplete.js.map +1 -1
  24. package/dist/esm/components/calendar/utils.js +20 -7
  25. package/dist/esm/components/calendar/utils.js.map +1 -1
  26. package/dist/esm/components/index.d.ts +1 -0
  27. package/dist/esm/components/index.js +2 -0
  28. package/dist/esm/components/index.js.map +1 -1
  29. package/dist/esm/components/one-time-password-input/COneTimePassword.d.ts +278 -0
  30. package/dist/esm/components/one-time-password-input/COneTimePassword.js +391 -0
  31. package/dist/esm/components/one-time-password-input/COneTimePassword.js.map +1 -0
  32. package/dist/esm/components/one-time-password-input/COneTimePasswordInput.d.ts +4 -0
  33. package/dist/esm/components/one-time-password-input/COneTimePasswordInput.js +17 -0
  34. package/dist/esm/components/one-time-password-input/COneTimePasswordInput.js.map +1 -0
  35. package/dist/esm/components/one-time-password-input/index.d.ts +3 -0
  36. package/dist/esm/components/one-time-password-input/utils.d.ts +2 -0
  37. package/dist/esm/components/one-time-password-input/utils.js +15 -0
  38. package/dist/esm/components/one-time-password-input/utils.js.map +1 -0
  39. package/dist/esm/components/stepper/CStepper.d.ts +1 -1
  40. package/dist/esm/index.js +2 -0
  41. package/dist/esm/index.js.map +1 -1
  42. package/package.json +6 -6
  43. package/src/components/autocomplete/CAutocomplete.ts +17 -16
  44. package/src/components/calendar/utils.ts +22 -7
  45. package/src/components/index.ts +1 -0
  46. package/src/components/one-time-password-input/COneTimePassword.ts +459 -0
  47. package/src/components/one-time-password-input/COneTimePasswordInput.ts +21 -0
  48. package/src/components/one-time-password-input/__tests__/COneTimePassword.spec.ts +210 -0
  49. package/src/components/one-time-password-input/__tests__/__snapshots__/COneTimePassword.spec.ts.snap +32 -0
  50. package/src/components/one-time-password-input/index.ts +4 -0
  51. package/src/components/one-time-password-input/utils.ts +13 -0
@@ -0,0 +1,459 @@
1
+ import { defineComponent, h, ref, computed, nextTick, watch } from 'vue'
2
+ import { CFormControlWrapper } from '../form/CFormControlWrapper'
3
+ import { COneTimePasswordInput } from './COneTimePasswordInput'
4
+ import { isValidInput, extractValidChars } from './utils'
5
+ import { getNextActiveElement, isRTL } from '../../utils'
6
+
7
+ const COneTimePassword = defineComponent({
8
+ name: 'COneTimePassword',
9
+ inheritAttrs: false,
10
+ props: {
11
+ /**
12
+ * Function to generate aria-label for each input field. Receives current index (0-based) and total number of inputs.
13
+ */
14
+ ariaLabel: {
15
+ type: Function,
16
+ default: (index: number, total: number) => `Digit ${index + 1} of ${total}`,
17
+ },
18
+ /**
19
+ * Automatically submit the form when all one time password fields are filled.
20
+ */
21
+ autoSubmit: {
22
+ type: Boolean,
23
+ default: false,
24
+ },
25
+ /**
26
+ * Disable all one time password (OTP) input fields.
27
+ */
28
+ disabled: {
29
+ type: Boolean,
30
+ default: false,
31
+ },
32
+ /**
33
+ * Initial value for uncontrolled Vue.js one time password input.
34
+ */
35
+ defaultValue: [String, Number],
36
+ /**
37
+ * Provide valuable, actionable feedback.
38
+ */
39
+ feedback: String,
40
+ /**
41
+ * Provide valuable, actionable feedback.
42
+ */
43
+ feedbackInvalid: String,
44
+ /**
45
+ * Provide valuable, actionable invalid feedback when using standard HTML form validation which applied two CSS pseudo-classes, `:invalid` and `:valid`.
46
+ */
47
+ feedbackValid: String,
48
+ /**
49
+ * A string of all className you want applied to the floating label wrapper.
50
+ */
51
+ floatingClassName: String,
52
+ /**
53
+ * Provide valuable, actionable valid feedback when using standard HTML form validation which applied two CSS pseudo-classes, `:invalid` and `:valid`.
54
+ */
55
+ floatingLabel: String,
56
+ /**
57
+ * ID attribute for the hidden input field.
58
+ */
59
+ id: String,
60
+ /**
61
+ * Set component validation state to invalid.
62
+ */
63
+ invalid: Boolean,
64
+ /**
65
+ * Add a caption for a component.
66
+ */
67
+ label: String,
68
+ /**
69
+ * Enforce sequential input (users must fill fields in order).
70
+ */
71
+ linear: {
72
+ type: Boolean,
73
+ default: true,
74
+ },
75
+ /**
76
+ * Show input as password (masked characters).
77
+ */
78
+ masked: {
79
+ type: Boolean,
80
+ default: false,
81
+ },
82
+ /**
83
+ * The default name for a value passed using v-model.
84
+ */
85
+ modelValue: [String, Number],
86
+ /**
87
+ * Name attribute for the hidden input field.
88
+ */
89
+ name: String,
90
+ /**
91
+ * Placeholder text for input fields. Single character applies to all fields, longer strings apply character-by-character.
92
+ */
93
+ placeholder: String,
94
+ /**
95
+ * Make Vue.js OTP input component read-only.
96
+ */
97
+ readonly: {
98
+ type: Boolean,
99
+ default: false,
100
+ },
101
+ /**
102
+ * Makes the input field required for form validation.
103
+ */
104
+ required: {
105
+ type: Boolean,
106
+ default: false,
107
+ },
108
+ /**
109
+ * Sets the visual size of the Vue.js one time password (OTP) input. Use 'sm' for small or 'lg' for large input fields.
110
+ */
111
+ size: {
112
+ type: String,
113
+ validator: (value: string) => ['sm', 'lg'].includes(value),
114
+ },
115
+ /**
116
+ * Add helper text to the component.
117
+ */
118
+ text: String,
119
+ /**
120
+ * Display validation feedback in a styled tooltip.
121
+ */
122
+ tooltipFeedback: Boolean,
123
+ /**
124
+ * Input validation type: 'number' for digits only, or 'text' for free text.
125
+ */
126
+ type: {
127
+ type: String,
128
+ default: 'number',
129
+ validator: (value: string) => ['number', 'text'].includes(value),
130
+ },
131
+ /**
132
+ * Set component validation state to valid.
133
+ */
134
+ valid: Boolean,
135
+ /**
136
+ * The current value of the one time password input.
137
+ */
138
+ value: [String, Number],
139
+ },
140
+ emits: [
141
+ /**
142
+ * Callback triggered when the Vue.js one time password (OTP) value changes.
143
+ */
144
+ 'update:modelValue',
145
+ /**
146
+ * Callback triggered when the Vue.js one time password (OTP) value changes.
147
+ */
148
+ 'change',
149
+ /**
150
+ * Callback triggered when all Vue.js one time password (OTP) fields are filled.
151
+ */
152
+ 'complete',
153
+ ],
154
+ setup(props, { attrs, slots, emit }) {
155
+ const inputRefs = ref<(HTMLInputElement | null)[]>([])
156
+ const hiddenInputRef = ref<HTMLInputElement | null>(null)
157
+ const inputValues = ref<string[]>([])
158
+
159
+ // Count valid OTP input children
160
+ const inputCount = computed(() => {
161
+ return inputRefs.value.filter((ref) => ref !== null).length
162
+ })
163
+
164
+ // Initialize input values
165
+ const initializeValues = () => {
166
+ const initialValue = String(props.modelValue ?? props.value ?? props.defaultValue ?? '')
167
+ inputValues.value = Array.from({ length: inputCount.value }, (_, i) => initialValue[i] || '')
168
+ }
169
+
170
+ // Watch for changes in modelValue or value (controlled mode)
171
+ watch(
172
+ () => props.modelValue ?? props.value,
173
+ (newValue) => {
174
+ if (newValue !== undefined) {
175
+ const valueString = String(newValue)
176
+ inputValues.value = Array.from(
177
+ { length: inputCount.value },
178
+ (_, i) => valueString[i] || ''
179
+ )
180
+ }
181
+ },
182
+ { immediate: true }
183
+ )
184
+
185
+ // Watch for changes in inputCount
186
+ watch(inputCount, initializeValues, { immediate: true })
187
+
188
+ // Update hidden input and trigger events
189
+ const updateValue = (newValues: string[]) => {
190
+ const newValue = newValues.join('')
191
+
192
+ if (hiddenInputRef.value) {
193
+ hiddenInputRef.value.value = newValue
194
+ }
195
+
196
+ emit('update:modelValue', newValue)
197
+ emit('change', newValue)
198
+
199
+ if (newValue.length === inputCount.value) {
200
+ emit('complete', newValue)
201
+
202
+ if (props.autoSubmit) {
203
+ nextTick(() => {
204
+ const form = hiddenInputRef.value?.closest('form') as HTMLFormElement
205
+ if (form && typeof form.requestSubmit === 'function') {
206
+ form.requestSubmit()
207
+ }
208
+ })
209
+ }
210
+ }
211
+ }
212
+
213
+ const handleInputChange = (index: number, event: Event) => {
214
+ const target = event.target as HTMLInputElement
215
+ const inputValue = target.value
216
+
217
+ if (inputValue.length === 1 && !isValidInput(inputValue, props.type as 'number' | 'text')) {
218
+ return
219
+ }
220
+
221
+ const newValues = [...inputValues.value]
222
+ newValues[index] = inputValue.length === 1 ? inputValue : ''
223
+
224
+ inputValues.value = newValues
225
+ updateValue(newValues)
226
+
227
+ if (inputValue.length === 1) {
228
+ const nextInput = getNextActiveElement(
229
+ inputRefs.value.filter(Boolean) as HTMLInputElement[],
230
+ target,
231
+ true,
232
+ false
233
+ )
234
+ nextInput?.focus()
235
+ }
236
+ }
237
+
238
+ const handleInputFocus = (event: FocusEvent) => {
239
+ const target = event.target as HTMLInputElement
240
+
241
+ if (target.value) {
242
+ setTimeout(() => {
243
+ target.select()
244
+ }, 0)
245
+ return
246
+ }
247
+
248
+ if (props.linear) {
249
+ const firstEmptyInput = inputRefs.value.find((input) => !input?.value)
250
+ if (firstEmptyInput && firstEmptyInput !== target) {
251
+ firstEmptyInput.focus()
252
+ }
253
+ }
254
+ }
255
+
256
+ const handleKeyDown = (event: KeyboardEvent) => {
257
+ const { key, target } = event
258
+
259
+ if (key === 'Backspace' && (target as HTMLInputElement).value === '') {
260
+ const newValues = [...inputValues.value]
261
+ const prevInput = getNextActiveElement(
262
+ inputRefs.value.filter(Boolean) as HTMLInputElement[],
263
+ target as HTMLInputElement,
264
+ false,
265
+ false
266
+ )
267
+
268
+ if (prevInput) {
269
+ const prevIndex = inputRefs.value.indexOf(prevInput as HTMLInputElement)
270
+ if (prevIndex !== -1) {
271
+ newValues[prevIndex] = ''
272
+ inputValues.value = newValues
273
+ updateValue(newValues)
274
+ prevInput.focus()
275
+ }
276
+ }
277
+ return
278
+ }
279
+
280
+ if (key === 'ArrowRight') {
281
+ if (props.linear && (target as HTMLInputElement).value === '') {
282
+ return
283
+ }
284
+
285
+ const shouldMoveNext = !isRTL(target as HTMLInputElement)
286
+ const nextInput = getNextActiveElement(
287
+ inputRefs.value.filter(Boolean) as HTMLInputElement[],
288
+ target as HTMLInputElement,
289
+ shouldMoveNext,
290
+ false
291
+ )
292
+ nextInput?.focus()
293
+ return
294
+ }
295
+
296
+ if (key === 'ArrowLeft') {
297
+ const shouldMoveNext = isRTL(target as HTMLInputElement)
298
+ const prevInput = getNextActiveElement(
299
+ inputRefs.value.filter(Boolean) as HTMLInputElement[],
300
+ target as HTMLInputElement,
301
+ shouldMoveNext,
302
+ false
303
+ )
304
+ prevInput?.focus()
305
+ }
306
+ }
307
+
308
+ const handlePaste = (index: number, event: ClipboardEvent) => {
309
+ event.preventDefault()
310
+ const pastedData = event.clipboardData?.getData('text') || ''
311
+ const validChars = extractValidChars(pastedData, props.type as 'number' | 'text')
312
+
313
+ if (!validChars) {
314
+ return
315
+ }
316
+
317
+ const newValues = [...inputValues.value]
318
+ const startIndex = index
319
+
320
+ for (let i = 0; i < validChars.length && startIndex + i < inputCount.value; i++) {
321
+ newValues[startIndex + i] = validChars[i]
322
+ }
323
+
324
+ inputValues.value = newValues
325
+ updateValue(newValues)
326
+
327
+ // Focus the next empty input or the last filled input
328
+ const nextEmptyIndex = startIndex + validChars.length
329
+ if (nextEmptyIndex < inputCount.value) {
330
+ inputRefs.value[nextEmptyIndex]?.focus()
331
+ } else {
332
+ inputRefs.value[inputRefs.value.length - 1]?.focus()
333
+ }
334
+ }
335
+
336
+ return () => {
337
+ if (!slots.default) {
338
+ return null
339
+ }
340
+
341
+ const children = slots.default()
342
+ let inputIndex = 0
343
+
344
+ const processedChildren = children?.map((child) => {
345
+ if (child.type && (child.type as any).name === 'COneTimePasswordInput') {
346
+ const currentInputIndex = inputIndex++
347
+
348
+ return h(COneTimePasswordInput, {
349
+ ...child.props,
350
+ key: `otp-input-${currentInputIndex}`,
351
+ type: props.masked ? 'password' : 'text',
352
+ class: [
353
+ {
354
+ 'is-invalid': props.invalid,
355
+ 'is-valid': props.valid,
356
+ },
357
+ child.props?.class,
358
+ ],
359
+ id: child.props?.id || (props.id ? `${props.id}-${currentInputIndex}` : undefined),
360
+ name:
361
+ child.props?.name || (props.name ? `${props.name}-${currentInputIndex}` : undefined),
362
+ placeholder:
363
+ child.props?.placeholder ||
364
+ (props.placeholder && props.placeholder.length > 1
365
+ ? props.placeholder[currentInputIndex]
366
+ : props.placeholder),
367
+ value: inputValues.value[currentInputIndex] || '',
368
+ tabindex:
369
+ currentInputIndex === 0 ? 0 : inputValues.value[currentInputIndex - 1] ? 0 : -1,
370
+ disabled: props.disabled || child.props?.disabled,
371
+ readonly: props.readonly || child.props?.readonly,
372
+ required: props.required || child.props?.required,
373
+ 'aria-label':
374
+ child.props?.['aria-label'] || props.ariaLabel!(currentInputIndex, inputCount.value),
375
+ inputmode: props.type === 'number' ? 'numeric' : 'text',
376
+ pattern: props.type === 'number' ? '[0-9]*' : '.*',
377
+ onInput: (event: Event) => {
378
+ handleInputChange(currentInputIndex, event)
379
+ },
380
+ onFocus: handleInputFocus,
381
+ onKeydown: (event: KeyboardEvent) => {
382
+ handleKeyDown(event)
383
+ },
384
+ onPaste: (event: ClipboardEvent) => {
385
+ handlePaste(currentInputIndex, event)
386
+ },
387
+ ref: (el: any) => {
388
+ // Get the actual DOM element - handle both direct elements and component instances
389
+ if (el) {
390
+ // If it's a component instance, get the DOM element
391
+ const domElement = el.$el || el
392
+ // Ensure it's actually an HTMLInputElement
393
+ if (domElement && domElement.tagName === 'INPUT') {
394
+ inputRefs.value[currentInputIndex] = domElement
395
+ } else {
396
+ inputRefs.value[currentInputIndex] = el
397
+ }
398
+ } else {
399
+ inputRefs.value[currentInputIndex] = null
400
+ }
401
+ },
402
+ })
403
+ }
404
+ return child
405
+ })
406
+
407
+ return h(
408
+ CFormControlWrapper,
409
+ {
410
+ ...(typeof attrs['aria-describedby'] === 'string' && {
411
+ describedby: attrs['aria-describedby'],
412
+ }),
413
+ feedback: props.feedback,
414
+ feedbackInvalid: props.feedbackInvalid,
415
+ feedbackValid: props.feedbackValid,
416
+ floatingClassName: props.floatingClassName,
417
+ floatingLabel: props.floatingLabel,
418
+ id: props.id,
419
+ invalid: props.invalid,
420
+ label: props.label,
421
+ text: props.text,
422
+ tooltipFeedback: props.tooltipFeedback,
423
+ valid: props.valid,
424
+ },
425
+ {
426
+ default: () => [
427
+ h(
428
+ 'div',
429
+ {
430
+ class: [
431
+ 'form-otp',
432
+ {
433
+ [`form-otp-${props.size}`]: props.size,
434
+ },
435
+ attrs.class,
436
+ ],
437
+ role: 'group',
438
+ ...attrs,
439
+ },
440
+ [
441
+ ...processedChildren,
442
+ h('input', {
443
+ type: 'hidden',
444
+ id: props.id,
445
+ name: props.name,
446
+ value: inputValues.value.join(''),
447
+ disabled: props.disabled,
448
+ ref: hiddenInputRef,
449
+ }),
450
+ ]
451
+ ),
452
+ ],
453
+ }
454
+ )
455
+ }
456
+ },
457
+ })
458
+
459
+ export { COneTimePassword }
@@ -0,0 +1,21 @@
1
+ import { defineComponent, h } from 'vue'
2
+
3
+ const COneTimePasswordInput = defineComponent({
4
+ name: 'COneTimePasswordInput',
5
+ inheritAttrs: false,
6
+ setup(_, { attrs, slots }) {
7
+ return () =>
8
+ h(
9
+ 'input',
10
+ {
11
+ ...attrs,
12
+ class: ['form-otp-control', attrs.class],
13
+ maxlength: 1,
14
+ autocomplete: 'off',
15
+ },
16
+ slots.default && slots.default(),
17
+ )
18
+ },
19
+ })
20
+
21
+ export { COneTimePasswordInput }
@@ -0,0 +1,210 @@
1
+ import { mount } from '@vue/test-utils'
2
+ import { COneTimePassword as Component, COneTimePasswordInput } from '../../../index'
3
+
4
+ const ComponentName = 'COneTimePassword'
5
+
6
+ const defaultWrapper = mount(Component, {
7
+ propsData: {},
8
+ slots: {
9
+ default: [
10
+ '<COneTimePasswordInput />',
11
+ '<COneTimePasswordInput />',
12
+ '<COneTimePasswordInput />',
13
+ '<COneTimePasswordInput />',
14
+ ],
15
+ },
16
+ })
17
+
18
+ const customWrapper = mount(Component, {
19
+ propsData: {
20
+ disabled: true,
21
+ readonly: true,
22
+ invalid: true,
23
+ size: 'lg',
24
+ type: 'text',
25
+ placeholder: '•',
26
+ masked: true,
27
+ },
28
+ slots: {
29
+ default: [
30
+ '<COneTimePasswordInput />',
31
+ '<COneTimePasswordInput />',
32
+ '<COneTimePasswordInput />',
33
+ '<COneTimePasswordInput />',
34
+ ],
35
+ },
36
+ })
37
+
38
+ const withValueWrapper = mount(Component, {
39
+ propsData: {
40
+ defaultValue: '1234',
41
+ },
42
+ slots: {
43
+ default: [
44
+ '<COneTimePasswordInput />',
45
+ '<COneTimePasswordInput />',
46
+ '<COneTimePasswordInput />',
47
+ '<COneTimePasswordInput />',
48
+ ],
49
+ },
50
+ })
51
+
52
+ describe(`Loads and display ${ComponentName} component`, () => {
53
+ it('has a name', () => {
54
+ expect(Component.name).toMatch(ComponentName)
55
+ })
56
+ it('renders correctly', () => {
57
+ expect(defaultWrapper.html()).toMatchSnapshot()
58
+ })
59
+ it('contain correct classes', () => {
60
+ expect(defaultWrapper.find('.form-otp').exists()).toBe(true)
61
+ expect(defaultWrapper.find('input[type="hidden"]').exists()).toBe(true)
62
+ })
63
+ })
64
+
65
+ describe(`Customize ${ComponentName} component`, () => {
66
+ it('renders correctly', () => {
67
+ expect(customWrapper.html()).toMatchSnapshot()
68
+ })
69
+ it('contain size classes', () => {
70
+ expect(customWrapper.find('.form-otp-lg').exists()).toBe(true)
71
+ })
72
+ it('has hidden input', () => {
73
+ expect(customWrapper.find('input[type="hidden"]').exists()).toBe(true)
74
+ })
75
+ })
76
+
77
+ describe(`${ComponentName} with default value`, () => {
78
+ it('renders correctly', () => {
79
+ expect(withValueWrapper.html()).toMatchSnapshot()
80
+ })
81
+ it('has hidden input with value', () => {
82
+ const hiddenInput = withValueWrapper.find('input[type="hidden"]')
83
+ expect(hiddenInput.exists()).toBe(true)
84
+ })
85
+ })
86
+
87
+ describe(`${ComponentName} form integration`, () => {
88
+ it('works in a form', () => {
89
+ const wrapper = mount({
90
+ components: { COneTimePassword: Component, COneTimePasswordInput },
91
+ template: `
92
+ <form>
93
+ <COneTimePassword name="otp" id="test-otp">
94
+ <COneTimePasswordInput />
95
+ <COneTimePasswordInput />
96
+ <COneTimePasswordInput />
97
+ <COneTimePasswordInput />
98
+ </COneTimePassword>
99
+ </form>
100
+ `,
101
+ })
102
+
103
+ const hiddenInput = wrapper.find('input[type="hidden"]')
104
+ expect(hiddenInput.exists()).toBe(true)
105
+ expect(hiddenInput.attributes('name')).toBe('otp')
106
+ expect(hiddenInput.attributes('id')).toBe('test-otp')
107
+ })
108
+ })
109
+
110
+ describe(`${ComponentName} accessibility`, () => {
111
+ it('has proper role attributes', () => {
112
+ const wrapper = mount({
113
+ components: { COneTimePassword: Component, COneTimePasswordInput },
114
+ template: `
115
+ <COneTimePassword>
116
+ <COneTimePasswordInput />
117
+ <COneTimePasswordInput />
118
+ <COneTimePasswordInput />
119
+ <COneTimePasswordInput />
120
+ </COneTimePassword>
121
+ `,
122
+ })
123
+
124
+ expect(wrapper.find('[role="group"]').exists()).toBe(true)
125
+ })
126
+ })
127
+
128
+ describe(`${ComponentName} validation`, () => {
129
+ it('applies validation classes when invalid', () => {
130
+ const wrapper = mount({
131
+ components: { COneTimePassword: Component, COneTimePasswordInput },
132
+ template: `
133
+ <COneTimePassword invalid>
134
+ <COneTimePasswordInput />
135
+ <COneTimePasswordInput />
136
+ <COneTimePasswordInput />
137
+ <COneTimePasswordInput />
138
+ </COneTimePassword>
139
+ `,
140
+ })
141
+
142
+ expect(wrapper.html()).toContain('is-invalid')
143
+ })
144
+
145
+ it('applies validation classes when valid', () => {
146
+ const wrapper = mount({
147
+ components: { COneTimePassword: Component, COneTimePasswordInput },
148
+ template: `
149
+ <COneTimePassword valid>
150
+ <COneTimePasswordInput />
151
+ <COneTimePasswordInput />
152
+ <COneTimePasswordInput />
153
+ <COneTimePasswordInput />
154
+ </COneTimePassword>
155
+ `,
156
+ })
157
+
158
+ expect(wrapper.html()).toContain('is-valid')
159
+ })
160
+ })
161
+
162
+ describe(`${ComponentName} props`, () => {
163
+ it('applies disabled state', () => {
164
+ const wrapper = mount({
165
+ components: { COneTimePassword: Component, COneTimePasswordInput },
166
+ template: `
167
+ <COneTimePassword disabled>
168
+ <COneTimePasswordInput />
169
+ <COneTimePasswordInput />
170
+ <COneTimePasswordInput />
171
+ <COneTimePasswordInput />
172
+ </COneTimePassword>
173
+ `,
174
+ })
175
+
176
+ expect(wrapper.html()).toContain('disabled')
177
+ })
178
+
179
+ it('applies readonly state', () => {
180
+ const wrapper = mount({
181
+ components: { COneTimePassword: Component, COneTimePasswordInput },
182
+ template: `
183
+ <COneTimePassword readonly>
184
+ <COneTimePasswordInput />
185
+ <COneTimePasswordInput />
186
+ <COneTimePasswordInput />
187
+ <COneTimePasswordInput />
188
+ </COneTimePassword>
189
+ `,
190
+ })
191
+
192
+ expect(wrapper.html()).toContain('readonly')
193
+ })
194
+
195
+ it('applies masked state', () => {
196
+ const wrapper = mount({
197
+ components: { COneTimePassword: Component, COneTimePasswordInput },
198
+ template: `
199
+ <COneTimePassword masked>
200
+ <COneTimePasswordInput />
201
+ <COneTimePasswordInput />
202
+ <COneTimePasswordInput />
203
+ <COneTimePasswordInput />
204
+ </COneTimePassword>
205
+ `,
206
+ })
207
+
208
+ expect(wrapper.html()).toContain('type="password"')
209
+ })
210
+ })