Reputation: 23300
I'm looking for a way to detect if a click event happened outside of a component, as described in this article. jQuery closest() is used to see if the target from a click event has the dom element as one of its parents. If there is a match the click event belongs to one of the children and is thus not considered to be outside of the component.
So in my component, I want to attach a click handler to the window
. When the handler fires I need to compare the target with the dom children of my component.
The click event contains properties like "path" which seems to hold the dom path that the event has traveled. I'm not sure what to compare or how to best traverse it, and I'm thinking someone must have already put that in a clever utility function... No?
Upvotes: 933
Views: 995946
Reputation: 8077
Here is a TypeScript version inspired from Tanner Linsley’s talk at JSConf Hawaii 2020:
import { MutableRefObject, useEffect, useRef } from 'react';
export function useClickOutside(
elementRefs: MutableRefObject<HTMLElement | null>[],
callback: (event: MouseEvent) => void
): void {
const callbackRef = useRef(callback);
callbackRef.current = callback;
useEffect(() => {
const handleClickOutside = (event: MouseEvent): void => {
const target = event.target;
if (
!(target instanceof Node) ||
elementRefs.every((elementRef) => !elementRef.current?.contains(target))
) {
callbackRef.current?.(event);
}
};
document.addEventListener('click', handleClickOutside, true);
return () =>
document.removeEventListener('click', handleClickOutside, true);
}, [elementRefs]);
}
Upvotes: 3
Reputation: 311
import { RefObject, useEffect } from 'react';
const useClickOutside = <T extends HTMLElement>(ref: RefObject<T>, fn: () => void) => {
useEffect(() => {
const element = ref?.current;
function handleClickOutside(event: Event) {
if (element && !element.contains(event.target as Node | null)) {
fn();
}
}
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [ref, fn]);
};
export default useClickOutside;
Upvotes: 9
Reputation: 71
There is an npm module which will make your life easier to handle the clicks outside a specific component. For Example: You make states as true and false. On dropdown a menu list your state is true and on clicking on close button your state converts to false and dropdown menu component gets disappear. But you want to close this drop down menu also clicking on outside of the drop down menu on the window. To deal with such scenario follow the below steps:
npm i react-outside-click-handler
Now Import this module in your React File:
import OutsideClickHandler from 'react-outside-click-handler';
Now You have imported a component from this module. This component takes a component outside of which you want to detect a click event
function MyComponent() {
return (
<OutsideClickHandler
onOutsideClick={() => {
alert("You clicked outside of this component!!!");
//Or any logic you want
}} >
<yourComponent />
</OutsideClickHandler>
);
}
Now Simply replace you Own component with . I hope this find you helpful :)
Upvotes: 4
Reputation: 1574
The Ez way... (UPDATED 2023)
useOutsideClick.ts
export function useOutsideClick(ref: any, onClickOut: () => void, deps = []){
useEffect(() => {
const onClick = ({target}: any) => !ref?.contains(target) && onClickOut?.()
document.addEventListener("click", onClick);
return () => document.removeEventListener("click", onClick);
}, deps);
}
componentRef
to your component and call useOutsideClick
export function Example(){
const ref: any = useRef();
useOutsideClick(ref.current, () => {
// do something here
});
return (
<div ref={ref}> My Component </div>
)
}
Upvotes: 50
Reputation: 91
In Typescript Case
Create Hook, we assume useOutsideAlerter
const useOutsideAlerter = (ref: RefObject<HTMLElement>, callback: () => void) => {
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (ref.current && !ref.current.contains(event.target as Node)) {
callback()
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => {
// Unbind the event listener on clean up
document.removeEventListener('mousedown', handleClickOutside)
}
}, [ref])
}
Call Hook In Your Component
const MyComponent: FC<{}> = (props) => {
const wrapperRef = useRef(null)
const handleOutSideClick = () => {
alert('click outside component')
}
const handleInSideClick = () => {
alert('click inside component')
}
useOutsideAlerter(wrapperRef, handleOutSideClick)
return (
<div ref={wrapperRef}><button onClick={handleInSideClick}>KLIK ME</button></div>
)
}
Upvotes: 2
Reputation: 25019
The most simple solution I have found is to use:
onBlur={(e) => {
const parent = e.currentTarget.parentNode;
const isDescendant = parent
? parent.contains(e.relatedTarget)
: false;
if (!isDescendant) {
setIsOpen(false);
}
}}
You might have to use multiple .parentNode
until you find the dropdown root element (parent
). If e.relatedTarget
is not a descendant of parent
then you can close the click was outside.
Upvotes: 1
Reputation: 459
Try this it works perfectly
import React, { useState, useRef, useEffect } from "react";
const Dropdown = () => {
const [state, setState] = useState(true);
const dropdownRef = useRef(null);
const handleToggleDropdown = () => {
if (state) setState(false);
else setState(true);
}
const handleClickOutside = (event) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
setState(true);
}
}
useEffect(() => {
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
}
}, []);
return (
<React.Fragment>
<div className="dropdown" ref={dropdownRef}>
<button onClick={() => handleToggleDropdown()}>
Options
</button>
<div className="dropdown-content" hidden={state}>
</div>
</div>
</React.Fragment>
);
}
export default Dropdown;
Upvotes: 3
Reputation: 307
In addition to other solution, here is the improved function that can unsubscribe the listener and add an element or group of element of the React component to track the click. This hook can be used to handle scenarios such as closing a dropdown menu when a user clicks outside of it, or hiding a modal when the user clicks outside of it.
function useOutsideClick(
reference: Array<React.RefObject<any>>,
onClickOut: () => void,
shouldListen = true,
) {
useEffect(() => {
const onClick = (event: Event) => {
if (
shouldListen &&
reference.some(
(reference_) =>
reference_.current?.contains(event.target as Node) === false,
)
) {
return onClickOut();
}
};
if (!shouldListen) {
return document.removeEventListener('click', onClick);
}
document.addEventListener('click', onClick);
return () => document.removeEventListener('click', onClick);
}, [reference, shouldListen]);
}
Upvotes: 0
Reputation: 473
If you need typescript version:
import React, { useRef, useEffect } from "react";
interface Props {
ref: React.MutableRefObject<any>;
}
export const useOutsideAlerter = ({ ref }: Props) => {
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (ref.current && !ref.current.contains(event.target as Node)) {
//do what ever you want
}
};
// Bind the event listener
document.addEventListener("mousedown", handleClickOutside);
return () => {
// Unbind the event listener on clean up
document.removeEventListener("mousedown", handleClickOutside);
};
}, [ref]);
};
export default useOutsideAlerter;
If you want to extend this to close a modal or hide something you can also do:
import React, { useRef, useEffect } from "react";
interface Props {
ref: React.MutableRefObject<any>;
setter: React.Dispatch<React.SetStateAction<boolean>>;
}
export const useOutsideAlerter = ({ ref, setter }: Props) => {
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (ref.current && !ref.current.contains(event.target as Node)) {
setter(false);
}
};
// Bind the event listener
document.addEventListener("mousedown", handleClickOutside);
return () => {
// Unbind the event listener on clean up
document.removeEventListener("mousedown", handleClickOutside);
};
}, [ref, setter]);
};
export default useOutsideAlerter;
Upvotes: 4
Reputation: 2182
All the solution proposed assume that an event can be added to the document and rely on the native method .contains()
to distinguish if the event is triggered inside or outside of the current component
ref.current.contains(event.target)
but this is not always valid in React. In React, in fact, there is the React.createPortal
API that permit to specify from a component a new real parent component in which the JSX is rendered into but, at the same time, the event bubbling is simulated as is the component is rendered in the declared place (i.e. where React.createPortal is invoked).
This is achieved attaching all the event to the app root element and simulating the events in Javascript.
So the solution proposed is broken in that scenario because a click inside a portal element, that for the standard HTML is outside the current element, actually should be handled as it is inside.
So I rewritten a solution proposed in a comment in this question, and refactored it to use the functional component. This works also in case of one or multiple nested portals.
export default function OutsideClickDetector({onOutsideClick, Component ='div', ...props} : OutsideClickDetectorProps) {
const isClickInside = useRef<boolean>(false);
const onMouseDown = () => {
isClickInside.current = true;
};
const handleBodyClick = useCallback((e) => {
if(!isClickInside.current) {
onOutsideClick?.(e);
}
isClickInside.current = false;
}, [isClickInside, onOutsideClick]);
useEffect(() => {
document.addEventListener('mousedown', handleBodyClick);
return () => document.removeEventListener('mousedown', handleBodyClick);
});
return <Component onMouseDown={onMouseDown} onMouseUp={() => isClickInside.current = false}{...props} />;
}
Upvotes: 0
Reputation: 511
Since for me the !ref.current.contains(e.target)
wasn't working because the DOM elements contained inside the ref were changing, I came up with a slightly different solution:
function useClickOutside<T extends HTMLElement>(
element: T | null,
onClickOutside: () => void,
) {
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
const xCoord = event.clientX;
const yCoord = event.clientY;
if (element) {
const { right, x, bottom, y } = element.getBoundingClientRect();
if (xCoord < right && xCoord > x && yCoord < bottom && yCoord > y) {
return;
}
onClickOutside();
}
}
document.addEventListener('click', handleClickOutside);
return () => {
document.removeEventListener('click', handleClickOutside);
};
}, [element, onClickOutside]);
Upvotes: 3
Reputation: 49661
import React, { useState, useEffect, useRef } from "react";
const YourComponent: React.FC<ComponentProps> = (props) => {
const ref = useRef<HTMLDivElement | null>(null);
const [myState, setMyState] = useState(false);
useEffect(() => {
const listener = (event: MouseEvent) => {
// we have to add some logic to decide whether or not a click event is inside of this editor
// if user clicks on inside the div we dont want to setState
// we add ref to div to figure out whether or not a user is clicking inside this div to determine whether or not event.target is inside the div
if (
ref.current &&
event.target &&
// contains is expect other: Node | null
ref.current.contains(event.target as Node)
) {
return;
}
// if we are outside
setMyState(false);
};
// anytime user clics anywhere on the dom, that click event will bubble up into our body element
// without { capture: true } it might not work
document.addEventListener("click", listener, { capture: true });
return () => {
document.removeEventListener("click", listener, { capture: true });
};
}, []);
return (
<div ref={ref}>
....
</div>
);
};
Upvotes: 0
Reputation: 8681
MUI has a small component to solve this problem: https://mui.com/base/react-click-away-listener/ that you can cherry-pick it. It weights below 1 kB gzipped, it supports mobile, IE 11, and portals.
Upvotes: 38
Reputation: 179
with typescript
function Tooltip(): ReactElement {
const [show, setShow] = useState(false);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
function handleClickOutside(event: MouseEvent): void {
if (ref.current && !ref.current.contains(event.target as Node)) {
setShow(false);
}
}
// Bind the event listener
document.addEventListener('mousedown', handleClickOutside);
return () => {
// Unbind the event listener on clean up
document.removeEventListener('mousedown', handleClickOutside);
};
});
return (
<div ref={ref}></div>
)
}
Upvotes: 15
Reputation: 79
Simply with ClickAwayListener from mui (material-ui):
<ClickAwayListener onClickAway={handleClickAway}>
{children}
<ClickAwayListener >
for more info you can check:https://mui.com/base/react-click-away-listener/
Upvotes: 7
Reputation: 49
I had a similar use case where I had to develop a custom dropdown menu. it should close automatically when the user clicks outside. here is the recent React Hooks implementation-
import { useEffect, useRef, useState } from "react";
export const App = () => {
const ref = useRef();
const [isMenuOpen, setIsMenuOpen] = useState(false);
useEffect(() => {
const checkIfClickedOutside = (e) => {
// If the menu is open and the clicked target is not within the menu,
// then close the menu
if (isMenuOpen && ref.current && !ref.current.contains(e.target)) {
setIsMenuOpen(false);
}
};
document.addEventListener("mousedown", checkIfClickedOutside);
return () => {
// Cleanup the event listener
document.removeEventListener("mousedown", checkIfClickedOutside);
};
}, [isMenuOpen]);
return (
<div className="wrapper" ref={ref}>
<button
className="button"
onClick={() => setIsMenuOpen((oldState) => !oldState)}
>
Click Me
</button>
{isMenuOpen && (
<ul className="list">
<li className="list-item">dropdown option 1</li>
<li className="list-item">dropdown option 2</li>
<li className="list-item">dropdown option 3</li>
<li className="list-item">dropdown option 4</li>
</ul>
)}
</div>
);
}
Upvotes: 3
Reputation: 4219
Typescript + simplified version of @ford04's proposal:
useOuterClick
APIconst Client = () => {
const ref = useOuterClick<HTMLDivElement>(e => { /* Custom-event-handler */ });
return <div ref={ref}> Inside </div>
};
export default function useOuterClick<T extends HTMLElement>(callback: Function) {
const callbackRef = useRef<Function>(); // initialize mutable ref, which stores callback
const innerRef = useRef<T>(null); // returned to client, who marks "border" element
// update cb on each render, so second useEffect has access to current value
useEffect(() => { callbackRef.current = callback; });
useEffect(() => {
document.addEventListener("click", _onClick);
return () => document.removeEventListener("click", _onClick);
function _onClick(e: any): void {
const clickedOutside = !(innerRef.current?.contains(e.target));
if (clickedOutside)
callbackRef.current?.(e);
}
}, []); // no dependencies -> stable click listener
return innerRef; // convenience for client (doesn't need to init ref himself)
}
Upvotes: 7
Reputation: 61
This is my way of solving the problem
I return a boolean value from my custom hook, and when this value changes (true if the click was outside of the ref that I passed as an arg), this way i can catch this change with an useEffect hook, i hope it's clear for you.
Here's a live example: Live Example on codesandbox
import { useEffect, useRef, useState } from "react";
const useOutsideClick = (ref) => {
const [outsieClick, setOutsideClick] = useState(null);
useEffect(() => {
const handleClickOutside = (e) => {
if (!ref.current.contains(e.target)) {
setOutsideClick(true);
} else {
setOutsideClick(false);
}
setOutsideClick(null);
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [ref]);
return outsieClick;
};
export const App = () => {
const buttonRef = useRef(null);
const buttonClickedOutside = useOutsideClick(buttonRef);
useEffect(() => {
// if the the click was outside of the button
// do whatever you want
if (buttonClickedOutside) {
alert("hey you clicked outside of the button");
}
}, [buttonClickedOutside]);
return (
<div className="App">
<button ref={buttonRef}>click outside me</button>
</div>
);
}
Upvotes: 4
Reputation: 16990
The following solution uses ES6 and follows best practices for binding as well as setting the ref through a method.
To see it in action:
import React, { useRef, useEffect } from "react";
/**
* Hook that alerts clicks outside of the passed ref
*/
function useOutsideAlerter(ref) {
useEffect(() => {
/**
* Alert if clicked on outside of element
*/
function handleClickOutside(event) {
if (ref.current && !ref.current.contains(event.target)) {
alert("You clicked outside of me!");
}
}
// Bind the event listener
document.addEventListener("mousedown", handleClickOutside);
return () => {
// Unbind the event listener on clean up
document.removeEventListener("mousedown", handleClickOutside);
};
}, [ref]);
}
/**
* Component that alerts if you click outside of it
*/
export default function OutsideAlerter(props) {
const wrapperRef = useRef(null);
useOutsideAlerter(wrapperRef);
return <div ref={wrapperRef}>{props.children}</div>;
}
After 16.3
import React, { Component } from "react";
/**
* Component that alerts if you click outside of it
*/
export default class OutsideAlerter extends Component {
constructor(props) {
super(props);
this.wrapperRef = React.createRef();
this.handleClickOutside = this.handleClickOutside.bind(this);
}
componentDidMount() {
document.addEventListener("mousedown", this.handleClickOutside);
}
componentWillUnmount() {
document.removeEventListener("mousedown", this.handleClickOutside);
}
/**
* Alert if clicked on outside of element
*/
handleClickOutside(event) {
if (this.wrapperRef && !this.wrapperRef.current.contains(event.target)) {
alert("You clicked outside of me!");
}
}
render() {
return <div ref={this.wrapperRef}>{this.props.children}</div>;
}
}
Before 16.3
import React, { Component } from "react";
/**
* Component that alerts if you click outside of it
*/
export default class OutsideAlerter extends Component {
constructor(props) {
super(props);
this.setWrapperRef = this.setWrapperRef.bind(this);
this.handleClickOutside = this.handleClickOutside.bind(this);
}
componentDidMount() {
document.addEventListener("mousedown", this.handleClickOutside);
}
componentWillUnmount() {
document.removeEventListener("mousedown", this.handleClickOutside);
}
/**
* Set the wrapper ref
*/
setWrapperRef(node) {
this.wrapperRef = node;
}
/**
* Alert if clicked on outside of element
*/
handleClickOutside(event) {
if (this.wrapperRef && !this.wrapperRef.contains(event.target)) {
alert("You clicked outside of me!");
}
}
render() {
return <div ref={this.setWrapperRef}>{this.props.children}</div>;
}
}
Upvotes: 1585
Reputation: 940
So I faced a similar problem but in my case the selected answer here wasn't working because I had a button for the dropdown which is, well, a part of the document. So clicking the button also triggered the handleClickOutside
function. To stop that from triggering, I had to add a new ref
to the button and this !menuBtnRef.current.contains(e.target)
to the conditional. I'm leaving it here if someone is facing the same issue like me.
Here's how the component looks like now:
const Component = () => {
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const menuRef = useRef(null);
const menuBtnRef = useRef(null);
const handleDropdown = (e) => {
setIsDropdownOpen(!isDropdownOpen);
}
const handleClickOutside = (e) => {
if (menuRef.current && !menuRef.current.contains(e.target) && !menuBtnRef.current.contains(e.target)) {
setIsDropdownOpen(false);
}
}
useEffect(() => {
document.addEventListener('mousedown', handleClickOutside, true);
return () => {
document.removeEventListener('mousedown', handleClickOutside, true);
};
}, []);
return (
<button ref={menuBtnRef} onClick={handleDropdown}></button>
<div ref={menuRef} className={`${isDropdownOpen ? styles.dropdownMenuOpen : ''}`}>
// ...dropdown items
</div>
)
}
Upvotes: 4
Reputation: 12129
I was stuck on the same issue. I am a bit late to the party here, but for me this is a really good solution. Hopefully it will be of help to someone else. You need to import findDOMNode
from react-dom
import ReactDOM from 'react-dom';
// ... ✂
componentDidMount() {
document.addEventListener('click', this.handleClickOutside, true);
}
componentWillUnmount() {
document.removeEventListener('click', this.handleClickOutside, true);
}
handleClickOutside = event => {
const domNode = ReactDOM.findDOMNode(this);
if (!domNode || !domNode.contains(event.target)) {
this.setState({
visible: false
});
}
}
You can create a reusable hook called useComponentVisible
.
import { useState, useEffect, useRef } from 'react';
export default function useComponentVisible(initialIsVisible) {
const [isComponentVisible, setIsComponentVisible] = useState(initialIsVisible);
const ref = useRef(null);
const handleClickOutside = (event) => {
if (ref.current && !ref.current.contains(event.target)) {
setIsComponentVisible(false);
}
};
useEffect(() => {
document.addEventListener('click', handleClickOutside, true);
return () => {
document.removeEventListener('click', handleClickOutside, true);
};
}, []);
return { ref, isComponentVisible, setIsComponentVisible };
}
Then in the component you wish to add the functionality to do the following:
const DropDown = () => {
const { ref, isComponentVisible } = useComponentVisible(true);
return (
<div ref={ref}>
{isComponentVisible && (<p>Dropdown Component</p>)}
</div>
);
}
Find a codesandbox example here.
Upvotes: 296
Reputation: 5813
2021 Update:
It has bee a while since I added this response, and since it still seems to garner some interest, I thought I would update it to a more current React version. On 2021, this is how I would write this component:
import React, { useState } from "react";
import "./DropDown.css";
export function DropDown({ options, callback }) {
const [selected, setSelected] = useState("");
const [expanded, setExpanded] = useState(false);
function expand() {
setExpanded(true);
}
function close() {
setExpanded(false);
}
function select(event) {
const value = event.target.textContent;
callback(value);
close();
setSelected(value);
}
return (
<div className="dropdown" tabIndex={0} onFocus={expand} onBlur={close} >
<div>{selected}</div>
{expanded ? (
<div className={"dropdown-options-list"}>
{options.map((O) => (
<div className={"dropdown-option"} onClick={select}>
{O}
</div>
))}
</div>
) : null}
</div>
);
}
Here is the solution that best worked for me without attaching events to the container:
Certain HTML elements can have what is known as "focus", for example input elements. Those elements will also respond to the blur event, when they lose that focus.
To give any element the capacity to have focus, just make sure its tabindex attribute is set to anything other than -1. In regular HTML that would be by setting the tabindex
attribute, but in React you have to use tabIndex
(note the capital I
).
You can also do it via JavaScript with element.setAttribute('tabindex',0)
This is what I was using it for, to make a custom DropDown menu.
var DropDownMenu = React.createClass({
getInitialState: function(){
return {
expanded: false
}
},
expand: function(){
this.setState({expanded: true});
},
collapse: function(){
this.setState({expanded: false});
},
render: function(){
if(this.state.expanded){
var dropdown = ...; //the dropdown content
} else {
var dropdown = undefined;
}
return (
<div className="dropDownMenu" tabIndex="0" onBlur={ this.collapse } >
<div className="currentValue" onClick={this.expand}>
{this.props.displayValue}
</div>
{dropdown}
</div>
);
}
});
Upvotes: 255
Reputation: 74710
Hook implementation based on Tanner Linsley's excellent talk at JSConf Hawaii 2020:
useOuterClick
APIconst Client = () => {
const innerRef = useOuterClick(ev => {/*event handler code on outer click*/});
return <div ref={innerRef}> Inside </div>
};
function useOuterClick(callback) {
const callbackRef = useRef(); // initialize mutable ref, which stores callback
const innerRef = useRef(); // returned to client, who marks "border" element
// update cb on each render, so second useEffect has access to current value
useEffect(() => { callbackRef.current = callback; });
useEffect(() => {
document.addEventListener("click", handleClick);
return () => document.removeEventListener("click", handleClick);
function handleClick(e) {
if (innerRef.current && callbackRef.current &&
!innerRef.current.contains(e.target)
) callbackRef.current(e);
}
}, []); // no dependencies -> stable click listener
return innerRef; // convenience for client (doesn't need to init ref himself)
}
Here is a working example:
/*
Custom Hook
*/
function useOuterClick(callback) {
const innerRef = useRef();
const callbackRef = useRef();
// set current callback in ref, before second useEffect uses it
useEffect(() => { // useEffect wrapper to be safe for concurrent mode
callbackRef.current = callback;
});
useEffect(() => {
document.addEventListener("click", handleClick);
return () => document.removeEventListener("click", handleClick);
// read most recent callback and innerRef dom node from refs
function handleClick(e) {
if (
innerRef.current &&
callbackRef.current &&
!innerRef.current.contains(e.target)
) {
callbackRef.current(e);
}
}
}, []); // no need for callback + innerRef dep
return innerRef; // return ref; client can omit `useRef`
}
/*
Usage
*/
const Client = () => {
const [counter, setCounter] = useState(0);
const innerRef = useOuterClick(e => {
// counter state is up-to-date, when handler is called
alert(`Clicked outside! Increment counter to ${counter + 1}`);
setCounter(c => c + 1);
});
return (
<div>
<p>Click outside!</p>
<div id="container" ref={innerRef}>
Inside, counter: {counter}
</div>
</div>
);
};
ReactDOM.render(<Client />, document.getElementById("root"));
#container { border: 1px solid red; padding: 20px; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js" integrity="sha256-Ef0vObdWpkMAnxp39TYSLVS/vVUokDE8CDFnx7tjY6U=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.12.0/umd/react-dom.production.min.js" integrity="sha256-p2yuFdE8hNZsQ31Qk+s8N+Me2fL5cc6NKXOC0U9uGww=" crossorigin="anonymous"></script>
<script> var {useRef, useEffect, useCallback, useState} = React</script>
<div id="root"></div>
useOuterClick
makes use of mutable refs to provide lean Client
API[]
deps)Client
can set callback without needing to memoize it by useCallback
iOS in general treats only certain elements as clickable. To make outer clicks work, choose a different click listener than document
- nothing upwards including body
. E.g. add a listener on the React root div
and expand its height, like height: 100vh
, to catch all outside clicks. Source: quirksmode.org
Upvotes: 120
Reputation: 241
NON INTRUSTIVE WAY NO NEED TO ADD ANOTHER DIV EL.
Note: React may say findDomNode isDeprecated but till now I have not faced any issue with it
@exceptions: class to ignore if clicked on it
@idException: id to ignore if clicked on it
import React from "react"
import ReactDOM from "react-dom"
type Func1<T1, R> = (a1: T1) => R
export function closest(
el: Element,
fn: (el: Element) => boolean
): Element | undefined {
let el_: Element | null = el;
while (el_) {
if (fn(el_)) {
return el_;
}
el_ = el_.parentElement;
}
}
let instances: ClickOutside[] = []
type Props = {
idException?: string,
exceptions?: (string | Func1<MouseEvent, boolean>)[]
handleClickOutside: Func1<MouseEvent, void>
}
export default class ClickOutside extends React.Component<Props> {
static defaultProps = {
exceptions: []
};
componentDidMount() {
if (instances.length === 0) {
document.addEventListener("mousedown", this.handleAll, true)
window.parent.document.addEventListener(
"mousedown",
this.handleAll,
true
)
}
instances.push(this)
}
componentWillUnmount() {
instances.splice(instances.indexOf(this), 1)
if (instances.length === 0) {
document.removeEventListener("mousedown", this.handleAll, true)
window.parent.document.removeEventListener(
"mousedown",
this.handleAll,
true
)
}
}
handleAll = (e: MouseEvent) => {
const target: HTMLElement = e.target as HTMLElement
if (!target) return
instances.forEach(instance => {
const { exceptions, handleClickOutside: onClickOutside, idException } = instance.props as Required<Props>
let exceptionsCount = 0
if (exceptions.length > 0) {
const { functionExceptions, stringExceptions } = exceptions.reduce(
(acc, exception) => {
switch (typeof exception) {
case "function":
acc.functionExceptions.push(exception)
break
case "string":
acc.stringExceptions.push(exception)
break
}
return acc
},
{ functionExceptions: [] as Func1<MouseEvent, boolean>[], stringExceptions: [] as string[] }
)
if (functionExceptions.length > 0) {
exceptionsCount += functionExceptions.filter(
exception => exception(e) === true
).length
}
if (exceptionsCount === 0 && stringExceptions.length > 0) {
const el = closest(target, (e) => stringExceptions.some(ex => e.classList.contains(ex)))
if (el) {
exceptionsCount++
}
}
}
if (idException) {
const target = e.target as HTMLDivElement
if (document.getElementById(idException)!.contains(target)) {
exceptionsCount++
}
}
if (exceptionsCount === 0) {
// eslint-disable-next-line react/no-find-dom-node
const node = ReactDOM.findDOMNode(instance)
if (node && !node.contains(target)) {
onClickOutside(e)
}
}
})
};
render() {
return React.Children.only(this.props.children)
}
}
Usage
<ClickOutside {...{ handleClickOutside: () => { alert('Clicked Outside') } }}>
<div >
<div>Breathe</div>
</div>
</ClickOutside>
Upvotes: 1
Reputation: 1671
https://stackoverflow.com/a/42234988/9536897 solution doesn't work on mobile phones.
You can try:
// returns true if the element or one of its parents has the class classname
hasSomeParentTheClass(element, classname) {
if(element.target)
element=element.target;
if (element.className&& element.className.split(" ").indexOf(classname) >= 0) return true;
return (
element.parentNode &&
this.hasSomeParentTheClass(element.parentNode, classname)
);
}
componentDidMount() {
const fthis = this;
$(window).click(function (element) {
if (!fthis.hasSomeParentTheClass(element, "myClass"))
fthis.setState({ pharmacyFocus: null });
});
}
Upvotes: 0
Reputation: 523
I like the @Ben Bud's answer but when there are visually nested elements, contains(event.target)
not works as expected.
So sometimes it's better to calculate the clicked point is visually inside of the element or not.
Here is my React Hook code for the situation.
import { useEffect } from 'react'
export function useOnClickRectOutside(ref, handler) {
useEffect(() => {
const listener = (event) => {
const targetEl = ref.current
if (targetEl) {
const clickedX = event.clientX
const clickedY = event.clientY
const rect = targetEl.getBoundingClientRect()
const targetElTop = rect.top
const targetElBottom = rect.top + rect.height
const targetElLeft = rect.left
const targetElRight = rect.left + rect.width
if (
// check X Coordinate
targetElLeft < clickedX &&
clickedX < targetElRight &&
// check Y Coordinate
targetElTop < clickedY &&
clickedY < targetElBottom
) {
return
}
// trigger event when the clickedX,Y is outside of the targetEl
handler(event)
}
}
document.addEventListener('mousedown', listener)
document.addEventListener('touchstart', listener)
return () => {
document.removeEventListener('mousedown', listener)
document.removeEventListener('touchstart', listener)
}
}, [ref, handler])
}
Upvotes: 1
Reputation: 595
Alternatively:
const onClickOutsideListener = () => {
alert("click outside")
document.removeEventListener("click", onClickOutsideListener)
}
...
return (
<div
onMouseLeave={() => {
document.addEventListener("click", onClickOutsideListener)
}}
>
...
</div>
Upvotes: 19
Reputation: 1
I had a case when I needed to insert children into the modal conditionally. Something like this, bellow.
const [view, setView] = useState(VIEWS.SomeView)
return (
<Modal onClose={onClose}>
{VIEWS.Result === view ? (
<Result onDeny={() => setView(VIEWS.Details)} />
) : VIEWS.Details === view ? (
<Details onDeny={() => setView(VIEWS.Result) /> />
) : null}
</Modal>
)
So !parent.contains(event.target)
doesn't work here, because once you detach children, parent (modal) doesn't contain event.target
anymore.
The solution I had (which works so far and have no any issue) is to write something like this:
const listener = (event: MouseEvent) => {
if (parentNodeRef && !event.path.includes(parentNodeRef)) callback()
}
If parent contained element from already detached tree, it wouldn't fire callback.
EDIT:
event.path
is new and doesn't exit in all browsers yet. Use compoesedPath
instead.
Upvotes: 0
Reputation: 1872
I did this partly by following this and by following the React official docs on handling refs which requires react ^16.3. This is the only thing that worked for me after trying some of the other suggestions here...
class App extends Component {
constructor(props) {
super(props);
this.inputRef = React.createRef();
}
componentWillMount() {
document.addEventListener("mousedown", this.handleClick, false);
}
componentWillUnmount() {
document.removeEventListener("mousedown", this.handleClick, false);
}
handleClick = e => {
/*Validating click is made inside a component*/
if ( this.inputRef.current === e.target ) {
return;
}
this.handleclickOutside();
};
handleClickOutside(){
/*code to handle what to do when clicked outside*/
}
render(){
return(
<div>
<span ref={this.inputRef} />
</div>
)
}
}
Upvotes: 6
Reputation: 1
This is the method that suits me the best to make a dropdown menu:
handleClick = () => {
document.getElementById("myDrop").classList.toggle("showing");
}
render() {
return (
<div className="courses">
<div class="dropdownBody">
<button onClick={this.handleClick} onBlur={this.handleClick} class="dropbtn">Dropdown</button>
<div id="myDrop" class="dropdown-content">
<a href="#home">Home</a>
<a href="#about">About</a>
<a href="#contact">Contact</a>
</div>
</div>
</div>
)
}
Upvotes: -2