Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SwipeableDrawer] Add callback to customise touchstart ignore for swipeable drawer #30759

Merged
merged 24 commits into from
Jan 23, 2023
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
fae7925
Add callback to customise touchstart ignore for swipeable drawer
tech-meppem Jan 24, 2022
26cee90
Fixes for tests
tech-meppem Jan 24, 2022
251fd02
Merge branch 'mui-org:master' into swipe-ignore
tech-meppem Jan 24, 2022
380d88d
Merge remote-tracking branch 'upstream/master' into swipe-ignore
tech-meppem Feb 8, 2022
cdc91e7
Update for migration PR
tech-meppem Feb 8, 2022
06e42d9
Merge remote-tracking branch 'upstream/master' into swipe-ignore
tech-meppem Feb 25, 2022
36e3e10
Changed to be 'AllowSwipeInChildren'
tech-meppem Feb 25, 2022
9958424
Merge remote-tracking branch 'upstream/master' into swipe-ignore
tech-meppem Feb 28, 2022
dce8162
Merge remote-tracking branch 'upstream/master' into swipe-ignore
tech-meppem Mar 8, 2022
05cefd6
Merge remote-tracking branch 'upstream/master' into swipe-ignore
tech-meppem Mar 17, 2022
74d2c81
Fix iOS click bug
tech-meppem Mar 22, 2022
51ecfff
Merge remote-tracking branch 'upstream/master' into swipe-ignore
tech-meppem Mar 22, 2022
a1c39e5
Merge remote-tracking branch 'upstream/master' into swipe-ignore
tech-meppem Apr 19, 2022
3ea297a
Merge remote-tracking branch 'upstream/master' into swipe-ignore
tech-meppem May 27, 2022
4411ecf
Change 'AllowSwipeInChildren' to 'allowSwipeInChildren'
tech-meppem May 27, 2022
97e848b
Merge remote-tracking branch 'upstream/master' into swipe-ignore
tech-meppem Jun 27, 2022
214221b
Merge remote-tracking branch 'upstream/master' into swipe-ignore
tech-meppem Aug 30, 2022
987a7a0
Added tests for allowSwipeInChildren prop
tech-meppem Aug 30, 2022
1dfaf72
prettier fixes
tech-meppem Aug 30, 2022
93704ac
Merge branch 'master' into swipe-ignore
tech-meppem Sep 28, 2022
fa7b76e
SwipeableDrawer: fix missing bool for iOS fix
tech-meppem Sep 30, 2022
d48c9ee
Merge remote-tracking branch 'upstream/master' into swipe-ignore
michaldudak Dec 28, 2022
6445cd4
updated comment for allowSwipeInChildren
tech-meppem Jan 3, 2023
20877e1
Merge branch 'master' into swipe-ignore
mnajdova Jan 17, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/pages/material-ui/api/swipeable-drawer.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
"props": {
"onClose": { "type": { "name": "func" }, "required": true },
"onOpen": { "type": { "name": "func" }, "required": true },
"allowSwipeInChildren": {
"type": { "name": "union", "description": "bool<br>&#124;&nbsp;func" },
"default": "false"
},
"children": { "type": { "name": "node" } },
"disableBackdropTransition": { "type": { "name": "bool" } },
"disableDiscovery": { "type": { "name": "bool" } },
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"componentDescription": "",
"propDescriptions": {
"allowSwipeInChildren": "Callback to determine what children of the drawer the user can use to drag open the drawer. Can be a custom function to control the behavior. Set or return true / false to allow / disallow the swipe event.",
"children": "O conteúdo do componente.",
"disableBackdropTransition": "Disable the backdrop transition. This can improve the FPS on low-end devices.",
"disableDiscovery": "If <code>true</code>, 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.",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"componentDescription": "",
"propDescriptions": {
"allowSwipeInChildren": "Callback to determine what children of the drawer the user can use to drag open the drawer. Can be a custom function to control the behavior. Set or return true / false to allow / disallow the swipe event.",
"children": "The content of the component.",
"disableBackdropTransition": "Disable the backdrop transition. This can improve the FPS on low-end devices.",
"disableDiscovery": "If <code>true</code>, 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.",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"componentDescription": "",
"propDescriptions": {
"allowSwipeInChildren": "If set to true, the swipe event will open the drawer even if the user begins the swipe on one of the drawer&#39;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).",
"children": "The content of the component.",
"disableBackdropTransition": "Disable the backdrop transition. This can improve the FPS on low-end devices.",
"disableDiscovery": "If <code>true</code>, 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.",
Expand Down
15 changes: 15 additions & 0 deletions packages/mui-material/src/SwipeableDrawer/SwipeableDrawer.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@ import * as React from 'react';
import { DrawerProps } from '../Drawer';

export interface SwipeableDrawerProps extends Omit<DrawerProps, 'onClose' | 'open'> {
/**
* 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?:
| boolean
| ((e: TouchEvent, swipeArea: HTMLDivElement, paper: HTMLDivElement) => boolean);
/**
* Disable the backdrop transition.
* This can improve the FPS on low-end devices.
Expand Down
88 changes: 65 additions & 23 deletions packages/mui-material/src/SwipeableDrawer/SwipeableDrawer.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ const SwipeableDrawer = React.forwardRef(function SwipeableDrawer(inProps, ref)
disableSwipeToOpen = iOS,
hideBackdrop,
hysteresis = 0.52,
allowSwipeInChildren = false,
minFlingVelocity = 450,
ModalProps: { BackdropProps, ...ModalPropsProp } = {},
onClose,
Expand Down Expand Up @@ -301,6 +302,39 @@ const SwipeableDrawer = React.forwardRef(function SwipeableDrawer(inProps, ref)
}
});

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)) {
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) {
Expand All @@ -312,6 +346,8 @@ const SwipeableDrawer = React.forwardRef(function SwipeableDrawer(inProps, ref)
return;
}

startMaybeSwiping(true);

const anchorRtl = getAnchor(theme, anchor);
const horizontalSwipe = isHorizontal(anchor);

Expand Down Expand Up @@ -477,7 +513,20 @@ const SwipeableDrawer = React.forwardRef(function SwipeableDrawer(inProps, ref)
);

if (!open) {
if (disableSwipeToOpen || nativeEvent.target !== swipeAreaRef.current) {
// 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) {
Expand All @@ -494,27 +543,7 @@ const SwipeableDrawer = React.forwardRef(function SwipeableDrawer(inProps, ref)
swipeInstance.current.startX = currentX;
swipeInstance.current.startY = currentY;

flushSync(() => {
setMaybeSwiping(true);
});

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;
startMaybeSwiping();
});

React.useEffect(() => {
Expand Down Expand Up @@ -574,7 +603,7 @@ const SwipeableDrawer = React.forwardRef(function SwipeableDrawer(inProps, ref)
PaperProps={{
...PaperProps,
style: {
pointerEvents: variant === 'temporary' && !open ? 'none' : '',
pointerEvents: variant === 'temporary' && !open && !allowSwipeInChildren ? 'none' : '',
...PaperProps.style,
},
ref: handleRef,
Expand Down Expand Up @@ -604,6 +633,19 @@ SwipeableDrawer.propTypes /* remove-proptypes */ = {
// | 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
*/
Expand Down
189 changes: 181 additions & 8 deletions packages/mui-material/src/SwipeableDrawer/SwipeableDrawer.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -501,17 +501,190 @@ describe('<SwipeableDrawer />', () => {
expect(document.querySelector('[class*=PrivateSwipeArea-root]')).not.to.equal(null);
});

it('should be able to attach paper ref passed through PaperProps', () => {
const ref = React.createRef();
render(
<SwipeableDrawer onOpen={() => {}} onClose={() => {}} PaperProps={{ ref }} open>
<div />
</SwipeableDrawer>,
);
expect(ref.current).not.to.equal(null);
const openTouchesForSwipingChildren = [
{ pageX: 0, clientY: windowHeight - 20 },
{ pageX: 0, clientY: windowHeight - 60 },
{ pageX: 0, clientY: windowHeight - 180 },
];

const handleHeight = 60;

describe('prop: allowSwipeInChildren', () => {
it('should allow swiping on children to open', () => {
const handleOpen = spy();
render(
<SwipeableDrawer
anchor={'bottom'}
allowSwipeInChildren
onOpen={handleOpen}
onClose={() => {}}
open={false}
swipeAreaWidth={20}
SwipeAreaProps={{
style: {
// ensure clicks will not be grabbed by swipe area to ensure testing just this functionality
pointerEvents: 'none',
},
}}
PaperProps={{ component: FakePaper }}
ModalProps={{
keepMounted: true,
sx: {
transform: `translateY(${handleHeight}px) !important`,
},
}}
>
<div data-testid="drawer" style={{ position: 'relative', pointerEvents: 'all' }}>
<div
data-testid="handle"
style={{
position: 'absolute',
height: `${handleHeight}px`,
marginTop: `-${handleHeight}px`,
}}
>
SwipeableDrawer
</div>
</div>
</SwipeableDrawer>,
);

const handle = screen.getAllByTestId('handle').slice(-1)[0];

fireEvent.touchStart(handle, {
touches: [
new Touch({ identifier: 0, target: handle, ...openTouchesForSwipingChildren[0] }),
],
});
fireEvent.touchMove(handle, {
touches: [
new Touch({ identifier: 0, target: handle, ...openTouchesForSwipingChildren[1] }),
],
});
fireEvent.touchMove(handle, {
touches: [
new Touch({ identifier: 0, target: handle, ...openTouchesForSwipingChildren[2] }),
],
});
fireEvent.touchEnd(handle, {
changedTouches: [
new Touch({ identifier: 0, target: handle, ...openTouchesForSwipingChildren[2] }),
],
});
expect(handleOpen.callCount).to.equal(1);
});

it('should not allow swiping on children to open that are excluded via a function', () => {
const handleOpen = spy();
render(
<SwipeableDrawer
anchor={'bottom'}
allowSwipeInChildren={(e) => {
const elem = e.target;
// ignore touch events from .ignore &^ from swipe area
return (
!elem.classList.contains('ignore') &&
!elem.classList.contains('PrivateSwipeArea-root')
);
}}
onOpen={handleOpen}
onClose={() => {}}
open={false}
swipeAreaWidth={20}
SwipeAreaProps={{
style: {
// ensure clicks will not be grabbed by swipe area to ensure testing just this functionality
pointerEvents: 'none',
},
}}
PaperProps={{ component: FakePaper }}
ModalProps={{
keepMounted: true,
sx: {
'& > *': {
pointerEvents: 'auto',
},
},
}}
>
<div
className="ignore"
data-testid="drawer"
style={{ position: 'relative', height: '40px', pointerEvents: 'all' }}
>
<div
data-testid="handle"
style={{ position: 'absolute', height: '40px', marginTop: '-40px' }}
>
SwipeableDrawer
</div>
</div>
</SwipeableDrawer>,
);

// should ignore the drawer touch events
const drawer = screen.getAllByTestId('drawer').slice(-1)[0];

fireEvent.touchStart(drawer, {
touches: [
new Touch({ identifier: 0, target: drawer, ...openTouchesForSwipingChildren[0] }),
],
});
fireEvent.touchMove(drawer, {
touches: [
new Touch({ identifier: 0, target: drawer, ...openTouchesForSwipingChildren[1] }),
],
});
fireEvent.touchMove(drawer, {
touches: [
new Touch({ identifier: 0, target: drawer, ...openTouchesForSwipingChildren[2] }),
],
});
fireEvent.touchEnd(drawer, {
changedTouches: [
new Touch({ identifier: 0, target: drawer, ...openTouchesForSwipingChildren[2] }),
],
});
expect(handleOpen.callCount).to.equal(0);

// should allow opening the drawer via handle
const handle = screen.getAllByTestId('handle').slice(-1)[0];

fireEvent.touchStart(handle, {
touches: [
new Touch({ identifier: 0, target: handle, ...openTouchesForSwipingChildren[0] }),
],
});
fireEvent.touchMove(handle, {
touches: [
new Touch({ identifier: 0, target: handle, ...openTouchesForSwipingChildren[1] }),
],
});
fireEvent.touchMove(handle, {
touches: [
new Touch({ identifier: 0, target: handle, ...openTouchesForSwipingChildren[2] }),
],
});
fireEvent.touchEnd(handle, {
changedTouches: [
new Touch({ identifier: 0, target: handle, ...openTouchesForSwipingChildren[2] }),
],
});
expect(handleOpen.callCount).to.equal(1);
});
});
});

it('should be able to attach paper ref passed through PaperProps', () => {
const ref = React.createRef();
render(
<SwipeableDrawer onOpen={() => {}} onClose={() => {}} PaperProps={{ ref }} open>
<div />
</SwipeableDrawer>,
);
expect(ref.current).not.to.equal(null);
});

describe('disableSwipeToOpen', () => {
it('should not support swipe to open if disableSwipeToOpen is set', () => {
const handleOpen = spy();
Expand Down