diff --git a/packages/material-ui/src/SwipeableDrawer/SwipeableDrawer.js b/packages/material-ui/src/SwipeableDrawer/SwipeableDrawer.js
index 862aaf47e8e6a6..56df40bac7af66 100644
--- a/packages/material-ui/src/SwipeableDrawer/SwipeableDrawer.js
+++ b/packages/material-ui/src/SwipeableDrawer/SwipeableDrawer.js
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';
import { elementTypeAcceptingRef } from '@material-ui/utils';
import Drawer, { getAnchor, isHorizontal } from '../Drawer/Drawer';
+import useEventCallback from '../utils/useEventCallback';
import { duration } from '../styles/transitions';
import useTheme from '../styles/useTheme';
import { getTransitionProps } from '../transitions/utils';
@@ -13,16 +14,6 @@ import SwipeArea from './SwipeArea';
// trigger a native scroll.
const UNCERTAINTY_THRESHOLD = 3; // px
-// We can only have one node at the time claiming ownership for handling the swipe.
-// Otherwise, the UX would be confusing.
-// That's why we use a singleton here.
-let nodeThatClaimedTheSwipe = null;
-
-// Exported for test purposes.
-export function reset() {
- nodeThatClaimedTheSwipe = null;
-}
-
function calculateCurrentX(anchor, touches) {
return anchor === 'right' ? document.body.offsetWidth - touches[0].pageX : touches[0].pageX;
}
@@ -82,14 +73,12 @@ const SwipeableDrawer = React.forwardRef(function SwipeableDrawer(props, ref) {
const paperRef = React.useRef();
const touchDetected = React.useRef(false);
- const openRef = React.useRef(open);
// Ref for transition duration based on / to match swipe speed
const calculatedDurationRef = React.useRef();
// Use a ref so the open value used is always up to date inside useCallback.
useEnhancedEffect(() => {
- openRef.current = open;
calculatedDurationRef.current = null;
}, [open]);
@@ -142,214 +131,230 @@ const SwipeableDrawer = React.forwardRef(function SwipeableDrawer(props, ref) {
[anchor, disableBackdropTransition, hideBackdrop, theme, transitionDuration],
);
- const handleBodyTouchEnd = React.useCallback(
- event => {
- if (!touchDetected.current) {
- return;
- }
- nodeThatClaimedTheSwipe = null;
- touchDetected.current = false;
- setMaybeSwiping(false);
-
- // The swipe wasn't started.
- if (!swipeInstance.current.isSwiping) {
- swipeInstance.current.isSwiping = null;
- return;
- }
+ const handleBodyTouchEnd = useEventCallback(event => {
+ if (!touchDetected.current) {
+ return;
+ }
+ touchDetected.current = false;
+ setMaybeSwiping(false);
+ // The swipe wasn't started.
+ if (!swipeInstance.current.isSwiping) {
swipeInstance.current.isSwiping = null;
+ return;
+ }
- const anchorRtl = getAnchor(theme, anchor);
- const horizontal = isHorizontal(anchor);
- let current;
- if (horizontal) {
- current = calculateCurrentX(anchorRtl, event.changedTouches);
- } else {
- current = calculateCurrentY(anchorRtl, event.changedTouches);
- }
+ swipeInstance.current.isSwiping = null;
- const startLocation = horizontal
- ? swipeInstance.current.startX
- : swipeInstance.current.startY;
- const maxTranslate = getMaxTranslate(horizontal, paperRef.current);
- const currentTranslate = getTranslate(current, startLocation, openRef.current, maxTranslate);
- const translateRatio = currentTranslate / maxTranslate;
-
- if (Math.abs(swipeInstance.current.velocity) > minFlingVelocity) {
- // Calculate transition duration to match swipe speed
- calculatedDurationRef.current =
- Math.abs((maxTranslate - currentTranslate) / swipeInstance.current.velocity) * 1000;
- }
+ const anchorRtl = getAnchor(theme, anchor);
+ const horizontal = isHorizontal(anchor);
+ let current;
+ if (horizontal) {
+ current = calculateCurrentX(anchorRtl, event.changedTouches);
+ } else {
+ current = calculateCurrentY(anchorRtl, event.changedTouches);
+ }
- if (openRef.current) {
- if (swipeInstance.current.velocity > minFlingVelocity || translateRatio > hysteresis) {
- onClose();
- } else {
- // Reset the position, the swipe was aborted.
- setPosition(0, {
- mode: 'exit',
- });
- }
+ const startLocation = horizontal ? swipeInstance.current.startX : swipeInstance.current.startY;
+ const maxTranslate = getMaxTranslate(horizontal, paperRef.current);
+ const currentTranslate = getTranslate(current, startLocation, open, maxTranslate);
+ const translateRatio = currentTranslate / maxTranslate;
- return;
- }
+ if (Math.abs(swipeInstance.current.velocity) > minFlingVelocity) {
+ // Calculate transition duration to match swipe speed
+ calculatedDurationRef.current =
+ Math.abs((maxTranslate - currentTranslate) / swipeInstance.current.velocity) * 1000;
+ }
- if (swipeInstance.current.velocity < -minFlingVelocity || 1 - translateRatio > hysteresis) {
- onOpen();
+ if (open) {
+ if (swipeInstance.current.velocity > minFlingVelocity || translateRatio > hysteresis) {
+ onClose();
} else {
// Reset the position, the swipe was aborted.
- setPosition(getMaxTranslate(horizontal, paperRef.current), {
- mode: 'enter',
+ setPosition(0, {
+ mode: 'exit',
});
}
- },
- [anchor, hysteresis, minFlingVelocity, onClose, onOpen, setPosition, theme],
- );
- const handleBodyTouchMove = React.useCallback(
- event => {
- // the ref may be null when a parent component updates while swiping
- if (!paperRef.current || !touchDetected.current) {
- return;
- }
+ return;
+ }
- const anchorRtl = getAnchor(theme, anchor);
- const horizontalSwipe = isHorizontal(anchor);
+ if (swipeInstance.current.velocity < -minFlingVelocity || 1 - translateRatio > hysteresis) {
+ onOpen();
+ } else {
+ // Reset the position, the swipe was aborted.
+ setPosition(getMaxTranslate(horizontal, paperRef.current), {
+ mode: 'enter',
+ });
+ }
+ });
- const currentX = calculateCurrentX(anchorRtl, event.touches);
- const currentY = calculateCurrentY(anchorRtl, event.touches);
+ const handleBodyTouchMove = useEventCallback(event => {
+ // the ref may be null when a parent component updates while swiping
+ if (!paperRef.current || !touchDetected.current) {
+ return;
+ }
- // We don't know yet.
- if (swipeInstance.current.isSwiping == null) {
- const dx = Math.abs(currentX - swipeInstance.current.startX);
- const dy = Math.abs(currentY - swipeInstance.current.startY);
+ const anchorRtl = getAnchor(theme, anchor);
+ const horizontalSwipe = isHorizontal(anchor);
- // We are likely to be swiping, let's prevent the scroll event on iOS.
- if (dx > dy) {
- if (event.cancelable) {
- event.preventDefault();
- }
+ const currentX = calculateCurrentX(anchorRtl, event.touches);
+ const currentY = calculateCurrentY(anchorRtl, event.touches);
+
+ // We don't know yet.
+ if (swipeInstance.current.isSwiping == null) {
+ const dx = Math.abs(currentX - swipeInstance.current.startX);
+ const dy = Math.abs(currentY - swipeInstance.current.startY);
+
+ // We are likely to be swiping, let's prevent the scroll event on iOS.
+ if (dx > dy) {
+ if (event.cancelable) {
+ event.preventDefault();
}
+ }
- const definitelySwiping = horizontalSwipe
- ? dx > dy && dx > UNCERTAINTY_THRESHOLD
- : dy > dx && dy > UNCERTAINTY_THRESHOLD;
-
- if (
- definitelySwiping === true ||
- (horizontalSwipe ? dy > UNCERTAINTY_THRESHOLD : dx > UNCERTAINTY_THRESHOLD)
- ) {
- swipeInstance.current.isSwiping = definitelySwiping;
- if (!definitelySwiping) {
- handleBodyTouchEnd(event);
- return;
- }
+ const definitelySwiping = horizontalSwipe
+ ? dx > dy && dx > UNCERTAINTY_THRESHOLD
+ : dy > dx && dy > UNCERTAINTY_THRESHOLD;
+
+ if (
+ definitelySwiping === true ||
+ (horizontalSwipe ? dy > UNCERTAINTY_THRESHOLD : dx > UNCERTAINTY_THRESHOLD)
+ ) {
+ swipeInstance.current.isSwiping = definitelySwiping;
+ if (!definitelySwiping) {
+ handleBodyTouchEnd(event);
+ return;
+ }
- // Shift the starting point.
- swipeInstance.current.startX = currentX;
- swipeInstance.current.startY = currentY;
+ // Shift the starting point.
+ swipeInstance.current.startX = currentX;
+ swipeInstance.current.startY = currentY;
- // Compensate for the part of the drawer displayed on touch start.
- if (!disableDiscovery && !openRef.current) {
- if (horizontalSwipe) {
- swipeInstance.current.startX -= swipeAreaWidth;
- } else {
- swipeInstance.current.startY -= swipeAreaWidth;
- }
+ // Compensate for the part of the drawer displayed on touch start.
+ if (!disableDiscovery && !open) {
+ if (horizontalSwipe) {
+ swipeInstance.current.startX -= swipeAreaWidth;
+ } else {
+ swipeInstance.current.startY -= swipeAreaWidth;
}
}
}
+ }
- if (!swipeInstance.current.isSwiping) {
- return;
- }
- const startLocation = horizontalSwipe
- ? swipeInstance.current.startX
- : swipeInstance.current.startY;
- const maxTranslate = getMaxTranslate(horizontalSwipe, paperRef.current);
-
- const translate = getTranslate(
- horizontalSwipe ? currentX : currentY,
- startLocation,
- openRef.current,
- maxTranslate,
- );
+ if (!swipeInstance.current.isSwiping) {
+ return;
+ }
- if (swipeInstance.current.lastTranslate === null) {
- swipeInstance.current.lastTranslate = translate;
- swipeInstance.current.lastTime = performance.now() + 1;
+ const maxTranslate = getMaxTranslate(horizontalSwipe, paperRef.current);
+ let startLocation = horizontalSwipe
+ ? swipeInstance.current.startX
+ : swipeInstance.current.startY;
+ if (open && !swipeInstance.current.paperHit) {
+ startLocation = Math.min(startLocation, maxTranslate);
+ }
+
+ const translate = getTranslate(
+ horizontalSwipe ? currentX : currentY,
+ startLocation,
+ open,
+ maxTranslate,
+ );
+
+ if (open) {
+ if (!swipeInstance.current.paperHit) {
+ const paperHit = horizontalSwipe ? currentX < maxTranslate : currentY < maxTranslate;
+ if (paperHit) {
+ swipeInstance.current.paperHit = true;
+ swipeInstance.current.startX = currentX;
+ swipeInstance.current.startY = currentY;
+ } else {
+ return;
+ }
+ } else if (translate === 0) {
+ swipeInstance.current.startX = currentX;
+ swipeInstance.current.startY = currentY;
}
+ }
- const velocity =
- ((translate - swipeInstance.current.lastTranslate) /
- (performance.now() - swipeInstance.current.lastTime)) *
- 1e3;
+ if (swipeInstance.current.lastTranslate === null) {
+ swipeInstance.current.lastTranslate = translate;
+ swipeInstance.current.lastTime = performance.now() + 1;
+ }
- // Low Pass filter.
- swipeInstance.current.velocity = swipeInstance.current.velocity * 0.4 + velocity * 0.6;
+ const velocity =
+ ((translate - swipeInstance.current.lastTranslate) /
+ (performance.now() - swipeInstance.current.lastTime)) *
+ 1e3;
- swipeInstance.current.lastTranslate = translate;
- swipeInstance.current.lastTime = performance.now();
+ // Low Pass filter.
+ swipeInstance.current.velocity = swipeInstance.current.velocity * 0.4 + velocity * 0.6;
- // We are swiping, let's prevent the scroll event on iOS.
- if (event.cancelable) {
- event.preventDefault();
- }
- setPosition(translate);
- },
- [setPosition, handleBodyTouchEnd, anchor, disableDiscovery, swipeAreaWidth, theme],
- );
+ swipeInstance.current.lastTranslate = translate;
+ swipeInstance.current.lastTime = performance.now();
- const handleBodyTouchStart = React.useCallback(
- event => {
- // We are not supposed to handle this touch move.
- if (nodeThatClaimedTheSwipe !== null && nodeThatClaimedTheSwipe !== swipeInstance.current) {
- return;
- }
+ // We are swiping, let's prevent the scroll event on iOS.
+ if (event.cancelable) {
+ event.preventDefault();
+ }
- const anchorRtl = getAnchor(theme, anchor);
- const horizontalSwipe = isHorizontal(anchor);
+ setPosition(translate);
+ });
- const currentX = calculateCurrentX(anchorRtl, event.touches);
- const currentY = calculateCurrentY(anchorRtl, event.touches);
+ const handleBodyTouchStart = useEventCallback(event => {
+ // We are not supposed to handle this touch move.
+ // Example of use case: ignore the event if there is a Slider.
+ if (event.defaultPrevented) {
+ return;
+ }
- if (!openRef.current) {
- if (disableSwipeToOpen || event.target !== swipeAreaRef.current) {
- return;
- }
- if (horizontalSwipe) {
- if (currentX > swipeAreaWidth) {
- return;
- }
- } else if (currentY > swipeAreaWidth) {
+ // We can only have one node at the time claiming ownership for handling the swipe.
+ if (event.muiHandled) {
+ return;
+ }
+
+ const anchorRtl = getAnchor(theme, anchor);
+ const horizontalSwipe = isHorizontal(anchor);
+
+ const currentX = calculateCurrentX(anchorRtl, event.touches);
+ const currentY = calculateCurrentY(anchorRtl, event.touches);
+
+ if (!open) {
+ if (disableSwipeToOpen || event.target !== swipeAreaRef.current) {
+ return;
+ }
+ if (horizontalSwipe) {
+ if (currentX > swipeAreaWidth) {
return;
}
+ } else if (currentY > swipeAreaWidth) {
+ return;
}
+ }
- nodeThatClaimedTheSwipe = swipeInstance.current;
- swipeInstance.current.startX = currentX;
- swipeInstance.current.startY = currentY;
-
- setMaybeSwiping(true);
- if (!openRef.current && paperRef.current) {
- // The ref may be null when a parent component updates while swiping.
- setPosition(
- getMaxTranslate(horizontalSwipe, paperRef.current) +
- (disableDiscovery ? 20 : -swipeAreaWidth),
- {
- changeTransition: false,
- },
- );
- }
+ event.muiHandled = true;
+ swipeInstance.current.startX = currentX;
+ swipeInstance.current.startY = currentY;
+
+ setMaybeSwiping(true);
+ if (!open && paperRef.current) {
+ // The ref may be null when a parent component updates while swiping.
+ setPosition(
+ getMaxTranslate(horizontalSwipe, paperRef.current) +
+ (disableDiscovery ? 20 : -swipeAreaWidth),
+ {
+ changeTransition: false,
+ },
+ );
+ }
- swipeInstance.current.velocity = 0;
- swipeInstance.current.lastTime = null;
- swipeInstance.current.lastTranslate = null;
+ swipeInstance.current.velocity = 0;
+ swipeInstance.current.lastTime = null;
+ swipeInstance.current.lastTranslate = null;
+ swipeInstance.current.paperHit = false;
- touchDetected.current = true;
- },
- [setPosition, anchor, disableDiscovery, disableSwipeToOpen, swipeAreaWidth, theme],
- );
+ touchDetected.current = true;
+ });
React.useEffect(() => {
if (variant === 'temporary') {
@@ -367,16 +372,6 @@ const SwipeableDrawer = React.forwardRef(function SwipeableDrawer(props, ref) {
return undefined;
}, [variant, handleBodyTouchStart, handleBodyTouchMove, handleBodyTouchEnd]);
- React.useEffect(
- () => () => {
- // We need to release the lock.
- if (nodeThatClaimedTheSwipe === swipeInstance.current) {
- nodeThatClaimedTheSwipe = null;
- }
- },
- [],
- );
-
React.useEffect(() => {
if (!open) {
setMaybeSwiping(false);
diff --git a/packages/material-ui/src/SwipeableDrawer/SwipeableDrawer.test.js b/packages/material-ui/src/SwipeableDrawer/SwipeableDrawer.test.js
index 4b9037e693a448..2333020c373e68 100644
--- a/packages/material-ui/src/SwipeableDrawer/SwipeableDrawer.test.js
+++ b/packages/material-ui/src/SwipeableDrawer/SwipeableDrawer.test.js
@@ -6,7 +6,7 @@ import describeConformance from '@material-ui/core/test-utils/describeConformanc
import PropTypes from 'prop-types';
import consoleErrorMock from 'test/utils/consoleErrorMock';
import Drawer from '../Drawer';
-import SwipeableDrawer, { reset } from './SwipeableDrawer';
+import SwipeableDrawer from './SwipeableDrawer';
import SwipeArea from './SwipeArea';
import useForkRef from '../utils/useForkRef';
@@ -147,7 +147,6 @@ describe('', () => {
});
afterEach(() => {
- reset();
if (wrapper.length > 0) {
wrapper.unmount();
}
@@ -443,12 +442,20 @@ describe('', () => {
,
);
- fireSwipeAreaMouseEvent(wrapper.find(SwipeableDrawer).at(0), 'touchstart', {
- touches: [{ pageX: 0, clientY: 0 }],
- });
- fireSwipeAreaMouseEvent(wrapper.find(SwipeableDrawer).at(1), 'touchstart', {
- touches: [{ pageX: 0, clientY: 0 }],
- });
+ // use the same event object for both touch start events, one would propagate to the other swipe area in the browser
+ const touchStartEvent = fireSwipeAreaMouseEvent(
+ wrapper.find(SwipeableDrawer).at(0),
+ 'touchstart',
+ {
+ touches: [{ pageX: 0, clientY: 0 }],
+ },
+ );
+ wrapper
+ .find(SwipeableDrawer)
+ .at(1)
+ .find(SwipeArea)
+ .getDOMNode()
+ .dispatchEvent(touchStartEvent);
fireBodyMouseEvent('touchmove', { touches: [{ pageX: 20, clientY: 0 }] });
fireBodyMouseEvent('touchmove', { touches: [{ pageX: 180, clientY: 0 }] });
fireBodyMouseEvent('touchend', { changedTouches: [{ pageX: 180, clientY: 0 }] });