I have a component that has a limitation, character limit that if it the user reach the limit the component will shake, thats the expected result, but in my current code nothing happen.
const useStyles = makeStyles(() => ({
shake:{
animation: 'description 0.5s',
animationIterationCount: '1',
},
'#keyframes description':{
'0%': { transform: 'translate(0)' },
'15%': { transform: 'translate(-4px, 0)' },
'30%': { transform: 'translate(6px, 0)' },
'45%': { transform: 'translate(-4px, 0)' },
'60%': { transform: 'translate(6px, 0)' },
'100%': { transform: 'translate(0)' },
},
}));
const StudentAdd = ({ setOpenAlertStudent }) => {
const classes = useStyles();
const CHARACTER_LIMIT = 100;
const [isShake, setShake] = useState(false)
const onHandleChangeInputDescription = (field, value) => {
if(value.length === 100){
setShake(true)
}
......
<TextArea
label="Description (optional)"
inputProps={{
maxLength: CHARACTER_LIMIT,
}}
values={getStringDescription.name}
onChange={(value) =>
onHandleChangeInputDescription('description', value)
}
helperText={`${getStringDescription.length}/${CHARACTER_LIMIT}`}
className={
isShake
? classes.shake
: null
}
id="description"
//id={isShake === true ? classes.shake : 'description'}
/>
......
this is the sample code on codesandbox
https://codesandbox.io/s/lucid-banach-o6sc1j?file=/src/App.js:1081-1089
import React, { useState, useContext } from "react";
import "./styles.css";
import { makeStyles } from "#mui/styles";
import { TextField } from "#mui/material";
const useStyles = makeStyles(() => ({
shake: {
animation: "$description 15s",
animationIterationCount: "1"
},
"#keyframes description": {
"0%": { opacity: 0, transform: "translateY(0)" },
"15%": { transform: "translateY(-4px, 0)" },
"30%": { transform: "translateY(6px, 0)" },
"45%": { transform: "translateY(-4px, 0)" },
"60%": { transform: "translateY(6px, 0)" },
"100%": { opacity: 1, transform: "translateY(0)" }
}
}));
export default function App() {
const classes = useStyles();
console.log(classes.shake);
const CHARACTER_LIMIT = 100;
const [isShake, setShake] = useState(false);
const [getStringDescription, setStringDescription] = useState({
length: 0,
value: ""
});
const onHandleChangeInputDescription = (field, value) => {
if (value.target.value.length === 100) {
setShake(true);
}
setStringDescription({
length: value.target.value.length,
value: value.target.value
});
};
return (
<div className="App">
<TextField
label="Description"
inputProps={{
maxLength: CHARACTER_LIMIT
}}
values={getStringDescription.name}
onChange={(value) =>
onHandleChangeInputDescription("description", value)
}
helperText={`${getStringDescription.length}/${CHARACTER_LIMIT}`}
sx={{
"& .MuiInputBase-input.MuiInputBase-inputMultiline": {
height: "100px !important"
},
"& .MuiTextField-root > .MuiOutlinedInput-root": {
padding: "8.5px 14px"
}
}}
id="description"
className={isShake ? classes.shake : null}
/>
</div>
);
}
your shaking animation code is not working, make changes in the keyframes for desired animation.
upon 100 character the animation class will be added. but once added you may need to remove the class, inorder to display the animation once again if the character is 100.
reference: https://stackoverflow.com/questions/58948890/how-to-apply-custom-animation-effect-keyframes-in-mui[1]
Related
I have a <TextInput> which, when the user enters anything into, will have a button appear from behind it.
This works fine using the Animated library from react, but each time I input text into the area, the animation "rubber bands" up and down.
My question is: How can I keep the button in place after the initial animation happens?
I still need to monitor whether the text box is empty in order to decide whether to show or hide the button.
My code:
import { View, TextInput, StyleSheet, Animated, TouchableOpacity } from "react-native";
import { useState } from "react";
import CurrencyInput from 'react-native-currency-input';
import { Ionicons } from "#expo/vector-icons";
type expenseItemData = {
item:string;
costAndSign:string;
cost:number | null;
}
type Props = {
sendItem: ({ item, cost, costAndSign }: expenseItemData) => void;
}
const AnimatedTouchable = Animated.createAnimatedComponent(TouchableOpacity);
const AddAndConfirmExpense: React.FC<Props> = ({sendItem}) => {
const [animation] = useState(new Animated.Value(-58));
const [expenseValue, setExpenseValue] = useState<number>(0.00);
const [expenseItem, setExpenseItem] = useState<string>("");
const [expenseValueAndSign, SetExpenseValueAndSign] = useState<string>("");
const [buttonLayoutPersistence, setButtonLayoutPersistence] = useState({
bottom:0
});
const [validExpenseItem, setValidExpenseItem] = useState<boolean>(false);
const onChangeExpenseItem = (text: string) => {
setExpenseItem(text);
if (text.trim().length === 0) {
setValidExpenseItem(false);
hideButton();
setButtonLayoutPersistence({bottom:0})
return;
}
if (validExpenseItem) {
setButtonLayoutPersistence({bottom:-48})
return
};
showButton();
setValidExpenseItem(true);
};
const onButtonPress = () => {
const newData:expenseItemData = {
item:expenseItem,
costAndSign:expenseValueAndSign,
cost:expenseValue
}
sendItem(newData);
};
const setAreaDynamicStyling = () => {
if (validExpenseItem) {
return {
borderTopRightRadius:5,
borderTopLeftRadius:5,
backgroundColor:"#f5f5f5"
}
}
return {borderRadius:5};
};
const setButtonDynamicStyling = () => {
if (!validExpenseItem) return {borderRadius:5}
return {borderBottomLeftRadius: 5,borderBottomRightRadius:5}
};
const animatedStyle = {transform: [{translateY:animation}],};
const showButton = () => {
Animated.timing(animation, {
toValue: -10,
duration: 1000,
useNativeDriver: true,
}).start();
}
const hideButton = () => {
Animated.timing(animation, {
toValue: -58,
duration: 500,
useNativeDriver: true,
}).start();
}
return (
<View>
<View style={validExpenseItem ? [styles.inputsContainer, setAreaDynamicStyling()] : [styles.inputsContainer,styles.shadowProp,setAreaDynamicStyling()]}>
<TextInput
style={styles.textInputArea}
placeholder='Item'
placeholderTextColor="#aaaaaa"
onChangeText={(text) => {onChangeExpenseItem(text)}}
value={expenseItem}
underlineColorAndroid="transparent"
autoCapitalize="none"
/>
<View style={styles.verticalLine}/>
<CurrencyInput
style={styles.currencyInputArea}
value={expenseValue}
onChangeValue={setExpenseValue}
prefix="£"
delimiter=","
separator="."
precision={2}
minValue={0}
onChangeText={(formattedValue) => {
SetExpenseValueAndSign(formattedValue)
}}
/>
</View>
<AnimatedTouchable onPress={()=>{onButtonPress()}} style={[{flex:1, zIndex:-1},animatedStyle]}>
<View style={[styles.confirmInputContainer, setButtonDynamicStyling(), buttonLayoutPersistence]}>
<Ionicons name="checkmark-circle-outline" size={24} color="white" />
</View>
</AnimatedTouchable>
</View>
)
}
const styles = StyleSheet.create({
confirmInputContainer:{
backgroundColor:"#7f96ff",
height: 48,
flexDirection:"row",
paddingVertical:10,
justifyContent:"center",
},
inputsContainer:{
backgroundColor:"white",
height: 48,
flexDirection:"row",
paddingVertical:10,
marginVertical:10,
justifyContent:"space-between",
},
shadowProp: {
shadowColor: '#353935',
shadowOffset: {width: -2, height: 4},
shadowOpacity: 0.2,
shadowRadius: 4,
},
textInputArea:{
width:"60%",
maxWidth:"60%",
marginLeft:20,
},
verticalLine:{
height: "100%",
width: 1,
backgroundColor: "#909090",
marginHorizontal:5,
},
currencyInputArea:{
maxWidth:"20%",
width:"20%",
marginRight:20
},
})
export default AddAndConfirmExpense;
EDIT:
I have added:
const [animationActive, setAnimationActive] = useState(false);
useEffect(() => {
if (!animationActive && validExpenseItem) {
setButtonLayoutPersistence({bottom:-48})
};
if (!animationActive && !validExpenseItem) {
setButtonLayoutPersistence({bottom:0})
}
},[animationActive])
And changed my show and hide functions to the following:
const showButton = () => {
if (animationActive) return;
setAnimationActive(true)
Animated.timing(animation, {
toValue: -10,
duration: 500,
useNativeDriver: true,
}).start(
() => {
setAnimationActive(false)
}
);
}
const hideButton = () => {
if (animationActive) return;
setAnimationActive(true)
Animated.timing(animation, {
toValue: -58,
duration: 500,
useNativeDriver: true,
}).start(
() => {
setAnimationActive(false)
}
);
}
I have also changed onChangeExpenseItem to :
const onChangeExpenseItem = (text: string) => {
setExpenseItem(text);
if (text.trim().length === 0) {
setValidExpenseItem(false);
hideButton();
setButtonLayoutPersistence({bottom:0})
return;
}
if (!validExpenseItem && !animationActive) {
setValidExpenseItem(true);
showButton();
return
};
};
It is slightly better now, but a better solution is still needed.
I have the below custom <CompleteToast/> component built using react-bootstrap. I want to apply custom CSS transitions, but don't understand how to achieve this. The docs say I should pass a custom react-transition-group TransitionComponent but don't expand on how or give an example.
I've tried looking at the default <ToastFade/> but don't understand Typescript. I've tried passing <ToastTransition/> as a reference but the children don't render (the reference works as "Exit animation complete!" is logged). Passing <ToastTransition children={...}/> causes errors.
What approach should I be taking here? Is this possible without TypeScript?
My instinct is that I need to figure out how ToastTransition can automatically inherit Toast's children.
CompleteToast.js
import PropTypes from 'prop-types';
import {Toast} from "react-bootstrap";
import {useContext, useEffect, useRef, useState} from "react";
import {IoCheckmarkCircleOutline} from "react-icons/all";
import {GlobalAppContext} from "../../App";
import ToastTransition from "../react-transition-group/ToastTransition";
const dividers = {
s: 1000,
m: 60000,
h: 3.6e+6,
d: 8.64e+7,
w: 6.048e+8,
mo: 2.628e+9,
y: 3.154e+10
}
const calculateTimeDiff = (timeInMs) => {
const diff = Date.now() - timeInMs;
switch (true) {
case (diff < dividers.s):
return "Just now";
case (diff < dividers.m):
return Math.floor(diff / dividers.s) + "s";
case (diff < dividers.h):
return Math.floor(diff / dividers.m) + "m";
case (diff < dividers.d):
return Math.floor(diff / dividers.h) + "h";
case (diff < dividers.w):
return Math.floor(diff / dividers.d) + "d";
case (diff < dividers.mo):
return Math.floor(diff / dividers.w) + "w";
case (diff < dividers.y):
return Math.floor(diff / dividers.mo) + "mo";
case (diff >= dividers.y):
return Math.floor(diff / dividers.y) + "y";
default:
return diff + "ms";
}
}
const timeUntilNext = (from, unit = "s") => {
let divider = dividers[unit];
return (Math.ceil(from / divider) * divider) - from;
}
function CompleteToast({show, title, timestamp, bodyText, headerClass, updateDeleteIds, id, deleteIds}) {
const [showState, setShowState] = useState(show);
const [timestampUpdated, setTimestampUpdated] = useState(null);
const [timestampState, setTimestampState] = useState(timestamp);
const [timestampText, setTimestampText] = useState("Just now");
const setToasts = useContext(GlobalAppContext)[0].setStateFunctions.toasts;
const shownOnce = useRef(false);
const deleteTimeout = useRef(null);
const hovering = useRef(false);
const close = () => {
if (!hovering.current) {
setShowState(false);
}
}
const setDeleteTimeout = () => {
if (shownOnce.current && !hovering.current) {
deleteTimeout.current = setTimeout(() => {
setToasts(prevState => prevState.filter(x => x.id !== id));
}, 2000)
}
}
useEffect(() => {
if (showState) {
setTimestampUpdated(Date.now());
setTimestampState(Date.now);
} else {
setDeleteTimeout();
}
}, [showState]);
useEffect(() => {
if (!showState) {
setShowState(true);
shownOnce.current = true;
}
}, []);
useEffect(() => {
if (showState) {
//timestamp has been updated and the toast is still showing - update the text to the current time difference
const timeDiff = calculateTimeDiff(timestampState);
setTimestampText(timeDiff);
setTimeout(() => {
//trigger new update to timestamp text on the next second
setTimestampUpdated(Date.now());
}, timeUntilNext(Date.now(), "s"));
}
}, [timestampUpdated]);
return (
<Toast style={{whiteSpace: "pre-wrap"}}
onClose={close}
onClick={close}
onMouseEnter={() => {
hovering.current = true;
if (deleteTimeout.current) {
clearTimeout(deleteTimeout.current);
deleteTimeout.current = null;
setShowState(true);
}
}}
onMouseLeave={() => {
hovering.current = false;
setTimeout(close, 3000);
}}
show={showState}
delay={4000}
autohide
id={id}
transition={ToastTransition}
className="cursor-pointer">
<Toast.Header className={headerClass} closeButton={false}>
<IoCheckmarkCircleOutline className={"smallIcon me-2"}/>
<p className="fs-5 my-0 me-auto">{title}</p>
<small>{timestampText}</small>
</Toast.Header>
<Toast.Body className={"position-relative"}>
{bodyText}
</Toast.Body>
</Toast>
);
}
CompleteToast.propTypes = {
show: PropTypes.bool,
handleClick: PropTypes.func,
buttonText: PropTypes.string,
buttonVariant: PropTypes.string,
bodyText: PropTypes.string,
headerClass: PropTypes.string,
title: PropTypes.string
};
CompleteToast.defaultProps = {
show: false,
timestamp: Date.now(),
timestampText: Date.now(),
title: "Success",
bodyText: "The operation was completed successfully",
headerClass: "bg-success text-white",
buttonVariant: "primary"
}
export default CompleteToast;
ToastTransition.js
import {Transition} from 'react-transition-group';
//copied from react-transition-group docks - aim is to getit working, then customise the actual transition
const duration = 300;
const defaultStyle = {
transition: `opacity ${duration}ms ease-out, maxHeight opacity ${duration}ms ease-out`,
opacity: 0,
maxHeight: 0,
}
const transitionStyles = {
entering: {opacity: 1, maxHeight: "200px"},
entered: {opacity: 1, maxHeight: "200px"},
exiting: {opacity: 0, maxHeight: 0},
exited: {opacity: 0, maxHeight: 0},
};
const ToastTransition = ({in: inProp, children}) => (
<Transition
in={inProp}
timeout={duration}
onExited={() => {
console.log("Exit animation complete!")
}}>
{state => (
<div style={{
...defaultStyle,
...transitionStyles[state]
}}>
{children}
</div>
)}
</Transition>
);
export default ToastTransition;
I'm an idiot - it's as simple as calling the children prop which is 'special' and is passed automatically.
It was visually rendering because I needed to manually set a show class on the <CompleteToast/> component, i.e.:
className="cursor-pointer show"
The react-transition-group component handles the actual showing/hiding it seems (this is inconsistent with the default behaviour, but as far as I can see it's not cuasing issues so far).
Working ToastTransition.js (need to tweak CSS)
import {Transition} from 'react-transition-group';
const duration = 500;
const defaultStyle = {
transition: `opacity ${duration}ms ease-in-out, max-height ${duration}ms ease-out`,
opacity: 0,
maxHeight: 0
}
const transitionStyles = {
entering: {opacity: 1, maxHeight: "200px"},
entered: {opacity: 1, maxHeight: "200px"},
exiting: {opacity: 0, maxHeight: 0},
exited: {opacity: 0, maxHeight: 0},
};
const ToastTransition = ({in: inProp, children}) => {
return (
<Transition
in={inProp}
timeout={duration}
onExited={() => {
children.props.onExited();
}}>
{(state) => (
<div style={{
...defaultStyle,
...transitionStyles[state]
}}>
{children}
</div>
)}
</Transition>
)
}
export default ToastTransition;
This is an image that changes every 5 seconds, but I think it's distracting, because it doesn't have any fade. How can I make it so that there is a short transition time like you can do in CSS?
import React, { useEffect, useState } from "react";
import aa from '../imgs/aa.JPG'
import aa2 from '../imgs/aa2.JPG'
import aa3 from '../imgs/aa3.JPG'
import aa4 from '../imgs/aa4.JPG'
import gg from '../imgs/gg.jpeg'
import gg2 from '../imgs/gg2.jpeg'
import gg3 from '../imgs/gg3.jpeg'
import gg4 from '../imgs/gg4.jpeg'
import './AnimatedGalery.css'
const images = [aa, aa2, aa3, aa4, gg, gg2, gg3, gg4];
export default function () {
let [currentIndex, setCurrentIndex] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
if(currentIndex == images.length - 1) {
setCurrentIndex(currentIndex = 0);
}
else {
setCurrentIndex(currentIndex = currentIndex + 1);
}
}, 5000)
return () => clearInterval(intervalId);
}, [])
return (
<div>
<img src={images[currentIndex]} />
</div>
)
}
Have a look at React Spring https://aleclarson.github.io/react-spring/v9/
Here is a quick sandbox showing a demo of what it sounds like you're after.
https://codesandbox.io/s/affectionate-nightingale-r7yjm?file=/src/App.tsx
Essentially, React Spring's useTransition hook will play spring-based animations when the data provided to the hook changes.
import { animated, useTransition } from "#react-spring/web";
import * as React from "react";
const imageUrls = [
"https://images.unsplash.com/photo-1462396240927-52058a6a84ec?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=966&q=80",
"https://images.unsplash.com/photo-1495314736024-fa5e4b37b979?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1352&q=80",
"https://images.unsplash.com/photo-1612004687343-617e7c8f68d8?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=634&q=80"
];
export default function App() {
const [index, setIndex] = React.useState(0);
const [imageUrl, setImageUrl] = React.useState(imageUrls[index]);
React.useEffect(() => {
const timeoutId = window.setTimeout(() => {
setIndex((s) => {
let newIndex = 0;
if (s < imageUrls.length - 1) newIndex = s + 1;
setImageUrl(imageUrls[newIndex]);
return newIndex;
});
}, 2500);
return () => clearTimeout(timeoutId);
}, [index]);
const transition = useTransition(imageUrl, {
from: { opacity: 0, transform: "translateY(-1rem) scale(0.75)" },
enter: { opacity: 1, transform: "translateY(-0rem) scale(1)" },
leave: { opacity: 0, transform: "translateY(1rem) scale(1.25)" }
});
const fragment = transition((style, item) => (
<animated.div style={{ ...style, position: "absolute" }}>
<img
src={item}
alt=""
style={{ width: 200, height: 200, objectFit: "cover" }}
/>
</animated.div>
));
return (
<div
style={{
display: "flex",
width: "100vw",
height: "100vh",
alignItems: "center",
justifyContent: "center"
}}
>
{fragment}
</div>
);
}
I am trying to learn React-Spring. One of the codes provided in its documentation throws an error when I run it. Any idea what possibly is wrong? How to solve it?
The code I'm trying to run is-
const TextContent = (props) => {
const [items] = useState([
{ id: '0', title: 'Text1' },
{ id: '1', title: 'Text2' },
{ id: '2', title: 'Text1' }
])
const [index, setIndex] = useState(0);
const transitions = useTransition(items[index], index => index.id,
{
from: { opacity: 0 },
enter: { opacity: 1 },
leave: { opacity: 0 },
config: { tension: 220, friction: 120 }
}
)
useEffect(() => {
const interval = setInterval(() => {
setIndex((state) => (state + 1) % items.length);
}, 4000)
return () => clearInterval(interval);
}, []);
{
transitions.map(({ item, props, key }) => (
<animated.div
key={key}
style={{ ...props, position: 'absolute' }}
>
<p>
{item.title}
</p>
</animated.div>
))
}
}
export default TextContent;
Add a return statement to your functional component
const TextContent = (props) => {
const [items] = useState([
{ id: '0', title: 'Text1' },
{ id: '1', title: 'Text2' },
{ id: '2', title: 'Text1' }
])
const [index, setIndex] = useState(0);
const transitions = useTransition(items[index], index => index.id,
{
from: { opacity: 0 },
enter: { opacity: 1 },
leave: { opacity: 0 },
config: { tension: 220, friction: 120 }
}
)
useEffect(() => {
const interval = setInterval(() => {
setIndex((state) => (state + 1) % items.length);
}, 4000)
return () => clearInterval(interval);
}, []);
return (
<div>
{
transitions.map(({ item, props, key }) => (
<animated.div
key={key}
style={{ ...props, position: 'absolute' }}
>
<p>{item.title}</p>
</animated.div>
))
}
</div>
)
}
export default TextContent;
Here is a codesandbox where I got it working
In addition to Al Duncanson answer: My problem was in exporting React Fragment instead of actual tag:
return (
<>
{ /* springs.map() */ }
</>
)
Hook started working after I changed it to
return (
<div>
{ /* springs.map() */ }
</div>
)
In my NextJs app, I got the same issue. In my case, I think it was a cache-related issue. Run the project after removing the ".next" folder fixed the issue. I hope removing the build folder in React will do the same.
And there are two similar functions ("useSpring" and "useSprings"). Make sure to pick the right one for your use case.
I've done some research on how to trigger CSS animation once the element comes into view, and I've found the answer that makes use of IntersectionObserver and element.classList.add('.some-class-name')
Above method is demonstrated in pure CSS, but I want to implement it with Material-UI. Here is my code.
import React, { useEffect } from 'react';
import { makeStyles } from '#material-ui/core/styles';
const useStyles = makeStyles((theme) => ({
root: {
height: '100vh'
},
box: {
opacity: 0,
width: 100,
height: 100,
backgroundColor: 'red'
},
animated: {
animationName: '$fadein',
animationDuration: '1s'
},
'#keyframes fadein': {
'0%': {
opacity: 0
},
'100%': {
opacity: 1
}
},
}));
function App() {
const classes = useStyles();
useEffect(() => {
const observer = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
if (entry.intersectionRatio > 0) {
// trigger animation
entry.target.classList.add('animated');
// remove observer
observer.unobserve(entry.target);
}
});
});
const element = document.getElementById('item');
observer.observe(element);
}, []);
return (
<div>
<div className={classes.root} />
<div id="item" className={classes.box} />
</div>
);
};
export default App;
Unfortunately, the above code isn't working and I think it's because the className 'animated' does not exist. I know Material-UI has internal logic that generates the unique className, so my question is how do I figure out the real className of 'animated'? Or, is there a better way to go about this? Any help would be appreciated.
This is what I came up with.
import React, { useEffect, useState, useRef } from 'react';
import { makeStyles } from '#material-ui/core/styles';
const useStyles = makeStyles((theme) => ({
root: {
height: '100vh'
},
box: {
opacity: 0,
width: 100,
height: 100,
backgroundColor: 'red'
},
animated: {
animationName: '$fadein',
animationDuration: '1s',
animationFillMode: 'forwards'
},
'#keyframes fadein': {
'0%': {
opacity: 0
},
'100%': {
opacity: 1
}
}
}));
function App() {
const classes = useStyles();
const BoxSection = (props) => {
const [isVisible, setVisible] = useState(false);
const domRef = useRef();
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => setVisible(entry.isIntersecting));
});
observer.observe(domRef.current);
return () => observer.unobserve(domRef.current); // clean up
}, []);
return (
<div className={`${classes.box} ${isVisible ? classes.animated : ''}`} ref={domRef}>
{props.children}
</div>
);
};
return (
<div>
<div className={classes.root} />
<BoxSection />
</div>
);
}
export default App;
Basically, I've decided to use state to trigger animation by adding the class like above. I've got some pointers from this article, if anyone is interested.