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;
}
Related
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>
);
}
In my react app, using React-Bootstrap, I set the navbar to fixed after srcolling, but after that I'm unable to use the toggler button, that was working before scrolling :
const [sticky, setSticky] = useState('');
React.useEffect(() => {
window.addEventListener('scroll', stickNavbar);
return () => window.removeEventListener('scroll', stickNavbar);
}, []);
const stickNavbar = () => {
if (window !== undefined) {
let windowHeight = window.scrollY;
// window height changed for the demo
windowHeight > 150 ? setSticky('top') : setSticky('');
}
};
return (
<Navbar bg="light" fixed={stickyClass} className="menu">
<Container fluid >
</Container>
</Navbar>
<Collapse in={open} timeout={200}>
<Container fluid className="dropdown-container">
<Row className="dropdown">
</Row>
</Container>
</Collapse>
)
CSS
.menu {
max-width: 100%;
height: 7rem;
border-bottom: 2px solid rgb(228, 228, 228);
}
.dropdown-container {
position: absolute;
}
.dropdown {
background-color: #f8f9fa;
border-bottom: 2px solid rgb(228, 228, 228);
position: relative;
}
Solution: I created for the useState, useEffect and stickNavbar corresponding items:
const [scroll, setScroll] = useState(0);
React.useEffect(() => {
window.addEventListener('scroll', stickNavbar);
return () => window.removeEventListener('scroll', stickNavbar);
}, []);
const scrollPos = () => {
if (window !== undefined) {
let posHeight = window.scrollY;
setScroll(posHeight)
}
};
than I used them to follow the change in the scroll Y coordinates to set the distance of the position + rem in pixels of the height of the navbar:
<Container fluid className="dropdown-container" style=
{{top:`${112+scroll}`+"px"}}>
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.
I'm trying to integrate CSSTransition to my Gatsby site, but it is not applying any of the classes. I'm utilizing CSS modules, and I've got a <div> that serves as the parent that fades in and out, essentially applying the fade effect to this and covering the content while it changes. It's got the class fadEffect. Here is my app-layout component, and the SASS.
AppLayout.tsx
import React, { ReactNode, useState } from 'react';
import { ApiContext } from 'contexts/ApiContext';
import { graphql, StaticQuery } from 'gatsby';
import { TransitionGroup, CSSTransition } from 'react-transition-group';
import { Devtools } from '../devtools/Devtools';
import { Footer } from '../footer/Footer';
import { Header } from '../header/Header';
import s from './AppLayout.scss';
interface AppLayoutProps {
children: ReactNode;
location: string;
}
const isDev = process.env.NODE_ENV === 'development';
// tslint:disable no-default-export
export default ({ children, location }: AppLayoutProps) => {
const [fadeEffectVisible, setFadeEffectVisible] = useState(false);
const handleFadeEffectEntered = () => {
setTimeout(() => {
setFadeEffectVisible(false);
}, 50);
};
return (
<StaticQuery
query={`${NavQuery}`}
render={(data) => (
<>
<ApiContext>
<Header navigationContent={data.prismic.allNavigations.edges[0].node} />
<CSSTransition
in={fadeEffectVisible}
timeout={150}
classNames={{
enter: s.fadeEffectEnter,
enterActive: s.fadeEffectEnterActive,
enterDone: s.fadeEffectEnterDone,
exit: s.fadeEffectExit,
exitActive: s.fadeEffectExitActive,
}}
onEntered={handleFadeEffectEntered}
>
<div className={s.fadeEffect} aria-hidden="true" />
</CSSTransition>
<TransitionGroup component={null}>
<CSSTransition
key={location}
timeout={150}
classNames={{
enter: s.pageEnter,
}}
>
<div className={s.layout}>
{children}
<Footer navigationItems={data.prismic.allNavigations.edges[0].node} />
{isDev && <Devtools />}
</div>
</CSSTransition>
</TransitionGroup>
</ApiContext>
</>
)}
/>
);
};
const NavQuery = graphql`
query NavQuery {
prismic {
allNavigations {
edges {
node {
...NotificationBar
...NavigationItems
...FooterNavigationItems
}
}
}
}
}
`;
AppLayout.scss
#import '~styles/config';
:global {
#import '~styles/base';
}
.layout {
display: block;
min-height: 100vh;
}
.pageEnter {
display: none;
}
.fadeEffect {
display: none;
position: fixed;
z-index: 9;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #fff;
transition: opacity 0.15s linear;
&Enter {
display: block;
opacity: 0;
}
&Active,
&Done,
&Exit {
display: block;
opacity: 1;
}
&ExitActive {
opacity: 0;
}
}
I'm happy to provide more details/code if this isn't enough. I'm newish to React and Gatsby, so I'm still learning the lingo. Thanks in advance.
I don't see part of your code where you are updating fadeEffectVisible to true for first CSSTransition and I don't see in property at all on second CSSTransition and I would bet that is your issue. Please take a look at this example from React Transition Group for understanding usage of properties.
App.js
function App() {
const [inProp, setInProp] = useState(false);
return (
<div>
<CSSTransition in={inProp} timeout={200} classNames="my-node">
<div>
{"I'll receive my-node-* classes"}
</div>
</CSSTransition>
<button type="button" onClick={() => setInProp(true)}>
Click to Enter
</button>
</div>
);
}
Style.css
.my-node-enter {
opacity: 0;
}
.my-node-enter-active {
opacity: 1;
transition: opacity 200ms;
}
.my-node-exit {
opacity: 1;
}
.my-node-exit-active {
opacity: 0;
transition: opacity 200ms;
}
When the in prop is set to true, the child component will first receive the class example-enter, then the example-enter-active will be added in the next tick.
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;