35. Swipe to delete
Quite a complicated example that combines layout
animations, useAnimate()
, animating a Motion value with animate()
, and info provided by an onDragEnd()
event.
Code component A few details in the Item()
component:
When the dragging stops (onDragEnd()
event), the handleDragEnd()
function looks at the current offset
and velocity
(values provided by the event).
When one of those is high enough:A useAnimate()
animation will slide the item offscreen,
and after a small setTimeout()
, the onDelete()
function in the parent component is called.
The items have a layout
property so that they animate to their new position automatically , with springy transition
settings. const initialItems = [ 0 , 1 , 2 , 3 , 4 ]
const height = 70
const padding = 10
const size = 150
function Item ( { total, index, onDelete } ) {
const [ scope, animate] = useAnimate ( )
function handleDragEnd ( event, info ) {
const offset = info. offset. x
const velocity = info. velocity. x
if ( offset < - 100 || velocity < - 500 ) {
animate ( scope. current, { x: "-100%" } , { duration: 0.2 } )
setTimeout ( ( ) => onDelete ( index) , 200 )
} else {
animate ( scope. current, { x: 0 , opacity: 1 } , { duration: 0.5 } )
}
}
return (
< motion.div
style = { {
width: 150 ,
height: height,
borderRadius: 20 ,
overflow: "hidden" ,
marginBottom: total - 1 === index ? 0 : 10 ,
willChange: "transform" ,
cursor: "grab" ,
} }
whileTap = { { cursor: "grabbing" } }
layout
transition = { { type : "spring" , stiffness: 600 , damping: 30 } }
>
< motion.div
style = { {
width: size,
height: height,
borderRadius: 20 ,
backgroundColor: "#fff" ,
} }
drag = " x"
dragDirectionLock
onDragEnd = { handleDragEnd}
ref = { scope}
/>
</ motion.div>
)
}
export default function CC_35_Swipe_to_delete ( props ) {
const y = useMotionValue ( 0 )
const [ items, setItems] = useState ( initialItems)
const { top, bottom } = useConstraints ( items)
const totalScroll = getHeight ( items)
const scrollContainer = 150
function onDelete ( index ) {
const newItems = [ ... items]
newItems. splice ( index, 1 )
const newScrollHeight = getHeight ( newItems)
const bottomOffset = - y. get ( ) + scrollContainer
const bottomWillBeVisible = newScrollHeight < bottomOffset
const isScrollHeightLarger = newScrollHeight >= scrollContainer
if ( bottomWillBeVisible && isScrollHeightLarger) {
animate ( y, - newScrollHeight + scrollContainer)
}
setItems ( newItems)
}
return (
< div>
< div
style = { {
width: size,
height: size,
borderRadius: 30 ,
background: "transparent" ,
overflow: "hidden" ,
position: "relative" ,
transform: "translateZ(0)" ,
} }
>
< motion.div
style = { { y: y, height: totalScroll } }
drag = " y"
dragDirectionLock
dragConstraints = { { top, bottom } }
>
{ items. map ( ( value, index ) => {
return (
< Item
total = { items. length}
index = { index}
onDelete = { onDelete}
key = { value}
/>
)
} ) }
</ motion.div>
</ div>
</ div>
)
}
function getHeight ( items ) {
const totalHeight = items. length * height
const totalPadding = ( items. length - 1 ) * padding
const totalScroll = totalHeight + totalPadding
return totalScroll
}
function useConstraints ( items ) {
const [ constraints, setConstraints] = useState ( { top: 0 , bottom: 0 } )
useEffect ( ( ) => {
setConstraints ( { top: size - getHeight ( items) , bottom: 0 } )
} , [ items] )
return constraints
}
Some more details in the main CC_35_Swipe_to_delete()
component:
The onDelete()
function also checks for space at the bottom (occurs when you delete the bottommost item) and will adjust the position (the y
Motion value) of the draggable <motion.div>
when needed, with an animate()
animation.
A reference to this onDelete()
function is passed down to every <Item>
. Code override While it might be possible to create this interaction with overrides, it would be way more complicated. Not really the effort.
34. Swapping elements
Code Overrides