Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/pages/material-ui/api/step-button.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@
"props": {
"children": { "type": { "name": "node" } },
"classes": { "type": { "name": "object" }, "additionalInfo": { "cssApi": true } },
"getAriaLabel": {
"type": { "name": "func" },
"signature": {
"type": "function(index: number, totalSteps: number) => string",
"describedArgs": ["index", "totalSteps"]
}
},
"icon": { "type": { "name": "node" } },
"optional": { "type": { "name": "node" } },
"sx": {
Expand Down
7 changes: 7 additions & 0 deletions docs/pages/material-ui/api/step.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@
"component": { "type": { "name": "elementType" } },
"disabled": { "type": { "name": "bool" } },
"expanded": { "type": { "name": "bool" }, "default": "false" },
"getAriaLabel": {
"type": { "name": "func" },
"signature": {
"type": "function(index: number, totalSteps: number) => string",
"describedArgs": ["index", "totalSteps"]
}
},
"index": { "type": { "name": "custom", "description": "integer" } },
"last": { "type": { "name": "bool" } },
"sx": {
Expand Down
7 changes: 7 additions & 0 deletions docs/pages/material-ui/api/stepper.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@
"classes": { "type": { "name": "object" }, "additionalInfo": { "cssApi": true } },
"component": { "type": { "name": "elementType" } },
"connector": { "type": { "name": "element" }, "default": "<StepConnector />" },
"getAriaLabel": {
"type": { "name": "func" },
"signature": {
"type": "function(totalSteps: number) => string",
"describedArgs": ["totalSteps"]
}
},
"nonLinear": { "type": { "name": "bool" }, "default": "false" },
"orientation": {
"type": { "name": "enum", "description": "'horizontal'<br>&#124;&nbsp;'vertical'" },
Expand Down
7 changes: 7 additions & 0 deletions docs/translations/api-docs/step-button/step-button.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@
"description": "Can be a <code>StepLabel</code> or a node to place inside <code>StepLabel</code> as children."
},
"classes": { "description": "Override or extend the styles applied to the component." },
"getAriaLabel": {
"description": "Accepts a function which returns a string value that provides a user-friendly name for the step button. This is important for screen reader users.",
"typeDescriptions": {
"index": { "name": "index", "description": "The step&#39;s index." },
"totalSteps": { "name": "totalSteps", "description": "The total number of steps." }
}
},
"icon": { "description": "The icon displayed by the step label." },
"optional": { "description": "The optional node to display." },
"sx": {
Expand Down
7 changes: 7 additions & 0 deletions docs/translations/api-docs/step/step.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@
"description": "If <code>true</code>, the step is disabled, will also disable the button if <code>StepButton</code> is a child of <code>Step</code>. Is passed to child components."
},
"expanded": { "description": "Expand the step." },
"getAriaLabel": {
"description": "Accepts a function which returns a string value that provides a user-friendly name for the step. This is important for screen reader users.",
"typeDescriptions": {
"index": { "name": "index", "description": "The step&#39;s index." },
"totalSteps": { "name": "totalSteps", "description": "The total number of steps." }
}
},
"index": {
"description": "The position of the step. The prop defaults to the value inherited from the parent Stepper component."
},
Expand Down
6 changes: 6 additions & 0 deletions docs/translations/api-docs/stepper/stepper.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@
"description": "The component used for the root node. Either a string to use a HTML element or a component."
},
"connector": { "description": "An element to be placed between each step." },
"getAriaLabel": {
"description": "Accepts a function which returns a string value that provides a user-friendly name for the stepper navigation. This is important for screen reader users when the stepper contains interactive steps.",
"typeDescriptions": {
"totalSteps": { "name": "totalSteps", "description": "The total number of steps." }
}
},
"nonLinear": {
"description": "If set the <code>Stepper</code> will not assist in controlling steps for linear flow."
},
Expand Down
8 changes: 8 additions & 0 deletions packages/mui-material/src/Step/Step.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ export interface StepOwnProps {
* @default false
*/
expanded?: boolean;
/**
* Accepts a function which returns a string value that provides a user-friendly name for the step.
* This is important for screen reader users.
* @param {number} index The step's index.
* @param {number} totalSteps The total number of steps.
* @returns {string}
*/
getAriaLabel?: (index: number, totalSteps: number) => string;
/**
* The position of the step.
* The prop defaults to the value inherited from the parent Stepper component.
Expand Down
28 changes: 24 additions & 4 deletions packages/mui-material/src/Step/Step.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,20 @@ const Step = React.forwardRef(function Step(inProps, ref) {
completed: completedProp,
disabled: disabledProp,
expanded = false,
getAriaLabel,
index,
last,
...other
} = props;

const { activeStep, connector, alternativeLabel, orientation, nonLinear } =
React.useContext(StepperContext);
const {
activeStep,
connector,
alternativeLabel,
orientation,
nonLinear,
totalSteps = 0,
} = React.useContext(StepperContext);

let [active = false, completed = false, disabled = false] = [
activeProp,
Expand All @@ -85,8 +92,8 @@ const Step = React.forwardRef(function Step(inProps, ref) {
}

const contextValue = React.useMemo(
() => ({ index, last, expanded, icon: index + 1, active, completed, disabled }),
[index, last, expanded, active, completed, disabled],
() => ({ index, last, expanded, icon: index + 1, active, completed, disabled, totalSteps }),
[index, last, expanded, active, completed, disabled, totalSteps],
);

const ownerState = {
Expand All @@ -102,12 +109,17 @@ const Step = React.forwardRef(function Step(inProps, ref) {

const classes = useUtilityClasses(ownerState);

// Only add aria-label if user explicitly provides getAriaLabel
// Otherwise, rely on visually hidden text in StepLabel to avoid redundancy
const ariaLabel = getAriaLabel ? getAriaLabel(index, totalSteps) : undefined;

const newChildren = (
<StepRoot
as={component}
className={clsx(classes.root, className)}
ref={ref}
ownerState={ownerState}
aria-label={ariaLabel}
{...other}
>
{connector && alternativeLabel && index !== 0 ? connector : null}
Expand Down Expand Up @@ -169,6 +181,14 @@ Step.propTypes /* remove-proptypes */ = {
* @default false
*/
expanded: PropTypes.bool,
/**
* Accepts a function which returns a string value that provides a user-friendly name for the step.
* This is important for screen reader users.
* @param {number} index The step's index.
* @param {number} totalSteps The total number of steps.
* @returns {string}
*/
getAriaLabel: PropTypes.func,
/**
* The position of the step.
* The prop defaults to the value inherited from the parent Stepper component.
Expand Down
41 changes: 41 additions & 0 deletions packages/mui-material/src/Step/Step.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,4 +121,45 @@ describe('<Step />', () => {
expect(stepLabels[1]).to.have.class(stepLabelClasses.disabled);
});
});

describe('accessibility', () => {
it('should not have aria-label by default (relies on StepLabel visually hidden text)', () => {
const { container } = render(
<Stepper activeStep={0}>
<Step>
<StepLabel>Step 1</StepLabel>
</Step>
<Step>
<StepLabel>Step 2</StepLabel>
</Step>
<Step>
<StepLabel>Step 3</StepLabel>
</Step>
</Stepper>,
);

const steps = container.querySelectorAll(`.${classes.root}`);
// No aria-label by default to avoid redundancy with visually hidden text
expect(steps[0]).not.to.have.attribute('aria-label');
expect(steps[1]).not.to.have.attribute('aria-label');
expect(steps[2]).not.to.have.attribute('aria-label');
});

it('should use custom getAriaLabel when provided', () => {
const { container } = render(
<Stepper activeStep={0}>
<Step getAriaLabel={(index, totalSteps) => `Item ${index + 1} of ${totalSteps}`}>
<StepLabel>First</StepLabel>
</Step>
<Step getAriaLabel={(index, totalSteps) => `Item ${index + 1} of ${totalSteps}`}>
<StepLabel>Second</StepLabel>
</Step>
</Stepper>,
);

const steps = container.querySelectorAll(`.${classes.root}`);
expect(steps[0]).to.have.attribute('aria-label', 'Item 1 of 2');
expect(steps[1]).to.have.attribute('aria-label', 'Item 2 of 2');
});
});
});
1 change: 1 addition & 0 deletions packages/mui-material/src/Step/StepContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export interface StepContextType {
active: boolean;
completed: boolean;
disabled: boolean;
totalSteps: number;
}

/**
Expand Down
8 changes: 8 additions & 0 deletions packages/mui-material/src/StepButton/StepButton.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ export interface StepButtonOwnProps {
* Override or extend the styles applied to the component.
*/
classes?: Partial<StepButtonClasses>;
/**
* Accepts a function which returns a string value that provides a user-friendly name for the step button.
* This is important for screen reader users.
* @param {number} index The step's index.
* @param {number} totalSteps The total number of steps.
* @returns {string}
*/
getAriaLabel?: (index: number, totalSteps: number) => string;
/**
* The icon displayed by the step label.
*/
Expand Down
24 changes: 22 additions & 2 deletions packages/mui-material/src/StepButton/StepButton.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,10 @@ const StepButtonRoot = styled(ButtonBase, {

const StepButton = React.forwardRef(function StepButton(inProps, ref) {
const props = useDefaultProps({ props: inProps, name: 'MuiStepButton' });
const { children, className, icon, optional, ...other } = props;
const { children, className, getAriaLabel, icon, optional, ...other } = props;

const { disabled, active } = React.useContext(StepContext);
const stepContext = React.useContext(StepContext);
const { disabled, active, index, totalSteps = 0 } = stepContext;
const { orientation } = React.useContext(StepperContext);

const ownerState = { ...props, orientation };
Expand All @@ -77,6 +78,14 @@ const StepButton = React.forwardRef(function StepButton(inProps, ref) {
<StepLabel {...childProps}>{children}</StepLabel>
);

// Add aria-label with step position
let ariaLabel;
if (getAriaLabel) {
ariaLabel = getAriaLabel(index, totalSteps);
} else if (totalSteps > 0 && index !== undefined) {
ariaLabel = `Step ${index + 1} of ${totalSteps}`;
}

return (
<StepButtonRoot
focusRipple
Expand All @@ -86,6 +95,7 @@ const StepButton = React.forwardRef(function StepButton(inProps, ref) {
ref={ref}
ownerState={ownerState}
aria-current={active ? 'step' : undefined}
aria-label={ariaLabel}
{...other}
>
{child}
Expand All @@ -110,6 +120,14 @@ StepButton.propTypes /* remove-proptypes */ = {
* @ignore
*/
className: PropTypes.string,
/**
* Accepts a function which returns a string value that provides a user-friendly name for the step button.
* This is important for screen reader users.
* @param {number} index The step's index.
* @param {number} totalSteps The total number of steps.
* @returns {string}
*/
getAriaLabel: PropTypes.func,
/**
* The icon displayed by the step label.
*/
Expand All @@ -128,4 +146,6 @@ StepButton.propTypes /* remove-proptypes */ = {
]),
};

StepButton.muiName = 'StepButton';

export default StepButton;
66 changes: 66 additions & 0 deletions packages/mui-material/src/StepButton/StepButton.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { spy } from 'sinon';
import { createRenderer, screen, fireEvent, supportsTouch } from '@mui/internal-test-utils';
import StepButton, { stepButtonClasses as classes } from '@mui/material/StepButton';
import Step from '@mui/material/Step';
import Stepper from '@mui/material/Stepper';
import StepLabel, { stepLabelClasses } from '@mui/material/StepLabel';
import ButtonBase from '@mui/material/ButtonBase';
import describeConformance from '../../test/describeConformance';
Expand Down Expand Up @@ -143,4 +144,69 @@ describe('<StepButton />', () => {

expect(screen.getByRole('button')).not.to.equal(null);
});

describe('accessibility', () => {
it('should have aria-label with step position', () => {
render(
<Stepper activeStep={0}>
<Step>
<StepButton>Step 1</StepButton>
</Step>
<Step>
<StepButton>Step 2</StepButton>
</Step>
<Step>
<StepButton>Step 3</StepButton>
</Step>
</Stepper>,
);

const buttons = screen.getAllByRole('button');
expect(buttons[0]).to.have.attribute('aria-label', 'Step 1 of 3');
expect(buttons[1]).to.have.attribute('aria-label', 'Step 2 of 3');
expect(buttons[2]).to.have.attribute('aria-label', 'Step 3 of 3');
});

it('should use custom getAriaLabel', () => {
render(
<Stepper activeStep={0}>
<Step>
<StepButton
getAriaLabel={(index, totalSteps) => `Go to step ${index + 1} of ${totalSteps}`}
>
First
</StepButton>
</Step>
<Step>
<StepButton
getAriaLabel={(index, totalSteps) => `Go to step ${index + 1} of ${totalSteps}`}
>
Second
</StepButton>
</Step>
</Stepper>,
);

const buttons = screen.getAllByRole('button');
expect(buttons[0]).to.have.attribute('aria-label', 'Go to step 1 of 2');
expect(buttons[1]).to.have.attribute('aria-label', 'Go to step 2 of 2');
});

it('should have aria-current="step" on active button', () => {
render(
<Stepper activeStep={1}>
<Step>
<StepButton>Step 1</StepButton>
</Step>
<Step>
<StepButton>Step 2</StepButton>
</Step>
</Stepper>,
);

const buttons = screen.getAllByRole('button');
expect(buttons[0]).not.to.have.attribute('aria-current');
expect(buttons[1]).to.have.attribute('aria-current', 'step');
});
});
});
7 changes: 7 additions & 0 deletions packages/mui-material/src/Stepper/Stepper.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ export interface StepperOwnProps extends Pick<PaperProps, 'elevation' | 'square'
* @default false
*/
alternativeLabel?: boolean;
/**
* Accepts a function which returns a string value that provides a user-friendly name for the stepper navigation.
* This is important for screen reader users when the stepper contains interactive steps.
* @param {number} totalSteps The total number of steps.
* @returns {string}
*/
getAriaLabel?: (totalSteps: number) => string;
/**
* Two or more `<Step />` components.
*/
Expand Down
Loading
Loading