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>
);
}
What I Need
I need a way to test if an accordion isCollapsed or not. I tried seeing if there was a way to grab if the maxHeight hasChanged, but from what I have read, it doesn't include measurements within the dom objects for the tests
The Problem
Writing the following test:
import React from 'react';
import { render, screen } from '#testing-library/react';
import userEvent from '#testing-library/user-event';
import Accordion from './Accordion';
import AccordionItem from './AccordionItem';
const accordionTitle = 'Title: Hello World';
const accordionContent = 'Content: Hello World';
function TestContainer() {
return (
<Accordion>
<AccordionItem title={accordionTitle}>
<p>{accordionContent}</p>
</AccordionItem>
</Accordion>
);
}
describe('Accordion', () => {
const userViewing = userEvent.setup();
it.only('expands content on item control click', async () => {
render(<TestContainer />);
expect(await screen.findByText(accordionContent)).not.toBeInTheDocument();
const accordionItem = screen.getByRole('button', { name: accordionTitle });
userViewing.click(accordionItem);
expect(await screen.findByText(accordionContent)).toBeInTheDocument();
});
});
Results in the following Error:
The Reason
I think this is because the content component still exists in the DOM, but is only being hidden by the overflow: hidden; and maxHeight: 0 or heightOfContent(for animation purposes).
The Component Code
Accordion.tsx:
import React, { ReactNode } from 'react';
function Accordion({ children }: PROPS): JSX.Element {
return <div>{children}</div>;
}
interface PROPS {
children: ReactNode;
}
export default Accordion;
AccordionItem.tsx
import React, { ReactNode, useState } from 'react';
import AccordionControlClick from './AccordionControlClick';
import AccordionContent from './AccordionContent';
import { useStyles } from './Styles';
function AccordionItem({ title, children }: PROPS): JSX.Element {
const classes = useStyles();
const [isCollapsed, setIsCollapsed] = useState(true);
return (
<div className={isCollapsed ? classes.accordionItemClosed : classes.accordionItemOpen}>
<AccordionControlClick
title={title}
isCollapsed={isCollapsed}
toggleIsCollapsed={setIsCollapsed}
/>
{children !== null && (
<AccordionContent isCollapsed={isCollapsed}>{children}</AccordionContent>
)}
</div>
);
}
interface PROPS {
title: string;
children?: ReactNode;
}
export default AccordionItem;
AccordionControlClick.tsx
import React from 'react';
import { useStyles } from './Styles';
function AccordionControlClick({ title, isCollapsed, toggleIsCollapsed }: PROPS): JSX.Element {
const classes = useStyles();
return (
<button
className={classes.accordionControlClick}
type="button"
onClick={() => toggleIsCollapsed(!isCollapsed)}
>
<h2>{title}</h2>
<span className={isCollapsed ? classes.iconChevronWrapper : classes.iconChevronWrapperRotate}>
<i class="fa-solid fa-chevron-down" />
</span>
</button>
);
}
interface PROPS {
title: string;
isCollapsed: boolean;
toggleIsCollapsed: (isOpen: boolean) => void;
}
export default AccordionControlClick;
AccordionContent.tsx
import React, { ReactNode, useRef, useLayoutEffect } from 'react';
import { useStyles } from './Styles';
function AccordionContent({ isCollapsed, children }: PROPS): JSX.Element {
// variables
const componentDomRef = useRef<any>(null);
const componentHeight = useRef(0);
const classes = useStyles();
// setup
useLayoutEffect(() => {
componentHeight.current = componentDomRef.current ? componentDomRef.current.scrollHeight : 0;
}, []);
// render
return (
<div
ref={componentDomRef}
className={classes.accordionContent}
style={isCollapsed ? { maxHeight: '0px' } : { maxHeight: `${componentHeight.current}px` }}
>
{children}
</div>
);
}
interface PROPS {
isCollapsed: boolean;
children: ReactNode;
}
export default AccordionContent;
Styles.ts
import { createUseStyles } from 'react-jss';
import { cssColors, cssSpacing } from '../../utils';
const accordionBoxShadow = '2px 3px 8px 1px rgba(0, 0, 0, 0.2)';
const accordionBoxShadowTransition = 'box-shadow 0.3s ease-in-out 0s;';
export const useStyles = createUseStyles({
accordionContent: {
boxSizing: 'border-box',
width: '100%',
padding: `0 ${cssSpacing.m}`,
overflow: 'hidden',
transition: 'max-height 0.3s ease-in-out'
},
accordionControlClick: {
boxSizing: 'border-box',
display: 'flex',
width: '100%',
alignItems: 'center',
padding: `9px ${cssSpacing.m}`,
border: 'none',
borderRadius: '8px',
outline: 'none',
backgroundColor: `${cssColors.backgroundLevel2}`,
cursor: 'pointer'
},
accordionItemClosed: {
boxSizing: 'border-box',
width: '100%',
marginBottom: cssSpacing.l,
border: `2px solid ${cssColors.accordionTitleBorder}`,
borderRadius: '8px',
boxShadow: 'none',
transition: 'none',
'&:hover': {
border: `2px solid ${cssColors.accordionTitleBorder}`,
boxShadow: accordionBoxShadow,
transition: accordionBoxShadowTransition
}
},
accordionItemOpen: {
boxSizing: 'border-box',
width: '100%',
marginBottom: cssSpacing.l,
border: `2px solid ${cssColors.accordionTitleBorder}`,
borderRadius: '8px',
boxShadow: accordionBoxShadow,
transition: accordionBoxShadowTransition,
'&:hover': {
border: `2px solid ${cssColors.accordionTitleBorder}`,
boxShadow: accordionBoxShadow,
transition: accordionBoxShadowTransition
}
},
iconChevronWrapper: {
marginLeft: 'auto',
transform: 'none',
transition: 'transform 300ms ease'
},
iconChevronWrapperRotate: {
marginLeft: 'auto',
transform: 'rotate(180deg)',
transition: 'transform 300ms ease'
}
});
In the AccordionContent.tsx file, I added in the following visibility
style={ isCollapsed ? { maxHeight: '0px', visibility: 'hidden' } : { maxHeight: `${componentHeightRef.current}px`, visibility: 'visible' } }
In the test file I make the following changes to test if the content is ToBeVisible vs toBeInTheDocument:
it.only('expands content on item control click', async () => {
render(<TestContainerOneItem />);
const accordionContentContainer = (await screen.findByText(accordionContent)).parentElement;
await waitFor(() => expect(accordionContentContainer).not.toBeVisible());
const accordionControlButton = screen.getByRole('button', { name: accordionTitle });
await userViewing.click(accordionControlButton);
await waitFor(() => expect(accordionContentContainer).toBeVisible());
});
I'm trying to change the button's color when clicked using useState hook and styled component.
When I click the button, the selected property changes but it won't apply the color.
Buttons:
{toggleFilter.map((item) => {
return (
<FilterButton
key={`${item.name}FilterButton`}
$selected={item.selected}
onClick={() => handleToggleFilter(item)}
>
{item.name}
</FilterButton>
);
})}
Styled component:
export const FilterButton = styled.button<{ $selected: boolean }>`
background-color: ${({ $selected }) => ($selected ? "#a144d5" : "#848485")};
transition: 0.3s ease-in-out;
color: white;
padding: 0.25rem 0.5rem;
margin: 0.5rem 0.1rem;
border: none;
`;
Handler:
const handleToggleFilter = (item: ToggleFilterItem) => {
toggleFilter.forEach((i) => {
if (i.name === item.name) {
i.selected = !i.selected;
}
});
setToggleFilter(toggleFilter);
};
You are mutating state, it should be treated as immutable, try:
const handleToggleFilter = (item: ToggleFilterItem) => {
setToggleFilter(
toggleFilter.map((i) =>
i.name === item.name ? { ...i, selected: !i.selected } : i
)
);
};
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;
}
I pass a property to my styled component. Basically I pass his height that starts with 400px.
And when I click a button it will go to 30px, but the way I did my div starts with 400px and when I click the button it goes to 30px and then doesn't expand the height size anymore:
export default function Menu() {
const [open, setOpen] = useState(true); // declare new state variable "open" with setter
const handleClick = e => {
e.preventDefault();
setOpen(false);
};
return (
<DivMenuButton height={open ? '400px' : '30px'}>
<button
style={{ margin:0, padding: 0, height: "30px", width: "100%", borderRadius:'0px' }}
onClick={handleClick}
>
Button
</button>
</DivMenuButton>
);
}
My styled component:
import React from 'react';
import styled from 'styled-components';
export const DivMenuButton = styled.div`
border: 0px;
background-color: #000; // was wrong syntax
width: 200px;
height: ${props => props.height}
`;
code:
https://codesandbox.io/s/beautiful-nightingale-fsm58
Your code is actually correct (almost).
The props and styled components parts are correct.
The thing you did wrong is that you always set open to false when clicking the button, instead of setting it to the opposite of what it was before.
So, instead of doing this:
const handleClick = e => {
e.preventDefault();
setOpen(false);
};
you should do this:
const handleClick = e => {
e.preventDefault();
setOpen(!open);
};