Drag and Drop in a scaled container with react-dnd - scale

I've almost got this working in a codesandbox, but the drop accuracy still isn't quite right. I'm at a loss for what else to try.
Here is my container:
import update from "immutability-helper";
import { useCallback, useState } from "react";
import { useDrop } from "react-dnd";
import { DraggableBox } from "./DraggableBox.js";
import { ItemTypes } from "./ItemTypes.js";
import { snapToGrid as doSnapToGrid } from "./snapToGrid.js";
const styles = {
width: 300,
height: 300,
border: "1px solid black",
position: "relative",
transform: "scale(.8)"
};
export const Container = ({ snapToGrid }) => {
const [boxes, setBoxes] = useState({
a: { top: 20, left: 80, title: "Drag me around" },
b: { top: 180, left: 20, title: "Drag me too" }
});
const moveBox = useCallback(
(id, left, top) => {
setBoxes(
update(boxes, {
[id]: {
$merge: { left, top }
}
})
);
},
[boxes]
);
const [, drop] = useDrop(
() => ({
accept: ItemTypes.BOX,
drop(item, monitor) {
const delta = monitor.getDifferenceFromInitialOffset();
let left = item.left + delta.x;
let top = item.top + delta.y;
if (snapToGrid) {
[left, top] = doSnapToGrid(left, top);
}
moveBox(item.id, left, top);
return undefined;
}
}),
[moveBox]
);
return (
<div ref={drop} style={styles}>
{Object.keys(boxes).map((key) => (
<DraggableBox key={key} id={key} {...boxes[key]} />
))}
</div>
);
};
Here is my draggable box:
import { memo, useEffect } from 'react'
import { useDrag } from 'react-dnd'
import { getEmptyImage } from 'react-dnd-html5-backend'
import { Box } from './Box.js'
import { ItemTypes } from './ItemTypes.js'
function getStyles(left, top, isDragging) {
const transform = `translate3d(${left }px, ${top }px, 0)`
return {
position: 'absolute',
transform,
WebkitTransform: transform,
// IE fallback: hide the real node using CSS when dragging
// because IE will ignore our custom "empty image" drag preview.
opacity: isDragging ? 0 : 1,
height: isDragging ? 0 : '',
}
}
export const DraggableBox = memo(function DraggableBox(props) {
const { id, title, left, top } = props
const [{ isDragging }, drag, preview] = useDrag(
() => ({
type: ItemTypes.BOX,
item: { id, left, top, title },
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
}),
[id, left, top, title],
)
useEffect(() => {
preview(getEmptyImage(), { captureDraggingState: true })
}, [])
return (
<div
ref={drag}
style={getStyles(left, top, isDragging)}
role="DraggableBox"
>
<Box title={title} />
</div>
)
})
Here is my custom drag layer:
import { useDragLayer } from "react-dnd";
import { BoxDragPreview } from "./BoxDragPreview.js";
import { ItemTypes } from "./ItemTypes.js";
import { snapToGrid } from "./snapToGrid.js";
const layerStyles = {
position: "fixed",
pointerEvents: "none",
zIndex: 100,
left: 0 ,
top: 0 ,
width: "100%",
height: "100%",
};
function getItemStyles(initialOffset, currentOffset, isSnapToGrid) {
if (!initialOffset || !currentOffset) {
return {
display: 'none',
}
}
let { x, y } = currentOffset
if (isSnapToGrid) {
x -= initialOffset.x
y -= initialOffset.y
;[x, y] = snapToGrid(x, y)
x += initialOffset.x
y += initialOffset.y
}
const transform = ` translate(${x }px, ${y }px)`
return {
transform,
WebkitTransform: transform,
}
}
export const CustomDragLayer = (props) => {
const {
itemType,
isDragging,
item,
initialOffset,
currentOffset,
delta
} = useDragLayer((monitor) => ({
item: monitor.getItem(),
itemType: monitor.getItemType(),
initialOffset: monitor.getInitialSourceClientOffset(),
currentOffset: monitor.getSourceClientOffset(),
delta: monitor.getDifferenceFromInitialOffset(),
isDragging: monitor.isDragging()
}));
function renderItem() {
switch (itemType) {
case ItemTypes.BOX:
return <BoxDragPreview title={item.title} />;
default:
return null;
}
}
if (!isDragging) {
return null;
}
return (
<div style={layerStyles}>
<div style={getItemStyles(initialOffset, currentOffset, props.snapToGrid)}>{renderItem()}</div>
</div>
);
};
And here is my box drag preview:
import { memo, useEffect, useState } from "react";
import { Box } from "./Box.js";
const styles = {
display: "inline-block",
transform: "scale(.8)",
transformOrigin: "top left"
};
export const BoxDragPreview = memo(function BoxDragPreview({ title }) {
const [tickTock, setTickTock] = useState(false);
useEffect(
function subscribeToIntervalTick() {
const interval = setInterval(() => setTickTock(!tickTock), 500);
return () => clearInterval(interval);
},
[tickTock]
);
return (
<div style={styles}>
<Box title={title} yellow={tickTock} preview />
</div>
);
});
I tried item.left + delta.x / scaleValue, but that was even less accurate than this example. Here is the url to the codesandbox.
https://codesandbox.io/s/dazzling-wozniak-2fvs81?file=/src/BoxDragPreview.js:0-616

Related

Applying CSS classes to a dynamic collection of React nodes on a consistent delay

I have a dynamically sized collection of objects being passed into a Nav component that are being mapped and rendered as buttons. I want to apply a CSS animation to each button so that they slide in from off screen one at a time when the Nav component mounts. I figured that I would set up a loop through each one that updates a boolean value inside of a corresponding state object which applies the CSS class to the button to animate it, but each time that state object is updated, all of the buttons rerender which in turn starts all of the animations over. How can I prevent these rerenders?
// Nav.jsx
import React, { useState, useEffect } from 'react';
import { Button } from '../../../components';
import './Nav.scss';
const Nav = ({ actions }) => {
const [renderStates, setRenderStates] = useState(actions.reduce((accum, val) => {
return {...accum, [val.id]: false};
}, {}));
useEffect(() => {
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
const updateStates = async () => {
for (let i = 0; i < actions.length; i++) {
if (i > 0) {
await delay(75);
}
setRenderStates((prev) => ({
...prev,
[i]: true,
}));
};
};
updateStates();
}, [actions.length]);
return (
<div className='Nav'>
{actions.map((act) => (
<div className={`Nav__Button ${renderStates[act.id] ? 'Animate' : ''}`} key={act.id}>
<Button icon={act.icon} onClick={act.onClick} />
</div>
))}
</div>
);
};
export default Nav;
/* Nav.scss */
.Nav {
height: 100%;
width: fit-content;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
align-self: center;
padding: 1rem;
}
.Nav > * {
margin: 20% 0,
}
.Nav__Button {
margin-left: -5rem;
}
.Animate {
animation: slideInFromLeft .4s ease;
}
#keyframes slideInFromLeft {
0% {
margin-left: -5rem;
}
75% {
margin-left: .5rem;
}
100% {
margin-left: 0;
}
}
Here's a codesandbox that illustrates the problem (refresh the embedded browser to see the issue):
https://codesandbox.io/s/react-css-animations-on-timer-8mxnsz
Any help would be appreciated. Thanks.
You will need to create a from the elements inside actions.map and render a memoized version of it so that if the props do not change it will not re-render.
import { useState, useEffect, memo } from "react";
import "./styles.css";
const Test = ({ animate, label }) => {
return (
<div className={`Nav__Button ${animate ? "Animate" : ""}`}>
<button>{label}</button>
</div>
);
};
const TestMemo = memo(Test);
export default function App() {
const actions = [
{
id: 0,
label: "button 0"
},
{
id: 1,
label: "button 1"
},
{
id: 2,
label: "button 2"
},
{
id: 3,
label: "button 3"
}
];
const [renderStates, setRenderStates] = useState(
actions.reduce((accum, val) => {
return { ...accum, [val.id]: false };
}, {})
);
useEffect(() => {
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const updateStates = async () => {
for (let i = 0; i < actions.length; i++) {
if (i > 0) {
await delay(2000);
}
setRenderStates((prev) => ({
...prev,
[i]: true
}));
}
};
updateStates();
}, [actions.length]);
return (
<div className="App">
{actions.map((act) => (
<TestMemo animate={renderStates[act.id]} label={act.label} />
))}
</div>
);
}

Ionic CSS Style Off when Deployed to iOS but fine everywhere else

I am not able to get my "Messages" Drawer to display in safari when in mobile, but it is displaying just fine in Chrome. Below are images of what it looks like in the 2 browsers. I assume this is a CSS issue and that the bottom option bar is covering the button in Safari, but I am not sure what needs to change to fix this issue?
I am noticing it has to do with the height and bottom values in the css code, but not sure exactly what to change it to to work in both web browsers?
Safari
Chrome
Here is the code for the Message Drawer
import { createGesture, IonButton, IonCard } from "#ionic/react";
/* Core CSS required for Ionic components to work properly */
import "#ionic/react/css/core.css";
import dynamic from "next/dynamic";
import { useRouter } from "next/router";
import React, { useEffect, useRef, useState } from "react";
import { useGetThreadPageByIdQuery } from "../../../graphql/generated/component";
import { Request_Participant_Status_Value_Enum } from "../../../graphql/generated/graphql";
import ThreadMessages from "../../Messages/List";
import styles from "./styles.module.css";
let MessageWrapper = dynamic(
() => import("../../../components/Threads/MessageBox"),
{ ssr: false }
);
interface refType {
dataset: {
open: string;
};
style: {
transition: string;
transform: string;
};
}
const MessageDrawer = () => {
let drawerRef = useRef();
let router = useRouter();
let { threadID } = router.query;
let messageContainerRef = useRef(null);
let { data, loading } = useGetThreadPageByIdQuery({
variables: { thread_id: threadID },
nextFetchPolicy: "cache-and-network",
});
let scrollToLastMessage = () => {
let timer = setTimeout(() => {
messageContainerRef.current?.scrollIntoView({ beavior: "smooth" });
console.log("This will run after 1 second!");
console.log("message last scroll", data);
}, 500);
return () => clearTimeout(timer);
};
let [initialScrollCompleted, setInitialScrollComplete] =
useState<boolean>(false);
useEffect(() => {
if (initialScrollCompleted) {
return;
}
if (data) {
scrollToLastMessage();
return setInitialScrollComplete(true);
}
}, [loading, data]);
useEffect(() => {
let c: HTMLIonCardElement = drawerRef.current;
c.dataset.open = "false";
let gesture = createGesture({
el: c,
gestureName: "my-swipe",
direction: "y",
onMove: (event) => {
if (event.deltaY < -300) return;
// closing with a downward swipe
if (event.deltaY > 20) {
c.style.transform = "";
c.dataset.open = "false";
return;
}
c.style.transform = `translateY(${event.deltaY}px)`;
},
onEnd: (event) => {
c.style.transition = ".5s ease-out";
if (event.deltaY < -30 && c.dataset.open !== "true") {
c.style.transform = `translateY(${-62}vh) `;
c.dataset.open = "true";
console.log("in on end");
}
},
});
// enable the gesture for the item
gesture.enable(true);
}, []);
let toggleDrawer = () => {
let c: HTMLIonCardElement = drawerRef.current;
if (c) {
if (c.dataset.open === "true") {
c.style.transition = ".5s ease-out";
c.style.transform = "";
c.dataset.open = "false";
} else {
c.style.transition = ".5s ease-in";
c.style.transform = `translateY(${-62}vh) `;
c.dataset.open = "true";
}
}
};
return (
<IonCard className={styles.bottomdrawer} ref={drawerRef}>
<div className={styles.toggleMessage}>
<IonButton className={styles.toggleMessagebtn} onClick={toggleDrawer}>
<ion-icon
class={styles.icon}
slot="start"
src="./assets/dashboard/icons/messages_tab.svg"
/>
<ion-label>Messages</ion-label>
</IonButton>
</div>
<div className={styles.messageContent}>
<ion-list>
{/* <ion-item slot="header" class={styles.item}>
<ion-icon
class={styles.icon}
slot="start"
color="dark"
src="./assets/dashboard/icons/messages_tab.svg"
/>
<ion-label>Messages</ion-label>
</ion-item> */}
<div
slot="content"
className={`${styles.list} ${styles.commentsContainer}`}
>
<ThreadMessages threadID={threadID as string} />
<div
className={`${styles.messageContainerEnd}`}
ref={messageContainerRef}
/>
</div>
</ion-list>
<div className={styles.messageTypeContainer}>
{!data?.thread_by_pk?.request && data?.thread_by_pk?.breakdown ? (
<MessageWrapper
participants={data?.thread_by_pk.participants}
thread_id={threadID as string}
/>
) : null}
{!data?.thread_by_pk
?.request ? null : data?.thread_by_pk?.request?.request_participants?.find(
(request_participant) =>
request_participant.status ===
Request_Participant_Status_Value_Enum.Canceled
) ? null : (
<MessageWrapper
participants={data?.thread_by_pk.participants}
thread_id={threadID as string}
/>
)}
</div>
</div>
</IonCard>
);
};
export default MessageDrawer;
Here is the CSS Code for the Message Drawer.
.bottomdrawer {
position: fixed;
right: 0;
left: 0;
bottom: -71vh;
height: 74vh;
max-height: 100%;
border-radius: 10px 10px 0 0px;
top: auto;
margin: 0;
padding: 0;
-webkit-box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
overflow: hidden;
width: calc(100% - 30px);
margin: 0 auto;
z-index: 9999;
}
.bottomdrawer[data-open="true"] {
bottom: -79vh;
I suggest placing it in the IonFooter component, this way it can be sticky on the bottom of the page without you having to manually calculate the distance.
However if you want to use the CSS, you can try:
.bottomdrawer {
position: fixed;
right: 0;
left: 0;
bottom: calc(env(safe-area-inset-bottom)-71vh);
...
}
.bottomdrawer[data-open="true"] {
bottom: calc(env(safe-area-inset-bottom)-79vh);

React component with state triggers animation in subcomponent when the state of parent component is changed

ProjectsSection component renders a document section with header and cards. A header animation should be triggered when the header is visible on the screen for the 1st time and run only once. However when I added a state to the ProjectsSection component with every state change a header animation runs and what is surprising to me only a part of it (letters/ spans that are children to h2 move, but brackets/ pseudoelements on h2 don't). In my project I use css modules for styling.
I have tried to wrap SectionHeader component in React.memo but it does not help.
Main component:
const ProjectsSection = () => {
const [openProjectCardId, setOpenProjectCardId] = useState('');
return (
<section id="projects" className="section">
<div className="container">
<SectionHeader>Projects</SectionHeader>
<div className={styles.content_wrapper}>
{projects.map((project, ind) => (
<Project
key={project.id}
project={project}
ind={ind}
isOpen={openProjectCardId === project.id}
setOpenProjectCardId={setOpenProjectCardId}
/>
))}
</div>
</div>
</section>
);
};
SectionHeader component:
const SectionHeader = ({ children }) => {
const headerRef = useIntersection(
styles.sectionHeader__isVisible,
{ rootMargin: '0px 0px -100px 0px', threshold: 1 },
);
const textToLetters = children.split('').map((letter) => {
const style = !letter ? styles.space : styles.letter;
return (
<span className={style} key={nanoid()}>
{letter}
</span>
);
});
return (
<div className={styles.sectionHeader_wrapper} ref={headerRef}>
<h2 className={styles.sectionHeader}>{textToLetters}</h2>
</div>
);
};
Css
.sectionHeader_wrapper {
position: relative;
// other properties
&::before {
display: block;
content: ' ';
position: absolute;
opacity: 0;
// other properties
}
&::after {
display: block;
content: ' ';
position: absolute;
opacity: 0;
// other properties
}
}
.sectionHeader {
position: relative;
display: inline-block;
overflow: hidden;
// other properties
}
.letter {
position: relative;
display: inline-block;
transform: translateY(100%) skew(0deg, 20deg);
}
.sectionHeader__isVisible .letter {
animation: typeletter .25s ease-out forwards;
}
useIntersection hook
const useIntersection = (
activeClass,
{ root = null, rootMargin = '0px', threshold = 1 },
dependency = [],
unobserveAfterFirstIntersection = true
) => {
const elementRef = useRef(null);
useEffect(() => {
const options: IntersectionObserverInit = {
root,
rootMargin,
threshold,
};
const observer = new IntersectionObserver((entries, observerObj) => {
entries.forEach((entry) => {
if (unobserveAfterFirstIntersection) {
if (entry.isIntersecting) {
entry.target.classList.add(activeClass);
observerObj.unobserve(entry.target);
}
} else if (entry.isIntersecting) {
entry.target.classList.add(activeClass);
} else {
entry.target.classList.remove(activeClass);
}
});
}, options);
// if (!elementRef.current) return;
if (elementRef.current) {
observer.observe(elementRef.current);
}
}, [...dependency]);
return elementRef;
};
I have found a solution to this problem.
This was apparently about textToLetters variable created every time the state of parent component was changing.
I used useMemo to keep this value and now animation is not triggered anymore.
const textToLetters = (text) =>
text.split('').map((letter) => {
const style = !letter ? styles.space : styles.letter;
return (
<span className={style} key={nanoid()}>
{letter}
</span>
);
});
const SectionHeader= ({ children }) => {
const headerRef = useIntersection(
styles.sectionHeader__isVisible,
{ rootMargin: '0px 0px -100px 0px', threshold: 1 },
[children]
);
const letters = React.useMemo(() => textToLetters(children), [children]);
return (
<div className={styles.sectionHeader_wrapper} ref={headerRef}>
<h2 className={styles.sectionHeader}>{letters}</h2>
</div>
);
};

Css Top Z-index cover bottom Scroll View

I am using React Native Typescript for my. In My app there are three elements one is Map, bottom-sheet (which position is absolute) and One button. I have created one custom bottom sheet. Bottom-sheet is animated Scrollable. Inside the Bottom-sheet I used Custom search bar and under the search bar there is Flat-list where I am rendering the data. In IOS simulator I can see the FlatList card under the Button. which is really bad from UI (IOS-Image) but in Android it looks perfect Android-simulator. I want to hide the flat-List data which is coming under the button. I tried background color white and tried with height, width, bottom.But none of of work. I want to make efficient css position instead of hard code position. But I don't know how to fixed it.
I share my code in Expo-snacks
This is my custom Bottom-sheet
import React from "react";
import {
SafeAreaView,
StyleSheet,
Text,
useWindowDimensions,
TouchableOpacity,
View,
} from "react-native";
import Animated, {
Extrapolate,
interpolate,
useAnimatedGestureHandler,
useAnimatedStyle,
withTiming,
} from "react-native-reanimated";
import { PanGestureHandler } from "react-native-gesture-handler";
import styled from "styled-components/native";
interface Props {
panY: number;
children: React.ReactNode;
}
export default function BottomSheet({ panY, children }: Props) {
const { height } = useWindowDimensions();
const gestureHandler = useAnimatedGestureHandler(
{
onStart(_, context) {
context.startY = panY.value;
},
onActive(event, context) {
panY.value = context.startY + event.translationY;
},
onEnd() {
if (panY.value < -height * 0.4) {
panY.value = withTiming(-(height * 0.6));
} else {
panY.value = withTiming(0);
}
},
},
[height]
);
const animatedStyle = useAnimatedStyle(() => {
return {
transform: [
{
translateY: interpolate(panY.value, [-1, 0], [-1, 0], {
extrapolateLeft: Extrapolate.EXTEND,
extrapolateRight: Extrapolate.CLAMP,
}),
},
],
};
});
return (
<PanGestureHandler onGestureEvent={gestureHandler}>
<Animated.View
style={[styles.container, { top: height * 0.7 }, animatedStyle]}
>
{children}
</Animated.View>
</PanGestureHandler>
);
}
const styles = StyleSheet.create({
container: {
position: "absolute",
top: 0,
left: 0,
right: 0,
backgroundColor: "white",
shadowColor: "black",
shadowOffset: {
height: -6,
width: 0,
},
shadowOpacity: 0.1,
shadowRadius: 5,
borderTopEndRadius: 15,
borderTopLeftRadius: 15,
},
});
This is My App component
import React, { useState } from "react";
import {
StyleSheet,
Text,
View,
Dimensions,
useWindowDimensions,
SafeAreaView,
StatusBar,
RefreshControl,
} from "react-native";
import styled from "styled-components/native";
import MapView from "react-native-maps";
import { useSharedValue } from "react-native-reanimated";
import { FlatList } from "react-native-gesture-handler";
import BottomSheet from "./ActionSheet";
import BottomButton from "./BottomButton";
import SafeAreaWrapper from "./SafeAreaWrapper";
import SearchBar from "./SearchBar";
const { width, height } = Dimensions.get("screen");
const api =
"http://open-api.myhelsinki.fi/v1/events/?distance_filter=60.1699%2C24.9384%2C10&language_filter=en&limit=100";
const initialRegion = {
latitudeDelta: 15,
longitudeDelta: 15,
latitude: 60.1098678,
longitude: 24.7385084,
};
export default function App() {
const { width, height } = useWindowDimensions();
const [loading, setLoading] = useState(false);
const [data, setData] = React.useState([]);
const updateState = async () => {
try {
const response = await fetch(api);
const data = await response.json();
setData(data.data);
} catch (error) {
console.log("failed to catch", error);
}
};
React.useEffect(() => {
updateState();
}, []);
const y = useSharedValue(0);
return (
<SafeAreaWrapper>
<Container>
<MapContent>
<MapView style={styles.mapStyle} initialRegion={initialRegion} />
</MapContent>
<BottomSheet panY={y}>
<Content>
<SearchContainer>
<SearchBar placeholder={"write some thing"} />
</SearchContainer>
{data && data === undefined ? (
<Text>loading</Text>
) : (
<HeroFlatList
data={data}
showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl
enabled={true}
refreshing={loading}
onRefresh={updateState}
/>
}
keyExtractor={(_, index) => index.toString()}
renderItem={({ item, index }) => {
const image = item?.description.images.map((img) => img.url);
const startDate = item?.event_dates?.starting_day;
return (
<EventContainer key={index}>
<EventImage
source={{
uri:
image[0] ||
"https://res.cloudinary.com/drewzxzgc/image/upload/v1631085536/zma1beozwbdc8zqwfhdu.jpg",
}}
/>
<DescriptionContainer>
<Title ellipsizeMode="tail" numberOfLines={1}>
{item?.name?.en}
</Title>
<DescriptionText>
{item?.description?.intro ||
"No description available"}
</DescriptionText>
<DateText>{startDate}</DateText>
</DescriptionContainer>
</EventContainer>
);
}}
/>
)}
</Content>
</BottomSheet>
<ButtonContainer>
<BottomButton
title={"Save"}
onPress={() => {
console.log("jjj");
}}
/>
</ButtonContainer>
</Container>
</SafeAreaWrapper>
);
}
const styles = StyleSheet.create({
mapStyle: {
width: width,
height: height, // I DON'T KNOW HOW TO CONVERT THEM INTO STYLED COMPONENT
},
});
const HeroFlatList = styled(FlatList).attrs({
contentContainerStyle: {
paddingTop: 20,
paddingBottom: 2000, // BAD PRACTICE
flexGrow: 1, //SEEMS LIKE IT DOES NOT WORK
},
})`
height: 2000px; // BAD PRACTICE
`;
const MapContent = styled.View`
flex: 1;
`;
const Content = styled.View`
flex: 1;
padding: 20px;
`;
const Container = styled.View`
flex: 1;
`;
const ButtonContainer = styled.View`
background-color: #fdfbfb; // My Button container
`;
const TextName = styled.Text`
color: orange;
font-weight: bold;
`;
const SearchContainer = styled.View`
margin-bottom: 5px;
`;
const Title = styled.Text`
font-size: 16px;
font-weight: 700;
margin-bottom: 5px;
`;
const DescriptionText = styled(Title)`
font-size: 14px;
opacity: 0.7;
`;
const DateText = styled(Title)`
font-size: 14px;
opacity: 0.8;
color: #0099cc;
`;
const EventImage = styled.Image`
width: 70px;
height: 70px;
border-radius: 70px;
margin-right: 20px;
`;
const DescriptionContainer = styled.View`
width: 200px;
`;
const EventContainer = styled.View`
flex-direction: row;
padding: 20px;
margin-bottom: 10px;
border-radius: 20px;
background-color: rgba(0, 0, 0, 0.8);
`;

React CSS: trying to give Color to class for a few seconds when there is a change on state vs prevState

Goal: I want to compare prevState with actualState to display a different class on a <div> depending on the comparision.
Problem: When the iteration results in no switch of class,i cant see the difference since the className doesnt trigger the keyframe event.
JSX:
import React, { useEffect, useState, useRef } from "react";
import styles from "./CoinContainer.module.css";
function usePrevious(data) {
const ref = useRef();
useEffect(() => {
ref.current = data;
}, [data]);
return ref.current;
}
export default function CoinContainer({ coin, price }) {
const [priceUpdated, setPrice] = useState("");
const prevPrice = usePrevious(priceUpdated);
useEffect(() => {
setInterval(priceUpdate, 20000);
});
const toggleClassName = () => {
return prevPrice > priceUpdated ? styles.redPrice : styles.greenPrice;
};
function priceUpdate() {
return fetch(
`https://api.coingecko.com/api/v3/simple/price?ids=${coin}&vs_currencies=usd`
)
.then((data) => data.json())
.then((result) => {
let key = Object.keys(result);
setPrice(result[key].usd);
});
}
return (
<div className={styles.padding}>
<h2>{coin}</h2>
<h3 className={toggleClassName()}>
{priceUpdated ? priceUpdated : price}$
</h3>
</div>
);
}
CSS:
#keyframes upFadeBetween {
from {
color: green;
}
to {
color: white;
}
}
#keyframes downFadeBetween {
from {
color: red;
}
to {
color: white;
}
}
.redPrice {
color: white;
animation: downFadeBetween 5s;
}
.greenPrice {
color: white;
animation: upFadeBetween 5s;
}
Thanks so much for any feedback/help!

Resources