Skip to content

Commit 6d549b5

Browse files
authored
feat: placeholder disappears as you type (#89)
1 parent 0da2158 commit 6d549b5

File tree

5 files changed

+83
-124
lines changed

5 files changed

+83
-124
lines changed

src/OtpInput/OtpInput.tsx

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { useOtpInput } from "./useOtpInput";
88

99
export const OtpInput = forwardRef<OtpInputRef, OtpInputProps>((props, ref) => {
1010
const {
11-
models: { text, inputRef, focusedInputIndex, isFocused, isPlaceholderActive },
11+
models: { text, inputRef, focusedInputIndex, isFocused, placeholder },
1212
actions: { clear, handlePress, handleTextChange, focus, handleFocus, handleBlur },
1313
forms: { setTextWithRef },
1414
} = useOtpInput(props);
@@ -23,7 +23,6 @@ export const OtpInput = forwardRef<OtpInputRef, OtpInputProps>((props, ref) => {
2323
theme = {},
2424
textInputProps,
2525
type = "numeric",
26-
placeholder,
2726
} = props;
2827
const {
2928
containerStyle,
@@ -61,16 +60,17 @@ export const OtpInput = forwardRef<OtpInputRef, OtpInputProps>((props, ref) => {
6160
};
6261

6362
const placeholderStyle = {
64-
opacity: isPlaceholderActive ? 0.5 : pinCodeTextStyle?.opacity || 1,
65-
...(isPlaceholderActive ? placeholderTextStyle : []),
63+
opacity: !!placeholder ? 0.5 : pinCodeTextStyle?.opacity || 1,
64+
...(!!placeholder ? placeholderTextStyle : []),
6665
};
6766

6867
return (
6968
<View style={[styles.container, containerStyle, inputsContainerStyle]}>
7069
{Array(numberOfDigits)
7170
.fill(0)
7271
.map((_, index) => {
73-
const char = isPlaceholderActive ? placeholder?.[index] || " " : text[index];
72+
const isPlaceholderCell = !!placeholder && !text?.[index];
73+
const char = isPlaceholderCell ? placeholder?.[index] || " " : text[index];
7474
const isFocusedInput = index === focusedInputIndex && !disabled && Boolean(isFocused);
7575
const isFilledLastInput = text.length === numberOfDigits && index === text.length - 1;
7676
const isFocusedContainer = isFocusedInput || (isFilledLastInput && Boolean(isFocused));
@@ -90,7 +90,13 @@ export const OtpInput = forwardRef<OtpInputRef, OtpInputProps>((props, ref) => {
9090
focusStickBlinkingDuration={focusStickBlinkingDuration}
9191
/>
9292
) : (
93-
<Text style={[styles.codeText, pinCodeTextStyle, placeholderStyle]}>
93+
<Text
94+
style={[
95+
styles.codeText,
96+
pinCodeTextStyle,
97+
isPlaceholderCell ? placeholderStyle : {},
98+
]}
99+
>
94100
{char && secureTextEntry ? "•" : char}
95101
</Text>
96102
)}

src/OtpInput/__tests__/OtpInput.test.tsx

Lines changed: 52 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -265,13 +265,43 @@ describe("OtpInput", () => {
265265
});
266266
});
267267
describe("Placeholder", () => {
268-
test("should show placeholder if text is empty", () => {
269-
renderOtpInput({ placeholder: "000000" });
268+
test("should cover the whole input if placeholder is set with one char", async () => {
269+
renderOtpInput({ placeholder: "0", hideStick: true });
270270

271271
const inputs = screen.getAllByTestId("otp-input");
272-
inputs.forEach((input) => {
273-
waitFor(() => expect(input).toHaveTextContent("0"));
274-
});
272+
await Promise.all(
273+
inputs.map(async (input) => {
274+
await waitFor(() => expect(input).toHaveTextContent("0"));
275+
})
276+
);
277+
});
278+
279+
test("should show placeholder if text is empty", async () => {
280+
renderOtpInput({ placeholder: "000000", hideStick: true });
281+
282+
const inputs = screen.getAllByTestId("otp-input");
283+
await Promise.all(
284+
inputs.map(async (input) => {
285+
await waitFor(() => expect(input).toHaveTextContent("0"));
286+
})
287+
);
288+
});
289+
290+
test("should show values for filled part", async () => {
291+
renderOtpInput({ placeholder: "000000", hideStick: true });
292+
const otp = "0124";
293+
294+
const hiddenInput = screen.getByTestId("otp-input-hidden");
295+
fireEvent.changeText(hiddenInput, otp);
296+
297+
const inputs = screen.getAllByTestId("otp-input");
298+
await Promise.all(
299+
inputs.map(async (input, index) => {
300+
await waitFor(() =>
301+
expect(input).toHaveTextContent(index < otp.length ? otp[index].toString() : "0")
302+
);
303+
})
304+
);
275305
});
276306

277307
test("should hide placeholder if text is not empty", () => {
@@ -286,7 +316,7 @@ describe("OtpInput", () => {
286316
});
287317

288318
test("should hide placeholder if input is focused", () => {
289-
renderOtpInput({ placeholder: "000000" });
319+
renderOtpInput({ placeholder: "000000", hideStick: true });
290320

291321
const input = screen.getByTestId("otp-input-hidden");
292322
fireEvent.press(input);
@@ -296,8 +326,8 @@ describe("OtpInput", () => {
296326
expect(placeholder).toBeFalsy();
297327
});
298328

299-
test("should show placeholder if input is blurred and text is empty", () => {
300-
renderOtpInputWithExtraInput({ placeholder: "000000" });
329+
test("should show placeholder if input is blurred and text is empty", async () => {
330+
renderOtpInputWithExtraInput({ placeholder: "000000", hideStick: true });
301331

302332
const input = screen.getByTestId("otp-input-hidden");
303333
const otherInput = screen.getByTestId("other-input");
@@ -306,13 +336,15 @@ describe("OtpInput", () => {
306336
fireEvent.press(otherInput);
307337

308338
const inputs = screen.getAllByTestId("otp-input");
309-
inputs.forEach((input) => {
310-
waitFor(() => expect(input).toHaveTextContent("0"));
311-
});
339+
await Promise.all(
340+
inputs.map(async (input) => {
341+
await waitFor(() => expect(input).toHaveTextContent("0"));
342+
})
343+
);
312344
});
313345

314346
test("should hide placeholder if input is blurred and text is not empty", () => {
315-
renderOtpInputWithExtraInput({ placeholder: "000000" });
347+
renderOtpInputWithExtraInput({ placeholder: "000000", hideStick: true });
316348

317349
const input = screen.getByTestId("otp-input-hidden");
318350
const otherInput = screen.getByTestId("other-input");
@@ -326,16 +358,16 @@ describe("OtpInput", () => {
326358
expect(placeholder).toBeFalsy();
327359
});
328360

329-
test('should leave empty spaces if "placeholder" is shorter than "numberOfDigits"', () => {
330-
renderOtpInput({ placeholder: "123" });
361+
test('should leave empty spaces if "placeholder" is shorter than "numberOfDigits"', async () => {
362+
renderOtpInput({ placeholder: "123", hideStick: true });
331363

332364
const inputs = screen.getAllByTestId("otp-input");
333-
waitFor(() => inputs[0].toHaveTextContent("1"));
334-
waitFor(() => expect(inputs[1]).toHaveTextContent("2"));
335-
waitFor(() => expect(inputs[2]).toHaveTextContent("3"));
336-
waitFor(() => expect(inputs[3]).toHaveTextContent(" "));
337-
waitFor(() => expect(inputs[4]).toHaveTextContent(" "));
338-
waitFor(() => expect(inputs[5]).toHaveTextContent(" "));
365+
expect(inputs[0]).toHaveTextContent("1");
366+
expect(inputs[1]).toHaveTextContent("2");
367+
expect(inputs[2]).toHaveTextContent("3");
368+
expect(inputs[3]).toHaveTextContent("");
369+
expect(inputs[4]).toHaveTextContent("");
370+
expect(inputs[5]).toHaveTextContent("");
339371
});
340372
});
341373
});

src/OtpInput/__tests__/__snapshots__/OtpInput.test.tsx.snap

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -142,9 +142,7 @@ exports[`OtpInput UI should render correctly 1`] = `
142142
"fontSize": 28,
143143
},
144144
undefined,
145-
{
146-
"opacity": 1,
147-
},
145+
{},
148146
]
149147
}
150148
/>
@@ -202,9 +200,7 @@ exports[`OtpInput UI should render correctly 1`] = `
202200
"fontSize": 28,
203201
},
204202
undefined,
205-
{
206-
"opacity": 1,
207-
},
203+
{},
208204
]
209205
}
210206
/>
@@ -262,9 +258,7 @@ exports[`OtpInput UI should render correctly 1`] = `
262258
"fontSize": 28,
263259
},
264260
undefined,
265-
{
266-
"opacity": 1,
267-
},
261+
{},
268262
]
269263
}
270264
/>
@@ -322,9 +316,7 @@ exports[`OtpInput UI should render correctly 1`] = `
322316
"fontSize": 28,
323317
},
324318
undefined,
325-
{
326-
"opacity": 1,
327-
},
319+
{},
328320
]
329321
}
330322
/>
@@ -382,9 +374,7 @@ exports[`OtpInput UI should render correctly 1`] = `
382374
"fontSize": 28,
383375
},
384376
undefined,
385-
{
386-
"opacity": 1,
387-
},
377+
{},
388378
]
389379
}
390380
/>

src/OtpInput/__tests__/useOtpInput.test.ts

Lines changed: 7 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { act, renderHook, waitFor } from "@testing-library/react-native";
1+
import { act, renderHook } from "@testing-library/react-native";
22
import * as React from "react";
33
import { Keyboard } from "react-native";
44
import { OtpInputProps } from "../OtpInput.types";
@@ -299,78 +299,14 @@ describe("useOtpInput", () => {
299299
});
300300

301301
describe("Placeholder", () => {
302-
test("should call setIsPlaceholderActive with `true`", () => {
303-
const mockSetState = jest.fn();
304-
jest.spyOn(React, "useState").mockImplementation(() => [false, mockSetState]);
305-
306-
renderUseOtInput({ placeholder: "00000000" });
307-
308-
waitFor(() => {
309-
expect(mockSetState).toBeCalledWith(true);
310-
});
311-
});
312-
313-
test("should set isPlaceholderActive to 'true' when placeholder is provided and input is not focused and text is empty", () => {
314-
const { result } = renderUseOtInput({ placeholder: "00000000" });
315-
316-
waitFor(() => {
317-
expect(result.current.models.isPlaceholderActive).toBe(true);
318-
});
319-
});
320-
321-
test("should set isPlaceholderActive to 'true' when placeholder is provided and text is empty", () => {
322-
const { result } = renderUseOtInput({ placeholder: "00000000" });
323-
result.current.actions.handleFocus();
324-
result.current.actions.handleBlur();
325-
326-
waitFor(() => {
327-
expect(result.current.models.isPlaceholderActive).toBe(true);
328-
});
329-
});
330-
331-
test("should set isPlaceholderActive to 'false' when placeholder is provided and input is focused", () => {
332-
const { result } = renderUseOtInput({ placeholder: "00000000" });
333-
result.current.actions.handleFocus();
334-
waitFor(() => {
335-
expect(result.current.models.isPlaceholderActive).toBe(false);
336-
});
337-
});
338-
339-
test("should set isPlaceholderActive to 'false' when placeholder is provided and text is not empty", () => {
340-
const { result } = renderUseOtInput({ placeholder: "00000000" });
341-
result.current.actions.handleTextChange("123456");
342-
waitFor(() => {
343-
expect(result.current.models.isPlaceholderActive).toBe(false);
344-
});
345-
});
346-
347-
test("should set isPlaceholderActive to 'false' when placeholder is provided and input is focused and text is not empty", async () => {
348-
const { result } = renderUseOtInput({ placeholder: "00000000" });
349-
result.current.actions.handleTextChange("123456");
350-
result.current.actions.handleFocus();
351-
waitFor(() => expect(result.current.models.isPlaceholderActive).toBe(false));
352-
});
353-
354-
test("should set isPlaceholderActive to 'false' when placeholder is provided and input is not focused and text is not empty", async () => {
355-
const { result } = renderUseOtInput({ placeholder: "00000000" });
356-
result.current.actions.handleTextChange("123456");
357-
result.current.actions.handleBlur();
358-
waitFor(() => expect(result.current.models.isPlaceholderActive).toBe(false));
359-
});
360-
361-
test("should set isPlaceholderActive to 'true' when placeholder is provided and input is focused and text is empty", async () => {
362-
const { result } = renderUseOtInput({ placeholder: "00000000" });
363-
result.current.actions.handleFocus();
364-
result.current.actions.handleBlur();
365-
waitFor(() => expect(result.current.models.isPlaceholderActive).toBe(true));
302+
test("should be populated to numberOfDigits if has only single char", () => {
303+
const { result } = renderUseOtInput({ placeholder: "2", numberOfDigits: 5 });
304+
expect(result.current.models.placeholder).toBe("22222");
366305
});
367306

368-
test("should set isPlaceholderActive to 'true' when placeholder is provided and input is not focused and text is empty", async () => {
369-
const { result } = renderUseOtInput({ placeholder: "00000000" });
370-
result.current.actions.handleTextChange("123456");
371-
result.current.actions.handleTextChange("");
372-
result.current.actions.handleBlur();
373-
waitFor(() => expect(result.current.models.isPlaceholderActive).toBe(true));
307+
test("should not be populated if more than one", () => {
308+
const { result } = renderUseOtInput({ placeholder: "22", numberOfDigits: 3 });
309+
expect(result.current.models.placeholder).toBe("22");
374310
});
375311
});
376312
});

src/OtpInput/useOtpInput.ts

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useRef, useState } from "react";
1+
import { useMemo, useRef, useState } from "react";
22
import { Keyboard, TextInput } from "react-native";
33
import { OtpInputProps } from "./OtpInput.types";
44

@@ -18,21 +18,16 @@ export const useOtpInput = ({
1818
type,
1919
onFocus,
2020
onBlur,
21-
placeholder,
21+
placeholder: _placeholder,
2222
}: OtpInputProps) => {
2323
const [text, setText] = useState("");
24-
const [isPlaceholderActive, setIsPlaceholderActive] = useState(!!placeholder && !text);
2524
const [isFocused, setIsFocused] = useState(autoFocus);
2625
const inputRef = useRef<TextInput>(null);
2726
const focusedInputIndex = text.length;
28-
29-
useEffect(() => {
30-
if (placeholder && !isFocused && !text) {
31-
setIsPlaceholderActive(true);
32-
} else {
33-
setIsPlaceholderActive(false);
34-
}
35-
}, [placeholder, isFocused, text]);
27+
const placeholder = useMemo(
28+
() => (_placeholder?.length === 1 ? _placeholder.repeat(numberOfDigits) : _placeholder),
29+
[_placeholder, numberOfDigits]
30+
);
3631

3732
const handlePress = () => {
3833
// To fix bug when keyboard is not popping up after being dismissed
@@ -77,7 +72,7 @@ export const useOtpInput = ({
7772
};
7873

7974
return {
80-
models: { text, inputRef, focusedInputIndex, isFocused, isPlaceholderActive },
75+
models: { text, inputRef, focusedInputIndex, isFocused, placeholder },
8176
actions: { handlePress, handleTextChange, clear, focus, handleFocus, handleBlur },
8277
forms: { setText, setTextWithRef },
8378
};

0 commit comments

Comments
 (0)