Skip to content

Commit 9e4795d

Browse files
Merge pull request #6166 from Hacker0x01/fix/string-date-props-crash
fix: prevent crash when date props are passed as strings
2 parents f0057e2 + 149d547 commit 9e4795d

5 files changed

Lines changed: 188 additions & 7 deletions

File tree

src/date_utils.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,24 @@ export function isValid(date: Date): boolean {
368368
return isValidDate(date);
369369
}
370370

371+
/**
372+
* Safely returns a valid Date or null.
373+
* This handles cases where a value might be passed as a string or other
374+
* invalid type at runtime, even though TypeScript expects a Date.
375+
* @param date - The value to check (typed as Date but could be anything at runtime)
376+
* @returns The date if it's a valid Date object, otherwise null
377+
*/
378+
export function safeToDate(date: Date | null | undefined): Date | null {
379+
if (date == null) {
380+
return null;
381+
}
382+
// Check if it's actually a Date object AND is valid
383+
if (isDate(date) && isValidDate(date)) {
384+
return date;
385+
}
386+
return null;
387+
}
388+
371389
// ** Date Formatting **
372390

373391
/**

src/index.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import {
5252
isSameMinute,
5353
toZonedTime,
5454
fromZonedTime,
55+
safeToDate,
5556
type HighlightDate,
5657
type HolidayItem,
5758
type TimeZone,
@@ -783,8 +784,10 @@ export class DatePicker extends Component<DatePickerProps, DatePickerState> {
783784
strictParsing,
784785
)
785786
: null;
786-
const startChanged = startDate?.getTime() !== startDateNew?.getTime();
787-
const endChanged = endDate?.getTime() !== endDateNew?.getTime();
787+
const startChanged =
788+
safeToDate(startDate)?.getTime() !== startDateNew?.getTime();
789+
const endChanged =
790+
safeToDate(endDate)?.getTime() !== endDateNew?.getTime();
788791

789792
if (!startChanged && !endChanged) {
790793
return;
@@ -1231,7 +1234,7 @@ export class DatePicker extends Component<DatePickerProps, DatePickerState> {
12311234

12321235
handleTimeOnlyArrowKey = (eventKey: string): void => {
12331236
const currentTime =
1234-
this.props.selected || this.state.preSelection || newDate();
1237+
safeToDate(this.props.selected) || this.state.preSelection || newDate();
12351238
const timeIntervals = this.props.timeIntervals ?? 30;
12361239
const dateFormat =
12371240
this.props.dateFormat ?? DatePicker.defaultProps.dateFormat;
@@ -1293,7 +1296,7 @@ export class DatePicker extends Component<DatePickerProps, DatePickerState> {
12931296
const timeFormat = this.props.timeFormat || "p";
12941297

12951298
const defaultTime =
1296-
this.state.preSelection || this.props.selected || newDate();
1299+
this.state.preSelection || safeToDate(this.props.selected) || newDate();
12971300
const parsedDate = parseDate(
12981301
inputValue,
12991302
dateFormat,

src/test/date_utils_test.test.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import {
5656
registerLocale,
5757
isMonthYearDisabled,
5858
getDefaultLocale,
59+
safeToDate,
5960
} from "../date_utils";
6061

6162
registerLocale("pt-BR", ptBR);
@@ -1733,4 +1734,67 @@ describe("date_utils", () => {
17331734
expect(typeof result).toBe("boolean");
17341735
});
17351736
});
1737+
1738+
describe("safeToDate", () => {
1739+
it("returns the date when given a valid Date object", () => {
1740+
const date = new Date("2024-01-15");
1741+
const result = safeToDate(date);
1742+
expect(result).toBe(date);
1743+
});
1744+
1745+
it("returns null when given null", () => {
1746+
const result = safeToDate(null);
1747+
expect(result).toBeNull();
1748+
});
1749+
1750+
it("returns null when given undefined", () => {
1751+
const result = safeToDate(undefined);
1752+
expect(result).toBeNull();
1753+
});
1754+
1755+
it("returns null when given a string", () => {
1756+
// TypeScript types this as Date, but at runtime it could be a string
1757+
const result = safeToDate("2024-01-15" as unknown as Date);
1758+
expect(result).toBeNull();
1759+
});
1760+
1761+
it("returns null when given an invalid date string", () => {
1762+
const result = safeToDate("not-a-date" as unknown as Date);
1763+
expect(result).toBeNull();
1764+
});
1765+
1766+
it("returns null when given an Invalid Date object", () => {
1767+
const invalidDate = new Date("invalid");
1768+
expect(isValid(invalidDate)).toBe(false);
1769+
const result = safeToDate(invalidDate);
1770+
expect(result).toBeNull();
1771+
});
1772+
1773+
it("returns null when given a number", () => {
1774+
const result = safeToDate(1705276800000 as unknown as Date);
1775+
expect(result).toBeNull();
1776+
});
1777+
1778+
it("returns null when given an object that is not a Date", () => {
1779+
const result = safeToDate({ year: 2024, month: 1 } as unknown as Date);
1780+
expect(result).toBeNull();
1781+
});
1782+
1783+
it("returns the date when given a Date created from newDate()", () => {
1784+
const date = newDate();
1785+
const result = safeToDate(date);
1786+
expect(result).toBe(date);
1787+
});
1788+
1789+
it("returns the date when given a Date at epoch", () => {
1790+
const date = new Date(0);
1791+
const result = safeToDate(date);
1792+
expect(result).toBe(date);
1793+
});
1794+
1795+
it("returns null when given an empty string", () => {
1796+
const result = safeToDate("" as unknown as Date);
1797+
expect(result).toBeNull();
1798+
});
1799+
});
17361800
});

src/test/timepicker_test.test.tsx

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1206,4 +1206,95 @@ describe("TimePicker", () => {
12061206
expect(instance.state.open).toBe(true);
12071207
});
12081208
});
1209+
1210+
describe("handling invalid date prop types", () => {
1211+
it("should not crash when selected prop is a string instead of Date", () => {
1212+
// This tests the fix for issue #5964
1213+
// Some users may pass a string to selected prop at runtime
1214+
expect(() => {
1215+
render(
1216+
<TestDatePicker
1217+
inline
1218+
selected={"2024-01-15"}
1219+
showTimeSelect
1220+
timeIntervals={15}
1221+
/>,
1222+
);
1223+
}).not.toThrow();
1224+
});
1225+
1226+
it("should render time options when selected prop is a string", () => {
1227+
const { container } = render(
1228+
<TestDatePicker
1229+
inline
1230+
selected={"2024-01-15"}
1231+
showTimeSelect
1232+
timeIntervals={60}
1233+
/>,
1234+
);
1235+
1236+
const timeList = container.querySelector(".react-datepicker__time-list");
1237+
expect(timeList).not.toBeNull();
1238+
1239+
const timeItems = container.querySelectorAll(
1240+
".react-datepicker__time-list-item",
1241+
);
1242+
expect(timeItems.length).toBeGreaterThan(0);
1243+
});
1244+
1245+
it("should not crash when openToDate prop is a string instead of Date", () => {
1246+
expect(() => {
1247+
render(
1248+
<TestDatePicker
1249+
inline
1250+
openToDate={"2024-06-15"}
1251+
showTimeSelect
1252+
timeIntervals={15}
1253+
/>,
1254+
);
1255+
}).not.toThrow();
1256+
});
1257+
1258+
it("should fall back to current date when selected is an invalid string", () => {
1259+
const { container } = render(
1260+
<TestDatePicker
1261+
inline
1262+
selected={"not-a-valid-date"}
1263+
showTimeSelect
1264+
timeIntervals={60}
1265+
/>,
1266+
);
1267+
1268+
// Should still render time options (falling back to newDate())
1269+
const timeItems = container.querySelectorAll(
1270+
".react-datepicker__time-list-item",
1271+
);
1272+
expect(timeItems.length).toBeGreaterThan(0);
1273+
});
1274+
1275+
it("should allow selecting a time when selected was initially a string", () => {
1276+
let selectedDate: Date | null = null;
1277+
const handleChange = (date: Date | null) => {
1278+
selectedDate = date;
1279+
};
1280+
1281+
const { container } = render(
1282+
<TestDatePicker
1283+
inline
1284+
selected={"2024-01-15"}
1285+
onChange={handleChange}
1286+
showTimeSelect
1287+
timeIntervals={60}
1288+
/>,
1289+
);
1290+
1291+
const firstTimeItem = container.querySelector(
1292+
".react-datepicker__time-list-item",
1293+
);
1294+
expect(firstTimeItem).not.toBeNull();
1295+
1296+
fireEvent.click(firstTimeItem!);
1297+
expect(selectedDate).toBeInstanceOf(Date);
1298+
});
1299+
});
12091300
});

src/time.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
getHoursInDay,
1414
isSameMinute,
1515
getSeconds,
16+
safeToDate,
1617
type Locale,
1718
type TimeFilterOptions,
1819
KeyType,
@@ -139,8 +140,10 @@ export default class Time extends Component<TimeProps, TimeState> {
139140
this.props.onChange?.(time);
140141
};
141142

142-
isSelectedTime = (time: Date) =>
143-
this.props.selected && isSameMinute(this.props.selected, time);
143+
isSelectedTime = (time: Date) => {
144+
const selected = safeToDate(this.props.selected);
145+
return selected && isSameMinute(selected, time);
146+
};
144147

145148
isDisabledTime = (time: Date): boolean | undefined =>
146149
((this.props.minTime || this.props.maxTime) &&
@@ -218,7 +221,9 @@ export default class Time extends Component<TimeProps, TimeState> {
218221
const intervals = this.props.intervals ?? Time.defaultProps.intervals;
219222

220223
const activeDate =
221-
this.props.selected || this.props.openToDate || newDate();
224+
safeToDate(this.props.selected) ||
225+
safeToDate(this.props.openToDate) ||
226+
newDate();
222227

223228
const base = getStartOfDay(activeDate);
224229
const sortedInjectTimes =

0 commit comments

Comments
 (0)