@lynx-js/lynx-ui-input-otp 0.0.0-oidc-bootstrap.0 → 3.136.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.
- package/CHANGELOG.md +16 -0
- package/README.md +35 -4
- package/dist/InputOTP.d.ts +3 -0
- package/dist/InputOTP.d.ts.map +1 -0
- package/dist/InputOTP.jsx +150 -0
- package/dist/InputOTP.jsx.map +1 -0
- package/dist/InputOTPContext.d.ts +4 -0
- package/dist/InputOTPContext.d.ts.map +1 -0
- package/dist/InputOTPContext.jsx +10 -0
- package/dist/InputOTPContext.jsx.map +1 -0
- package/dist/InputOTPSlot.d.ts +3 -0
- package/dist/InputOTPSlot.d.ts.map +1 -0
- package/dist/InputOTPSlot.jsx +37 -0
- package/dist/InputOTPSlot.jsx.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.jsx +2 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.docs.d.ts +340 -0
- package/dist/types/index.docs.d.ts.map +1 -0
- package/dist/types/index.docs.jsx +1 -0
- package/dist/types/index.jsx +1 -0
- package/dist/utils.d.ts +5 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.jsx +19 -0
- package/dist/utils.jsx.map +1 -0
- package/package.json +44 -5
- package/src/InputOTP.tsx +311 -0
- package/src/InputOTPContext.ts +19 -0
- package/src/InputOTPSlot.tsx +79 -0
- package/src/index.tsx +18 -0
- package/src/types/index.docs.ts +397 -0
- package/src/types/index.ts +5 -0
- package/src/utils.ts +29 -0
- package/tsconfig.docs.json +7 -0
- package/tsconfig.json +9 -0
package/src/InputOTP.tsx
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
// Copyright 2026 The Lynx Authors. All rights reserved.
|
|
2
|
+
// Licensed under the Apache License Version 2.0 that can be found in the
|
|
3
|
+
// LICENSE file in the root directory of this source tree.
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
forwardRef,
|
|
7
|
+
memo,
|
|
8
|
+
useContext,
|
|
9
|
+
useEffect,
|
|
10
|
+
useImperativeHandle,
|
|
11
|
+
useMemo,
|
|
12
|
+
useRef,
|
|
13
|
+
useState,
|
|
14
|
+
} from '@lynx-js/react'
|
|
15
|
+
import type { ForwardedRef } from '@lynx-js/react'
|
|
16
|
+
|
|
17
|
+
import { InvokeRejectError, useMemoizedFn } from '@lynx-js/lynx-ui-common'
|
|
18
|
+
import { KeyboardAwareTriggerContext } from '@lynx-js/lynx-ui-input'
|
|
19
|
+
import type {
|
|
20
|
+
BaseEvent,
|
|
21
|
+
CSSProperties,
|
|
22
|
+
InputBlurEvent,
|
|
23
|
+
InputFocusEvent,
|
|
24
|
+
InputInputEvent,
|
|
25
|
+
NodesRef,
|
|
26
|
+
} from '@lynx-js/types'
|
|
27
|
+
import { clsx } from 'clsx'
|
|
28
|
+
|
|
29
|
+
import { InputOTPContext } from './InputOTPContext'
|
|
30
|
+
import type {
|
|
31
|
+
InputOTPProps,
|
|
32
|
+
InputOTPRef,
|
|
33
|
+
InputOTPRenderProps,
|
|
34
|
+
InputOTP as InputOTPType,
|
|
35
|
+
} from './types'
|
|
36
|
+
import {
|
|
37
|
+
nativeInputFilters,
|
|
38
|
+
normalizeInputOTPLength,
|
|
39
|
+
normalizeOTPValue,
|
|
40
|
+
} from './utils'
|
|
41
|
+
|
|
42
|
+
const hiddenInputStyle: CSSProperties = {
|
|
43
|
+
position: 'absolute',
|
|
44
|
+
top: '0px',
|
|
45
|
+
left: '0px',
|
|
46
|
+
width: '1px',
|
|
47
|
+
height: '1px',
|
|
48
|
+
padding: '0px',
|
|
49
|
+
borderWidth: '0px',
|
|
50
|
+
opacity: 0,
|
|
51
|
+
color: 'transparent',
|
|
52
|
+
caretColor: 'transparent',
|
|
53
|
+
backgroundColor: 'transparent',
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const InputOTP = memo(forwardRef(InputOTPImpl)) as InputOTPType
|
|
57
|
+
|
|
58
|
+
function reportInputError(error: unknown) {
|
|
59
|
+
console.warn(
|
|
60
|
+
`[lynx-ui-input-otp][InputOTP] native input command failed: ${
|
|
61
|
+
String(error)
|
|
62
|
+
}`,
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function InputOTPImpl(
|
|
67
|
+
props: InputOTPProps,
|
|
68
|
+
ref: ForwardedRef<InputOTPRef>,
|
|
69
|
+
) {
|
|
70
|
+
const {
|
|
71
|
+
length = 6,
|
|
72
|
+
inputType = 'numeric',
|
|
73
|
+
value,
|
|
74
|
+
defaultValue = '',
|
|
75
|
+
autoFocus = false,
|
|
76
|
+
disabled = false,
|
|
77
|
+
invalid = false,
|
|
78
|
+
className,
|
|
79
|
+
style,
|
|
80
|
+
children,
|
|
81
|
+
onChange,
|
|
82
|
+
onComplete,
|
|
83
|
+
onFocus,
|
|
84
|
+
onBlur,
|
|
85
|
+
} = props
|
|
86
|
+
|
|
87
|
+
const fieldLength = normalizeInputOTPLength(length)
|
|
88
|
+
const isControlled = value !== undefined
|
|
89
|
+
const [uncontrolledValue, setUncontrolledValue] = useState(
|
|
90
|
+
normalizeOTPValue(defaultValue, fieldLength, inputType),
|
|
91
|
+
)
|
|
92
|
+
const [focused, setFocused] = useState(false)
|
|
93
|
+
const inputRef = useRef<NodesRef>(null)
|
|
94
|
+
const { onInputBlurred, onInputFocused } = useContext(
|
|
95
|
+
KeyboardAwareTriggerContext,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
const displayedValue = normalizeOTPValue(
|
|
99
|
+
isControlled ? value : uncontrolledValue,
|
|
100
|
+
fieldLength,
|
|
101
|
+
inputType,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
const invokeInput = useMemoizedFn(
|
|
105
|
+
(
|
|
106
|
+
method: 'focus' | 'blur' | 'setValue',
|
|
107
|
+
nextValue?: string,
|
|
108
|
+
): Promise<void> =>
|
|
109
|
+
new Promise((resolve, reject) => {
|
|
110
|
+
if (!inputRef.current) {
|
|
111
|
+
reject(new Error('InputOTP input is not mounted.'))
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
inputRef.current
|
|
116
|
+
.invoke({
|
|
117
|
+
method,
|
|
118
|
+
...(method === 'setValue'
|
|
119
|
+
? {
|
|
120
|
+
params: {
|
|
121
|
+
value: nextValue ?? '',
|
|
122
|
+
},
|
|
123
|
+
}
|
|
124
|
+
: {}),
|
|
125
|
+
success: () => {
|
|
126
|
+
resolve()
|
|
127
|
+
},
|
|
128
|
+
fail: (result: { code: number, data: string }) => {
|
|
129
|
+
reject(new InvokeRejectError(result.code, result.data))
|
|
130
|
+
},
|
|
131
|
+
})
|
|
132
|
+
.exec()
|
|
133
|
+
}),
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
const focus = useMemoizedFn(() =>
|
|
137
|
+
disabled ? Promise.resolve() : invokeInput('focus')
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
const blur = useMemoizedFn(() => invokeInput('blur'))
|
|
141
|
+
|
|
142
|
+
const syncNativeValue = useMemoizedFn(
|
|
143
|
+
(nextValue: string) => invokeInput('setValue', nextValue),
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
const emitValue = useMemoizedFn(
|
|
147
|
+
(nextValue: string, previousValue: string) => {
|
|
148
|
+
if (nextValue === previousValue) {
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
onChange?.(nextValue)
|
|
153
|
+
|
|
154
|
+
if (
|
|
155
|
+
nextValue.length === fieldLength
|
|
156
|
+
&& previousValue.length !== fieldLength
|
|
157
|
+
) {
|
|
158
|
+
onComplete?.(nextValue)
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
const setValue = useMemoizedFn((nextRawValue: string) => {
|
|
164
|
+
const nextValue = normalizeOTPValue(
|
|
165
|
+
nextRawValue,
|
|
166
|
+
fieldLength,
|
|
167
|
+
inputType,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
if (!isControlled) {
|
|
171
|
+
setUncontrolledValue(nextValue)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
emitValue(nextValue, displayedValue)
|
|
175
|
+
|
|
176
|
+
return syncNativeValue(nextValue)
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
useImperativeHandle(
|
|
180
|
+
ref,
|
|
181
|
+
() => ({
|
|
182
|
+
focus,
|
|
183
|
+
blur,
|
|
184
|
+
setValue,
|
|
185
|
+
clear: () => setValue(''),
|
|
186
|
+
getValue: () => displayedValue,
|
|
187
|
+
}),
|
|
188
|
+
[blur, displayedValue, focus, setValue],
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
useEffect(() => {
|
|
192
|
+
if (isControlled) {
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const nextValue = normalizeOTPValue(
|
|
197
|
+
uncontrolledValue,
|
|
198
|
+
fieldLength,
|
|
199
|
+
inputType,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
if (nextValue !== uncontrolledValue) {
|
|
203
|
+
setUncontrolledValue(nextValue)
|
|
204
|
+
}
|
|
205
|
+
}, [fieldLength, inputType, isControlled, uncontrolledValue])
|
|
206
|
+
|
|
207
|
+
useEffect(() => {
|
|
208
|
+
void syncNativeValue(displayedValue).catch(reportInputError)
|
|
209
|
+
}, [displayedValue, syncNativeValue])
|
|
210
|
+
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
if (autoFocus && !disabled) {
|
|
213
|
+
void focus().catch(reportInputError)
|
|
214
|
+
}
|
|
215
|
+
}, [autoFocus, disabled, focus])
|
|
216
|
+
|
|
217
|
+
const handleInput = useMemoizedFn(
|
|
218
|
+
(event: BaseEvent<'bindinput', InputInputEvent>) => {
|
|
219
|
+
const rawValue = event.detail.value
|
|
220
|
+
const nextValue = normalizeOTPValue(rawValue, fieldLength, inputType)
|
|
221
|
+
|
|
222
|
+
if (!isControlled) {
|
|
223
|
+
setUncontrolledValue(nextValue)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
emitValue(nextValue, displayedValue)
|
|
227
|
+
|
|
228
|
+
if (rawValue !== nextValue) {
|
|
229
|
+
void syncNativeValue(nextValue).catch(reportInputError)
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
const handleFocus = useMemoizedFn(
|
|
235
|
+
(_event: BaseEvent<'bindfocus', InputFocusEvent>) => {
|
|
236
|
+
setFocused(true)
|
|
237
|
+
onInputFocused?.()
|
|
238
|
+
onFocus?.()
|
|
239
|
+
},
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
const handleBlur = useMemoizedFn(
|
|
243
|
+
(_event: BaseEvent<'bindblur', InputBlurEvent>) => {
|
|
244
|
+
setFocused(false)
|
|
245
|
+
onInputBlurred?.()
|
|
246
|
+
onBlur?.()
|
|
247
|
+
},
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
const handleTap = useMemoizedFn(() => {
|
|
251
|
+
if (!disabled) {
|
|
252
|
+
void focus().catch(reportInputError)
|
|
253
|
+
}
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
const contextValue = useMemo<InputOTPRenderProps>(() => ({
|
|
257
|
+
value: displayedValue,
|
|
258
|
+
length: fieldLength,
|
|
259
|
+
inputType,
|
|
260
|
+
focused,
|
|
261
|
+
complete: displayedValue.length === fieldLength,
|
|
262
|
+
disabled,
|
|
263
|
+
invalid,
|
|
264
|
+
}), [
|
|
265
|
+
disabled,
|
|
266
|
+
displayedValue,
|
|
267
|
+
fieldLength,
|
|
268
|
+
focused,
|
|
269
|
+
inputType,
|
|
270
|
+
invalid,
|
|
271
|
+
])
|
|
272
|
+
|
|
273
|
+
const renderedChildren = typeof children === 'function'
|
|
274
|
+
? children(contextValue)
|
|
275
|
+
: children
|
|
276
|
+
|
|
277
|
+
return (
|
|
278
|
+
<InputOTPContext.Provider value={contextValue}>
|
|
279
|
+
<view
|
|
280
|
+
bindtap={handleTap}
|
|
281
|
+
className={clsx(className, {
|
|
282
|
+
'ui-focused': focused,
|
|
283
|
+
'ui-complete': contextValue.complete,
|
|
284
|
+
'ui-disabled': disabled,
|
|
285
|
+
'ui-invalid': invalid,
|
|
286
|
+
})}
|
|
287
|
+
flatten={false}
|
|
288
|
+
style={style}
|
|
289
|
+
>
|
|
290
|
+
{renderedChildren}
|
|
291
|
+
<input
|
|
292
|
+
ref={inputRef}
|
|
293
|
+
android-fullscreen-mode={false}
|
|
294
|
+
bindblur={handleBlur}
|
|
295
|
+
bindfocus={handleFocus}
|
|
296
|
+
bindinput={handleInput}
|
|
297
|
+
confirm-type='done'
|
|
298
|
+
disabled={disabled}
|
|
299
|
+
ignore-focus={true}
|
|
300
|
+
input-filter={nativeInputFilters[inputType]}
|
|
301
|
+
ios-auto-correct={false}
|
|
302
|
+
ios-spell-check={false}
|
|
303
|
+
maxlength={fieldLength}
|
|
304
|
+
show-soft-input-on-focus={true}
|
|
305
|
+
style={hiddenInputStyle}
|
|
306
|
+
type={inputType === 'numeric' ? 'number' : 'text'}
|
|
307
|
+
/>
|
|
308
|
+
</view>
|
|
309
|
+
</InputOTPContext.Provider>
|
|
310
|
+
)
|
|
311
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Copyright 2026 The Lynx Authors. All rights reserved.
|
|
2
|
+
// Licensed under the Apache License Version 2.0 that can be found in the
|
|
3
|
+
// LICENSE file in the root directory of this source tree.
|
|
4
|
+
|
|
5
|
+
import { createContext, useContext } from '@lynx-js/react'
|
|
6
|
+
|
|
7
|
+
import type { InputOTPRenderProps } from './types'
|
|
8
|
+
|
|
9
|
+
export const InputOTPContext = createContext<InputOTPRenderProps | null>(null)
|
|
10
|
+
|
|
11
|
+
export function useInputOTPContext(): InputOTPRenderProps {
|
|
12
|
+
const context = useContext(InputOTPContext)
|
|
13
|
+
|
|
14
|
+
if (!context) {
|
|
15
|
+
throw new Error('InputOTPSlot must be used within InputOTP.')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return context
|
|
19
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Copyright 2026 The Lynx Authors. All rights reserved.
|
|
2
|
+
// Licensed under the Apache License Version 2.0 that can be found in the
|
|
3
|
+
// LICENSE file in the root directory of this source tree.
|
|
4
|
+
|
|
5
|
+
import { memo } from '@lynx-js/react'
|
|
6
|
+
|
|
7
|
+
import { clsx } from 'clsx'
|
|
8
|
+
|
|
9
|
+
import { useInputOTPContext } from './InputOTPContext'
|
|
10
|
+
import type {
|
|
11
|
+
InputOTPSlotProps,
|
|
12
|
+
InputOTPSlotRenderProps,
|
|
13
|
+
InputOTPSlot as InputOTPSlotType,
|
|
14
|
+
} from './types'
|
|
15
|
+
|
|
16
|
+
export const InputOTPSlot = memo(InputOTPSlotImpl) as InputOTPSlotType
|
|
17
|
+
|
|
18
|
+
function InputOTPSlotImpl(props: InputOTPSlotProps) {
|
|
19
|
+
const {
|
|
20
|
+
index,
|
|
21
|
+
className,
|
|
22
|
+
style,
|
|
23
|
+
children,
|
|
24
|
+
} = props
|
|
25
|
+
const context = useInputOTPContext()
|
|
26
|
+
|
|
27
|
+
const {
|
|
28
|
+
value,
|
|
29
|
+
length,
|
|
30
|
+
focused,
|
|
31
|
+
complete,
|
|
32
|
+
disabled,
|
|
33
|
+
invalid,
|
|
34
|
+
} = context
|
|
35
|
+
const char = value[index]
|
|
36
|
+
const isFilled = char !== undefined
|
|
37
|
+
const hasFakeCaret = focused
|
|
38
|
+
&& !disabled
|
|
39
|
+
&& value.length < length
|
|
40
|
+
&& index === value.length
|
|
41
|
+
const slotState: InputOTPSlotRenderProps = {
|
|
42
|
+
index,
|
|
43
|
+
char,
|
|
44
|
+
focused: hasFakeCaret,
|
|
45
|
+
filled: isFilled,
|
|
46
|
+
complete,
|
|
47
|
+
disabled,
|
|
48
|
+
invalid,
|
|
49
|
+
}
|
|
50
|
+
const renderedChildren = typeof children === 'function'
|
|
51
|
+
? children(slotState)
|
|
52
|
+
: children
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<view
|
|
56
|
+
className={clsx(className, {
|
|
57
|
+
'ui-focused': hasFakeCaret,
|
|
58
|
+
'ui-filled': isFilled,
|
|
59
|
+
'ui-complete': complete,
|
|
60
|
+
'ui-disabled': disabled,
|
|
61
|
+
'ui-invalid': invalid,
|
|
62
|
+
})}
|
|
63
|
+
style={style}
|
|
64
|
+
>
|
|
65
|
+
{children === undefined
|
|
66
|
+
? (
|
|
67
|
+
<>
|
|
68
|
+
{char === undefined
|
|
69
|
+
? null
|
|
70
|
+
: <text>{char}</text>}
|
|
71
|
+
{hasFakeCaret
|
|
72
|
+
? <view />
|
|
73
|
+
: null}
|
|
74
|
+
</>
|
|
75
|
+
)
|
|
76
|
+
: renderedChildren}
|
|
77
|
+
</view>
|
|
78
|
+
)
|
|
79
|
+
}
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Copyright 2026 The Lynx Authors. All rights reserved.
|
|
2
|
+
// Licensed under the Apache License Version 2.0 that can be found in the
|
|
3
|
+
// LICENSE file in the root directory of this source tree.
|
|
4
|
+
|
|
5
|
+
export { InputOTP } from './InputOTP'
|
|
6
|
+
export { InputOTPSlot } from './InputOTPSlot'
|
|
7
|
+
|
|
8
|
+
export type {
|
|
9
|
+
InputOTPInputType,
|
|
10
|
+
InputOTPLength,
|
|
11
|
+
InputOTPProps,
|
|
12
|
+
InputOTPRef,
|
|
13
|
+
InputOTPRenderProps,
|
|
14
|
+
InputOTPSlotProps,
|
|
15
|
+
InputOTPSlotRenderProps,
|
|
16
|
+
InputOTPSlotUIVariants,
|
|
17
|
+
InputOTPUIVariants,
|
|
18
|
+
} from './types'
|