Repository: mui/material-ui (TypeScript)
Date: 2019-03-24 18:58:11+00:00
Task type: Bug Fix

Issue:
[SwipeableDrawer] Swipe to open works through dialogs
<!--- Provide a general summary of the issue in the Title above -->
While the `z-index` of the swipe area (added in #11031) was carefully chosen to be _below_ `Dialog`s, the opening logic still uses the body event, so you can swipe the drawer open even if a fullscreen dialog is shown.

I already tried fixing this for an hour but I didn't find a solution that doesn't break discovery. :disappointed: Maybe we can find a solution together. :sunglasses: 

<!--
    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 🤔
The swipeable drawer should only be opened when the swipe starts _on_ the swipe area. That means, it shouldn't start if there is anything on top of that area, e.g. a dialog.

## Current Behavior 😯
The swipeable drawer can be swiped in even if there's a dialog on top of it. In the docs, the drawer isn't visible then, but in my app, it is – on top of the dialog.

## 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/next/examples/create-react-app

    If you're using typescript a better starting point would be
    https://codesandbox.io/s/github/mui-org/material-ui/tree/next/examples/create-react-app-with-typescript

    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#full-screen-dialogs

1. Go into mobile device mode
2. Push the button to open a fullscreen dialog
3. Swipe the drawer open (left edge to right)
4. Close the dialog
5. See how the drawer is now open :open_mouth: 

## Context 🔦
<!---
    What are you trying to accomplish? How has this issue affected you?
    Providing context helps us come up with a solution that is most useful in the real world.
-->
I have a swipeable drawer and fullscreen dialogs in my mobile app.

## Your Environment 🌎

| Tech         | Version |
|--------------|---------|
| Material-UI  | v3.9.2  |
| React        |         |
| Browser      |         |
| TypeScript   |         |
| etc.         |         |



Issue discussion:
PR incoming… :tada: 

Solution patch:
diff --git a/packages/material-ui/src/SwipeableDrawer/SwipeArea.js b/packages/material-ui/src/SwipeableDrawer/SwipeArea.js
--- a/packages/material-ui/src/SwipeableDrawer/SwipeArea.js
+++ b/packages/material-ui/src/SwipeableDrawer/SwipeArea.js
@@ -35,7 +35,7 @@ export const styles = theme => ({
 /**
  * @ignore - internal component.
  */
-function SwipeArea(props) {
+const SwipeArea = React.forwardRef((props, ref) => {
   const { anchor, classes, className, width, ...other } = props;
 
   return (
@@ -44,10 +44,11 @@ function SwipeArea(props) {
       style={{
         [isHorizontal(props) ? 'width' : 'height']: width,
       }}
+      ref={ref}
       {...other}
     />
   );
-}
+});
 
 SwipeArea.propTypes = {
   /**
diff --git a/packages/material-ui/src/SwipeableDrawer/SwipeableDrawer.js b/packages/material-ui/src/SwipeableDrawer/SwipeableDrawer.js
--- a/packages/material-ui/src/SwipeableDrawer/SwipeableDrawer.js
+++ b/packages/material-ui/src/SwipeableDrawer/SwipeableDrawer.js
@@ -35,6 +35,8 @@ class SwipeableDrawer extends React.Component {
 
   isSwiping = null;
 
+  swipeAreaRef = React.createRef();
+
   componentDidMount() {
     if (this.props.variant === 'temporary') {
       this.listenTouchStart();
@@ -158,7 +160,7 @@ class SwipeableDrawer extends React.Component {
         : event.touches[0].clientY;
 
     if (!open) {
-      if (disableSwipeToOpen) {
+      if (disableSwipeToOpen || event.target !== this.swipeAreaRef.current) {
         return;
       }
       if (isHorizontal(this.props)) {
@@ -193,7 +195,7 @@ class SwipeableDrawer extends React.Component {
   };
 
   handleBodyTouchMove = event => {
-    // the ref may be null when a parent component updates while swiping
+    // The ref may be null when a parent component updates while swiping.
     if (!this.paperRef) return;
 
     const anchor = getAnchor(this.props);
@@ -390,9 +392,14 @@ class SwipeableDrawer extends React.Component {
           anchor={anchor}
           {...other}
         />
-        {!disableDiscovery && !disableSwipeToOpen && variant === 'temporary' && (
+        {!disableSwipeToOpen && variant === 'temporary' && (
           <NoSsr>
-            <SwipeArea anchor={anchor} width={swipeAreaWidth} {...SwipeAreaProps} />
+            <SwipeArea
+              anchor={anchor}
+              innerRef={this.swipeAreaRef}
+              width={swipeAreaWidth}
+              {...SwipeAreaProps}
+            />
           </NoSsr>
         )}
       </React.Fragment>


Test patch:
diff --git a/packages/material-ui/src/SwipeableDrawer/SwipeableDrawer.test.js b/packages/material-ui/src/SwipeableDrawer/SwipeableDrawer.test.js
--- a/packages/material-ui/src/SwipeableDrawer/SwipeableDrawer.test.js
+++ b/packages/material-ui/src/SwipeableDrawer/SwipeableDrawer.test.js
@@ -19,6 +19,20 @@ function fireBodyMouseEvent(name, properties = {}) {
   return event;
 }
 
+function fireSwipeAreaMouseEvent(wrapper, name, properties = {}) {
+  const event = document.createEvent('MouseEvents');
+  event.initEvent(name, true, true);
+  Object.keys(properties).forEach(key => {
+    event[key] = properties[key];
+  });
+  const swipeArea = wrapper.find(SwipeArea);
+  if (swipeArea.length >= 1) {
+    // if no SwipeArea is mounted, the body event wouldn't propagate to it anyway
+    swipeArea.getDOMNode().dispatchEvent(event);
+  }
+  return event;
+}
+
 describe('<SwipeableDrawer />', () => {
   const SwipeableDrawerNaked = unwrap(SwipeableDrawer);
   let mount;
@@ -76,20 +90,6 @@ describe('<SwipeableDrawer />', () => {
     wrapper.unmount();
   });
 
-  it('should hide the SwipeArea if discovery is disabled', () => {
-    const wrapper = mount(
-      <SwipeableDrawerNaked
-        onOpen={() => {}}
-        onClose={() => {}}
-        open={false}
-        theme={createMuiTheme()}
-        disableDiscovery
-      />,
-    );
-    assert.strictEqual(wrapper.children().length, 1);
-    wrapper.unmount();
-  });
-
   it('should accept user custom style', () => {
     const customStyle = { style: { backgroundColor: 'hotpink' } };
     const wrapper = mount(
@@ -208,7 +208,7 @@ describe('<SwipeableDrawer />', () => {
           // simulate open swipe
           const handleOpen = spy();
           wrapper.setProps({ onOpen: handleOpen });
-          fireBodyMouseEvent('touchstart', { touches: [params.openTouches[0]] });
+          fireSwipeAreaMouseEvent(wrapper, 'touchstart', { touches: [params.openTouches[0]] });
           assert.strictEqual(wrapper.state().maybeSwiping, true);
           fireBodyMouseEvent('touchmove', { touches: [params.openTouches[1]] });
           assert.strictEqual(instance.isSwiping, true);
@@ -221,7 +221,7 @@ describe('<SwipeableDrawer />', () => {
           instance.setPosition.resetHistory();
           const handleClose = spy();
           wrapper.setProps({ open: true, onClose: handleClose });
-          fireBodyMouseEvent('touchstart', { touches: [params.closeTouches[0]] });
+          fireSwipeAreaMouseEvent(wrapper, 'touchstart', { touches: [params.closeTouches[0]] });
           assert.strictEqual(wrapper.state().maybeSwiping, true);
           fireBodyMouseEvent('touchmove', { touches: [params.closeTouches[1]] });
           assert.strictEqual(instance.isSwiping, true);
@@ -235,7 +235,7 @@ describe('<SwipeableDrawer />', () => {
           // simulate open swipe that doesn't swipe far enough
           const handleOpen = spy();
           wrapper.setProps({ onOpen: handleOpen });
-          fireBodyMouseEvent('touchstart', { touches: [params.openTouches[0]] });
+          fireSwipeAreaMouseEvent(wrapper, 'touchstart', { touches: [params.openTouches[0]] });
           assert.strictEqual(wrapper.state().maybeSwiping, true);
           fireBodyMouseEvent('touchmove', { touches: [params.openTouches[1]] });
           assert.strictEqual(instance.isSwiping, true);
@@ -247,7 +247,7 @@ describe('<SwipeableDrawer />', () => {
           // simulate close swipe that doesn't swipe far enough
           const handleClose = spy();
           wrapper.setProps({ open: true, onClose: handleClose });
-          fireBodyMouseEvent('touchstart', { touches: [params.closeTouches[0]] });
+          fireSwipeAreaMouseEvent(wrapper, 'touchstart', { touches: [params.closeTouches[0]] });
           assert.strictEqual(wrapper.state().maybeSwiping, true);
           fireBodyMouseEvent('touchmove', { touches: [params.closeTouches[1]] });
           assert.strictEqual(instance.isSwiping, true);
@@ -258,7 +258,7 @@ describe('<SwipeableDrawer />', () => {
         it('should ignore swiping in the wrong direction if discovery is disabled', () => {
           wrapper.setProps({ disableDiscovery: true });
 
-          fireBodyMouseEvent('touchstart', { touches: [params.openTouches[0]] });
+          fireSwipeAreaMouseEvent(wrapper, 'touchstart', { touches: [params.openTouches[0]] });
           if (['left', 'right'].indexOf(params.anchor) !== -1) {
             fireBodyMouseEvent('touchmove', {
               touches: [
@@ -288,7 +288,7 @@ describe('<SwipeableDrawer />', () => {
           const handleOpen = spy();
           const handleClose = spy();
           wrapper.setProps({ onOpen: handleOpen, onClose: handleClose });
-          fireBodyMouseEvent('touchstart', { touches: [params.edgeTouch] });
+          fireSwipeAreaMouseEvent(wrapper, 'touchstart', { touches: [params.edgeTouch] });
           assert.strictEqual(wrapper.state().maybeSwiping, true);
           assert.strictEqual(instance.setPosition.callCount, 1);
           fireBodyMouseEvent('touchend', { changedTouches: [params.edgeTouch] });
@@ -307,7 +307,7 @@ describe('<SwipeableDrawer />', () => {
             onOpen: handleOpen,
             onClose: handleClose,
           });
-          fireBodyMouseEvent('touchstart', { touches: [params.edgeTouch] });
+          fireSwipeAreaMouseEvent(wrapper, 'touchstart', { touches: [params.edgeTouch] });
           assert.strictEqual(wrapper.state().maybeSwiping, true);
           assert.strictEqual(instance.setPosition.callCount, 1);
           fireBodyMouseEvent('touchend', { changedTouches: [params.edgeTouch] });
@@ -327,7 +327,7 @@ describe('<SwipeableDrawer />', () => {
             onOpen: handleOpen,
             onClose: handleClose,
           });
-          fireBodyMouseEvent('touchstart', { touches: [params.ignoreTouch] });
+          fireSwipeAreaMouseEvent(wrapper, 'touchstart', { touches: [params.ignoreTouch] });
           assert.strictEqual(wrapper.state().maybeSwiping, false);
           assert.strictEqual(instance.setPosition.callCount, 0);
           fireBodyMouseEvent('touchend', { changedTouches: [params.ignoreTouch] });
@@ -342,7 +342,7 @@ describe('<SwipeableDrawer />', () => {
         open: true,
       });
       assert.strictEqual(instance.isSwiping, null);
-      fireBodyMouseEvent('touchstart', { touches: [{ pageX: 0, clientY: 0 }] });
+      fireSwipeAreaMouseEvent(wrapper, 'touchstart', { touches: [{ pageX: 0, clientY: 0 }] });
       assert.strictEqual(instance.isSwiping, null);
       fireBodyMouseEvent('touchmove', { touches: [{ pageX: 10, clientY: 0 }] });
       assert.strictEqual(instance.isSwiping, true);
@@ -355,7 +355,7 @@ describe('<SwipeableDrawer />', () => {
 
     it('should wait for a clear signal to determine this.isSwiping', () => {
       assert.strictEqual(instance.isSwiping, null);
-      fireBodyMouseEvent('touchstart', { touches: [{ pageX: 0, clientY: 0 }] });
+      fireSwipeAreaMouseEvent(wrapper, 'touchstart', { touches: [{ pageX: 0, clientY: 0 }] });
       assert.strictEqual(instance.isSwiping, null);
       fireBodyMouseEvent('touchmove', { touches: [{ pageX: 3, clientY: 0 }] });
       assert.strictEqual(instance.isSwiping, null);
@@ -364,7 +364,7 @@ describe('<SwipeableDrawer />', () => {
     });
 
     it('removes event listeners on unmount', () => {
-      fireBodyMouseEvent('touchstart', { touches: [{ pageX: 0, clientY: 0 }] });
+      fireSwipeAreaMouseEvent(wrapper, 'touchstart', { touches: [{ pageX: 0, clientY: 0 }] });
       wrapper.unmount();
       //  trigger setState warning if listeners aren't cleaned.
       fireBodyMouseEvent('touchmove', { touches: [{ pageX: 180, clientY: 0 }] });
@@ -399,7 +399,7 @@ describe('<SwipeableDrawer />', () => {
 
       // simulate open swipe
       wrapper.setProps({ disableSwipeToOpen: true });
-      fireBodyMouseEvent('touchstart', { touches: [{ pageX: 0, clientY: 0 }] });
+      fireSwipeAreaMouseEvent(wrapper, 'touchstart', { touches: [{ pageX: 0, clientY: 0 }] });
       assert.strictEqual(
         wrapper.state().maybeSwiping,
         false,
@@ -455,7 +455,12 @@ describe('<SwipeableDrawer />', () => {
           </SwipeableDrawerNaked>
         </div>,
       );
-      fireBodyMouseEvent('touchstart', { touches: [{ pageX: 0, clientY: 0 }] });
+      fireSwipeAreaMouseEvent(wrapper.find(SwipeableDrawerNaked).at(0), 'touchstart', {
+        touches: [{ pageX: 0, clientY: 0 }],
+      });
+      fireSwipeAreaMouseEvent(wrapper.find(SwipeableDrawerNaked).at(1), 'touchstart', {
+        touches: [{ pageX: 0, clientY: 0 }],
+      });
       fireBodyMouseEvent('touchmove', { touches: [{ pageX: 20, clientY: 0 }] });
       fireBodyMouseEvent('touchmove', { touches: [{ pageX: 180, clientY: 0 }] });
       fireBodyMouseEvent('touchend', { changedTouches: [{ pageX: 180, clientY: 0 }] });
@@ -475,7 +480,7 @@ describe('<SwipeableDrawer />', () => {
         <h1>Hello</h1>
       </SwipeableDrawerNaked>,
     );
-    fireBodyMouseEvent('touchstart', { touches: [{ pageX: 0, clientY: 0 }] });
+    fireSwipeAreaMouseEvent(wrapper, 'touchstart', { touches: [{ pageX: 0, clientY: 0 }] });
     // simulate paper ref being null because of the drawer being updated
     wrapper.instance().handlePaperRef(null);
     fireBodyMouseEvent('touchmove', { touches: [{ pageX: 20, clientY: 0 }] });
@@ -483,7 +488,7 @@ describe('<SwipeableDrawer />', () => {
 
   describe('no backdrop', () => {
     it('does not crash when backdrop is hidden while swiping', () => {
-      mount(
+      const wrapper = mount(
         <SwipeableDrawerNaked
           onClose={() => {}}
           onOpen={() => {}}
@@ -492,11 +497,11 @@ describe('<SwipeableDrawer />', () => {
           hideBackdrop
         />,
       );
-      fireBodyMouseEvent('touchstart', { touches: [{ pageX: 0, clientY: 0 }] });
+      fireSwipeAreaMouseEvent(wrapper, 'touchstart', { touches: [{ pageX: 0, clientY: 0 }] });
     });
 
     it('does not crash when backdrop props are empty while swiping', () => {
-      mount(
+      const wrapper = mount(
         <SwipeableDrawerNaked
           onClose={() => {}}
           onOpen={() => {}}
@@ -505,7 +510,7 @@ describe('<SwipeableDrawer />', () => {
           BackdropProps={{}}
         />,
       );
-      fireBodyMouseEvent('touchstart', { touches: [{ pageX: 0, clientY: 0 }] });
+      fireSwipeAreaMouseEvent(wrapper, 'touchstart', { touches: [{ pageX: 0, clientY: 0 }] });
     });
   });
 });