Skip to content

Commit

Permalink
Merge pull request #5060 from laug/parse-date-range
Browse files Browse the repository at this point in the history
Parse date range
  • Loading branch information
martijnrusschen authored Aug 29, 2024
2 parents c5468e4 + 55f49a0 commit c834746
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 27 deletions.
2 changes: 1 addition & 1 deletion src/date_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -794,7 +794,7 @@ export function isDayDisabled(
if (excludeDate instanceof Date) {
return isSameDay(day, excludeDate);
} else {
return isSameDay(day, excludeDate.date ?? new Date());
return isSameDay(day, excludeDate.date);
}
})) ||
(excludeDateIntervals &&
Expand Down
98 changes: 72 additions & 26 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,7 @@ export default class DatePicker extends Component<
}
};

// handleChange is called when user types in the textbox
handleChange = (
...allArgs: Parameters<Required<DatePickerProps>["onChangeRaw"]>
) => {
Expand All @@ -585,36 +586,77 @@ export default class DatePicker extends Component<
event?.target instanceof HTMLInputElement ? event.target.value : null,
lastPreSelectChange: PRESELECT_CHANGE_VIA_INPUT,
});

const {
dateFormat = DatePicker.defaultProps.dateFormat,
strictParsing = DatePicker.defaultProps.strictParsing,
selectsRange,
startDate,
endDate,
} = this.props;
let date = parseDate(
event?.target instanceof HTMLInputElement ? event.target.value : "",
dateFormat,
this.props.locale,
strictParsing,
this.props.minDate,
);
// Use date from `selected` prop when manipulating only time for input value
if (
this.props.showTimeSelectOnly &&
this.props.selected &&
date &&
!isSameDay(date, this.props.selected)
) {
date = set(this.props.selected, {
hours: getHours(date),
minutes: getMinutes(date),
seconds: getSeconds(date),
});
}
if (
date ||
!(event?.target instanceof HTMLInputElement) ||
!event?.target.value
) {
this.setSelected(date, event, true);

const value =
event?.target instanceof HTMLInputElement ? event.target.value : "";

if (selectsRange) {
const [valueStart, valueEnd] = value
.split("-", 2)
.map((val) => val.trim());
const startDateNew = parseDate(
valueStart ?? "",
dateFormat,
this.props.locale,
strictParsing,
);
const endDateNew = parseDate(
valueEnd ?? "",
dateFormat,
this.props.locale,
strictParsing,
);
const startChanged = startDate?.getTime() !== startDateNew?.getTime();
const endChanged = endDate?.getTime() !== endDateNew?.getTime();

if (!startChanged && !endChanged) {
return;
}

if (startDateNew && isDayDisabled(startDateNew, this.props)) {
return;
}
if (endDateNew && isDayDisabled(endDateNew, this.props)) {
return;
}

this.props.onChange?.([startDateNew, endDateNew], event);
} else {
// not selectsRange
let date = parseDate(
value,
dateFormat,
this.props.locale,
strictParsing,
this.props.minDate,
);

// Use date from `selected` prop when manipulating only time for input value
if (
this.props.showTimeSelectOnly &&
this.props.selected &&
date &&
!isSameDay(date, this.props.selected)
) {
date = set(this.props.selected, {
hours: getHours(date),
minutes: getMinutes(date),
seconds: getSeconds(date),
});
}

// Update selection if either (1) date was successfully parsed, or (2) input field is empty
if (date || !value) {
this.setSelected(date, event, true);
}
}
};

Expand Down Expand Up @@ -654,6 +696,7 @@ export default class DatePicker extends Component<
}
};

// 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)
setSelected = (
date: Date | null,
event?: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>,
Expand All @@ -662,6 +705,7 @@ export default class DatePicker extends Component<
) => {
let changedDate = date;

// Early return if selected year/month/day is disabled
if (this.props.showYearPicker) {
if (
changedDate !== null &&
Expand Down Expand Up @@ -697,6 +741,7 @@ export default class DatePicker extends Component<
selectsMultiple
) {
if (changedDate !== null) {
// Preserve previously selected time if only date is currently being changed
if (
this.props.selected &&
(!keepInput ||
Expand Down Expand Up @@ -734,6 +779,7 @@ export default class DatePicker extends Component<
this.setState({ monthSelectedIn: monthSelectedIn });
}
}

if (selectsRange) {
const noRanges = !startDate && !endDate;
const hasStartRange = startDate && !endDate;
Expand Down
78 changes: 78 additions & 0 deletions src/test/datepicker_test.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3221,6 +3221,84 @@ describe("DatePicker", () => {

expect(onCalendarCloseSpy).toHaveBeenCalled();
});

it("should select start date and end date if user inputs the range manually in the input box", () => {
const onChangeSpy = jest.fn();
let instance: DatePicker | null = null;
const { container } = render(
<DatePicker
selectsRange
startDate={undefined}
endDate={undefined}
onChange={onChangeSpy}
excludeDates={[newDate("2024-01-01")]}
ref={(node) => {
instance = node;
}}
/>,
);

expect(instance).toBeTruthy();
const input = safeQuerySelector<HTMLInputElement>(container, "input");
fireEvent.change(input, {
target: {
value: "03/04/2024 - 05/06/2024",
},
});

// cover `if (startDateNew && isDayDisabled(startDateNew, this.props))` block
fireEvent.change(input, {
target: {
value: "01/01/2024-05/06/2024",
},
});

// cover `if (endDateNew && isDayDisabled(endDateNew, this.props))` block
fireEvent.change(input, {
target: {
value: "03/04/2023-01/01/2024",
},
});

expect(onChangeSpy).toHaveBeenCalledTimes(1);
expect(Array.isArray(onChangeSpy.mock.calls[0][0])).toBe(true);
expect(onChangeSpy.mock.calls[0][0][0]).toBeTruthy();
expect(onChangeSpy.mock.calls[0][0][1]).toBeTruthy();
expect(formatDate(onChangeSpy.mock.calls[0][0][0], "MM/dd/yyyy")).toBe(
"03/04/2024",
);
expect(formatDate(onChangeSpy.mock.calls[0][0][1], "MM/dd/yyyy")).toBe(
"05/06/2024",
);
});

it("should not fire onChange a second time if user edits text box without the parsing result changing", () => {
const onChangeSpy = jest.fn();
let instance: DatePicker | null = null;
const { container } = render(
<DatePicker
selectsRange
startDate={newDate("2024-03-04")}
endDate={newDate("2024-05-06")}
onChange={onChangeSpy}
ref={(node) => {
instance = node;
}}
/>,
);

expect(instance).toBeTruthy();
const input = safeQuerySelector<HTMLInputElement>(container, "input");

// cover `if (!startChanged && !endChanged)` block
fireEvent.change(input, {
target: {
value: "03/04/2024-05/06/2024",
},
});

expect(onChangeSpy).not.toHaveBeenCalled();
});
});

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

0 comments on commit c834746

Please sign in to comment.