I have made a dropdown on React Js. I want the dropdown to close when I click outside using onBlur
function. The problem here is that it also closes when a dropdown option is clicked, and doesn't execute the function attached to the option. I want to add a condition to the onBlur
function which will check if area clicked was inside or outside the dropdown and act accordingly, but I am not sure how to check that.
I tried document.activeElement
, but that is selecting the entire body
element.
Here is my code Sandbox link: https://codesandbox.io/s/intelligent-tdd-zq4qcr?file=/src/App.js:938-941
This is the function I need help with:
const close = (event) => {
// Help me with this function please
// I need to wrap this function in a condition that checks
// if the click was outside the dropdown
let dropdown = event.target.nextSibling;
// If I comment the lines below,
// it updates the number, but doesn't close the dropdown
if (dropdown?.classList.contains("display")) {
dropdown.classList.remove("display");
setOption("");
}
};
I am adding a class with property display: unset
when the dropdown button is clicked. By default, the display is set to none
.
EDIT: The number of these dropdown in my original project are variable. They could be two or they could be 10, depending on the user. So I can't maintain it in the state and need to use JavaScript to track which dropdown is clicked.
CodePudding user response:
You had a great start, but there were some things missing. Instead of having to add/remove the class by yourself, you can create a state for that and let React deal with it for you.
Also if you add an onFocus event on your div, you can manage when the dropdown will be open.
export default function App() {
const [option, setOption] = useState("");
const [isOpen, setIsOpen] = useState(false);
const close = () => {
setIsOpen(false);
}
const open = () => {
setIsOpen(true);
}
const selectValue = (event) => {
const value = event.target.value;
setOption(value);
close();
}
return (
<div className="App">
<h2>Option: {option}</h2>
<p>
Dropdown should close when I click outside it
<br />
Option Number should update
</p>
<div className="dropdown" tabIndex={0} onBlur={close} onFocus={open}>
<button onClick={open}>•••</button>
<div className={`list ${isOpen ? "display" : ""} `}>
<ul>
<li value={1} onClick={selectValue}>Option 1</li>
<li value={2} onClick={selectValue}>Option 2</li>
</ul>
</div>
</div>
</div>
);
}
CodePudding user response:
You could store the open state and conditionally add the display class. To detect click outside of div, you can pass a ref to the wrapper div and use a custom hook.
Note that you should avoid manipulating the dom directly in general, if you feel like you have to do it, it most likely mean that you should revisit your approach.
function useOnClickOutside(ref, cb) {
useEffect(() => {
function handleClickOutside(event) {
if (ref.current && !ref.current.contains(event.target)) {
cb();
}
}
// Bind the event listener
document.addEventListener("mousedown", handleClickOutside);
return () => {
// Unbind the event listener on clean up
document.removeEventListener("mousedown", handleClickOutside);
};
}, [ref, cb]);
}
function Dropdown({onSelect, values}) {
const [isOpen, setIsOpen] = useState(false);
const wrapperRef = useRef(null);
const close = () => {
setIsOpen(false);
};
useOnClickOutside(wrapperRef, close);
const toggle = () => {
setIsOpen(prev => !prev)
};
return (
<div className={`dropdown`} tabIndex={0}>
<button onClick={toggle}>•••</button>
<div ref={wrapperRef} style={{zIndex: 999}} className={`list ${isOpen ? 'display' : ''}`}>
<ul>
{values.map(v => <li
key={v}
onClick={() => {
onSelect(v)
setIsOpen(false)
}}>{"Option " v}</li>)}
</ul>
</div>
</div>
)
}
export default function App() {
const [option, setOption] = useState(null);
return (
<div className="App">
<h2>Option: {option}</h2>
<p>
Dropdown should close when I click outside it
<br/>
Option Number should update
</p>
<Dropdown onSelect={setOption} values={[1, 2, 3]}/>
<Dropdown onSelect={setOption} values={[4, 5, 6]}/>
<Dropdown onSelect={setOption} values={[6, 7, 8]}/>
</div>
);
}