HOC for Blur (Unfocus) event handling of React component
npm i --save react-onblur
import reactOnBlur from 'react-onblur'; is DEPRECATED.
Use import { withOnBlur } from 'react-onblur';.
To support react 19 we need to get rid of using react-dom/findDOMNode, so now you have to pass rootNodeRef to setBlurListener.
It is simple HOC function
import { withOnBlur } from "react-onblur";
// ...
export default withOnBlur(/* args */)(YourReactComponent);which puts in your component two extra props
setBlurListener: function (Object)
unsetBlurListener: function ()
setBlurListener({ onBlur: onBlurCallback, once: true, checkInOutside: isOutside, rootNodeRef: parentRef })
rootNodeRef: Function() | React.ref, required. Should return parent node OR be ref object with current, all targets out of this node will handle 'onBlur'.onBlur: Function(event), required. It will be called once your component is unfocused or a user clicks outside your component.once: Boolean, optional. Whentruelisteners will be removed after the first blur event.checkInOutside: Function(domNode: Node, isOutsideDecision: Boolean): Boolean, optional. This function called always, when each of selected events are fired (e.g. when a user clicks on element on page or press key). It takes dom element target of an event and a Boolean sign that this element is outside. It should return Boolean value as sign that this element is outside. If it returns true thenonBlurwill be called. More information in next part of this doc.
unsetBlurListener should be called when your want to remove events from document (e.g. once popover is hidden).
Could be called inside onBlur callback.
(!!!) All document listeners will be removed on component unmount automatically.
You should create new component:
import { useCallback, useRef, useState } from "react";
import { withOnBlur } from "react-onblur";
function DemoComponent({ setBlurListener, unsetBlurListener }) {
const rootRef = useRef(null);
const [isOpened, setIsOpened] = useState(false);
const onBlur = useCallback(() => setIsOpened(false), []);
const onClickOpen = useCallback(() => {
setIsOpened(true);
setBlurListener({ onBlur, once: true, rootNodeRef: rootRef });
}, [setBlurListener, onBlur]);
return (
<div ref={rootRef}>
<button onClick={onClickOpen}>
Open list
</button>
{isOpened && (
<ul>
// ... list items
</ul>
)}
</div>
);
}
export default withOnBlur({ debug: true })(DemoComponent);Next, you can use this component in your App:
function App() {
return (
<div className="App">
<div>
<button>Outside button</button>
</div>
<DemoComponent />
<div>
<button>Second outside button</button>
</div>
</div>
);
}| args | type | default | description |
|---|---|---|---|
listenClick |
bool | true | when true will add mousedown event for document |
listenTab |
bool | true | when true will add keyup and keydown listeners for document to check Tab key press |
listenEsc |
bool | true | when true will add keydown event for document to check Esc key is pressed |
debug |
bool | false | when true will write debug messages to console |
autoUnset |
bool | false | if true then unsetBlurListener will be called after callback action call once your component is unfocused or user will click outside of your component |
import { withOnBlur } from "react-onblur";
// ...
export default withOnBlur({
listenClick: true,
listenTab: false,
debug: true,
autoUnset: true
})(YourReactComponent);If you use ReactDOM.createPortal then you can put element to another parent and it will not be child by DOM tree. But you can use checkInOutside to say about it to react-onblur.
import { useCallback, useRef, useState } from "react";
import ReactDOM from 'react-dom';
import { withOnBlur, isDomElementChild } from "react-onblur";
function DemoPortalComponent({ setBlurListener, unsetBlurListener }) {
const [isOpened, setIsOpened] = useState(false);
const menuRef = useRef(null);
const rootRef = useRef(null);
const checkInOutside = useCallback((element, isOutside) => (
// if it is in outside then we should check that it is not in our list,
// which was moved to body by createPortal
// For it we can use `isDomElementChild(parent, node)` from `react-onblur`
// and if element is child of list then it returns false.
isOutside && !isDomElementChild(menuRef.current, element)
), []);
const onBlur = useCallback(() => setIsOpened(false), []);
const onClickOpen = useCallback(() => {
setIsOpened(true);
setBlurListener({
checkInOutside,
onBlur,
once: true,
rootNodeRef: rootRef
});
}, [setBlurListener, checkInOutside, onBlur]);
return (
<div>
This is content outside of parent component, so clicking here will trigger blur event
<div ref={rootRef}>
<button onClick={onClickOpen}>
Open menu
</button>
{isOpened && (
ReactDOM.createPortal(
<ul ref={menuRef}>
// ... any content
</ul>,
document.querySelector('body')
)
)}
</div>
</div>
);
}
export default withOnBlur()(DemoPortalComponent);Check that node is child of parent.
isDomElementChild: Function(parent: DomNode, node: DomNode): Boolean
import { withOnBlur, isDomElementChild } from "react-onblur";
isDomElementChild(document.querySelector('body'), document.querySelector('div'));
// true for <body><div>1</div></body>