-
Notifications
You must be signed in to change notification settings - Fork 147
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
Swipe to delete/remove #299
Comments
UX example video on that page https://www.npmjs.com/package/react-swipe-to-delete-ios I would like to use a large, general purpose component/library like this one to achieve a similar effect. |
@strarsis Did you come up with a solution? |
@isaachinman: I had to pause the implementation because of incompatibilities of an older react version used by the app. |
I came up with an opinionated <SwipeActions.Container>
<SwipeActions.Content>
Your list row content
</SwipeActions.Content>
<SwipeActions.Action
side='left'
action={leftAction}
>
Left action content revealed on swipe
</SwipeActions.Action>
<SwipeActions.Action
side='right'
action={rightAction}
destructive
>
Right action content revealed on swipe
</SwipeActions.Action>
</SwipeActions.Container> It's rather opinionated/app-specific, as you say. If it's useful for anyone, I can clean it up a bit and publish on npm. The only dependency is |
That would be great! I want to use this in a react-admin app records list. |
@strarsis Was thinking about this a bit more today – I don't really have the time or interest in maintaining an opinionated UI package at the moment. That said, I will share what I've written. I think the styling of these kind of swipe actions will be different with every implementation. Some people will want two actions on each side, etc... All that I think Bear in mind that I use PandaCSS for all styling and had to rip that stuff out, and replaced it with I wrote all of this delta calculation logic from scratch, and I wouldn't be surprised if it's flawed 😄 You can see the usage pattern above. This is a pretty weird way to write a React component, I will admit. Basically the props from the actual There will be a million ways to achieve the same thing, but hopefully this helps someone out a bit. import React, { PropsWithChildren, useCallback, useRef, useState } from 'react'
import { useSwipeable } from 'react-swipeable'
type ActionProps = PropsWithChildren<{
action: () => Promise<void>
destructive?: boolean
side: 'left' | 'right'
}>
export const Action: React.FC<ActionProps> = ({
action: _action,
children,
destructive: _destructive,
side: _side,
}) => (
<div
style={{
height: '100%',
position: 'absolute',
width: '100%',
}}
>
{children}
</div>
)
export const Content: React.FC<PropsWithChildren> = ({ children }) => (
<div
style={{
width: '100%',
}}
>
{children}
</div>
)
export const Container: React.FC<PropsWithChildren> = ({ children }) => {
const container = useRef<HTMLDivElement>(null)
const leftActionContainer = useRef<HTMLDivElement>(null)
const rightActionContainer = useRef<HTMLDivElement>(null)
const xStart = useRef(0)
const xDelta = useRef(0)
const rem = 16
const animationDuration = 50
const [swipeInProgress, setSwipeInProgress] = useState(false)
const [animateToSnap, setAnimateToSnap] = useState(true)
const snapPoints = [
{ point: 0, type: 'start' },
{ point: 6 * rem, type: 'open' },
{ point: 10 * rem, type: 'end' },
]
const setXTransform = () => {
if (container.current) {
container.current.style.transform = `translateX(${xDelta.current}px)`
}
}
const childrenArray = React.Children.toArray(children) as React.ReactElement[]
const actionChildren = childrenArray.filter(
x => React.isValidElement(x) && x.type === Action,
)
const contentChild = childrenArray.find(
x => React.isValidElement(x) && x.type === Content,
)
const leftActionContent = actionChildren.find(x => x.props.side === 'left')
const rightActionContent = actionChildren.find(x => x.props.side === 'right')
const leftAction = leftActionContent?.props.action
const rightAction = rightActionContent?.props.action
const callAction = useCallback((direction: 'left' | 'right') => {
const action = direction === 'right' ? rightAction : leftAction
const destructive = Boolean(direction === 'right' ? rightActionContent?.props.destructive : leftActionContent?.props.destructive)
if (action) {
if (destructive === true) {
xDelta.current = direction === 'right' ? -window.innerWidth : window.innerWidth
setXTransform()
}
setTimeout(async () => {
await action()
if (destructive === false) {
xDelta.current = 0
setXTransform()
}
}, animationDuration)
}
}, [leftActionContent, rightActionContent])
const swipeHandler = useSwipeable({
delta: 0,
onSwipeStart: () => {
xStart.current = xDelta.current
setAnimateToSnap(false)
setSwipeInProgress(true)
},
onSwiped: () => {
const direction = xDelta.current < 0 ? 'right' : 'left'
const absXDelta = Math.abs(xDelta.current)
let [lastSnapPointPassed] = snapPoints
for (const snapPoint of snapPoints) {
if (absXDelta >= snapPoint.point) {
lastSnapPointPassed = snapPoint
}
}
setAnimateToSnap(true)
if (lastSnapPointPassed.type === 'end') {
callAction(direction)
} else {
xDelta.current = direction === 'right' ? -lastSnapPointPassed.point : lastSnapPointPassed.point
setXTransform()
}
setSwipeInProgress(false)
},
onSwiping: ({ deltaX, dir }) => {
if (dir === 'Left' || dir === 'Right') {
xDelta.current = Math.round(xStart.current + deltaX)
if (!leftAction) {
xDelta.current = Math.min(xDelta.current, 0)
}
if (!rightAction) {
xDelta.current = Math.max(xDelta.current, 0)
}
setXTransform()
if (rightActionContainer.current && leftActionContainer.current) {
if (xDelta.current < 0) {
rightActionContainer.current.style.display = 'flex'
leftActionContainer.current.style.display = 'none'
} else if (xDelta.current > 0) {
leftActionContainer.current.style.display = 'flex'
rightActionContainer.current.style.display = 'none'
}
}
}
},
})
return (
<div
{...swipeHandler}
style={{
maxWidth: '100%',
touchAction: swipeInProgress ? 'pan-x' : 'pan-y',
width: '100%',
}}
>
<div
ref={container}
style={{
transitionDuration: animateToSnap ? `${animationDuration}ms` : undefined,
transitionProperty: 'transform, background-color',
transitionTimingFunction: 'ease-in-out',
width: '100%',
}}
>
{contentChild}
</div>
<div
id='swipe-container'
style={{
backgroundColor: 'grey',
display: 'flex',
flexDirection: 'row',
height: '100%',
left: '0',
position: 'absolute',
top: '0',
width: '100%',
zIndex: '-1',
}}
>
{leftAction && (
<div
ref={leftActionContainer}
onClick={() => callAction('left')}
style={{
display: 'none',
height: '100%',
marginRight: 'auto',
width: 6 * rem,
}}
>
{leftActionContent}
</div>
)}
{rightAction && (
<div
ref={rightActionContainer}
onClick={() => callAction('right')}
style={{
display: 'none',
height: '100%',
marginLeft: 'auto',
width: 6 * rem,
}}
>
{rightActionContent}
</div>
)}
</div>
</div>
)
}
export const SwipeActions = {
Action,
Container,
Content,
} |
How can the swipe-to-delete gesture implemented with this? When the user swipes the item over a specific threshold, the delete action should be triggered and the item visually collapse to indicate that is was deleted.
The text was updated successfully, but these errors were encountered: