Repository: mui/material-ui (TypeScript)
Date: 2018-10-27 12:24:16+00:00
Task type: Bug Fix

Issue:
Unable to interact with scrollbar when Dialog scroll='body'
<!--- Provide a general summary of the issue in the Title above -->
When using a Dialog with scroll='body', the only way to scroll is to use the Page Up/Down or Up/Down keys. Clicking or dragging the scrollbar causes the dialog to close. 

<!--
    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 a v1.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
<!---
    Describe what should happen.
-->
Clicking or dragging the scrollbar should cause the browser content to scroll.

## Current Behavior
<!---
    Describe what happens instead of the expected behavior.
-->
Clicking or dragging the scrollbar causes the dialog to close.

## Steps to Reproduce
<!---
    Provide a link to a live example (you can use codesandbox.io) and an unambiguous set of steps to reproduce this bug.
    Include code to reproduce, if relevant (which it most likely is).

    This codesandbox.io template _may_ be a good starting point:
    https://codesandbox.io/s/github/mui-org/material-ui/tree/master/examples/create-react-app


    If YOU DO NOT take time to provide a codesandbox.io reproduction, should the COMMUNITY take time to help you?

-->
Link: https://material-ui.com/demos/dialogs/#scrolling-long-content

1. Select SCROLL=BODY
2. Click or drag the scrollbar

## Your Environment
<!--- Include as many relevant details about the environment with which you experienced the bug. -->

| Tech         | Version |
|--------------|---------|
| Material-UI  | v1.4.3  |
| React        |   The version used in the material-ui.com demo page      |
| browser      |  Chrome Version 67.0.3396.99 (Official Build) (64-bit)   |
| OS         |  Windows 7 64-bit   |



Issue discussion:
Note: using `disableBackdropClick` does not work either :/

Another problem with using "scroll=body" is that you'll get scrollbar flickering when animating the dialog using an transition (eg. slideup)

Solution patch:
diff --git a/BACKERS.md b/BACKERS.md
--- a/BACKERS.md
+++ b/BACKERS.md
@@ -68,7 +68,7 @@ via [Patreon](https://www.patreon.com/oliviertassinari)
 | Avétis KAZARIAN | Withinpixels | SIM KIM SIA | Renaud Bompuis | Yaron Malin |
 | Arvanitis Panagiotis | Jesse Weigel | Bogdan Mihai Nicolae | Dung Tran | Kyle Pennell |
 | Kai Mit Pansen | Eric Nagy | Karens Grigorjancs | Mohamed Turco | Haroun Serang |
-| Antonio Seveso | Ali Akhavan | Bruno Winck |
+| Antonio Seveso | Ali Akhavan | Bruno Winck | Alessandro Annini |
 
 via [OpenCollective](https://opencollective.com/material-ui)
 
diff --git a/docs/src/modules/components/Ad.js b/docs/src/modules/components/Ad.js
--- a/docs/src/modules/components/Ad.js
+++ b/docs/src/modules/components/Ad.js
@@ -27,18 +27,21 @@ const styles = theme => ({
   },
   paper: {
     padding: theme.spacing.unit,
+    display: 'block',
   },
 });
 
 function getAdblock(classes) {
   return (
-    <Paper elevation={0} className={classes.paper}>
-      <Typography gutterBottom>Like Material-UI?</Typography>
-      <Typography gutterBottom>
+    <Paper component="span" elevation={0} className={classes.paper}>
+      <Typography component="span" gutterBottom>
+        Like Material-UI?
+      </Typography>
+      <Typography component="span" gutterBottom>
         {`If you don't mind tech-related ads, and want to support Open Source,
             please whitelist Material-UI in your ad blocker.`}
       </Typography>
-      <Typography>
+      <Typography component="span">
         Thank you!{' '}
         <span role="img" aria-label="Love">
           ❤️
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
@@ -1,3 +1,5 @@
+/* eslint-disable jsx-a11y/click-events-have-key-events */
+/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
 // @inheritedComponent Modal
 
 import React from 'react';
@@ -24,6 +26,12 @@ export const styles = theme => ({
     overflowY: 'auto',
     overflowX: 'hidden',
   },
+  /* Styles applied to the container element. */
+  container: {
+    height: '100%',
+    // We disable the focus ring for mouse, touch and keyboard users.
+    outline: 'none',
+  },
   /* Styles applied to the `Paper` component. */
   paper: {
     display: 'flex',
@@ -31,8 +39,6 @@ export const styles = theme => ({
     margin: 48,
     position: 'relative',
     overflowY: 'auto', // Fix IE 11 issue, to remove at some point.
-    // We disable the focus ring for mouse, touch and keyboard users.
-    outline: 'none',
   },
   /* Styles applied to the `Paper` component if `scroll="paper"`. */
   paperScrollPaper: {
@@ -100,77 +106,99 @@ export const styles = theme => ({
 /**
  * Dialogs are overlaid modal paper based components with a backdrop.
  */
-function Dialog(props) {
-  const {
-    BackdropProps,
-    children,
-    classes,
-    className,
-    disableBackdropClick,
-    disableEscapeKeyDown,
-    fullScreen,
-    fullWidth,
-    maxWidth,
-    onBackdropClick,
-    onClose,
-    onEnter,
-    onEntered,
-    onEntering,
-    onEscapeKeyDown,
-    onExit,
-    onExited,
-    onExiting,
-    open,
-    PaperProps,
-    scroll,
-    TransitionComponent,
-    transitionDuration,
-    TransitionProps,
-    ...other
-  } = props;
+class Dialog extends React.Component {
+  handleBackdropClick = event => {
+    if (event.target !== event.currentTarget) {
+      return;
+    }
+
+    if (this.props.onBackdropClick) {
+      this.props.onBackdropClick(event);
+    }
+
+    if (!this.props.disableBackdropClick && this.props.onClose) {
+      this.props.onClose(event, 'backdropClick');
+    }
+  };
+
+  render() {
+    const {
+      BackdropProps,
+      children,
+      classes,
+      className,
+      disableBackdropClick,
+      disableEscapeKeyDown,
+      fullScreen,
+      fullWidth,
+      maxWidth,
+      onBackdropClick,
+      onClose,
+      onEnter,
+      onEntered,
+      onEntering,
+      onEscapeKeyDown,
+      onExit,
+      onExited,
+      onExiting,
+      open,
+      PaperProps,
+      scroll,
+      TransitionComponent,
+      transitionDuration,
+      TransitionProps,
+      ...other
+    } = this.props;
 
-  return (
-    <Modal
-      className={classNames(classes.root, classes[`scroll${capitalize(scroll)}`], className)}
-      BackdropProps={{
-        transitionDuration,
-        ...BackdropProps,
-      }}
-      disableBackdropClick={disableBackdropClick}
-      disableEscapeKeyDown={disableEscapeKeyDown}
-      onBackdropClick={onBackdropClick}
-      onEscapeKeyDown={onEscapeKeyDown}
-      onClose={onClose}
-      open={open}
-      role="dialog"
-      {...other}
-    >
-      <TransitionComponent
-        appear
-        in={open}
-        timeout={transitionDuration}
-        onEnter={onEnter}
-        onEntering={onEntering}
-        onEntered={onEntered}
-        onExit={onExit}
-        onExiting={onExiting}
-        onExited={onExited}
-        {...TransitionProps}
+    return (
+      <Modal
+        className={classNames(classes.root, className)}
+        BackdropProps={{
+          transitionDuration,
+          ...BackdropProps,
+        }}
+        disableBackdropClick={disableBackdropClick}
+        disableEscapeKeyDown={disableEscapeKeyDown}
+        onBackdropClick={onBackdropClick}
+        onEscapeKeyDown={onEscapeKeyDown}
+        onClose={onClose}
+        open={open}
+        role="dialog"
+        {...other}
       >
-        <Paper
-          elevation={24}
-          className={classNames(classes.paper, classes[`paperScroll${capitalize(scroll)}`], {
-            [classes[`paperWidth${maxWidth ? capitalize(maxWidth) : ''}`]]: maxWidth,
-            [classes.paperFullScreen]: fullScreen,
-            [classes.paperFullWidth]: fullWidth,
-          })}
-          {...PaperProps}
+        <TransitionComponent
+          appear
+          in={open}
+          timeout={transitionDuration}
+          onEnter={onEnter}
+          onEntering={onEntering}
+          onEntered={onEntered}
+          onExit={onExit}
+          onExiting={onExiting}
+          onExited={onExited}
+          {...TransitionProps}
         >
-          {children}
-        </Paper>
-      </TransitionComponent>
-    </Modal>
-  );
+          <div
+            className={classNames(classes.container, classes[`scroll${capitalize(scroll)}`])}
+            onClick={this.handleBackdropClick}
+            role="document"
+          >
+            <Paper
+              elevation={24}
+              className={classNames(classes.paper, classes[`paperScroll${capitalize(scroll)}`], {
+                [classes[`paperWidth${maxWidth ? capitalize(maxWidth) : ''}`]]: maxWidth,
+                [classes.paperFullScreen]: fullScreen,
+                [classes.paperFullWidth]: fullWidth,
+              })}
+              {...PaperProps}
+            >
+              {children}
+            </Paper>
+          </div>
+        </TransitionComponent>
+      </Modal>
+    );
+  }
 }
 
 Dialog.propTypes = {
diff --git a/pages/api/dialog.md b/pages/api/dialog.md
--- a/pages/api/dialog.md
+++ b/pages/api/dialog.md
@@ -55,6 +55,7 @@ This property accepts the following keys:
 | <span class="prop-name">root</span> | Styles applied to the root element.
 | <span class="prop-name">scrollPaper</span> | Styles applied to the root element if `scroll="paper"`.
 | <span class="prop-name">scrollBody</span> | Styles applied to the root element if `scroll="body"`.
+| <span class="prop-name">container</span> | Styles applied to the container element.
 | <span class="prop-name">paper</span> | Styles applied to the `Paper` component.
 | <span class="prop-name">paperScrollPaper</span> | Styles applied to the `Paper` component if `scroll="paper"`.
 | <span class="prop-name">paperScrollBody</span> | Styles applied to the `Paper` component if `scroll="body"`.


Test patch:
diff --git a/packages/material-ui-lab/src/SpeedDial/SpeedDial.test.js b/packages/material-ui-lab/src/SpeedDial/SpeedDial.test.js
--- a/packages/material-ui-lab/src/SpeedDial/SpeedDial.test.js
+++ b/packages/material-ui-lab/src/SpeedDial/SpeedDial.test.js
@@ -277,7 +277,7 @@ describe('<SpeedDial />', () => {
 
     it('displays the actions on focus gain', () => {
       resetDialToOpen();
-      assert.strictEqual(wrapper.prop('open'), true);
+      assert.strictEqual(wrapper.props().open, true);
     });
 
     describe('first item selection', () => {
diff --git a/packages/material-ui/src/Card/Card.test.js b/packages/material-ui/src/Card/Card.test.js
--- a/packages/material-ui/src/Card/Card.test.js
+++ b/packages/material-ui/src/Card/Card.test.js
@@ -32,6 +32,6 @@ describe('<Card />', () => {
 
   it('should spread custom props on the root node', () => {
     const wrapper = shallow(<Card data-my-prop="woofCard" />);
-    assert.strictEqual(wrapper.prop('data-my-prop'), 'woofCard', 'custom prop should be woofCard');
+    assert.strictEqual(wrapper.props()['data-my-prop'], 'woofCard');
   });
 });
diff --git a/packages/material-ui/src/CardMedia/CardMedia.test.js b/packages/material-ui/src/CardMedia/CardMedia.test.js
--- a/packages/material-ui/src/CardMedia/CardMedia.test.js
+++ b/packages/material-ui/src/CardMedia/CardMedia.test.js
@@ -20,7 +20,7 @@ describe('<CardMedia />', () => {
 
   it('should have the backgroundImage specified', () => {
     const wrapper = shallow(<CardMedia image="/foo.jpg" />);
-    assert.strictEqual(wrapper.prop('style').backgroundImage, 'url("/foo.jpg")');
+    assert.strictEqual(wrapper.props().style.backgroundImage, 'url("/foo.jpg")');
   });
 
   it('should spread custom props on the root node', () => {
@@ -34,15 +34,15 @@ describe('<CardMedia />', () => {
 
   it('should have backgroundImage specified even though custom styles got passed', () => {
     const wrapper = shallow(<CardMedia image="/foo.jpg" style={{ height: 200 }} />);
-    assert.strictEqual(wrapper.prop('style').backgroundImage, 'url("/foo.jpg")');
-    assert.strictEqual(wrapper.prop('style').height, 200);
+    assert.strictEqual(wrapper.props().style.backgroundImage, 'url("/foo.jpg")');
+    assert.strictEqual(wrapper.props().style.height, 200);
   });
 
   it('should be possible to overwrite backgroundImage via custom styles', () => {
     const wrapper = shallow(
       <CardMedia image="/foo.jpg" style={{ backgroundImage: 'url(/bar.jpg)' }} />,
     );
-    assert.strictEqual(wrapper.prop('style').backgroundImage, 'url(/bar.jpg)');
+    assert.strictEqual(wrapper.props().style.backgroundImage, 'url(/bar.jpg)');
   });
 
   describe('prop: component', () => {
@@ -58,7 +58,7 @@ describe('<CardMedia />', () => {
 
     it('should not have default inline style when media component specified', () => {
       const wrapper = shallow(<CardMedia src="/foo.jpg" component="picture" />);
-      assert.strictEqual(wrapper.prop('style'), undefined);
+      assert.strictEqual(wrapper.props().style, undefined);
     });
 
     it('should not have `src` prop if not media component specified', () => {
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
@@ -1,5 +1,6 @@
 import React from 'react';
 import { assert } from 'chai';
+import { spy } from 'sinon';
 import { createShallow, getClasses } from '../test-utils';
 import Paper from '../Paper';
 import Fade from '../Fade';
@@ -77,14 +78,17 @@ describe('<Dialog />', () => {
     assert.strictEqual(wrapper.hasClass('woofDialog'), true);
   });
 
-  it('should render Fade > Paper > children inside the Modal', () => {
+  it('should render Fade > div > Paper > children inside the Modal', () => {
     const children = <p>Hello</p>;
     const wrapper = shallow(<Dialog {...defaultProps}>{children}</Dialog>);
 
     const fade = wrapper.childAt(0);
     assert.strictEqual(fade.type(), Fade);
 
-    const paper = fade.childAt(0);
+    const div = fade.childAt(0);
+    assert.strictEqual(div.type(), 'div');
+
+    const paper = div.childAt(0);
     assert.strictEqual(paper.length === 1 && paper.type(), Paper);
 
     assert.strictEqual(paper.hasClass(classes.paper), true);
@@ -107,6 +111,71 @@ describe('<Dialog />', () => {
     assert.strictEqual(wrapper.find(Fade).props().appear, true);
   });
 
+  describe('backdrop', () => {
+    let wrapper;
+
+    beforeEach(() => {
+      wrapper = shallow(<Dialog open>foo</Dialog>);
+    });
+
+    it('should attach a handler to the backdrop that fires onClose', () => {
+      const onClose = spy();
+      wrapper.setProps({ onClose });
+
+      const handler = wrapper.instance().handleBackdropClick;
+      const backdrop = wrapper.find('div');
+      assert.strictEqual(
+        backdrop.props().onClick,
+        handler,
+        'should attach the handleBackdropClick handler',
+      );
+
+      handler({});
+      assert.strictEqual(onClose.callCount, 1, 'should fire the onClose callback');
+    });
+
+    it('should let the user disable backdrop click triggering onClose', () => {
+      const onClose = spy();
+      wrapper.setProps({ onClose, disableBackdropClick: true });
+
+      const handler = wrapper.instance().handleBackdropClick;
+
+      handler({});
+      assert.strictEqual(onClose.callCount, 0, 'should not fire the onClose callback');
+    });
+
+    it('should call through to the user specified onBackdropClick callback', () => {
+      const onBackdropClick = spy();
+      wrapper.setProps({ onBackdropClick });
+
+      const handler = wrapper.instance().handleBackdropClick;
+
+      handler({});
+      assert.strictEqual(onBackdropClick.callCount, 1, 'should fire the onBackdropClick callback');
+    });
+
+    it('should ignore the backdrop click if the event did not come from the backdrop', () => {
+      const onBackdropClick = spy();
+      wrapper.setProps({ onBackdropClick });
+
+      const handler = wrapper.instance().handleBackdropClick;
+
+      handler({
+        target: {
+          /* a dom node */
+        },
+        currentTarget: {
+          /* another dom node */
+        },
+      });
+      assert.strictEqual(
+        onBackdropClick.callCount,
+        0,
+        'should not fire the onBackdropClick callback',
+      );
+    });
+  });
+
   describe('prop: classes', () => {
     it('should add the class on the Paper element', () => {
       const className = 'foo';
diff --git a/packages/material-ui/src/DialogContent/DialogContent.test.js b/packages/material-ui/src/DialogContent/DialogContent.test.js
--- a/packages/material-ui/src/DialogContent/DialogContent.test.js
+++ b/packages/material-ui/src/DialogContent/DialogContent.test.js
@@ -20,7 +20,7 @@ describe('<DialogContent />', () => {
   it('should spread custom props on the root node', () => {
     const wrapper = shallow(<DialogContent data-my-prop="woofDialogContent" />);
     assert.strictEqual(
-      wrapper.prop('data-my-prop'),
+      wrapper.props()['data-my-prop'],
       'woofDialogContent',
       'custom prop should be woofDialogContent',
     );
diff --git a/packages/material-ui/src/DialogTitle/DialogTitle.test.js b/packages/material-ui/src/DialogTitle/DialogTitle.test.js
--- a/packages/material-ui/src/DialogTitle/DialogTitle.test.js
+++ b/packages/material-ui/src/DialogTitle/DialogTitle.test.js
@@ -20,7 +20,7 @@ describe('<DialogTitle />', () => {
   it('should spread custom props on the root node', () => {
     const wrapper = shallow(<DialogTitle data-my-prop="woofDialogTitle">foo</DialogTitle>);
     assert.strictEqual(
-      wrapper.prop('data-my-prop'),
+      wrapper.props()['data-my-prop'],
       'woofDialogTitle',
       'custom prop should be woofDialogTitle',
     );
diff --git a/packages/material-ui/src/GridList/GridList.test.js b/packages/material-ui/src/GridList/GridList.test.js
--- a/packages/material-ui/src/GridList/GridList.test.js
+++ b/packages/material-ui/src/GridList/GridList.test.js
@@ -63,7 +63,7 @@ describe('<GridList />', () => {
       wrapper
         .children()
         .at(0)
-        .prop('style').height,
+        .props().style.height,
       cellHeight + 4,
       'should have height to 254',
     );
@@ -110,7 +110,7 @@ describe('<GridList />', () => {
       wrapper
         .children()
         .at(0)
-        .prop('style').width,
+        .props().style.width,
       '25%',
       'should have 25% of width',
     );
@@ -138,7 +138,7 @@ describe('<GridList />', () => {
       wrapper
         .children()
         .at(0)
-        .prop('style').padding,
+        .props().style.padding,
       spacing / 2,
       'should have 5 of padding',
     );
@@ -162,11 +162,7 @@ describe('<GridList />', () => {
     );
 
     assert.strictEqual(wrapper.find('.grid-tile').length, 2, 'should contain the children');
-    assert.strictEqual(
-      wrapper.prop('style').backgroundColor,
-      style.backgroundColor,
-      'should have a red backgroundColor',
-    );
+    assert.strictEqual(wrapper.props().style.backgroundColor, style.backgroundColor);
   });
 
   describe('prop: cellHeight', () => {
diff --git a/packages/material-ui/src/Modal/Modal.test.js b/packages/material-ui/src/Modal/Modal.test.js
--- a/packages/material-ui/src/Modal/Modal.test.js
+++ b/packages/material-ui/src/Modal/Modal.test.js
@@ -82,14 +82,10 @@ describe('<Modal />', () => {
 
       const handler = wrapper.instance().handleBackdropClick;
       const backdrop = wrapper.find(Backdrop);
-      assert.strictEqual(
-        backdrop.prop('onClick'),
-        handler,
-        'should attach the handleBackdropClick handler',
-      );
+      assert.strictEqual(backdrop.props().onClick, handler);
 
       handler({});
-      assert.strictEqual(onClose.callCount, 1, 'should fire the onClose callback');
+      assert.strictEqual(onClose.callCount, 1);
     });
 
     it('should let the user disable backdrop click triggering onClose', () => {
@@ -99,7 +95,7 @@ describe('<Modal />', () => {
       const handler = wrapper.instance().handleBackdropClick;
 
       handler({});
-      assert.strictEqual(onClose.callCount, 0, 'should not fire the onClose callback');
+      assert.strictEqual(onClose.callCount, 0);
     });
 
     it('should call through to the user specified onBackdropClick callback', () => {
@@ -109,7 +105,7 @@ describe('<Modal />', () => {
       const handler = wrapper.instance().handleBackdropClick;
 
       handler({});
-      assert.strictEqual(onBackdropClick.callCount, 1, 'should fire the onBackdropClick callback');
+      assert.strictEqual(onBackdropClick.callCount, 1);
     });
 
     it('should ignore the backdrop click if the event did not come from the backdrop', () => {
@@ -126,11 +122,7 @@ describe('<Modal />', () => {
           /* another dom node */
         },
       });
-      assert.strictEqual(
-        onBackdropClick.callCount,
-        0,
-        'should not fire the onBackdropClick callback',
-      );
+      assert.strictEqual(onBackdropClick.callCount, 0);
     });
   });
 
diff --git a/packages/material-ui/src/Popover/Popover.test.js b/packages/material-ui/src/Popover/Popover.test.js
--- a/packages/material-ui/src/Popover/Popover.test.js
+++ b/packages/material-ui/src/Popover/Popover.test.js
@@ -232,7 +232,7 @@ describe('<Popover />', () => {
         wrapper
           .childAt(0)
           .childAt(0)
-          .prop('elevation'),
+          .props().elevation,
         8,
         'should be 8 elevation by default',
       );
@@ -241,7 +241,7 @@ describe('<Popover />', () => {
         wrapper
           .childAt(0)
           .childAt(0)
-          .prop('elevation'),
+          .props().elevation,
         16,
         'should be 16 elevation',
       );
diff --git a/packages/material-ui/src/TableRow/TableRow.test.js b/packages/material-ui/src/TableRow/TableRow.test.js
--- a/packages/material-ui/src/TableRow/TableRow.test.js
+++ b/packages/material-ui/src/TableRow/TableRow.test.js
@@ -24,11 +24,7 @@ describe('<TableRow />', () => {
 
   it('should spread custom props on the root node', () => {
     const wrapper = shallow(<TableRow data-my-prop="woofTableRow" />);
-    assert.strictEqual(
-      wrapper.prop('data-my-prop'),
-      'woofTableRow',
-      'custom prop should be woofTableRow',
-    );
+    assert.strictEqual(wrapper.props()['data-my-prop'], 'woofTableRow');
   });
 
   it('should render with the user and root classes', () => {