I have a parent layout-grid that contains multiple cards, the code for the parent & child are all in a single tsx
file.
I am trying to implement an action to close the selected card in two ways:
The issue is with the second method. I am passing the handler closeCard
to the child component but the the closing part is not working. I tried to console log if the handler is being executed and it does actually being called but the state change is not being reflected.
Help! I'm currently stuck at this roadblock and the coffee is no longer helping.
LayoutGrid.tsx
export const LayoutGrid = ({
className,
cards
}: {
className?: string,
cards: LayoutCardProps[]
}) => {
const [selected, setSelected] = useState<LayoutCardProps | null>(null);
const [lastSelected, setLastSelected] = useState<LayoutCardProps | null>(null);
const [blockScroll, allowScroll] = useScrollBlock();
const handleClick = (card: LayoutCardProps) => {
setLastSelected(selected);
setSelected(card);
};
const closeCard = () => {
setLastSelected(selected);
setSelected(null);
console.log("yes?")
};
useEffect(() => {
if (selected) {
handleSmoothScroll('layout-grid');
blockScroll();
} else {
allowScroll();
}
}, [selected])
return (<>
{/* {selected&&
<div className="h-[100vh] w-[100vw] fixed inset-0 bg-slate-400 z-40"/>
} */}
<div id="layout-grid" className={cn('w-full h-full p-2 sm:p-5 md:p-10 grid grid-cols-1 md:grid-cols-3 max-w-7xl mx-auto gap-4 relative', className)}>
{cards.map((card, i) => (
<div key={i} className={cn(card.className, "")}>
<motion.div
// id={selected?.id === card?.id ? 'selected-card' : `card-${i}`}
onClick={() => handleClick(card)}
className={cn(
card.className,
"relative scroll-offset-top-biography",
selected?.id === card.id
? "rounded-lg fixed inset-0 top-10 md:top-20 h-[75vh] md:h-[85vh] w-[95%] sm:w-[85%] md:w-[80%] m-auto z-50 flex justify-center items-center flex-wrap flex-col"
: `bg-white rounded-xl h-full w-full border ${lastSelected?.id === card.id && 'z-40'}`
)}
layoutId={`card-${card.id}`}
>
{card.thumbnail && <ImageComponent card={card} />}
{selected?.id === card.id ?
<SelectedCard selected={selected} closeCard={closeCard}/>
:
<ContentCard card={card} />
}
</motion.div>
</div>
))}
<motion.div
onClick={closeCard}
className={cn(
"absolute h-full w-full left-0 top-0 bg-black opacity-0 z-40",
selected ? "pointer-events-auto" : "pointer-events-none"
)}
animate={{ opacity: selected ? 0.3 : 0 }}
/>
</div>
</>);
};
const SelectedCard = ({
selected,
closeCard,
}: {
selected: LayoutCardProps | null,
closeCard: ()=>void;
}) => {
return (<>
<div
className="relative h-full w-full flex flex-col justify-start rounded-lg shadow-2xl z-[70]"
>
<motion.div
initial={{
opacity: 0,
}}
animate={{
opacity: 0.6,
}}
className="absolute inset-0 h-full w-full bg-black opacity-60 rounded-xl"
/>
<motion.div
layoutId={`content-${selected?.id}`}
initial={{
opacity: 0,
y: 100,
}}
animate={{
opacity: 1,
y: 0,
}}
exit={{
opacity: 0,
y: 100,
}}
transition={{
duration: 0.3,
ease: "easeInOut",
}}
className="relative h-full w-full p-2"
>
{selected?.contentFull}
</motion.div>
<button
onClick={closeCard}
className="absolute top-0 right-0 p-2 mr-4 mt-3 bg-slate-700 hover:bg-orange-400 rounded-full"
>
<MdOutlineCloseFullscreen size={20} className="text-slate-50 hover:text-slate-900"/>
</button>
</div>
</>);
};
What's happening here is that closeCard
is being called, but then that event propagates up to the container that has onClick={() => handleClick(card)}
and the card is effectively being re-opened.
Part of browser event handling is a something called propagation which can sometimes be surprising if you haven't seen it before. Essentially then you click on an element, that element's click handler is invoked, then the event propagates up to the element's parent. If the parent has a matching event handler, it will be invoked. And so on until reaching the root of the document. This is useful for situations where you want a click anywhere inside an element to be caught and handled in a certain way, and indeed your expanded content is inside the original click target. The way to handle a this is to call .stopPropagation()
on the event object that event handlers take as an argument, indicating you don't want the event to bubble up to its parent/ancestors.
Consider changing closeCard
like so:
const closeCard = (e) => {
e.stopPropagation();
setLastSelected(selected);
setSelected(null);
console.log("yes?")
};
If you have multiple interactable elements within your card component, you may want to have a blanket click handler to stop all propagation of click events out of it:
const SelectedCard = ({
selected,
closeCard,
}: {
selected: LayoutCardProps | null,
closeCard: ()=>void;
}) => {
return (<>
<div
onClick={e => e.stopPropagation()}
className="relative h-full w-full flex flex-col justify-start rounded-lg shadow-2xl z-[70]"
>
and this would remove the need to have each element inside the card doing it themselves