Skip to content

Commit

Permalink
Add disabled state to pin container (#35)
Browse files Browse the repository at this point in the history
* add disabled logic

* add disabled container styles

* add more tests for disabled state

* chore: update tests and build

* test: add missing case for disabled state

---------

Co-authored-by: Anday <48630069+anday013@users.noreply.github.com>
Co-authored-by: anday013 <anday.ismayilzada@gmail.com>
  • Loading branch information
3 people authored Mar 15, 2024
1 parent ee71a07 commit 3680e2b
Show file tree
Hide file tree
Showing 13 changed files with 101 additions and 9 deletions.
11 changes: 7 additions & 4 deletions dist/src/OtpInput/OtpInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ 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, focus }, forms: { setTextWithRef }, } = (0, useOtpInput_1.useOtpInput)(props);
const { numberOfDigits = 6, autoFocus = true, hideStick, focusColor = "#A4D0A4", focusStickBlinkingDuration, secureTextEntry = false, theme = {}, } = props;
const { containerStyle, inputsContainerStyle, pinCodeContainerStyle, pinCodeTextStyle, focusStickStyle, focusedPinCodeContainerStyle, filledPinCodeContainerStyle, } = theme;
const { disabled, numberOfDigits = 6, autoFocus = true, hideStick, focusColor = "#A4D0A4", focusStickBlinkingDuration, secureTextEntry = false, theme = {}, } = props;
const { containerStyle, inputsContainerStyle, pinCodeContainerStyle, pinCodeTextStyle, focusStickStyle, focusedPinCodeContainerStyle, filledPinCodeContainerStyle, disabledPinCodeContainerStyle, } = theme;
(0, react_1.useImperativeHandle)(ref, () => ({ clear, focus, setValue: setTextWithRef }));
const generatePinCodeContainerStyle = (isFocusedInput, char) => {
const stylesArray = [OtpInput_styles_1.styles.codeContainer, pinCodeContainerStyle];
Expand All @@ -22,6 +22,9 @@ exports.OtpInput = (0, react_1.forwardRef)((props, ref) => {
if (filledPinCodeContainerStyle && Boolean(char)) {
stylesArray.push(filledPinCodeContainerStyle);
}
if (disabledPinCodeContainerStyle && disabled) {
stylesArray.push(disabledPinCodeContainerStyle);
}
return stylesArray;
};
return (<react_native_1.View style={[OtpInput_styles_1.styles.container, containerStyle]}>
Expand All @@ -31,13 +34,13 @@ exports.OtpInput = (0, react_1.forwardRef)((props, ref) => {
.map((_, index) => {
const char = text[index];
const isFocusedInput = index === focusedInputIndex;
return (<react_native_1.Pressable key={`${char}-${index}`} onPress={handlePress} style={generatePinCodeContainerStyle(isFocusedInput, char)} testID="otp-input">
return (<react_native_1.Pressable key={`${char}-${index}`} disabled={disabled} onPress={handlePress} style={generatePinCodeContainerStyle(isFocusedInput, char)} testID="otp-input">
{isFocusedInput && !hideStick ? (<VerticalStick_1.VerticalStick focusColor={focusColor} style={focusStickStyle} focusStickBlinkingDuration={focusStickBlinkingDuration}/>) : (<react_native_1.Text style={[OtpInput_styles_1.styles.codeText, pinCodeTextStyle]}>
{char && secureTextEntry ? "•" : char}
</react_native_1.Text>)}
</react_native_1.Pressable>);
})}
</react_native_1.View>
<react_native_1.TextInput value={text} onChangeText={handleTextChange} maxLength={numberOfDigits} inputMode="numeric" textContentType="oneTimeCode" ref={inputRef} autoFocus={autoFocus} style={OtpInput_styles_1.styles.hiddenInput} secureTextEntry={secureTextEntry} autoComplete="one-time-code" testID="otp-input-hidden"/>
<react_native_1.TextInput value={text} onChangeText={handleTextChange} maxLength={numberOfDigits} inputMode="numeric" textContentType="oneTimeCode" ref={inputRef} autoFocus={autoFocus} style={OtpInput_styles_1.styles.hiddenInput} secureTextEntry={secureTextEntry} autoComplete="one-time-code" aria-disabled={disabled} editable={!disabled} testID="otp-input-hidden"/>
</react_native_1.View>);
});
18 changes: 18 additions & 0 deletions dist/src/OtpInput/OtpInput.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@ describe("OtpInput", () => {
const input = react_native_1.screen.getByTestId("otp-input-hidden");
expect(input.props.autoFocus).toBe(false);
});
test("it should not allow input if disabled is true", () => {
renderOtpInput({ disabled: true });
const input = react_native_1.screen.getByTestId("otp-input-hidden");
react_native_1.fireEvent.changeText(input, "123456");
const inputs = react_native_1.screen.getAllByTestId("otp-input");
expect(inputs[0]).not.toHaveTextContent("1");
inputs.forEach((i) => expect(i).toBeDisabled());
const hiddenInput = react_native_1.screen.getByTestId("otp-input-hidden");
expect(hiddenInput).toBeDisabled();
});
test("focusColor should not be overridden by theme", () => {
renderOtpInput({
focusColor: "#000",
Expand Down Expand Up @@ -72,6 +82,14 @@ describe("OtpInput", () => {
expect(inputs[1]).toHaveStyle({ borderBottomColor: "red" });
expect(inputs[2]).not.toHaveStyle({ borderBottomColor: "red" });
});
test("disabledPinCodeContainerStyle should allow for new style when input is disabled", () => {
renderOtpInput({
disabled: true,
theme: { disabledPinCodeContainerStyle: { borderBottomColor: "red" } },
});
const inputs = react_native_1.screen.getAllByTestId("otp-input");
expect(inputs[0]).toHaveStyle({ borderBottomColor: "red" });
});
});
describe("Logic", () => {
test("should split text on screen from the text written in the hidden input", () => {
Expand Down
2 changes: 2 additions & 0 deletions dist/src/OtpInput/OtpInput.types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export interface OtpInputProps {
focusStickBlinkingDuration?: number;
secureTextEntry?: boolean;
theme?: Theme;
disabled?: boolean;
}
export interface OtpInputRef {
clear: () => void;
Expand All @@ -23,4 +24,5 @@ export interface Theme {
pinCodeTextStyle?: TextStyle;
focusStickStyle?: ViewStyle;
focusedPinCodeContainerStyle?: ViewStyle;
disabledPinCodeContainerStyle?: ViewStyle;
}
2 changes: 1 addition & 1 deletion dist/src/OtpInput/useOtpInput.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/// <reference types="react" />
import { TextInput } from "react-native";
import { OtpInputProps } from "./OtpInput.types";
export declare const useOtpInput: ({ onTextChange, onFilled, numberOfDigits }: OtpInputProps) => {
export declare const useOtpInput: ({ onTextChange, onFilled, numberOfDigits, disabled, }: OtpInputProps) => {
models: {
text: string;
inputRef: import("react").RefObject<TextInput>;
Expand Down
4 changes: 3 additions & 1 deletion dist/src/OtpInput/useOtpInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
exports.useOtpInput = void 0;
const react_1 = require("react");
const react_native_1 = require("react-native");
const useOtpInput = ({ onTextChange, onFilled, numberOfDigits = 6 }) => {
const useOtpInput = ({ onTextChange, onFilled, numberOfDigits = 6, disabled, }) => {
const [text, setText] = (0, react_1.useState)("");
const inputRef = (0, react_1.useRef)(null);
const focusedInputIndex = text.length;
Expand All @@ -15,6 +15,8 @@ const useOtpInput = ({ onTextChange, onFilled, numberOfDigits = 6 }) => {
inputRef.current?.focus();
};
const handleTextChange = (value) => {
if (disabled)
return;
setText(value);
onTextChange?.(value);
if (value.length === numberOfDigits) {
Expand Down
11 changes: 11 additions & 0 deletions dist/src/OtpInput/useOtpInput.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,17 @@ describe("useOtpInput", () => {
expect(mockOnTextChange).toHaveBeenCalledWith(value);
});
});
test("handleTextChange() should not proceed if the input is disabled", () => {
const value = "123456";
const mockOnTextChange = jest.fn();
jest.spyOn(React, "useState").mockImplementation(() => ["", jest.fn()]);
const { result } = renderUseOtInput({ onTextChange: mockOnTextChange, disabled: true });
result.current.actions.handleTextChange(value);
(0, react_native_1.act)(() => {
expect(result.current.forms.setText).not.toHaveBeenCalled();
expect(mockOnTextChange).not.toHaveBeenCalled();
});
});
test("onFilled() should be called when the input filled", () => {
const value = "123456";
const mockOnFilled = jest.fn();
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 24 additions & 0 deletions src/OtpInput/OtpInput.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,20 @@ describe("OtpInput", () => {
expect(input.props.autoFocus).toBe(false);
});

test("it should not allow input if disabled is true", () => {
renderOtpInput({ disabled: true });

const input = screen.getByTestId("otp-input-hidden");
fireEvent.changeText(input, "123456");

const inputs = screen.getAllByTestId("otp-input");
expect(inputs[0]).not.toHaveTextContent("1");
inputs.forEach((i) => expect(i).toBeDisabled());

const hiddenInput = screen.getByTestId("otp-input-hidden");
expect(hiddenInput).toBeDisabled();
});

test("focusColor should not be overridden by theme", () => {
renderOtpInput({
focusColor: "#000",
Expand Down Expand Up @@ -102,6 +116,16 @@ describe("OtpInput", () => {
expect(inputs[1]).toHaveStyle({ borderBottomColor: "red" });
expect(inputs[2]).not.toHaveStyle({ borderBottomColor: "red" });
});

test("disabledPinCodeContainerStyle should allow for new style when input is disabled", () => {
renderOtpInput({
disabled: true,
theme: { disabledPinCodeContainerStyle: { borderBottomColor: "red" } },
});

const inputs = screen.getAllByTestId("otp-input");
expect(inputs[0]).toHaveStyle({ borderBottomColor: "red" });
});
});

describe("Logic", () => {
Expand Down
9 changes: 9 additions & 0 deletions src/OtpInput/OtpInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const OtpInput = forwardRef<OtpInputRef, OtpInputProps>((props, ref) => {
forms: { setTextWithRef },
} = useOtpInput(props);
const {
disabled,
numberOfDigits = 6,
autoFocus = true,
hideStick,
Expand All @@ -28,6 +29,7 @@ export const OtpInput = forwardRef<OtpInputRef, OtpInputProps>((props, ref) => {
focusStickStyle,
focusedPinCodeContainerStyle,
filledPinCodeContainerStyle,
disabledPinCodeContainerStyle,
} = theme;

useImperativeHandle(ref, () => ({ clear, focus, setValue: setTextWithRef }));
Expand All @@ -46,6 +48,10 @@ export const OtpInput = forwardRef<OtpInputRef, OtpInputProps>((props, ref) => {
stylesArray.push(filledPinCodeContainerStyle);
}

if (disabledPinCodeContainerStyle && disabled) {
stylesArray.push(disabledPinCodeContainerStyle);
}

return stylesArray;
};

Expand All @@ -61,6 +67,7 @@ export const OtpInput = forwardRef<OtpInputRef, OtpInputProps>((props, ref) => {
return (
<Pressable
key={`${char}-${index}`}
disabled={disabled}
onPress={handlePress}
style={generatePinCodeContainerStyle(isFocusedInput, char)}
testID="otp-input"
Expand Down Expand Up @@ -91,6 +98,8 @@ export const OtpInput = forwardRef<OtpInputRef, OtpInputProps>((props, ref) => {
style={styles.hiddenInput}
secureTextEntry={secureTextEntry}
autoComplete="one-time-code"
aria-disabled={disabled}
editable={!disabled}
testID="otp-input-hidden"
/>
</View>
Expand Down
2 changes: 2 additions & 0 deletions src/OtpInput/OtpInput.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export interface OtpInputProps {
focusStickBlinkingDuration?: number;
secureTextEntry?: boolean;
theme?: Theme;
disabled?: boolean;
}

export interface OtpInputRef {
Expand All @@ -26,4 +27,5 @@ export interface Theme {
pinCodeTextStyle?: TextStyle;
focusStickStyle?: ViewStyle;
focusedPinCodeContainerStyle?: ViewStyle;
disabledPinCodeContainerStyle?: ViewStyle;
}
1 change: 1 addition & 0 deletions src/OtpInput/__snapshots__/OtpInput.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,7 @@ exports[`OtpInput UI should render correctly 1`] = `
<TextInput
autoComplete="one-time-code"
autoFocus={true}
editable={true}
inputMode="numeric"
maxLength={6}
onChangeText={[Function]}
Expand Down
14 changes: 14 additions & 0 deletions src/OtpInput/useOtpInput.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,20 @@ describe("useOtpInput", () => {
});
});

test("handleTextChange() should not proceed if the input is disabled", () => {
const value = "123456";
const mockOnTextChange = jest.fn();
jest.spyOn(React, "useState").mockImplementation(() => ["", jest.fn()]);

const { result } = renderUseOtInput({ onTextChange: mockOnTextChange, disabled: true });
result.current.actions.handleTextChange(value);

act(() => {
expect(result.current.forms.setText).not.toHaveBeenCalled();
expect(mockOnTextChange).not.toHaveBeenCalled();
});
});

test("onFilled() should be called when the input filled", () => {
const value = "123456";
const mockOnFilled = jest.fn();
Expand Down
8 changes: 7 additions & 1 deletion src/OtpInput/useOtpInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import { useRef, useState } from "react";
import { Keyboard, TextInput } from "react-native";
import { OtpInputProps } from "./OtpInput.types";

export const useOtpInput = ({ onTextChange, onFilled, numberOfDigits = 6 }: OtpInputProps) => {
export const useOtpInput = ({
onTextChange,
onFilled,
numberOfDigits = 6,
disabled,
}: OtpInputProps) => {
const [text, setText] = useState("");
const inputRef = useRef<TextInput>(null);
const focusedInputIndex = text.length;
Expand All @@ -16,6 +21,7 @@ export const useOtpInput = ({ onTextChange, onFilled, numberOfDigits = 6 }: OtpI
};

const handleTextChange = (value: string) => {
if (disabled) return;
setText(value);
onTextChange?.(value);
if (value.length === numberOfDigits) {
Expand Down

0 comments on commit 3680e2b

Please sign in to comment.