Skip to content

File tree

src/stepper.css.ts

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,13 @@ const stepIconKeyframes = keyframes({
6565
export const stepper = style([
6666
sprinkles({
6767
display: 'flex',
68+
alignItems: 'flex-start',
6869
minHeight: 24,
6970
}),
7071
{
72+
listStyle: 'none',
73+
margin: 0,
74+
padding: 0,
7175
'@media': {
7276
[mq.desktopOrBigger]: {
7377
minHeight: stepperMinHeight,
@@ -79,6 +83,11 @@ export const stepper = style([
7983
},
8084
]);
8185

86+
export const listItem = sprinkles({
87+
display: 'flex',
88+
alignItems: 'center',
89+
});
90+
8291
export const step = style([
8392
sprinkles({
8493
position: 'relative',
@@ -88,13 +97,15 @@ export const step = style([
8897
}),
8998
{
9099
textAlign: 'center',
91-
':first-child': {
92-
alignItems: 'flex-start',
93-
textAlign: 'left',
94-
},
95-
':last-child': {
96-
alignItems: 'flex-end',
97-
textAlign: 'right',
100+
selectors: {
101+
':first-child > &': {
102+
alignItems: 'flex-start',
103+
textAlign: 'left',
104+
},
105+
':last-child > &': {
106+
alignItems: 'flex-end',
107+
textAlign: 'right',
108+
},
98109
},
99110
},
100111
]);
@@ -164,6 +175,11 @@ export const textContainer = style([
164175
{
165176
width: 200,
166177
top: `calc(${pxToRem(24)} + 18px)`,
178+
'@media': {
179+
[mq.tabletOrSmaller]: {
180+
display: 'none',
181+
},
182+
},
167183
},
168184
]);
169185

@@ -184,15 +200,6 @@ export const bar = style([
184200
{
185201
margin: '0 8px',
186202
background: skinVars.colors.barTrack,
187-
':last-child': {
188-
display: 'none',
189-
},
190-
'@media': {
191-
[mq.desktopOrBigger]: {
192-
position: 'relative',
193-
top: `calc(${pxToRem(8)} + 6px)`,
194-
},
195-
},
196203
},
197204
]);
198205

src/stepper.tsx

Lines changed: 49 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,37 @@
22
import * as React from 'react';
33
import classnames from 'classnames';
44
import {Text2, Text1} from './text';
5-
import {useScreenSize, useElementDimensions, useTheme} from './hooks';
5+
import {useElementDimensions, useTheme} from './hooks';
6+
import * as tokens from './text-tokens';
67
import IconSuccess from './icons/icon-success';
78
import * as styles from './stepper.css';
89
import {pxToRem, applyCssVars} from './utils/css';
910
import {vars} from './skins/skin-contract.css';
1011
import {getPrefixedDataAttributes} from './utils/dom';
12+
import ScreenReaderOnly from './screen-reader-only';
1113

1214
import type {DataAttributes} from './utils/types';
1315

1416
type StepperProps = {
1517
steps: ReadonlyArray<string>;
1618
currentIndex: number;
1719
'aria-label'?: string;
18-
children?: void;
20+
'aria-labelledby'?: string;
21+
'aria-description'?: string;
22+
'aria-describedby'?: string;
1923
dataAttributes?: DataAttributes;
2024
};
2125

2226
const Stepper = ({
2327
steps,
2428
currentIndex,
2529
'aria-label': ariaLabel,
30+
'aria-labelledby': ariaLabelledby,
31+
'aria-description': ariaDescription,
32+
'aria-describedby': ariaDescribedby,
2633
dataAttributes,
2734
}: StepperProps): JSX.Element => {
28-
const {textPresets} = useTheme();
29-
const {isDesktopOrBigger} = useScreenSize();
35+
const {texts, t, textPresets} = useTheme();
3036
const {height, ref} = useElementDimensions();
3137
const textContainerHeight = height;
3238

@@ -41,13 +47,24 @@ const Stepper = ({
4147
}
4248
}, [currentIndex, steps, step]);
4349

50+
const completedText = texts.stepperCompletedStep || t(tokens.stepperCompletedStep);
51+
const currentText = texts.stepperCurrentStep || t(tokens.stepperCurrentStep);
52+
4453
return (
45-
<div
54+
// The explicit role="list" is needed for Safari VoiceOver when setting css list-style: none
55+
// aria-description is not supported by the eslint rule
56+
// eslint-disable-next-line jsx-a11y/no-redundant-roles, jsx-a11y/role-supports-aria-props
57+
<ol
58+
role="list"
4659
className={styles.stepper}
4760
style={applyCssVars({
4861
[styles.vars.stepperMinHeight]: pxToRem(40 + textContainerHeight),
4962
})}
5063
{...getPrefixedDataAttributes(dataAttributes, 'Stepper')}
64+
aria-label={ariaLabel}
65+
aria-labelledby={ariaLabelledby}
66+
aria-description={ariaDescription}
67+
aria-describedby={ariaDescribedby}
5168
>
5269
{steps.map((text, index) => {
5370
const isCurrent = index === step;
@@ -56,16 +73,13 @@ const Stepper = ({
5673
const hasAnimation = index === step - 1;
5774

5875
return (
59-
<React.Fragment key={index}>
60-
<div
61-
className={styles.step}
62-
role="progressbar"
63-
aria-valuenow={isCurrent ? index + 1 : undefined}
64-
aria-valuemin={1}
65-
aria-valuemax={steps.length}
66-
aria-valuetext={text}
67-
aria-label={ariaLabel}
68-
>
76+
<li
77+
key={index}
78+
className={styles.listItem}
79+
style={isLastStep ? undefined : {flex: 1}}
80+
aria-current={isCurrent ? 'step' : undefined}
81+
>
82+
<div className={styles.step}>
6983
{isCompleted ? (
7084
<div
7185
className={classnames(styles.stepIconNumber, {
@@ -99,21 +113,24 @@ const Stepper = ({
99113
</Text1>
100114
</div>
101115
)}
102-
{isDesktopOrBigger && (
103-
<div className={styles.textContainer} ref={ref} aria-hidden="true">
104-
<Text2
105-
as="span"
106-
regular
107-
color={
108-
isCompleted || isCurrent
109-
? vars.colors.textPrimary
110-
: vars.colors.textSecondary
111-
}
112-
>
113-
{text}
114-
</Text2>
115-
</div>
116-
)}
116+
117+
<div className={styles.textContainer} aria-hidden="true" ref={ref}>
118+
<Text2
119+
as="div"
120+
regular
121+
color={
122+
isCompleted || isCurrent
123+
? vars.colors.textPrimary
124+
: vars.colors.textSecondary
125+
}
126+
>
127+
{text}
128+
</Text2>
129+
</div>
130+
131+
<ScreenReaderOnly>
132+
<span>{`${isCompleted ? `${completedText}: ` : isCurrent ? `${currentText}: ` : ''}${text}`}</span>
133+
</ScreenReaderOnly>
117134
</div>
118135
{!isLastStep && (
119136
<div className={styles.bar} aria-hidden="true">
@@ -128,10 +145,10 @@ const Stepper = ({
128145
)}
129146
</div>
130147
)}
131-
</React.Fragment>
148+
</li>
132149
);
133150
})}
134-
</div>
151+
</ol>
135152
);
136153
};
137154

src/text-tokens.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ export type Dictionary = {
4343
sheetConfirmButton: string;
4444
progressBarCompletedLabel: string;
4545
progressBarStepLabel: string;
46+
stepperCurrentStep: string;
47+
stepperCompletedStep: string;
4648
pinFieldInputLabel: string;
4749
counterRemoveLabel: string;
4850
counterIncreaseLabel: string;
@@ -370,6 +372,20 @@ export const progressBarStepLabel: TextToken = {
370372
pt: 'Etapa 1$s de 2$s',
371373
};
372374

375+
export const stepperCurrentStep: TextToken = {
376+
es: 'En curso',
377+
en: 'Current',
378+
de: 'Aktuell',
379+
pt: 'Em andamento',
380+
};
381+
382+
export const stepperCompletedStep: TextToken = {
383+
es: 'Completado',
384+
en: 'Completed',
385+
de: 'Abgeschlossen',
386+
pt: 'Concluído',
387+
};
388+
373389
export const pinFieldInputLabel: TextToken = {
374390
es: 'Dígito 1$s de 2$s',
375391
en: 'Digit 1$s of 2$s',

0 commit comments

Comments
 (0)