Created
August 10, 2019 19:36
-
-
Save ajsmth/360e46aa57b1546da7427a7ea8f0b11c to your computer and use it in GitHub Desktop.
Pager Drag Handlers
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useState, useEffect } from 'react' | |
import { useSpring, animated, interpolate } from 'react-spring' | |
import { useDrag } from 'react-use-gesture' | |
// let's handle gestures within our pager component | |
const PAGE_SIZE = 375 | |
// arbitrary value that will determine if we should transition after dragging | |
const DRAG_THRESHOLD = Math.floor(PAGE_SIZE * 0.3) | |
function Pager({ children, activeIndex, onChange }) { | |
const offset = activeIndex * 100 * -1 | |
// dragX will represent the current drag value to animate | |
const [{ translateX, dragX }, set] = useSpring(() => ({ | |
translateX: offset, | |
dragX: 0, | |
})) | |
// this might look a bit strange but it's part of the api for useDrag | |
// bind() is a function we'll add to our container div that gives us a bunch of gesture state data | |
// think of this as an event listener for gestures | |
const bind = useDrag(({ delta, last, vxvy }) => { | |
// this is the drag value | |
const [x] = delta | |
// the velocity of the drag -- important to track to prevent jank after user releases | |
const [vx] = vxvy | |
// we want the value to immediate update w/ a user drag event, not spring to the value | |
set({ dragX: x, immediate: true }) | |
// last is true when the user releases from dragging | |
if (last) { | |
// user has dragged beyond our threshold to transition (either left or right) | |
const shouldTransition = Math.abs(x) >= DRAG_THRESHOLD | |
if (!shouldTransition) { | |
// restore to initial position when user started dragging: | |
set({ dragX: 0, immediate: false }) | |
} else { | |
// determine the next position based on the drag value (left or right) | |
let nextPosition | |
if (x > DRAG_THRESHOLD) { | |
// transition to previous page | |
nextPosition = offset + 100 | |
// update our controller component w/ the previous index | |
onChange(activeIndex - 1) | |
} | |
if (x < DRAG_THRESHOLD) { | |
// transition to next page | |
nextPosition = offset - 100 | |
// update our controller component w/ the next index | |
onChange(activeIndex + 1) | |
} | |
// start spring transition to next position | |
// we want to spring the drag value back to 0 as we translate to the next position | |
set({ | |
dragX: 0, | |
translateX: nextPosition, | |
immediate: false, | |
config: { | |
velocity: vx, | |
}, | |
}) | |
} | |
} | |
}) | |
useEffect(() => { | |
set({ translateX: offset }) | |
}, [offset, set]) | |
// attach the bind() function to our container div to listen for gesture events | |
// we will now include our drag values in the interpolate() function below | |
return ( | |
<animated.div | |
{...bind()} | |
style={{ | |
border: 'thin solid blue', | |
position: 'relative', | |
height: '100%', | |
width: '100%', | |
transform: interpolate( | |
[translateX, dragX], | |
(translateX, dragX) => `translateX(calc(${translateX}% + ${dragX}px))`, | |
), | |
}}> | |
{React.Children.map(children, (element, index) => { | |
return ( | |
<div | |
style={{ | |
...absoluteFill, | |
transform: `translateX(${index * 100}%)`, | |
}}> | |
{element} | |
</div> | |
) | |
})} | |
</animated.div> | |
) | |
} | |
const absoluteFill = { | |
position: 'absolute', | |
left: 0, | |
right: 0, | |
bottom: 0, | |
top: 0, | |
} | |
// this will represent a consumer component or any part of your application | |
function App() { | |
// all we need to pass are children and an activeIndex prop to our pager component | |
const [activeIndex, setActiveIndex] = useState(3) | |
function handleChange(index) { | |
setActiveIndex(index) | |
} | |
const children = Array.from({ length: 10 }).map((c, i) => ( | |
<h1 key={i} style={{ textAlign: 'center' }}> | |
Index {i} | |
</h1> | |
)) | |
return ( | |
<div style={{ overflow: 'hidden' }}> | |
<div | |
style={{ | |
width: PAGE_SIZE, | |
height: PAGE_SIZE, | |
display: 'flex', | |
margin: 'auto', | |
padding: '5px', | |
border: 'thin solid red', | |
}}> | |
<Pager activeIndex={activeIndex} onChange={handleChange}>{children}</Pager> | |
</div> | |
<div | |
style={{ | |
display: 'flex', | |
flexDirection: 'column', | |
alignItems: 'center', | |
justifyContent: 'space-around', | |
}}> | |
<strong style={{ margin: '5px 0' }}>activeIndex: {activeIndex}</strong> | |
<button | |
style={{ margin: '5px 0' }} | |
onClick={() => setActiveIndex(activeIndex + 1)}> | |
Increment | |
</button> | |
<button | |
style={{ margin: '5px 0' }} | |
onClick={() => setActiveIndex(activeIndex - 1)}> | |
Decrement | |
</button> | |
</div> | |
</div> | |
) | |
} | |
export default App |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment