@djangocfg/ui-nextjs 2.1.29 → 2.1.30
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.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/ui-nextjs",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.30",
|
|
4
4
|
"description": "Next.js UI component library with Radix UI primitives, Tailwind CSS styling, charts, and form components",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ui-components",
|
|
@@ -58,8 +58,8 @@
|
|
|
58
58
|
"check": "tsc --noEmit"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
|
-
"@djangocfg/api": "^2.1.
|
|
62
|
-
"@djangocfg/ui-core": "^2.1.
|
|
61
|
+
"@djangocfg/api": "^2.1.30",
|
|
62
|
+
"@djangocfg/ui-core": "^2.1.30",
|
|
63
63
|
"@types/react": "^19.1.0",
|
|
64
64
|
"@types/react-dom": "^19.1.0",
|
|
65
65
|
"consola": "^3.4.2",
|
|
@@ -74,9 +74,11 @@
|
|
|
74
74
|
},
|
|
75
75
|
"dependencies": {
|
|
76
76
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
77
|
+
"@radix-ui/react-icons": "^1.3.2",
|
|
77
78
|
"@radix-ui/react-menubar": "^1.1.16",
|
|
78
79
|
"@radix-ui/react-navigation-menu": "^1.2.14",
|
|
79
80
|
"@radix-ui/react-slot": "^1.2.4",
|
|
81
|
+
"input-otp": "1.4.2",
|
|
80
82
|
"@rjsf/core": "^6.1.2",
|
|
81
83
|
"@rjsf/utils": "^6.1.2",
|
|
82
84
|
"@rjsf/validator-ajv8": "^6.1.2",
|
|
@@ -104,7 +106,7 @@
|
|
|
104
106
|
"vidstack": "next"
|
|
105
107
|
},
|
|
106
108
|
"devDependencies": {
|
|
107
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
109
|
+
"@djangocfg/typescript-config": "^2.1.30",
|
|
108
110
|
"@types/node": "^24.7.2",
|
|
109
111
|
"eslint": "^9.37.0",
|
|
110
112
|
"tailwindcss-animate": "1.0.7",
|
package/src/components/index.ts
CHANGED
|
@@ -84,3 +84,7 @@ export type { OptionBuilderConfig } from './multi-select-pro/helpers';
|
|
|
84
84
|
// Markdown Components
|
|
85
85
|
export { MarkdownMessage } from './markdown';
|
|
86
86
|
export type { MarkdownMessageProps } from './markdown';
|
|
87
|
+
|
|
88
|
+
// OTP Components (Smart OTP input with validation)
|
|
89
|
+
export { OTPInput, InputOTPGroup, InputOTPSlot, InputOTPSeparator, useSmartOTP } from './otp';
|
|
90
|
+
export type { OTPInputProps, OTPValidationMode, OTPPasteBehavior, OTPValidator } from './otp';
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { cn } from '@djangocfg/ui-core'
|
|
5
|
+
import {
|
|
6
|
+
InputOTP,
|
|
7
|
+
InputOTPGroup,
|
|
8
|
+
InputOTPSlot,
|
|
9
|
+
} from '@djangocfg/ui-core'
|
|
10
|
+
import { MinusIcon } from 'lucide-react'
|
|
11
|
+
import { useSmartOTP, createPasteHandler } from './use-otp-input'
|
|
12
|
+
import type { SmartOTPProps } from './types'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Size variants for OTP slots
|
|
16
|
+
*/
|
|
17
|
+
const sizeVariants = {
|
|
18
|
+
sm: 'h-8 w-8 text-sm',
|
|
19
|
+
default: 'h-10 w-10 text-base',
|
|
20
|
+
lg: 'h-14 w-14 text-2xl',
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* OTP Separator Component
|
|
25
|
+
*/
|
|
26
|
+
const InputOTPSeparator = React.forwardRef<
|
|
27
|
+
HTMLDivElement,
|
|
28
|
+
React.ComponentPropsWithoutRef<'div'>
|
|
29
|
+
>(({ className, ...props }, ref) => (
|
|
30
|
+
<div ref={ref} role="separator" className={cn('flex items-center', className)} {...props}>
|
|
31
|
+
<MinusIcon />
|
|
32
|
+
</div>
|
|
33
|
+
))
|
|
34
|
+
InputOTPSeparator.displayName = 'InputOTPSeparator'
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Smart OTP Input Component
|
|
38
|
+
*
|
|
39
|
+
* Features:
|
|
40
|
+
* - Automatic paste handling with cleaning
|
|
41
|
+
* - Validation (numeric, alphanumeric, alpha, custom)
|
|
42
|
+
* - Auto-submit on completion
|
|
43
|
+
* - Customizable appearance
|
|
44
|
+
* - Error/success states
|
|
45
|
+
* - Optional separator
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```tsx
|
|
49
|
+
* <OTPInput
|
|
50
|
+
* length={6}
|
|
51
|
+
* value={code}
|
|
52
|
+
* onChange={setCode}
|
|
53
|
+
* onComplete={(value) => console.log('Complete:', value)}
|
|
54
|
+
* />
|
|
55
|
+
* ```
|
|
56
|
+
*
|
|
57
|
+
* @example With custom validation
|
|
58
|
+
* ```tsx
|
|
59
|
+
* <OTPInput
|
|
60
|
+
* length={4}
|
|
61
|
+
* validationMode="custom"
|
|
62
|
+
* customValidator={(char) => /[A-F0-9]/i.test(char)}
|
|
63
|
+
* value={hexCode}
|
|
64
|
+
* onChange={setHexCode}
|
|
65
|
+
* />
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export const OTPInput = React.forwardRef<
|
|
69
|
+
React.ComponentRef<typeof InputOTP>,
|
|
70
|
+
SmartOTPProps
|
|
71
|
+
>(
|
|
72
|
+
(
|
|
73
|
+
{
|
|
74
|
+
length = 6,
|
|
75
|
+
value,
|
|
76
|
+
onChange,
|
|
77
|
+
onComplete,
|
|
78
|
+
validationMode = 'numeric',
|
|
79
|
+
customValidator,
|
|
80
|
+
pasteBehavior = 'clean',
|
|
81
|
+
autoSubmit = false,
|
|
82
|
+
disabled = false,
|
|
83
|
+
showSeparator = false,
|
|
84
|
+
separatorIndex,
|
|
85
|
+
containerClassName,
|
|
86
|
+
slotClassName,
|
|
87
|
+
separatorClassName,
|
|
88
|
+
autoFocus = true,
|
|
89
|
+
size = 'default',
|
|
90
|
+
error = false,
|
|
91
|
+
success = false,
|
|
92
|
+
...props
|
|
93
|
+
},
|
|
94
|
+
ref
|
|
95
|
+
) => {
|
|
96
|
+
const {
|
|
97
|
+
value: otpValue,
|
|
98
|
+
handleChange,
|
|
99
|
+
handleComplete,
|
|
100
|
+
} = useSmartOTP({
|
|
101
|
+
length,
|
|
102
|
+
value,
|
|
103
|
+
onChange,
|
|
104
|
+
onComplete,
|
|
105
|
+
validationMode,
|
|
106
|
+
customValidator,
|
|
107
|
+
autoSubmit,
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
// Create paste handler
|
|
111
|
+
const pasteHandler = React.useMemo(
|
|
112
|
+
() =>
|
|
113
|
+
createPasteHandler(
|
|
114
|
+
length,
|
|
115
|
+
validationMode,
|
|
116
|
+
pasteBehavior,
|
|
117
|
+
handleChange,
|
|
118
|
+
customValidator
|
|
119
|
+
),
|
|
120
|
+
[length, validationMode, pasteBehavior, handleChange, customValidator]
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
// Calculate separator position (default to middle)
|
|
124
|
+
const separatorPosition = separatorIndex ?? Math.floor(length / 2)
|
|
125
|
+
|
|
126
|
+
// Render slots
|
|
127
|
+
const slots = React.useMemo(() => {
|
|
128
|
+
const slotElements: React.ReactNode[] = []
|
|
129
|
+
|
|
130
|
+
for (let i = 0; i < length; i++) {
|
|
131
|
+
// Add separator if needed
|
|
132
|
+
if (showSeparator && i === separatorPosition && i !== 0) {
|
|
133
|
+
slotElements.push(
|
|
134
|
+
<InputOTPSeparator
|
|
135
|
+
key={`separator-${i}`}
|
|
136
|
+
className={cn('mx-1', separatorClassName)}
|
|
137
|
+
/>
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Add slot
|
|
142
|
+
slotElements.push(
|
|
143
|
+
<InputOTPSlot
|
|
144
|
+
key={i}
|
|
145
|
+
index={i}
|
|
146
|
+
className={cn(
|
|
147
|
+
sizeVariants[size],
|
|
148
|
+
error && 'border-destructive ring-destructive/20',
|
|
149
|
+
success && 'border-green-500 ring-green-500/20',
|
|
150
|
+
slotClassName
|
|
151
|
+
)}
|
|
152
|
+
/>
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return slotElements
|
|
157
|
+
}, [length, showSeparator, separatorPosition, separatorClassName, size, error, success, slotClassName])
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<InputOTP
|
|
161
|
+
ref={ref}
|
|
162
|
+
maxLength={length}
|
|
163
|
+
value={otpValue}
|
|
164
|
+
onChange={handleChange}
|
|
165
|
+
onComplete={handleComplete}
|
|
166
|
+
disabled={disabled}
|
|
167
|
+
containerClassName={cn('gap-2', containerClassName)}
|
|
168
|
+
autoFocus={autoFocus}
|
|
169
|
+
onPaste={pasteHandler}
|
|
170
|
+
{...props}
|
|
171
|
+
>
|
|
172
|
+
<InputOTPGroup>{slots}</InputOTPGroup>
|
|
173
|
+
</InputOTP>
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
OTPInput.displayName = 'OTPInput'
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Re-export base components for advanced usage
|
|
182
|
+
*/
|
|
183
|
+
export { InputOTPGroup, InputOTPSlot } from '@djangocfg/ui-core'
|
|
184
|
+
export { InputOTPSeparator }
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Re-export types
|
|
188
|
+
*/
|
|
189
|
+
export type {
|
|
190
|
+
SmartOTPProps as OTPInputProps,
|
|
191
|
+
OTPValidationMode,
|
|
192
|
+
OTPPasteBehavior,
|
|
193
|
+
OTPValidator,
|
|
194
|
+
} from './types'
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Re-export hook for advanced usage
|
|
198
|
+
*/
|
|
199
|
+
export { useSmartOTP } from './use-otp-input'
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { OTPInputProps as BaseOTPInputProps } from 'input-otp'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* OTP Input validation modes
|
|
5
|
+
*/
|
|
6
|
+
export type OTPValidationMode = 'numeric' | 'alphanumeric' | 'alpha' | 'custom'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* OTP Input paste behavior
|
|
10
|
+
*/
|
|
11
|
+
export type OTPPasteBehavior = 'clean' | 'strict' | 'lenient'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Custom validator function
|
|
15
|
+
*/
|
|
16
|
+
export type OTPValidator = (value: string) => boolean
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Props for the smart OTP component
|
|
20
|
+
*/
|
|
21
|
+
export interface SmartOTPProps {
|
|
22
|
+
/**
|
|
23
|
+
* Number of OTP slots
|
|
24
|
+
* @default 6
|
|
25
|
+
*/
|
|
26
|
+
length?: number
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Current OTP value
|
|
30
|
+
*/
|
|
31
|
+
value?: string
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Callback when value changes
|
|
35
|
+
*/
|
|
36
|
+
onChange?: (value: string) => void
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Callback when OTP is complete
|
|
40
|
+
*/
|
|
41
|
+
onComplete?: (value: string) => void
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Validation mode
|
|
45
|
+
* @default 'numeric'
|
|
46
|
+
*/
|
|
47
|
+
validationMode?: OTPValidationMode
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Custom validator function (used when validationMode is 'custom')
|
|
51
|
+
*/
|
|
52
|
+
customValidator?: OTPValidator
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Paste behavior
|
|
56
|
+
* - clean: Remove all non-valid characters
|
|
57
|
+
* - strict: Only accept paste if all characters are valid
|
|
58
|
+
* - lenient: Accept paste and keep valid characters only
|
|
59
|
+
* @default 'clean'
|
|
60
|
+
*/
|
|
61
|
+
pasteBehavior?: OTPPasteBehavior
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Auto-submit when complete
|
|
65
|
+
* @default false
|
|
66
|
+
*/
|
|
67
|
+
autoSubmit?: boolean
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Disabled state
|
|
71
|
+
*/
|
|
72
|
+
disabled?: boolean
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Show separator between slots
|
|
76
|
+
* @default false
|
|
77
|
+
*/
|
|
78
|
+
showSeparator?: boolean
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Separator position (0-based index, appears after this index)
|
|
82
|
+
*/
|
|
83
|
+
separatorIndex?: number
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Custom class name for container
|
|
87
|
+
*/
|
|
88
|
+
containerClassName?: string
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Custom class name for slots
|
|
92
|
+
*/
|
|
93
|
+
slotClassName?: string
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Custom class name for separator
|
|
97
|
+
*/
|
|
98
|
+
separatorClassName?: string
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Auto-focus first slot on mount
|
|
102
|
+
* @default true
|
|
103
|
+
*/
|
|
104
|
+
autoFocus?: boolean
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Slot size variant
|
|
108
|
+
* @default 'default'
|
|
109
|
+
*/
|
|
110
|
+
size?: 'sm' | 'default' | 'lg'
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Error state
|
|
114
|
+
*/
|
|
115
|
+
error?: boolean
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Success state
|
|
119
|
+
*/
|
|
120
|
+
success?: boolean
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Return type for useSmartOTP hook
|
|
125
|
+
*/
|
|
126
|
+
export interface UseSmartOTPReturn {
|
|
127
|
+
value: string
|
|
128
|
+
handleChange: (newValue: string) => void
|
|
129
|
+
handleComplete: (completedValue: string) => void
|
|
130
|
+
isComplete: boolean
|
|
131
|
+
isValid: boolean
|
|
132
|
+
clear: () => void
|
|
133
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback, useMemo, useEffect } from 'react'
|
|
4
|
+
import type {
|
|
5
|
+
SmartOTPProps,
|
|
6
|
+
UseSmartOTPReturn,
|
|
7
|
+
OTPValidationMode,
|
|
8
|
+
OTPPasteBehavior,
|
|
9
|
+
OTPValidator,
|
|
10
|
+
} from './types'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Validation patterns for different modes
|
|
14
|
+
*/
|
|
15
|
+
const VALIDATION_PATTERNS: Record<Exclude<OTPValidationMode, 'custom'>, RegExp> = {
|
|
16
|
+
numeric: /^\d+$/,
|
|
17
|
+
alphanumeric: /^[a-zA-Z0-9]+$/,
|
|
18
|
+
alpha: /^[a-zA-Z]+$/,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Clean input based on validation mode
|
|
23
|
+
*/
|
|
24
|
+
function cleanInput(
|
|
25
|
+
input: string,
|
|
26
|
+
validationMode: OTPValidationMode,
|
|
27
|
+
customValidator?: OTPValidator
|
|
28
|
+
): string {
|
|
29
|
+
if (!input) return ''
|
|
30
|
+
|
|
31
|
+
// Remove all whitespace and convert to uppercase for consistency
|
|
32
|
+
let cleaned = input.replace(/\s+/g, '').trim()
|
|
33
|
+
|
|
34
|
+
if (validationMode === 'custom' && customValidator) {
|
|
35
|
+
// For custom validation, filter character by character
|
|
36
|
+
return cleaned
|
|
37
|
+
.split('')
|
|
38
|
+
.filter((char) => customValidator(char))
|
|
39
|
+
.join('')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// For built-in modes, use regex patterns
|
|
43
|
+
const pattern = VALIDATION_PATTERNS[validationMode as keyof typeof VALIDATION_PATTERNS]
|
|
44
|
+
if (!pattern) return cleaned
|
|
45
|
+
|
|
46
|
+
return cleaned
|
|
47
|
+
.split('')
|
|
48
|
+
.filter((char) => pattern.test(char))
|
|
49
|
+
.join('')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Validate entire value
|
|
54
|
+
*/
|
|
55
|
+
function validateValue(
|
|
56
|
+
value: string,
|
|
57
|
+
validationMode: OTPValidationMode,
|
|
58
|
+
customValidator?: OTPValidator
|
|
59
|
+
): boolean {
|
|
60
|
+
if (!value) return true // Empty is valid
|
|
61
|
+
|
|
62
|
+
if (validationMode === 'custom' && customValidator) {
|
|
63
|
+
return customValidator(value)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const pattern = VALIDATION_PATTERNS[validationMode as keyof typeof VALIDATION_PATTERNS]
|
|
67
|
+
return pattern ? pattern.test(value) : true
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Process pasted content based on paste behavior
|
|
72
|
+
*/
|
|
73
|
+
function processPaste(
|
|
74
|
+
pastedText: string,
|
|
75
|
+
validationMode: OTPValidationMode,
|
|
76
|
+
pasteBehavior: OTPPasteBehavior,
|
|
77
|
+
length: number,
|
|
78
|
+
customValidator?: OTPValidator
|
|
79
|
+
): string | null {
|
|
80
|
+
const cleaned = cleanInput(pastedText, validationMode, customValidator)
|
|
81
|
+
|
|
82
|
+
switch (pasteBehavior) {
|
|
83
|
+
case 'strict':
|
|
84
|
+
// Only accept if the cleaned version matches the original (no invalid chars)
|
|
85
|
+
if (cleaned.length !== pastedText.replace(/\s+/g, '').length) {
|
|
86
|
+
return null // Reject paste
|
|
87
|
+
}
|
|
88
|
+
return cleaned.slice(0, length)
|
|
89
|
+
|
|
90
|
+
case 'lenient':
|
|
91
|
+
// Accept and use only valid characters
|
|
92
|
+
return cleaned.slice(0, length)
|
|
93
|
+
|
|
94
|
+
case 'clean':
|
|
95
|
+
default:
|
|
96
|
+
// Clean and use valid characters
|
|
97
|
+
return cleaned.slice(0, length)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Smart OTP Input Hook
|
|
103
|
+
* Handles validation, paste behavior, and state management
|
|
104
|
+
*/
|
|
105
|
+
export function useSmartOTP({
|
|
106
|
+
length = 6,
|
|
107
|
+
value: controlledValue,
|
|
108
|
+
onChange,
|
|
109
|
+
onComplete,
|
|
110
|
+
validationMode = 'numeric',
|
|
111
|
+
customValidator,
|
|
112
|
+
autoSubmit = false,
|
|
113
|
+
}: Pick<
|
|
114
|
+
SmartOTPProps,
|
|
115
|
+
| 'length'
|
|
116
|
+
| 'value'
|
|
117
|
+
| 'onChange'
|
|
118
|
+
| 'onComplete'
|
|
119
|
+
| 'validationMode'
|
|
120
|
+
| 'customValidator'
|
|
121
|
+
| 'autoSubmit'
|
|
122
|
+
>): UseSmartOTPReturn {
|
|
123
|
+
const [internalValue, setInternalValue] = useState('')
|
|
124
|
+
|
|
125
|
+
// Use controlled value if provided, otherwise use internal state
|
|
126
|
+
const value = controlledValue !== undefined ? controlledValue : internalValue
|
|
127
|
+
|
|
128
|
+
const isComplete = useMemo(() => value.length === length, [value, length])
|
|
129
|
+
|
|
130
|
+
const isValid = useMemo(
|
|
131
|
+
() => validateValue(value, validationMode, customValidator),
|
|
132
|
+
[value, validationMode, customValidator]
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Handle value change with validation
|
|
137
|
+
*/
|
|
138
|
+
const handleChange = useCallback(
|
|
139
|
+
(newValue: string) => {
|
|
140
|
+
// Clean the input
|
|
141
|
+
const cleaned = cleanInput(newValue, validationMode, customValidator)
|
|
142
|
+
|
|
143
|
+
// Limit to max length
|
|
144
|
+
const limited = cleaned.slice(0, length)
|
|
145
|
+
|
|
146
|
+
// Update state
|
|
147
|
+
if (controlledValue === undefined) {
|
|
148
|
+
setInternalValue(limited)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Call onChange callback
|
|
152
|
+
onChange?.(limited)
|
|
153
|
+
},
|
|
154
|
+
[validationMode, customValidator, length, onChange, controlledValue]
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Handle completion
|
|
159
|
+
*/
|
|
160
|
+
const handleComplete = useCallback(
|
|
161
|
+
(completedValue: string) => {
|
|
162
|
+
if (completedValue.length === length && isValid) {
|
|
163
|
+
onComplete?.(completedValue)
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
[length, isValid, onComplete]
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Clear value
|
|
171
|
+
*/
|
|
172
|
+
const clear = useCallback(() => {
|
|
173
|
+
if (controlledValue === undefined) {
|
|
174
|
+
setInternalValue('')
|
|
175
|
+
}
|
|
176
|
+
onChange?.('')
|
|
177
|
+
}, [onChange, controlledValue])
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Auto-submit when complete
|
|
181
|
+
*/
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
if (autoSubmit && isComplete && isValid) {
|
|
184
|
+
handleComplete(value)
|
|
185
|
+
}
|
|
186
|
+
}, [autoSubmit, isComplete, isValid, value, handleComplete])
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
value,
|
|
190
|
+
handleChange,
|
|
191
|
+
handleComplete,
|
|
192
|
+
isComplete,
|
|
193
|
+
isValid,
|
|
194
|
+
clear,
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Create a paste handler for OTP input
|
|
200
|
+
*/
|
|
201
|
+
export function createPasteHandler(
|
|
202
|
+
length: number,
|
|
203
|
+
validationMode: OTPValidationMode,
|
|
204
|
+
pasteBehavior: OTPPasteBehavior,
|
|
205
|
+
onChange: (value: string) => void,
|
|
206
|
+
customValidator?: OTPValidator
|
|
207
|
+
) {
|
|
208
|
+
return (e: React.ClipboardEvent) => {
|
|
209
|
+
e.preventDefault()
|
|
210
|
+
const pastedText = e.clipboardData.getData('text')
|
|
211
|
+
|
|
212
|
+
const processed = processPaste(
|
|
213
|
+
pastedText,
|
|
214
|
+
validationMode,
|
|
215
|
+
pasteBehavior,
|
|
216
|
+
length,
|
|
217
|
+
customValidator
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
if (processed !== null) {
|
|
221
|
+
onChange(processed)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|