Skip to content

Commit c834746

Browse files
Merge pull request #5060 from laug/parse-date-range
Parse date range
2 parents c5468e4 + 55f49a0 commit c834746

File tree

3 files changed

+151
-27
lines changed

3 files changed

+151
-27
lines changed

src/date_utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -794,7 +794,7 @@ export function isDayDisabled(
794794
if (excludeDate instanceof Date) {
795795
return isSameDay(day, excludeDate);
796796
} else {
797-
return isSameDay(day, excludeDate.date ?? new Date());
797+
return isSameDay(day, excludeDate.date);
798798
}
799799
})) ||
800800
(excludeDateIntervals &&

src/index.tsx

Lines changed: 72 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -565,6 +565,7 @@ export default class DatePicker extends Component<
565565
}
566566
};
567567

568+
// handleChange is called when user types in the textbox
568569
handleChange = (
569570
...allArgs: Parameters<Required<DatePickerProps>["onChangeRaw"]>
570571
) => {
@@ -585,36 +586,77 @@ export default class DatePicker extends Component<
585586
event?.target instanceof HTMLInputElement ? event.target.value : null,
586587
lastPreSelectChange: PRESELECT_CHANGE_VIA_INPUT,
587588
});
589+
588590
const {
589591
dateFormat = DatePicker.defaultProps.dateFormat,
590592
strictParsing = DatePicker.defaultProps.strictParsing,
593+
selectsRange,
594+
startDate,
595+
endDate,
591596
} = this.props;
592-
let date = parseDate(
593-
event?.target instanceof HTMLInputElement ? event.target.value : "",
594-
dateFormat,
595-
this.props.locale,
596-
strictParsing,
597-
this.props.minDate,
598-
);
599-
// Use date from `selected` prop when manipulating only time for input value
600-
if (
601-
this.props.showTimeSelectOnly &&
602-
this.props.selected &&
603-
date &&
604-
!isSameDay(date, this.props.selected)
605-
) {
606-
date = set(this.props.selected, {
607-
hours: getHours(date),
608-
minutes: getMinutes(date),
609-
seconds: getSeconds(date),
610-
});
611-
}
612-
if (
613-
date ||
614-
!(event?.target instanceof HTMLInputElement) ||
615-
!event?.target.value
616-
) {
617-
this.setSelected(date, event, true);
597+
598+
const value =
599+
event?.target instanceof HTMLInputElement ? event.target.value : "";
600+
601+
if (selectsRange) {
602+
const [valueStart, valueEnd] = value
603+
.split("-", 2)
604+
.map((val) => val.trim());
605+
const startDateNew = parseDate(
606+
valueStart ?? "",
607+
dateFormat,
608+
this.props.locale,
609+
strictParsing,
610+
);
611+
const endDateNew = parseDate(
612+
valueEnd ?? "",
613+
dateFormat,
614+
this.props.locale,
615+
strictParsing,
616+
);
617+
const startChanged = startDate?.getTime() !== startDateNew?.getTime();
618+
const endChanged = endDate?.getTime() !== endDateNew?.getTime();
619+
620+
if (!startChanged && !endChanged) {
621+
return;
622+
}
623+
624+
if (startDateNew && isDayDisabled(startDateNew, this.props)) {
625+
return;
626+
}
627+
if (endDateNew && isDayDisabled(endDateNew, this.props)) {
628+
return;
629+
}
630+
631+
this.props.onChange?.([startDateNew, endDateNew], event);
632+
} else {
633+
// not selectsRange
634+
let date = parseDate(
635+
value,
636+
dateFormat,
637+
this.props.locale,
638+
strictParsing,
639+
this.props.minDate,
640+
);
641+
642+
// Use date from `selected` prop when manipulating only time for input value
643+
if (
644+
this.props.showTimeSelectOnly &&
645+
this.props.selected &&
646+
date &&
647+
!isSameDay(date, this.props.selected)
648+
) {
649+
date = set(this.props.selected, {
650+
hours: getHours(date),
651+
minutes: getMinutes(date),
652+
seconds: getSeconds(date),
653+
});
654+
}
655+
656+
// Update selection if either (1) date was successfully parsed, or (2) input field is empty
657+
if (date || !value) {
658+
this.setSelected(date, event, true);
659+
}
618660
}
619661
};
620662

@@ -654,6 +696,7 @@ export default class DatePicker extends Component<
654696
}
655697
};
656698

699+
// setSelected is called either from handleChange (user typed date into textbox and it was parsed) or handleSelect (user selected date from calendar using mouse or keyboard)
657700
setSelected = (
658701
date: Date | null,
659702
event?: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>,
@@ -662,6 +705,7 @@ export default class DatePicker extends Component<
662705
) => {
663706
let changedDate = date;
664707

708+
// Early return if selected year/month/day is disabled
665709
if (this.props.showYearPicker) {
666710
if (
667711
changedDate !== null &&
@@ -697,6 +741,7 @@ export default class DatePicker extends Component<
697741
selectsMultiple
698742
) {
699743
if (changedDate !== null) {
744+
// Preserve previously selected time if only date is currently being changed
700745
if (
701746
this.props.selected &&
702747
(!keepInput ||
@@ -734,6 +779,7 @@ export default class DatePicker extends Component<
734779
this.setState({ monthSelectedIn: monthSelectedIn });
735780
}
736781
}
782+
737783
if (selectsRange) {
738784
const noRanges = !startDate && !endDate;
739785
const hasStartRange = startDate && !endDate;

src/test/datepicker_test.test.tsx

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3221,6 +3221,84 @@ describe("DatePicker", () => {
32213221

32223222
expect(onCalendarCloseSpy).toHaveBeenCalled();
32233223
});
3224+
3225+
it("should select start date and end date if user inputs the range manually in the input box", () => {
3226+
const onChangeSpy = jest.fn();
3227+
let instance: DatePicker | null = null;
3228+
const { container } = render(
3229+
<DatePicker
3230+
selectsRange
3231+
startDate={undefined}
3232+
endDate={undefined}
3233+
onChange={onChangeSpy}
3234+
excludeDates={[newDate("2024-01-01")]}
3235+
ref={(node) => {
3236+
instance = node;
3237+
}}
3238+
/>,
3239+
);
3240+
3241+
expect(instance).toBeTruthy();
3242+
const input = safeQuerySelector<HTMLInputElement>(container, "input");
3243+
fireEvent.change(input, {
3244+
target: {
3245+
value: "03/04/2024 - 05/06/2024",
3246+
},
3247+
});
3248+
3249+
// cover `if (startDateNew && isDayDisabled(startDateNew, this.props))` block
3250+
fireEvent.change(input, {
3251+
target: {
3252+
value: "01/01/2024-05/06/2024",
3253+
},
3254+
});
3255+
3256+
// cover `if (endDateNew && isDayDisabled(endDateNew, this.props))` block
3257+
fireEvent.change(input, {
3258+
target: {
3259+
value: "03/04/2023-01/01/2024",
3260+
},
3261+
});
3262+
3263+
expect(onChangeSpy).toHaveBeenCalledTimes(1);
3264+
expect(Array.isArray(onChangeSpy.mock.calls[0][0])).toBe(true);
3265+
expect(onChangeSpy.mock.calls[0][0][0]).toBeTruthy();
3266+
expect(onChangeSpy.mock.calls[0][0][1]).toBeTruthy();
3267+
expect(formatDate(onChangeSpy.mock.calls[0][0][0], "MM/dd/yyyy")).toBe(
3268+
"03/04/2024",
3269+
);
3270+
expect(formatDate(onChangeSpy.mock.calls[0][0][1], "MM/dd/yyyy")).toBe(
3271+
"05/06/2024",
3272+
);
3273+
});
3274+
3275+
it("should not fire onChange a second time if user edits text box without the parsing result changing", () => {
3276+
const onChangeSpy = jest.fn();
3277+
let instance: DatePicker | null = null;
3278+
const { container } = render(
3279+
<DatePicker
3280+
selectsRange
3281+
startDate={newDate("2024-03-04")}
3282+
endDate={newDate("2024-05-06")}
3283+
onChange={onChangeSpy}
3284+
ref={(node) => {
3285+
instance = node;
3286+
}}
3287+
/>,
3288+
);
3289+
3290+
expect(instance).toBeTruthy();
3291+
const input = safeQuerySelector<HTMLInputElement>(container, "input");
3292+
3293+
// cover `if (!startChanged && !endChanged)` block
3294+
fireEvent.change(input, {
3295+
target: {
3296+
value: "03/04/2024-05/06/2024",
3297+
},
3298+
});
3299+
3300+
expect(onChangeSpy).not.toHaveBeenCalled();
3301+
});
32243302
});
32253303

32263304
describe("duplicate dates when multiple months", () => {

0 commit comments

Comments
 (0)