Best way to make a custom smooth scroll and scrollto coexist (ReactJs - Framer motion) - reactjs

Made a smoothscroll component using framer motion that's working well :
export default function SmoothScroll({ children }: Props) {
const { width } = useWindowSize();
const scrollContainer = useRef() as RefObject<HTMLDivElement>;
const [pageHeight, setPageHeight] = useState(0);
useEffect(() => {
setTimeout(() => {
// added a setTimeout so the page has the time to load and it still fits
const scrollContainerSize =
scrollContainer.current?.getBoundingClientRect();
scrollContainerSize && setPageHeight(scrollContainerSize.height);
}, 500);
}, [width]);
const { scrollY } = useScroll(); // measures how many pixels user has scrolled vertically
// as scrollY changes between 0px and the scrollable height, create a negative scroll value...
// ... based on current scroll position to translateY
const transform = useTransform(scrollY, [0, pageHeight], [0, -pageHeight]);
const physics = { damping: 15, mass: 0.17, stiffness: 55 }; // easing of smooth scroll
const spring = useSpring(transform, physics); // apply easing to the negative scroll value
return (
<>
<motion.div
ref={scrollContainer}
style={{ y: spring }} // translateY of scroll container using negative scroll value
className="app fixed overflow-hidden w-screen"
>
{children}
</motion.div>
<motion.div style={{ height: pageHeight }} />
</>
);
}
The thing is, I'd like to scrollTo sections of my page upon click on the navbar but don't really know how to implement it without removing the smoothScroll ...
Tried the following logic but obviously it did not work as the vanilla scroll has been hijacked :
const scrollToSection = (
e: React.MouseEvent<HTMLLIElement, globalThis.MouseEvent>,
anchor?: string
) => {
e.preventDefault();
if (!anchor) return;
const section = document.querySelector(anchor);
section?.scrollIntoView({ behavior: "smooth" });
};
Is it doable ?

Related

Framer motion Follow same duration on hover end

I have an animation that scales the div on hover with duration of 2 which is working correctly. I want same duration to be followed when the mouse leaves the div. Currently the div shrinks instantly to original size.
import "./styles.css";
import { motion } from "framer-motion";
const variants = {
hover: { scale: 2, transition: { duration: 2 } }
};
export default function App() {
return (
<div className="App">
<motion.div
variants={variants}
whileHover="hover"
className="card"
></motion.div>
</div>
);
}
There is a onHoverEnd prop but it is a function instead. Do I have to manually start another animation in that function?
Codesandbox link
Achieved it by manually stating the start and ending animations with onHoverStart and onHoverEnd
const start = useCallback(() => {
controls.start("hoverStart");
}, [controls]);
const end = useCallback(() => {
controls.start("hoverEnd");
}, [controls]);

odd behavior with framer motion animate on presence

I'm working ona project management application , and I'm developing this feature where the project header can go into editing state where user can edit project title , this header has two child components HeaderContent and EditProjectForm :
>ProjectHeader
-->HeaderContent
-->EditProjectForm
my problem now is that when I fade out the HeaderContent the faded In EditProjectForm is intially pushed down then it jumps to its place , it seems that this happens because even though HeaderContent was faded out it still was affecting the dom structure
here is a short screen recording I just uploaded to further make things clear https://www.youtube.com/watch?v=UerYDuEcUWQ
Header component
const ProjectHeader=()=>{
const [isEditingState, setisEditingProject] = useState({value:false,triggerFrom:"PANEL_HEADER"})
return <div>
<HeaderContent {...{isEditingProject, setisEditingProject}} />
<EditProjectForm {...{isEditingProject, setisEditingProject}} />
</div>
}
HeaderContent
const HeaderContent =({isEditingProject, setisEditingProject})=>{
const [render, setrender] = useState(true)
useEffect(() => {
let ref=null
// if(isEditingProject.triggerFrom =="EDIT_PROJECT_FORM") if I have don this whenever I change isEditingProject.value from this component this setTimeout below will be fired and I want to only be fired when this compoent is unmounted
if(isEditingProject.triggerFrom =="EDIT_PROJECT_FORM"){
ref= setTimeout(() => {
setrender(!isEditingProject.value)
}, 200);//wait until edit form finishes its fade_out animtion
}
return ()=>ref && clearTimeout(ref)
}, [isEditingProject])
return <AnimatePresence initial={false} >
{
render
&&(<motion.div
animate={{opacity:1 ,y:0}}
initial={{opacity:1 ,y:0}}
exit ={{opacity:0 ,y:10}}
transition={{
duration:.2,
opacity: { type: "spring", stiffness: 100 },
}}>
//.. header links and buttons and the title
<button onClick={e=>{
setrender(false)
setisEditingProject({...isEditingProject,value:true,triggerFrom:"PANEL_HEADER"})
}} >edit</button>
}
</AnimatePresence >
}
EditProjectForm
const EditProjectForm =({isEditingProject, setisEditingProject})=>{
const [render, setrender] = useState(true)
useEffect(() => {
let ref =null
// if(isEditingProject.triggerFrom =="PANEL_HEADER") if I haven't don this whenever I change isEditingProject.value from this component this setTimeout below will be fired and I want to only be fired when this compoent is unmounted
if(isEditingProject.triggerFrom =="PANEL_HEADER"){
ref=setTimeout(() => {
setrender(isEditingProject.value)
}, 200);
}
return ()=>ref && clearTimeout(ref)
}, [isEditingProject.value])
return <AnimatePresence>
{
render && <motion.form
animate={{ opacity:1 ,y:0 }}
initial={{ opacity:1 ,y:10 }}
exit ={{ opacity:0 ,y:-10}}
transition={{
duration:.2,
opacity: { type: "spring", stiffness: 100 },
}}
>
/.. title input
<button onClick={e=>{
setrender(false)
setisEditingProject({...isEditingProject,value:true,triggerFrom:"EDIT_PROJECT_FORM"})
}} >edit</button>
</motion.form>
}
</AnimatePresence>
}
It's problem with css, not with framer. My advise is to wrap your component with div absolute position, where top:0;
Maybe, you have some flex divs which trigger this "strange" behaviour

How to make a Header that Animates from Transparent to Opaque Color on Scrolling down in React-Native React Navigation 5?

I'm trying to make header that will animate from transparent to solid opaque color upon scrolling down using in React-Native React Navigation 5.
Starts to transition to opaque when scrolling halfway
Becomes fully opaque when reach the maximum offset
You can do this by setting the header style opacity to an animated value.
First define your animated value, we'll interpolate the yOffset to get the opacity desired.
const yOffset = useRef(new Animated.Value(0)).current;
const headerOpacity = yOffset.interpolate({
inputRange: [0, 200],
outputRange: [0, 1],
extrapolate: "clamp",
});
then you want to attach an animated.event listener to an animated scroll view
<Animated.ScrollView
onScroll={Animated.event(
[
{
nativeEvent: {
contentOffset: {
y: yOffset,
},
},
},
],
{ useNativeDriver: true }
)}
scrollEventThrottle={16}
>
Your content should be inside the scroll view
In your screen add a on mount or use effect where you set the animatedValue as the header opacity
useEffect(() => {
navigation.setOptions({
headerStyle: {
opacity: headerOpacity,
},
headerBackground: () => (
<Animated.View
style={{
backgroundColor: "white",
...StyleSheet.absoluteFillObject,
opacity: headerOpacity,
}}
/>
),
headerTransparent: true,
});
}, [headerOpacity, navigation]);
I've used header transparent and header background so that the background component changes also.
Here is an example:
https://snack.expo.io/#dannyhw/react-navigation-animated-header
const handleScroll = () => {
YourElementRef.current.style.backgroundColor = `rgba(245, 245, 245, ${window.scrollY > 300 ? 1 : '0.' + (window.scrollY * 3)})`;
}
window.addEventListener('scroll', handleScroll, true);
You need to add scroll listener and call function that animates it.
The scroll element is represented by a ref stuff. e.g.
const YourElementRef = React.useRef(null);
<SomeElement ref={YourElementRef}...

How to re-animate react-spring animation using hooks on button click?

Following simple component from the official examples:
import {useSpring, animated} from 'react-spring'
function App() {
const props = useSpring({opacity: 1, from: {opacity: 0}})
return <animated.div style={props}>I will fade in</animated.div>
}
Question
How do I animate the fadeIn-effect (or any other animation) again for example when I click on a button or when a promise is resolved?
You can basically make two effect with useSpring and an event.
You can change the style for example the opacity depending on the state of an event.
You can restart an animation on state change. The easiest way to restart is to rerender it.
I created an example. I think you want the second case. In my example I rerender the second component with changing its key property.
const Text1 = ({ on }) => {
const props = useSpring({ opacity: on ? 1 : 0, from: { opacity: 0 } });
return <animated.div style={props}>I will fade on and off</animated.div>;
};
const Text2 = () => {
const props = useSpring({ opacity: 1, from: { opacity: 0 } });
return <animated.div style={props}>I will restart animation</animated.div>;
};
function App() {
const [on, set] = React.useState(true);
return (
<div className="App">
<Text1 on={on} />
<Text2 key={on} />
<button onClick={() => set(!on)}>{on ? "On" : "Off"}</button>
</div>
);
}
Here is the working example: https://codesandbox.io/s/upbeat-kilby-ez7jy
I hope this is what you meant.

Setting the fontSize of <html> in React for using REM in responsive design

I'm currently working on a pet web project with react and I'm using a container that's 16:9 and scaling with the current viewport. The idea is based on a pen, I've created with vanilla JS (https://codepen.io/AllBitsEqual/pen/PgMrgm) and that's already working like a charm.
function adjustScreen() {...}
adjustScreen()
[...]
const resizeHandler = function () { adjustScreen() }
window.addEventListener('resize', helpers.debounce(resizeHandler, 250, this))
I've now written a script that's sitting in my App.jsx, (un)binds itself via useEffect and calculates the current size of the viewport to adjust the container, whenever the viewport changes (throttled for performance). I'm also using media queries to adjust the size and font size of elements in the container, which is working ok but isn't much fun to work with.
I want to expand on this idea and change the font size of the HTML Element in the same function that calculated the current container size so that I can use REM to scale font-size and other elements based on my calculated root font size.
Is there a valid and future-proofed way of changing the font-size style of my Tag via ReactJS either via the style tag or style attribute?
For now I've resorted to using "document.documentElement" and "style.fontSize" to achieve what I wanted but I'm not 100% sure this is the best solution. I'll see if I can find or get a better solution before I accept my own answer as the best...
I'm using useState for the game dimensions and within the useEffect, I'm attaching the listener to resize events, which I throttle a bit for performance reasons.
const App = () => {
const game_outerDOMNode = useRef(null)
const rootElement = document.documentElement
function getWindowDimensions() {
const { innerWidth: width, innerHeight: height } = window
return {
width,
height,
}
}
const [gameDimensions, setGameDimensions] = useState({ width: 0, height: 0})
useEffect(() => {
function adjustScreen() {
const game_outer = game_outerDOMNode.current
const ratioHeight = 1 / 1.78
const ratioWidth = 1.78
const { width: vw, height: vh } = getWindowDimensions()
const width = (vw > vh)
? (vh * ratioWidth <= vw)
? (vh * ratioWidth)
: (vw)
: (vh * ratioHeight <= vw)
? (vh * ratioHeight)
: (vw)
const height = (vw > vh)
? (vw * ratioHeight <= vh)
? (vw * ratioHeight)
: (vh)
: (vw * ratioWidth <= vh)
? (vw * ratioWidth)
: (vh)
const longestSide = height > width ? height : width
const fontSize = longestSide/37.5 // my calculated global base size
setGameDimensions({ width, height })
rootElement.style.fontSize = `${fontSize}px`
}
const debouncedResizeHandler = debounce(200, () => {
adjustScreen()
})
adjustScreen()
window.addEventListener('resize', debouncedResizeHandler)
return () => window.removeEventListener('resize', debouncedResizeHandler)
}, [rootElement.style.fontSize])
const { width: gameWidth, height: gameHeight } = gameDimensions
return (
<div
className="game__outer"
ref={game_outerDOMNode}
style={{ width: gameWidth, height: gameHeight }}
>
<div className="game__inner">
{my actual game code}
</div>
</div>
)
}

Resources