Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

useClickAway should support one or more elements #2490

Open
momesana opened this issue Mar 22, 2023 · 2 comments
Open

useClickAway should support one or more elements #2490

momesana opened this issue Mar 22, 2023 · 2 comments

Comments

@momesana
Copy link

momesana commented Mar 22, 2023

I have a case where a popover is supposed to be closed when I click anything outside the popover. The button which triggers the popover also works as a toggle, i. e. clicking it again closes the popover. This behavior is quite common and intuitive and should be preserved. Now, using useClickAway I can close the popover but when I try to do the same via toggling said button, the events interfere with each other and result in the popover being displayed again (the click outside closing it and the click on the button showing it again).

The apparent fix would be to allow useClickAway to receive one element or a list of elements and in the latter case only invoke the callback function if the click happened outside all observed elements.

I've adapted the useClickAway implementation to do this as follows and it works for my specific case:

import type { MutableRefObject } from "react";
import { useEffect, useRef } from "react";
import { off, on } from "react-use/lib/misc/util";

const defaultEvents = ["mousedown", "touchstart"];

// This is based on react-use's clickAway hook but made to work with multiple refs in addition to a single ref,
// meaning the click needs to happen outside all registered refs.
// See https://github.com/streamich/react-use/blob/master/src/useClickAway.ts for the original hook
export function useClickAway<
  T extends Element | null = Element,
  E extends Event = Event
>(
  refs: Array<MutableRefObject<T>>,
  onClickAway: (event: E) => void,
  events: string[] = defaultEvents
): void {
  const savedCallback = useRef(onClickAway);
  const savedRefs = useRef(refs);
  useEffect(() => {
    savedCallback.current = onClickAway;
    savedRefs.current = refs;
  }, [onClickAway, refs]);

  useEffect(() => {
    const handler = (event: E) => {
      const clickedOutside = savedRefs.current.every(ref => {
        const { current: el } = ref;
        return el && !el.contains(event.target as Node);
      });
      if (clickedOutside) {
        savedCallback.current(event);
      }
    };
    for (const eventName of events) {
      on(document, eventName, handler);
    }
    return () => {
      for (const eventName of events) {
        off(document, eventName, handler);
      }
    };
  }, [events]);
}

I'd appreciate it if something like that became available upstream so I could get rid of my custom implementation. I could also make a PR if that's increases the chance of it being made available upstream.

@BoyYangzai
Copy link

+1,I have encountered the same business scenario😄
I made a single version of useClickAway and a plus version of useClickAway for myself and much test cases
https://github.com/BoyYangzai/boyy-utils

@BoyYangzai
Copy link

For react-use, if you want to add a new feature without affecting the previous user, you should do it like this:

import { RefObject, useEffect, useRef } from 'react';
import { off, on } from './misc/util';

const defaultEvents = ['mousedown', 'touchstart'];

const useClickAway = <E extends Event = Event>(
  ref: RefObject<HTMLElement | null> | Array<RefObject<HTMLElement | null>>,
  onClickAway: (event: E) => void,
  events: string[] = defaultEvents
) => {
  const savedCallback = useRef(onClickAway);
  const savedRefs = useRef<Array<RefObject<HTMLElement | null>>>(
    Array.isArray(ref) ? ref : [ref]
  );

  useEffect(() => {
    savedCallback.current = onClickAway;
    savedRefs.current = Array.isArray(ref) ? ref : [ref];
  }, [onClickAway, ref]);

  useEffect(() => {
    const handler = (event: E) => {
      const clickedOutside = savedRefs.current.every((ref) => {
        const { current: el } = ref;
        return el && !el.contains(event.target as Node);
      });

      if (clickedOutside) {
        savedCallback.current(event);
      }
    };

    for (const eventName of events) {
      on(document, eventName, handler);
    }

    return () => {
      for (const eventName of events) {
        off(document, eventName, handler);
      }
    };
  }, [events]);
};

export default useClickAway;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants