Skip to content

Commit

Permalink
Merge pull request #148 from kosori/feat/use-step-hook
Browse files Browse the repository at this point in the history
  • Loading branch information
codingcodax authored Sep 28, 2024
2 parents 6c8e082 + bde0ad8 commit ad59021
Show file tree
Hide file tree
Showing 5 changed files with 259 additions and 0 deletions.
79 changes: 79 additions & 0 deletions apps/www/content/docs/hooks/use-step.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
---
title: useStep
description: Copy text to the clipboard
---

<ComponentPreview name='use-step'>
```json doc-gen:file
{
"file": "./src/components/demos/UseStep/UseStep.tsx",
"codeblock": true
}
```
</ComponentPreview>

## Installation

<Steps>

<Step>
### Copy-paste the hook

Copy and paste the hook code in a `.ts` file.

```json doc-gen:file
{
"file": "./src/hooks/use-step.ts",
"codeblock": true
}
```

</Step>

</Steps>

### Usage

```ts
import { useStep } from '~/hooks/use-step';
```

```tsx
const { currentStep, setStep } = useStep(4);

<p>{currentStep}</p>;
```

## Details

### Step Navigation

- The `useStep` hook provides a straightforward way to manage multi-step navigation in applications, such as wizards or forms. It allows users to move between steps easily while enforcing boundaries based on the maximum step defined.
- The hook ensures that users cannot navigate beyond the defined steps, preventing errors and enhancing user experience.

### State Management

- **`currentStep`**: A state variable that holds the current step number, initialized to `1`. This allows components to easily access the current step and render appropriate content based on it.
- **`canGoToNextStep`**: A boolean that indicates whether the user can proceed to the next step. This is useful for enabling or disabling navigation buttons.
- **`canGoToPrevStep`**: A boolean that indicates whether the user can go back to the previous step, providing similar functionality for backward navigation.

### Step Control Functions

- **`goToNextStep`**: A function that increments the `currentStep` by `1` if the next step is available. This function can be called directly from UI elements like buttons.
- **`goToPrevStep`**: A function that decrements the `currentStep` by `1` if the previous step exists, allowing users to navigate backward through the steps.
- **`setStep`**: A versatile function that allows setting the current step either directly with a number or by providing a function that computes the new step based on the previous one. This flexibility mimics the API of `useState`.

### Reset Functionality

- **`reset`**: A function that resets the `currentStep` back to `1`. This is particularly useful for scenarios where the user might want to restart the process or when the form is submitted successfully.

### Return Value

- The hook returns an object containing:
- **`currentStep`**: The current step number, which can be used to render step-specific content.
- **`goToNextStep`**: A function to navigate to the next step.
- **`goToPrevStep`**: A function to navigate to the previous step.
- **`canGoToNextStep`**: A boolean indicating if the next step is available.
- **`canGoToPrevStep`**: A boolean indicating if the previous step is available.
- **`setStep`**: A function to set the current step directly or via a function.
- **`reset`**: A function to reset the step to the initial state.
9 changes: 9 additions & 0 deletions apps/www/src/components/ComponentPreview/Components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1213,6 +1213,15 @@ export const Components: Record<string, Component> = {
})),
),
},
'use-step': {
name: 'use-step',
type: 'hook:example',
component: lazy(() =>
import('../demos/UseStep').then((module) => ({
default: module.UseStepDemo,
})),
),
},
'use-toggle': {
name: 'use-toggle',
type: 'hook:example',
Expand Down
99 changes: 99 additions & 0 deletions apps/www/src/components/demos/UseStep/UseStep.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
'use client';

import {
ChevronLeftIcon,
ChevronRightIcon,
DoubleArrowLeftIcon,
DoubleArrowRightIcon,
} from '@radix-ui/react-icons';

import { Button } from '@kosori/ui/button';

import { useStep } from '~/hooks/use-step';

export const UseStepDemo = () => {
const {
currentStep,
canGoToPrevStep,
canGoToNextStep,
setStep,
goToPrevStep,
goToNextStep,
} = useStep(4);

return (
<div className='flex flex-col items-center justify-center gap-4'>
<p className='font-mono'>Current step: {currentStep}</p>

<div className='flex gap-1'>
<Button
disabled={!canGoToPrevStep}
icon

Check warning on line 31 in apps/www/src/components/demos/UseStep/UseStep.tsx

View workflow job for this annotation

GitHub Actions / lint

Shorthand props must be listed before all other props
variant='ghost'
onClick={() => setStep(1)}
>
<DoubleArrowLeftIcon />
</Button>

<Button
disabled={!canGoToPrevStep}
icon

Check warning on line 40 in apps/www/src/components/demos/UseStep/UseStep.tsx

View workflow job for this annotation

GitHub Actions / lint

Shorthand props must be listed before all other props
variant='ghost'
onClick={goToPrevStep}
>
<ChevronLeftIcon />
</Button>

<Button
icon
variant={currentStep === 1 ? 'outline' : 'ghost'}
onClick={() => setStep(1)}
>
1
</Button>

<Button
icon
variant={currentStep === 2 ? 'outline' : 'ghost'}
onClick={() => setStep(2)}
>
2
</Button>

<Button
icon
variant={currentStep === 3 ? 'outline' : 'ghost'}
onClick={() => setStep(3)}
>
3
</Button>

<Button
icon
variant={currentStep === 4 ? 'outline' : 'ghost'}
onClick={() => setStep(4)}
>
4
</Button>

<Button
disabled={!canGoToNextStep}
onClick={goToNextStep}

Check warning on line 81 in apps/www/src/components/demos/UseStep/UseStep.tsx

View workflow job for this annotation

GitHub Actions / lint

Callbacks must be listed after all other props
icon
variant='ghost'
>
<ChevronRightIcon />
</Button>

<Button
disabled={!canGoToNextStep}
onClick={() => setStep(4)}

Check warning on line 90 in apps/www/src/components/demos/UseStep/UseStep.tsx

View workflow job for this annotation

GitHub Actions / lint

Callbacks must be listed after all other props
icon
variant='ghost'
>
<DoubleArrowRightIcon />
</Button>
</div>
</div>
);
};
1 change: 1 addition & 0 deletions apps/www/src/components/demos/UseStep/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './UseStep';
71 changes: 71 additions & 0 deletions apps/www/src/hooks/use-step.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { useCallback, useState } from 'react';

/**
* A custom hook that manages multi-step navigation in applications.
*
* @param {number} [maxStep] - The maximum number of steps in the navigation process.
*
* @returns An object containing the following properties:
* - `currentStep`: The current step number, initialized to 1.
* - `goToNextStep`: A function that increments the current step by 1 if the next step is available.
* - `goToPrevStep`: A function that decrements the current step by 1 if the previous step exists.
* - `canGoToNextStep`: A boolean indicating whether the user can proceed to the next step.
* - `canGoToPrevStep`: A boolean indicating whether the user can go back to the previous step.
* - `setStep`: A function that sets the current step either directly with a number or via a function that computes the new step based on the previous one.
* - `reset`: A function that resets the current step back to 1.
*
* @example
* const { currentStep, goToNextStep, setStep, reset } = useStep(5);
*
* <p>{currentStep}</p>
* <button onClick={goToNextStep}>Next</button>
* <button onClick={() => setStep(currentStep + 2)}>Set Step to 3</button>
* <button onClick={reset}>Reset</button>
*/
export const useStep = (maxStep: number) => {
const [currentStep, setCurrentStep] = useState(1);

const canGoToNextStep = currentStep + 1 <= maxStep;
const canGoToPrevStep = currentStep - 1 > 0;

const setStep = useCallback(
(step: number | ((step: number) => number)) => {
// Allow value to be a function so we have the same API as useState
const newStep = step instanceof Function ? step(currentStep) : step;

if (newStep >= 1 && newStep <= maxStep) {
setCurrentStep(newStep);
return;
}

throw new Error('Step not valid');
},
[maxStep, currentStep],
);

const goToNextStep = useCallback(() => {
if (canGoToNextStep) {
setCurrentStep((step) => step + 1);
}
}, [canGoToNextStep]);

const goToPrevStep = useCallback(() => {
if (canGoToPrevStep) {
setCurrentStep((step) => step - 1);
}
}, [canGoToPrevStep]);

const reset = useCallback(() => {
setCurrentStep(1);
}, []);

return {
currentStep,
goToNextStep,
goToPrevStep,
canGoToNextStep,
canGoToPrevStep,
setStep,
reset,
};
};

0 comments on commit ad59021

Please sign in to comment.