After some research, i found a good bottom sheet for my remix application on codesandbox. The problem is, that the bottom sheet is powered by react-spring and #use-gestures but i want to use framer-motion for the animation part. I don't want to use both.
so the question: is there an alternative to useSpring() hook in framer-motion, so i can use the following code? Can i create a very similar animation in framer-motion, or should I better use react-spring for this kind of animation, what do you think?
import React from 'react'
import { useDrag } from '#use-gesture/react'
import { a, useSpring, config } from '#react-spring/web'
import styles from './styles.module.css'
const items = ['save item', 'open item', 'share item', 'delete item', 'cancel']
const height = items.length * 60 + 80
export default function App() {
const [{ y }, api] = useSpring(() => ({ y: height }))
const open = ({ canceled }) => {
// when cancel is true, it means that the user passed the upwards threshold
// so we change the spring config to create a nice wobbly effect
api.start({ y: 0, immediate: false, config: canceled ? config.wobbly : config.stiff })
}
const close = (velocity = 0) => {
api.start({ y: height, immediate: false, config: { ...config.stiff, velocity } })
}
const bind = useDrag(
({ last, velocity: [, vy], direction: [, dy], movement: [, my], cancel, canceled }) => {
// if the user drags up passed a threshold, then we cancel
// the drag so that the sheet resets to its open position
if (my < -70) cancel()
// when the user releases the sheet, we check whether it passed
// the threshold for it to close, or if we reset it to its open positino
if (last) {
my > height * 0.5 || (vy > 0.5 && dy > 0) ? close(vy) : open({ canceled })
}
// when the user keeps dragging, we just move the sheet according to
// the cursor position
else api.start({ y: my, immediate: true })
},
{ from: () => [0, y.get()], filterTaps: true, bounds: { top: 0 }, rubberband: true }
)
const display = y.to((py) => (py < height ? 'block' : 'none'))
const bgStyle = {
transform: y.to([0, height], ['translateY(-8%) scale(1.16)', 'translateY(0px) scale(1.05)']),
opacity: y.to([0, height], [0.4, 1], 'clamp')
}
return (
<div className="flex" style={{ overflow: 'hidden' }}>
<a.div className={styles.bg} onClick={() => close()} style={bgStyle}>
<img
src="https://images.pexels.com/photos/1239387/pexels-photo-1239387.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940"
alt=""
/>
<img
src="https://images.pexels.com/photos/5181179/pexels-photo-5181179.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940"
alt=""
/>
</a.div>
<div className={styles.actionBtn} onClick={open} />
<a.div className={styles.sheet} {...bind()} style={{ display, bottom: `calc(-100vh + ${height - 100}px)`, y }}>
{items.map((entry, i) => (
<div
key={entry}
onClick={() => (i < items.length - 1 ? alert('clicked on ' + entry) : close())}
children={entry}
/>
))}
</a.div>
</div>
)
}
Link to Codesandbox:
https://codesandbox.io/s/github/pmndrs/use-gesture/tree/main/demo/src/sandboxes/action-sheet?file=/src/App.jsx:0-2734
Related
I'm a beginner trying to get some text and a photo to animate correctly on my React app.
I have a handful of elements that get updated with new content when I cycle through the array that stores their data. On a browser this is done using last/next buttons, but on mobile the user can swipe left or right.
I would like to have the content of the elements animate (fade in/out) upon entry and exit, but since the element doesn't re-render when the next index of the array changes, React doesn't want to apply the animation. The animations are stored in the css document right now.
I used a hack I found here where a random key is assigned to each element when a change is made, forcing the element to re-render and animate. This works great on desktop, but the problem is: on mobile, my 'onTouchStart' function seems to be forcing this random key to re-generate, overriding the swipe functionality and causing the elements to re-render every time they are touched.
How can I force the key to only re-generate when the user swipes?
I've also tried using CSSTransition but have not had any success figuring that out. Other solutions welcome!
const Testimonials = props =>{
const testimonials = [
{
name: 'z',
position: 'a',
photo: require('./img/chrisphoto.png'),
text:
"1"
},
{
name: 'y',
position: 'b',
photo: require('./img/jillphoto.png'),
text:
'2'
},
{
name: 'x',
position: 'c',
photo: require('./img/mikephoto.png'),
text:
'3'
},
];
const [idx, setIdx] = useState(0);
let name = testimonials[idx].name;
let position= testimonials[idx].position;
let photo= testimonials[idx].photo;
let text = testimonials[idx].text;
const [touchPosition, setTouchPosition] = useState(null)
const handleTouchStart = (e) => {
const touchDown = e.touches[0].clientX
setTouchPosition(touchDown)
}
const handleTouchMove = (e) => {
const touchDown = touchPosition
if(touchDown === null) {
return
}
const currentTouch = e.touches[0].clientX
const diff = touchDown - currentTouch
if (diff > 5) {
setIdx((prevIndex) =>
prevIndex === testimonials.length - 1 ? 0 : prevIndex + 1
)
}
if (diff < -5) {
setIdx((prevIndex) =>
prevIndex === 0 ? testimonials.length-1 : prevIndex - 1
)
}
setTouchPosition(null)
}
const rand = Math.random();
return (
<div className='testimonial-entry'>
<button className="next-testimonial" onClick={() => {
setIdx(idx => (idx + 1) % testimonials.length);
}}style={{
backgroundImage: `url(${nextarrow})`,
backgroundRepeat: 'no-repeat',
backgroundPosition: 'center',
backgroundSize: '10px'}}></button>
<button className="last-testimonial" onClick={() => {
setIdx(idx === 0 ? testimonials.length-1 : idx => (idx - 1) % testimonials.length);
}}style={{
backgroundImage: `url(${lastarrow})`,
backgroundRepeat: 'no-repeat',
backgroundPosition: 'center',
backgroundSize: '10px'}}></button>
<img className='testimonial-photo' key={rand} src={photo}></img>
<div className='testimonial-text'>
<h3 className='testimonial-name2' key={rand} >{name}</h3></div>
<div className='testimonial-text2'>
<h3 className='testimonial-title2' key={rand} >{position}</h3></div>
<div className='testimonial-body-container'>
<h3 className='testimonial-body2' key={rand} style={{fontStyle:"italic"}}>{text}</h3></div>
</div>
);
}
export default Testimonials;
Thanks!
I have a Navigation component in which the Menu Items float in separately on load and float out on click.
When I added Router and changed the items to Links, the exit animation didn't work because it loaded the new Route component right away.
I want to keep the items individual animation with Link functionality.
Here is the link:
https://codesandbox.io/s/elastic-leaf-fxsswo?file=/src/components/Navigation.js
Code:
export const Navigation = () => {
const navRef = useRef(null);
const onResize = () => {
setIsColumn(window.innerWidth <= 715);
};
const [clickOnMenu, setClick] = useState(false);
const [itemtransition, setTransition] = useState(
Array(menuItems.length).fill(0)
);
const [isColumn, setIsColumn] = useState(window.innerWidth <= 715);
const click = (e) => {
const copy = [...itemtransition];
const index = e.target.id;
setTransition(copy.map((e, i) => (Math.abs(index - i) + 1) / 10));
setTimeout(() => setClick(true), 50);
};
useEffect(() => {
window.addEventListener("resize", onResize);
return () => window.removeEventListener("resize", onResize);
}, []);
return (
<AnimatePresence exitBeforeEnter>
{!clickOnMenu && (
<Nav ref={navRef}>
{menuItems.map((e, i) => {
const text = Object.keys(e)[0];
const value = Object.values(e)[0];
return (
<Item
id={i}
key={value}
animate={{
x: 0,
y: 0,
opacity: 1,
transition: { delay: (i + 1) / 10 }
}}
initial={{
x: isColumn ? 1000 : 0,
y: isColumn ? 0 : 1000,
opacity: 0
}}
exit={{
x: isColumn ? -1000 : 0,
y: isColumn ? 0 : -1000,
opacity: 0,
transition: { delay: itemtransition[i] }
}}
onClick={click}
>
{/*<Link to={`/${value}`}>{text}</Link>*/}
{text}
</Item>
);
})}
</Nav>
)}
</AnimatePresence>
);
};
In the sandbox in Navigation.js 69-70. row:
This is the desired animation.
69. {/*<Link to={`/${value}`}>{text}</Link>*/}
70. {text}
But when I use Link there is no exit animation
69. <Link to={`/${value}`}>{text}</Link>
70. {/*text*/}
Is there a workaround or I should forget router-dom.
Thank you in forward!
This may be a bit hackish, but with routing and transitions sometimes that is the nature. I suggest rendering the Link so the semantic HTML is correct and add an onClick handler to prevent the default navigation action from occurring. This allows any transitions/animations to go through. Then update the click handler of the Item component to consume the link target and issue an imperative navigation action on a timeout to allow transitions/animations to complete.
I used a 750ms timeout but you may need to tune this value to better suit your needs.
Example:
...
import { Link, useNavigate } from "react-router-dom";
...
export const Navigation = () => {
const navRef = useRef(null);
const navigate = useNavigate(); // <-- access navigate function
...
const click = target => (e) => { // <-- consume target
const copy = [...itemtransition];
const index = e.target.id;
setTransition(copy.map((e, i) => (Math.abs(index - i) + 1) / 10));
setTimeout(() => {
setClick(true);
}, 50);
setTimeout(() => {
navigate(target); // <-- navigate after some delta
}, 750);
};
...
return (
<AnimatePresence exitBeforeEnter>
{!clickOnMenu && (
<Nav ref={navRef}>
{menuItems.map((e, i) => {
const text = Object.keys(e)[0];
const value = Object.values(e)[0];
return (
<Item
...
onClick={click(`/${value}`)} // <-- pass target to handler
>
<Link
to={`/${value}`}
onClick={e => e.preventDefault()} // <-- prevent link click
>
{text}
</Link>
</Item>
);
})}
</Nav>
)}
</AnimatePresence>
);
};
...
It is regarding the following example:
https://codesandbox.io/s/cards-forked-4bcix?file=/src/index.js
I want a tinder like functionality where I can trigger the same transition as drag.
I am trying to add like and dislike button functionality like tinder, but since the buttons are not part of the useSprings props loop, it is hard to align which card I should transform. I want the like or dislike button to communicate with useDrag to trigger a drag, I have tried toggling by useState and passing as an argument of useDrag, and onClick handler on button which set(x:1000, y:0) but that removes all of the cards.
Spent a whole day figuring it out, and I need to deliver things very soon, help will be great please!
Below is the code, and I am using Next.js
import React, { useState, useRef } from "react";
import { useSprings, animated } from "react-spring";
import { useDrag } from "react-use-gesture";
const Try: React.SFC = () => {
const cards = [
"https://upload.wikimedia.org/wikipedia/en/5/53/RWS_Tarot_16_Tower.jpg",
"https://upload.wikimedia.org/wikipedia/en/9/9b/RWS_Tarot_07_Chariot.jpg",
"https://upload.wikimedia.org/wikipedia/en/d/db/RWS_Tarot_06_Lovers.jpg",
// "https://upload.wikimedia.org/wikipedia/en/thumb/8/88/RWS_Tarot_02_High_Priestess.jpg/690px-RWS_Tarot_02_High_Priestess.jpg",
"https://upload.wikimedia.org/wikipedia/en/d/de/RWS_Tarot_01_Magician.jpg",
];
const [hasLiked, setHasLiked] = useState(false);
const [props, set] = useSprings(cards.length, (i) => ({
x: 0,
y: 0,
}));
const [gone, setGone] = useState(() => new Set()); // The set flags all the cards that are flicked out
const itemsRef = useRef([]);
const bind = useDrag(
({
args: [index, hasLiked],
down,
movement: [mx],
distance,
direction: [xDir],
velocity,
}) => {
const trigger = velocity > 0.2;
const dir = xDir < 0 ? -1 : 1;
if (!down && trigger) gone.add(index); // If button/finger's up and trigger velocity is reached, we flag the card ready to fly out
set((i) => {
if (index !== i) return; // We're only interested in changing spring-data for the current spring
const isGone = gone.has(index);
const x = isGone ? (200 + window.innerWidth) * dir : down ? mx : 0; // When a card is gone it flys out left or right, otherwise goes back to zero
const rot = mx / 100 + (isGone ? dir * 10 * velocity : 0); // How much the card tilts, flicking it harder makes it rotate faster
const scale = down ? 1.1 : 1; // Active cards lift up a bit
return {
x,
rot,
scale,
delay: undefined,
config: { friction: 50, tension: down ? 800 : isGone ? 200 : 500 },
};
});
if (!down && gone.size === cards.length)
setTimeout(() => gone.clear(), 600);
}
);
console.log(gone);
function handleLikeButtonClick(e) {
gone.add(1);
}
function handleDisLikeButtonClick(e) {
set({ x: -1000, y: 0 });
}
return (
<>
<div id="deckContainer">
{props.map(({ x, y }, i) => (
<animated.div
{...bind(i, hasLiked)}
key={i}
style={{
x,
y,
}}
ref={(el) => (itemsRef.current[i] = el)}
>
<img src={`${cards[i]}`} />
</animated.div>
))}
</div>
<div className="buttonsContainer">
<button onClick={(e) => handleLikeButtonClick(e)}>Like</button>
<button onClick={(e) => handleDisLikeButtonClick(e)}>Dislike</button>
</div>
<style jsx>{`
.buttonsContainer {
background-color: tomato;
}
#deckContainer {
display: flex;
flex-direction: column;
align-items: center;
width: 100vw;
height: 100vh;
position: relative;
}
`}</style>
</>
);
};
export default Try;
done needed to be smart with js:
function handleLikeButtonClick(e) {
let untransformedCards = [];
//loop through all cards and add ones who still have not been transformed
[].forEach.call(itemsRef.current, (p) => {
if (p.style.transform == "none") {
untransformedCards.push(p);
}
});
// add transform property to the latest card
untransformedCards[untransformedCards.length - 1].style.transform =
"translate3d(1000px, 0 ,0)";
}
function handleDisLikeButtonClick(e) {
let untransformedCards = [];
[].forEach.call(itemsRef.current, (p) => {
if (p.style.transform == "none") {
untransformedCards.push(p);
}
});
untransformedCards[untransformedCards.length - 1].style.transform =
"translate3d(-1000px, 0 ,0)";
}
I have a button which closes a navigation. This button follows the mouse. Everything is working, but I have a depricationwarning, which I wanna get rid of, but don't know exactly how. (I only know that useEffect is playing a certain role:
Here is the class:
import React from "react"
class NavigationCloseMouseButton extends React.Component {
static defaultProps = {
visible: true,
offsetX: 0,
offsetY: 0,
}
state = {
xPosition: 0,
yPosition: 0,
mouseMoved: false,
listenerActive: false,
}
componentDidMount() {
this.addListener()
}
componentDidUpdate() {
this.updateListener()
}
componentWillUnmount() {
this.removeListener()
}
getTooltipPosition = ({ clientX: xPosition, clientY: yPosition }) => {
this.setState({
xPosition,
yPosition,
mouseMoved: true,
})
}
addListener = () => {
window.addEventListener("mousemove", this.getTooltipPosition)
this.setState({ listenerActive: true })
}
removeListener = () => {
window.removeEventListener("mousemove", this.getTooltipPosition)
this.setState({ listenerActive: false })
}
updateListener = () => {
if (!this.state.listenerActive && this.props.visible) {
this.addListener()
}
if (this.state.listenerActive && !this.props.visible) {
this.removeListener()
}
}
render() {
return (
<div
onClick={this.props.toggleNavigation}
className="tooltip color-bg"
style={{
display:
this.props.visible && this.state.mouseMoved ? "block" : "none",
opacity: this.props.visible && this.state.mouseMoved ? "1" : "0",
top: this.state.yPosition + this.props.offsetY,
left: this.state.xPosition + this.props.offsetX,
}}
>
Close Menu
</div>
)
}
}
export default NavigationCloseMouseButton
And this is what I've so far, but results with errors:
ReferenceError: getTooltipPosition is not defined
import React, { useState, useEffect } from "react"
const NavigationCloseMouseButton = () => {
const defaults = {
visible: true,
offsetX: 0,
offsetY: 0,
}
const defaultState = {
xPosition: 0,
yPosition: 0,
mouseMoved: false,
listenerActive: false,
}
const [defaultProps, setDefaultProps] = useState(defaults)
const [state, setState] = useState(defaultState)
useEffect(() => {
// Update the document title using the browser API
addListener()
}, [])
getTooltipPosition = ({ clientX: xPosition, clientY: yPosition }) => {
setState({
xPosition,
yPosition,
mouseMoved: true,
})
}
addListener = () => {
window.addEventListener("mousemove", getTooltipPosition)
setState({ listenerActive: true })
}
removeListener = () => {
window.removeEventListener("mousemove", getTooltipPosition)
setState({ listenerActive: false })
}
updateListener = () => {
if (!state.listenerActive && props.visible) {
addListener()
}
if (state.listenerActive && !props.visible) {
removeListener()
}
}
return (
<div
onClick={props.toggleNavigation}
className="tooltip color-bg"
style={{
display: props.visible && state.mouseMoved ? "block" : "none",
opacity: props.visible && state.mouseMoved ? "1" : "0",
top: state.yPosition + props.offsetY,
left: state.xPosition + props.offsetX,
}}
>
Close Menu
</div>
)
}
export default NavigationCloseMouseButton
Setting Defaults
You can destructure individual props from the props object (the argument of the function component). While destructuring, you can use the = operator to set a default value for when this prop is not set.
const NavigationCloseMouseButton = ({ visible = true, offsetX = 0, offsetY = 0, toggleNavigation }) => {
Updating a Listener
I'm sure there a lots of great answers about this so I won't go into too much detail.
You want to handle adding and removing the listener from inside your useEffect. You should use a useEffect cleanup function for the final remove. We don't want to be adding and removing the same listener so we can memoize it with useCallback.
I'm not sure what you are trying to do with listenerActive. This could be a prop, but it also seems a bit redundant with visible. I don't know that we need this at all.
Calculating Offset
I also don't know that it makes sense to pass offsetX and offsetY as props. We need the mouse to be on top of the tooltip in order for it to be clickable. We can measure the tooltip div inside this component and deal with it that way.
// ref to DOM node for measuring
const divRef = useRef<HTMLDivElement>(null);
// can caluculate offset instead of passing in props
const offsetX = -.5 * (divRef.current?.offsetWidth || 0);
const offsetY = -.5 * (divRef.current?.offsetHeight || 0);
Animation
Setting the style property display as "block" or "none" makes it hard to do any sort of CSS transition. Instead, I recommend that you handle style switching by changing the className. You could still set display: block and display: none on those classes, but I am choosing to use transform: scale(0); instead.
Code
const NavigationCloseMouseButton = ({
visible = true,
toggleNavigation
}) => {
// state of the movement
const [state, setState] = useState({
xPosition: 0,
yPosition: 0,
mouseMoved: false
});
// memoized event listener
const getTooltipPosition = useCallback(
// plain event, not a React synthetic event
({ clientX: xPosition, clientY: yPosition }) => {
setState({
xPosition,
yPosition,
mouseMoved: true
});
},
[]
); // never re-creates
useEffect(() => {
// don't need to listen when it's not visible
if (visible) {
window.addEventListener("mousemove", getTooltipPosition);
} else {
window.removeEventListener("mousemove", getTooltipPosition);
}
// clean-up function to remove on unmount
return () => {
window.removeEventListener("mousemove", getTooltipPosition);
};
}, [visible, getTooltipPosition]); // re-run the effect if prop `visible` changes
// ref to DOM node for measuring
const divRef = useRef(null);
// can caluculate offset instead of passing in props
const offsetX = -.5 * (divRef.current?.offsetWidth || 0);
const offsetY = -.5 * (divRef.current?.offsetHeight || 0);
// don't show until after mouse is moved
const isVisible = visible && state.mouseMoved;
return (
<div
ref={divRef}
onClick={toggleNavigation}
// control most styling through className
className={`tooltip ${isVisible ? "tooltip-visible" : "tooltip-hidden"}`}
style={{
// need absolute position to use top and left
position: "absolute",
top: state.yPosition + offsetY,
left: state.xPosition + offsetX
}}
>
Close Menu
</div>
);
};
Other Uses
We can easily make this NavigationCloseMouseButton into a more flexible MovingTooltip by removing some of the hard-coded specifics.
Get the contents from props.children instead of always using "Close Menu"
Accept a className as a prop
Change the name of toggleNavigation to onClick
Code Sandbox Demo
This is quite difficult to explain so I have created this codesandbox to illustrate the problem.
I am working on a package that basically is a wrapper around mousetrap so you can add keyboard events to either the document object or a specific element.
I am testing it out with this code:
const boxes = [{ x: 0, y: 0 }, { x: 0, y: 0 }, { x: 0, y: 0 }, { x: 0, y: 0 }, { x: 0, y: 0 }].map(
(b): Box => {
return { ...b, color: `hsl(${Math.random() * 360}, 100%, 50%)` };
}
);
export const App: React.FC = () => {
const [boxState, setState] = useState(boxes);
const handleMove = (newPosition: Partial<Point>, index: number) => {
setState((state) => {
return state.map((box, i) => {
return index === i ? { ...box, ...newPosition } : { ...box };
});
});
};
return (
<div>
<h1>Click on any box and use arrow keys or WSAD</h1>
{boxState.map(({ x, y, color }, index) => (
<MovableBox key={index} color={color} index={index} x={x} y={y} onMoveRequest={handleMove} />
))}
</div>
);
};
In the code sandbox, you can move the squares down by either pressing the D button in each square of if you click a square, the div gets focus and the arrow keys should move the boxes around.
The problem is that it works fine when pressing the button but when you use the arrow key, the new state is not persisted. It always starts at it's initial value.
The Box component looks like this:
export const Box: React.FC<BoxType & BoxProps> = ({ x, y, color, index, onMoveRequest }) => {
const style: CSSProperties = {
width: '100px',
height: '100px',
backgroundColor: color,
textAlign: 'center',
lineHeight: '100px',
position: 'absolute',
top: `${y + index * 120}px`,
left: `${x + index * 120}px`
};
if (index === 0) {
console.log({ x, y });
}
const SHIFT = 10;
const handleMove = (action) => {
console.log(index);
switch (action) {
case 'MOVE_LEFT':
onMoveRequest({ x: x - SHIFT }, index);
break;
case 'MOVE_RIGHT':
onMoveRequest({ x: x + SHIFT }, index);
break;
case 'MOVE_UP':
onMoveRequest({ y: y - SHIFT }, index);
break;
case 'MOVE_DOWN':
onMoveRequest({ y: y + SHIFT }, index);
break;
default:
throw new Error('Unknown action');
}
};
return (
<Shortcuts shortcutMap={shortcutMap} mapKey="BOX" handler={handleMove} scoped>
<div style={style}>
{index + 1} ({x}, {y})
<button type="button" onClick={() => handleMove('MOVE_DOWN')}>
D
</button>
</div>
</Shortcuts>
);
};
I cannot for the life of me work out why it is different from the keyboard event.
Ok your issue:
You initialize the keyboard shortcuts in the Shortcut component on "didMount". That means it only registers the handler function when it mounts, with the reference to the initial y.
To fix this pass x & y to the Shortcuts component then take everything from componentDidMount and add it to a new method - initializeShortcuts => (props: Props) => {...}.
Run this function in componentDidMount() & componentWillReceiveProps() (or getDerivedState whatever with props & nextProps respectively and it works. Also remember to unbind the shortcuts before binding them again in the latter.
Working version - https://codesandbox.io/s/wnl6v7x69w.