Repository: mui/material-ui (TypeScript)
Date: 2019-06-27 13:17:37+00:00
Task type: Bug Fix

Issue:
[ButtonBase] Improve Button accessibility
Revert #10339 removed safeguard and add additional one so anchors missing href's are treated as buttons.

<!--
    Thank you very much for contributing to Material-UI by creating an issue! ❤️
    To avoid duplicate issues we ask you to check off the following list.
-->

<!-- Checked checkbox should look like this: [x] -->
- [x] This is not a v0.x issue. <!-- (v0.x is no longer maintained) -->
- [x] I have searched the [issues](https://github.com/mui-org/material-ui/issues) of this repository and believe that this is not a duplicate.

## Expected Behavior 🤔
An anchor with an href should not be assigned `role=button`.

## Current Behavior 😯

An anchor with prop `button` w/ href is being given role of button as it misleads users. A button does not alter the url path while an href does. 

## Steps to Reproduce 🕹
```
<ListItem component={Link} to={path} button>
Hi there
</ListItem>
```
## Context 🔦
Screen reader



Issue discussion:
@bluSCALE4 Do you have a specific fix in mind? I think that we can apply the following but it won't help your case:
```diff
--- a/packages/material-ui/src/ButtonBase/ButtonBase.js
+++ b/packages/material-ui/src/ButtonBase/ButtonBase.js
@@ -287,7 +287,10 @@ class ButtonBase extends React.Component {
       buttonProps.type = type;
       buttonProps.disabled = disabled;
     } else {
-      buttonProps.role = 'button';
+      if (!(ComponentProp === 'a' && other.href)) {
+        buttonProps.role = 'button';
+      }
+
       buttonProps['aria-disabled'] = disabled;
```
That looks good. It should solve the original ask. But you're right, my use case is different.
The alternative would be to not provide a `role` prop if we don't know the actual DOM node rendered. We could defer the responsibility to the component the user is providing. Yeah, it could work. I like this heuristic cc @eps1lon.
I don't understand this component anymore. Why should the ButtonBase not be a button? 
@bluSCALE4 Have you considered not using the button prop of the list? 
It's just we extend and extend this heuristic and I don't think this will ever be a good one. @bluSCALE4 Can you share some resources why anchors shouldn't have the button role?
One potential point of confusion with the current behavior, is that the [button role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/button_role) indicates that the user should be able to use `space` or `enter` to trigger its functionality. In the case of a link, `enter` still works, but `space` does not.

> Why should the ButtonBase not be a button?

The use of `ButtonBase` with an `a` tag is primarily for aesthetic reasons (to make it **look** like a button). The role would ideally convey whether or not it behaves like a button in response to keyboard actions. We don't include the role when `ComponentProp === 'button'` because the element type already conveys what it is. Similarly when `ComponentProp === 'a'` and `href` is specified, the element type already conveys that it is a link, so no role is necessary to communicate how the element will behave.

Unfortunately, what we really want to drive off of is [`button.tagName`](https://github.com/mui-org/material-ui/blob/next/packages/material-ui/src/ButtonBase/ButtonBase.js#L195), but we can't know that reliably until after rendering. The most typical use cases are going to be like the one in the initial issue comment: `component={Link}`, where `ComponentProp` will not be `'a'` but `button.tagName` would be `'A'`.

Developers can use `role={null}` (e.g. `<ListItem component={Link} to={path} button role={null}>`) to control this themselves.
@ryancogswell So, you are leaning toward `role={null}` userland and 
```diff
--- a/packages/material-ui/src/ButtonBase/ButtonBase.js
+++ b/packages/material-ui/src/ButtonBase/ButtonBase.js
@@ -287,7 +287,9 @@ class ButtonBase extends React.Component {
       buttonProps.type = type;
       buttonProps.disabled = disabled;
     } else {
-      buttonProps.role = 'button';
+      if (!(ComponentProp === 'a' && other.href)) {
+        buttonProps.role = 'button';
+      }
       buttonProps['aria-disabled'] = disabled;
     }
```

instead of?

```diff
--- a/packages/material-ui/src/ButtonBase/ButtonBase.js
+++ b/packages/material-ui/src/ButtonBase/ButtonBase.js
@@ -287,7 +287,9 @@ class ButtonBase extends React.Component {
       buttonProps.type = type;
       buttonProps.disabled = disabled;
     } else {
-      buttonProps.role = 'button';
+      if (typeof ComponentProp === 'string' && !(ComponentProp === 'a' && other.href)) {
+        buttonProps.role = 'button';
+      }
       buttonProps['aria-disabled'] = disabled;
     }
```
@oliviertassinari The `typeof ComponentProp === 'string'` condition is interesting. If ComponentProp is not a 'string', then we really don't know what the role **should** be. I'm not sure what is worse, including `role=button` when we shouldn't or potentially dropping it when it should be there. We'd be trading the former for the latter with the `typeof ComponentProp === 'string'` condition, but it would be more intuitive (and more straightforward to document) to explicitly add `role="button"` to add the role than to add `role={null}` to suppress it.

I suspect that when ComponentProp is not a string, that it is more often a link than something that should receive the button role, but I really don't know if that is true (but it is true in my own codebase).

When I experimented with `role={null}` in a [CodeSandbox](https://codesandbox.io/s/l51mlw899q), it worked, but the eslint rules in the sandbox complained about it: "Elements with ARIA roles must use a valid, non-abstract ARIA role. (jsx-a11y/aria-role)".

So I like your `(typeof ComponentProp === 'string' && !(ComponentProp === 'a' && other.href))` proposal, but I'm not sure how many people it might have adverse effects on.

As a side note, I need to read up on how to do those nifty diff comments.
@ryancogswell This synthesis my reflection. I agree with every word.
I'm *slightly* more in favor of the `typeof ComponentProp === 'string'` path. Still, what should we do 🤔? 
@oliviertassinari Given that:

- the potential adverse effect is pretty minimal (just losing `role="button"`)
- the adverse effect is easy to remediate by adding `role="button"` as a prop
- I'm not aware of any common pattern that would use a non-string `component` prop for something that **should** receive the `button` role
- the `Link` component pattern is quite common

I would opt to go ahead and use your suggested:

```diff
--- a/packages/material-ui/src/ButtonBase/ButtonBase.js
+++ b/packages/material-ui/src/ButtonBase/ButtonBase.js
@@ -287,7 +287,9 @@ class ButtonBase extends React.Component {
       buttonProps.type = type;
       buttonProps.disabled = disabled;
     } else {
-      buttonProps.role = 'button';
+      if (typeof ComponentProp === 'string' && !(ComponentProp === 'a' && other.href)) {
+        buttonProps.role = 'button';
+      }
       buttonProps['aria-disabled'] = disabled;
     }
```
@bluSCALE4 Do you want to submit a pull request? :)
Well then, thank you both for the feedback. I'll try to get this in before
the end of the week.

On Mon, Apr 29, 2019, 4:13 PM Olivier Tassinari <notifications@github.com>
wrote:

> @bluSCALE4 <https://github.com/bluSCALE4> Do you want to submit a pull
> request? :)
>
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub
> <https://github.com/mui-org/material-ui/issues/15512#issuecomment-487745771>,
> or mute the thread
> <https://github.com/notifications/unsubscribe-auth/AABQ526PY5JKX3CMXZZKM33PS5QGNANCNFSM4HI5Y2SQ>
> .
>

I came across something that is in a way related. One of Google's skip to links (the hidden links you see when you tab into the app) doesn't have an href but instead uses role="link". It functions like a link but doesn't include an href. I'm questioning being dogmatic on href and thinking of adding yet another check for role link. Thoughts?
@bluSCALE4 Any role explicitly specified by the user (including `null` or `link`) will win without any additional logic. A `role` specified by the user will be part of `...other` and `...other` will win over `...buttonProps` where any overlap occurs due to the order that they are spread onto the component:

```
        {...buttonProps}
        {...other}
      >
```
Here is a modification of the [Third-party routing library demo](https://next.material-ui.com/demos/buttons/#third-party-routing-library) showing this: https://codesandbox.io/s/xo31norvxq

Solution patch:
diff --git a/docs/src/modules/components/Link.js b/docs/src/modules/components/Link.js
--- a/docs/src/modules/components/Link.js
+++ b/docs/src/modules/components/Link.js
@@ -32,6 +32,7 @@ function Link(props) {
     className: classNameProps,
     innerRef,
     naked,
+    role: roleProp,
     router,
     userLanguage,
     ...other
@@ -45,11 +46,16 @@ function Link(props) {
     other.as = `/${userLanguage}${other.href}`;
   }
 
+  // catch role passed from ButtonBase. This is definitely a link
+  const role = roleProp === 'button' ? undefined : roleProp;
+
   if (naked) {
-    return <NextComposed className={className} ref={innerRef} {...other} />;
+    return <NextComposed className={className} ref={innerRef} role={role} {...other} />;
   }
 
-  return <MuiLink component={NextComposed} className={className} ref={innerRef} {...other} />;
+  return (
+    <MuiLink component={NextComposed} className={className} ref={innerRef} role={role} {...other} />
+  );
 }
 
 Link.propTypes = {
@@ -61,6 +67,7 @@ Link.propTypes = {
   naked: PropTypes.bool,
   onClick: PropTypes.func,
   prefetch: PropTypes.bool,
+  role: PropTypes.string,
   router: PropTypes.shape({
     pathname: PropTypes.string.isRequired,
   }).isRequired,
diff --git a/packages/material-ui/src/ButtonBase/ButtonBase.js b/packages/material-ui/src/ButtonBase/ButtonBase.js
--- a/packages/material-ui/src/ButtonBase/ButtonBase.js
+++ b/packages/material-ui/src/ButtonBase/ButtonBase.js
@@ -268,7 +268,9 @@ const ButtonBase = React.forwardRef(function ButtonBase(props, ref) {
     buttonProps.type = type;
     buttonProps.disabled = disabled;
   } else {
-    buttonProps.role = 'button';
+    if (ComponentProp !== 'a' || !other.href) {
+      buttonProps.role = 'button';
+    }
     buttonProps['aria-disabled'] = disabled;
   }
 


Test patch:
diff --git a/packages/material-ui/src/ButtonBase/ButtonBase.test.js b/packages/material-ui/src/ButtonBase/ButtonBase.test.js
--- a/packages/material-ui/src/ButtonBase/ButtonBase.test.js
+++ b/packages/material-ui/src/ButtonBase/ButtonBase.test.js
@@ -104,15 +104,16 @@ describe('<ButtonBase />', () => {
       expect(button).to.not.have.attribute('type');
     });
 
-    it('should automatically change the button to an a element when href is provided', () => {
-      const { getByRole } = render(<ButtonBase href="https://google.com">Hello</ButtonBase>);
-      const button = getByRole('button');
+    it('should automatically change the button to an anchor element when href is provided', () => {
+      const { getByText } = render(<ButtonBase href="https://google.com">Hello</ButtonBase>);
+      const button = getByText('Hello');
 
       expect(button).to.have.property('nodeName', 'A');
+      expect(button).not.to.have.attribute('role');
       expect(button).to.have.attribute('href', 'https://google.com');
     });
 
-    it('should change the button type to a and set role="button"', () => {
+    it('applies role="button" when an anchor is used without href', () => {
       const { getByRole } = render(<ButtonBase component="a">Hello</ButtonBase>);
       const button = getByRole('button');
 
@@ -120,7 +121,7 @@ describe('<ButtonBase />', () => {
       expect(button).not.to.have.attribute('type');
     });
 
-    it('should not change the button to an a element', () => {
+    it('should not use an anchor element if explicit component and href is passed', () => {
       const { getByRole } = render(
         // @ts-ignore
         <ButtonBase component="span" href="https://google.com">
@@ -601,12 +602,12 @@ describe('<ButtonBase />', () => {
       it('should ignore anchors with href', () => {
         const onClick = spy();
         const onKeyDown = spy(event => event.defaultPrevented);
-        const { getByRole } = render(
+        const { getByText } = render(
           <ButtonBase component="a" href="href" onClick={onClick} onKeyDown={onKeyDown}>
             Hello
           </ButtonBase>,
         );
-        const button = getByRole('button');
+        const button = getByText('Hello');
         button.focus();
         fireEvent.keyDown(document.activeElement || document.body, {
           key: 'Enter',