Skip to content

adjust/handle-with-input-focus #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 118 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ npm install react-native-input-code-otp

## Usage

```ts
```tsx
import {
TextInputOTP,
TextInputOTPSlot,
Expand All @@ -35,7 +35,7 @@ export function MyComponent() {
<TextInputOTPSlot index={5} />
</TextInputOTPGroup>
</TextInputOTP>
)
);
}
```

Expand All @@ -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. |
Expand All @@ -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`. |
Expand All @@ -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

<details>
<summary>Usage with react-hook-form</summary>

```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<FormValues>({
defaultValues: {
code: '',
},
});

function onSubmit({ code }: FormValues) {
console.log({ code });
}

return (
<View>
<Controller
name="code"
control={control}
render={({ field: { onChange, value } }) => (
<TextInputOTP value={value} onChangeText={onChange} maxLength={6}>
<TextInputOTPGroup>
<TextInputOTPSlot index={0} />
<TextInputOTPSlot index={1} />
<TextInputOTPSlot index={2} />
</TextInputOTPGroup>
<TextInputOTPSeparator />
<TextInputOTPGroup>
<TextInputOTPSlot index={3} />
<TextInputOTPSlot index={4} />
<TextInputOTPSlot index={5} />
</TextInputOTPGroup>
</TextInputOTP>
)}
/>

<Button title="Submit" onPress={handleSubmit(onSubmit)} />
</View>
);
}
```

</details>

<details>
<summary>Usage of ref to programmatically set OTP value</summary>

```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<TextInputOTPRef>(null);

function onSomeAction() {
inputRef.current?.setValue('123456');
}

return (
<View>
<TextInputOTP ref={inputRef} maxLength={6}>
<TextInputOTPGroup>
<TextInputOTPSlot index={0} />
<TextInputOTPSlot index={1} />
<TextInputOTPSlot index={2} />
</TextInputOTPGroup>
<TextInputOTPSeparator />
<TextInputOTPGroup>
<TextInputOTPSlot index={3} />
<TextInputOTPSlot index={4} />
<TextInputOTPSlot index={5} />
</TextInputOTPGroup>
</TextInputOTP>

<Button title="Submit" onPress={onSomeAction} />
</View>
);
}
```

</details>

## Contributing

Expand Down
16 changes: 3 additions & 13 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
@@ -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<TextInputOTPRef>(null);
const backgroundColor = colorSchema === 'light' ? 'white' : 'black';

function handleSubmit(code: string) {
console.log({ code });
}

return (
<View style={[styles.container, { backgroundColor }]}>
<TextInputOTP ref={inputRef} maxLength={6} onFilled={handleSubmit}>
<View style={styles.container}>
<TextInputOTP maxLength={6} onFilled={(code) => console.log(code)}>
<TextInputOTPGroup>
<TextInputOTPSlot index={0} />
<TextInputOTPSlot index={1} />
Expand Down
2 changes: 1 addition & 1 deletion src/components/text-input-otp-slot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ function TextInputOTPSlotComponent({

return (
<Pressable
onPress={handlePress}
style={StyleSheet.flatten([
styles.slot,
borderStyles,
isFocused ? focusedSlotStyles : slotStyles,
])}
onPress={() => handlePress(index)}
{...rest}
>
{code[index] && (
Expand Down
17 changes: 4 additions & 13 deletions src/components/text-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,8 @@ export const TextInput = forwardRef<
TextInputOTPRef,
Omit<TextInputOTPProps, 'children'>
>(({ autoFocus = true, ...rest }, ref) => {
const {
inputRef,
handleKeyPress,
handleChangeText,
setValue,
focus,
blur,
clear,
} = useTextInputOTP();
const { inputRef, handleChangeText, setValue, focus, blur, clear } =
useTextInputOTP();

useImperativeHandle(ref, () => ({
setValue,
Expand All @@ -26,14 +19,12 @@ export const TextInput = forwardRef<

return (
<RNTextInput
value=""
ref={inputRef}
onKeyPress={handleKeyPress}
onChangeText={handleChangeText}
style={styles.input}
keyboardType="number-pad"
autoFocus={autoFocus}
style={styles.input}
{...rest}
onChangeText={handleChangeText}
/>
);
});
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/use-slot-border-styles.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { UseSlotBorderStylesProps } from '../types/text-input-otp-slot';
import type { UseSlotBorderStylesProps } from '../types';

export function useSlotBorderStyles({
isFocused,
Expand Down
89 changes: 28 additions & 61 deletions src/hooks/use-text-input-otp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<TextInputOTPContextProps>({
code: [],
code: '',
currentIndex: 0,
inputRef: { current: null },
handleKeyPress: () => {},
handleChangeText: () => {},
handlePress: () => {},
setValue: () => {},
Expand All @@ -31,76 +25,50 @@ const TextInputOTPContext = createContext<TextInputOTPContextProps>({
export function TextInputOTPProvider({
autoFocus = true,
maxLength,
value = '',
onFilled,
onChangeText,
children,
}: PropsWithChildren<
Pick<TextInputOTPProps, 'autoFocus' | 'maxLength' | 'onFilled'>
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<TextInput>(null);

const updateCodeAtIndex = useCallback(
(index: number, value: string) => {
const newCode = [...code];
newCode[index] = value;
setCode(newCode);

return newCode;
},
[code]
);

const handleKeyPress = useCallback(
(event: NativeSyntheticEvent<TextInputKeyPressEventData>) => {
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]
Expand All @@ -115,24 +83,23 @@ 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,
focus,
blur,
clear,
}),
[clear, code, currentIndex, handleChangeText, handleKeyPress, setValue]
[clear, code, currentIndex, handleChangeText, handlePress, setValue, value]
);

return (
Expand Down
Loading