use-onclickoutside
use-onclickoutside copied to clipboard
Disabling useOutsideClick callback for specified elements
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.

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();
}
});
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.
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.
You may also use [...document.querySelectorAll(className)].some(x => evt.target.contains(x)) to avoid to traverse the DOM manually.
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
I think querySelector and querySelectorAll are very optimised, but I guess one would need to benchmark to be sure.
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> }
</>)
This is what I would probably do in "trigger+tooltip~" situation 👍
@Andarist what about multiple refs as parameter, do you still prefer doing multiple hooks calls?
@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);
});
how would it work? would it check against if the click originated outside of ANY of given refs or outside of ALL of them?
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
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);
})
);