skip to Main Content

I’m using react typescript for the first time, and I implemented outside click event. I’m running into a problem where whenever i click on the active or currently selected component, it seems to trigger the onclick and outside click event.

Attached here i the GIF to better understand my problem.

enter image description here

when the selected component is clicked, it set as active, and when i click it again it seems like it triggers the outside click then trigger the click event.

here is my code implementation.

import { Button } from "@/components/ui/button";
import {
    Menubar,
    MenubarContent,
    MenubarItem,
    MenubarMenu,
    MenubarTrigger,
} from "@/components/ui/menubar";
import { MapPin, MoreVertical, PinOff } from "lucide-react";
import { HtmlHTMLAttributes, useState } from "react";
import OutsideClick from "outsideclick-react";
import { Props } from "@/utilities/navigation_props";

interface DocumentCardProps extends Props, HtmlHTMLAttributes<HTMLDivElement> {
data: DateType,
onSelectActive: (id: number|null) => void,
}

type DateType = {
    id: number
}

const DocumentCard = ({
    data,
    active,
    onSelectActive,
    ...props
}: DocumentCardProps) => {

return (
    <OutsideClick
        onOutsideClick={() => {
            onSelectActive(null)
            console.log(active)
        }}
        ignoreElement={[".menubarEl"]}
        className="pointer-events-none"
        onClick={(e) => {
            e.stopPropagation()

            onSelectActive(data.id)
        }}
    >
        <div
            {...props}
            
            className={`h-56 p-2 flex flex-col rounded-xl cursor-default pointer-events-auto
            ${active == data.id ? "dark:bg-green-400/50 bg-green-400/50" : "hover:bg-tertiary"}`}
        >
            <div className="bg-tertiary grow rounded-lg overflow-hidden pointer-events-none">
                <img src="https://picsum.photos/400" alt="" className="" />
            </div>
            <div className="shrink-0 py-2 text-sm font-medium px-0.5 flex items-center">
                <div className="grow">
                    <div className={`${active == data.id ? "dark:text-emerald-200" : ""}`}>
                        Document title
                    </div>
                    <div
                        className={`opacity-70 font-normal text-xs ${active == data.id ? "dark:text-emerald-300" : ""
                            }`}
                    >
                        Document{">"}path{">"}name
                    </div>
                </div>
                <div>
                    <Menubar className="!border-none !bg-transparent !p-0">
                        <MenubarMenu>
                            <MenubarTrigger className="!px-0 focus:!bg-transparent data-[state=open]:!bg-transparent !p-0 !rounded-full ring-1">
                                <Button
                                    asChild
                                    variant={"ghost"}
                                    size={"iconxs"}
                                    className={`hover:dark:border-white/10 !rounded-full border hover:border-gray-300/60 dark:hover:bg-white/5 border-transparent`}
                                >
                                    <div>
                                        <MoreVertical className="h-4 w-4" />
                                    </div>
                                </Button>
                            </MenubarTrigger>
                            <MenubarContent className="ring-1 ring-inset menubarEl" align="end" alignOffset={20} sideOffset={-15}>
                                <MenubarItem>
                                    <PinOff className="w-5 h-5" strokeWidth={1.7} />
                                    <div className="ml-3">Unpin</div>
                                </MenubarItem>
                                <MenubarItem>
                                    <MapPin className="w-5 h-5" strokeWidth={1.7} />
                                    <div className="ml-3">Track</div>
                 

               </MenubarItem>
                                </MenubarContent>
                            </MenubarMenu>
                        </Menubar>
                    </div>
                </div>
            </div>
        </OutsideClick>
    );
};

function Pinned() {
    const [active, setActive] = useState<number|null>(null)

const documents = [
    { id: 1 },
    { id: 2 },
    { id: 3 },
    { id: 4 },
    { id: 5 },
    { id: 6 },
    { id: 7 },
];

return (
    <div className="grid grid-cols-[repeat(auto-fill,minmax(17rem,1fr))] gap-1">
        {documents.map((item, index) => (
            <DocumentCard
                key={index}
                data={item}
                active={active}
                onSelectActive={(id) => {
                    setActive(id)
                }}
            />
        ))}
    </div>
);
}

export default Pinned;

I’m using outsideclick-react, and shadcn for the popover

2

Answers


  1. Chosen as BEST ANSWER

    After implementing various ways for the problem, I came up with a solution by implementing the outsideclick on the parent component. I noticed that when i click outside the DocumentCard component, other DocumentCard components triggers an outsideclick which causes the problem. This was the revision i made:

    import { Button } from "@/components/ui/button";
    import {
        Menubar,
        MenubarContent,
        MenubarItem,
        MenubarMenu,
        MenubarTrigger,
    } from "@/components/ui/menubar";
    import { MapPin, MoreVertical, PinOff } from "lucide-react";
    import { HtmlHTMLAttributes, useEffect, useState } from "react";
    import OutsideClick from "outsideclick-react";
    import { Props } from "@/utilities/navigation_props";
    
    interface DocumentCardProps extends Props, HtmlHTMLAttributes<HTMLDivElement> {
        data: DateType;
        onSelectActive: (id: number | null) => void;
        onClickMenu: () => void;
    }
    
    type DateType = {
        id: number;
    };
    
    const DocumentCard = ({
        data,
        active,
        onClickMenu,
        onSelectActive,
        ...props
    }: DocumentCardProps) => {
        return (
            <div
                {...props}
                onClick={() => {
                    onSelectActive(data.id);
                }}
                className={`h-56 p-2 flex flex-col rounded-xl cursor-default
                    ${active == data.id
                        ? "dark:bg-green-400/50 bg-green-400/50"
                        : "hover:bg-tertiary"
                    }`}
            >
                <div className="bg-tertiary grow rounded-lg overflow-hidden pointer-events-none">
                    <img src="https://picsum.photos/400" alt="" className="" />
                </div>
                <div className="shrink-0 py-2 text-sm font-medium px-0.5 flex items-center">
                    <div className="grow">
                        <div
                            className={`${active == data.id ? "dark:text-emerald-200" : ""}`}
                        >
                            Document title
                        </div>
                        <div
                            className={`opacity-70 font-normal text-xs ${active == data.id ? "dark:text-emerald-300" : ""
                                }`}
                        >
                            Document{">"}path{">"}name
                        </div>
                    </div>
                    <div id={"menubar-trigger-" + data.id}>
                        <Menubar className="!border-none !bg-transparent !p-0">
                            <MenubarMenu>
                                <MenubarTrigger
                                    className="!px-0 focus:!bg-transparent data-[state=open]:!bg-transparent !p-0 !rounded-full"
                                >
                                    <Button
                                        asChild
                                        variant={"ghost"}
                                        size={"iconxs"}
                                        className={`hover:dark:border-white/10 !rounded-full border hover:border-gray-300/60 dark:hover:bg-white/5 border-transparent`}
                                    >
                                        <div>
                                            <MoreVertical className="h-4 w-4" />
                                        </div>
                                    </Button>
                                </MenubarTrigger>
                                <MenubarContent
                                    className=" menubarContainer"
                                    align="end"
                                    alignOffset={2}
                                    sideOffset={2}
                                >
                                    <MenubarItem className="menubarItem" onClick={() => onClickMenu()}>
                                        <PinOff className="w-5 h-5" strokeWidth={1.7} />
                                        <div className="ml-3">Unpin</div>
                                    </MenubarItem>
                                    <MenubarItem className="menubarItem" onClick={() => onClickMenu()}>
                                        <MapPin className="w-5 h-5" strokeWidth={1.7} />
                                        <div className="ml-3">Track</div>
                                    </MenubarItem>
                                </MenubarContent>
                            </MenubarMenu>
                        </Menubar>
                    </div>
                </div>
            </div>
        );
    };
    
    function Pinned() {
        const [active, setActive] = useState<number | null>(null);
    
        const documents = [
            { id: 1 },
            { id: 2 },
            { id: 3 },
            { id: 4 },
            { id: 5 },
            { id: 6 },
            { id: 7 },
        ];
    
        useEffect(() => {
            const onOutsideClick = (e: MouseEvent) => {
                const element = document.getElementById("document-card-" + active);
                const menubar = document.querySelector(".menubarContainer");
                const menubarTrigger = document.getElementById(
                    "menubar-tigger-" + active
                );
                const menubarItem = document.querySelector(".menubarItem");
                if (
                    active &&
                    !element?.contains(e.target as Node) &&
                    !menubar?.contains(e.target as Node) &&
                    !menubarItem?.contains(e.target as Node) &&
                    !menubarTrigger?.contains(e.target as Node)
                ) {
                    setActive(null);
                }
                console.log(e.target)
            };
    
            document.addEventListener("mousedown", onOutsideClick);
    
            return () => document.removeEventListener("mousedown", onOutsideClick);
        }, [active]);
    
        return (
            <div className="grid grid-cols-[repeat(auto-fill,minmax(17rem,1fr))] gap-1">
                {documents.map((item, index) => (
                    <DocumentCard
                        key={index}
                        data={item}
                        active={active}
                        id={"document-card-" + item.id}
                        onSelectActive={(id) => {
                            setActive(id);
                        }}
                        onClickMenu={() => console.log('clickedMenu')}
                    />
                ))}
            </div>
        );
    }
    
    export default Pinned;
    

    So, the revised implementation is to put the outsideclick in the parent component ensuring that it only triggers once.


  2. Stop event propagation to parent , use preventDefault()/stopPropagation()

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search