I have a list of two elements scenes rendered as <Rnd /> components from "react-rnd". What I'd like to happen is when I drag one element close to another one they should swap positions, in the list and in the UI. so [first, second] becomes [second, first]. The <Rnd /> position is controlled, this is the code I'm using:
import { useState } from "react";
import "./styles.css";
import { Rnd, Position, DraggableData } from "react-rnd";
interface IScene {
id: string;
name: string;
position: Position;
}
function Scene({
scene,
activeScene,
setActiveScene,
onDrag
}: {
scene: IScene;
activeScene: string;
setActiveScene: (id: string) => void;
onDrag: (d: DraggableData) => void;
}) {
const [dragged, setDragged] = useState(false);
return (
<Rnd
default={{
x: 0,
y: 0,
width: "200px",
height: "100px"
}}
position={scene.position}
onDragStart={() => setDragged(true)}
onDragStop={() => setDragged(false)}
onDrag={(_, d) => onDrag(d)}
onMouseDown={() => setActiveScene(scene.id)}
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
backgroundColor: "gray",
transition: dragged ? "" : "transform 0.5s",
border: "1px solid",
borderColor: activeScene === scene.id ? "white" : "gray",
zIndex: activeScene === scene.id ? "1" : "0"
}}
>
{scene.name}
</Rnd>
);
}
const initialScenesState = [
{
id: "1",
name: "first",
position: {
x: 0,
y: 0
}
},
{
id: "2",
name: "second",
position: {
x: 200,
y: 0
}
}
];
export default function App() {
const [scenes, setScenes] = useState<IScene[]>(initialScenesState);
const [activeScene, setActiveScene] = useState<string>("");
const handleStackScenes = () => {
setScenes((scenes) => {
let currentPosition = 0;
return scenes.map((scene) => {
const result = {
...scene,
position: {
x: currentPosition,
y: 0
}
};
currentPosition += 200;
return result;
});
});
};
const swapScenes = (first: IScene, second: IScene) => {
setScenes((scenes) => {
return scenes.map((scene) => {
if (scene.id === first.id) {
return second;
} else if (scene.id === second.id) {
return first;
} else return scene;
});
});
handleStackScenes();
};
const handleDrag = (scene: IScene) => (d: DraggableData) => {
console.log(d.x);
for (let i = 0; i < scenes.length; i++) {
if (
Math.abs(scenes[i].position.x - d.x) < 30 &&
scenes[i].id !== scene.id
) {
swapScenes(scene, scenes[i]);
}
}
};
console.log(scenes);
return (
<div className="App">
{scenes.map((scene) => (
<Scene
key={scene.id}
scene={scene}
activeScene={activeScene}
setActiveScene={setActiveScene}
onDrag={handleDrag(scene)}
/>
))}
</div>
);
}
The problem that I'm facing with my code, is that when I drag the left element to the right one, the swap happens exactly how I wanted, in the other way around the swap happens but I don't see a transition effect, when I checked what happens on the console, it seems that on the second case, the dom elements don't swap, but just the content and it the first case the actual dom elements move. What am I doing wrong?
EDIT: CodeSandBox
Related
I trying to update human.x using requestAnimationFrame and useState. I want to add elements called "humans" and use requestAnimationFrame to update the position to humans walk in "world" (using css properties). The method addHumans work but requestAnimationFrame update and empty my list.
World.tsx
import { useState, useEffect } from 'react'
import Human from './Human'
import IHuman from './Human'
type IHuman = {
id: number;
x: number;
y: number;
color: string;
}
export default function World() {
const [humans, setHumans] = useState<IHuman[]>([])
const addHuman = () => {
setHumans([...humans, {id: Math.random(), x: 0, y: 0, color: 'red'}])
}
const animate = () => {
setHumans(humans?.map((human, i) => {
return {...human, x: human.x + 1}
}));
// console.log(humans)
requestAnimationFrame(animate);
}
useEffect(() => {
requestAnimationFrame(animate);
}, []); // run once
return (
<>
<button onClick={addHuman}>new human</button>
<main>
{humans?.map(human => {
return <Human key = {human.id} x = {human.x} y = {human.y} color = {human.color} />
})}
</main>
</>
)
}
Human.tsx
export default function Human(props: any) {
return (
<div key={props.id} className="human" style={{ top: props.y + 'px', left: props.x + 'px', backgroundColor: props.color }}>{props.id}</div>
)
}
This is called a stale state. Update your code like this:
setHumans(humans => humans?.map((human, i) => {
return {...human, x: human.x + 1}
}));
Hi Guys I need a guide on this.
I am creating a React DnD. I am creating a Container and I am creating Boxes inside .
How I cannot Drag one over other.
What has more sense, is ask for the position in the callBack and don't allow the drop of the coordinates or modified the ref and "remove the dropable area"?
Anybody with experience in ReactDnD could give me a guide.
import update from 'immutability-helper'
import { useCallback, useState } from 'react'
import { useDrop } from 'react-dnd'
import { Box } from './Box.js'
import { ItemTypes } from './ItemTypes.js'
// This is the size of the area of Drop
const styles = {
width: 500,
height: 500,
border: '1px solid black',
position: 'relative',
}
export const Container = () => {
const [boxes, setBoxes] = useState({
'01': { top: 0, left: 0, title: 'Ship01',width:2,height:1 },
'02': { top: 100, left: 0, title: 'Ship02',width:3,height:1 },
'03': { top: 200, left: 0, title: 'Ship03',width:4,height:1 },
'04': { top: 300, left: 0, title: 'Ship04',width:5,height:1 },
'05': { top: 400, left: 0, title: 'Ship05',width:6,height:1 },
})
const moveBox = useCallback(
(id, left, top) => {
setBoxes(
update(boxes, {
[id]: {
$merge: { left, top },
},
}),
)
},
[boxes, setBoxes],
)
const [, drop] = useDrop(() => ({accept: ItemTypes.BOX, drop(item, monitor) {
const delta = monitor.getDifferenceFromInitialOffset()
const left = Math.round(item.left + delta.x)
const top = Math.round(item.top + delta.y)
moveBox(item.id, left, top)
return undefined}}),[moveBox],)
return (
<div ref={drop} style={styles}>
{Object.keys(boxes).map((key) => {
const { left, top, title,width, height} = boxes[key]
return (
<Box key={key} id={key} left={left} top={top} width={width} height={height} boxes={boxes}> {title} </Box>
)
})}
</div>
)
}
I have two issues.
Onload, the bottom links should render only once, but they are rendered twice.
When clicking the box links at the bottom, the current links should smoothly fade out and the new ones fade in.
Instead, the new ones initially fade in stacked with the previous ones.
what am I doing wrong here?
Sandbox Link
Relevant animation code:
export default function DynamicScreen({}: IProps) {
const { screenKey } = useParams()
const [isLeavingScreen, setIsLeavingScreen] = useState(false)
const [screen, setScreen] = useState<IDynamicScreen>()
const [screenLinks, setScreenLinks] = useState<IDynamicScreenLink[]>([])
const navigate = useNavigate()
const isFirstRenderRef = useRef(true)
useEffect(() => {
if (isFirstRenderRef.current) {
isFirstRenderRef.current = false
return
}
}, [])
useEffect(() => {
const loadScreen = async () => {
console.log("Screen - Loading")
const { data: _screen } = await api.getScreen(screenKey!)
setScreen(_screen)
setScreenLinks(
[_screen.link1?.[0], _screen.link2?.[0], _screen.link3?.[0]].filter(
Boolean,
),
)
console.log("Screen - Loaded")
}
loadScreen()
}, [screenKey])
const springApiLory = useSpringRef()
const springLoryStyle = useSpring({
from: { opacity: 0, scale: 0 },
to: { opacity: 1, scale: 1 },
ref: springApiLory,
})
const springApiTitle = useSpringRef()
const springTitleStyle = useSpring({
from: { opacity: 0, transform: "translateY(-25px)" },
to: !isLeavingScreen
? { opacity: 1, transform: "translateY(0)" }
: {
opacity: 0,
transform: "translateY(-25px)",
},
ref: springApiTitle,
})
const springApiScriptHtml = useSpringRef()
const springScriptHtmlStyle = useSpring({
from: { opacity: 0, transform: "translateX(-25px)" },
to: !isLeavingScreen
? { opacity: 1, transform: "translateY(0)" }
: {
opacity: 0,
transform: "translateX(-25px)",
},
ref: springApiScriptHtml,
})
const springApiLinks = useSpringRef()
const linkTransition = useTransition(isLeavingScreen ? [] : screenLinks, {
ref: springApiLinks,
from: { opacity: 0 },
enter: { opacity: 1 },
leave: isLeavingScreen ? { opacity: 0 } : {},
trail: 100,
})
const chain = [
springApiLory,
springApiTitle,
springApiScriptHtml,
springApiLinks,
]
useChain(
!isLeavingScreen ? chain : chain.reverse(),
[...Array(chain.length).keys()].map(calcStaggerDelay),
)
const goToLink = (link: string) => {
setIsLeavingScreen(true)
setTimeout(() => {
setScreenLinks([])
navigate(`/${link}`)
setIsLeavingScreen(false)
}, 1000)
}
if (!screen) return null
return (
<>
<div className="dynamic-screen-top">
<div className="left">
<animated.div style={springTitleStyle}>
<h1>{screen.title}</h1>
</animated.div>
<animated.div style={springScriptHtmlStyle}>
<div
dangerouslySetInnerHTML={{
__html: screen.scriptHtml,
}}
/>
</animated.div>
{/* TODO screen.actions */}
</div>
<animated.div style={springLoryStyle} className="right">
💁♀️
</animated.div>
</div>
<div className="dynamic-screen-bottom">
{linkTransition((style, screenLink) => (
<animated.div
style={style}
className="dynamic-screen-bottom-link-wrap"
>
<DynamicScreenLink
linkKey={screenLink!.key}
onClick={goToLink}
text={screenLink!.text}
imageUrl={screenLink!.imageUrl}
size={screenLink!.size}
/>
</animated.div>
))}
</div>
</>
)
}
interface IProps {}
const calcStaggerDelay = (i: number) => i * 0.1 + 0.1
I don't know if this helps you continue, but what i did first of all is to set screenLinks the to an empty array when navigating to new page
const goToLink = (link: string) => {
setScreenLinks([]);
setIsLeavingScreen(true);
setTimeout(() => {
navigate(`/${link}`);
setIsLeavingScreen(false);
}, 300);
};
and also, if isLeavingScreen, then set the width to zero, in order for the next screenLinks to take whole space
const linkTransition = useTransition(isLeavingScreen ? [] : screenLinks, {
ref: springApiLinks,
from: { opacity: 0 },
enter: { opacity: 1 },
leave: isLeavingScreen ? { display: "none", width: 0 } : {},
trail: 100
});
demo
I'm using react.js, Typescript, chakra and animation framer.
There is a MotionSwipe component that allows you to swipe right and left. It can be dragged to swipe the Box of a child element.
We want the Box to move to the right or left not only by dragging, but also by pressing a button.
In the function onClickSwipe, which is called when the button is pressed, I executed the animateCardSwipe function to make it swipe to the right. However, the animation is too fast for me to see. I would like to slow down the animation of the Box moving to the right when the button is pressed.
import { Button,Box } from '#chakra-ui/react';
import { PanInfo, useMotionValue, useTransform } from 'framer-motion';
import { NextPage } from 'next';
import { MotionSwipe } from 'components/MotionSwipe';
import React, { useState } from 'react';
const Component: React.VoidFunctionComponent = () => {
const [cards, setCards] = useState([
{ text: 'Up or down', background: 'red' },
{ text: 'Left or right', background: 'green' },
{ text: 'Swipe me!', background: 'gray' },
]);
const [dragStart, setDragStart] = useState<{ axis: string | null; animation: { x: number; y: number } }>({
axis: null,
animation: { x: 0, y: 0 },
});
const x = useMotionValue(0);
const y = useMotionValue(0);
const onDirectionLock = (axis: string | null) => setDragStart({ ...dragStart, axis: axis });
const animateCardSwipe = (animation: { x: number; y: number }) => {
setDragStart({ ...dragStart, animation });
setTimeout(() => {
setDragStart({ axis: null, animation: { x: 0, y: 0 } });
x.set(0);
y.set(0);
setCards([...cards.slice(0, cards.length - 1)]);
}, 200);
};
const onDragEnd = (info: PanInfo) => {
if (dragStart.axis === 'x') {
if (info.offset.x >= 400) animateCardSwipe({ x: 300, y: 0 });
else if (info.offset.x <= -400) animateCardSwipe({ x: -300, y: 0 });
} else {
if (info.offset.y >= 100) animateCardSwipe({ x: 0, y: 100 });
else if (info.offset.y <= -100) animateCardSwipe({ x: 0, y: -100 });
}
};
const rotate = useTransform(x, [-700, 700], [-90, 90]);
const onClickSwipe = () => {
animateCardSwipe({ x: 500, y: 0 });
};
return (
<>
<Box display="flex" justifyContent="center" width="300px">
{cards.map((card, index) =>
index === cards.length - 1 ? (
<MotionSwipe
animate={dragStart.animation}
card={card}
key={index}
onDirectionLock={(axis: string) => onDirectionLock(axis)}
onDragEnd={(e: MouseEvent, info: PanInfo) => onDragEnd(info)}
style={{ x, y, zIndex: index, rotate: rotate }}
>
<Box background={card.background} height="300px" width="300px"></Box>
</MotionSwipe>
) : (
<MotionSwipe
card={card}
key={index}
style={{
zIndex: index,
}}
>
<Box background={card.background} height="300px" width="300px"></Box>
</MotionSwipe>
),
)}
</Box>
<Button onClick={onClickSwipe}>
○
</Button>
<Button>✖︎</Button>
</>
);
};
const SwipeBox: NextPage = () => {
return <Component />;
};
export default SwipeBox;
interface Props {
animate?: { x: number; y: number };
onDirectionLock?: (axis: 'x' | 'y') => void;
onDragEnd?: (e: MouseEvent, info: PanInfo) => void;
style: { x?: MotionValue; y?: MotionValue; zIndex: number; rotate?: MotionValue };
card: { text: string; background: string };
}
export const MotionSwipe: React.FunctionComponent<Props> = (props) => {
return (
<MotionBox
animate={props.animate}
background="white"
borderTopRadius="8px"
className="card"
display="grid"
drag
dragConstraints={{ left: 0, right: 0, top: 0, bottom: 0 }}
dragDirectionLock
left={0}
onDirectionLock={props.onDirectionLock}
onDragEnd={props.onDragEnd}
placeItems="center center"
position="absolute"
style={{ ...props.style }}
top={0}
transition={{ ease: [0.6, 0.05, -0.01, 0.9], duration: 3 }}
>
{props.children}
</MotionBox>
);
};
Yes, it's ok when it moves fast. How to change:
set useState variable to check if the button was pressed.
For example: const [pressed, setPressed] = useState(false);
If the button was pressed, you set setPressed(true)
If pressed(true) you add some transition or animation props with duration.
If you don't have duration, your element will triggers instantly after button click.
I am creating a screen in functional component where I have to execute an animation when there is any event occurs ... This event could occur 1000 times when screen is open ... so I have implemented a custom component which takes position on screen and animates ....
const FloatingComponent = (props) => {
const animationView = useSharedValue(1)
const animationOpacityView = useSharedValue(1)
const animationViewStyle = useAnimatedStyle(() => {
return {
transform: [
{
translateY: withTiming(animationView.value, {
duration: 3500
}),
}
],
opacity: withTiming(animationOpacityView.value, {
duration: 1500
})
}
})
useEffect(() => {
console.log('Component animation called')
animationView.value = -((Screen.width * 0.25))
animationOpacityView.value = 0
});
return (
<Animated.View style={[Styles.handImg, { top: props.topDistance }, animationViewStyle]}>
<Image
style={Styles.handImage}
source={require('../../../assets/images/hand.png')}
/>
</Animated.View>
);
};
To create it dynamically I implemented it like this
const driverFactory = (itemNumber) => {
console.log(itemNumber)
return (<FloatingComponent id={1} topDistance={(Screen.width * 0.25) * itemNumber} />);
};
but it never show up and executes ....
while if I add this
<FloatingDriver id={5} topDistance={(Screen.width * 0.25) * 6} />
to main screen return it always executes .... but by these I can not create n number of components at any time when i receive notification ...
I would have a parent component manage these n child components and their rendering. Here is an example showing how one might render all the components with animations and handle showing/removing them as needed.
import React from 'react';
import {Animated, Button, SafeAreaView, StyleSheet} from 'react-native';
const Particle = ({particle: {x, y}, onFinish}) => {
const opacity = React.useRef(new Animated.Value(0)).current;
React.useEffect(() => {
Animated.timing(opacity, {
toValue: 1,
duration: 2000,
useNativeDriver: true,
}).start(() => {
onFinish();
});
}, []);
return (
<Animated.View
style={{
position: 'absolute',
left: x - 10,
bottom: y - 10,
width: 20,
height: 20,
backgroundColor: 'yellow',
opacity,
}}
/>
);
};
const ParticleSystem = () => {
const [layoutRectangle, setLayoutRectangle] = React.useState();
const [particleList, setParticleList] = React.useState([]);
const currentRef = React.useRef();
const addRandomParticle = React.useCallback(() => {
if (layoutRectangle) {
const newParticle = {
id: Math.floor(Math.random() * Number.MAX_SAFE_INTEGER),
x: Math.random() * layoutRectangle.width,
y: Math.random() * layoutRectangle.height,
};
setParticleList([newParticle, ...particleList]);
}
}, [layoutRectangle, particleList]);
const removeParticle = React.useCallback(
(particle) => {
setParticleList(particleList.filter(({id}) => id !== particle.id));
},
[particleList],
);
currentRef.current = removeParticle;
return (
<SafeAreaView
style={styles.screen}
onLayout={(event) => setLayoutRectangle(event.nativeEvent.layout)}>
{particleList.map((particle) => (
<Particle
key={particle.id}
particle={particle}
onFinish={() => {
currentRef.current(particle);
}}
/>
))}
<Button title="Add particle" onPress={() => addRandomParticle()} />
</SafeAreaView>
);
};
export default ParticleSystem;
const styles = StyleSheet.create({
screen: {
flex: 1,
backgroundColor: 'black',
},
});