Skip to content

Commit 38d6b69

Browse files
committed
refactor(Stepper): stepper vanilla extract (#5666)
* refactor: stepper vanilla extract * fix: remove data attributes * fix: add changeset
1 parent 6c81c14 commit 38d6b69

File tree

5 files changed

+606
-3698
lines changed

5 files changed

+606
-3698
lines changed

.changeset/cool-facts-search.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@ultraviolet/ui": minor
3+
---
4+
5+
Refactor component `Stepper` to use vanilla extract instead of Emotion
Lines changed: 53 additions & 189 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
11
'use client'
22

3-
import { css, keyframes } from '@emotion/react'
4-
import styled from '@emotion/styled'
53
import { CheckIcon } from '@ultraviolet/icons'
64
import type { ReactNode } from 'react'
75
import { useMemo } from 'react'
86
import { Bullet } from '../Bullet'
97
import { Stack } from '../Stack'
108
import { Text } from '../Text'
119
import { useStepper } from './StepperProvider'
12-
13-
const LINE_HEIGHT_SIZES = {
14-
medium: 4,
15-
small: 2,
16-
} as const
10+
import {
11+
animationStepperContainer,
12+
stepBullet,
13+
stepContainer,
14+
stepperContainerRecipe,
15+
stepperInteractive,
16+
stepText,
17+
} from './styles.css'
1718

1819
type StepProps = {
1920
onClick?: (index: number) => void
@@ -37,157 +38,6 @@ type StepProps = {
3738
className?: string
3839
'data-testid'?: string
3940
}
40-
const loadingAnimation = (size: 'small' | 'medium') => keyframes`
41-
from {
42-
width: 0;
43-
}
44-
to {
45-
width: calc(100% - ${size === 'small' ? '24px' : '32px'} - 8px)};
46-
`
47-
48-
const loadingStyle = (size: 'small' | 'medium') => css`
49-
animation: ${loadingAnimation(size)} 1s linear infinite;
50-
`
51-
52-
const StyledBullet = styled(Bullet)<{
53-
size: 'small' | 'medium'
54-
isActive: boolean
55-
}>`
56-
transition: box-shadow 300ms;
57-
min-width: ${({ theme, size }) =>
58-
size === 'small' ? theme.space[3] : theme.space[4]};
59-
${({ theme, isActive }) =>
60-
isActive
61-
? `background-color: ${theme.colors.primary.backgroundStrongHover};
62-
box-shadow: ${theme.shadows.focusPrimary};`
63-
: null};
64-
`
65-
66-
const StyledText = styled(Text)`
67-
transition: text-decoration-color 250ms ease-out;
68-
text-decoration-thickness: 1px;
69-
text-underline-offset: 2px;
70-
text-decoration-color: transparent;
71-
`
72-
73-
const StyledStepContainer = styled(Stack)<{
74-
'data-disabled': boolean
75-
'data-interactive': boolean
76-
'data-hide-separator': boolean
77-
'data-label-position': 'bottom' | 'right'
78-
size: 'small' | 'medium'
79-
'data-selected': boolean
80-
'data-done': boolean
81-
'data-animated': boolean
82-
}>`
83-
display: flex;
84-
white-space: nowrap;
85-
transition: text-decoration 300ms;
86-
87-
&[data-interactive="true"]:not([data-disabled="true"]) {
88-
cursor: pointer;
89-
90-
&[data-selected="true"]:hover {
91-
& > ${StyledBullet} {
92-
box-shadow: ${({ theme }) => theme.shadows.focusPrimary};
93-
& > ${StyledText} {
94-
color: ${({ theme }) => theme.colors.primary.textHover};
95-
text-decoration: underline
96-
${({ theme }) => theme.colors.primary.textHover};
97-
text-decoration-thickness: 1px;
98-
}
99-
}
100-
}
101-
102-
&[data-done="true"]:hover {
103-
& > ${StyledBullet} {
104-
box-shadow: ${({ theme }) => theme.shadows.focusPrimary};
105-
}
106-
& > ${StyledText} {
107-
color: ${({ theme }) => theme.colors.neutral.textHover};
108-
text-decoration: underline
109-
${({ theme }) => theme.colors.neutral.textHover};
110-
text-decoration-thickness: 1px;
111-
}
112-
}
113-
}
114-
115-
&[data-disabled="true"] {
116-
cursor: not-allowed;
117-
118-
& > ${StyledText} {
119-
color: ${({ theme }) => theme.colors.neutral.textDisabled};
120-
}
121-
122-
& > ${StyledBullet} {
123-
background-color: ${({ theme }) =>
124-
theme.colors.neutral.backgroundDisabled};
125-
box-shadow: none;
126-
color: ${({ theme }) => theme.colors.neutral.textDisabled};
127-
border-color: ${({ theme }) => theme.colors.neutral.borderDisabled};
128-
}
129-
}
130-
131-
&:not([data-hide-separator="true"]):not([data-label-position="right"]) {
132-
flex-direction: column;
133-
flex: 1;
134-
135-
& > ${StyledText} {
136-
margin-top: ${({ theme }) => theme.space[1]};
137-
}
138-
139-
&:not(:last-child){
140-
&:after {
141-
content: "";
142-
position: relative;
143-
align-self: baseline;
144-
border-radius: ${({ theme }) => theme.radii.default};
145-
top: ${({ theme }) => theme.space[2]};
146-
width: calc(100% - ${({ theme, size }) => (size === 'small' ? theme.space[5] : theme.space[6])});
147-
left: calc(50% + 25px);
148-
order: -1;
149-
height: ${({ size }) =>
150-
size === 'small'
151-
? LINE_HEIGHT_SIZES.small
152-
: LINE_HEIGHT_SIZES.medium}px;
153-
}
154-
155-
&[data-done="true"]:after {
156-
background-color: ${({ theme }) =>
157-
theme.colors.primary.backgroundStrong};
158-
}
159-
&[data-selected="true"][data-animated="true"]:after {
160-
${({ size }) => loadingStyle(size)}
161-
background-color: ${({ theme }) =>
162-
theme.colors.primary.backgroundStrong};
163-
164-
}
165-
}
166-
&:not(:last-child){
167-
&::before {
168-
content: "";
169-
position: relative;
170-
align-self: baseline;
171-
border-radius: ${({ theme }) => theme.radii.default};
172-
background-color: ${({ theme }) =>
173-
theme.colors.neutral.backgroundStrong};
174-
top: 20px;
175-
width: calc(
176-
100% - ${({ theme, size }) => (size === 'small' ? theme.space[5] : theme.space[6])});
177-
left: calc(50% + 25px);
178-
order: -1;
179-
height: ${({ size }) =>
180-
size === 'small'
181-
? LINE_HEIGHT_SIZES.small
182-
: LINE_HEIGHT_SIZES.medium}px;
183-
}
184-
}
185-
186-
&:last-child {
187-
margin-top: ${({ theme }) => theme.space[1]};
188-
}
189-
}
190-
`
19141

19242
export const Step = ({
19343
index = 0,
@@ -198,74 +48,88 @@ export const Step = ({
19848
className,
19949
'data-testid': dataTestId,
20050
}: StepProps) => {
201-
const currentState = useStepper()
202-
const isActive = index === currentState.step
203-
const isDone = index < currentState.step
51+
const {
52+
separator,
53+
labelPosition,
54+
animated,
55+
size,
56+
interactive,
57+
step,
58+
setStep,
59+
} = useStepper()
60+
const isActive = index === step
61+
const isDone = index < step
62+
const separatorBottom = separator && labelPosition === 'bottom'
63+
const interactiveDone = isDone && interactive
20464

20565
const textVariant = useMemo(() => {
206-
if (currentState.size === 'medium') {
66+
if (size === 'medium') {
20767
return isActive ? 'bodyStrong' : 'body'
20868
}
20969

21070
return isActive ? 'bodySmallStrong' : 'bodySmall'
211-
}, [currentState.size, isActive])
71+
}, [size, isActive])
21272

21373
return (
214-
<StyledStepContainer
74+
<Stack
21575
alignItems="center"
216-
className={className ?? 'step'}
217-
data-animated={currentState.animated}
218-
data-disabled={disabled}
219-
data-done={isDone}
220-
data-hide-separator={!currentState.separator}
221-
data-interactive={currentState.interactive && isDone}
222-
data-label-position={currentState.labelPosition}
223-
data-selected={isActive}
76+
className={`${className ? `${className} ` : 'step '}${stepContainer} ${separatorBottom ? stepperContainerRecipe({ animated, disabled, done: isDone, labelPosition, separator, size }) : ''} ${isActive && separator && animated ? animationStepperContainer[size] : ''} ${interactiveDone && !disabled ? stepperInteractive[isActive ? 'active' : 'inactive'] : ''}`}
22477
data-testid={dataTestId ?? `stepper-step-${index}`}
225-
direction={currentState.labelPosition === 'right' ? 'row' : 'column'}
226-
gap={currentState.labelPosition === 'right' ? 1 : 0}
78+
direction={labelPosition === 'right' ? 'row' : 'column'}
79+
gap={labelPosition === 'right' ? 1 : 0}
22780
justifyContent="flex-start"
22881
onClick={() => {
229-
if (currentState.interactive && !disabled) {
230-
if (index < currentState.step) {
231-
currentState.setStep(index)
82+
if (interactive && !disabled) {
83+
if (index < step) {
84+
setStep(index)
23285
}
23386
onClick?.(index)
23487
}
23588
}}
236-
size={currentState.size}
23789
>
23890
{isDone && !disabled ? (
239-
<StyledBullet
240-
isActive={isActive}
91+
<Bullet
92+
className={stepBullet({
93+
disabled,
94+
isActive,
95+
size,
96+
})}
24197
prominence="strong"
24298
sentiment="primary"
243-
size={currentState.size}
99+
size={size}
244100
>
245101
<CheckIcon />
246-
</StyledBullet>
102+
</Bullet>
247103
) : (
248-
<StyledBullet
249-
isActive={isActive}
104+
<Bullet
105+
className={stepBullet({
106+
disabled,
107+
isActive,
108+
size,
109+
})}
250110
prominence="strong"
251111
sentiment={isDone || isActive ? 'primary' : 'neutral'}
252-
size={currentState.size}
112+
size={size}
253113
>
254114
{(index + 1).toString()}
255-
</StyledBullet>
115+
</Bullet>
256116
)}
257117
{title ? (
258-
<StyledText
118+
<Text
259119
as="span"
120+
className={stepText({
121+
addMarginTop: separator && labelPosition !== 'right',
122+
disabled,
123+
})}
260124
prominence={isDone || isActive ? 'default' : 'weak'}
261125
sentiment={isActive ? 'primary' : 'neutral'}
262126
variant={textVariant}
263127
whiteSpace="normal"
264128
>
265129
{title}
266-
</StyledText>
130+
</Text>
267131
) : null}
268132
{children ?? null}
269-
</StyledStepContainer>
133+
</Stack>
270134
)
271135
}

0 commit comments

Comments
 (0)