stories/molecules/components/stepper/Stepper.stories.mdx
import { ArgsTable, Meta, Story, Canvas } from '@storybook/addon-docs'
import { Stepper } from 'ui/molecules/stepper/Stepper'
import { Typography } from 'ui/atoms/typography/Typography'
import { Button } from 'ui/atoms/button/Button'
import { Card } from 'ui/atoms/card/Card'
import { Toast } from 'ui/atoms/toast/Toast'
import { ThemeProvider } from 'ui/atoms/theme/ThemeProvider'
import { ThemeSwitcher } from 'ui/atoms/theme/ThemeSwitcher'
import { useCallback, useMemo, useState } from 'react'
import { UseStepper, useStepper } from 'hook/stepper/useStepper'
import { getUpdatedSteps, getActiveStepId } from 'ui/molecules/stepper/selector/stepper.selector'
import './stepper.scss'
<Meta
title="Molecules/Stepper"
component={Stepper}
parameters={{
actions: { argTypesRegex: '^on.*' },
docs: {
source: {
type: 'code'
}
}
}}
decorators={[
Story => (
<ThemeProvider>
<div className="story-theme-switcher">
<ThemeSwitcher />
</div>
<div className="component-story-main">
<Story />
</div>
</ThemeProvider>
)
]}
/>
# Stepper
> Allows to navigate and follow the progress between different ordered steps.
[![stability-unstable](https://img.shields.io/badge/stability-unstable-yellow.svg)](https://github.com/emersion/stability-badges#unstable)
## Description
The Stepper allows a user to follow a progression through different ordered steps.
Each step of the Stepper can be validated to move on to the next step. The last step also provides the possibility to perform an additional action upon validation. Once all steps are completed, the user can reset the stepper.
## Overview
<Story
name="Overview"
argTypes={{
steps: {
type: {
required: true
},
control: false
},
activeStepId: { control: false },
onPrevious: { control: false },
onNext: { control: false },
onSubmit: { control: false },
onReset: { control: false }
}}
args={{
isSubmitDisabled: false,
submitButtonLabel: 'Submit',
resetButtonLabel: 'Reset'
}}
>
{args => {
const steps = [
{
id: 'TMPd6bsSkUiTauLbHa0aWw',
label: 'Step 1',
content: (
<>
<Typography as="div" fontFamily="brand" fontWeight="xlight">
The content of the step 1
</Typography>
<Typography as="div" fontFamily="brand" fontSize="small" fontWeight="xlight">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud
exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure
dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt
mollit anim id est laborum.
</Typography>
</>
)
},
{
id: 'Z12X3iypAU2HIuoxhzKoTw',
label: 'Step 2',
content: (
<>
<Typography as="div" fontFamily="brand" fontWeight="xlight">
The content of the step 2
</Typography>
<Typography as="div" fontFamily="brand" fontSize="small" fontWeight="xlight">
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque
laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi
architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas
sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione
voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit
amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut
labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis
nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea
commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit
esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas
nulla pariatur?
</Typography>
</>
)
},
{
id: 'YUlLCaAK80ixOpHMNG6wHw',
label: 'Step 3',
content: (
<Typography as="div" fontFamily="brand" fontWeight="xlight">
The content of the step 3
</Typography>
)
}
]
const { state, dispatch, error } = useStepper(
steps.map(step => ({ id: step.id, status: step.status })),
'TMPd6bsSkUiTauLbHa0aWw'
)
const [submitted, setSubmitted] = useState(false)
const handleOpenChange = () => setSubmitted(false)
const handlePrevious = useCallback(() => {
dispatch({
type: 'previousClicked'
})
}, [dispatch])
const handleNext = useCallback(() => {
dispatch({
type: 'stepCompleted'
})
}, [dispatch])
const handleSubmit = useCallback(() => {
setSubmitted(true)
dispatch({
type: 'stepperSubmitted'
})
}, [dispatch])
const handleReset = useCallback(() => {
setSubmitted(false)
dispatch({
type: 'stepperReset'
})
}, [dispatch, steps])
return (
!error && (
<div className="stepper-container-small">
<Stepper
{...args}
activeStepId={getActiveStepId(state)}
onNext={handleNext}
onPrevious={handlePrevious}
onReset={handleReset}
onSubmit={handleSubmit}
steps={getUpdatedSteps(steps, state)}
/>
<Toast
isOpened={submitted}
onOpenChange={handleOpenChange}
title="Submit button clicked !"
severityLevel="success"
/>
</div>
)
)
}}
</Story>
## Properties
<ArgsTable story="Overview" />
## Usage
It is then up to the user to define the size (height and width) of the stepper, for example by placing it in a `div` and defining its size, as well as the content styles of each step. The content of the stepper is scrollable.
## Use the Stepper with built-in `useStepper` hook and selectors
To facilitate the use of Stepper, the `@okp4/ui` library provides a custom React hook to manage the status of steps during navigation.
Along with this hook, the library provides some functions (aka selectors) that can compute the current state returned by the `useStepper` hook without any additional user action.
We recommend using the `useStepper` hook with the selector functions to make it easier to use the Stepper. You can of course implement your own.
The [example](/docs/molecules-stepper--example) below will give you everything you need to understand how to use Stepper with the `useStepper` hook and selectors.
### `useStepper` hook
The `useStepper` provides you with the `state` object and the `dispatch` function.
To call the `useStepper` hook, you must provide two parameters :
- `steps: InitialStep[]` the initial steps list.
- `activeStepId: string` the initial step id that the Stepper should display first.
Where `InitialStep` is the following type :
```tsx
type InitialStep = {
id: string
status?: 'disabled' | 'invalid' | 'completed' | 'uncompleted'
}
```
Example :
```tsx
// Call the `useStepper` hook
const { state, dispatch, error } = useStepper(
steps.map(step => ({ id: step.id, status: step.status })), // The initial steps,
'DcKIvKNG1EOhV-oHJb85PA' // The initial active step id
)
```
We recommend to use strong and unique id as UUIDS.
##### The state
The `state` object contains the following values:
- `steps`: a map that provides the status and the order of each step and if the step is active.
- `initialSteps`: the snapshot list of the steps after the first initialization.
##### The dispatch function
The `dispatch` function allows to dispatch one of the following actions :
- `previousClicked` : sets the active step to **uncompleted** (unless it is **invalid**) and then sets the previous step as active.
- `stepCompleted` : sets the active step to **completed** and then set the next step as active.
- `stepFailed` : sets the active step to **invalid**.
- `stepAddedBefore` : adds a step before the provided one. This action comes with a `payload`.
- `stepAddedAfter` : adds a step after the provided one. This action comes with a `payload`.
- `stepperSubmitted` : sets the active step to **completed**.
- `stepperReset` : resets all steps to `initialSteps`.
Some actions come with a payload.
The `stepAddedBefore` and `stepAddedAfter` actions requires the step with the order of where to insert it before or after.
The `stepAddedBefore` action requires `StepWithBeforeOrder` as payload, and `stepAddedAfter` requires `StepWithAfterOrder`.
```tsx
type StepWithBeforeOrder = {
id: string
status?: 'disabled' | 'invalid' | 'completed' | 'uncompleted'
beforeOrder: number
}
type StepWithAfterOrder = {
id: string
status?: 'disabled' | 'invalid' | 'completed' | 'uncompleted'
afterOrder: number
}
```
Example :
```tsx
// Dispatch the `stepAddedBefore` action
const handleAddStep = useCallback(() => {
dispatch({
type: 'stepAddedBefore',
payload: {
step: {
id: 'p_laQI3gf0idl_QNg-4GyQ', // The id of the step to add
status: 'uncompleted' // The status of the step to add
beforeOrder: 3 // The zero-based index in which the step will be inserted before
}
}
})
}, [dispatch])
// Dispatch the `stepAddedAfter` action
const handleAddStep = useCallback(() => {
dispatch({
type: 'stepAddedAfter',
payload: {
step: {
id: 'p_laQI3gf0idl_QNg-4GyQ', // The id of the step to add
status: 'uncompleted' // The status of the step to add
afterOrder: 3 // The zero-based index in which the step will be inserted after
}
}
})
}, [dispatch])
```
### Selectors
The `@okp4/ui` library provides the following selectors to facilitate the use of the hook with the Stepper :
- `getUpdatedSteps` - It allows to get the steps with their updated status and order thanks to the state returned by the `useStepper` hook.
- `getActiveStepId` - It allows to get the active step id from the state returned by the `useStepper` hook.
### Example
Here is a complete example of how to use the Stepper with the `useStepper` hook and selector.
##### 1 - Steps initialization :
First, define your steps :
```tsx
const steps = [
{
id: 'eIi90zMum0SYOW-GPn7AOg',
label: 'ØKP4 Protocol',
status: 'uncompleted'
},
{
id: 'wSOOshtGhUGpixfKkZBoaQ',
label: 'News & Docs',
status: 'uncompleted'
},
{
id: 'pxJ9KKM5FkeezKZva-XOpQ',
label: 'Røadmap',
status: 'uncompleted',
onValidate: () => true
},
{
id: 'dlR3jRWttkCa9vW6FwDurA',
label: 'Team',
status: 'uncompleted'
}
]
```
##### 2- Hook call
Then call the hook with the id of the initial current step and map your steps to keep only the id and the status.
```tsx
const { state, dispatch, error }: UseStepper = useStepper(
steps.map(step => ({ id: step.id, status: step.status })),
'eIi90zMum0SYOW-GPn7AOg'
)
```
##### 3- Callbacks functions declarations
Define your `onPrevious`, `onNext`, `onSubmit` and `onReset` callback functions.
These functions can then call the `dispatch` function of the hook to execute the appropriate actions and update the state.
```tsx
const handlePrevious = useCallback(() => {
// You can define your logic here and call the dispatch function with the appropriated action
dispatch({
type: 'previousClicked'
})
}, [dispatch])
const handleNext = useCallback(() => {
// You can check the step with the `onValidate` callback function of the step and then call the appropriate action
const currentStep = steps.find((step: Step) => step.id === state.activeStepId)
const stepCompleted = currentStep && (!currentStep.onValidate || currentStep.onValidate())
dispatch({
type: stepCompleted ? 'stepCompleted' : 'stepFailed'
})
}, [dispatch, state.activeStepId, steps])
const handleSubmit = useCallback(() => {
// You can check if the last step is valid and then call the appropriate action
const lastStep = steps[steps.length - 1]
const stepCompleted = !lastStep.onValidate || lastStep.onValidate()
dispatch({
type: stepCompleted ? 'stepperSubmitted' : 'stepFailed'
})
}, [dispatch, steps])
const handleReset = useCallback(() => {
dispatch({
type: 'stepperReset'
})
}, [dispatch, steps])
```
##### 4- Stepper implementation
Finally, use the appropriate props and selector function to return the Stepper.
```tsx
return (
<div className="stepper-container-x-small">
<Stepper
steps={getUpdatedSteps(steps, state)}
activeStepId={getActiveStepId(state)}
onPrevious={handlePrevious}
onNext={handleNext}
onSubmit={handleSubmit}
onReset={handleReset}
isSubmitDisabled={false}
/>
</div>
)
```
##### Complete example
And here is the complete illustration of our example.
<Canvas>
<Story name="Example">
{() => {
// Define your steps
const steps = [
{
id: 'eIi90zMum0SYOW-GPn7AOg',
label: 'ØKP4 Protocol',
status: 'uncompleted'
},
{
id: 'wSOOshtGhUGpixfKkZBoaQ',
label: 'News & Docs',
status: 'uncompleted'
},
{
id: 'pxJ9KKM5FkeezKZva-XOpQ',
label: 'Røadmap',
status: 'uncompleted',
onValidate: () => true
},
{
id: 'dlR3jRWttkCa9vW6FwDurA',
label: 'Team',
status: 'uncompleted'
}
]
// Call the hook with two parameters
const { state, dispatch, error } = useStepper(
steps.map(step => ({ id: step.id, status: step.status })),
'eIi90zMum0SYOW-GPn7AOg'
)
// Define your callback functions `onPrevious`, `onNext`, `onSubmit` and `onReset`
const handlePrevious = useCallback(() => {
// You can define your logic here and call the dispatch function with the appropriated action
dispatch({
type: 'previousClicked'
})
}, [dispatch])
// You can check the step with the `onValidate` callback function of the step
// and then call the appropriated action
const handleNext = useCallback(() => {
const currentStep = steps.find(step => step.id === getActiveStepId(state))
const stepCompleted = currentStep && (!currentStep.onValidate || currentStep.onValidate())
dispatch({
type: stepCompleted ? 'stepCompleted' : 'stepFailed'
})
}, [dispatch, state.activeStepId, steps])
// You can check if the last step is valid and then call the appropriated action
const handleSubmit = useCallback(() => {
const lastStep = steps[steps.length - 1]
const stepCompleted = !lastStep.onValidate || lastStep.onValidate()
dispatch({
type: stepCompleted ? 'stepperSubmitted' : 'stepFailed'
})
}, [dispatch, steps])
const handleReset = useCallback(() => {
dispatch({
type: 'stepperReset'
})
}, [dispatch, steps])
return (
// Check if error before rendering the Stepper
!error && (
<div className="stepper-container-x-small">
<Stepper
activeStepId={getActiveStepId(state)}
onPrevious={handlePrevious}
onNext={handleNext}
onSubmit={handleSubmit}
onReset={handleReset}
steps={getUpdatedSteps(steps, state)}
/>
</div>
)
)
}}
</Story>
</Canvas>
## Step
The Stepper comes with a list of steps. A _Step_ must respect the following type :
```ts
type Step = {
/**
* The id of the step.
*/
readonly id: string
/**
* The title of the step.
*/
readonly label?: string
/**
* The status of the step.
*/
readonly status?: 'disabled' | 'invalid' | 'completed' | 'uncompleted'
/**
* The content of the step.
*/
readonly content?: JSX.Element
/**
* Callback function which allows to validate the step.
*/
readonly onValidate?: () => boolean
}
```
<Canvas>
<Story name="Step">
{() => {
const steps = [
{
id: 'Ppi_Ewca7kO8XPkJooI9jA',
label: 'This is the step label',
status: 'disabled',
content: (
<Typography as="div" fontFamily="brand" fontSize="small" fontWeight="xlight">
This is the content of the step. It can be any JSX Element.
</Typography>
)
},
{
id: 'cfMEx_oTAEur4lg7WoHZ1A',
label: 'This is a disabled step'
},
{
id: 'kqS6w9p31UKbukEM-J_vjg',
label: 'And finally the last step',
content: (
<Typography as="div" fontFamily="brand" fontSize="small" fontWeight="xlight">
This is the content of the last step.
</Typography>
)
}
]
const { state, dispatch, error } = useStepper(
steps.map(step => ({ id: step.id, status: step.status ?? 'uncompleted' })),
'cfMEx_oTAEur4lg7WoHZ1A'
)
const handlePrevious = useCallback(() => {
dispatch({
type: 'previousClicked'
})
}, [dispatch])
const handleNext = useCallback(() => {
dispatch({
type: 'stepCompleted'
})
}, [dispatch])
const handleSubmit = useCallback(() => {
dispatch({
type: 'stepperSubmitted'
})
}, [dispatch])
const handleReset = useCallback(() => {
dispatch({
type: 'stepperReset'
})
}, [dispatch, steps])
return (
!error && (
<div className="stepper-container-x-small">
<Stepper
activeStepId={getActiveStepId(state)}
onNext={handleNext}
onPrevious={handlePrevious}
onReset={handleReset}
onSubmit={handleSubmit}
steps={getUpdatedSteps(steps, state)}
/>
</div>
)
)
}}
</Story>
</Canvas>
## Step label
The `label` is the title of the step.
<Canvas>
<Story name="Step label">
{() => {
const steps = [
{
id: 'vFsGhDsnZEK0IqB2Y2VaEQ',
label: 'Step 1 label'
},
{
id: 'NtlpCFPunUSmnnrrm7fGig',
label: 'Step 2 label'
},
{
id: 'tyGwxwPvS0WH9SKZT5UxgA',
label: 'Step 3 label'
},
{
id: 'nFAsHIMCkkSt3S95FOF2SQ',
label: 'Step 4 label'
}
]
const { state, dispatch, error } = useStepper(
steps.map(step => ({ id: step.id, status: step.status })),
'vFsGhDsnZEK0IqB2Y2VaEQ'
)
const handlePrevious = useCallback(() => {
dispatch({
type: 'previousClicked'
})
}, [dispatch])
const handleNext = useCallback(() => {
dispatch({
type: 'stepCompleted'
})
}, [dispatch])
const handleSubmit = useCallback(() => {
dispatch({
type: 'stepperSubmitted'
})
}, [dispatch])
const handleReset = useCallback(() => {
dispatch({
type: 'stepperReset'
})
}, [dispatch, steps])
return (
!error && (
<div className="stepper-container-x-small">
<Stepper
activeStepId={getActiveStepId(state)}
onNext={handleNext}
onPrevious={handlePrevious}
onReset={handleReset}
onSubmit={handleSubmit}
steps={getUpdatedSteps(steps, state)}
/>
</div>
)
)
}}
</Story>
</Canvas>
## Step status
It is possible to provide an initial status to each step. The initial status is one of the following values : **disabled**, **invalid**, **completed**, or **uncompleted**.
By default, a step will be **uncompleted**.
- **disabled** - The step is disabled and can't be reached when clicked on the _Previous_ or the _Next_ button.
- **invalid** - The step is in error. It becomes **completed** if you go to the next step without error, or **invalid** if an error is detected (see the [onValidate](/docs/molecules-stepper--on-validate) section).
- **completed** - The step is already completed.
- **uncompleted** - The step is not completed yet. This status is the default status.
<Canvas>
<Story name="Step status">
{() => {
const steps = [
{
id: 'o0vGP_CSf0SMJn1yLugEBw',
label: 'First step already completed',
content: (
<Typography as="div" fontFamily="brand" fontSize="small" fontWeight="xlight">
This step is already completed.
</Typography>
),
status: 'completed'
},
{
id: 'nVqL02TVyUmRjtdU0DJRMg',
label: 'Second step disabled',
content: (
<Typography as="div" fontFamily="brand" fontSize="small" fontWeight="xlight">
This step is disbaled.
</Typography>
),
status: 'disabled'
},
{
id: 'IkiEpY147ES6trsLM8f--Q',
label: 'Third step in error',
content: (
<Typography as="div" fontFamily="brand" fontSize="small" fontWeight="xlight">
This step is in error. It remains in error if you click on Previous.
</Typography>
),
status: 'invalid'
},
{
id: 'N5sSREHzwkW5HivBG6xG_g',
label: 'Fourth step',
content: (
<Typography as="div" fontFamily="brand" fontSize="small" fontWeight="xlight">
The step 4 is the initial active step.
</Typography>
)
},
{
id: 'x9Kl7ud6mEWg9aWIyxTxUA',
label: 'Fifth step uncompleted',
content: (
<Typography as="div" fontFamily="brand" fontSize="small" fontWeight="xlight">
This step is not completed yet.
</Typography>
),
status: 'uncompleted'
},
{
id: 'GQTgLWIMVEqgCbovwkEtJQ',
label: 'Sixth step also uncompleted',
content: (
<Typography as="div" fontFamily="brand" fontSize="small" fontWeight="xlight">
This step is also not completed yet.
</Typography>
)
}
]
const { state, dispatch, error } = useStepper(
steps.map(step => ({ id: step.id, status: step.status })),
'N5sSREHzwkW5HivBG6xG_g'
)
const handlePrevious = useCallback(() => {
dispatch({
type: 'previousClicked'
})
}, [dispatch])
const handleNext = useCallback(() => {
dispatch({
type: 'stepCompleted'
})
}, [dispatch])
const handleSubmit = useCallback(() => {
dispatch({
type: 'stepperSubmitted'
})
}, [dispatch])
const handleReset = useCallback(() => {
dispatch({
type: 'stepperReset'
})
}, [dispatch, steps])
return (
!error && (
<div className="stepper-container-x-small">
<Stepper
activeStepId={getActiveStepId(state)}
onNext={handleNext}
onPrevious={handlePrevious}
onReset={handleReset}
onSubmit={handleSubmit}
steps={getUpdatedSteps(steps, state)}
/>
</div>
)
)
}}
</Story>
</Canvas>
## Step content
The `content` of the step can be any `JSX.Element`.
<Canvas>
<Story name="Step content">
{() => {
const steps = [
{
id: '9CSubVYzIky5BQgM2eNUVQ',
label: 'ØKP4 Protocol',
content: (
<div>
<Typography as="div" fontFamily="brand" fontSize="small" fontWeight="xlight">
ØKP4 protocol is a community-run technology powering many Data Spaces and the KNØW
cryptocurrency (coming soon).
</Typography>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
<img
style={{
padding: '15px',
height: '170px'
}}
src="https://okp4.network/wp-content/themes/okp4/img/compatibility.png"
data-lazy-src="https://okp4.network/wp-content/themes/okp4/img/compatibility.png"
data-ll-status="loaded"
/>
</div>
</div>
)
},
{
id: 'P6O-gUx0g0awFyCKjuYTJA',
label: 'News & Docs',
content: (
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center'
}}
>
<a href="https://docs.okp4.network/docs/whitepaper/abstract">
<img
style={{
maxWidth: '100%',
maxHeight: '100%'
}}
src="https://okp4.network/wp-content/themes/okp4/img/whitepaper.png"
/>
</a>
<Typography fontSize="small" fontWeight="light">
Whitepaper
</Typography>
</div>
)
},
{
id: 'fCc7rBouj0S2O4MtgNEeVw',
label: 'Røadmap',
content: (
<div
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
gap: '40px',
maxWidth: '100%',
maxHeight: '100%'
}}
>
<Card
size="small"
background="primary"
withBorder="true"
header={
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-end'
}}
>
<Typography
as="div"
fontFamily="brand"
fontSize="small"
fontWeight="light"
style={{
border: 'solid 1px',
padding: '4px 8px'
}}
>
2022
</Typography>
</div>
}
content={
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center'
}}
>
<Typography fontSize="medium">The takeoff</Typography>
</div>
}
/>
<Card
size="small"
background="primary"
withBorder="true"
header={
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-end'
}}
>
<Typography
as="div"
fontFamily="brand"
fontSize="small"
fontWeight="light"
style={{
border: 'solid 1px',
padding: '4px 8px'
}}
>
2023
</Typography>
</div>
}
content={
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center'
}}
>
<Typography fontSize="medium">Mainnet</Typography>
</div>
}
/>
</div>
)
},
{
id: 'mwcYWxrvCkqwwt8GeZOGRg',
label: 'Team',
content: (
<div>
<Typography as="p" fontFamily="brand" fontSize="small" fontWeight="xlight">
Meet the team on
<Typography fontFamily="brand" fontSize="small" fontWeight="bold">
<a
href="https://okp4.network/#team"
target="_blank"
style={{ textDecoration: 'unset', color: 'inherit' }}
>
{` ØKP4 `}
</a>
</Typography>
web site.
</Typography>
</div>
)
}
]
const { state, dispatch, error } = useStepper(
steps.map(step => ({ id: step.id, status: step.status })),
'9CSubVYzIky5BQgM2eNUVQ'
)
const handlePrevious = useCallback(() => {
dispatch({
type: 'previousClicked'
})
}, [dispatch])
const handleNext = useCallback(() => {
dispatch({
type: 'stepCompleted'
})
}, [dispatch])
const handleSubmit = useCallback(() => {
dispatch({
type: 'stepperSubmitted'
})
}, [dispatch])
const handleReset = useCallback(() => {
dispatch({
type: 'stepperReset'
})
}, [dispatch, steps])
return (
!error && (
<div className="stepper-container-medium">
<Stepper
activeStepId={getActiveStepId(state)}
onNext={handleNext}
onPrevious={handlePrevious}
onReset={handleReset}
onSubmit={handleSubmit}
steps={getUpdatedSteps(steps, state)}
submitButtonLabel="Finish"
/>
</div>
)
)
}}
</Story>
</Canvas>
## Callback function `onValidate`
The `onValidate` function is provided to validate the step before going to the next one. The function is called when clicking on the next button.
The Stepper goes to the next step if the function returns **true**, else the step becomes **invalid**.
<Canvas>
<Story name="onValidate">
{() => {
const [isStepError, setStepError] = useState(false)
const [isStepCompleted, setStepCompleted] = useState(false)
const handleValidateTypeOfDough = () => {
setStepCompleted(true)
return true
}
const handleValidateExtraIngredients = () => {
setStepError(true)
return false
}
const steps = [
{
id: 'zxnPnxshjUCmQHi9HF9KXA',
label: 'Type of dough',
onValidate: handleValidateTypeOfDough,
content: (
<Typography as="div" fontFamily="brand" fontSize="small" fontWeight="xlight">
This step is always valid because `onValidate` function returns `true`.
</Typography>
)
},
{
id: '-oUkvVVTvkyEPz8PxCO1Qg',
label: 'Extra ingredients',
onValidate: handleValidateExtraIngredients,
content: (
<Typography as="div" fontFamily="brand" fontSize="small" fontWeight="xlight">
This step is never valid because `onValidate` function returns `false`.
</Typography>
)
},
{
id: 'MNcGln9R8EWwTe4nKil6-w',
label: 'Payment'
}
]
const { state, dispatch, error } = useStepper(
steps.map(step => ({ id: step.id, status: step.status })),
'zxnPnxshjUCmQHi9HF9KXA'
)
const handlePrevious = useCallback(() => {
dispatch({
type: 'previousClicked'
})
}, [dispatch])
const handleNext = useCallback(() => {
const currentStep = steps.find(step => step.id === getActiveStepId(state))
const stepCompleted = currentStep && (!currentStep.onValidate || currentStep.onValidate())
dispatch({
type: stepCompleted ? 'stepCompleted' : 'stepFailed'
})
}, [dispatch, state, steps])
const handleSubmit = useCallback(() => {
dispatch({
type: 'stepperSubmitted'
})
}, [dispatch])
const handleReset = useCallback(() => {
dispatch({
type: 'stepperReset'
})
}, [dispatch, steps])
return (
!error && (
<div className="stepper-container-x-small">
<Stepper
activeStepId={getActiveStepId(state)}
onNext={handleNext}
onPrevious={handlePrevious}
onReset={handleReset}
onSubmit={handleSubmit}
steps={getUpdatedSteps(steps, state)}
submitButtonLabel="Finish"
/>
<Toast
description="The step is completed : `onValidate` returns true"
isOpened={isStepCompleted}
onOpenChange={() => setStepCompleted(false)}
title="Completed"
severityLevel="success"
autoDuration={5000}
/>
<Toast
description="The step is in error : `onValidate` returns false"
isOpened={isStepError}
onOpenChange={() => setStepError(false)}
title="Error"
severityLevel="error"
autoDuration={5000}
/>
</div>
)
)
}}
</Story>
</Canvas>
## Disable submit button
The `isSubmitDisabled` property allows to disabled the _Submit_ button.
<Canvas>
<Story name="Disable submit button">
{() => {
const steps = [
{
id: 'DXHjI4R2_U64-hYFhrmbLw',
label: 'ØKP4 Protocol',
status: 'completed'
},
{
id: 'mqL8tTYpJE2oGSBkYiZChw',
label: 'News & Docs',
status: 'completed'
},
{
id: 'GHqgISIzaEC8ELqnBlsHcw',
label: 'Røadmap',
status: 'completed'
},
{
id: 'N5XgeO4ffUaKn8ffAf2a8w',
label: 'Team'
}
]
const { state, dispatch, error } = useStepper(
steps.map(step => ({ id: step.id, status: step.status })),
'N5XgeO4ffUaKn8ffAf2a8w'
)
const handlePrevious = useCallback(() => {
dispatch({
type: 'previousClicked'
})
}, [dispatch])
const handleNext = useCallback(() => {
dispatch({
type: 'stepCompleted'
})
}, [dispatch])
const handleSubmit = useCallback(() => {
dispatch({
type: 'stepperSubmitted'
})
}, [dispatch])
const handleReset = useCallback(() => {
dispatch({
type: 'stepperReset'
})
}, [dispatch, steps])
return (
!error && (
<div className="stepper-container-x-small">
<Stepper
activeStepId={getActiveStepId(state)}
onNext={handleNext}
onPrevious={handlePrevious}
onReset={handleReset}
onSubmit={handleSubmit}
steps={getUpdatedSteps(steps, state)}
isSubmitDisabled={true}
/>
</div>
)
)
}}
</Story>
</Canvas>
## Buttons label
Users can provide labels for the _Submit_ and _Reset_ buttons using the `submitButtonLabel` and `resetButtonLabel` properties.
By default their labels are provided by the default translation files (respectively "Submit" and "Reset": see the Internationalization section).
<Canvas>
<Story name="Buttons label">
{() => {
const steps = [
{
id: '3-rsCn8oPkyj1DDMlCfu3A',
label: 'Q1',
status: 'completed',
content: (
<Typography as="div" fontFamily="brand">
<ul
style={{
display: 'flex',
flexDirection: 'column',
gap: '15px'
}}
>
<li>
<Typography as="div" fontFamily="brand" fontWeight="xlight">
Core blockchain developments (minting, stacking, slashing…)
</Typography>
</li>
<li>
<Typography as="div" fontFamily="brand" fontWeight="xlight">
Test phase I - Private Testnet launch
</Typography>
</li>
<li>
<Typography as="div" fontFamily="brand" fontWeight="xlight">
Open sourcing of protocol developments
</Typography>
</li>
</ul>
</Typography>
)
},
{
id: '7Dc3MyAr9US6NdIWB1a5jQ',
label: 'Q2',
status: 'completed',
content: (
<Typography as="div" fontFamily="brand">
<ul
style={{
display: 'flex',
flexDirection: 'column',
gap: '15px'
}}
>
<li>
<Typography as="div" fontFamily="brand" fontWeight="xlight">
Core blockchain developments (token generation, security, API functions,
governance…)
</Typography>
</li>
<li>
<Typography as="div" fontFamily="brand" fontWeight="xlight">
Core ecosystem development connected to the blockchain (workflow engine, event
streaming, orchestration system…)
</Typography>
</li>
<li>
<Typography as="div" fontFamily="brand" fontWeight="xlight">
Core concept developments – knowledge graph, tokenomics & model, mechanism
design
</Typography>
</li>
<li>
<Typography as="div" fontFamily="brand" fontWeight="xlight">
Community tools: discord, medium, twitter
</Typography>
</li>
<li>
<Typography as="div" fontFamily="brand" fontWeight="xlight">
Branding
</Typography>
</li>
</ul>
</Typography>
)
},
{
id: 'Gu2WQlLxFkiBnTIfSeh_xw',
label: 'Q3',
status: 'completed',
content: (
<Typography as="div" fontFamily="brand">
<ul
style={{
display: 'flex',
flexDirection: 'column',
gap: '15px'
}}
>
<li>
<Typography as="div" fontFamily="brand" fontWeight="xlight">
Core blockchain developments (custom modules, identity & access management,
business model design…)
</Typography>
</li>
<li>
<Typography as="div" fontFamily="brand" fontWeight="xlight">
OKP4 seed round conducted
</Typography>
</li>
<li>
<Typography as="div" fontFamily="brand" fontWeight="xlight">
Devnet launch + faucet launch
</Typography>
</li>
<li>
<Typography as="div" fontFamily="brand" fontWeight="xlight">
First public Data Space release on devnet (Rhizome)
</Typography>
</li>
<li>
<Typography as="div" fontFamily="brand" fontWeight="xlight">
Tools & documentation release to interact & contribute (wallet, dataverse
portal, knowledge graphs, catalogs)
</Typography>
</li>
</ul>
</Typography>
)
},
{
id: 'CWGWpazGakaKUHoBnQmofw',
label: 'Q4',
status: 'active',
content: (
<Typography as="div" fontFamily="brand">
<ul
style={{
display: 'flex',
flexDirection: 'column',
gap: '15px'
}}
>
<li>
<Typography as="div" fontFamily="brand" fontWeight="xlight">
Tools & documentation release to build custom Data Spaces and next-gen dApps
(SDK)
</Typography>
</li>
<li>
<Typography as="div" fontFamily="brand" fontWeight="xlight">
Incentivized testnet launch
</Typography>
</li>
<li>
<Typography as="div" fontFamily="brand" fontWeight="xlight">
Builders program launch
</Typography>
</li>
<li>
<Typography as="div" fontFamily="brand" fontWeight="xlight">
Launch of Community Calls & events
</Typography>
</li>
</ul>
</Typography>
)
}
]
const { state, dispatch, error } = useStepper(
steps.map(step => ({ id: step.id, status: step.status })),
'CWGWpazGakaKUHoBnQmofw'
)
const handlePrevious = useCallback(() => {
dispatch({
type: 'previousClicked'
})
}, [dispatch])
const handleNext = useCallback(() => {
dispatch({
type: 'stepCompleted'
})
}, [dispatch])
const handleSubmit = useCallback(() => {
dispatch({
type: 'stepperSubmitted'
})
}, [dispatch])
const handleReset = useCallback(() => {
dispatch({
type: 'stepperReset'
})
}, [dispatch, steps])
return (
!error && (
<div className="stepper-container-large">
<Stepper
activeStepId={getActiveStepId(state)}
onNext={handleNext}
onPrevious={handlePrevious}
onReset={handleReset}
onSubmit={handleSubmit}
steps={getUpdatedSteps(steps, state)}
submitButtonLabel="End of reading"
resetButtonLabel="Read again"
/>
</div>
)
)
}}
</Story>
</Canvas>
## Internationalization
Stepper is provided with its defaults translation files (`stepper_en.json` and `stepper_fr.json`).
It is possible to overwrite the translations or to add new translation keys if needed.
Here are the keys and their english translations:
##### **`stepper_en.json`**
```json
{
"stepper": {
"button": {
"previous": "Previous",
"next": "Next",
"submit": "Submit",
"reset": "Reset"
}
}
}
```
See <a href="?path=/docs/guidelines-internationalization--page">internationalization</a> for information on how to load translations.
## Callback functions `onPrevious`, `onNext`, `onSubmit` and `onReset`
The Stepper provides the `onPrevious`, `onNext`, `onSubmit` and `onReset` properties.
- `onPrevious` is called when the _Previous_ button is clicked.
- `onNext` is called when the _Next_ button is clicked.
- `onSubmit` is called when the _Submit_ button is clicked in the last step.
- `onReset` is called when the _Reset_ button is clicked. This button appears once the last step is completed.
<Canvas>
<Story name="Callback functions">
{() => {
const [isPreviousOpened, setPreviousOpened] = useState(false)
const [isNextOpened, setNextOpened] = useState(false)
const [isSubmitOpened, setSubmitOpened] = useState(false)
const [isResetOpened, setResetOpened] = useState(false)
const handlePreviousOpened = bool => () => setPreviousOpened(bool)
const handleNextOpened = bool => () => setNextOpened(bool)
const handleSubmitOpened = bool => () => setSubmitOpened(bool)
const handleResetOpened = bool => () => setResetOpened(bool)
const steps = [
{
id: 'UOfvE34NWU2vXiMgLVx6Dw',
label: 'ØKP4 Protocol'
},
{
id: 'd7HBdh1ZW0ik-NcW7V45Uw',
label: 'News & Docs'
},
{
id: '7v5rV05mlkqkt2ZyUzFfaw',
label: 'Røadmap'
},
{
id: 'EqDQEo9kBUqdH6Pz2193Ew',
label: 'Team'
}
]
const { state, dispatch, error } = useStepper(
steps.map(step => ({ id: step.id, status: step.status })),
'UOfvE34NWU2vXiMgLVx6Dw'
)
const handlePrevious = useCallback(() => {
setPreviousOpened(true)
dispatch({
type: 'previousClicked'
})
}, [dispatch])
const handleNext = useCallback(() => {
setNextOpened(true)
dispatch({
type: 'stepCompleted'
})
}, [dispatch])
const handleSubmit = useCallback(() => {
setSubmitOpened(true)
dispatch({
type: 'stepperSubmitted'
})
}, [dispatch])
const handleReset = useCallback(() => {
setResetOpened(true)
dispatch({
type: 'stepperReset'
})
}, [dispatch, steps])
return (
!error && (
<div className="stepper-container-x-small">
<Stepper
activeStepId={getActiveStepId(state)}
onNext={handleNext}
onPrevious={handlePrevious}
onReset={handleReset}
onSubmit={handleSubmit}
steps={getUpdatedSteps(steps, state)}
/>
<Toast
description="You clicked on Previous button"
isOpened={isPreviousOpened}
onOpenChange={handlePreviousOpened(false)}
severityLevel="success"
title="onPrevious"
/>
<Toast
description="You clicked on Next button"
isOpened={isNextOpened}
onOpenChange={handleNextOpened(false)}
severityLevel="success"
title="onNext"
/>
<Toast
description="You clicked on Submit button"
isOpened={isSubmitOpened}
onOpenChange={handleSubmitOpened(false)}
severityLevel="success"
title="onSubmit"
/>
<Toast
description="You clicked on Reset button"
isOpened={isResetOpened}
onOpenChange={handleResetOpened(false)}
severityLevel="success"
title="onReset"
/>
</div>
)
)
}}
</Story>
</Canvas>