Skip to content

Commit

Permalink
Merge pull request #12 from keyurparalkar/release
Browse files Browse the repository at this point in the history
high priorty bug fixes
  • Loading branch information
keyurparalkar authored Oct 7, 2023
2 parents 9f2f1dc + a69004e commit d10701c
Show file tree
Hide file tree
Showing 10 changed files with 4,114 additions and 256 deletions.
11 changes: 11 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
name: check-unit-tests
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install modules
run: yarn
- name: Run tests
run: yarn test
27 changes: 27 additions & 0 deletions .versionrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{

"types": [

{ "type": "feat", "section": "Features" },

{ "type": "fix", "section": "Bug Fixes" },

{ "type": "chore", "hidden": true },

{ "type": "docs", "hidden": true },

{ "type": "style", "hidden": true },

{ "type": "refactor", "hidden": true },

{ "type": "perf", "hidden": true },

{ "type": "test", "hidden": true }

],

"commitUrlFormat": "https://github.com/keyurparalkar/react-headless-passcode/commits/{{hash}}",

"compareUrlFormat": "https://github.com/keyurparalkar/react-headless-passcode/compare/{{previousTag}}...{{currentTag}}"

}
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).
5 changes: 5 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: "ts-jest",
testEnvironment: "jsdom",
};
19 changes: 16 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-headless-passcode",
"version": "0.1.4",
"version": "0.1.6",
"description": "A headless UI for building easy to use passcode component",
"keywords": [
"passcode",
Expand Down Expand Up @@ -28,22 +28,35 @@
"build"
],
"scripts": {
"build": "rollup -c"
"build": "rollup -c",
"test": "jest",
"release": "standard-version",
"release:minor": "standard-version --release-as minor",
"release:patch": "standard-version --release-as patch",
"release:major": "standard-version --release-as major"
},
"sideEffects": false,
"author": "Keyur Paralkar",
"license": "MIT",
"devDependencies": {
"@rollup/plugin-node-resolve": "^13.3.0",
"@testing-library/jest-dom": "^6.1.3",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.5.1",
"@types/jest": "^29.5.5",
"@types/react": "^18.0.12",
"@types/react-dom": "^18.0.5",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"react": "^18.1.0",
"react-dom": "^18.1.0",
"rollup": "^2.75.6",
"rollup-plugin-peer-deps-external": "^2.2.4",
"rollup-plugin-terser": "^7.0.2",
"rollup-plugin-typescript2": "^0.31.2",
"typescript": "^4.6.4"
"standard-version": "^9.5.0",
"ts-jest": "^29.1.1",
"typescript": "^5.2.2"
},
"peerDependencies": {
"react": ">=16.8.0",
Expand Down
72 changes: 72 additions & 0 deletions package/lib/hook/usePasscode.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import "@testing-library/jest-dom";
import { render, renderHook, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import usePasscode from "./usePasscode";

const TestComponent = (props: { isAlphaNumeric: boolean }) => {
const { passcode, getEventHandlers, refs } = usePasscode({
count: 4,
isAlphaNumeric: props.isAlphaNumeric,
});

return (
<>
{passcode.map((value: string | number, index: number) => (
<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}`}
data-testid={`index-${index}`}
{...getEventHandlers(index)}
/>
))}
</>
);
};

describe("test basic workflow", () => {
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 () => {
render(<TestComponent isAlphaNumeric={false} />);
// focus on the first input
const firstInput: HTMLInputElement = screen.getByTestId("index-0");
firstInput.focus();
expect(firstInput).toHaveFocus();

//Type in first input and check the focus of next input
userEvent.type(firstInput, "1");
const secondtInput: HTMLInputElement = screen.getByTestId("index-1");
await waitFor(() => {
expect(secondtInput).toHaveFocus();
});
});

it("3. test if the focus changes to previous element when backspaced", async () => {
render(<TestComponent isAlphaNumeric={false} />);
// focus on the first input
const firstInput: HTMLInputElement = screen.getByTestId("index-0");
firstInput.focus();

//Type in first input and check the focus of next input
userEvent.type(firstInput, "1");
const secondtInput: HTMLInputElement = screen.getByTestId("index-1");
await waitFor(() => {
expect(secondtInput).toHaveFocus();
});

//Backspace and observe focus shift
userEvent.keyboard("{Backspace}");
await waitFor(() => {
expect(firstInput).toHaveFocus();
});
});
});
Loading

0 comments on commit d10701c

Please sign in to comment.