-
Notifications
You must be signed in to change notification settings - Fork 20
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(Stepper): various enhancements and refactor (#3781)
* feat: stepper enhancements draft * feat: enhancement stepper * fix: automatic indexing and more controllable * fix: refactor code * fix: update tests * fix: tests with vitest * fix: tests * fix: add children and fix problem with long titles * fix: use data instead of js prop for style * fix: rebase and update snapshots
- Loading branch information
Showing
17 changed files
with
5,719 additions
and
727 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
--- | ||
"@ultraviolet/ui": minor | ||
--- | ||
|
||
`<Stepper />` enhancement: | ||
- Added a new disabled state | ||
- Added possibility to navigate throughout the steps for the user by clicking on the bullets | ||
- Refactor code : use `<Stepper.Step />` for the steps instead of a list of children | ||
- New style overall |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1564,4 +1564,4 @@ exports[`TimeField > should trigger events 1`] = ` | |
</form> | ||
</div> | ||
</DocumentFragment> | ||
`; | ||
`; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,258 @@ | ||
import { css, keyframes } from '@emotion/react' | ||
import styled from '@emotion/styled' | ||
import type { ReactNode } from 'react' | ||
import { useMemo } from 'react' | ||
import { Bullet } from '../Bullet' | ||
import { Stack } from '../Stack' | ||
import { Text } from '../Text' | ||
import { useStepper } from './StepperProvider' | ||
|
||
const LINE_HEIGHT_SIZES = { | ||
small: 2, | ||
medium: 4, | ||
} as const | ||
|
||
type StepProps = { | ||
onClick?: (index: number) => void | ||
/** | ||
* Automatically attribued by the parent Stepper | ||
*/ | ||
index?: number | ||
/** | ||
* Whether the step is disabled | ||
*/ | ||
disabled?: boolean | ||
/** | ||
* Title of the step | ||
*/ | ||
title?: ReactNode | ||
/** | ||
* For additional information. | ||
* Use prop `title` to properly name the step | ||
*/ | ||
children?: ReactNode | ||
className?: string | ||
'data-testid'?: string | ||
} | ||
const loadingAnimation = (size: 'small' | 'medium') => keyframes` | ||
from { | ||
width: 0; | ||
} | ||
to { | ||
width: calc( | ||
100% - ${size === 'small' ? '24px' : '32px'} - | ||
8px} | ||
); | ||
} | ||
` | ||
|
||
const loadingStyle = (size: 'small' | 'medium') => css` | ||
animation: ${loadingAnimation(size)} 1s linear infinite; | ||
` | ||
|
||
const StyledBullet = styled(Bullet)<{ | ||
size: 'small' | 'medium' | ||
isActive: boolean | ||
}>` | ||
margin-top: ${({ theme, size }) => | ||
size === 'small' ? theme.space['0.5'] : 0}; | ||
transition: box-shadow 300ms; | ||
min-width: ${({ theme, size }) => | ||
size === 'small' ? theme.space[3] : theme.space[4]}; | ||
${({ theme, isActive }) => | ||
isActive | ||
? `background-color: ${theme.colors.primary.backgroundStrongHover}; | ||
box-shadow: ${theme.shadows.focusPrimary};` | ||
: null}; | ||
` | ||
|
||
const StyledText = styled(Text)` | ||
margin-top: ${({ theme }) => theme.space['1']}; | ||
transition: text-decoration-color 250ms ease-out; | ||
text-decoration-thickness: 1px; | ||
text-underline-offset: 2px; | ||
text-decoration-color: transparent; | ||
white-space: normal; | ||
` | ||
|
||
const StyledStepContainer = styled(Stack)<{ | ||
'data-disabled': boolean | ||
'data-interactive': boolean | ||
'data-hide-separator': boolean | ||
'data-label-position': 'bottom' | 'right' | ||
size: 'small' | 'medium' | ||
'aria-selected': boolean | ||
'data-done': boolean | ||
'data-animated': boolean | ||
}>` | ||
display: flex; | ||
white-space: nowrap; | ||
transition: text-decoration 300ms; | ||
&[data-interactive='true']:not([data-disabled='true']) { | ||
cursor: pointer; | ||
&[aria-selected='true']:hover { | ||
& > ${StyledBullet} { | ||
box-shadow: ${({ theme }) => theme.shadows.focusPrimary}; | ||
& > ${StyledText} { | ||
color: ${({ theme }) => theme.colors.primary.textHover}; | ||
text-decoration: underline | ||
${({ theme }) => theme.colors.primary.textHover}; | ||
text-decoration-thickness: 1px; | ||
} | ||
} | ||
} | ||
&[data-done='true']:hover { | ||
& > ${StyledBullet} { | ||
box-shadow: ${({ theme }) => theme.shadows.focusPrimary}; | ||
} | ||
& > ${StyledText} { | ||
color: ${({ theme }) => theme.colors.neutral.textHover}; | ||
text-decoration: underline | ||
${({ theme }) => theme.colors.neutral.textHover}; | ||
text-decoration-thickness: 1px; | ||
} | ||
} | ||
} | ||
&[data-disabled='true'] { | ||
cursor: not-allowed; | ||
& > ${StyledText} { | ||
color: ${({ theme }) => theme.colors.neutral.textDisabled}; | ||
} | ||
& > ${StyledBullet} { | ||
background-color: ${({ theme }) => | ||
theme.colors.neutral.backgroundDisabled}; | ||
box-shadow: none; | ||
color: ${({ theme }) => theme.colors.neutral.textDisabled}; | ||
border-color: ${({ theme }) => theme.colors.neutral.borderDisabled}; | ||
} | ||
} | ||
&:not([data-hide-separator='true']):not([data-label-position='right']) { | ||
flex-direction: column; | ||
flex: 1; | ||
&:not(:last-child) { | ||
&:after { | ||
content: ''; | ||
position: relative; | ||
align-self: baseline; | ||
border-radius: ${({ theme }) => theme.radii.default}; | ||
background-color: ${({ theme }) => | ||
theme.colors.neutral.backgroundStrong}; | ||
top: 20px; | ||
width: calc( | ||
100% - ${({ size }) => (size === 'small' ? '24px' : '32px')} - | ||
${({ theme }) => theme.space[2]} | ||
); | ||
left: calc(50% + 25px); | ||
order: -1; | ||
height: ${({ size }) => | ||
size === 'small' | ||
? LINE_HEIGHT_SIZES.small | ||
: LINE_HEIGHT_SIZES.medium}px; | ||
} | ||
&[data-done='true']:after { | ||
background-color: ${({ theme }) => | ||
theme.colors.primary.backgroundStrong}; | ||
} | ||
&[aria-selected='true'][data-animated='true']:after { | ||
background-color: ${({ theme }) => | ||
theme.colors.primary.backgroundStrong}; | ||
${({ size }) => loadingStyle(size)} | ||
} | ||
} | ||
&:last-child { | ||
margin-top: ${({ theme, size }) => | ||
size === 'small' ? '6px' : theme.space[1]}; | ||
} | ||
} | ||
` | ||
|
||
export const Step = ({ | ||
index = 0, | ||
onClick, | ||
disabled = false, | ||
title, | ||
children, | ||
className, | ||
'data-testid': dataTestId, | ||
}: StepProps) => { | ||
const currentState = useStepper() | ||
const isActive = index === currentState.step | ||
const isDone = index < currentState.step | ||
|
||
const textVariant = useMemo(() => { | ||
if (currentState.size === 'medium') { | ||
return isActive ? 'bodyStrong' : 'body' | ||
} | ||
|
||
return isActive ? 'bodySmallStrong' : 'bodySmall' | ||
}, [currentState.size, isActive]) | ||
|
||
return ( | ||
<StyledStepContainer | ||
gap={currentState.labelPosition === 'right' ? 1 : 0.5} | ||
direction={currentState.labelPosition === 'right' ? 'row' : 'column'} | ||
alignItems={ | ||
currentState.labelPosition === 'right' ? 'baseline' : 'center' | ||
} | ||
justifyContent="flex-start" | ||
className={className ?? 'step'} | ||
data-interactive={currentState.interactive && isDone} | ||
onClick={() => { | ||
if (currentState.interactive && !disabled) { | ||
if (index < currentState.step) { | ||
currentState.setStep(index) | ||
} | ||
onClick?.(index) | ||
} | ||
}} | ||
data-disabled={disabled} | ||
data-testid={dataTestId ?? `stepper-step-${index}`} | ||
data-hide-separator={!currentState.separator} | ||
data-label-position={currentState.labelPosition} | ||
size={currentState.size} | ||
aria-selected={isActive} | ||
data-done={isDone} | ||
data-animated={currentState.animated} | ||
> | ||
{isDone && !disabled ? ( | ||
<StyledBullet | ||
sentiment="primary" | ||
icon="check" | ||
prominence="strong" | ||
size={currentState.size} | ||
isActive={isActive} | ||
/> | ||
) : ( | ||
<StyledBullet | ||
sentiment={isDone || isActive ? 'primary' : 'neutral'} | ||
text={index.toString()} | ||
prominence="strong" | ||
size={currentState.size} | ||
isActive={isActive} | ||
/> | ||
)} | ||
{title ? ( | ||
<StyledText | ||
as="span" | ||
variant={textVariant} | ||
prominence={isDone || isActive ? 'default' : 'weak'} | ||
sentiment={isActive ? 'primary' : 'neutral'} | ||
> | ||
{title} | ||
</StyledText> | ||
) : null} | ||
{children ?? null} | ||
</StyledStepContainer> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import type { ReactNode } from 'react' | ||
import { createContext, useContext, useEffect, useMemo, useState } from 'react' | ||
|
||
type ContextType = { | ||
step: number | ||
setStep: React.Dispatch<React.SetStateAction<number>> | ||
interactive: boolean | ||
size: 'medium' | 'small' | ||
animated: boolean | ||
labelPosition: 'bottom' | 'right' | ||
separator: boolean | ||
} | ||
|
||
const StepperContext = createContext<ContextType>({ | ||
step: 0, | ||
setStep: () => {}, | ||
interactive: false, | ||
size: 'medium', | ||
animated: false, | ||
labelPosition: 'bottom', | ||
separator: true, | ||
}) | ||
|
||
type StepperProviderProps = { | ||
children: ReactNode | ||
interactive: boolean | ||
animated: boolean | ||
selected: number | ||
labelPosition: 'bottom' | 'right' | ||
size: 'small' | 'medium' | ||
separator: boolean | ||
} | ||
|
||
export const useStepper = () => useContext(StepperContext) | ||
/** | ||
* Stepper component to show the progress of a process in a linear way. | ||
*/ | ||
export const StepperProvider = ({ | ||
children, | ||
interactive, | ||
selected, | ||
animated, | ||
labelPosition, | ||
size, | ||
separator, | ||
}: StepperProviderProps) => { | ||
const currentSelected = useMemo(() => selected, [selected]) | ||
const [step, setStep] = useState(currentSelected) | ||
const value = useMemo( | ||
() => ({ | ||
step, | ||
setStep, | ||
interactive, | ||
size, | ||
animated, | ||
labelPosition, | ||
separator, | ||
}), | ||
[step, interactive, size, animated, labelPosition, separator], | ||
) | ||
useEffect(() => setStep(selected), [selected]) | ||
|
||
return ( | ||
<StepperContext.Provider value={value}>{children}</StepperContext.Provider> | ||
) | ||
} |
33 changes: 33 additions & 0 deletions
33
packages/ui/src/components/Stepper/__stories__/Children.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import type { StoryFn } from '@storybook/react' | ||
import { Icon } from '@ultraviolet/icons' | ||
import { Stepper } from '..' | ||
import { Text } from '../../Text' | ||
|
||
export const Children: StoryFn<typeof Stepper> = args => ( | ||
<Stepper {...args}> | ||
<Stepper.Step title="Step 1">Children</Stepper.Step> | ||
<Stepper.Step title="Step 2"> | ||
<Text as="span" variant="body" sentiment="primary"> | ||
Custom text! | ||
</Text> | ||
</Stepper.Step> | ||
<Stepper.Step title="Step 3" /> | ||
<Stepper.Step title="Step 4" /> | ||
<Stepper.Step title="Step 5"> | ||
Icon: <Icon name="sun" /> | ||
</Stepper.Step> | ||
</Stepper> | ||
) | ||
|
||
Children.parameters = { | ||
docs: { | ||
description: { | ||
story: 'You can add children to add more information', | ||
}, | ||
}, | ||
} | ||
|
||
Children.args = { | ||
selected: 3, | ||
interactive: false, | ||
} |
Oops, something went wrong.