diff --git a/README.md b/README.md index 8aa7a40..bd1ca9d 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ npm install react-native-input-code-otp ## Usage -```ts +```tsx import { TextInputOTP, TextInputOTPSlot, @@ -35,7 +35,7 @@ export function MyComponent() { - ) + ); } ``` @@ -46,10 +46,14 @@ export function MyComponent() { | `maxLength` | number - Required | The max number of digits to OTP code. | | `onFilled` | (code: string) => void - Optional | The callback function is executed when the OTP input has been entirely completed, and it receives the OTP code as a parameter. | +--- + | TextInputOTPGroup | Type | Description | | ----------------- | -------------------- | ----------------------------- | | `groupStyles` | ViewStyle - Optional | Custom styles for the `View`. | +--- + | TextInputOTPSlot | Type | Description | | ----------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------- | | `index` | number - Required | The position of the slot within the OTP input sequence. Each slot must have a unique index starting from 0. | @@ -58,6 +62,8 @@ export function MyComponent() { | `slotTextStyles` | TextStyle - Optional | Custom styles for the `Text`. | | `focusedSlotTextStyles` | TextStyle - Optional | Custom styles applied to the slot `Text` when it is focused. | +--- + | TextInputOTPSeparator | Type | Description | | --------------------- | -------------------- | ----------------------------- | | `separatorStyles` | ViewStyle - Optional | Custom styles for the `View`. | @@ -66,12 +72,116 @@ export function MyComponent() { The `TextInputOTP` component exposes these functions with `ref`: -| Prop | Type | Description | -| ---------- | ------------------------ | -------------------------------------------------------------------------- | -| `clear` | () => void; | Resets the OTP input by clearing all entered values. | -| `focus` | () => void; | Activates the OTP input field, allowing the user to type. | -| `blue` | () => void; | Deactivates the OTP input field, removing focus. | -| `setValue` | (value: string) => void; | Sets a custom value to the OTP input fields, overriding any current input. | +| Prop | Type | Description | +| ---------- | ----------------------- | -------------------------------------------------------------------------- | +| `clear` | () => void | Resets the OTP input by clearing all entered values. | +| `focus` | () => void | Activates the OTP input field, allowing the user to type. | +| `blue` | () => void | Deactivates the OTP input field, removing focus. | +| `setValue` | (value: string) => void | Sets a custom value to the OTP input fields, overriding any current input. | + +## Examples + +
+Usage with react-hook-form + +```tsx +import { Button, View } from 'react-native'; +import { + TextInputOTP, + TextInputOTPSlot, + TextInputOTPGroup, + TextInputOTPSeparator, +} from 'react-native-input-code-otp'; +import { Controller, useForm } from 'react-hook-form'; + +type FormValues = { + code: string; +}; + +export function MyComponent() { + const { control, handleSubmit } = useForm({ + defaultValues: { + code: '', + }, + }); + + function onSubmit({ code }: FormValues) { + console.log({ code }); + } + + return ( + + ( + + + + + + + + + + + + + + )} + /> + +
+ +
+Usage of ref to programmatically set OTP value + +```tsx +import { useRef } from 'react'; +import { Button, View } from 'react-native'; +import { + TextInputOTP, + TextInputOTPSlot, + TextInputOTPGroup, + TextInputOTPSeparator, +} from 'react-native-input-code-otp'; + +export function MyComponent() { + const inputRef = useRef(null); + + function onSomeAction() { + inputRef.current?.setValue('123456'); + } + + return ( + + + + + + + + + + + + + + + +
## Contributing diff --git a/example/src/App.tsx b/example/src/App.tsx index fd7fd8e..5732a01 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -1,25 +1,15 @@ -import { useRef } from 'react'; -import { StyleSheet, View, useColorScheme } from 'react-native'; +import { StyleSheet, View } from 'react-native'; import { TextInputOTP, TextInputOTPSlot, TextInputOTPGroup, TextInputOTPSeparator, - type TextInputOTPRef, } from 'react-native-input-code-otp'; export default function App() { - const colorSchema = useColorScheme(); - const inputRef = useRef(null); - const backgroundColor = colorSchema === 'light' ? 'white' : 'black'; - - function handleSubmit(code: string) { - console.log({ code }); - } - return ( - - + + console.log(code)}> diff --git a/src/components/text-input-otp-slot.tsx b/src/components/text-input-otp-slot.tsx index 5059030..9722bda 100644 --- a/src/components/text-input-otp-slot.tsx +++ b/src/components/text-input-otp-slot.tsx @@ -25,12 +25,12 @@ function TextInputOTPSlotComponent({ return ( handlePress(index)} {...rest} > {code[index] && ( diff --git a/src/components/text-input.tsx b/src/components/text-input.tsx index dde36b9..ee11032 100644 --- a/src/components/text-input.tsx +++ b/src/components/text-input.tsx @@ -7,15 +7,8 @@ export const TextInput = forwardRef< TextInputOTPRef, Omit >(({ autoFocus = true, ...rest }, ref) => { - const { - inputRef, - handleKeyPress, - handleChangeText, - setValue, - focus, - blur, - clear, - } = useTextInputOTP(); + const { inputRef, handleChangeText, setValue, focus, blur, clear } = + useTextInputOTP(); useImperativeHandle(ref, () => ({ setValue, @@ -26,14 +19,12 @@ export const TextInput = forwardRef< return ( ); }); diff --git a/src/hooks/use-slot-border-styles.ts b/src/hooks/use-slot-border-styles.ts index ae880d3..711228a 100644 --- a/src/hooks/use-slot-border-styles.ts +++ b/src/hooks/use-slot-border-styles.ts @@ -1,4 +1,4 @@ -import type { UseSlotBorderStylesProps } from '../types/text-input-otp-slot'; +import type { UseSlotBorderStylesProps } from '../types'; export function useSlotBorderStyles({ isFocused, diff --git a/src/hooks/use-text-input-otp.tsx b/src/hooks/use-text-input-otp.tsx index f0ed698..e03d172 100644 --- a/src/hooks/use-text-input-otp.tsx +++ b/src/hooks/use-text-input-otp.tsx @@ -7,19 +7,13 @@ import { useState, type PropsWithChildren, } from 'react'; -import type { - NativeSyntheticEvent, - TextInput, - TextInputKeyPressEventData, -} from 'react-native'; -import { BACKSPACE_KEY } from '../constants'; +import type { TextInput } from 'react-native'; import type { TextInputOTPContextProps, TextInputOTPProps } from '../types'; const TextInputOTPContext = createContext({ - code: [], + code: '', currentIndex: 0, inputRef: { current: null }, - handleKeyPress: () => {}, handleChangeText: () => {}, handlePress: () => {}, setValue: () => {}, @@ -31,76 +25,50 @@ const TextInputOTPContext = createContext({ export function TextInputOTPProvider({ autoFocus = true, maxLength, + value = '', onFilled, + onChangeText, children, }: PropsWithChildren< - Pick + Pick< + TextInputOTPProps, + 'autoFocus' | 'maxLength' | 'onFilled' | 'onChangeText' | 'value' + > >) { - const [code, setCode] = useState(Array(maxLength).fill('')); - const [currentIndex, setCurrentIndex] = useState(autoFocus ? 0 : -1); + const [code, setCode] = useState(value); + const [currentIndex, setCurrentIndex] = useState(() => (autoFocus ? 0 : -1)); const inputRef = useRef(null); - const updateCodeAtIndex = useCallback( - (index: number, value: string) => { - const newCode = [...code]; - newCode[index] = value; - setCode(newCode); - - return newCode; - }, - [code] - ); - - const handleKeyPress = useCallback( - (event: NativeSyntheticEvent) => { - const { key } = event.nativeEvent; - - if (key !== BACKSPACE_KEY) { - return; - } - - if (!code[currentIndex] && currentIndex > 0) { - updateCodeAtIndex(currentIndex - 1, ''); - setCurrentIndex((prev) => prev - 1); - return; - } - - updateCodeAtIndex(currentIndex, ''); - }, - [code, currentIndex, updateCodeAtIndex] - ); - const handleChangeText = useCallback( (text: string) => { - if (text.length > 1) { + if (text.length > maxLength) { return; } - const updatedCode = updateCodeAtIndex(currentIndex, text); + setCode(text); + onChangeText?.(text); - if (currentIndex < maxLength - 1) { - setCurrentIndex((prev) => prev + 1); - return; + if (text.length === maxLength) { + onFilled?.(text); } - const finalCode = [...updatedCode].join(''); - - if (finalCode.length === maxLength) { - onFilled?.(finalCode); + if (text.length < maxLength) { + setCurrentIndex(text.length); } }, - [currentIndex, maxLength, onFilled, updateCodeAtIndex] + [maxLength, onChangeText, onFilled] ); - function handlePress(index: number) { - setCurrentIndex(index); + const handlePress = useCallback(() => { + setCurrentIndex(code.length); inputRef.current?.focus(); - } + }, [code.length]); const setValue = useCallback( (text: string) => { - const value = text.length > maxLength ? text.slice(0, maxLength) : text; - setCode(Array.from(value)); + const filledCode = + text.length > maxLength ? text.slice(0, maxLength) : text; + setCode(filledCode); setCurrentIndex(maxLength - 1); }, [maxLength] @@ -115,16 +83,15 @@ export function TextInputOTPProvider({ } const clear = useCallback(() => { - setCode(Array(maxLength).fill('')); + setCode(''); setCurrentIndex(0); - }, [maxLength]); + }, []); const contextValue = useMemo( () => ({ - code, + code: value || code, currentIndex, inputRef, - handleKeyPress, handleChangeText, handlePress, setValue, @@ -132,7 +99,7 @@ export function TextInputOTPProvider({ blur, clear, }), - [clear, code, currentIndex, handleChangeText, handleKeyPress, setValue] + [clear, code, currentIndex, handleChangeText, handlePress, setValue, value] ); return ( diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..0bd8351 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,64 @@ +import type { ReactNode, RefObject } from 'react'; +import type { + PressableProps, + StyleProp, + TextInput, + TextInputProps, + TextStyle, + ViewProps, + ViewStyle, +} from 'react-native'; + +export type TextInputOTPContextProps = { + code: string; + currentIndex: number; + inputRef: RefObject; + handleChangeText: (text: string) => void; + handlePress: () => void; + setValue: (text: string) => void; + focus: () => void; + blur: () => void; + clear: () => void; +}; + +export type TextInputOTPGroupProps = { + groupStyles?: StyleProp; +} & Omit; + +export type TextInputOTPRef = { + setValue: (text: string) => void; + focus: () => void; + blur: () => void; + clear: () => void; +}; + +export type TextInputOTPSeparatorProps = { + separatorStyles?: StyleProp; +} & Omit; + +export type TextInputOTPSlotProps = { + index: number; + focusedSlotStyles?: StyleProp; + slotStyles?: StyleProp; + focusedSlotTextStyles?: StyleProp; + slotTextStyles?: StyleProp; +} & PressableProps; + +export type TextInputOTPSlotInternalProps = { + isFirst?: boolean; + isLast?: boolean; +}; + +export type UseSlotBorderStylesProps = { + isFocused: boolean; + isFirst?: boolean; + isLast?: boolean; +}; + +export type TextInputOTPProps = { + children: ReactNode; + autoFocus?: boolean; + maxLength: number; + onFilled?: (text: string) => void; + containerStyles?: StyleProp; +} & Omit; diff --git a/src/types/index.ts b/src/types/index.ts deleted file mode 100644 index 3a7b32a..0000000 --- a/src/types/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from './text-input-otp'; -export * from './text-input-otp-context'; -export * from './text-input-otp-group'; -export * from './text-input-otp-ref'; -export * from './text-input-otp-separator'; -export * from './text-input-otp-slot'; diff --git a/src/types/text-input-otp-context.ts b/src/types/text-input-otp-context.ts deleted file mode 100644 index f3c35f4..0000000 --- a/src/types/text-input-otp-context.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { RefObject } from 'react'; -import type { - NativeSyntheticEvent, - TextInput, - TextInputKeyPressEventData, -} from 'react-native'; - -export type TextInputOTPContextProps = { - code: string[]; - currentIndex: number; - inputRef: RefObject; - handleKeyPress: ( - event: NativeSyntheticEvent - ) => void; - handleChangeText: (text: string) => void; - handlePress: (index: number) => void; - setValue: (text: string) => void; - focus: () => void; - blur: () => void; - clear: () => void; -}; diff --git a/src/types/text-input-otp-group.ts b/src/types/text-input-otp-group.ts deleted file mode 100644 index bee8046..0000000 --- a/src/types/text-input-otp-group.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { StyleProp, ViewProps, ViewStyle } from 'react-native'; - -export type TextInputOTPGroupProps = { - groupStyles?: StyleProp; -} & Omit; diff --git a/src/types/text-input-otp-ref.ts b/src/types/text-input-otp-ref.ts deleted file mode 100644 index 54250d4..0000000 --- a/src/types/text-input-otp-ref.ts +++ /dev/null @@ -1,6 +0,0 @@ -export type TextInputOTPRef = { - setValue: (text: string) => void; - focus: () => void; - blur: () => void; - clear: () => void; -}; diff --git a/src/types/text-input-otp-separator.ts b/src/types/text-input-otp-separator.ts deleted file mode 100644 index f1b3a54..0000000 --- a/src/types/text-input-otp-separator.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { StyleProp, ViewProps, ViewStyle } from 'react-native'; - -export type TextInputOTPSeparatorProps = { - separatorStyles?: StyleProp; -} & Omit; diff --git a/src/types/text-input-otp-slot.ts b/src/types/text-input-otp-slot.ts deleted file mode 100644 index df39ffc..0000000 --- a/src/types/text-input-otp-slot.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { - PressableProps, - StyleProp, - TextStyle, - ViewStyle, -} from 'react-native'; - -export type TextInputOTPSlotProps = { - index: number; - focusedSlotStyles?: StyleProp; - slotStyles?: StyleProp; - focusedSlotTextStyles?: StyleProp; - slotTextStyles?: StyleProp; -} & PressableProps; - -export type TextInputOTPSlotInternalProps = { - isFirst?: boolean; - isLast?: boolean; -}; - -export type UseSlotBorderStylesProps = { - isFocused: boolean; - isFirst?: boolean; - isLast?: boolean; -}; diff --git a/src/types/text-input-otp.ts b/src/types/text-input-otp.ts deleted file mode 100644 index eebd0d8..0000000 --- a/src/types/text-input-otp.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { ReactNode } from 'react'; -import type { StyleProp, TextInputProps, ViewStyle } from 'react-native'; - -export type TextInputOTPProps = { - children: ReactNode; - autoFocus?: boolean; - maxLength: number; - onFilled?: (text: string) => void; - containerStyles?: StyleProp; -} & Omit;