use-onclickoutside icon indicating copy to clipboard operation
use-onclickoutside copied to clipboard

Disabling useOutsideClick callback for specified elements

Open freddydumont opened this issue 6 years ago • 12 comments
trafficstars

What's your thoughts on adding similar functionality to the react-onclickoutside library for disabling outside click callback on certain specified elements?

See this gif for an example. I'd like to disable the callback for the toggler.

2019-01-21 11 00 49

Right now I'm doing this, which works fine for that use case but it'd be nice to have this included with the library:

  const ref = useRef(null);
  useOutsideClick(ref, e => {
    if (!e.target.parentElement.className.includes('ignore-onOutsideClick')) {
      toggle();
    }
  });

freddydumont avatar Jan 21 '19 16:01 freddydumont

The are no plans for this, the beauty of hooks IMHO lies in their composability and I prefer this over configurability and too many features inside "the box" 😉

You can easily build this on top of this hook (which you kinda already have done 🚀). You could go even further and encapsulate this behaviour in a reusable fashion:

// myUseOnClickOutside.js
import useOnClickOutside from 'use-onclickoutside'

const hasIgnoredClass = (element, ignoredClass) =>
  (element.correspondingElement
    ? element.correspondingElement
    : element
  ).classList.contains(ignoredClass)

const isInIgnoredElement = (element, ignoredClass) => {
  do {
    if (hasIgnoredClass(element, ignoredClass)) {
      return true
    }
  } while ((element = element.parentElement))

  return false
}

export default (ref, handler, ignoredClass = 'ignore-onClickOutside') =>
  useOnClickOutside(ref, event => {
    if (isInIgnoredElement(event.target, ignoredClass)) {
      return
    }

    handler(event)
  })

I could however consider exporting this in "addon" form, so everyone could compose their own logic (if needed) - but still benefit from the centralized addons. Not sure what API exactly that could have though.

Andarist avatar Jan 21 '19 19:01 Andarist

Thanks for the in-depth answer! I'll definitely use your snippet if I find myself having to reuse the ignored class logic all over the place.

freddydumont avatar Jan 22 '19 14:01 freddydumont

You may also use [...document.querySelectorAll(className)].some(x => evt.target.contains(x)) to avoid to traverse the DOM manually.

FezVrasta avatar Feb 04 '19 17:02 FezVrasta

Yeah, sure - but u do a lot more of traversing by querying whole DOM first and then querying received subtrees. Whereas when doing manual traversal you check only a single node path

Andarist avatar Feb 04 '19 18:02 Andarist

I think querySelector and querySelectorAll are very optimised, but I guess one would need to benchmark to be sure.

FezVrasta avatar Feb 04 '19 18:02 FezVrasta

If you want to not trigger it when clicking inside some element, you can also do it this way:

const [open, setOpen] = React.useState(false)
const buttonRef = React.useRef(null)
const clickOutsideRef = React.useRef(null)

useOnClickOutside(ref, event => {
  if (!buttonRef.current.contains(event.target)) {
    setOpen(false)
  }
})

return (<>
  <button ref={buttonRef} onClick={() => setOpen(!open)}>Click to toggle</button>
  { open && <div ref={clickOutsideRef}>…</div> }
</>)

renchap avatar Mar 02 '19 12:03 renchap

This is what I would probably do in "trigger+tooltip~" situation 👍

Andarist avatar Mar 02 '19 14:03 Andarist

@Andarist what about multiple refs as parameter, do you still prefer doing multiple hooks calls?

slorber avatar May 03 '19 17:05 slorber

@Andarist just implemented a dropdown and not supporting multiple refs makes it a bit more complicated.

  const buttonRef = useRef<HTMLDivElement | null>(null);
  const dropdownRef = useRef<HTMLDivElement | null>(null);

  useOnClickOutside(buttonRef, event => {
    const isDropdownClick =
      dropdownRef.current && dropdownRef.current.contains(event.target as Node);
    
    if (!isDropdownClick) {
      setOpened(false);
    }
  });

I'd rather have this instead:

  const buttonRef = useRef<HTMLDivElement | null>(null);
  const dropdownRef = useRef<HTMLDivElement | null>(null);

  useOnClickOutside([buttonRef,dropdownRef], event => {
    setOpened(false);
  });

slorber avatar May 15 '19 12:05 slorber

how would it work? would it check against if the click originated outside of ANY of given refs or outside of ALL of them?

Andarist avatar May 15 '19 12:05 Andarist

I would say any of them.

If you want "contained in all of them", you can pass the most "specific" ref only to get that behavior, or it will always be false if refs are not nested. Can't find an usecase where this would be useful

slorber avatar May 15 '19 13:05 slorber

The are no plans for this, the beauty of hooks IMHO lies in their composability and I prefer this over configurability and too many features inside "the box" 😉

You can easily build this on top of this hook (which you kinda already have done 🚀). You could go even further and encapsulate this behaviour in a reusable fashion:

// myUseOnClickOutside.js
import useOnClickOutside from 'use-onclickoutside'

const hasIgnoredClass = (element, ignoredClass) =>
  (element.correspondingElement
    ? element.correspondingElement
    : element
  ).classList.contains(ignoredClass)

const isInIgnoredElement = (element, ignoredClass) => {
  do {
    if (hasIgnoredClass(element, ignoredClass)) {
      return true
    }
  } while ((element = element.parentElement))

  return false
}

export default (ref, handler, ignoredClass = 'ignore-onClickOutside') =>
  useOnClickOutside(ref, event => {
    if (isInIgnoredElement(event.target, ignoredClass)) {
      return
    }

    handler(event)
  })

I could however consider exporting this in "addon" form, so everyone could compose their own logic (if needed) - but still benefit from the centralized addons. Not sure what API exactly that could have though.

@Andarist Thanks for the snippet 😄 Although for some odd reason in rare occasions, event.target was null, causing hasIgnoredClass to error on the first call.

Here's a fix if anyones interested, with added recursion because reasons 💫

// myUseOnClickOutside.js
import useOnClickOutside from 'use-onclickoutside';

const hasClass = (element, className) => (
  element && (
    element.correspondingElement ? element.correspondingElement : element
  ).classList.contains(className)
);

// Returns true if the given element, or any parentElement has the ignoredClass class
const isInIgnoredElement = (element, ignoredClass) => {
  if (!element) {
    return false;
  }

  if (hasClass(element, ignoredClass)) {
    return true;
  }

  return isInIgnoredElement(element.parentElement, ignoredClass);
};

export default (ref, handler, ignoredClass = 'ignore-onClickOutside') => (
  useOnClickOutside(ref, (event) => {
    if (isInIgnoredElement(event.target, ignoredClass)) {
      return;
    }

    handler(event);
  })
);

hrafnkellbaldurs avatar Nov 04 '19 17:11 hrafnkellbaldurs