diff --git a/src/calendar.tsx b/src/calendar.tsx index b5987d86e..b6fb15172 100644 --- a/src/calendar.tsx +++ b/src/calendar.tsx @@ -149,6 +149,9 @@ type CalendarProps = React.PropsWithChildren< > & Omit & Omit & { + selectsRange?: boolean; + startDate?: Date | null; + endDate?: Date | null; className?: string; container?: React.ElementType; showYearPicker?: boolean; @@ -205,7 +208,7 @@ type CalendarProps = React.PropsWithChildren< | React.KeyboardEvent | React.KeyboardEvent, ) => void; - onTimeChange?: TimeProps["onChange"] | InputTimeProps["onChange"]; + onTimeChange?: (time: Date, modifyDateType?: "start" | "end") => void; timeFormat?: TimeProps["format"]; timeIntervals?: TimeProps["intervals"]; } & ( @@ -1107,6 +1110,54 @@ export default class Calendar extends Component { }; renderInputTimeSection = (): React.ReactElement | undefined => { + if (!this.props.showTimeInput) { + return; + } + + // Handle selectsRange mode - render two time inputs + if (this.props.selectsRange) { + const { startDate, endDate } = this.props; + + const startTime = startDate ? new Date(startDate) : undefined; + const startTimeValid = + startTime && isValid(startTime) && Boolean(startDate); + const startTimeString = startTimeValid + ? `${addZero(startTime.getHours())}:${addZero(startTime.getMinutes())}` + : ""; + + const endTime = endDate ? new Date(endDate) : undefined; + const endTimeValid = endTime && isValid(endTime) && Boolean(endDate); + const endTimeString = endTimeValid + ? `${addZero(endTime.getHours())}:${addZero(endTime.getMinutes())}` + : ""; + + return ( + <> + { + this.props.onTimeChange?.(time, "start"); + }} + timeInputLabel={(this.props.timeInputLabel ?? "Time") + " (Start)"} + /> + { + this.props.onTimeChange?.(time, "end"); + }} + timeInputLabel={(this.props.timeInputLabel ?? "Time") + " (End)"} + /> + + ); + } + + // Single date mode (original behavior) const time = this.props.selected ? new Date(this.props.selected) : undefined; @@ -1114,18 +1165,17 @@ export default class Calendar extends Component { const timeString = timeValid ? `${addZero(time.getHours())}:${addZero(time.getMinutes())}` : ""; - if (this.props.showTimeInput) { - return ( - - ); - } - return; + return ( + { + this.props.onTimeChange?.(time); + }} + /> + ); }; renderAriaLiveRegion = (): React.ReactElement => { diff --git a/src/index.tsx b/src/index.tsx index 6906e112f..1d05c9cc5 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -963,7 +963,7 @@ export class DatePicker extends Component { this.setOpen(!this.state.open); }; - handleTimeChange = (time: Date): void => { + handleTimeChange = (time: Date, modifyDateType?: "start" | "end"): void => { if (this.props.selectsMultiple) { return; } @@ -972,39 +972,69 @@ export class DatePicker extends Component { if (selectsRange) { // In range mode, apply time to the appropriate date - // If we have a startDate but no endDate, apply time to startDate - // If we have both, apply time to endDate - const hasStartRange = startDate && !endDate; - - if (hasStartRange) { - // Apply time to startDate - const changedStartDate = setTime(startDate, { - hour: getHours(time), - minute: getMinutes(time), - }); - this.setState({ - preSelection: changedStartDate, - }); - onChange?.([changedStartDate, null], undefined); - } else if (startDate && endDate) { - // Apply time to endDate - const changedEndDate = setTime(endDate, { - hour: getHours(time), - minute: getMinutes(time), - }); - this.setState({ - preSelection: changedEndDate, - }); - onChange?.([startDate, changedEndDate], undefined); + // If modifyDateType is specified, use that to determine which date to modify + // Otherwise, use the legacy behavior: + // - If we have a startDate but no endDate, apply time to startDate + // - If we have both, apply time to endDate + + if (modifyDateType === "start") { + // Explicitly modify start date + if (startDate) { + const changedStartDate = setTime(startDate, { + hour: getHours(time), + minute: getMinutes(time), + }); + this.setState({ + preSelection: changedStartDate, + }); + onChange?.([changedStartDate, endDate ?? null], undefined); + } + } else if (modifyDateType === "end") { + // Explicitly modify end date + if (endDate) { + const changedEndDate = setTime(endDate, { + hour: getHours(time), + minute: getMinutes(time), + }); + this.setState({ + preSelection: changedEndDate, + }); + onChange?.([startDate ?? null, changedEndDate], undefined); + } } else { - // No dates selected yet, just update preSelection - const changedDate = setTime(this.getPreSelection(), { - hour: getHours(time), - minute: getMinutes(time), - }); - this.setState({ - preSelection: changedDate, - }); + // Legacy behavior for showTimeSelect (single time picker) + const hasStartRange = startDate && !endDate; + + if (hasStartRange) { + // Apply time to startDate + const changedStartDate = setTime(startDate, { + hour: getHours(time), + minute: getMinutes(time), + }); + this.setState({ + preSelection: changedStartDate, + }); + onChange?.([changedStartDate, null], undefined); + } else if (startDate && endDate) { + // Apply time to endDate + const changedEndDate = setTime(endDate, { + hour: getHours(time), + minute: getMinutes(time), + }); + this.setState({ + preSelection: changedEndDate, + }); + onChange?.([startDate, changedEndDate], undefined); + } else { + // No dates selected yet, just update preSelection + const changedDate = setTime(this.getPreSelection(), { + hour: getHours(time), + minute: getMinutes(time), + }); + this.setState({ + preSelection: changedDate, + }); + } } } else { // Single date mode (original behavior) diff --git a/src/test/datepicker_test.test.tsx b/src/test/datepicker_test.test.tsx index 268201ab1..166cf6ad3 100644 --- a/src/test/datepicker_test.test.tsx +++ b/src/test/datepicker_test.test.tsx @@ -6036,6 +6036,281 @@ describe("DatePicker", () => { }); }); + describe("showTimeInput with selectsRange", () => { + it("should render two time inputs when selectsRange is true", () => { + const startDate = newDate("2024-01-15 09:00:00"); + const endDate = newDate("2024-01-20 14:30:00"); + + const { container } = render( + , + ); + + const timeInputs = container.querySelectorAll( + ".react-datepicker__input-time-container", + ); + + // Should render two time input containers (one for start, one for end) + expect(timeInputs.length).toBe(2); + + // Check labels + const labels = container.querySelectorAll( + ".react-datepicker-time__caption", + ); + expect(labels.length).toBe(2); + expect(labels[0]?.textContent).toContain("Start"); + expect(labels[1]?.textContent).toContain("End"); + }); + + it("should apply time to startDate when start time input is changed", () => { + const startDate = newDate("2024-01-15 09:00:00"); + const endDate = newDate("2024-01-20 14:30:00"); + const onChange = jest.fn(); + + const { container } = render( + , + ); + + const timeInputs = container.querySelectorAll( + ".react-datepicker-time__input input", + ); + expect(timeInputs.length).toBe(2); + + // Change the start time input + fireEvent.change(timeInputs[0]!, { target: { value: "11:30" } }); + + expect(onChange).toHaveBeenCalledTimes(1); + const [changedStartDate, changedEndDate] = onChange.mock.calls[0][0]; + + // startDate should have the new time applied + expect(changedStartDate).toBeTruthy(); + expect(getHours(changedStartDate)).toBe(11); + expect(getMinutes(changedStartDate)).toBe(30); + + // endDate should remain unchanged + expect(changedEndDate).toBeTruthy(); + expect(getHours(changedEndDate)).toBe(14); + expect(getMinutes(changedEndDate)).toBe(30); + }); + + it("should apply time to endDate when end time input is changed", () => { + const startDate = newDate("2024-01-15 09:00:00"); + const endDate = newDate("2024-01-20 14:30:00"); + const onChange = jest.fn(); + + const { container } = render( + , + ); + + const timeInputs = container.querySelectorAll( + ".react-datepicker-time__input input", + ); + expect(timeInputs.length).toBe(2); + + // Change the end time input + fireEvent.change(timeInputs[1]!, { target: { value: "16:45" } }); + + expect(onChange).toHaveBeenCalledTimes(1); + const [changedStartDate, changedEndDate] = onChange.mock.calls[0][0]; + + // startDate should remain unchanged + expect(changedStartDate).toBeTruthy(); + expect(getHours(changedStartDate)).toBe(9); + expect(getMinutes(changedStartDate)).toBe(0); + + // endDate should have the new time applied + expect(changedEndDate).toBeTruthy(); + expect(getHours(changedEndDate)).toBe(16); + expect(getMinutes(changedEndDate)).toBe(45); + }); + + it("should render only one time input when selectsRange is false", () => { + const selected = newDate("2024-01-15 09:00:00"); + + const { container } = render( + , + ); + + const timeInputs = container.querySelectorAll( + ".react-datepicker__input-time-container", + ); + + // Should render only one time input container + expect(timeInputs.length).toBe(1); + }); + + it("should show correct initial time values in both inputs", () => { + const startDate = newDate("2024-01-15 09:30:00"); + const endDate = newDate("2024-01-20 14:45:00"); + + const { container } = render( + , + ); + + const timeInputs = container.querySelectorAll( + ".react-datepicker-time__input input", + ); + expect(timeInputs.length).toBe(2); + + // Check start time input value + expect(timeInputs[0]?.value).toBe("09:30"); + + // Check end time input value + expect(timeInputs[1]?.value).toBe("14:45"); + }); + + it("should handle case when only startDate is selected", () => { + const startDate = newDate("2024-01-15 09:00:00"); + const onChange = jest.fn(); + + const { container } = render( + , + ); + + const timeInputs = container.querySelectorAll( + ".react-datepicker-time__input input", + ); + expect(timeInputs.length).toBe(2); + + // Change the start time input + fireEvent.change(timeInputs[0]!, { target: { value: "11:30" } }); + + expect(onChange).toHaveBeenCalledTimes(1); + const [changedStartDate, changedEndDate] = onChange.mock.calls[0][0]; + + // startDate should have the new time applied + expect(changedStartDate).toBeTruthy(); + expect(getHours(changedStartDate)).toBe(11); + expect(getMinutes(changedStartDate)).toBe(30); + + // endDate should still be null + expect(changedEndDate).toBeNull(); + }); + + it("should not throw TypeError when changing time with selectsRange enabled", () => { + const startDate = newDate("2024-01-15 09:00:00"); + const endDate = newDate("2024-01-20 14:30:00"); + const onChange = jest.fn(); + + const { container } = render( + , + ); + + const timeInputs = container.querySelectorAll( + ".react-datepicker-time__input input", + ); + + // This should not throw any errors + expect(() => { + fireEvent.change(timeInputs[0]!, { target: { value: "11:30" } }); + }).not.toThrow(); + + expect(() => { + fireEvent.change(timeInputs[1]!, { target: { value: "16:45" } }); + }).not.toThrow(); + }); + + it("should apply time change in single date mode (non-range)", () => { + const selected = newDate("2024-01-15 09:00:00"); + const onChange = jest.fn(); + + const { container } = render( + , + ); + + const timeInputs = container.querySelectorAll( + ".react-datepicker-time__input input", + ); + expect(timeInputs.length).toBe(1); + + // Change the time input + fireEvent.change(timeInputs[0]!, { target: { value: "14:30" } }); + + expect(onChange).toHaveBeenCalledTimes(1); + const changedDate = onChange.mock.calls[0][0]; + + // Date should have the new time applied + expect(changedDate).toBeTruthy(); + expect(getHours(changedDate)).toBe(14); + expect(getMinutes(changedDate)).toBe(30); + }); + + it("should render two time inputs with empty values when no dates are selected in range mode", () => { + const { container } = render( + , + ); + + const timeInputs = container.querySelectorAll( + ".react-datepicker-time__input input", + ); + + // Should render two time input containers + expect(timeInputs.length).toBe(2); + + // Both inputs should have empty values since no dates are selected + expect(timeInputs[0]?.value).toBe(""); + expect(timeInputs[1]?.value).toBe(""); + }); + }); + describe("Critical functions coverage - best in class", () => { it("should handle handleTimeChange with selectsMultiple (line 942)", () => { const onChange = jest.fn();