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
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export default function LoadingButtonsTransition() {
loading={loading}
variant="outlined"
disabled
focusableWhenDisabled
>
Disabled
</Button>
Expand All @@ -42,6 +43,7 @@ export default function LoadingButtonsTransition() {
loading={loading}
loadingIndicator="Loading…"
variant="outlined"
focusableWhenDisabled
>
Fetch data
</Button>
Expand All @@ -52,6 +54,7 @@ export default function LoadingButtonsTransition() {
loading={loading}
loadingPosition="end"
variant="contained"
focusableWhenDisabled
>
Send
</Button>
Expand All @@ -63,19 +66,27 @@ export default function LoadingButtonsTransition() {
loadingPosition="start"
startIcon={<SaveIcon />}
variant="contained"
focusableWhenDisabled
>
Save
</Button>
</Box>
<Box sx={{ '& > button': { m: 1 } }}>
<Button onClick={handleClick} loading={loading} variant="outlined" disabled>
<Button
onClick={handleClick}
loading={loading}
variant="outlined"
disabled
focusableWhenDisabled
>
Disabled
</Button>
<Button
onClick={handleClick}
loading={loading}
loadingIndicator="Loading…"
variant="outlined"
focusableWhenDisabled
>
Fetch data
</Button>
Expand All @@ -85,6 +96,7 @@ export default function LoadingButtonsTransition() {
loading={loading}
loadingPosition="end"
variant="contained"
focusableWhenDisabled
>
Send
</Button>
Expand All @@ -95,6 +107,7 @@ export default function LoadingButtonsTransition() {
loadingPosition="start"
startIcon={<SaveIcon />}
variant="contained"
focusableWhenDisabled
>
Save
</Button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export default function LoadingButtonsTransition() {
loading={loading}
variant="outlined"
disabled
focusableWhenDisabled
>
Disabled
</Button>
Expand All @@ -42,6 +43,7 @@ export default function LoadingButtonsTransition() {
loading={loading}
loadingIndicator="Loading…"
variant="outlined"
focusableWhenDisabled
>
Fetch data
</Button>
Expand All @@ -52,6 +54,7 @@ export default function LoadingButtonsTransition() {
loading={loading}
loadingPosition="end"
variant="contained"
focusableWhenDisabled
>
Send
</Button>
Expand All @@ -63,19 +66,27 @@ export default function LoadingButtonsTransition() {
loadingPosition="start"
startIcon={<SaveIcon />}
variant="contained"
focusableWhenDisabled
>
Save
</Button>
</Box>
<Box sx={{ '& > button': { m: 1 } }}>
<Button onClick={handleClick} loading={loading} variant="outlined" disabled>
<Button
onClick={handleClick}
loading={loading}
variant="outlined"
disabled
focusableWhenDisabled
>
Disabled
</Button>
<Button
onClick={handleClick}
loading={loading}
loadingIndicator="Loading…"
variant="outlined"
focusableWhenDisabled
>
Fetch data
</Button>
Expand All @@ -85,6 +96,7 @@ export default function LoadingButtonsTransition() {
loading={loading}
loadingPosition="end"
variant="contained"
focusableWhenDisabled
>
Send
</Button>
Expand All @@ -95,6 +107,7 @@ export default function LoadingButtonsTransition() {
loadingPosition="start"
startIcon={<SaveIcon />}
variant="contained"
focusableWhenDisabled
>
Save
</Button>
Expand Down
6 changes: 5 additions & 1 deletion docs/data/material/components/buttons/LoadingIconButton.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ export default function LoadingIconButton() {
});
return (
<Tooltip title="Click to see loading">
<IconButton onClick={() => setLoading(true)} loading={loading}>
<IconButton
onClick={() => setLoading(true)}
loading={loading}
focusableWhenDisabled
>
<ShoppingCartIcon />
</IconButton>
</Tooltip>
Expand Down
6 changes: 5 additions & 1 deletion docs/data/material/components/buttons/LoadingIconButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ export default function LoadingIconButton() {
});
return (
<Tooltip title="Click to see loading">
<IconButton onClick={() => setLoading(true)} loading={loading}>
<IconButton
onClick={() => setLoading(true)}
loading={loading}
focusableWhenDisabled
>
<ShoppingCartIcon />
</IconButton>
</Tooltip>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
<Tooltip title="Click to see loading">
<IconButton onClick={() => setLoading(true)} loading={loading}>
<IconButton
onClick={() => setLoading(true)}
loading={loading}
focusableWhenDisabled
>
<ShoppingCartIcon />
</IconButton>
</Tooltip>
26 changes: 20 additions & 6 deletions docs/data/material/components/buttons/buttons.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ Buttons communicate actions that users can take. They are typically placed throu

{{"component": "@mui/internal-core-docs/ComponentLinkHeader"}}

## Usage guidelines

- **Keep disabled buttons discoverable**: Use `focusableWhenDisabled` to keep disabled `Button`s and `IconButton`s in the tab order. This makes unavailable actions discoverable to screen reader users while still preventing activation.

## Basic button

The `Button` comes with three variants: text (default), contained, and outlined.
Expand Down Expand Up @@ -115,6 +119,7 @@ Use `color` prop to apply theme color palette to component.
### Loading

Starting from v6.4.0, use `loading` prop to set icon buttons in a loading state and disable interactions.
For icon buttons that enter a loading state after being clicked, set `focusableWhenDisabled` to retain focus and maintain tab order while disabled.

{{"demo": "LoadingIconButton.js"}}

Expand All @@ -133,6 +138,7 @@ To create a file upload button, turn the button into a label using `component="l
## Loading

Starting from v6.4.0, use the `loading` prop to set buttons in a loading state and disable interactions.
For buttons that enter a loading state after being clicked, set `focusableWhenDisabled` to retain focus and maintain tab order while disabled.

{{"demo": "LoadingButtons.js"}}

Expand Down Expand Up @@ -191,23 +197,31 @@ Here is a [more detailed guide](/material-ui/integrations/routing/#button).

### Cursor not-allowed

The ButtonBase component sets `pointer-events: none;` on disabled buttons, which prevents the appearance of a disabled cursor.
The ButtonBase component sets `pointer-events: none;` on disabled buttons by default, which prevents the appearance of a disabled cursor.

If you wish to use `not-allowed`, you have two options:

1. **CSS only**. You can remove the pointer-events style on the disabled state of the `<button>` element:
1. **CSS only**. You can remove the pointer-events style on the disabled state:

```css
.MuiButtonBase-root:disabled {
.MuiButtonBase-root.Mui-disabled {
cursor: not-allowed;
pointer-events: auto;
}
```

However:
For disabled `Button` and `IconButton` components that need to trigger a Tooltip, use the `focusableWhenDisabled` prop instead:

```jsx
<Tooltip title="You don't have permission to do this">
<Button disabled focusableWhenDisabled>
Disabled
</Button>
</Tooltip>
```

- You should add `pointer-events: none;` back when you need to display [tooltips on disabled elements](/material-ui/react-tooltip/#disabled-elements).
- The cursor won't change if you render something other than a button element, for instance, a link `<a>` element.
The prop keeps disabled buttons focusable and hoverable while preventing activation.
Disabled links remain non-focusable and pointer-inert.

2. **DOM change**. You can wrap the button:

Expand Down
8 changes: 5 additions & 3 deletions docs/data/material/components/snackbars/snackbars.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ Note that notistack prevents Snackbars from being [closed by pressing <kbd class

## Accessibility

The user should be able to dismiss Snackbars by pressing <kbd class="key">Escape</kbd>. If there are multiple instances appearing at the same time and you want <kbd class="key">Escape</kbd> to dismiss only the oldest one that's currently open, call `event.preventDefault` in the `onClose` prop.
The user should be able to dismiss Snackbars by pressing <kbd class="key">Escape</kbd>. If there are multiple instances appearing at the same time and you want <kbd class="key">Escape</kbd> to dismiss only the oldest one that's currently open, call `event.preventDefault()` in the `onClose` prop.

```jsx
export default function MyComponent() {
Expand All @@ -133,9 +133,11 @@ export default function MyComponent() {
<Snackbar
open={open}
onClose={(event, reason) => {
// `reason === 'escapeKeyDown'` if `Escape` was pressed
setOpen(false);
// call `event.preventDefault` to only close one Snackbar at a time.
if (reason === 'escapeKeyDown') {
// Only close one Snackbar at a time.
event.preventDefault();
}
}}
/>
<Snackbar open={open} onClose={() => setOpen(false)} />
Expand Down
1 change: 1 addition & 0 deletions docs/pages/material-ui/api/button.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"disableFocusRipple": { "type": { "name": "bool" }, "default": "false" },
"disableRipple": { "type": { "name": "bool" }, "default": "false" },
"endIcon": { "type": { "name": "node" } },
"focusableWhenDisabled": { "type": { "name": "bool" }, "default": "false" },
"fullWidth": { "type": { "name": "bool" }, "default": "false" },
"href": { "type": { "name": "string" } },
"loading": { "type": { "name": "bool" }, "default": "null" },
Expand Down
1 change: 1 addition & 0 deletions docs/pages/material-ui/api/icon-button.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
},
"default": "false"
},
"focusableWhenDisabled": { "type": { "name": "bool" }, "default": "false" },
"loading": { "type": { "name": "bool" }, "default": "null" },
"loadingIndicator": {
"type": { "name": "node" },
Expand Down
3 changes: 3 additions & 0 deletions docs/translations/api-docs/button/button.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
"description": "If <code>true</code>, the ripple effect is disabled.<br>⚠️ Without a ripple there is no styling for :focus-visible by default. Be sure to highlight the element by applying separate styles with the <code>.Mui-focusVisible</code> class."
},
"endIcon": { "description": "Element placed after the children." },
"focusableWhenDisabled": {
"description": "If <code>true</code>, allows a disabled component to retain keyboard and programmatic focusability while preventing activation. Disabled links remain non-focusable."
},
"fullWidth": {
"description": "If <code>true</code>, the button will take up the full width of its container."
},
Expand Down
3 changes: 3 additions & 0 deletions docs/translations/api-docs/icon-button/icon-button.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
"edge": {
"description": "If given, uses a negative margin to counteract the padding on one side (this is often helpful for aligning the left or right side of the icon with content above or below, without ruining the border size and shape)."
},
"focusableWhenDisabled": {
"description": "If <code>true</code>, allows a disabled component to retain keyboard and programmatic focusability while preventing activation. Disabled links remain non-focusable."
},
"loading": {
"description": "If <code>true</code>, the loading indicator is visible and the button is disabled. If <code>true | false</code>, the loading wrapper is always rendered before the children to prevent <a href=\"https://github.com/mui/material-ui/issues/27853\">Google Translation Crash</a>."
},
Expand Down
6 changes: 6 additions & 0 deletions packages/mui-material/src/Button/Button.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ export interface ButtonOwnProps {
* Element placed after the children.
*/
endIcon?: React.ReactNode;
/**
* If `true`, allows a disabled component to retain keyboard and programmatic focusability while preventing activation.
* Disabled links remain non-focusable.
* @default false
*/
focusableWhenDisabled?: boolean | undefined;
/**
* If `true`, the button will take up the full width of its container.
* @default false
Expand Down
25 changes: 22 additions & 3 deletions packages/mui-material/src/Button/Button.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ const ButtonRoot = styled(ButtonBase, {
padding: '6px 16px',
border: 0,
borderRadius: (theme.vars || theme).shape.borderRadius,
'--Button-focusRingColor': (theme.vars || theme).palette.primary.main,
...getTransitionStyles(theme, ['background-color', 'box-shadow', 'border-color', 'color'], {
duration: theme.transitions.duration.short,
}),
Expand All @@ -114,6 +115,15 @@ const ButtonRoot = styled(ButtonBase, {
color: (theme.vars || theme).palette.action.disabled,
},
variants: [
{
props: { focusableWhenDisabled: true },
style: {
[`&.${buttonClasses.disabled}.${buttonClasses.focusVisible}`]: {
outline: '2px solid var(--Button-focusRingColor)',
outlineOffset: 2,
},
},
},
{
props: { variant: 'contained' },
style: {
Expand Down Expand Up @@ -174,8 +184,9 @@ const ButtonRoot = styled(ButtonBase, {
),
'--variant-containedColor': (theme.vars || theme).palette[color].contrastText,
'--variant-containedBg': (theme.vars || theme).palette[color].main,
'--Button-focusRingColor': (theme.vars || theme).palette[color].main,
'@media (hover: hover)': {
'&:hover': {
[`&:hover:not(.${buttonClasses.disabled})`]: {
'--variant-containedBg': (theme.vars || theme).palette[color].dark,
'--variant-textBg': theme.alpha(
(theme.vars || theme).palette[color].main,
Expand All @@ -200,8 +211,9 @@ const ButtonRoot = styled(ButtonBase, {
'--variant-containedBg': theme.vars
? theme.vars.palette.Button.inheritContainedBg
: inheritContainedBackgroundColor,
'--Button-focusRingColor': (theme.vars || theme).palette.text.primary,
'@media (hover: hover)': {
'&:hover': {
[`&:hover:not(.${buttonClasses.disabled})`]: {
'--variant-containedBg': theme.vars
? theme.vars.palette.Button.inheritContainedHoverBg
: inheritContainedHoverBackgroundColor,
Expand Down Expand Up @@ -509,6 +521,7 @@ const Button = React.forwardRef(function Button(inProps, ref) {
disableElevation = false,
disableFocusRipple = false,
endIcon: endIconProp,
focusableWhenDisabled = false,
focusVisibleClassName,
fullWidth = false,
id: idProp,
Expand All @@ -521,7 +534,6 @@ const Button = React.forwardRef(function Button(inProps, ref) {
variant = 'text',
...other
} = props;

const loadingId = useId(idProp);
const loadingIndicator = loadingIndicatorProp ?? (
<CircularProgress aria-labelledby={loadingId} color="inherit" size={16} />
Expand Down Expand Up @@ -597,6 +609,7 @@ const Button = React.forwardRef(function Button(inProps, ref) {
type={type}
id={loading ? loadingId : idProp}
{...other}
focusableWhenDisabled={focusableWhenDisabled === true}
classes={forwardedClasses}
>
{startIcon}
Expand Down Expand Up @@ -667,6 +680,12 @@ Button.propTypes /* remove-proptypes */ = {
* Element placed after the children.
*/
endIcon: PropTypes.node,
/**
* If `true`, allows a disabled component to retain keyboard and programmatic focusability while preventing activation.
* Disabled links remain non-focusable.
* @default false
*/
focusableWhenDisabled: PropTypes.bool,
/**
* @ignore
*/
Expand Down
Loading
Loading