From 55b7aa977eb4eb52d04a4fc0cfd908dd6bba2176 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien?= <2128970+GoldAndLink@users.noreply.github.com> Date: Thu, 30 Nov 2023 07:21:05 +0100 Subject: [PATCH 1/4] feat: add focus + autofocus --- dist/src/OtpInput/OtpInput.js | 4 ++-- dist/src/OtpInput/OtpInput.types.d.ts | 2 ++ dist/src/OtpInput/useOtpInput.js | 5 ++++- src/OtpInput/OtpInput.tsx | 7 ++++--- src/OtpInput/useOtpInput.ts | 6 +++++- src/index.d.ts | 10 ++++++++++ 6 files changed, 27 insertions(+), 7 deletions(-) diff --git a/dist/src/OtpInput/OtpInput.js b/dist/src/OtpInput/OtpInput.js index 37fdbc1..2272b11 100644 --- a/dist/src/OtpInput/OtpInput.js +++ b/dist/src/OtpInput/OtpInput.js @@ -8,7 +8,7 @@ const VerticalStick_1 = require("./VerticalStick"); const useOtpInput_1 = require("./useOtpInput"); exports.OtpInput = (0, react_1.forwardRef)((props, ref) => { const { models: { text, inputRef, focusedInputIndex }, actions: { clear, handlePress, handleTextChange }, forms: { setTextWithRef }, } = (0, useOtpInput_1.useOtpInput)(props); - const { numberOfDigits, hideStick, focusColor = "#A4D0A4", focusStickBlinkingDuration, secureTextEntry = false, theme = {}, } = props; + const { autoFocus, numberOfDigits, hideStick, focusColor = "#A4D0A4", focusStickBlinkingDuration, secureTextEntry = false, theme = {}, } = props; const { containerStyle, inputsContainerStyle, pinCodeContainerStyle, pinCodeTextStyle, focusStickStyle, focusedPinCodeContainerStyle, } = theme; (0, react_1.useImperativeHandle)(ref, () => ({ clear, setValue: setTextWithRef })); return ( @@ -32,6 +32,6 @@ exports.OtpInput = (0, react_1.forwardRef)((props, ref) => { ); })} - + ); }); diff --git a/dist/src/OtpInput/OtpInput.types.d.ts b/dist/src/OtpInput/OtpInput.types.d.ts index 9ffe92c..b642c73 100644 --- a/dist/src/OtpInput/OtpInput.types.d.ts +++ b/dist/src/OtpInput/OtpInput.types.d.ts @@ -1,5 +1,6 @@ import { ColorValue, TextStyle, ViewStyle } from "react-native"; export interface OtpInputProps { + autoFocus?: boolean; numberOfDigits: number; focusColor?: ColorValue; onTextChange?: (text: string) => void; @@ -11,6 +12,7 @@ export interface OtpInputProps { } export interface OtpInputRef { clear: () => void; + focus: () => void; setValue: (value: string) => void; } export interface Theme { diff --git a/dist/src/OtpInput/useOtpInput.js b/dist/src/OtpInput/useOtpInput.js index 233162b..8eb9af8 100644 --- a/dist/src/OtpInput/useOtpInput.js +++ b/dist/src/OtpInput/useOtpInput.js @@ -28,9 +28,12 @@ const useOtpInput = ({ onTextChange, onFilled, numberOfDigits }) => { const clear = () => { setText(""); }; + const focus = () => { + inputRef.current?.focus(); + }; return { models: { text, inputRef, focusedInputIndex }, - actions: { handlePress, handleTextChange, clear }, + actions: { handlePress, handleTextChange, clear, focus }, forms: { setText, setTextWithRef }, }; }; diff --git a/src/OtpInput/OtpInput.tsx b/src/OtpInput/OtpInput.tsx index f073f55..e10817e 100644 --- a/src/OtpInput/OtpInput.tsx +++ b/src/OtpInput/OtpInput.tsx @@ -8,11 +8,12 @@ import { useOtpInput } from "./useOtpInput"; export const OtpInput = forwardRef((props, ref) => { const { models: { text, inputRef, focusedInputIndex }, - actions: { clear, handlePress, handleTextChange }, + actions: { clear, handlePress, handleTextChange, focus }, forms: { setTextWithRef }, } = useOtpInput(props); const { numberOfDigits, + autoFocus, hideStick, focusColor = "#A4D0A4", focusStickBlinkingDuration, @@ -28,7 +29,7 @@ export const OtpInput = forwardRef((props, ref) => { focusedPinCodeContainerStyle, } = theme; - useImperativeHandle(ref, () => ({ clear, setValue: setTextWithRef })); + useImperativeHandle(ref, () => ({ clear, focus, setValue: setTextWithRef })); return ( @@ -74,7 +75,7 @@ export const OtpInput = forwardRef((props, ref) => { maxLength={numberOfDigits} inputMode="numeric" ref={inputRef} - autoFocus + autoFocus={autoFocus} style={styles.hiddenInput} secureTextEntry={secureTextEntry} testID="otp-input-hidden" diff --git a/src/OtpInput/useOtpInput.ts b/src/OtpInput/useOtpInput.ts index 7679657..1e35405 100644 --- a/src/OtpInput/useOtpInput.ts +++ b/src/OtpInput/useOtpInput.ts @@ -32,9 +32,13 @@ export const useOtpInput = ({ onTextChange, onFilled, numberOfDigits }: OtpInput setText(""); }; + const focus = () => { + inputRef.current?.focus(); + }; + return { models: { text, inputRef, focusedInputIndex }, - actions: { handlePress, handleTextChange, clear }, + actions: { handlePress, handleTextChange, clear, focus }, forms: { setText, setTextWithRef }, }; }; diff --git a/src/index.d.ts b/src/index.d.ts index 3f3c23a..31053b3 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -2,6 +2,11 @@ declare module "OTPInput" { import { ColorValue, TextStyle, ViewStyle } from "react-native"; export interface OtpEntryProps { + /** + * Autofocus. + */ + autoFocus: boolean; + /** * The number of digits to be displayed in the OTP entry. */ @@ -35,6 +40,11 @@ declare module "OTPInput" { */ clear: () => void; + /** + * Focus of the OTP input. + */ + focus: () => void; + /** * Sets the value of the OTP input. * @param value - The value to be set. From b2d14dee3ae8aacd8087352086b2f4cde413af19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien?= <2128970+GoldAndLink@users.noreply.github.com> Date: Thu, 30 Nov 2023 07:21:16 +0100 Subject: [PATCH 2/4] docs: update doc --- README.MD | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.MD b/README.MD index 97cdeb5..70052cc 100644 --- a/README.MD +++ b/README.MD @@ -71,6 +71,7 @@ The `react-native-otp-entry` component accepts the following props: | Prop | Type | Description | | ---------------------------- | ---------------------- | -------------------------------------------------------------------------------------------------------------- | | `numberOfDigits` | number | The number of digits to be displayed in the OTP entry. | +| `autoFocus` | boolean | Set autofocus. | | `focusColor` | ColorValue | The color of the input field border and stick when it is focused. | | `onTextChange` | (text: string) => void | A callback function is invoked when the OTP text changes. It receives the updated text as an argument. | | `onFilled` | (text: string) => void | A callback function is invoked when the OTP input is fully filled. It receives a full otp code as an argument. | @@ -96,6 +97,7 @@ The `react-native-otp-entry` component exposes these functions with `ref`: | Prop | Type | Description | | ---------- | ------------------------ | ---------------------------------- | | `clear` | () => void; | Clears the value of the OTP input. | +| `focus` | () => void; | Focus of the OTP input. | | `setValue` | (value: string) => void; | Sets the value of the OTP input. | ## License From 9b2bd73f62b34ed0a4e54efba9f0ee4e14681224 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien?= <2128970+GoldAndLink@users.noreply.github.com> Date: Thu, 30 Nov 2023 23:10:53 +0100 Subject: [PATCH 3/4] fix: Property 'autoFocus' does not exist on type 'OtpInputProps' --- src/OtpInput/OtpInput.types.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/OtpInput/OtpInput.types.ts b/src/OtpInput/OtpInput.types.ts index 5714565..5c95cc5 100644 --- a/src/OtpInput/OtpInput.types.ts +++ b/src/OtpInput/OtpInput.types.ts @@ -2,6 +2,7 @@ import { ColorValue, TextStyle, ViewStyle } from "react-native"; export interface OtpInputProps { numberOfDigits: number; + autoFocus?: boolean; focusColor?: ColorValue; onTextChange?: (text: string) => void; onFilled?: (text: string) => void; @@ -13,6 +14,7 @@ export interface OtpInputProps { export interface OtpInputRef { clear: () => void; + focus: () => void; setValue: (value: string) => void; } From 195831f2b858ae8d9e2a60edcc0df6b7603f531b Mon Sep 17 00:00:00 2001 From: anday013 Date: Fri, 15 Dec 2023 21:38:05 +0100 Subject: [PATCH 4/4] test: add tests for focus and autofocus --- dist/src/OtpInput/OtpInput.js | 6 +++--- dist/src/OtpInput/OtpInput.test.js | 10 ++++++++++ dist/src/OtpInput/OtpInput.types.d.ts | 2 +- dist/src/OtpInput/useOtpInput.d.ts | 1 + dist/src/OtpInput/useOtpInput.test.js | 8 ++++++++ package-lock.json | 4 ++-- src/OtpInput/OtpInput.test.tsx | 16 ++++++++++++++++ src/OtpInput/OtpInput.tsx | 2 +- src/OtpInput/useOtpInput.test.ts | 11 +++++++++++ 9 files changed, 53 insertions(+), 7 deletions(-) diff --git a/dist/src/OtpInput/OtpInput.js b/dist/src/OtpInput/OtpInput.js index 2272b11..d8bc5fd 100644 --- a/dist/src/OtpInput/OtpInput.js +++ b/dist/src/OtpInput/OtpInput.js @@ -7,10 +7,10 @@ const OtpInput_styles_1 = require("./OtpInput.styles"); const VerticalStick_1 = require("./VerticalStick"); const useOtpInput_1 = require("./useOtpInput"); exports.OtpInput = (0, react_1.forwardRef)((props, ref) => { - const { models: { text, inputRef, focusedInputIndex }, actions: { clear, handlePress, handleTextChange }, forms: { setTextWithRef }, } = (0, useOtpInput_1.useOtpInput)(props); - const { autoFocus, numberOfDigits, hideStick, focusColor = "#A4D0A4", focusStickBlinkingDuration, secureTextEntry = false, theme = {}, } = props; + const { models: { text, inputRef, focusedInputIndex }, actions: { clear, handlePress, handleTextChange, focus }, forms: { setTextWithRef }, } = (0, useOtpInput_1.useOtpInput)(props); + const { numberOfDigits, autoFocus = true, hideStick, focusColor = "#A4D0A4", focusStickBlinkingDuration, secureTextEntry = false, theme = {}, } = props; const { containerStyle, inputsContainerStyle, pinCodeContainerStyle, pinCodeTextStyle, focusStickStyle, focusedPinCodeContainerStyle, } = theme; - (0, react_1.useImperativeHandle)(ref, () => ({ clear, setValue: setTextWithRef })); + (0, react_1.useImperativeHandle)(ref, () => ({ clear, focus, setValue: setTextWithRef })); return ( {Array(numberOfDigits) diff --git a/dist/src/OtpInput/OtpInput.test.js b/dist/src/OtpInput/OtpInput.test.js index 70024a9..78e7bd5 100644 --- a/dist/src/OtpInput/OtpInput.test.js +++ b/dist/src/OtpInput/OtpInput.test.js @@ -24,6 +24,16 @@ describe("OtpInput", () => { expect(input).toHaveTextContent("•"); }); }); + test("should autoFocused by default", () => { + renderOtpInput(); + const input = react_native_1.screen.getByTestId("otp-input-hidden"); + expect(input.props.autoFocus).toBe(true); + }); + test('should not focus if "autoFocus" is false', () => { + renderOtpInput({ autoFocus: false }); + const input = react_native_1.screen.getByTestId("otp-input-hidden"); + expect(input.props.autoFocus).toBe(false); + }); test("focusColor should not be overridden by theme", () => { renderOtpInput({ focusColor: "#000", diff --git a/dist/src/OtpInput/OtpInput.types.d.ts b/dist/src/OtpInput/OtpInput.types.d.ts index b642c73..36641ab 100644 --- a/dist/src/OtpInput/OtpInput.types.d.ts +++ b/dist/src/OtpInput/OtpInput.types.d.ts @@ -1,7 +1,7 @@ import { ColorValue, TextStyle, ViewStyle } from "react-native"; export interface OtpInputProps { - autoFocus?: boolean; numberOfDigits: number; + autoFocus?: boolean; focusColor?: ColorValue; onTextChange?: (text: string) => void; onFilled?: (text: string) => void; diff --git a/dist/src/OtpInput/useOtpInput.d.ts b/dist/src/OtpInput/useOtpInput.d.ts index f505a8d..24001b4 100644 --- a/dist/src/OtpInput/useOtpInput.d.ts +++ b/dist/src/OtpInput/useOtpInput.d.ts @@ -11,6 +11,7 @@ export declare const useOtpInput: ({ onTextChange, onFilled, numberOfDigits }: O handlePress: () => void; handleTextChange: (value: string) => void; clear: () => void; + focus: () => void; }; forms: { setText: import("react").Dispatch>; diff --git a/dist/src/OtpInput/useOtpInput.test.js b/dist/src/OtpInput/useOtpInput.test.js index f6446df..0f7f026 100644 --- a/dist/src/OtpInput/useOtpInput.test.js +++ b/dist/src/OtpInput/useOtpInput.test.js @@ -23,6 +23,14 @@ describe("useOtpInput", () => { expect(result.current.forms.setText).toHaveBeenCalledWith(""); }); }); + test("focus() should focus on input", () => { + jest.spyOn(React, "useRef").mockReturnValue({ current: { focus: jest.fn() } }); + const { result } = renderUseOtInput(); + result.current.actions.focus(); + (0, react_native_1.act)(() => { + expect(result.current.models.inputRef.current?.focus).toHaveBeenCalled(); + }); + }); test("setTextWithRef() should only call setText the first 'numberOfDigits' characters", () => { jest.spyOn(React, "useState").mockImplementation(() => ["", jest.fn()]); const { result } = renderUseOtInput(); diff --git a/package-lock.json b/package-lock.json index 51306ca..b07f720 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "react-native-otp-entry", - "version": "1.2.0", + "version": "1.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "react-native-otp-entry", - "version": "1.2.0", + "version": "1.3.0", "license": "MIT", "devDependencies": { "@testing-library/jest-native": "^5.4.3", diff --git a/src/OtpInput/OtpInput.test.tsx b/src/OtpInput/OtpInput.test.tsx index 6f75d87..9d922e1 100644 --- a/src/OtpInput/OtpInput.test.tsx +++ b/src/OtpInput/OtpInput.test.tsx @@ -34,6 +34,22 @@ describe("OtpInput", () => { }); }); + test("should autoFocused by default", () => { + renderOtpInput(); + + const input = screen.getByTestId("otp-input-hidden"); + + expect(input.props.autoFocus).toBe(true); + }); + + test('should not focus if "autoFocus" is false', () => { + renderOtpInput({ autoFocus: false }); + + const input = screen.getByTestId("otp-input-hidden"); + + expect(input.props.autoFocus).toBe(false); + }); + test("focusColor should not be overridden by theme", () => { renderOtpInput({ focusColor: "#000", diff --git a/src/OtpInput/OtpInput.tsx b/src/OtpInput/OtpInput.tsx index e10817e..1fc960e 100644 --- a/src/OtpInput/OtpInput.tsx +++ b/src/OtpInput/OtpInput.tsx @@ -13,7 +13,7 @@ export const OtpInput = forwardRef((props, ref) => { } = useOtpInput(props); const { numberOfDigits, - autoFocus, + autoFocus = true, hideStick, focusColor = "#A4D0A4", focusStickBlinkingDuration, diff --git a/src/OtpInput/useOtpInput.test.ts b/src/OtpInput/useOtpInput.test.ts index e78d919..1c45d43 100644 --- a/src/OtpInput/useOtpInput.test.ts +++ b/src/OtpInput/useOtpInput.test.ts @@ -36,6 +36,17 @@ describe("useOtpInput", () => { }); }); + test("focus() should focus on input", () => { + jest.spyOn(React, "useRef").mockReturnValue({ current: { focus: jest.fn() } } as any); + + const { result } = renderUseOtInput(); + result.current.actions.focus(); + + act(() => { + expect(result.current.models.inputRef.current?.focus).toHaveBeenCalled(); + }); + }); + test("setTextWithRef() should only call setText the first 'numberOfDigits' characters", () => { jest.spyOn(React, "useState").mockImplementation(() => ["", jest.fn()]); const { result } = renderUseOtInput();