mui / material-ui

Material UI: Comprehensive React component library that implements Google's Material Design. Free forever.
https://mui.com/material-ui/
MIT License
94.06k stars 32.32k forks source link

[SwipeableDrawer] Passive events cancelable = true #37814

Open meelist opened 1 year ago

meelist commented 1 year ago

Duplicates

Latest version

Steps to reproduce 🕹

Repository with minimal code as an example: https://github.com/meelist/mui-swipeable-drawer-bug

Steps:

  1. npm i
  2. npm start
  3. try to swipe the drawer open
  4. observe errors in console

Current behavior 😯

Touch events should be cancelable since there is a passive event listener when the drawer is closed. However if there is an overlapping (even underneath) event listener which is not passive, then all touch events become cancelable.

Expected behavior 🤔

Additional check should be added when calling preventDefault in the SwipeableDrawer to check if the listener is passive or not.

Context 🔦

I have a full screen Google Map and I want to overlay the SwipeableDrawer on top of it. Both of these elements have touch event listeners it seems, but Google Map has an active event listener where as the drawer has a passive one when the drawer is closed. This causes a flood of error messages when swiping open the drawer since all events have become cancelable due to this overlap, but the drawer listener was configured as passive. No issues when closing the drawer because the listener isn't passive then. I have reproduced the same issue by just creating a full screen div with a non-passive touch listener.

I think perhaps this issue could be resolved if there was an additional check to see if the listener was set as passive or not when trying to call preventDefault.

Here is where the passive behaviour was changed: https://github.com/mui/material-ui/issues/22493

Your environment 🌎

npx @mui/envinfo ``` Don't forget to mention which browser you used. Output from `npx @mui/envinfo` goes here. ``` System: OS: macOS 13.4 Binaries: Node: 20.2.0 - /opt/homebrew/bin/node Yarn: 1.22.19 - /opt/homebrew/bin/yarn npm: 9.6.6 - /opt/homebrew/bin/npm Browsers: Chrome: 114.0.5735.198 Edge: Not Found Safari: 16.5 npmPackages: @emotion/react: ^11.11.1 => 11.11.1 @emotion/styled: ^11.11.0 => 11.11.0 @mui/base: 5.0.0-beta.6 @mui/core-downloads-tracker: 5.13.7 @mui/material: ^5.13.7 => 5.13.7 @mui/private-theming: 5.13.7 @mui/styled-engine: 5.13.2 @mui/system: 5.13.7 @mui/types: 7.2.4 @mui/utils: 5.13.7 @types/react: ^18.2.14 => 18.2.14 react: ^18.2.0 => 18.2.0 react-dom: ^18.2.0 => 18.2.0 typescript: ^4.9.5 => 4.9.5 Browser: Chrome Version 114.0.5735.198 (Official Build) (arm64)
mnajdova commented 1 year ago

I don't see how it can be fixed on Material UI's side. The Swipeable component itself, correctly register the listeners. It is happening because of the way the touch even bubbles. One quick fix would be adding the SwipeableDrawer component first, this removes the warnings:

index a9c00c3..7dd18c1 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -12,6 +12,7 @@ function App() {

   return (
     <div className="App">
+      <SwipeableEdgeDrawer />
       <div style={{ height: "100vh", width: "100%" }}>
         {/* when using below line instead and elements dont overlap, then works correctly */}
         {/* <div style={{ height: "calc(100vh - 56px)", width: "100%" }}> */}
@@ -21,7 +22,6 @@ function App() {
           defaultZoom={defaultProps.zoom}
         ></GoogleMapReact>
       </div>
-      <SwipeableEdgeDrawer />
     </div>
   );
 }
meelist commented 1 year ago

For me this doesn't remove the warnings, they still persist since the components with the same event listeners overlap.

I proposed that MUI could also store the state if the event listener was registered as passive or not. That check can be added to the existing checks then where cancelling of the event is attempted. It should be taken into account that passive event listeners can receive an event which has cancelable set to true, but throw this error if you try to cancel.

GrantASL19 commented 1 year ago

FWIW we have a very similar problem (errors when swiping a drawer displayed over a Google map), and like @meelist rendering the SwipeableDrawer before the map doesn’t prevent the warnings.

meelist commented 1 year ago

An alternative solution for those in need: https://react-spring.bottom-sheet.dev/

I love MUI but this component just doesn't offer what material bottom sheet needs.

mnajdova commented 1 year ago

I proposed that MUI could also store the state if the event listener was registered as passive or not. That check can be added to the existing checks then where cancelling of the event is attempted. It should be taken into account that passive event listeners can receive an event which has cancelable set to true, but throw this error if you try to cancel.

Would you like to create a pull request for this so that we can test all scenarios?

Another option would be to add a prop that will indicate whether the passive option should always be set to false. Would this help? What I mean is using: passive: false instead of passive: !open.

As far as I could test using passive: false it works, here are the components that you can test with locally:

Open to see the code SwipeableDrawer.js ``` 'use client'; import * as React from 'react'; import * as ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; import { elementTypeAcceptingRef } from '@mui/utils'; import { useThemeProps } from '@mui/system'; import { NoSsr } from '@mui/base'; import Drawer from '@mui/material/Drawer'; import { useForkRef, ownerDocument, ownerWindow, useEventCallback, unstable_useEnhancedEffect as useEnhancedEffect } from '@mui/material/utils'; import { useTheme } from '@mui/material/styles'; import SwipeArea from './SwipeArea'; const oppositeDirection = { left: 'right', right: 'left', top: 'down', bottom: 'up', }; function isHorizontal(anchor) { return ['left', 'right'].indexOf(anchor) !== -1; } function getAnchor(theme, anchor) { return theme.direction === 'rtl' && isHorizontal(anchor) ? oppositeDirection[anchor] : anchor; } export function getTransitionProps(props, options) { const { timeout, easing, style = {} } = props; return { duration: style.transitionDuration ?? (typeof timeout === 'number' ? timeout : timeout[options.mode] || 0), easing: style.transitionTimingFunction ?? (typeof easing === 'object' ? easing[options.mode] : easing), delay: style.transitionDelay, }; } // This value is closed to what browsers are using internally to // trigger a native scroll. const UNCERTAINTY_THRESHOLD = 3; // px // This is the part of the drawer displayed on touch start. const DRAG_STARTED_SIGNAL = 20; // px // We can only have one instance at the time claiming ownership for handling the swipe. // Otherwise, the UX would be confusing. // That's why we use a singleton here. let claimedSwipeInstance = null; // Exported for test purposes. export function reset() { claimedSwipeInstance = null; } function calculateCurrentX(anchor, touches, doc) { return anchor === 'right' ? doc.body.offsetWidth - touches[0].pageX : touches[0].pageX; } function calculateCurrentY(anchor, touches, containerWindow) { return anchor === 'bottom' ? containerWindow.innerHeight - touches[0].clientY : touches[0].clientY; } function getMaxTranslate(horizontalSwipe, paperInstance) { return horizontalSwipe ? paperInstance.clientWidth : paperInstance.clientHeight; } function getTranslate(currentTranslate, startLocation, open, maxTranslate) { return Math.min( Math.max( open ? startLocation - currentTranslate : maxTranslate + startLocation - currentTranslate, 0, ), maxTranslate, ); } /** * @param {Element | null} element * @param {Element} rootNode */ function getDomTreeShapes(element, rootNode) { // Adapted from https://github.com/oliviertassinari/react-swipeable-views/blob/7666de1dba253b896911adf2790ce51467670856/packages/react-swipeable-views/src/SwipeableViews.js#L129 const domTreeShapes = []; while (element && element !== rootNode.parentElement) { const style = ownerWindow(rootNode).getComputedStyle(element); if ( // Ignore the scroll children if the element is absolute positioned. style.getPropertyValue('position') === 'absolute' || // Ignore the scroll children if the element has an overflowX hidden style.getPropertyValue('overflow-x') === 'hidden' ) { // noop } else if ( (element.clientWidth > 0 && element.scrollWidth > element.clientWidth) || (element.clientHeight > 0 && element.scrollHeight > element.clientHeight) ) { // Ignore the nodes that have no width. // Keep elements with a scroll domTreeShapes.push(element); } element = element.parentElement; } return domTreeShapes; } /** * @param {object} param0 * @param {ReturnType} param0.domTreeShapes */ function computeHasNativeHandler({ domTreeShapes, start, current, anchor }) { // Adapted from https://github.com/oliviertassinari/react-swipeable-views/blob/7666de1dba253b896911adf2790ce51467670856/packages/react-swipeable-views/src/SwipeableViews.js#L175 const axisProperties = { scrollPosition: { x: 'scrollLeft', y: 'scrollTop', }, scrollLength: { x: 'scrollWidth', y: 'scrollHeight', }, clientLength: { x: 'clientWidth', y: 'clientHeight', }, }; return domTreeShapes.some((shape) => { // Determine if we are going backward or forward. let goingForward = current >= start; if (anchor === 'top' || anchor === 'left') { goingForward = !goingForward; } const axis = anchor === 'left' || anchor === 'right' ? 'x' : 'y'; const scrollPosition = Math.round(shape[axisProperties.scrollPosition[axis]]); const areNotAtStart = scrollPosition > 0; const areNotAtEnd = scrollPosition + shape[axisProperties.clientLength[axis]] < shape[axisProperties.scrollLength[axis]]; if ((goingForward && areNotAtEnd) || (!goingForward && areNotAtStart)) { return true; } return false; }); } const iOS = typeof navigator !== 'undefined' && /iPad|iPhone|iPod/.test(navigator.userAgent); const SwipeableDrawer = React.forwardRef(function SwipeableDrawer(inProps, ref) { const props = useThemeProps({ name: 'MuiSwipeableDrawer', props: inProps }); const theme = useTheme(); const transitionDurationDefault = { enter: theme.transitions.duration.enteringScreen, exit: theme.transitions.duration.leavingScreen, }; const { anchor = 'left', disableBackdropTransition = false, disableDiscovery = false, disableSwipeToOpen = iOS, hideBackdrop, hysteresis = 0.52, allowSwipeInChildren = false, minFlingVelocity = 450, ModalProps: { BackdropProps, ...ModalPropsProp } = {}, onClose, onOpen, open = false, PaperProps = {}, SwipeAreaProps, swipeAreaWidth = 20, transitionDuration = transitionDurationDefault, variant = 'temporary', // Mobile first. ...other } = props; const [maybeSwiping, setMaybeSwiping] = React.useState(false); const swipeInstance = React.useRef({ isSwiping: null, }); const swipeAreaRef = React.useRef(); const backdropRef = React.useRef(); const paperRef = React.useRef(); const handleRef = useForkRef(PaperProps.ref, paperRef); const touchDetected = React.useRef(false); // 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(() => { calculatedDurationRef.current = null; }, [open]); const setPosition = React.useCallback( (translate, options = {}) => { const { mode = null, changeTransition = true } = options; const anchorRtl = getAnchor(theme, anchor); const rtlTranslateMultiplier = ['right', 'bottom'].indexOf(anchorRtl) !== -1 ? 1 : -1; const horizontalSwipe = isHorizontal(anchor); const transform = horizontalSwipe ? `translate(${rtlTranslateMultiplier * translate}px, 0)` : `translate(0, ${rtlTranslateMultiplier * translate}px)`; const drawerStyle = paperRef.current.style; drawerStyle.webkitTransform = transform; drawerStyle.transform = transform; let transition = ''; if (mode) { transition = theme.transitions.create( 'all', getTransitionProps( { easing: undefined, style: undefined, timeout: transitionDuration, }, { mode, }, ), ); } if (changeTransition) { drawerStyle.webkitTransition = transition; drawerStyle.transition = transition; } if (!disableBackdropTransition && !hideBackdrop) { const backdropStyle = backdropRef.current.style; backdropStyle.opacity = 1 - translate / getMaxTranslate(horizontalSwipe, paperRef.current); if (changeTransition) { backdropStyle.webkitTransition = transition; backdropStyle.transition = transition; } } }, [anchor, disableBackdropTransition, hideBackdrop, theme, transitionDuration], ); const handleBodyTouchEnd = useEventCallback((nativeEvent) => { if (!touchDetected.current) { return; } claimedSwipeInstance = null; touchDetected.current = false; ReactDOM.flushSync(() => { setMaybeSwiping(false); }); // The swipe wasn't started. if (!swipeInstance.current.isSwiping) { swipeInstance.current.isSwiping = null; return; } swipeInstance.current.isSwiping = null; const anchorRtl = getAnchor(theme, anchor); const horizontal = isHorizontal(anchor); let current; if (horizontal) { current = calculateCurrentX( anchorRtl, nativeEvent.changedTouches, ownerDocument(nativeEvent.currentTarget), ); } else { current = calculateCurrentY( anchorRtl, nativeEvent.changedTouches, ownerWindow(nativeEvent.currentTarget), ); } 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; 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 (open) { if (swipeInstance.current.velocity > minFlingVelocity || translateRatio > hysteresis) { onClose(); } else { // Reset the position, the swipe was aborted. setPosition(0, { mode: 'exit', }); } return; } 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 startMaybeSwiping = (force = false) => { if (!maybeSwiping) { // on Safari Mobile, if you want to be able to have the 'click' event fired on child elements, nothing in the DOM can be changed. // this is because Safari Mobile will not fire any mouse events (still fires touch though) if the DOM changes during mousemove. // so do this change on first touchmove instead of touchstart if (force || !(disableDiscovery && allowSwipeInChildren)) { ReactDOM.flushSync(() => { setMaybeSwiping(true); }); } const horizontalSwipe = isHorizontal(anchor); if (!open && paperRef.current) { // The ref may be null when a parent component updates while swiping. setPosition( getMaxTranslate(horizontalSwipe, paperRef.current) + (disableDiscovery ? 15 : -DRAG_STARTED_SIGNAL), { changeTransition: false, }, ); } swipeInstance.current.velocity = 0; swipeInstance.current.lastTime = null; swipeInstance.current.lastTranslate = null; swipeInstance.current.paperHit = false; touchDetected.current = true; } }; const handleBodyTouchMove = useEventCallback((nativeEvent) => { // the ref may be null when a parent component updates while swiping if (!paperRef.current || !touchDetected.current) { return; } // We are not supposed to handle this touch move because the swipe was started in a scrollable container in the drawer if (claimedSwipeInstance !== null && claimedSwipeInstance !== swipeInstance.current) { return; } startMaybeSwiping(true); const anchorRtl = getAnchor(theme, anchor); const horizontalSwipe = isHorizontal(anchor); const currentX = calculateCurrentX( anchorRtl, nativeEvent.touches, ownerDocument(nativeEvent.currentTarget), ); const currentY = calculateCurrentY( anchorRtl, nativeEvent.touches, ownerWindow(nativeEvent.currentTarget), ); if (open && paperRef.current.contains(nativeEvent.target) && claimedSwipeInstance === null) { const domTreeShapes = getDomTreeShapes(nativeEvent.target, paperRef.current); const hasNativeHandler = computeHasNativeHandler({ domTreeShapes, start: horizontalSwipe ? swipeInstance.current.startX : swipeInstance.current.startY, current: horizontalSwipe ? currentX : currentY, anchor, }); if (hasNativeHandler) { claimedSwipeInstance = true; return; } claimedSwipeInstance = swipeInstance.current; } // 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 definitelySwiping = horizontalSwipe ? dx > dy && dx > UNCERTAINTY_THRESHOLD : dy > dx && dy > UNCERTAINTY_THRESHOLD; if (definitelySwiping && nativeEvent.cancelable) { nativeEvent.preventDefault(); } if ( definitelySwiping === true || (horizontalSwipe ? dy > UNCERTAINTY_THRESHOLD : dx > UNCERTAINTY_THRESHOLD) ) { swipeInstance.current.isSwiping = definitelySwiping; if (!definitelySwiping) { handleBodyTouchEnd(nativeEvent); return; } // 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 && !open) { if (horizontalSwipe) { swipeInstance.current.startX -= DRAG_STARTED_SIGNAL; } else { swipeInstance.current.startY -= DRAG_STARTED_SIGNAL; } } } } if (!swipeInstance.current.isSwiping) { return; } 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; } } if (swipeInstance.current.lastTranslate === null) { swipeInstance.current.lastTranslate = translate; swipeInstance.current.lastTime = performance.now() + 1; } const velocity = ((translate - swipeInstance.current.lastTranslate) / (performance.now() - swipeInstance.current.lastTime)) * 1e3; // Low Pass filter. swipeInstance.current.velocity = swipeInstance.current.velocity * 0.4 + velocity * 0.6; swipeInstance.current.lastTranslate = translate; swipeInstance.current.lastTime = performance.now(); // We are swiping, let's prevent the scroll event on iOS. if (nativeEvent.cancelable) { nativeEvent.preventDefault(); } setPosition(translate); }); const handleBodyTouchStart = useEventCallback((nativeEvent) => { // We are not supposed to handle this touch move. // Example of use case: ignore the event if there is a Slider. if (nativeEvent.defaultPrevented) { return; } // We can only have one node at the time claiming ownership for handling the swipe. if (nativeEvent.defaultMuiPrevented) { return; } // At least one element clogs the drawer interaction zone. if ( open && (hideBackdrop || !backdropRef.current.contains(nativeEvent.target)) && !paperRef.current.contains(nativeEvent.target) ) { return; } const anchorRtl = getAnchor(theme, anchor); const horizontalSwipe = isHorizontal(anchor); const currentX = calculateCurrentX( anchorRtl, nativeEvent.touches, ownerDocument(nativeEvent.currentTarget), ); const currentY = calculateCurrentY( anchorRtl, nativeEvent.touches, ownerWindow(nativeEvent.currentTarget), ); if (!open) { // logic for if swipe should be ignored: // if disableSwipeToOpen // if target != swipeArea, and target is not a child of paper ref // if is a child of paper ref, and `allowSwipeInChildren` does not allow it if ( disableSwipeToOpen || !( nativeEvent.target === swipeAreaRef.current || (paperRef.current?.contains(nativeEvent.target) && (typeof allowSwipeInChildren === 'function' ? allowSwipeInChildren(nativeEvent, swipeAreaRef.current, paperRef.current) : allowSwipeInChildren)) ) ) { return; } if (horizontalSwipe) { if (currentX > swipeAreaWidth) { return; } } else if (currentY > swipeAreaWidth) { return; } } nativeEvent.defaultMuiPrevented = true; claimedSwipeInstance = null; swipeInstance.current.startX = currentX; swipeInstance.current.startY = currentY; startMaybeSwiping(); }); React.useEffect(() => { if (variant === 'temporary') { const doc = ownerDocument(paperRef.current); doc.addEventListener('touchstart', handleBodyTouchStart); // A blocking listener prevents Firefox's navbar to auto-hide on scroll. // It only needs to prevent scrolling on the drawer's content when open. // When closed, the overlay prevents scrolling. doc.addEventListener('touchmove', handleBodyTouchMove, { passive: false }); doc.addEventListener('touchend', handleBodyTouchEnd); return () => { doc.removeEventListener('touchstart', handleBodyTouchStart); doc.removeEventListener('touchmove', handleBodyTouchMove, { passive: false }); doc.removeEventListener('touchend', handleBodyTouchEnd); }; } return undefined; }, [variant, open, handleBodyTouchStart, handleBodyTouchMove, handleBodyTouchEnd]); React.useEffect( () => () => { // We need to release the lock. if (claimedSwipeInstance === swipeInstance.current) { claimedSwipeInstance = null; } }, [], ); React.useEffect(() => { if (!open) { setMaybeSwiping(false); } }, [open]); return ( {!disableSwipeToOpen && variant === 'temporary' && ( )} ); }); SwipeableDrawer.propTypes /* remove-proptypes */ = { // ----------------------------- Warning -------------------------------- // | These PropTypes are generated from the TypeScript type definitions | // | To update them edit the d.ts file and run "yarn proptypes" | // ---------------------------------------------------------------------- /** * If set to true, the swipe event will open the drawer even if the user begins the swipe on one of the drawer's children. * This can be useful in scenarios where the drawer is partially visible. * You can customize it further with a callback that determines which children the user can drag over to open the drawer * (for example, to ignore other elements that handle touch move events, like sliders). * * @param {TouchEvent} event The 'touchstart' event * @param {HTMLDivElement} swipeArea The swipe area element * @param {HTMLDivElement} paper The drawer's paper element * * @default false */ allowSwipeInChildren: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]), /** * @ignore */ anchor: PropTypes.oneOf(['bottom', 'left', 'right', 'top']), /** * The content of the component. */ children: PropTypes.node, /** * Disable the backdrop transition. * This can improve the FPS on low-end devices. * @default false */ disableBackdropTransition: PropTypes.bool, /** * If `true`, touching the screen near the edge of the drawer will not slide in the drawer a bit * to promote accidental discovery of the swipe gesture. * @default false */ disableDiscovery: PropTypes.bool, /** * If `true`, swipe to open is disabled. This is useful in browsers where swiping triggers * navigation actions. Swipe to open is disabled on iOS browsers by default. * @default typeof navigator !== 'undefined' && /iPad|iPhone|iPod/.test(navigator.userAgent) */ disableSwipeToOpen: PropTypes.bool, /** * @ignore */ hideBackdrop: PropTypes.bool, /** * Affects how far the drawer must be opened/closed to change its state. * Specified as percent (0-1) of the width of the drawer * @default 0.52 */ hysteresis: PropTypes.number, /** * Defines, from which (average) velocity on, the swipe is * defined as complete although hysteresis isn't reached. * Good threshold is between 250 - 1000 px/s * @default 450 */ minFlingVelocity: PropTypes.number, /** * @ignore */ ModalProps: PropTypes /* @typescript-to-proptypes-ignore */.shape({ BackdropProps: PropTypes.shape({ component: elementTypeAcceptingRef, }), }), /** * Callback fired when the component requests to be closed. * * @param {object} event The event source of the callback. */ onClose: PropTypes.func.isRequired, /** * Callback fired when the component requests to be opened. * * @param {object} event The event source of the callback. */ onOpen: PropTypes.func.isRequired, /** * If `true`, the component is shown. * @default false */ open: PropTypes.bool.isRequired, /** * @ignore */ PaperProps: PropTypes /* @typescript-to-proptypes-ignore */.shape({ component: elementTypeAcceptingRef, style: PropTypes.object, }), /** * The element is used to intercept the touch events on the edge. */ SwipeAreaProps: PropTypes.object, /** * The width of the left most (or right most) area in `px` that * the drawer can be swiped open from. * @default 20 */ swipeAreaWidth: PropTypes.number, /** * The duration for the transition, in milliseconds. * You may specify a single timeout for all transitions, or individually with an object. * @default { * enter: theme.transitions.duration.enteringScreen, * exit: theme.transitions.duration.leavingScreen, * } */ transitionDuration: PropTypes.oneOfType([ PropTypes.number, PropTypes.shape({ appear: PropTypes.number, enter: PropTypes.number, exit: PropTypes.number, }), ]), /** * @ignore */ variant: PropTypes.oneOf(['permanent', 'persistent', 'temporary']), }; export default SwipeableDrawer; ``` SwipeArea.js ``` 'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; import clsx from 'clsx'; import { styled } from '@mui/material/styles' import { unstable_capitalize as capitalize } from '@mui/utils'; const oppositeDirection = { left: 'right', right: 'left', top: 'down', bottom: 'up', }; function isHorizontal(anchor) { return ['left', 'right'].indexOf(anchor) !== -1; } function getAnchor(theme, anchor) { return theme.direction === 'rtl' && isHorizontal(anchor) ? oppositeDirection[anchor] : anchor; } const SwipeAreaRoot = styled('div')(({ theme, ownerState }) => ({ position: 'fixed', top: 0, left: 0, bottom: 0, zIndex: theme.zIndex.drawer - 1, ...(ownerState.anchor === 'left' && { right: 'auto', }), ...(ownerState.anchor === 'right' && { left: 'auto', right: 0, }), ...(ownerState.anchor === 'top' && { bottom: 'auto', right: 0, }), ...(ownerState.anchor === 'bottom' && { top: 'auto', bottom: 0, right: 0, }), })); /** * @ignore - internal component. */ const SwipeArea = React.forwardRef(function SwipeArea(props, ref) { const { anchor, classes = {}, className, width, style, ...other } = props; const ownerState = props; return ( ); }); SwipeArea.propTypes = { /** * Side on which to attach the discovery area. */ anchor: PropTypes.oneOf(['left', 'top', 'right', 'bottom']).isRequired, /** * @ignore */ classes: PropTypes.object, /** * @ignore */ className: PropTypes.string, /** * @ignore */ style: PropTypes.object, /** * The width of the left most (or right most) area in `px` where the * drawer can be swiped open from. */ width: PropTypes.number.isRequired, }; export default SwipeArea; ```

The error is not appearing on consecutive refreshes of the page, I had to restart the server, this is why I got the false positive that it works above.

lucas2005gao commented 9 months ago

Having the same issue displaying the SwipableDrawer Component on top of a react FullCalendar component.

Upon opening the SwipableDrawer I get SwipeableDrawer.js?3240:373 Unable to preventDefault inside passive event listener invocation. error in the console

khashashin commented 1 month ago

Any update on this? I'm using SwipeableDrawer positioned above a Leaflet map, and I'm experiencing an issue with opening the drawer using the "notch" or puller. When the drawer is initially closed, trying to open it by swiping on the puller results in the swipe action being canceled, causing the drawer to snap back to its closed position.

I've set up the drawer with customized SwipeAreaProps and the puller, which works as expected when the drawer is open. However, when closed, the swipe action to open feels inconsistent and frequently interrupted.

Here’s the component code:

Click to expand ```tsx import { ActionSheetStates, useSidebarStore } from '@/hooks/useSidebarStore.tsx'; import { Box, IconButton, Stack, styled, SwipeableDrawer, Typography } from '@mui/material'; import { Global } from '@emotion/react'; import Sidebar from '@/pages/App/Sidebar.tsx'; import CloseIcon from '@mui/icons-material/Close'; import { useAddressStore } from '@/hooks/useAddressStore.tsx'; import AddressSearch from '@/pages/App/AddressSearch.tsx'; import { useTranslation } from 'react-i18next'; const Root = styled('div')(() => ({ height: '100%' })); const Puller = styled('div')(({ theme }) => ({ width: 40, height: 6, backgroundColor: theme.palette.primary.main, borderRadius: 3, position: 'absolute', top: 18, left: 'calc(50% - 20px)' })); const AddressInfo = ({ label, perimeter }: { label: string; perimeter: string | null | undefined; }) => { const { t } = useTranslation('app'); return ( {label} {perimeter && {t(`decision-tree-perimeter-${perimeter}`)}} {!perimeter && {t('no-district-heating-found')}} ); }; const drawerBleeding = 112; export default function SidebarMobile() { const isOpen = useSidebarStore((state) => state.isOpen); const toggle = useSidebarStore((state) => state.toggle); const address = useAddressStore((state) => state.address); const decisionTree = useAddressStore((state) => state.decisionTree); const setAddress = useAddressStore((state) => state.setAddress); const actionSheetState = useSidebarStore((state) => state.actionSheetState); const setActionSheetState = useSidebarStore((state) => state.setActionSheetState); const iOS = typeof navigator !== 'undefined' && /iPad|iPhone|iPod/.test(navigator.userAgent); const toggleDrawer = (open: boolean) => (_event: any) => { if (_event && _event.type === 'keydown' && (_event.key === 'Tab' || _event.key === 'Shift')) { return; } toggle(open ? 'show' : 'hide'); }; return ( .MuiPaper-root': { height: `calc(${actionSheetState}% - ${drawerBleeding}px)`, overflow: 'visible' } }} /> {address && ( { setAddress(null); setActionSheetState(ActionSheetStates.IDLE); toggle('hide'); }}> )} {!isOpen && ( <> {address && } {!address && ( { toggle('show'); setActionSheetState(ActionSheetStates.SEARCH); }}> )} )} {isOpen && ( <> {!address && } {address && } )} ); }
khashashin commented 1 month ago

Just tested adding { passive: false } as shown here: https://github.com/mui/material-ui/issues/37814#issuecomment-1693421592 and I'm not getting errors anymore