renders correctly with step number in row 1`] = `
class="emotion-0 emotion-1"
>
renders correctly with step number in row 1`] = `
-
- Step 1
-
+ step 1
-
-
diff --git a/packages/ui/src/components/Stepper/__tests__/index.test.tsx b/packages/ui/src/components/Stepper/__tests__/index.test.tsx
index 5519328451..f4988e8192 100644
--- a/packages/ui/src/components/Stepper/__tests__/index.test.tsx
+++ b/packages/ui/src/components/Stepper/__tests__/index.test.tsx
@@ -1,56 +1,139 @@
-import { shouldMatchEmotionSnapshot } from '@utils/test'
-import { describe, test } from 'vitest'
+import { screen } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import { renderWithTheme, shouldMatchEmotionSnapshot } from '@utils/test'
+import { describe, expect, test } from 'vitest'
import { Stepper } from '..'
describe('Stepper', () => {
test('renders correctly with default props', () =>
shouldMatchEmotionSnapshot(
-
- Step 1
- Step 2
- Step 3
+
+
+
+
,
))
test('renders correctly with selected prop', () =>
shouldMatchEmotionSnapshot(
-
- Step 1
- Step 2
- Step 3
+
+
+
+
+ ,
+ ))
+ test('renders correctly with children', () =>
+ shouldMatchEmotionSnapshot(
+
+ Children
+
+
,
))
-
test('renders correctly with animation', () =>
shouldMatchEmotionSnapshot(
-
- Step 1
- Step 2
- Step 3
+
+
+
+
,
))
test('renders correctly with all selected', () =>
shouldMatchEmotionSnapshot(
- Step 1
- Step 2
- Step 3
+
+
+
,
))
test('renders correctly with step number in row', () =>
shouldMatchEmotionSnapshot(
- Step 1
- Step 2
- Step 3
+
+
+
,
))
test('renders correctly with small size', () =>
shouldMatchEmotionSnapshot(
+
+
+
+ ,
+ ))
+
+ test('renders correctly without separator', () =>
+ shouldMatchEmotionSnapshot(
+
+
+
+
+ ,
+ ))
+
+ test('renders correctly without separator with label at the right', () =>
+ shouldMatchEmotionSnapshot(
+
+
+
+
+ ,
+ ))
+
+ test('renders correctly with disabled steps', () =>
+ shouldMatchEmotionSnapshot(
+
+
+
+
+ ,
+ ))
+
+ test('handles clicks when interactive', async () => {
+ const { asFragment } = renderWithTheme(
+
+
+
+
+ ,
+ )
+ await userEvent.click(screen.getByTestId('stepper-step-1'))
+ await userEvent.click(screen.getByTestId('stepper-step-2')) // should do nothing
+ expect(asFragment()).toMatchSnapshot()
+ })
+
+ test('handles clicks when interactive and small', async () => {
+ const { asFragment } = renderWithTheme(
+
+
+
+
+ ,
+ )
+ await userEvent.click(screen.getByTestId('stepper-step-1'))
+ expect(asFragment()).toMatchSnapshot()
+ })
+
+ test('handles clicks when not interactive', async () => {
+ const { asFragment } = renderWithTheme(
+
+
+
+
+ ,
+ )
+
+ await userEvent.click(screen.getByTestId('stepper-step-1'))
+ expect(asFragment()).toMatchSnapshot()
+ })
+
+ test('renders correctly without Stepper.Step', () =>
+ shouldMatchEmotionSnapshot(
+
Step 1
Step 2
Step 3
diff --git a/packages/ui/src/components/Stepper/index.tsx b/packages/ui/src/components/Stepper/index.tsx
index b7aa2745f0..c39a3dd79c 100644
--- a/packages/ui/src/components/Stepper/index.tsx
+++ b/packages/ui/src/components/Stepper/index.tsx
@@ -1,17 +1,54 @@
import { css, keyframes } from '@emotion/react'
import styled from '@emotion/styled'
import type { ReactNode } from 'react'
-import { Children, Fragment } from 'react'
-import { Bullet } from '../Bullet'
-import { Text } from '../Text'
-
-type Temporal = 'previous' | 'next' | 'current'
+import { Children, Fragment, isValidElement } from 'react'
+import { Step } from './Step'
+import { StepperProvider } from './StepperProvider'
const LINE_HEIGHT_SIZES = {
small: 2,
medium: 4,
} as const
+type Temporal = 'previous' | 'next' | 'current'
+
+type StepperProps = {
+ animated?: boolean
+ /**
+ * When true, the user can navigate through the steps by clicking on the bullets
+ */
+ interactive?: boolean
+ /**
+ * Number of the active step
+ */
+ selected?: number
+ children: ReactNode
+ className?: string
+ labelPosition?: 'bottom' | 'right'
+ size?: 'small' | 'medium'
+ 'data-testid'?: string
+ separator?: boolean
+}
+
+const StyledContainer = styled.div<{
+ size: 'small' | 'medium'
+ labelPosition: 'bottom' | 'right'
+ separator: boolean
+}>`
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: ${({ separator, labelPosition }) =>
+ !separator && labelPosition === 'right' ? 'flex-end' : 'flex-start'};
+ gap: ${({ theme, labelPosition, separator }) => {
+ if (separator) {
+ return labelPosition === 'bottom' ? theme.space['0.5'] : theme.space['1']
+ }
+
+ return theme.space['4']
+ }};
+`
+
const loadingAnimation = keyframes`
from {
width: 0;
@@ -21,175 +58,105 @@ const loadingAnimation = keyframes`
}
`
-const StyledStepContainer = styled.div`
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: flex-start;
-`
-
-const StyledText = styled(Text)`
- margin-top: ${({ theme }) => theme.space['1']};
-`
-
-const StyledBullet = styled(Bullet)<{ size: 'small' | 'medium' }>`
- margin-top: ${({ theme, size }) =>
- size === 'small' ? theme.space['0.5'] : 0};
-`
-
const loadingStyle = css`
animation: ${loadingAnimation} 1s linear infinite;
`
-const StyledLine = styled.div<{ temporal: Temporal; animated: boolean }>`
- border-radius: ${({ theme }) => theme.radii.default};
- flex-grow: 1;
- border-radius: ${({ theme }) => theme.radii.default};
- background-color: ${({ theme }) => theme.colors.neutral.backgroundStrong};
- position: relative;
-
- ::after {
- content: '';
- position: absolute;
- left: 0;
- top: 0;
- height: 100%;
- border-radius: ${({ theme }) => theme.radii.default};
- background-color: ${({ theme }) => theme.colors.primary.backgroundStrong};
- ${({ temporal }) => temporal === 'previous' && `width: 100%;`}
- ${({ temporal, animated }) =>
- temporal === 'current' && animated && loadingStyle}
- }
-`
-
-const StyledContainer = styled.div<{
- size: 'small' | 'medium'
- labelPosition: 'bottom' | 'right'
+const StyledLine = styled.div<{
+ temporal: Temporal
+ animated: boolean
+ 'data-size': 'small' | 'medium'
}>`
- display: flex;
- flex-direction: row;
- justify-content: center;
- align-items: flex-start;
- gap: 0 ${({ theme }) => theme.space['1']};
- gap: ${({ theme, labelPosition, size }) =>
- size === 'medium' && labelPosition === 'bottom'
- ? theme.space['0']
- : theme.space['1']};
-
- ${StyledStepContainer} {
- display: flex;
- flex-direction: ${({ labelPosition }) =>
- labelPosition === 'bottom' ? 'column' : 'row'};
- align-items: ${({ labelPosition }) =>
- labelPosition === 'bottom' ? 'center' : 'baseline'};
- gap: ${({ theme, labelPosition }) =>
- labelPosition === 'bottom' ? theme.space['0.5'] : theme.space['1']};
- white-space: nowrap;
- }
-
- ${StyledLine} {
- height: ${({ size }) => LINE_HEIGHT_SIZES[size]}px;
+ border-radius: ${({ theme }) => theme.radii.default};
+ flex-grow: 1;
+ border-radius: ${({ theme }) => theme.radii.default};
+ background-color: ${({ theme }) => theme.colors.neutral.backgroundStrong};
+ position: relative;
+ height: ${LINE_HEIGHT_SIZES.medium}px;
margin-top: ${({ theme }) => theme.space['2']};
margin-bottom: ${({ theme }) => theme.space['2']};
- }
-`
-
-type StepperNumbersProps = {
- temporal: Temporal
- children: ReactNode
- CurrentStep: number
- size?: 'small' | 'medium'
-}
-const StepperNumbers = ({
- temporal,
- children,
- CurrentStep,
- size = 'medium',
-}: StepperNumbersProps) => (
-
-
+ &[data-size='small'] {
+ height: ${LINE_HEIGHT_SIZES.small}px;
+ }
+ ::after {
+ content: '';
+ position: absolute;
+ left: 0;
+ top: 0;
+ height: 100%;
+ border-radius: ${({ theme }) => theme.radii.default};
+ background-color: ${({ theme }) => theme.colors.primary.backgroundStrong};
+ ${({ temporal }) => {
+ if (temporal === 'previous') return 'width: 100%;'
+
+ return null
+ }}
+ ${({ temporal, animated }) =>
+ temporal === 'current' && animated && loadingStyle}
+ }
+ }
+ `
-
- {children}
-
-
-)
-
-type StepperProps = {
- animated?: boolean
- selected?: number
- children: ReactNode[]
- className?: string
- labelPosition?: 'bottom' | 'right'
- size?: 'small' | 'medium'
- 'data-testid'?: string
-}
-
-/**
- * Stepper component to show the progress of a process in a linear way.
- */
export const Stepper = ({
children,
- selected = 0,
+ interactive = false,
+ selected = 1,
animated = false,
className,
labelPosition = 'bottom',
size = 'medium',
'data-testid': dataTestId,
+ separator = true,
}: StepperProps) => {
- const cleanChildren = Children.toArray(children)
+ const cleanChildren = Children.toArray(children).filter(isValidElement)
const lastStep = Children.count(cleanChildren) - 1
return (
-
- {Children.map(cleanChildren, (child, index) => {
- const getTemporal = () => {
- if (selected > index) return 'previous'
-
- if (selected === index) return 'current'
-
- return 'next'
- }
- const isNotLast = index < lastStep
- const temporal = getTemporal()
-
- return (
- // eslint-disable-next-line react/no-array-index-key
-
-
- {child}
-
- {isNotLast ? (
-
- ) : null}
-
- )
- })}
-
+
+ {Children.map(cleanChildren, (child, index) => {
+ const getTemporal = () => {
+ if (selected > index + 1) return 'previous'
+
+ if (selected === index + 1) return 'current'
+
+ return 'next'
+ }
+ const isNotLast = index < lastStep
+ const temporal = getTemporal()
+
+ return (
+ // eslint-disable-next-line react/no-array-index-key
+
+
+
+ {isNotLast && separator && labelPosition === 'right' ? (
+
+ ) : null}
+
+ )
+ })}
+
+
)
}
+
+Stepper.Step = Step