Skip to content
Closed
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
7 changes: 7 additions & 0 deletions docs-site/src/components/Examples/config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import CustomCalendarClassName from "../../examples/ts/customCalendarClassName?r
import CustomClassName from "../../examples/ts/customClassName?raw";
import CustomDayClassName from "../../examples/ts/customDayClassName?raw";
import CustomDateFormat from "../../examples/ts/customDateFormat?raw";
import IntlDateFormat from "../../examples/ts/intlDateFormat?raw";
import CustomTimeClassName from "../../examples/ts/customTimeClassName?raw";
import CustomTimeInput from "../../examples/ts/customTimeInput?raw";
import DateRange from "../../examples/ts/dateRange?raw";
Expand Down Expand Up @@ -234,6 +235,12 @@ export const EXAMPLE_CONFIG: IExampleConfig[] = [
title: "Custom Date Format",
component: CustomDateFormat,
},
{
title: "Custom Date Format (Intl.DateTimeFormat)",
description:
"Use formatDateDisplay to format dates with Intl.DateTimeFormat or any custom logic. The dateFormat prop is still used for parsing typed input.",
component: IntlDateFormat,
},
{
title: "Custom Time Class Name",
component: CustomTimeClassName,
Expand Down
21 changes: 21 additions & 0 deletions docs-site/src/examples/ts/intlDateFormat.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// undefined falls back to the browser's default locale
const formatter = new Intl.DateTimeFormat(undefined, {
weekday: "short",
year: "numeric",
month: "long",
day: "numeric",
});

const IntlDateFormat = () => {
const [selectedDate, setSelectedDate] = useState<Date | null>(new Date());

return (
<DatePicker
formatDateDisplay={(date) => formatter.format(date)}
selected={selectedDate}
onChange={setSelectedDate}
/>
);
};

render(IntlDateFormat);
9 changes: 7 additions & 2 deletions src/date_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -520,19 +520,24 @@ export function safeMultipleDatesFormat(
dateFormat: string | string[];
locale?: Locale;
timeZone?: TimeZone;
formatDateDisplay?: (date: Date) => string;
},
): string {
if (!dates?.length) {
return "";
}

const formattedFirstDate = dates[0] ? safeDateFormat(dates[0], props) : "";
const formatDate = props.formatDateDisplay
? (date: Date) => props.formatDateDisplay!(date)
: (date: Date) => safeDateFormat(date, props);

const formattedFirstDate = dates[0] ? formatDate(dates[0]) : "";
if (dates.length === 1) {
return formattedFirstDate;
}

if (dates.length === 2 && dates[1]) {
const formattedSecondDate = safeDateFormat(dates[1], props);
const formattedSecondDate = formatDate(dates[1]);
return `${formattedFirstDate}, ${formattedSecondDate}`;
}

Expand Down
38 changes: 22 additions & 16 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import {
parseDateForNavigation,
formatDate,
safeDateFormat,
safeDateRangeFormat,
getHighLightDaysMap,
getYear,
getMonth,
Expand Down Expand Up @@ -172,6 +171,8 @@ export type DatePickerProps = OmitUnion<
className?: string;
customInput?: Parameters<typeof cloneElement>[0];
dateFormat?: string | string[];
/** Custom function to format dates for input display. When provided, overrides dateFormat for display only — dateFormat is still used for parsing typed input. */
formatDateDisplay?: (date: Date) => string;
showDateSelect?: boolean;
highlightDates?: (Date | HighlightDate)[];
onCalendarOpen?: VoidFunction;
Expand Down Expand Up @@ -524,6 +525,7 @@ export class DatePicker extends Component<DatePickerProps, DatePickerState> {

getInputValue = (): string => {
const {
formatDateDisplay,
locale,
startDate,
endDate,
Expand All @@ -545,30 +547,34 @@ export class DatePicker extends Component<DatePickerProps, DatePickerState> {
return value;
} else if (typeof inputValue === "string") {
return inputValue;
} else if (selectsRange) {
return safeDateRangeFormat(startDate, endDate, {
dateFormat,
locale,
rangeSeparator,
timeZone,
});
}

const formatSingleDate = formatDateDisplay
? (date: Date | null | undefined) => (date ? formatDateDisplay(date) : "")
: (date: Date | null | undefined) =>
safeDateFormat(date, { dateFormat, locale, timeZone });

if (selectsRange) {
if (!startDate && !endDate) {
return "";
}
const separator = rangeSeparator || DATE_RANGE_SEPARATOR;
return `${formatSingleDate(startDate)}${separator}${formatSingleDate(endDate)}`;
} else if (selectsMultiple) {
if (formatMultipleDates) {
const formatDateFn = (date: Date) =>
safeDateFormat(date, { dateFormat, locale, timeZone });
return formatMultipleDates(selectedDates ?? [], formatDateFn);
return formatMultipleDates(
selectedDates ?? [],
(date: Date) => formatSingleDate(date) as string,
);
}
return safeMultipleDatesFormat(selectedDates ?? [], {
dateFormat,
locale,
timeZone,
formatDateDisplay,
});
}
return safeDateFormat(selected, {
dateFormat,
locale,
timeZone,
});
return formatSingleDate(selected);
};

resetHiddenStatus = (): void => {
Expand Down
52 changes: 52 additions & 0 deletions src/test/date_utils_test.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import {
getWeek,
safeDateRangeFormat,
safeDateFormat,
safeMultipleDatesFormat,
getHolidaysMap,
arraysAreEqual,
startOfMinute,
Expand Down Expand Up @@ -1715,6 +1716,57 @@ describe("date_utils", () => {
});
});

describe("formatDateDisplay in safeMultipleDatesFormat", () => {
const formatDateDisplay = (date: Date) =>
new Intl.DateTimeFormat("en-US", {
year: "numeric",
month: "long",
day: "numeric",
}).format(date);

it("should format single date using formatDateDisplay", () => {
const dates = [new Date("2024-01-15T00:00:00")];
const result = safeMultipleDatesFormat(dates, {
dateFormat: "MM/dd/yyyy",
formatDateDisplay,
});
expect(result).toBe("January 15, 2024");
});

it("should format two dates using formatDateDisplay", () => {
const dates = [
new Date("2024-01-15T00:00:00"),
new Date("2024-03-20T00:00:00"),
];
const result = safeMultipleDatesFormat(dates, {
dateFormat: "MM/dd/yyyy",
formatDateDisplay,
});
expect(result).toBe("January 15, 2024, March 20, 2024");
});

it("should format three+ dates with count badge using formatDateDisplay", () => {
const dates = [
new Date("2024-01-15T00:00:00"),
new Date("2024-03-20T00:00:00"),
new Date("2024-06-10T00:00:00"),
];
const result = safeMultipleDatesFormat(dates, {
dateFormat: "MM/dd/yyyy",
formatDateDisplay,
});
expect(result).toBe("January 15, 2024 (+2)");
});

it("should fall back to dateFormat when formatDateDisplay is not provided", () => {
const dates = [new Date("2024-01-15T00:00:00")];
const result = safeMultipleDatesFormat(dates, {
dateFormat: "MM/dd/yyyy",
});
expect(result).toBe("01/15/2024");
});
});

describe("isDayInRange error handling", () => {
it("returns false when isWithinInterval throws", async () => {
jest.doMock("date-fns", () => {
Expand Down
72 changes: 72 additions & 0 deletions src/test/datepicker_test.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7074,4 +7074,76 @@ describe("DatePicker", () => {
expect(datepicker).not.toBeNull();
});
});

describe("formatDateDisplay", () => {
const intlFormatter = (date: Date) =>
new Intl.DateTimeFormat("en-US", {
year: "numeric",
month: "long",
day: "numeric",
}).format(date);

it("should render input value using formatDateDisplay", () => {
const { container } = render(
<DatePicker
selected={new Date("2024/01/15")}
onChange={() => {}}
formatDateDisplay={intlFormatter}
/>,
);

const input = container.querySelector("input");
expect(input?.value).toBe("January 15, 2024");
});

it("should render date range using formatDateDisplay", () => {
const { container } = render(
<DatePicker
selectsRange
startDate={new Date("2024/01/15")}
endDate={new Date("2024/01/20")}
onChange={() => {}}
formatDateDisplay={intlFormatter}
/>,
);

const input = container.querySelector("input");
expect(input?.value).toBe("January 15, 2024 - January 20, 2024");
});

it("should render partial date range using formatDateDisplay", () => {
const { container } = render(
<DatePicker
selectsRange
startDate={new Date("2024/01/15")}
endDate={null}
onChange={() => {}}
formatDateDisplay={intlFormatter}
/>,
);

const input = container.querySelector("input");
expect(input?.value).toBe("January 15, 2024 - ");
});

it("should still parse typed input using dateFormat", () => {
const onChange = jest.fn();
const { container } = render(
<DatePicker
selected={new Date("2024/01/15")}
onChange={onChange}
dateFormat="MM/dd/yyyy"
formatDateDisplay={intlFormatter}
/>,
);

const input = container.querySelector("input")!;
fireEvent.change(input, { target: { value: "02/20/2024" } });

expect(onChange).toHaveBeenCalled();
const receivedDate = onChange.mock.calls[0]?.[0] as Date;
expect(receivedDate.getMonth()).toBe(1);
expect(receivedDate.getDate()).toBe(20);
});
});
});
39 changes: 39 additions & 0 deletions src/test/multiple_selected_dates.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,4 +134,43 @@ describe("Multiple Dates Selected", function () {
expect(typeof receivedFormatDate).toBe("function");
expect(receivedFormatDate(new Date("2024/01/01"))).toBe("01/01/2024");
});

const shortDateFormatter = (date: Date) =>
new Intl.DateTimeFormat("en-US", {
year: "numeric",
month: "short",
day: "numeric",
}).format(date);

it("should display multiple dates using formatDateDisplay", () => {
const { container: datePicker } = getDatePicker({
selectsMultiple: true,
selectedDates: [
new Date("2024/01/01"),
new Date("2024/01/15"),
new Date("2024/03/15"),
],
formatDateDisplay: shortDateFormatter,
});

const input = datePicker.querySelector("input");

expect(input).not.toBeNull();
expect(input?.value).toBe("Jan 1, 2024 (+2)");
});

it("should use formatDateDisplay with formatMultipleDates", () => {
const { container: datePicker } = getDatePicker({
selectsMultiple: true,
selectedDates: [new Date("2024/01/01"), new Date("2024/01/15")],
formatDateDisplay: shortDateFormatter,
formatMultipleDates: (dates, formatDate) =>
dates.map(formatDate).join(" | "),
});

const input = datePicker.querySelector("input");

expect(input).not.toBeNull();
expect(input?.value).toBe("Jan 1, 2024 | Jan 15, 2024");
});
});