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
18 changes: 18 additions & 0 deletions src/date_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,24 @@ export function isValid(date: Date): boolean {
return isValidDate(date);
}

/**
* Safely returns a valid Date or null.
* This handles cases where a value might be passed as a string or other
* invalid type at runtime, even though TypeScript expects a Date.
* @param date - The value to check (typed as Date but could be anything at runtime)
* @returns The date if it's a valid Date object, otherwise null
*/
export function safeToDate(date: Date | null | undefined): Date | null {
if (date == null) {
return null;
}
// Check if it's actually a Date object AND is valid
if (isDate(date) && isValidDate(date)) {
return date;
}
return null;
}

// ** Date Formatting **

/**
Expand Down
11 changes: 7 additions & 4 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import {
isSameMinute,
toZonedTime,
fromZonedTime,
safeToDate,
type HighlightDate,
type HolidayItem,
type TimeZone,
Expand Down Expand Up @@ -783,8 +784,10 @@ export class DatePicker extends Component<DatePickerProps, DatePickerState> {
strictParsing,
)
: null;
const startChanged = startDate?.getTime() !== startDateNew?.getTime();
const endChanged = endDate?.getTime() !== endDateNew?.getTime();
const startChanged =
safeToDate(startDate)?.getTime() !== startDateNew?.getTime();
const endChanged =
safeToDate(endDate)?.getTime() !== endDateNew?.getTime();

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

handleTimeOnlyArrowKey = (eventKey: string): void => {
const currentTime =
this.props.selected || this.state.preSelection || newDate();
safeToDate(this.props.selected) || this.state.preSelection || newDate();
const timeIntervals = this.props.timeIntervals ?? 30;
const dateFormat =
this.props.dateFormat ?? DatePicker.defaultProps.dateFormat;
Expand Down Expand Up @@ -1293,7 +1296,7 @@ export class DatePicker extends Component<DatePickerProps, DatePickerState> {
const timeFormat = this.props.timeFormat || "p";

const defaultTime =
this.state.preSelection || this.props.selected || newDate();
this.state.preSelection || safeToDate(this.props.selected) || newDate();
const parsedDate = parseDate(
inputValue,
dateFormat,
Expand Down
64 changes: 64 additions & 0 deletions src/test/date_utils_test.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import {
registerLocale,
isMonthYearDisabled,
getDefaultLocale,
safeToDate,
} from "../date_utils";

registerLocale("pt-BR", ptBR);
Expand Down Expand Up @@ -1733,4 +1734,67 @@ describe("date_utils", () => {
expect(typeof result).toBe("boolean");
});
});

describe("safeToDate", () => {
it("returns the date when given a valid Date object", () => {
const date = new Date("2024-01-15");
const result = safeToDate(date);
expect(result).toBe(date);
});

it("returns null when given null", () => {
const result = safeToDate(null);
expect(result).toBeNull();
});

it("returns null when given undefined", () => {
const result = safeToDate(undefined);
expect(result).toBeNull();
});

it("returns null when given a string", () => {
// TypeScript types this as Date, but at runtime it could be a string
const result = safeToDate("2024-01-15" as unknown as Date);
expect(result).toBeNull();
});

it("returns null when given an invalid date string", () => {
const result = safeToDate("not-a-date" as unknown as Date);
expect(result).toBeNull();
});

it("returns null when given an Invalid Date object", () => {
const invalidDate = new Date("invalid");
expect(isValid(invalidDate)).toBe(false);
const result = safeToDate(invalidDate);
expect(result).toBeNull();
});

it("returns null when given a number", () => {
const result = safeToDate(1705276800000 as unknown as Date);
expect(result).toBeNull();
});

it("returns null when given an object that is not a Date", () => {
const result = safeToDate({ year: 2024, month: 1 } as unknown as Date);
expect(result).toBeNull();
});

it("returns the date when given a Date created from newDate()", () => {
const date = newDate();
const result = safeToDate(date);
expect(result).toBe(date);
});

it("returns the date when given a Date at epoch", () => {
const date = new Date(0);
const result = safeToDate(date);
expect(result).toBe(date);
});

it("returns null when given an empty string", () => {
const result = safeToDate("" as unknown as Date);
expect(result).toBeNull();
});
});
});
91 changes: 91 additions & 0 deletions src/test/timepicker_test.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1206,4 +1206,95 @@ describe("TimePicker", () => {
expect(instance.state.open).toBe(true);
});
});

describe("handling invalid date prop types", () => {
it("should not crash when selected prop is a string instead of Date", () => {
// This tests the fix for issue #5964
// Some users may pass a string to selected prop at runtime
expect(() => {
render(
<TestDatePicker
inline
selected={"2024-01-15"}
showTimeSelect
timeIntervals={15}
/>,
);
}).not.toThrow();
});

it("should render time options when selected prop is a string", () => {
const { container } = render(
<TestDatePicker
inline
selected={"2024-01-15"}
showTimeSelect
timeIntervals={60}
/>,
);

const timeList = container.querySelector(".react-datepicker__time-list");
expect(timeList).not.toBeNull();

const timeItems = container.querySelectorAll(
".react-datepicker__time-list-item",
);
expect(timeItems.length).toBeGreaterThan(0);
});

it("should not crash when openToDate prop is a string instead of Date", () => {
expect(() => {
render(
<TestDatePicker
inline
openToDate={"2024-06-15"}
showTimeSelect
timeIntervals={15}
/>,
);
}).not.toThrow();
});

it("should fall back to current date when selected is an invalid string", () => {
const { container } = render(
<TestDatePicker
inline
selected={"not-a-valid-date"}
showTimeSelect
timeIntervals={60}
/>,
);

// Should still render time options (falling back to newDate())
const timeItems = container.querySelectorAll(
".react-datepicker__time-list-item",
);
expect(timeItems.length).toBeGreaterThan(0);
});

it("should allow selecting a time when selected was initially a string", () => {
let selectedDate: Date | null = null;
const handleChange = (date: Date | null) => {
selectedDate = date;
};

const { container } = render(
<TestDatePicker
inline
selected={"2024-01-15"}
onChange={handleChange}
showTimeSelect
timeIntervals={60}
/>,
);

const firstTimeItem = container.querySelector(
".react-datepicker__time-list-item",
);
expect(firstTimeItem).not.toBeNull();

fireEvent.click(firstTimeItem!);
expect(selectedDate).toBeInstanceOf(Date);
});
});
});
11 changes: 8 additions & 3 deletions src/time.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
getHoursInDay,
isSameMinute,
getSeconds,
safeToDate,
type Locale,
type TimeFilterOptions,
KeyType,
Expand Down Expand Up @@ -139,8 +140,10 @@ export default class Time extends Component<TimeProps, TimeState> {
this.props.onChange?.(time);
};

isSelectedTime = (time: Date) =>
this.props.selected && isSameMinute(this.props.selected, time);
isSelectedTime = (time: Date) => {
const selected = safeToDate(this.props.selected);
return selected && isSameMinute(selected, time);
};

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

const activeDate =
this.props.selected || this.props.openToDate || newDate();
safeToDate(this.props.selected) ||
safeToDate(this.props.openToDate) ||
newDate();

const base = getStartOfDay(activeDate);
const sortedInjectTimes =
Expand Down
Loading