@coreui/react-pro 5.22.0 → 5.23.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 (40) hide show
  1. package/README.md +1 -1
  2. package/dist/cjs/components/autocomplete/CAutocomplete.js +2 -2
  3. package/dist/cjs/components/autocomplete/CAutocomplete.js.map +1 -1
  4. package/dist/cjs/components/index.d.ts +1 -0
  5. package/dist/cjs/components/one-time-password-input/COneTimePassword.d.ts +73 -0
  6. package/dist/cjs/components/one-time-password-input/COneTimePassword.js +178 -0
  7. package/dist/cjs/components/one-time-password-input/COneTimePassword.js.map +1 -0
  8. package/dist/cjs/components/one-time-password-input/COneTimePasswordInput.d.ts +8 -0
  9. package/dist/cjs/components/one-time-password-input/COneTimePasswordInput.js +14 -0
  10. package/dist/cjs/components/one-time-password-input/COneTimePasswordInput.js.map +1 -0
  11. package/dist/cjs/components/one-time-password-input/index.d.ts +5 -0
  12. package/dist/cjs/components/one-time-password-input/utils.d.ts +2 -0
  13. package/dist/cjs/components/one-time-password-input/utils.js +28 -0
  14. package/dist/cjs/components/one-time-password-input/utils.js.map +1 -0
  15. package/dist/cjs/index.js +4 -0
  16. package/dist/cjs/index.js.map +1 -1
  17. package/dist/esm/components/autocomplete/CAutocomplete.js +2 -2
  18. package/dist/esm/components/autocomplete/CAutocomplete.js.map +1 -1
  19. package/dist/esm/components/index.d.ts +1 -0
  20. package/dist/esm/components/one-time-password-input/COneTimePassword.d.ts +73 -0
  21. package/dist/esm/components/one-time-password-input/COneTimePassword.js +176 -0
  22. package/dist/esm/components/one-time-password-input/COneTimePassword.js.map +1 -0
  23. package/dist/esm/components/one-time-password-input/COneTimePasswordInput.d.ts +8 -0
  24. package/dist/esm/components/one-time-password-input/COneTimePasswordInput.js +12 -0
  25. package/dist/esm/components/one-time-password-input/COneTimePasswordInput.js.map +1 -0
  26. package/dist/esm/components/one-time-password-input/index.d.ts +5 -0
  27. package/dist/esm/components/one-time-password-input/utils.d.ts +2 -0
  28. package/dist/esm/components/one-time-password-input/utils.js +25 -0
  29. package/dist/esm/components/one-time-password-input/utils.js.map +1 -0
  30. package/dist/esm/index.js +2 -0
  31. package/dist/esm/index.js.map +1 -1
  32. package/package.json +9 -9
  33. package/src/components/autocomplete/CAutocomplete.tsx +18 -16
  34. package/src/components/index.ts +1 -0
  35. package/src/components/one-time-password-input/COneTimePassword.tsx +462 -0
  36. package/src/components/one-time-password-input/COneTimePasswordInput.tsx +25 -0
  37. package/src/components/one-time-password-input/__tests__/COneTimePassword.spec.tsx +361 -0
  38. package/src/components/one-time-password-input/__tests__/__snapshots__/COneTimePassword.spec.tsx.snap +130 -0
  39. package/src/components/one-time-password-input/index.ts +6 -0
  40. package/src/components/one-time-password-input/utils.ts +23 -0
@@ -0,0 +1,361 @@
1
+ import * as React from 'react'
2
+ import { render, screen, fireEvent, waitFor } from '@testing-library/react'
3
+ import '@testing-library/jest-dom'
4
+ import { COneTimePassword } from '../COneTimePassword'
5
+ import { COneTimePasswordInput } from '../COneTimePasswordInput'
6
+
7
+ // Test helpers
8
+ const customAriaLabel = (index: number, total: number) => `Code digit ${index + 1} of ${total}`
9
+
10
+ // Helper to render OTP with default inputs
11
+ const renderOTP = (props = {}) => {
12
+ return render(
13
+ <COneTimePassword {...props}>
14
+ <COneTimePasswordInput />
15
+ <COneTimePasswordInput />
16
+ <COneTimePasswordInput />
17
+ <COneTimePasswordInput />
18
+ </COneTimePassword>
19
+ )
20
+ }
21
+
22
+ describe('COneTimePassword', () => {
23
+ // Snapshot tests
24
+ test('loads and displays COneTimePassword component', () => {
25
+ const { container } = renderOTP()
26
+ expect(container).toMatchSnapshot()
27
+ })
28
+
29
+ test('COneTimePassword customize props', () => {
30
+ const { container } = renderOTP({
31
+ className: 'custom-otp',
32
+ size: 'lg',
33
+ disabled: true,
34
+ readOnly: true,
35
+ placeholder: '•',
36
+ type: 'text',
37
+ })
38
+ expect(container).toMatchSnapshot()
39
+ expect(container.querySelector('.form-otp')).toHaveClass('custom-otp')
40
+ expect(container.querySelector('.form-otp')).toHaveClass('form-otp-lg')
41
+ })
42
+
43
+ // Controlled pattern tests
44
+ test('works as controlled component with value prop', async () => {
45
+ const onChange = jest.fn()
46
+ const { rerender } = renderOTP({ value: '12', onChange })
47
+
48
+ const inputs = screen.getAllByRole('textbox')
49
+ expect(inputs[0]).toHaveValue('1')
50
+ expect(inputs[1]).toHaveValue('2')
51
+ expect(inputs[2]).toHaveValue('')
52
+ expect(inputs[3]).toHaveValue('')
53
+
54
+ // Update value prop
55
+ rerender(
56
+ <COneTimePassword value="1234" onChange={onChange}>
57
+ <COneTimePasswordInput />
58
+ <COneTimePasswordInput />
59
+ <COneTimePasswordInput />
60
+ <COneTimePasswordInput />
61
+ </COneTimePassword>
62
+ )
63
+
64
+ expect(inputs[0]).toHaveValue('1')
65
+ expect(inputs[1]).toHaveValue('2')
66
+ expect(inputs[2]).toHaveValue('3')
67
+ expect(inputs[3]).toHaveValue('4')
68
+ })
69
+
70
+ test('controlled component calls onChange when value changes', () => {
71
+ const onChange = jest.fn()
72
+ renderOTP({ value: '', onChange })
73
+
74
+ const inputs = screen.getAllByRole('textbox')
75
+ fireEvent.input(inputs[0], { target: { value: '5' } })
76
+
77
+ expect(onChange).toHaveBeenCalledWith('5')
78
+ })
79
+
80
+ // Uncontrolled pattern tests
81
+ test('works as uncontrolled component without value prop', () => {
82
+ const onChange = jest.fn()
83
+ renderOTP({ onChange })
84
+
85
+ const inputs = screen.getAllByRole('textbox')
86
+ expect(inputs[0]).toHaveValue('')
87
+
88
+ fireEvent.input(inputs[0], { target: { value: '7' } })
89
+ expect(inputs[0]).toHaveValue('7')
90
+ expect(onChange).toHaveBeenCalledWith('7')
91
+ })
92
+
93
+ test('works with defaultValue prop for uncontrolled mode', () => {
94
+ const onChange = jest.fn()
95
+ renderOTP({ defaultValue: '9876', onChange })
96
+
97
+ const inputs = screen.getAllByRole('textbox')
98
+ expect(inputs[0]).toHaveValue('9')
99
+ expect(inputs[1]).toHaveValue('8')
100
+ expect(inputs[2]).toHaveValue('7')
101
+ expect(inputs[3]).toHaveValue('6')
102
+ })
103
+
104
+ test('defaultValue works with partial values', () => {
105
+ renderOTP({ defaultValue: '42' })
106
+
107
+ const inputs = screen.getAllByRole('textbox')
108
+ expect(inputs[0]).toHaveValue('4')
109
+ expect(inputs[1]).toHaveValue('2')
110
+ expect(inputs[2]).toHaveValue('')
111
+ expect(inputs[3]).toHaveValue('')
112
+ })
113
+
114
+ test('controlled mode overrides defaultValue', () => {
115
+ renderOTP({ defaultValue: '1111', value: '2222' })
116
+
117
+ const inputs = screen.getAllByRole('textbox')
118
+ expect(inputs[0]).toHaveValue('2')
119
+ expect(inputs[1]).toHaveValue('2')
120
+ expect(inputs[2]).toHaveValue('2')
121
+ expect(inputs[3]).toHaveValue('2')
122
+ })
123
+
124
+ // Focus and navigation tests
125
+ test('auto-focuses next input on valid entry', () => {
126
+ renderOTP()
127
+
128
+ const inputs = screen.getAllByRole('textbox')
129
+ fireEvent.input(inputs[0], { target: { value: '1' } })
130
+
131
+ expect(inputs[1]).toHaveFocus()
132
+ })
133
+
134
+ test('backspace moves to previous input when current is empty', () => {
135
+ renderOTP({ defaultValue: '12' })
136
+
137
+ const inputs = screen.getAllByRole('textbox')
138
+ inputs[2].focus()
139
+
140
+ fireEvent.keyDown(inputs[2], { key: 'Backspace' })
141
+ expect(inputs[1]).toHaveFocus()
142
+ expect(inputs[1]).toHaveValue('')
143
+ })
144
+
145
+ test('arrow key navigation works', () => {
146
+ renderOTP({ defaultValue: '1234' })
147
+
148
+ const inputs = screen.getAllByRole('textbox')
149
+ inputs[1].focus()
150
+
151
+ fireEvent.keyDown(inputs[1], { key: 'ArrowRight' })
152
+ expect(inputs[2]).toHaveFocus()
153
+
154
+ fireEvent.keyDown(inputs[2], { key: 'ArrowLeft' })
155
+ expect(inputs[1]).toHaveFocus()
156
+ })
157
+
158
+ test('linear mode prevents out-of-order focus', () => {
159
+ renderOTP({ linear: true })
160
+
161
+ const inputs = screen.getAllByRole('textbox')
162
+ fireEvent.focus(inputs[2]) // Try to focus third input
163
+
164
+ expect(inputs[0]).toHaveFocus() // Should focus first empty input instead
165
+ })
166
+
167
+ // Paste functionality tests
168
+ test('paste distributes characters across inputs', () => {
169
+ const onChange = jest.fn()
170
+ renderOTP({ onChange })
171
+
172
+ const inputs = screen.getAllByRole('textbox')
173
+ inputs[0].focus()
174
+
175
+ fireEvent.paste(inputs[0], { clipboardData: { getData: () => '5678' } })
176
+
177
+ expect(inputs[0]).toHaveValue('5')
178
+ expect(inputs[1]).toHaveValue('6')
179
+ expect(inputs[2]).toHaveValue('7')
180
+ expect(inputs[3]).toHaveValue('8')
181
+ expect(onChange).toHaveBeenLastCalledWith('5678')
182
+ })
183
+
184
+ test('paste from middle input distributes remaining characters', () => {
185
+ renderOTP({ defaultValue: '12' })
186
+
187
+ const inputs = screen.getAllByRole('textbox')
188
+ inputs[1].focus()
189
+
190
+ fireEvent.paste(inputs[1], { clipboardData: { getData: () => '99' } })
191
+
192
+ expect(inputs[0]).toHaveValue('1')
193
+ expect(inputs[1]).toHaveValue('9')
194
+ expect(inputs[2]).toHaveValue('9')
195
+ expect(inputs[3]).toHaveValue('')
196
+ })
197
+
198
+ test('paste respects input type validation', () => {
199
+ renderOTP({ type: 'number' })
200
+
201
+ const inputs = screen.getAllByRole('textbox')
202
+ inputs[0].focus()
203
+
204
+ fireEvent.paste(inputs[0], { clipboardData: { getData: () => 'a1b2' } })
205
+
206
+ expect(inputs[0]).toHaveValue('1')
207
+ expect(inputs[1]).toHaveValue('2')
208
+ expect(inputs[2]).toHaveValue('')
209
+ expect(inputs[3]).toHaveValue('')
210
+ })
211
+
212
+ // Validation and type tests
213
+ test('number type only accepts digits', () => {
214
+ renderOTP({ type: 'number' })
215
+
216
+ const inputs = screen.getAllByRole('textbox')
217
+ fireEvent.input(inputs[0], { target: { value: 'a' } })
218
+
219
+ expect(inputs[0]).toHaveValue('')
220
+ })
221
+
222
+ test('text type accepts any character', () => {
223
+ renderOTP({ type: 'text' })
224
+
225
+ const inputs = screen.getAllByRole('textbox')
226
+ fireEvent.input(inputs[0], { target: { value: 'a' } })
227
+
228
+ expect(inputs[0]).toHaveValue('a')
229
+ })
230
+
231
+ // Completion and submission tests
232
+ test('calls onComplete when all fields are filled', () => {
233
+ const onComplete = jest.fn()
234
+ renderOTP({ onComplete })
235
+
236
+ const inputs = screen.getAllByRole('textbox')
237
+ fireEvent.input(inputs[0], { target: { value: '1' } })
238
+ fireEvent.input(inputs[1], { target: { value: '2' } })
239
+ fireEvent.input(inputs[2], { target: { value: '3' } })
240
+ fireEvent.input(inputs[3], { target: { value: '4' } })
241
+
242
+ expect(onComplete).toHaveBeenCalledWith('1234')
243
+ })
244
+
245
+ test('autoSubmit submits form when complete', async () => {
246
+ const onSubmit = jest.fn((e) => e.preventDefault())
247
+
248
+ render(
249
+ <form onSubmit={onSubmit}>
250
+ <COneTimePassword autoSubmit>
251
+ <COneTimePasswordInput />
252
+ <COneTimePasswordInput />
253
+ <COneTimePasswordInput />
254
+ <COneTimePasswordInput />
255
+ </COneTimePassword>
256
+ </form>
257
+ )
258
+
259
+ const inputs = screen.getAllByRole('textbox')
260
+ fireEvent.input(inputs[0], { target: { value: '1' } })
261
+ fireEvent.input(inputs[1], { target: { value: '2' } })
262
+ fireEvent.input(inputs[2], { target: { value: '3' } })
263
+ fireEvent.input(inputs[3], { target: { value: '4' } })
264
+
265
+ await waitFor(() => {
266
+ expect(onSubmit).toHaveBeenCalled()
267
+ })
268
+ })
269
+
270
+ // Disabled and readonly tests
271
+ test('disabled prop disables all inputs', () => {
272
+ renderOTP({ disabled: true })
273
+
274
+ const inputs = screen.getAllByRole('textbox')
275
+ inputs.forEach((input) => {
276
+ expect(input).toBeDisabled()
277
+ })
278
+ })
279
+
280
+ test('readOnly prop makes all inputs readonly', () => {
281
+ renderOTP({ readOnly: true })
282
+
283
+ const inputs = screen.getAllByRole('textbox')
284
+ inputs.forEach((input) => {
285
+ expect(input).toHaveAttribute('readonly')
286
+ })
287
+ })
288
+
289
+ // Masked input tests
290
+ test('masked prop sets input type to password', () => {
291
+ renderOTP({ masked: true })
292
+
293
+ const inputs = document.querySelectorAll('input[type="password"]')
294
+ expect(inputs).toHaveLength(4)
295
+ inputs.forEach((input) => {
296
+ expect(input).toHaveAttribute('type', 'password')
297
+ })
298
+ })
299
+
300
+ // Hidden input tests
301
+ test('creates hidden input with combined value', () => {
302
+ renderOTP({ name: 'otp', id: 'test-otp' })
303
+
304
+ const hiddenInput = document.querySelector('input[type="hidden"]')
305
+ expect(hiddenInput).toBeInTheDocument()
306
+ expect(hiddenInput).toHaveAttribute('name', 'otp')
307
+ expect(hiddenInput).toHaveAttribute('id', 'test-otp')
308
+
309
+ const inputs = screen.getAllByRole('textbox')
310
+ fireEvent.input(inputs[0], { target: { value: '9' } })
311
+
312
+ expect(hiddenInput).toHaveValue('9')
313
+ })
314
+
315
+ // Accessibility tests
316
+ test('provides proper ARIA labels', () => {
317
+ renderOTP()
318
+
319
+ const inputs = screen.getAllByRole('textbox')
320
+ expect(inputs[0]).toHaveAttribute('aria-label', 'Digit 1 of 4')
321
+ expect(inputs[1]).toHaveAttribute('aria-label', 'Digit 2 of 4')
322
+ expect(inputs[2]).toHaveAttribute('aria-label', 'Digit 3 of 4')
323
+ expect(inputs[3]).toHaveAttribute('aria-label', 'Digit 4 of 4')
324
+ })
325
+
326
+ test('custom ariaLabel function works', () => {
327
+ renderOTP({ ariaLabel: customAriaLabel })
328
+
329
+ const inputs = screen.getAllByRole('textbox')
330
+ expect(inputs[0]).toHaveAttribute('aria-label', 'Code digit 1 of 4')
331
+ })
332
+
333
+ test('applies form validation classes', () => {
334
+ renderOTP({ invalid: true })
335
+
336
+ const inputs = screen.getAllByRole('textbox')
337
+ inputs.forEach((input) => {
338
+ expect(input).toHaveClass('is-invalid')
339
+ })
340
+ })
341
+
342
+ // Placeholder tests
343
+ test('single character placeholder applies to all inputs', () => {
344
+ renderOTP({ placeholder: '•' })
345
+
346
+ const inputs = screen.getAllByRole('textbox')
347
+ inputs.forEach((input) => {
348
+ expect(input).toHaveAttribute('placeholder', '•')
349
+ })
350
+ })
351
+
352
+ test('multi-character placeholder distributes by index', () => {
353
+ renderOTP({ placeholder: 'ABCD' })
354
+
355
+ const inputs = screen.getAllByRole('textbox')
356
+ expect(inputs[0]).toHaveAttribute('placeholder', 'A')
357
+ expect(inputs[1]).toHaveAttribute('placeholder', 'B')
358
+ expect(inputs[2]).toHaveAttribute('placeholder', 'C')
359
+ expect(inputs[3]).toHaveAttribute('placeholder', 'D')
360
+ })
361
+ })
@@ -0,0 +1,130 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`COneTimePassword COneTimePassword customize props 1`] = `
4
+ <div>
5
+ <div
6
+ class="form-otp form-otp-lg custom-otp"
7
+ role="group"
8
+ >
9
+ <input
10
+ aria-label="Digit 1 of 4"
11
+ autocomplete="off"
12
+ class="form-otp-control"
13
+ disabled=""
14
+ inputmode="text"
15
+ maxlength="1"
16
+ pattern=".*"
17
+ placeholder="•"
18
+ readonly=""
19
+ tabindex="0"
20
+ type="text"
21
+ value=""
22
+ />
23
+ <input
24
+ aria-label="Digit 2 of 4"
25
+ autocomplete="off"
26
+ class="form-otp-control"
27
+ disabled=""
28
+ inputmode="text"
29
+ maxlength="1"
30
+ pattern=".*"
31
+ placeholder="•"
32
+ readonly=""
33
+ tabindex="-1"
34
+ type="text"
35
+ value=""
36
+ />
37
+ <input
38
+ aria-label="Digit 3 of 4"
39
+ autocomplete="off"
40
+ class="form-otp-control"
41
+ disabled=""
42
+ inputmode="text"
43
+ maxlength="1"
44
+ pattern=".*"
45
+ placeholder="•"
46
+ readonly=""
47
+ tabindex="-1"
48
+ type="text"
49
+ value=""
50
+ />
51
+ <input
52
+ aria-label="Digit 4 of 4"
53
+ autocomplete="off"
54
+ class="form-otp-control"
55
+ disabled=""
56
+ inputmode="text"
57
+ maxlength="1"
58
+ pattern=".*"
59
+ placeholder="•"
60
+ readonly=""
61
+ tabindex="-1"
62
+ type="text"
63
+ value=""
64
+ />
65
+ <input
66
+ disabled=""
67
+ type="hidden"
68
+ value=""
69
+ />
70
+ </div>
71
+ </div>
72
+ `;
73
+
74
+ exports[`COneTimePassword loads and displays COneTimePassword component 1`] = `
75
+ <div>
76
+ <div
77
+ class="form-otp"
78
+ role="group"
79
+ >
80
+ <input
81
+ aria-label="Digit 1 of 4"
82
+ autocomplete="off"
83
+ class="form-otp-control"
84
+ inputmode="numeric"
85
+ maxlength="1"
86
+ pattern="[0-9]*"
87
+ tabindex="0"
88
+ type="text"
89
+ value=""
90
+ />
91
+ <input
92
+ aria-label="Digit 2 of 4"
93
+ autocomplete="off"
94
+ class="form-otp-control"
95
+ inputmode="numeric"
96
+ maxlength="1"
97
+ pattern="[0-9]*"
98
+ tabindex="-1"
99
+ type="text"
100
+ value=""
101
+ />
102
+ <input
103
+ aria-label="Digit 3 of 4"
104
+ autocomplete="off"
105
+ class="form-otp-control"
106
+ inputmode="numeric"
107
+ maxlength="1"
108
+ pattern="[0-9]*"
109
+ tabindex="-1"
110
+ type="text"
111
+ value=""
112
+ />
113
+ <input
114
+ aria-label="Digit 4 of 4"
115
+ autocomplete="off"
116
+ class="form-otp-control"
117
+ inputmode="numeric"
118
+ maxlength="1"
119
+ pattern="[0-9]*"
120
+ tabindex="-1"
121
+ type="text"
122
+ value=""
123
+ />
124
+ <input
125
+ type="hidden"
126
+ value=""
127
+ />
128
+ </div>
129
+ </div>
130
+ `;
@@ -0,0 +1,6 @@
1
+ import { COneTimePassword } from './COneTimePassword'
2
+ import { COneTimePasswordInput } from './COneTimePasswordInput'
3
+
4
+ export { COneTimePassword, COneTimePasswordInput }
5
+ export type { COneTimePasswordProps } from './COneTimePassword'
6
+ export type { COneTimePasswordInputProps } from './COneTimePasswordInput'
@@ -0,0 +1,23 @@
1
+ export const isValidInput = (inputValue: string, type: 'number' | 'text'): boolean => {
2
+ if (inputValue.length !== 1) return false
3
+
4
+ switch (type) {
5
+ case 'number': {
6
+ return /^\d$/.test(inputValue)
7
+ }
8
+ default: {
9
+ return /^.$/.test(inputValue)
10
+ }
11
+ }
12
+ }
13
+
14
+ export const extractValidChars = (text: string, type: 'number' | 'text'): string => {
15
+ switch (type) {
16
+ case 'number': {
17
+ return text.replaceAll(/\D/g, '')
18
+ }
19
+ default: {
20
+ return text
21
+ }
22
+ }
23
+ }