I have created a collapse component, but the transition effect is not working when i open and close the collapse component.
index.js
const Collapse = ({ title, text, child, ...props }) => {
const [isOpen, setIsOpen] = useState(false);
const toggleCollapse = () => setIsOpen((isOpen) => !isOpen);
const closeCollapse = () => setIsOpen(false);
const content = useRef(null);
const isParentOpen = props.isParentOpen;
useEffect(() => {
if (!isParentOpen) closeCollapse();
}, [isParentOpen]);
const height = !isOpen ? "0px" : `auto`; // ${content.current?.scrollHeight}px
return (
<CollapseContainer>
<CollapseButton isOpen={isOpen} onClick={toggleCollapse}>
<CollapseTitleWrapper>{title}</CollapseTitleWrapper>
</CollapseButton>
<CollapseContent ref={content} max_height={height}>
<CollapseText>
{text}
{child?.map((datumn, index) => (
<Collapse
{...datumn}
key={`collapse-child-${index}`}
isParentOpen={isOpen}
/>
))}
</CollapseText>
</CollapseContent>
</CollapseContainer>
);
};
I think the height calculation should happen when the nested child also gets opened and closed.
Working codesandbox
Related
I have an Articles component that renders article boxes. When a box is clicked on the page animates out and the article animates in.
It saves the scrollTop position in state so that when the article is closed it scrolls back to where the container scroll position from before.
I'm having trouble writing a test for it though as from my understanding you can't access state values directly in functional components, and spying wouldn't give me the the values right?
I've been searching and reading various posts and articles about it but I can only seem to find info on mocking the value which i don't need or the scrollTo method which is not what i'm using.
Essentially the code I have is something along these lines:
const Articles = ({articlesJsx}) => {
const pageContRef = React.useRef(null);
const currentArticle = useRef(articlesJsx[0]);
const scrollPos = useRef(0);
const [articleVisible, setArticleVisible] = useState(false);
const setScrollPos = () => {
setTimeout(() => {
pageContRef.current.scrollTop = scrollPos.current;
}, 500);
};
const scrollToTop = () => {
setTimeout(() => {
pageContRef.current.scrollTop = 0;
}, 500);
};
const handleShowArticle = (index) => {
currentArticle.current = articlesJsx[index];
scrollPos.current = pageContRef.current.scrollTop;
setArticleVisible(true);
setTimeout(() => {
scrollToTop();
}, 500);
};
const closeArticle = () => {
setArticleVisible(false);
setTimeout(() => {
setScrollPos();
}, 500);
};
return (
<>
<div
className={pageCont}
data-test="articlesContainer"
ref={pageContRef}
>
<div className={styles[animDir]}>
<CSSTransition
in={!articleVisible}
>
<Hero />
{articlesJsx.map((article, index) => (
<ArticleBox handleShowArticle={() => handleShowArticle(index)} />
))}
</CSSTransition>
<CSSTransition
in={articleVisible}
unmountOnExit
>
<Article
content={currentArticle.current}
>
<BackArrow handleCloseArticle={closeArticle} />
<Article />
</CSSTransition>
</div>
</div>
</>
);
};
My test so far:
describe("Articles tests", () => {
const articles = articlesData;
let component;
beforeEach(() => {
jest.useFakeTimers();
const useRefSpy = jest.spyOn(React, "useRef");
component = mount(
<Articles articlesJsx={articles} />
});
it("should scroll to the top of the article when viewed", () => {
const articleBox = findByTestAttr(component, "articleBoxMain");
let articlesCont = findByTestAttr(component, "articlesContainer");
articlesCont.simulate("scroll", {target: {scrollTop: 100}});
articlesCont = findByTestAttr(component, "articlesContainer");
act(() => {
articleBox.first().simulate("click");
});
act(() => jest.runAllTimers());
// const scrollTopPosition = articlesCont.......?
expect(scrollTopPosition).toEqual(0);
});
});
Enzyme docs say you can find a component through one of its properties but console.log(component.find({scrollTop: 100}).length); comes up as 0.
As a last resort I tried expect(useRefSpy).toHaveBeenCalledWith(0) but the received value comes up as undefined.
What is the correct way to test for this?
I need to add draggable divs, one for every image in an array of images, to a container.
The below works BUT only on the second drag. i.e. I have to drag the component twice for it to move. It obviously has something to do with the hooks used and how the divs are being created. Any help will be appreciated.
Code sandbox
const App = ({images}) => {
const [selectedImages, setSelectedImages] = useState(images)
const [dragId, setDragId] = useState()
const handleDrag = (ev) => {
setDragId(ev.currentTarget.id)
}
const handleDrop = (ev) => {
const sortedImages = sortImages(selectedImages, dragId)
setSelectedImages(sortedImages)
}
return (
<Container
images={selectedImages}
handleDrag={handleDrag}
handleDrop={handleDrop}
/>
)
}
export default App
const Container = ({ images, handleDrag, handleDrop }) => {
const ref = useRef(null)
useEffect(() => {
if (containerRef.current) {
for (let i = 0; i < images.length; ++i) {
const draggable = document.createElement('div')
draggable.ondragstart = handleDrag
draggable.ondrop = handleDrop
const style = 'position: absolute; ...'
draggable.setAttribute('style', style)
draggable.setAttribute('draggable', true)
draggable.setAttribute('id', images[i].id)
ref.current.appendChild(draggable)
}
}
}, [images, ref, handleDrag, handleDrop])
return (
<div className={'relative'}>
<div ref={ref} />
</div>
)
}
export default Container
It looks like it is setting the dragId on first drag and then only on the second drag it actually drags and drops the div. How can I set this up so that it drags and drops on the first try?
I should have just used React to add the functions
const Container = ({ images, handleDrag, handleDrop }) => {
const containerRef = useRef(null)
return (
<div ref={containerRef}>
{images.map((image) => (
<div
key={image.id}
id={image.id}
draggable
onDragOver={(ev) => ev.preventDefault()}
onDrop={handleDrop}
onDragStart={handleDrag}
style={{
position: 'absolute'...`,
}}
/>
))}
</div>
)
}
export default Container
Sandbox
const Parent = ({list}) => {
const closeAll = () => {
// What should be in here?
}
return (
<>
<button onClick={() => closeAll()}>Close All</button>
{list.map(item => <Accordion item={item}/>)}
</>
)
}
const Accordion = ({item}) => {
const [open, setOpen] = useState(false);
return (
<div onClick={() => setOpen(o => !o)}>
<p>item.name</p>
{open && <p>item.detail</p>
</div>
)
}
Basically, as above, there is the Accordion components and a parent component that hosts all of them. Each Accordion component has a state called open. I want to change state of each child from parent component. How can I send an order to a child component to change its state?
Lift your state up into Parent.
closeAll can just map over the list and set all the open properties to false.
Have a handleClick callback that you pass down to Accordion which sets the state of the clicked item's open property to the inverse in Parent
Take a look at the react docs for lifting state up.
import { useState } from "react";
const data = [
{
detail: "foo",
name: "bar"
},
{
detail: "foo1",
name: "bar1"
}
];
const Parent = ({ defaultList = data }) => {
const [list, setList] = useState(
defaultList.map((i) => ({
...i,
open: false
}))
);
const closeAll = () => {
setList(
list.map((i) => ({
...i,
open: false
}))
);
};
const handleClick = (i) => {
const newList = [...list];
newList[i].open = !list[i].open;
setList(newList);
};
return (
<>
<button onClick={() => closeAll()}>Close All</button>
{list.map((item, i) => (
<Accordion item={item} handleClick={() => handleClick(i)} />
))}
</>
);
};
const Accordion = ({ item, handleClick }) => {
return (
<div>
<button onClick={handleClick}>{item.name}</button>
{item.open && <p>{item.detail}</p>}
</div>
);
};
export default Parent;
If you are unable to lift your state there is an alternative approach using react refs.
Create ref (initially an empty array) that each Accordion will push its own close state setting function into when it first renders.
In Parent, loop over the the array of close state settings functions inside the ref and execute each.
const Parent = ({ list = data }) => {
const myRef = useRef([]);
const closeAll = () => {
myRef.current.forEach((c) => c());
};
return (
<>
<button onClick={() => closeAll()}>Close All</button>
{list.map((item, i) => (
<Accordion item={item} myRef={myRef} />
))}
</>
);
};
const Accordion = ({ item, myRef }) => {
const [open, setOpen] = useState(false);
useEffect(() => {
myRef.current.push(() => setOpen(false));
}, [myRef]);
return (
<div>
<button onClick={() => setOpen((o) => !o)}>{item.name}</button>
{open && <p>{item.detail}</p>}
</div>
);
};
export default Parent;
Using an internal state for the component is not recommended, at least from my point of view for what you are doing.
you can control the open state of each list item from its properties like the example here:
const Parent = ({ list }) => {
const [isAllClosed, setIsAllClosed] = useState(false);
const closeAll = () => {
setIsAllClosed(true)
};
return (
<>
<button onClick={closeAll}>Close All</button>
{list.map((item) => (
item.open = isAllClosed != null ? (!isAllClosed) : true;
<Accordion item={item} />
))}
</>
);
};
const Accordion = ({ item }) => {
return (
<div onClick={() => console.log('item clicked')}>
<p>item.name</p>
{item.open ? <p>item.detail</p> : null}
</div>
);
};
I also replaced you short circuit evaluation open && <p>item.detail</p> to a ternary. The reason for that is that you will get a string false being printed if not true, does it make sense?
You will need somehow control the state of the whole list whether an item is open or not from whoever is using the parent.
But avoid using internal state when you can.
I think you can try creating a state variable inside the parent and passing it as a prop to the child to control the behavior of the child.
const Parent = ({ list }) => {
const [masterOpen, setMasterOpen] = useState(true);
<>
<button onClick={() => setMasterOpen(false)}>Close All</button>
{list.map((item) => (
<Accordion item={item} parentOpen={masterOpen} />
))}
</>
);
};
const Accordion = ({ item, parentOpen }) => {
const [open, setOpen] = useState(false);
if (!parentOpen) {
setOpen(false);
}
return (
<div onClick={() => setOpen((o) => !o)}>
<p>{item.name}</p>
{open && <p>item.detail</p>}
</div>
);
};
I'm testing the use-context-selector lib to prevent unnecessary rendering in react ContextApi. I create this simple application:
export const HomeProvider = (props: PropsWithChildren<{}>) => {
const [x, setX] = React.useState(0);
const [y, setY] = React.useState(0);
return (
<HomeContext.Provider value={{ x, setX, y, setY }}>
{props.children}
</HomeContext.Provider>
);
};
export const HomeModule = () => {
return (
<HomeProvider>
<CounterScreen></CounterScreen>
<br />
<br />
<MessageScreen></MessageScreen>
</HomeProvider>
);
};
The child components are:
export const MessageScreen = () => {
const x = useContextSelector(HomeContext, (context) => context.x);
const setX = useContextSelector(HomeContext, (context) => context.setX);
return (
<>
<h1>Teste X</h1>
{x} <button onClick={() => setX(x + 1)}>X</button>
</>
);
};
export const MessageScreen = () => {
const x = useContextSelector(HomeContext, (context) => context.x);
const setX = useContextSelector(HomeContext, (context) => context.setX);
return (
<>
<h1>Teste Y</h1>
{y} <button onClick={() => setY(y + 1)}>X</button>
</>
);
};
The result, when I click in any one of those button is that indeed the child components doesn't rerender:
https://i.stack.imgur.com/9adCh.png
But, when I use the option: Highlight updates when components render in settings, all components on the screen is highlighted.
So, am i missing something or i'm right about the child components not rerendering ?? I saw other videos on youtube that show that when using this lib, the component that didn't render was not highlighted.
I want to open a modal from a parent component with props, and then close it when everything it's done and notify to the parent in case he wants to open again.
const ModalChild = (props) => {
const [Activate, setActivate] = useState(props.Activate);
const toggle = () => setActivate(!Activate);
useEffect(() => {
setActivate(props.Activate)
}
}, []);
<Modal isOpen={Activate} toggle={false} >
<text>hello {props.hey}<text>
</Modal>
}
and the parents component, something like this:
const Accountlist= () => {
const [Activate, setActivate] = useState(false);
const toggle = (value) => {
setActivate(true)
}
render(
<button onClick={() => toggle(value)} />
<ModalEdit props={Activate}/>
)}
Please, anyone have anyidea?
If it were me, I will do it like this.
const Accountlist= () => {
const [activate, setActivate] = useState(false);
const toggleActivate = (value) => {
setActivate(value);
}
return [
<button onClick={() => toggleActivate(!activate)} />,
<ModalEdit isActivated={activate} toggleActivate={toggleActivate} />
]}
And at the child component.
const ModalChild = ({ isActivated, toggleActivate }) => {
useEffect(() => {
toggleActivate(!isActivated);
}, []);
<Modal isOpen={isActivated} toggle={() => toggleActivate(false)} >
<text>hello {props.hey}<text>
</Modal>
}
I'm not sure what you are trying to do but the rest with the logic I'll leave it to you.