Skip to content

Commit

Permalink
Merge pull request #11 from keyurparalkar/chore/chore-updates-and-bug…
Browse files Browse the repository at this point in the history
…-fixes

Chore/chore updates and bug fixes
  • Loading branch information
keyurparalkar authored Oct 7, 2023
2 parents 0166ee5 + 19abf29 commit df75131
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 133 deletions.
109 changes: 55 additions & 54 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,95 +4,96 @@

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

```shell
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 (
<input
className="single-input"
ref={(el) => 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 (
<input
className="single-input"
ref={(el) => 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<HTMLInputElement[] \| []>` | 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<HTMLInputElement[] \| []>` | 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).

React is [MIT licensed](./LICENSE).
10 changes: 5 additions & 5 deletions package/lib/hook/usePasscode.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) => (
<input
ref={(el) => el && (refs.current[index] = el)}
type="text"
Expand All @@ -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(<TestComponent isAlphaNumeric={false} />);
expect(screen.getAllByTestId(/index-[0-9]/)).toHaveLength(4);
});

it("2. test if the focus changes to next element when typed", async () => {
Expand Down
66 changes: 30 additions & 36 deletions package/lib/hook/usePasscode.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {
BaseSyntheticEvent,
KeyboardEvent,
useEffect,
useRef,
useState,
useMemo,
Expand All @@ -22,19 +21,21 @@ 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<Array<HTMLInputElement> | []>([]);

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.
*/
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)) {
Expand All @@ -49,7 +50,6 @@ const usePasscode = (props: PasscodeProps) => {

const onFocus = (e: BaseSyntheticEvent) => {
setCurrentFocusedIndex(index);
e.target.focus();
};

const onKeyUp = (e: KeyboardEvent<HTMLInputElement>) => {
Expand All @@ -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 (
Expand All @@ -92,29 +92,20 @@ const usePasscode = (props: PasscodeProps) => {

// Preventing typing of any other keys except for 1 to 9 And backspace
const onKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
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.");
}

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<string | number> =
clipboardContent.split("");
newArray = isAlphaNumeric
Expand All @@ -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,
Expand Down
Loading

0 comments on commit df75131

Please sign in to comment.