Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion src/calendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ export default class Calendar extends Component<CalendarProps, CalendarState> {
componentDidUpdate(prevProps: CalendarProps) {
if (
this.props.preSelection &&
isValid(this.props.preSelection) &&
(!isSameDay(this.props.preSelection, prevProps.preSelection) ||
this.props.monthSelectedIn !== prevProps.monthSelectedIn)
) {
Expand Down Expand Up @@ -468,6 +469,11 @@ export default class Calendar extends Component<CalendarProps, CalendarState> {
};

header = (date: Date = this.state.date): React.ReactElement[] => {
// Return empty array if date is invalid
if (!isValid(date)) {
return [];
}

const disabled = this.props.disabled;
const startOfWeek = getStartOfWeek(
date,
Expand Down Expand Up @@ -781,7 +787,9 @@ export default class Calendar extends Component<CalendarProps, CalendarState> {
}
return (
<h2 className={classes.join(" ")}>
{formatDate(date, this.props.dateFormat, this.props.locale)}
{isValid(date)
? formatDate(date, this.props.dateFormat, this.props.locale)
: ""}
</h2>
);
};
Expand Down Expand Up @@ -1112,6 +1120,17 @@ export default class Calendar extends Component<CalendarProps, CalendarState> {
};

renderAriaLiveRegion = (): React.ReactElement => {
// Don't render aria-live message if date is invalid
if (!isValid(this.state.date)) {
return (
<span
role="alert"
aria-live="polite"
className="react-datepicker__aria-live"
/>
);
}

const { startPeriod, endPeriod } = getYearsPeriod(
this.state.date,
this.props.yearItemNumber ?? Calendar.defaultProps.yearItemNumber,
Expand Down
15 changes: 13 additions & 2 deletions src/month.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
isSameMonth,
isSameQuarter,
isSpaceKeyDown,
isValid,
newDate,
setMonth,
setQuarter,
Expand Down Expand Up @@ -447,6 +448,11 @@ export default class Month extends Component<MonthProps> {
};

renderWeeks = () => {
// Return empty array if day is invalid
if (!isValid(this.props.day)) {
return [];
}

const weeks = [];
const isFixedHeight = this.props.fixedHeight;

Expand Down Expand Up @@ -1138,6 +1144,11 @@ export default class Month extends Component<MonthProps> {
? ariaLabelPrefix.trim() + " "
: "";

// Format aria-label, return empty string if date is invalid
const formattedAriaLabel = isValid(day)
? `${formattedAriaLabelPrefix}${formatDate(day, "MMMM, yyyy", this.props.locale)}`
: "";

const shouldUseListboxRole = showMonthYearPicker || showQuarterYearPicker;

if (shouldUseListboxRole) {
Expand All @@ -1150,7 +1161,7 @@ export default class Month extends Component<MonthProps> {
onPointerLeave={
this.props.usePointerEvent ? this.handleMouseLeave : undefined
}
aria-label={`${formattedAriaLabelPrefix}${formatDate(day, "MMMM, yyyy", this.props.locale)}`}
aria-label={formattedAriaLabel}
role="listbox"
>
{showMonthYearPicker ? this.renderMonths() : this.renderQuarters()}
Expand All @@ -1172,7 +1183,7 @@ export default class Month extends Component<MonthProps> {
onPointerLeave={
this.props.usePointerEvent ? this.handleMouseLeave : undefined
}
aria-label={`${formattedAriaLabelPrefix}${formatDate(day, "MMMM, yyyy", this.props.locale)}`}
aria-label={formattedAriaLabel}
role="rowgroup"
>
{this.renderWeeks()}
Expand Down
3 changes: 2 additions & 1 deletion src/test/calendar_test.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
* @jest-environment jsdom
*/

import { render, fireEvent, act, waitFor } from "@testing-library/react";
import { render, fireEvent, waitFor } from "@testing-library/react";
import { act } from "react";
import {
setDate,
startOfMonth,
Expand Down
31 changes: 31 additions & 0 deletions src/test/click_outside_wrapper.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -194,4 +194,35 @@ describe("ClickOutsideWrapper", () => {

removeEventListenerSpy.mockRestore();
});

it("invokes handler registered on document with composedPath target", () => {
const addEventListenerSpy = jest.spyOn(document, "addEventListener");
const removeEventListenerSpy = jest.spyOn(document, "removeEventListener");

const { unmount } = render(
<ClickOutsideWrapper onClickOutside={onClickOutsideMock}>
<div>Inside</div>
</ClickOutsideWrapper>,
);

const handlerEntry = addEventListenerSpy.mock.calls.find(
([type]) => type === "mousedown",
);
const handler = handlerEntry?.[1] as EventListener;

const outsideNode = document.createElement("div");
const mockEvent = {
composed: true,
composedPath: () => [outsideNode],
target: outsideNode,
} as unknown as MouseEvent;

handler(mockEvent);

expect(onClickOutsideMock).toHaveBeenCalledTimes(1);

unmount();
addEventListenerSpy.mockRestore();
removeEventListenerSpy.mockRestore();
});
});
84 changes: 77 additions & 7 deletions src/test/date_utils_test.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -926,6 +926,41 @@ describe("date_utils", () => {
});
});

describe("isTimeInDisabledRange edge cases", () => {
it("throws when either minTime or maxTime is missing", () => {
expect(() =>
isTimeInDisabledRange(newDate(), { minTime: newDate() }),
).toThrow("Both minTime and maxTime props required");
});

it("returns false when isWithinInterval throws", async () => {
jest.doMock("date-fns", () => {
const actual = jest.requireActual("date-fns");
return {
...actual,
isWithinInterval: () => {
throw new Error("boom");
},
};
});

try {
const { isTimeInDisabledRange: mockedIsTimeInDisabledRange } =
await import("../date_utils");

expect(
mockedIsTimeInDisabledRange(new Date(), {
minTime: new Date(),
maxTime: new Date(),
}),
).toBe(false);
} finally {
jest.resetModules();
jest.dontMock("date-fns");
}
});
});

describe("isDayInRange", () => {
it("should tell if day is in range", () => {
const day = newDate("2016-02-15 09:40");
Expand Down Expand Up @@ -1105,6 +1140,14 @@ describe("date_utils", () => {

expect(isMonthInRange(startDate, endDate, 5, day)).toBe(true);
});

it("should return false when the start date is after the end date", () => {
const day = newDate("2024-01-01");
const startDate = newDate("2024-02-01");
const endDate = newDate("2024-01-01");

expect(isMonthInRange(startDate, endDate, 1, day)).toBe(false);
});
});

describe("getStartOfYear", () => {
Expand Down Expand Up @@ -1139,6 +1182,14 @@ describe("date_utils", () => {

expect(isQuarterInRange(startDate, endDate, 5, day)).toBe(true);
});

it("should return false when the start quarter is after the end quarter", () => {
const day = newDate("2024-01-01");
const startDate = newDate("2024-10-01");
const endDate = newDate("2024-04-01");

expect(isQuarterInRange(startDate, endDate, 1, day)).toBe(false);
});
});

describe("isYearInRange", () => {
Expand Down Expand Up @@ -1580,14 +1631,33 @@ describe("date_utils", () => {
});

describe("isDayInRange error handling", () => {
it("returns false when isWithinInterval throws", () => {
const testDate = new Date("2024-01-15");
const invalidStartDate = new Date("invalid");
const invalidEndDate = new Date("also-invalid");

const result = isDayInRange(testDate, invalidStartDate, invalidEndDate);
it("returns false when isWithinInterval throws", async () => {
jest.doMock("date-fns", () => {
const actual = jest.requireActual("date-fns");
return {
...actual,
isWithinInterval: () => {
throw new Error("boom");
},
};
});

expect(result).toBe(false);
try {
const { isDayInRange: mockedIsDayInRange } = await import(
"../date_utils"
);

expect(
mockedIsDayInRange(
new Date("2024-01-15"),
new Date("2024-01-10"),
new Date("2024-01-20"),
),
).toBe(false);
} finally {
jest.resetModules();
jest.dontMock("date-fns");
}
});

it("returns true for dates inside a valid range", () => {
Expand Down
Loading
Loading