Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(hooks): create use step #148

Merged
merged 5 commits into from
Sep 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
};
};