Calendar reopens after clicking outside: resetInputValue writes a stale state snapshot on blur
Describe the bug
resetInputValue writes a full snapshot of this.state back through setState:
https://github.com/Hacker0x01/react-datepicker/blob/main/src/index.tsx#L702-L707
resetInputValue = () => {
this.setState({
...this.state,
inputValue: null,
});
};handleBlur calls it. When the user clicks away, two things happen in one task:
- The document
mousedownlistener inClickOutsideWrapperrunshandleCalendarClickOutside, which callssetOpen(false). - Focus leaves the input, so
handleBlurruns.
That listener is a plain addEventListener, so React has not committed the open: false update yet when step 2 runs. this.state still holds open: true, and the snapshot spread writes it back. The calendar reopens immediately after closing and stays on screen, covering whatever is below it.
Only inputValue needs to be set here. Spreading this.state is what causes the problem:
resetInputValue = () => {
this.setState({ inputValue: null });
};To Reproduce
Failing test, jsdom + React 19 + @testing-library/react 16. No other dependencies:
import { useState } from "react";
import { render, act } from "@testing-library/react";
import DatePicker from "react-datepicker";
const Fixture = () => {
const [date, setDate] = useState<Date | null>(null);
return (
<div>
<DatePicker id="start" selected={date} onChange={setDate} />
<button id="outside">outside</button>
</div>
);
};
const openCalendars = () => document.querySelectorAll(".react-datepicker");
it("closes when an outside mousedown and the input blur land in the same task", () => {
render(<Fixture />);
const input = document.querySelector("#start") as HTMLInputElement;
const outside = document.querySelector("#outside") as HTMLButtonElement;
act(() => {
input.focus();
input.dispatchEvent(new FocusEvent("focusin", { bubbles: true }));
});
expect(openCalendars()).toHaveLength(1);
// A real browser dispatches the outside mousedown and the resulting input
// blur in one task, so React has not committed setOpen(false) yet when the
// blur handler runs. Both inside one act() reproduces that ordering.
act(() => {
outside.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
input.dispatchEvent(new FocusEvent("focusout", { bubbles: true }));
});
expect(openCalendars()).toHaveLength(0);
});On 9.1.0 this fails with Expected length: 0, Received length: 1. With the one-line change above it passes.
Note that userEvent.click does not reproduce it. userEvent awaits between pointer events, which lets React commit open: false before the blur handler reads this.state. The two events have to land in the same task, as they do in a browser.
Expected behavior
Clicking outside the calendar closes it and it stays closed.
Desktop
- OS: macOS 15
- Browser: Chrome / Electron 118
- Version: react-datepicker 9.1.0, React 19.2.7
Additional context
Introduced in 8.5.0 by 58f4afc5 ("Reset inputValue back to null on blur of input (even when the calendar popup is not open)"). resetInputValue does not exist in 8.4.0 or earlier — I checked the published tarballs from 7.6.0 through 9.1.0. The line is unchanged on main today.
It likely became easier to hit in 9.x: PR #6180 added the masked-input clearing work to handleBlur immediately before the resetInputValue() call.
This is not #6292. There the outside element calls stopPropagation() on mousedown so the document listener never fires. Here the listener fires and closes the calendar correctly, and the blur handler then reopens it.
We hit this in a Chakra UI app after upgrading 7.6.0 to 9.1.0: five browser tests started failing because a date picker stayed open over a <select> beneath it. Reverting that one line fixes all of them with no change to our own code.
Source: Hacker0x01/react-datepicker