How can I use useRef when using ScrollTigger in React? - reactjs

I'm using Gsap's ScrollTigger to develop horizontal scrolling.
If a ref is passed when using Gsap's toArray, only the ref of the last element that uses the ref will be referenced. How can I pass all used refs to toArray?
Is only className used as an argument to toArray? Or is there another way to implement horizontal scrolling differently?
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import { useLayoutEffect, useRef } from 'react';
import styled from 'styled-components';
gsap.registerPlugin(ScrollTrigger);
const Home = () => {
const panelRef = useRef(null);
const containerRef = useRef(null);
useLayoutEffect(() => {
const sections = gsap.utils.toArray(panelRef); // If you pass a ref, only the last ref will be referenced
gsap.to(sections, {
xPercent: -100 * (sections.length - 1),
scrollTrigger: {
trigger: containerRef.current,
pin: true,
scrub: 1,
end: '+=3500',
},
});
}, []);
return (
<Container ref={containerRef}>
<Panel className="panel" ref={panelRef}>
ONE
</Panel>
<Panel className="panel" ref={panelRef}>
TWO
</Panel>
<Panel className="panel" ref={panelRef}>
THREE
</Panel>
</Container>
);
};
const Container = styled.div`
position: relative;
overscroll-behavior: none;
height: 100%;
width: max-content;
display: flex;
flex-direction: row;
`;
const Panel = styled.div`
height: 100%;
width: 100vw;
background-color: #000;
`;
export default Home;

import { useRef, useEffect } from 'react';
import { ScrollTrigger } from 'react-scroll-trigger';
function MyComponent() {
const triggerRef = useRef(null);
useEffect(() => {
const current = triggerRef.current;
current.addEventListener("enter", () => {
// do something
});
current.addEventListener("leave", () => {
// do something
});
return () => {
current.removeEventListener("enter", () => {});
current.removeEventListener("leave", () => {});
};
}, []);
return (
<div>
<ScrollTrigger ref={triggerRef}>
<MyContent />
</ScrollTrigger>
</div>
);
}

Related

I want to close the modal when react esc is pressed

We are using react and styled-components.
When the button is pressed, the modal(drawer) is displayed from the right.
I want the modal to be closed when the esc button is pressed.
How do I detect that the esc button has been pressed?
codesandbox
import Drawer from "./components/Drawer";
import { FunctionComponent, useState } from "react";
const App: FunctionComponent = () => {
const [isOpen, setIsOpen] = useState(false);
const onClose = () => {
setIsOpen(false);
};
return (
<div className="App">
<button onClick={() => setIsOpen(!isOpen)}>button</button>
<Drawer isOpen={isOpen} onClose={onClose}></Drawer>
</div>
);
};
export default App;
import React from "react";
import styled from "styled-components";
type Props = {
isOpen: boolean;
padding?: number;
children: React.ReactNode;
onClose: () => void;
handleKeyDown: () => void;
};
export default (props: Props) => {
return (
<>
<Overlay isOpen={props.isOpen} onClick={props.onClose} />
<DrawerModal {...props}>{props.children}</DrawerModal>
</>
);
};
const DrawerModal = styled.div<Props>`
position: absolute;
overflow: scroll;
top: 0;
right: 0;
height: 100vh;
width: ${({ isOpen }: Props) => (isOpen ? 400 : 0)}px;
background: blue;
box-shadow: 0px 0px 20px rgba(0, 0, 0, 0.25);
transition: 200ms cubic-bezier(0.25, 0.1, 0.24, 1);
`;
const Overlay = styled.div<{ isOpen: boolean }>`
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: ${(props: { isOpen: boolean }) => (props.isOpen ? 0 : -1)};
`;
Use an effect like this to handle keyboard events like this:
useEffect(() => {
function handleEscapeKey(event: KeyboardEvent) {
if (event.code === 'Escape') {
setIsOpen(false)
}
}
document.addEventListener('keydown', handleEscapeKey)
return () => document.removeEventListener('keydown', handleEscapeKey)
}, [])
The effect declares a function that handle a keydown event and checks if the escape key was pressed. Then it binds that event handler to the document. When the component is removed, it cleans up by removing the event handler from the document.

react styled component created dynamically

I am new to styled Components from react and I am having trouble creating dynamically a clock with has an initial degree for minutes(minute degrees) and an initial degree for hours (hourDegrees).
This is so far what I have achieved, but I get the following message:
Keyframes.js:20 The component styled.div with the id of "..." has been created dynamically.
You may see this warning because you've called styled inside another component.
To resolve this only create new StyledComponents outside of any render method and function component.
APP CODE
function App() {
return (
<div className="App">
<main>
<div className="clock-wrap">
<section className="firstMinute">
{numbers[0].map((coord, index) => {
return (
<Clock
key={index}
hourDegrees={coord[0]}
minuteDegrees={coord[1]}
/>
);
})}
</section>
<section className="secondMinute">
{numbers[1].map((coord, index) => {
return (
<Clock
key={index}
hourDegrees={coord[0]}
minuteDegrees={coord[1]}
/>
);
})}
</section>
</div>
</main>
</div>
);
}
But I can't solve this issue as, from what I understand, I have created separated components with the info passed as props.
import styled from 'styled-components'
export default function Clock ({ minuteDegrees, hourDegrees, index}) {
const finalHourDegrees = Number(hourDegrees + 360)
const finalMinuteDegrees = Number(minuteDegrees + 360)
const StyledClock = styled.div`
width: 10vh;
`
//hour
const animationHour = keyframes`
from {
transform: rotate(${props => props.hourDegrees}deg);
}
to {
transform: rotate(${finalHourDegrees}deg);
}
`
const HourStyled = styled.div`
animation: ${animationHour} 4s ease-out infinite;
`
//minutes
const animationMinute = keyframes`
from {
transform: rotate(${props => props.minuteDegrees}deg);
}
to {
transform: rotate(${finalMinuteDegrees}deg);
}
`
const MinuteStyled = styled.div`
animation: ${animationMinute} 4s ease-out infinite;
`
return(
<StyledClock className={index}>
<HourStyled className={hourDegrees} key={index} hourDegrees={hourDegrees}/>
<MinuteStyled className={minuteDegrees} key={index} minuteDegrees={minuteDegrees}/>
</StyledClock>
)
}
Thanks a lot beforehand!
You can create StyledClock or MinuteStyled styled-components outside the target component. Also, you can send props to the styled components if needed.
UPDATED I forked your code below and updated by using a callback function for the keyframes to pass the dynamic degrees codesandbox
const StyledComponent = styled.div`
background: ${props => props.background};
`;
Clock
const StyledClock = styled.div`
width: 6vw;
`;
export default function Clock({ minuteDegrees, hourDegrees, index }) {
return (
<StyledClock className={index}>
<Hours index={index} hourDegrees={hourDegrees} />
<Minutes index={index} minuteDegrees={minuteDegrees} />
</StyledClock>
);
}
Minutes
const animationMinute = keyframes`
from {
transform: rotate(${(props) => props.minuteDegrees}deg);
}
to {
transform: rotate(${(props) => props.finalMinuteDegrees}deg);
}
`;
const MinuteStyled = styled.div`
animation: ${animationMinute} 4s ease-out infinite;
`;
export default function Minutes({ minuteDegrees, index }) {
const finalMinuteDegrees = Number(minuteDegrees + 360);
return (
<MinuteStyled
className={minuteDegrees}
key={index}
minuteDegrees={minuteDegrees}
finalMinuteDegrees={finalMinuteDegrees}
/>
);
}

React GridList w/ Modal Images

Well, how to start, hi!
I'm creating some Slider with images using Material-UI GridList, and I want those images to be opened in a modal way, just to see them clearly.
I will put the code, and then explain.
import React, {useState} from "react";
import { makeStyles } from "#material-ui/core/styles";
import Modal from "#material-ui/core/Modal";
import tileData from './../../utils/tileData'
import useStylesForSlider from './../../hooks/useStylesForSlider'
import GridList from '#material-ui/core/GridList'
function getModalStyle() {
const top = 50
const left = 50
return {
top: `${top}%`,
left: `${left}%`,
transform: `translate(-${top}%, -${left}%)`
};
}
const useStyles = makeStyles((theme) => ({
paper: {
position: "absolute",
width: 400,
backgroundColor: theme.palette.background.paper,
border: "2px solid #000",
boxShadow: theme.shadows[5],
padding: theme.spacing(2, 4, 3)
}
}));
export default function SimpleModal() {
// I have this in another folder, i will put the other ones too, i'm just starting
const classesRoot = useStylesForSlider()
const classes = useStyles();
// getModalStyle is not a pure function, we roll the style only on the first render
const [modalStyle] = useState(getModalStyle);
const [open, setOpen] = useState(false);
const handleOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
const body = (
<div style={modalStyle} className={classes.paper}>
</div>
);
return (
<div className={classesRoot.root}>
<GridList className={classesRoot.gridList} cols={2.5}>
{tileData.map((tile) => (<img src={tile.img} alt={tile.img} onClick={handleOpen} key={tile.img}/>))}
</GridList>
<Modal
open={open}
onClose={handleClose}>
{body}
</Modal>
</div>
);
}
I have in "tileData" an array with the images, and I map them into a tag just to put them in the slider (GridList). It works well. Now, i want to click some img, and then open it in modal window. I click it, and the modal opens, but now comes my question, how do I put the image I clicked somewhere in the "body" constant, or how do I do to do it well. I don't know if i'm explaining well, but I expect to have some good advices, i'm pretty new in React world
You could create a state for the current chosen image index (or a unique id) of your tileData array and then load the image in the body by its index (or id). Here is an example:
import React, { useState } from "react";
import { makeStyles } from "#material-ui/core/styles";
import Modal from "#material-ui/core/Modal";
import tileData from './../../utils/tileData'
import GridList from "#material-ui/core/GridList";
function getModalStyle() {
const top = 50;
const left = 50;
return {
top: `${top}%`,
left: `${left}%`,
transform: `translate(-${top}%, -${left}%)`
};
}
const useStyles = makeStyles((theme) => ({
paper: {
position: "absolute",
width: 400,
backgroundColor: theme.palette.background.paper,
border: "2px solid #000",
boxShadow: theme.shadows[5],
padding: theme.spacing(2, 4, 3)
}
}));
export default function SimpleModal() {
// I have this in another folder, i will put the other ones too, i'm just starting
//const classesRoot = useStylesForSlider();
const classes = useStyles();
// getModalStyle is not a pure function, we roll the style only on the first render
const [modalStyle] = useState(getModalStyle);
const [open, setOpen] = useState(false);
const [currentIdx, setCurrentIdx] = useState(null); // add a state for the current index
const handleOpen = (idx) => {
setCurrentIdx(idx); // set new current index
setOpen(true);
};
const handleClose = () => {
setCurrentIdx(null); // reset current index
setOpen(false);
};
const body = (
<div style={modalStyle} className={classes.paper}>
{tileData[currentIdx] && (
<img src={tileData[currentIdx].img} alt={tileData[currentIdx].img} />
)}
</div>
);
return (
<div>
<GridList cols={2.5}>
{tileData.map((tile, idx) => (
<img
src={tile.img}
alt={tile.img}
onClick={() => handleOpen(idx)}
key={tile.img}
/>
))}
</GridList>
<Modal open={open} onClose={handleClose}>
{body}
</Modal>
</div>
);
}
Live Demo

React Hooks Drawer Menu not Showing CSS Transition

My Menu Drawer is working except for the css transitions. I think whta is happening is, when I change the value of menuOpen (which is a useState), the DOM rerenders and the transition never happens. How do I stop this? I think I need to use the useRef I have already, but not sure how?
My Page Component with a white div that will be the drawer:
import React, { useState, useEffect, useRef } from 'react';
import { Typography } from '#material-ui/core';
import './page.css';
function Page({ someProps }) {
const [ menuOpen, setMenuOpen ] = useState(false);
const menuRef = useRef();
const handleMenuClick = () => {
setMenuOpen(!menuOpen);
console.log('MENU CLICKED!!!!!!!!!!!!!!!!!!!!', menuOpen);
};
const handleClickOutside = (event) => {
console.log('CLICKED!!!!!!!!!!!!!!!!!!!!', event, menuRef.current);
if (menuRef.current && !menuRef.current.contains(event.target) && menuOpen === true) {
setMenuOpen(false);
}
};
useEffect(
() => {
document.addEventListener('click', handleClickOutside, false);
return () => {
document.removeEventListener('click', handleClickOutside, false);
};
},
[ menuOpen ]
);
return (
<Typography className="screen">
<div className="menuButton" onClick={handleMenuClick}>
MENU
</div>
{menuOpen && <div ref={menuRef} className={`menuContainer ${menuOpen === true ? 'isOpen' : ''}`} />}
</Typography>
);
}
export default Page;
My page.css:
.menuContainer {
position: fixed;
top: 0;
left: 0;
width: 250px;
height: 100vh;
background-color: white;
z-index: 1;
transition: margin 1s ease-in;
margin: 0 0 0 -250px;
}
.menuContainer.isOpen {
margin: 0 0 0 0px;
transition: margin 2s;
}

How to trigger animation using react-transition-group

The problem is I have a form with three states: error, info, and success. Depending on there a response from server am firing toaster using above states I need to add a fade in-out animation when a response from the server is available.
toasterService.js
import React, {useState} from 'react';
import {Transition} from 'react-transition-group';
import './toasterService.css'
export default function ToasterService(content, timeout, style) {
const inProp = useState(true); // always call hook on top level
const duration = timeout;
const transitionStyles = {
entering: {opacity: 1},
entered: {opacity: 1},
exiting: {opacity: 0},
exited: {opacity: 0},
};
let defaultStyle = {};
switch (style) {
case 'info' :
defaultStyle = {
transition: `opacity ${duration}ms ease-in-out`,
opacity: 0,
backgroundColor: '#00c5dc',
color: '#ffffff'
};
break;
case 'success' :
defaultStyle = {
transition: `opacity ${duration}ms ease-in-out`,
opacity: 0,
backgroundColor: '#8ebe4b',
color: '#ffffff'
};
break;
case 'danger' :
defaultStyle = {
transition: `opacity ${duration}ms ease-in-out`,
opacity: 0,
backgroundColor: '#FF0000',
color: '#ffffff'
};
break;
default :
}
return (<div className="main-alert">
<Transition in={inProp} timeout={duration}>
{state => (
<div style={{
...defaultStyle,
...transitionStyles[state]
}}>
{content}
</div>
)}
</Transition>
</div>
);
}
Login.js
import ToastService from '../../services/core/toasterService';
// on click of login btn
socialSignIn = () => {
let obj = {};
obj = this.state;
fetch(url,
{
method: 'post',
body: JSON.stringify(obj)
}).then(function (res) {
console.log(res.json());
ToastService('success', 5000,'info');
return res.json();
})
};
Toast Service receiving 3 arguments but the toaster is not appearing. What I am missing?
I recently built a Toastr component myself, with basic functionality using styled-components, animejs and react-transition-group that might help you to get it right.
Note: I think it's easier to use animejs rather than setting styles for each phase of the transition. You basically get the reference for the entering or exiting element and animate it how you like using animejs.
react-transition-group will give you a reference to the element from these props:
<Transition
key={item.id}
onEntering={animateEnter} // animateEnter will have a reference to the element
onExiting={animateExit} // animateExist will have a reference to the element
timeout={{
enter: 500,
exit: 500
}}
unmountOnExit={true} // I was testing, but I don't think this prop is necessary in my component
>
See working example on CodeSandbox
This is the code:
index.js
import React from "react";
import ReactDOM from "react-dom";
import Toastr from "./Toastr";
import SomeComponent from "./SomeComponent";
import "./styles.css";
function App() {
return (
<Toastr>
<SomeComponent />
</Toastr>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Toastr.js
import React, { useRef, useEffect, useState } from "react";
import styled from "styled-components";
import { TransitionGroup, Transition } from "react-transition-group";
import anime from "animejs";
import ToastrContext from "./ToastrContext";
// CREATE A USE TOASTER HOOK ?
// MAYBE CREATE AN APP STATE TO STORE THE TOASTS
const S = {};
S.FixedContainer = styled.div`
position: fixed;
bottom: 10px;
/* right: 5px; */
/* left: 0; right: 0; */
/* CENTER IT HORIZONTALLY */
left: 50%;
transform: translateX(-50%);
`;
S.ToastContainer = styled.div`
width: 300px;
height: 64px;
margin-top: 10px;
margin-bottom: 10px;
/* padding-left: 10px; */
color: white;
font-weight: bold;
background: #39c16c;
border-radius: 5px;
display: flex;
align-items: center;
justify-content: center;
`;
function Toastr(props) {
const lastToastLengthRef = useRef(0);
const [toasts, setToasts] = useState([]);
const toastID = useRef(0);
console.log("Toastr rendering...");
console.log(toasts);
function addNewToast(toast) {
setToasts(prevState => {
const aux = Array.from(prevState);
aux.push({ msg: toast, id: toastID.current });
toastID.current = toastID.current + 1;
return aux;
});
}
useEffect(() => {
if (toasts.length > lastToastLengthRef.current) {
console.log("useEffect: Toast was added...");
// TOAST WAS ADDED
setTimeout(() => {
setToasts(prevState => {
const aux = Array.from(prevState);
aux.shift();
return aux;
});
}, 1000);
lastToastLengthRef.current = toasts.length;
return;
}
lastToastLengthRef.current = toasts.length;
}, [toasts]);
function animateEnter(element) {
anime({
targets: element,
opacity: 0,
duration: 0
});
anime({
targets: element,
opacity: 1,
easing: "easeOutExpo",
duration: 2000
});
}
function animateExit(element) {
anime({
targets: element,
opacity: 0,
easing: "easeOutExpo",
duration: 2000
});
}
// const toastItems = toasts.map((item,index) =>
// <S.ToastContainer key={item.id}>{item.msg}</S.ToastContainer>
// );
const toastItems = toasts.map((item, index) => (
<Transition
key={item.id}
onEntering={animateEnter}
onExiting={animateExit}
timeout={{
enter: 500,
exit: 500
}}
unmountOnExit={true}
>
<S.ToastContainer>{item.msg}</S.ToastContainer>
</Transition>
));
return (
<React.Fragment>
<S.FixedContainer>
<TransitionGroup component={null}>{toastItems}</TransitionGroup>
{/* {toastItems} */}
</S.FixedContainer>
<ToastrContext.Provider value={addNewToast}>
{props.children}
</ToastrContext.Provider>
</React.Fragment>
);
}
// Toastr.whyDidYouRender = true;
export default Toastr;
ToastrContext.js
import React from "react";
const ToastrContext = React.createContext(null);
export default ToastrContext;
SomeComponent.js (will emit toasts)
import React, { useContext } from "react";
import ToastrContext from "./ToastrContext";
import styled from "styled-components";
function SomeComponent() {
const sendToast = useContext(ToastrContext);
return (
<React.Fragment>
<div>Hey! Click for some toasts!</div>
<button onClick={() => sendToast("This is your toast!")}>Click</button>
</React.Fragment>
);
}
export default SomeComponent;

Resources