diff --git a/README.md b/README.md
index 238e29b..5379543 100644
--- a/README.md
+++ b/README.md
@@ -4,14 +4,14 @@
A headless UI for building easy to use passcode component.
-*What is an passcode component?*
+_What is an passcode component?_
It is a group of input elements with each element only accepting one character. This component is generally used in authentication flows.
-* [Installation](#installation)
-* [Usage](#usage)
-* [Features](#features)
-* [API](#api)
-* [License](#license)
+- [Installation](#installation)
+- [Usage](#usage)
+- [Features](#features)
+- [API](#api)
+- [License](#license)
## Installation
@@ -19,80 +19,81 @@ It is a group of input elements with each element only accepting one character.
yarn add react-headless-passcode
```
-
## Usage
```tsx
import { usePasscode } from "react-headless-passcode";
```
-With the `usePasscode` hook you just need to pass the `arrayValue` default property and in return you get the `array` in which the actual passcode value is stored, various event hanlders that handles the focus management between multiple inputs and `refs` that references each input element.
+With the `usePasscode` hook you just need to pass the `count` property and in return you get the `array` in which the actual passcode value is stored, various event hanlders that handles the focus management logic between multiple inputs and `refs` that references each input element.
For example:
```tsx
const PasscodeComponent = () => {
- const { array, getEventHandlers, refs } = usePasscode({
- arrayValue: [0, 0, 0, 0, 0, 0],
- });
-
- return (
- <>
- {array.map((value, index) => {
- const { ...rest } = getEventHandlers(index);
- return (
- el && (refs.current[index] = el)}
- type="text"
- inputMode="numeric"
- autoComplete="one-time-code"
- maxLength={1}
- pattern="\d{1}"
- value={String(value)}
- key={`index-${index}`}
- {...rest}
- />
- );
- })}
- >
- );
+ const { array, getEventHandlers, refs } = usePasscode({
+ count: 4,
+ });
+
+ return (
+ <>
+ {array.map((value, index) => {
+ const { ...rest } = getEventHandlers(index);
+ return (
+ el && (refs.current[index] = el)}
+ type="text"
+ inputMode="numeric"
+ autoComplete="one-time-code"
+ maxLength={1}
+ pattern="\d{1}"
+ value={String(value)}
+ key={`index-${index}`}
+ {...rest}
+ />
+ );
+ })}
+ >
+ );
};
-
```
->**NOTE:**
+> **NOTE:**
> It is important to initialize the `refs` object with the current input element because this is how the `usepasscode` is able to track the current index and manage the focused state across multiple inputs. Make sure to assign this element to the `refs` or else the focus won't change!!
+
```tsx
ref={(el) => el && (refs.current[index] = el)}
```
## Features
-- Allow entering alpha numeric characters
-- Expose a flag: `isComplete` that tells whether all the input boxes are filled or not
-- Expose a state variable: `currentFocusedIndex`. It tells us the currently focused index of the passcode component.
-- Exposes event handlers that can be seamlessly used with the input element.
-- Passcode value can be pasted partially, fully, from start, or from middle.
+
+- Allow entering alpha numeric characters
+- Expose a flag: `isComplete` that tells whether all the input boxes are filled or not
+- Expose a state variable: `currentFocusedIndex`. It tells us the currently focused index of the passcode component.
+- Exposes event handlers that can be seamlessly used with the input element.
+- Passcode value can be pasted partially, fully, from start, or from middle.
## API
The `usePasscode` hook accepts following props
-| Prop Name | Type | Description |
-|---------------- |------------------------ |----------------------------------------------------------------------- |
-| arrayValue | `(number \| string)[]` | Default array value that helps to determine the size of the component |
-| isAlphaNumeric | `boolean` | If `true`, allows to enter alpha numeric value in the component |
+| Prop Name | Type | Description |
+|---------------- |------------------------ |----------------------------------------------------------------------- |
+| count | `number` | Number of input boxes to create in the passcode component |
+| isAlphaNumeric | `boolean` | If `true`, allows to enter alpha numeric value in the component |
The hook returns an object that consists of:
-| Property | Type | Description |
-|------------------------ |------------------------ |------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
-| array | `(string \| number)[]` | The current array value of the entire component. |
-| setArray | `function` | A function that sets the internal state variable:`array`'s value inside the hook. |
-| currentFocusedIndex | `number` | Index of the currently focused input element. |
-| setCurrentFocusedIndex | `function` | A function that sets the internal state variable: `currentFocusedIndex`'s value inside the hook. |
-| getEventHandler | `function` | A function that accepts an index as a parameter. It returns the following event handlers for the input positioned at index `i`: `onChange` `onFocus` `onKeyUp` `onKeyDown` |
-| refs | `React.MutableRefObject` | A ref array that contains reference of all the input boxes. |
-| isComplete | `boolean` | A boolean flag that tells if all the input boxes are filled or not. |
+| Property | Type | Description |
+| ---------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| passcode | `(string \| number)[]` | The current array value of the entire component. |
+| setPasscode | `function` | A function that sets the internal state variable:`passcode`'s value inside the hook. |
+| currentFocusedIndex | `number` | Index of the currently focused input element. |
+| setCurrentFocusedIndex | `function` | A function that sets the internal state variable: `currentFocusedIndex`'s value inside the hook. |
+| getEventHandler | `function` | A function that accepts an index as a parameter. It returns the following event handlers for the input positioned at index `i`: `onChange` `onFocus` `onKeyUp` `onKeyDown` `onPaste` |
+| refs | `React.MutableRefObject` | A ref array that contains reference of all the input boxes. |
+| isComplete | `boolean` | A boolean flag that tells if all the input boxes are filled or not. |
## License
-React is [MIT licensed](./LICENSE).
\ No newline at end of file
+
+React is [MIT licensed](./LICENSE).
diff --git a/package/lib/hook/usePasscode.test.tsx b/package/lib/hook/usePasscode.test.tsx
index 868f0bd..7bafe19 100644
--- a/package/lib/hook/usePasscode.test.tsx
+++ b/package/lib/hook/usePasscode.test.tsx
@@ -4,14 +4,14 @@ import userEvent from "@testing-library/user-event";
import usePasscode from "./usePasscode";
const TestComponent = (props: { isAlphaNumeric: boolean }) => {
- const { array, getEventHandlers, refs } = usePasscode({
+ const { passcode, getEventHandlers, refs } = usePasscode({
count: 4,
isAlphaNumeric: props.isAlphaNumeric,
});
return (
<>
- {array.map((value: string | number, index: number) => (
+ {passcode.map((value: string | number, index: number) => (
el && (refs.current[index] = el)}
type="text"
@@ -30,9 +30,9 @@ const TestComponent = (props: { isAlphaNumeric: boolean }) => {
};
describe("test basic workflow", () => {
- it("1. test whether passing no. of inputs creates an array of equal number ", () => {
- const { result } = renderHook(() => usePasscode({ count: 4 }));
- expect(result.current.array).toHaveLength(4);
+ it("1. test whether passing count prop creates an array(input elements) with size count", () => {
+ render();
+ expect(screen.getAllByTestId(/index-[0-9]/)).toHaveLength(4);
});
it("2. test if the focus changes to next element when typed", async () => {
diff --git a/package/lib/hook/usePasscode.ts b/package/lib/hook/usePasscode.ts
index ce1f775..869982c 100644
--- a/package/lib/hook/usePasscode.ts
+++ b/package/lib/hook/usePasscode.ts
@@ -1,7 +1,6 @@
import {
BaseSyntheticEvent,
KeyboardEvent,
- useEffect,
useRef,
useState,
useMemo,
@@ -22,11 +21,13 @@ type PasscodeProps = {
const usePasscode = (props: PasscodeProps) => {
const { count, isAlphaNumeric = false } = props;
const filledArray = useMemo(() => Array(count).fill("", 0, count), [count]);
- const [array, setArray] = useState(filledArray);
+ const [passcode, setPasscode] = useState(filledArray);
const [currentFocusedIndex, setCurrentFocusedIndex] = useState(0);
const inputRefs = useRef | []>([]);
- const isComplete = array?.every((value: string | number) => value !== "");
+ const isComplete = passcode?.every(
+ (value: string | number) => value !== ""
+ );
/**
* A function that returns the necessary event handlers based on index.
@@ -34,7 +35,7 @@ const usePasscode = (props: PasscodeProps) => {
const getEventHandlers = (index: number) => {
const onChange = (e: BaseSyntheticEvent) => {
// Change the arrayValue and update only when number key is pressed
- setArray((preValue: (string | number)[]) => {
+ setPasscode((preValue: (string | number)[]) => {
const newArray = [...preValue];
if (parseInt(e.target.value)) {
@@ -49,7 +50,6 @@ const usePasscode = (props: PasscodeProps) => {
const onFocus = (e: BaseSyntheticEvent) => {
setCurrentFocusedIndex(index);
- e.target.focus();
};
const onKeyUp = (e: KeyboardEvent) => {
@@ -70,13 +70,13 @@ const usePasscode = (props: PasscodeProps) => {
/**
* Update focus only when number key is pressed
* We do a -2 below because we don't want the last input to update the currentFocusedIndex
- * If we allow it then we get array out of bound error.
+ * If we allow it then we get passcode out of bound error.
* */
if (
(isAlphaNumeric
? ALPHANUMERIC_REGEX.test(e.key)
: parseInt(e.key)) &&
- index <= array.length - 2
+ index <= passcode.length - 2
) {
setCurrentFocusedIndex(index + 1);
if (
@@ -92,21 +92,12 @@ const usePasscode = (props: PasscodeProps) => {
// Preventing typing of any other keys except for 1 to 9 And backspace
const onKeyDown = (e: KeyboardEvent) => {
- if (shouldPreventDefault(e.which, isAlphaNumeric, e.metaKey)) {
+ if (shouldPreventDefault(e.key, isAlphaNumeric, e.metaKey)) {
e.preventDefault();
}
};
- return {
- onKeyUp,
- onKeyDown,
- onFocus,
- onChange,
- };
- };
-
- useEffect(() => {
- document.addEventListener("paste", async () => {
+ const onPaste = async (e: BaseSyntheticEvent) => {
const copyPermission = await getClipboardReadPermission();
if (copyPermission.state === "denied") {
throw new Error("Not allowed to read clipboard.");
@@ -114,7 +105,7 @@ const usePasscode = (props: PasscodeProps) => {
const clipboardContent = await getClipboardContent();
try {
- // We convert the clipboard conent into an array of string or number depending upon isAlphaNumeric;
+ // We convert the clipboard conent into an passcode of string or number depending upon isAlphaNumeric;
let newArray: Array =
clipboardContent.split("");
newArray = isAlphaNumeric
@@ -125,38 +116,41 @@ const usePasscode = (props: PasscodeProps) => {
* Pasting of this content is stopped when the last input is reached.
**/
const filledArray = getFilledArray(
- array,
+ passcode,
newArray,
currentFocusedIndex
);
- setArray(filledArray);
- // Below we update the current focused index and also focus to the last input
+ setPasscode(filledArray);
+
+ const newFocusedIndex = currentFocusedIndex + newArray.length;
if (
- newArray.length < array.length &&
- currentFocusedIndex === 0
+ newFocusedIndex >= 0 &&
+ newFocusedIndex < passcode.length - 1
) {
- setCurrentFocusedIndex(newArray.length - 1);
- inputRefs.current[newArray.length - 1].focus();
+ setCurrentFocusedIndex(newFocusedIndex);
+ inputRefs.current[newFocusedIndex].focus();
} else {
- setCurrentFocusedIndex(array.length - 1);
- inputRefs.current[array.length - 1].focus();
+ setCurrentFocusedIndex(passcode.length - 1);
+ inputRefs.current[passcode.length - 1].focus();
}
} catch (err) {
console.error(err);
}
- });
+ };
- return () => {
- document.removeEventListener("paste", () =>
- console.log("Removed paste listner")
- );
+ return {
+ onKeyUp,
+ onKeyDown,
+ onFocus,
+ onChange,
+ onPaste,
};
- }, [currentFocusedIndex, array, isAlphaNumeric]);
+ };
return {
- array,
- setArray,
+ passcode,
+ setPasscode,
currentFocusedIndex,
setCurrentFocusedIndex,
getEventHandlers,
diff --git a/package/lib/utils/index.ts b/package/lib/utils/index.ts
index cf1cc93..17c5698 100644
--- a/package/lib/utils/index.ts
+++ b/package/lib/utils/index.ts
@@ -1,43 +1,37 @@
export const ALPHANUMERIC_REGEX = /^[a-z0-9]$/i;
export const shouldPreventDefault = (
- keyCode: number,
- isAlphaNumeric: boolean = false,
- isMeta: boolean = false
+ key: string,
+ isAlphaNumeric: boolean = false,
+ isMeta: boolean = false
) => {
- const isAlphabet = keyCode >= 64 && keyCode <= 90;
+ const parsed = Number(key);
- // Below flag also checks if the typeed key is from numpad
- const isNumeric =
- (keyCode >= 48 && keyCode <= 57) || (keyCode >= 96 && keyCode <= 105);
+ // By default we only allow numbers to be pressed = DONE
+ if (parsed) return false;
- //Crtl + v:
- if (isMeta && keyCode === 86) {
- return false;
- }
+ // Crtl + V
+ if (isMeta && key === "v") return false;
- // By default we only allow numbers to be pressed
- if (isNumeric) return false;
+ // Allow Backspace
+ if (key === "Backspace") return false;
- // We only allow alphabets to be pressed when the isAplhaNumeric flag is true
- if (isAlphabet && isAlphaNumeric) return false;
-
- // Backspace
- if (keyCode === 8) {
- return false;
- }
+ // We only allow alphabets to be pressed when the isAplhaNumeric flag is true = DONE
+ if (isAlphaNumeric && isNaN(parsed)) {
+ return false;
+ }
- return true;
+ return true;
};
export const getClipboardReadPermission = () => {
- return navigator.permissions.query({
- name: "clipboard-read" as PermissionName,
- });
+ return navigator.permissions.query({
+ name: "clipboard-read" as PermissionName,
+ });
};
export const getClipboardContent = () => {
- return navigator.clipboard.readText();
+ return navigator.clipboard.readText();
};
/**
@@ -48,19 +42,19 @@ export const getClipboardContent = () => {
* The array before the focused index will be filled with existing values.
*/
export const getFilledArray = (
- arr: (number | string)[],
- pastingArr: (number | string)[],
- currentFocusedIndex: number
+ arr: (number | string)[],
+ pastingArr: (number | string)[],
+ currentFocusedIndex: number
) => {
- const lastIndex = arr.length - 1;
-
- if (currentFocusedIndex > 0) {
- for (let i = currentFocusedIndex; i <= lastIndex; i++) {
- arr[i] = pastingArr[i - currentFocusedIndex] ?? "";
+ const lastIndex = arr.length - 1;
+
+ if (currentFocusedIndex > 0) {
+ for (let i = currentFocusedIndex; i <= lastIndex; i++) {
+ arr[i] = pastingArr[i - currentFocusedIndex] ?? "";
+ }
+ return arr;
+ } else {
+ // Starts pasting the values in the array from 0th index
+ return [...pastingArr, ...arr.slice(pastingArr.length - 1, lastIndex)];
}
- return arr;
- } else {
- // Starts pasting the values in the array from 0th index
- return [...pastingArr, ...arr.slice(pastingArr.length - 1, lastIndex)];
- }
};
diff --git a/package/lib/utils/utils.test.ts b/package/lib/utils/utils.test.ts
new file mode 100644
index 0000000..acc589f
--- /dev/null
+++ b/package/lib/utils/utils.test.ts
@@ -0,0 +1,31 @@
+import { shouldPreventDefault } from "./index";
+
+describe("test shouldPreventDefault", () => {
+ test("1. Should return false if key that is pressed is: Ctrl + V, Backspace, and digits 1-9 when isAlphaNumeric = false", () => {
+ let key = "1";
+ expect(shouldPreventDefault(key)).toBeFalsy();
+
+ key = "v";
+ expect(shouldPreventDefault(key, false, true)).toBeFalsy();
+
+ key = "Backspace";
+ expect(shouldPreventDefault(key)).toBeFalsy();
+
+ key = "a";
+ expect(shouldPreventDefault(key)).toBeTruthy();
+ });
+
+ test("2. Should return false if key that is pressed is: Ctrl + V, Backspace, and digits 1-9 when isAlphaNumeric = true", () => {
+ let key = "1";
+ expect(shouldPreventDefault(key, true)).toBeFalsy();
+
+ key = "v"; // Ctrl + v
+ expect(shouldPreventDefault(key, true, true)).toBeFalsy();
+
+ key = "Backspace";
+ expect(shouldPreventDefault(key, true)).toBeFalsy();
+
+ key = "a";
+ expect(shouldPreventDefault(key, true)).toBeFalsy();
+ });
+});