@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.
@@ -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'