Repository: mui/material-ui (TypeScript)
Date: 2019-10-25 09:03:56+00:00
Task type: Bug Fix

Issue:
[Dialogs, accessibility] NVDA doesn't read anything after open a dialog
1. After open any dialog, NVDA doesn't read anything, it works after press tab and move focus to actionable element (note: if any actionable element exists on dialog)
2. It works when we put some element with autoFocus. but with JAWS on IE it is not work correctly.
3. If we are tabbed to focus trap (to the end of dialog) and return to item on dialog by tab, the whole dialog is vocalized again.

I tested that section "Accessibility" with Modal, not Dialog. The issue is similar.

Also I noticed that switching role="dialog" to paper component case the most issues related with reading. And before focus was going to paper, now to container, this cause the SR doesn't read anything at start.

- [x] The issue is present in the latest release.
- [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.

## Current Behavior 😯

Described above.

## Expected Behavior 🤔

1. Screen Reader should read dialog after open it.
2. Focus trap shouldn't cause that again reading whole dialog.

## Steps to Reproduce 🕹

Described above.

## Context 🔦

Dialogs are not accessible. 

## Your Environment 🌎

| Tech        | Version |
| ----------- | ------- |
| Material-UI | v4.4.0 - v4.5.1 |
| React       | 16.10.1 |
| Browser     | chrome/firefox/ie |



Issue discussion:

Solution patch:
diff --git a/docs/pages/api/dialog.md b/docs/pages/api/dialog.md
--- a/docs/pages/api/dialog.md
+++ b/docs/pages/api/dialog.md
@@ -24,6 +24,8 @@ Dialogs are overlaid modal paper based components with a backdrop.
 
 | Name | Type | Default | Description |
 |:-----|:-----|:--------|:------------|
+| <span class="prop-name">aria-describedby</span> | <span class="prop-type">string</span> |  | The id(s) of the element(s) that describe the dialog. |
+| <span class="prop-name">aria-labelledby</span> | <span class="prop-type">string</span> |  | The id(s) of the element(s) that label the dialog. |
 | <span class="prop-name required">children&nbsp;*</span> | <span class="prop-type">node</span> |  | Dialog children, usually the included sub-components. |
 | <span class="prop-name">classes</span> | <span class="prop-type">object</span> |  | Override or extend the styles applied to the component. See [CSS API](#css) below for more details. |
 | <span class="prop-name">disableBackdropClick</span> | <span class="prop-type">bool</span> | <span class="prop-default">false</span> | If `true`, clicking the backdrop will not fire the `onClose` callback. |
diff --git a/docs/src/pages/components/dialogs/ConfirmationDialog.js b/docs/src/pages/components/dialogs/ConfirmationDialog.js
--- a/docs/src/pages/components/dialogs/ConfirmationDialog.js
+++ b/docs/src/pages/components/dialogs/ConfirmationDialog.js
@@ -84,7 +84,7 @@ function ConfirmationDialogRaw(props) {
         </RadioGroup>
       </DialogContent>
       <DialogActions>
-        <Button onClick={handleCancel} color="primary">
+        <Button autoFocus onClick={handleCancel} color="primary">
           Cancel
         </Button>
         <Button onClick={handleOk} color="primary">
diff --git a/docs/src/pages/components/dialogs/ConfirmationDialog.tsx b/docs/src/pages/components/dialogs/ConfirmationDialog.tsx
--- a/docs/src/pages/components/dialogs/ConfirmationDialog.tsx
+++ b/docs/src/pages/components/dialogs/ConfirmationDialog.tsx
@@ -92,7 +92,7 @@ function ConfirmationDialogRaw(props: ConfirmationDialogRawProps) {
         </RadioGroup>
       </DialogContent>
       <DialogActions>
-        <Button onClick={handleCancel} color="primary">
+        <Button autoFocus onClick={handleCancel} color="primary">
           Cancel
         </Button>
         <Button onClick={handleOk} color="primary">
diff --git a/docs/src/pages/components/dialogs/CustomizedDialogs.js b/docs/src/pages/components/dialogs/CustomizedDialogs.js
--- a/docs/src/pages/components/dialogs/CustomizedDialogs.js
+++ b/docs/src/pages/components/dialogs/CustomizedDialogs.js
@@ -84,7 +84,7 @@ export default function CustomizedDialogs() {
           </Typography>
         </DialogContent>
         <DialogActions>
-          <Button onClick={handleClose} color="primary">
+          <Button autoFocus onClick={handleClose} color="primary">
             Save changes
           </Button>
         </DialogActions>
diff --git a/docs/src/pages/components/dialogs/CustomizedDialogs.tsx b/docs/src/pages/components/dialogs/CustomizedDialogs.tsx
--- a/docs/src/pages/components/dialogs/CustomizedDialogs.tsx
+++ b/docs/src/pages/components/dialogs/CustomizedDialogs.tsx
@@ -91,7 +91,7 @@ export default function CustomizedDialogs() {
           </Typography>
         </DialogContent>
         <DialogActions>
-          <Button onClick={handleClose} color="primary">
+          <Button autoFocus onClick={handleClose} color="primary">
             Save changes
           </Button>
         </DialogActions>
diff --git a/docs/src/pages/components/dialogs/DraggableDialog.js b/docs/src/pages/components/dialogs/DraggableDialog.js
--- a/docs/src/pages/components/dialogs/DraggableDialog.js
+++ b/docs/src/pages/components/dialogs/DraggableDialog.js
@@ -48,7 +48,7 @@ export default function DraggableDialog() {
           </DialogContentText>
         </DialogContent>
         <DialogActions>
-          <Button onClick={handleClose} color="primary">
+          <Button autoFocus onClick={handleClose} color="primary">
             Cancel
           </Button>
           <Button onClick={handleClose} color="primary">
diff --git a/docs/src/pages/components/dialogs/DraggableDialog.tsx b/docs/src/pages/components/dialogs/DraggableDialog.tsx
--- a/docs/src/pages/components/dialogs/DraggableDialog.tsx
+++ b/docs/src/pages/components/dialogs/DraggableDialog.tsx
@@ -48,7 +48,7 @@ export default function DraggableDialog() {
           </DialogContentText>
         </DialogContent>
         <DialogActions>
-          <Button onClick={handleClose} color="primary">
+          <Button autoFocus onClick={handleClose} color="primary">
             Cancel
           </Button>
           <Button onClick={handleClose} color="primary">
diff --git a/docs/src/pages/components/dialogs/FullScreenDialog.js b/docs/src/pages/components/dialogs/FullScreenDialog.js
--- a/docs/src/pages/components/dialogs/FullScreenDialog.js
+++ b/docs/src/pages/components/dialogs/FullScreenDialog.js
@@ -53,7 +53,7 @@ export default function FullScreenDialog() {
             <Typography variant="h6" className={classes.title}>
               Sound
             </Typography>
-            <Button color="inherit" onClick={handleClose}>
+            <Button autoFocus color="inherit" onClick={handleClose}>
               save
             </Button>
           </Toolbar>
diff --git a/docs/src/pages/components/dialogs/FullScreenDialog.tsx b/docs/src/pages/components/dialogs/FullScreenDialog.tsx
--- a/docs/src/pages/components/dialogs/FullScreenDialog.tsx
+++ b/docs/src/pages/components/dialogs/FullScreenDialog.tsx
@@ -56,7 +56,7 @@ export default function FullScreenDialog() {
             <Typography variant="h6" className={classes.title}>
               Sound
             </Typography>
-            <Button color="inherit" onClick={handleClose}>
+            <Button autoFocus color="inherit" onClick={handleClose}>
               save
             </Button>
           </Toolbar>
diff --git a/docs/src/pages/components/dialogs/MaxWidthDialog.js b/docs/src/pages/components/dialogs/MaxWidthDialog.js
--- a/docs/src/pages/components/dialogs/MaxWidthDialog.js
+++ b/docs/src/pages/components/dialogs/MaxWidthDialog.js
@@ -72,6 +72,7 @@ export default function MaxWidthDialog() {
             <FormControl className={classes.formControl}>
               <InputLabel htmlFor="max-width">maxWidth</InputLabel>
               <Select
+                autoFocus
                 value={maxWidth}
                 onChange={handleMaxWidthChange}
                 inputProps={{
diff --git a/docs/src/pages/components/dialogs/MaxWidthDialog.tsx b/docs/src/pages/components/dialogs/MaxWidthDialog.tsx
--- a/docs/src/pages/components/dialogs/MaxWidthDialog.tsx
+++ b/docs/src/pages/components/dialogs/MaxWidthDialog.tsx
@@ -74,6 +74,7 @@ export default function MaxWidthDialog() {
             <FormControl className={classes.formControl}>
               <InputLabel htmlFor="max-width">maxWidth</InputLabel>
               <Select
+                autoFocus
                 value={maxWidth}
                 onChange={handleMaxWidthChange}
                 inputProps={{
diff --git a/docs/src/pages/components/dialogs/ResponsiveDialog.js b/docs/src/pages/components/dialogs/ResponsiveDialog.js
--- a/docs/src/pages/components/dialogs/ResponsiveDialog.js
+++ b/docs/src/pages/components/dialogs/ResponsiveDialog.js
@@ -40,7 +40,7 @@ export default function ResponsiveDialog() {
           </DialogContentText>
         </DialogContent>
         <DialogActions>
-          <Button onClick={handleClose} color="primary">
+          <Button autoFocus onClick={handleClose} color="primary">
             Disagree
           </Button>
           <Button onClick={handleClose} color="primary" autoFocus>
diff --git a/docs/src/pages/components/dialogs/ResponsiveDialog.tsx b/docs/src/pages/components/dialogs/ResponsiveDialog.tsx
--- a/docs/src/pages/components/dialogs/ResponsiveDialog.tsx
+++ b/docs/src/pages/components/dialogs/ResponsiveDialog.tsx
@@ -40,7 +40,7 @@ export default function ResponsiveDialog() {
           </DialogContentText>
         </DialogContent>
         <DialogActions>
-          <Button onClick={handleClose} color="primary">
+          <Button autoFocus onClick={handleClose} color="primary">
             Disagree
           </Button>
           <Button onClick={handleClose} color="primary" autoFocus>
diff --git a/docs/src/pages/components/dialogs/ScrollDialog.js b/docs/src/pages/components/dialogs/ScrollDialog.js
--- a/docs/src/pages/components/dialogs/ScrollDialog.js
+++ b/docs/src/pages/components/dialogs/ScrollDialog.js
@@ -19,6 +19,14 @@ export default function ScrollDialog() {
     setOpen(false);
   };
 
+  const descriptionElementRef = React.useRef(null);
+  React.useEffect(() => {
+    const { current: descriptionElement } = descriptionElementRef;
+    if (descriptionElement !== null) {
+      descriptionElement.focus();
+    }
+  }, [open]);
+
   return (
     <div>
       <Button onClick={handleClickOpen('paper')}>scroll=paper</Button>
@@ -28,10 +36,15 @@ export default function ScrollDialog() {
         onClose={handleClose}
         scroll={scroll}
         aria-labelledby="scroll-dialog-title"
+        aria-describedby="scroll-dialog-description"
       >
         <DialogTitle id="scroll-dialog-title">Subscribe</DialogTitle>
         <DialogContent dividers={scroll === 'paper'}>
-          <DialogContentText>
+          <DialogContentText
+            id="scroll-dialog-description"
+            ref={descriptionElementRef}
+            tabIndex={-1}
+          >
             {[...new Array(50)]
               .map(
                 () => `Cras mattis consectetur purus sit amet fermentum.
diff --git a/docs/src/pages/components/dialogs/ScrollDialog.tsx b/docs/src/pages/components/dialogs/ScrollDialog.tsx
--- a/docs/src/pages/components/dialogs/ScrollDialog.tsx
+++ b/docs/src/pages/components/dialogs/ScrollDialog.tsx
@@ -19,6 +19,14 @@ export default function ScrollDialog() {
     setOpen(false);
   };
 
+  const descriptionElementRef = React.useRef<HTMLElement>(null);
+  React.useEffect(() => {
+    const { current: descriptionElement } = descriptionElementRef;
+    if (descriptionElement !== null) {
+      descriptionElement.focus();
+    }
+  }, [open]);
+
   return (
     <div>
       <Button onClick={handleClickOpen('paper')}>scroll=paper</Button>
@@ -28,10 +36,15 @@ export default function ScrollDialog() {
         onClose={handleClose}
         scroll={scroll}
         aria-labelledby="scroll-dialog-title"
+        aria-describedby="scroll-dialog-description"
       >
         <DialogTitle id="scroll-dialog-title">Subscribe</DialogTitle>
         <DialogContent dividers={scroll === 'paper'}>
-          <DialogContentText>
+          <DialogContentText
+            id="scroll-dialog-description"
+            ref={descriptionElementRef}
+            tabIndex={-1}
+          >
             {[...new Array(50)]
               .map(
                 () => `Cras mattis consectetur purus sit amet fermentum.
diff --git a/docs/src/pages/components/dialogs/SimpleDialog.js b/docs/src/pages/components/dialogs/SimpleDialog.js
--- a/docs/src/pages/components/dialogs/SimpleDialog.js
+++ b/docs/src/pages/components/dialogs/SimpleDialog.js
@@ -49,7 +49,7 @@ function SimpleDialog(props) {
           </ListItem>
         ))}
 
-        <ListItem button onClick={() => handleListItemClick('addAccount')}>
+        <ListItem autoFocus button onClick={() => handleListItemClick('addAccount')}>
           <ListItemAvatar>
             <Avatar>
               <AddIcon />
diff --git a/docs/src/pages/components/dialogs/SimpleDialog.tsx b/docs/src/pages/components/dialogs/SimpleDialog.tsx
--- a/docs/src/pages/components/dialogs/SimpleDialog.tsx
+++ b/docs/src/pages/components/dialogs/SimpleDialog.tsx
@@ -53,7 +53,7 @@ function SimpleDialog(props: SimpleDialogProps) {
             <ListItemText primary={email} />
           </ListItem>
         ))}
-        <ListItem button onClick={() => handleListItemClick('addAccount')}>
+        <ListItem autoFocus button onClick={() => handleListItemClick('addAccount')}>
           <ListItemAvatar>
             <Avatar>
               <AddIcon />
diff --git a/packages/material-ui/src/Dialog/Dialog.js b/packages/material-ui/src/Dialog/Dialog.js
--- a/packages/material-ui/src/Dialog/Dialog.js
+++ b/packages/material-ui/src/Dialog/Dialog.js
@@ -168,6 +168,8 @@ const Dialog = React.forwardRef(function Dialog(props, ref) {
     TransitionComponent = Fade,
     transitionDuration = defaultTransitionDuration,
     TransitionProps,
+    'aria-describedby': ariaDescribedby,
+    'aria-labelledby': ariaLabelledby,
     ...other
   } = props;
 
@@ -240,6 +242,8 @@ const Dialog = React.forwardRef(function Dialog(props, ref) {
           <PaperComponent
             elevation={24}
             role="dialog"
+            aria-describedby={ariaDescribedby}
+            aria-labelledby={ariaLabelledby}
             {...PaperProps}
             className={clsx(
               classes.paper,
@@ -261,6 +265,14 @@ const Dialog = React.forwardRef(function Dialog(props, ref) {
 });
 
 Dialog.propTypes = {
+  /**
+   * The id(s) of the element(s) that describe the dialog.
+   */
+  'aria-describedby': PropTypes.string,
+  /**
+   * The id(s) of the element(s) that label the dialog.
+   */
+  'aria-labelledby': PropTypes.string,
   /**
    * @ignore
    */


Test patch:
diff --git a/packages/material-ui/src/Dialog/Dialog.test.js b/packages/material-ui/src/Dialog/Dialog.test.js
--- a/packages/material-ui/src/Dialog/Dialog.test.js
+++ b/packages/material-ui/src/Dialog/Dialog.test.js
@@ -264,4 +264,20 @@ describe('<Dialog />', () => {
       expect(getByTestId('paper')).to.have.class('custom-paper-class');
     });
   });
+
+  describe('a11y', () => {
+    it('can be labelled by another element', () => {
+      const { getByRole } = render(
+        <Dialog open aria-labelledby="dialog-title">
+          <h1 id="dialog-title">Choose either one</h1>
+          <div>Actually you cant</div>
+        </Dialog>,
+      );
+
+      const dialog = getByRole('dialog');
+      expect(dialog).to.have.attr('aria-labelledby', 'dialog-title');
+      const label = document.getElementById(dialog.getAttribute('aria-labelledby'));
+      expect(label).to.have.text('Choose either one');
+    });
+  });
 });