Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to implement a click-away listener in Next js

I have a reusable component called DropDown and a custom hook called useClickOutside. I am using context API to track multiple dropdowns using unique ids.

Code for useClickOutside

import { useEffect, useRef } from "react";
import { toUSVString } from "util";

export function useClickOutside(callback: () => void) {
  const ref = useRef<HTMLDivElement | null>(null);

  useEffect(() => {
    function handleClick(event: MouseEvent) {
      const target = event.target as HTMLDivElement | HTMLButtonElement;
      if (event.target === ref.current) {
        return;
      }

      if (target?.contains(ref.current) && target !== ref.current) {
        console.log("clicked outside");
        callback();
      }
    }

    window && window.addEventListener("click", handleClick, { capture: true });
    return () => {
      window &&
        window.removeEventListener("click", handleClick, { capture: true });
    };
  }, [callback, ref.current]);

  return ref;
}

code for DropDown component

import { ReactNode } from "react";
import { useClickOutside } from "../../util/hooks/useClickOutside";

export default function DropDown({
  id,
  onOpen,
  onClose,
  isOpen,
  content,
  buttonContent,
}: {
  id: string;
  onOpen: (id: string) => void | undefined;
  onClose: () => void | undefined;
  isOpen: boolean;
  content?: ReactNode;
  buttonContent?: any;
}) {

    let elementRef = useClickOutside(() => {
    if (isOpen) {
      onClose();
    }
  });
  

  return (
    <div className="relative z-30">
      <button
        className="rounded-full p-0 border-2 border-slate-300"
        onClick={() => (isOpen ? onClose() : onOpen(id))}
      >
        {buttonContent}
      </button>
      {isOpen && (
        <div
          ref={elementRef}
          className="absolute space-y-2 right-0 w-56 mt-2 origin-top-right bg-white rounded-lg shadow-xl ring-1 ring-black ring-opacity-5 focus:outline-none z-30"
        >
          {content}
        </div>
      )}
    </div>
  );
}

Discussion on Nextjs Repository Loom with demo

Current Behavior DropDown close when I click outside the element but within the component that wraps around the DropDown component.

Expected Behavior I want the DropDown to close no matter where I click on the entire screen. Whether I click within the wrapping component or any other components, the event should be fired.

like image 933
Benjamin Kalungi Avatar asked Nov 02 '25 09:11

Benjamin Kalungi


1 Answers

You are explicitly saying that you don't want to call the callback when clicking other elements, but what you need to do is, simply skip calling the callback when it's not necessary.

// useClickOutside.ts
import { useEffect, useRef } from "react";

export function useClickOutside(callback: () => void) {
  const ref = useRef<HTMLDivElement | null>(null);

  useEffect(() => {
    function handleClick(event: MouseEvent) {
      const el = ref?.current;

      // Do nothing if clicking ref's element or descendent elements
      if (!el || el.contains(event.target as Node)) {
        return;
      }

      callback();
    }

    window?.addEventListener("click", handleClick, { capture: true });
    return () => {
      window?.removeEventListener("click", handleClick, { capture: true });
    };
  }, [callback, ref.current]);

  return ref;
}

like image 176
M.A Shahbazi Avatar answered Nov 03 '25 21:11

M.A Shahbazi