Skip to content

Commit f9e7364

Browse files
committed
feat: add formatDateDisplay prop for custom date formatting
Adds a new `formatDateDisplay` prop that accepts `(date: Date) => string` for custom input display formatting (e.g. Intl.DateTimeFormat). Unlike overloading `dateFormat`, this approach preserves the existing parsing behavior — `dateFormat` continues to handle typed input parsing while `formatDateDisplay` only controls the displayed value. Works with single dates, date ranges, multiple dates, and the `formatMultipleDates` callback.
1 parent 548a1f3 commit f9e7364

7 files changed

Lines changed: 220 additions & 18 deletions

File tree

docs-site/src/components/Examples/config.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import CustomCalendarClassName from "../../examples/ts/customCalendarClassName?r
2828
import CustomClassName from "../../examples/ts/customClassName?raw";
2929
import CustomDayClassName from "../../examples/ts/customDayClassName?raw";
3030
import CustomDateFormat from "../../examples/ts/customDateFormat?raw";
31+
import IntlDateFormat from "../../examples/ts/intlDateFormat?raw";
3132
import CustomTimeClassName from "../../examples/ts/customTimeClassName?raw";
3233
import CustomTimeInput from "../../examples/ts/customTimeInput?raw";
3334
import DateRange from "../../examples/ts/dateRange?raw";
@@ -234,6 +235,12 @@ export const EXAMPLE_CONFIG: IExampleConfig[] = [
234235
title: "Custom Date Format",
235236
component: CustomDateFormat,
236237
},
238+
{
239+
title: "Custom Date Format (Intl.DateTimeFormat)",
240+
description:
241+
"Use formatDateDisplay to format dates with Intl.DateTimeFormat or any custom logic. The dateFormat prop is still used for parsing typed input.",
242+
component: IntlDateFormat,
243+
},
237244
{
238245
title: "Custom Time Class Name",
239246
component: CustomTimeClassName,
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// undefined falls back to the browser's default locale
2+
const formatter = new Intl.DateTimeFormat(undefined, {
3+
weekday: "short",
4+
year: "numeric",
5+
month: "long",
6+
day: "numeric",
7+
});
8+
9+
const IntlDateFormat = () => {
10+
const [selectedDate, setSelectedDate] = useState<Date | null>(new Date());
11+
12+
return (
13+
<DatePicker
14+
formatDateDisplay={(date) => formatter.format(date)}
15+
selected={selectedDate}
16+
onChange={setSelectedDate}
17+
/>
18+
);
19+
};
20+
21+
render(IntlDateFormat);

src/date_utils.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -520,19 +520,24 @@ export function safeMultipleDatesFormat(
520520
dateFormat: string | string[];
521521
locale?: Locale;
522522
timeZone?: TimeZone;
523+
formatDateDisplay?: (date: Date) => string;
523524
},
524525
): string {
525526
if (!dates?.length) {
526527
return "";
527528
}
528529

529-
const formattedFirstDate = dates[0] ? safeDateFormat(dates[0], props) : "";
530+
const formatDate = props.formatDateDisplay
531+
? (date: Date) => props.formatDateDisplay!(date)
532+
: (date: Date) => safeDateFormat(date, props);
533+
534+
const formattedFirstDate = dates[0] ? formatDate(dates[0]) : "";
530535
if (dates.length === 1) {
531536
return formattedFirstDate;
532537
}
533538

534539
if (dates.length === 2 && dates[1]) {
535-
const formattedSecondDate = safeDateFormat(dates[1], props);
540+
const formattedSecondDate = formatDate(dates[1]);
536541
return `${formattedFirstDate}, ${formattedSecondDate}`;
537542
}
538543

src/index.tsx

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ import {
3131
parseDateForNavigation,
3232
formatDate,
3333
safeDateFormat,
34-
safeDateRangeFormat,
3534
getHighLightDaysMap,
3635
getYear,
3736
getMonth,
@@ -172,6 +171,8 @@ export type DatePickerProps = OmitUnion<
172171
className?: string;
173172
customInput?: Parameters<typeof cloneElement>[0];
174173
dateFormat?: string | string[];
174+
/** Custom function to format dates for input display. When provided, overrides dateFormat for display only — dateFormat is still used for parsing typed input. */
175+
formatDateDisplay?: (date: Date) => string;
175176
showDateSelect?: boolean;
176177
highlightDates?: (Date | HighlightDate)[];
177178
onCalendarOpen?: VoidFunction;
@@ -524,6 +525,7 @@ export class DatePicker extends Component<DatePickerProps, DatePickerState> {
524525

525526
getInputValue = (): string => {
526527
const {
528+
formatDateDisplay,
527529
locale,
528530
startDate,
529531
endDate,
@@ -545,30 +547,34 @@ export class DatePicker extends Component<DatePickerProps, DatePickerState> {
545547
return value;
546548
} else if (typeof inputValue === "string") {
547549
return inputValue;
548-
} else if (selectsRange) {
549-
return safeDateRangeFormat(startDate, endDate, {
550-
dateFormat,
551-
locale,
552-
rangeSeparator,
553-
timeZone,
554-
});
550+
}
551+
552+
const formatSingleDate = formatDateDisplay
553+
? (date: Date | null | undefined) => (date ? formatDateDisplay(date) : "")
554+
: (date: Date | null | undefined) =>
555+
safeDateFormat(date, { dateFormat, locale, timeZone });
556+
557+
if (selectsRange) {
558+
if (!startDate && !endDate) {
559+
return "";
560+
}
561+
const separator = rangeSeparator || DATE_RANGE_SEPARATOR;
562+
return `${formatSingleDate(startDate)}${separator}${formatSingleDate(endDate)}`;
555563
} else if (selectsMultiple) {
556564
if (formatMultipleDates) {
557-
const formatDateFn = (date: Date) =>
558-
safeDateFormat(date, { dateFormat, locale, timeZone });
559-
return formatMultipleDates(selectedDates ?? [], formatDateFn);
565+
return formatMultipleDates(
566+
selectedDates ?? [],
567+
(date: Date) => formatSingleDate(date) as string,
568+
);
560569
}
561570
return safeMultipleDatesFormat(selectedDates ?? [], {
562571
dateFormat,
563572
locale,
564573
timeZone,
574+
formatDateDisplay,
565575
});
566576
}
567-
return safeDateFormat(selected, {
568-
dateFormat,
569-
locale,
570-
timeZone,
571-
});
577+
return formatSingleDate(selected);
572578
};
573579

574580
resetHiddenStatus = (): void => {

src/test/date_utils_test.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import {
4848
getWeek,
4949
safeDateRangeFormat,
5050
safeDateFormat,
51+
safeMultipleDatesFormat,
5152
getHolidaysMap,
5253
arraysAreEqual,
5354
startOfMinute,
@@ -1715,6 +1716,57 @@ describe("date_utils", () => {
17151716
});
17161717
});
17171718

1719+
describe("formatDateDisplay in safeMultipleDatesFormat", () => {
1720+
const formatDateDisplay = (date: Date) =>
1721+
new Intl.DateTimeFormat("en-US", {
1722+
year: "numeric",
1723+
month: "long",
1724+
day: "numeric",
1725+
}).format(date);
1726+
1727+
it("should format single date using formatDateDisplay", () => {
1728+
const dates = [new Date("2024-01-15T00:00:00")];
1729+
const result = safeMultipleDatesFormat(dates, {
1730+
dateFormat: "MM/dd/yyyy",
1731+
formatDateDisplay,
1732+
});
1733+
expect(result).toBe("January 15, 2024");
1734+
});
1735+
1736+
it("should format two dates using formatDateDisplay", () => {
1737+
const dates = [
1738+
new Date("2024-01-15T00:00:00"),
1739+
new Date("2024-03-20T00:00:00"),
1740+
];
1741+
const result = safeMultipleDatesFormat(dates, {
1742+
dateFormat: "MM/dd/yyyy",
1743+
formatDateDisplay,
1744+
});
1745+
expect(result).toBe("January 15, 2024, March 20, 2024");
1746+
});
1747+
1748+
it("should format three+ dates with count badge using formatDateDisplay", () => {
1749+
const dates = [
1750+
new Date("2024-01-15T00:00:00"),
1751+
new Date("2024-03-20T00:00:00"),
1752+
new Date("2024-06-10T00:00:00"),
1753+
];
1754+
const result = safeMultipleDatesFormat(dates, {
1755+
dateFormat: "MM/dd/yyyy",
1756+
formatDateDisplay,
1757+
});
1758+
expect(result).toBe("January 15, 2024 (+2)");
1759+
});
1760+
1761+
it("should fall back to dateFormat when formatDateDisplay is not provided", () => {
1762+
const dates = [new Date("2024-01-15T00:00:00")];
1763+
const result = safeMultipleDatesFormat(dates, {
1764+
dateFormat: "MM/dd/yyyy",
1765+
});
1766+
expect(result).toBe("01/15/2024");
1767+
});
1768+
});
1769+
17181770
describe("isDayInRange error handling", () => {
17191771
it("returns false when isWithinInterval throws", async () => {
17201772
jest.doMock("date-fns", () => {

src/test/datepicker_test.test.tsx

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7074,4 +7074,76 @@ describe("DatePicker", () => {
70747074
expect(datepicker).not.toBeNull();
70757075
});
70767076
});
7077+
7078+
describe("formatDateDisplay", () => {
7079+
const intlFormatter = (date: Date) =>
7080+
new Intl.DateTimeFormat("en-US", {
7081+
year: "numeric",
7082+
month: "long",
7083+
day: "numeric",
7084+
}).format(date);
7085+
7086+
it("should render input value using formatDateDisplay", () => {
7087+
const { container } = render(
7088+
<DatePicker
7089+
selected={new Date("2024/01/15")}
7090+
onChange={() => {}}
7091+
formatDateDisplay={intlFormatter}
7092+
/>,
7093+
);
7094+
7095+
const input = container.querySelector("input");
7096+
expect(input?.value).toBe("January 15, 2024");
7097+
});
7098+
7099+
it("should render date range using formatDateDisplay", () => {
7100+
const { container } = render(
7101+
<DatePicker
7102+
selectsRange
7103+
startDate={new Date("2024/01/15")}
7104+
endDate={new Date("2024/01/20")}
7105+
onChange={() => {}}
7106+
formatDateDisplay={intlFormatter}
7107+
/>,
7108+
);
7109+
7110+
const input = container.querySelector("input");
7111+
expect(input?.value).toBe("January 15, 2024 - January 20, 2024");
7112+
});
7113+
7114+
it("should render partial date range using formatDateDisplay", () => {
7115+
const { container } = render(
7116+
<DatePicker
7117+
selectsRange
7118+
startDate={new Date("2024/01/15")}
7119+
endDate={null}
7120+
onChange={() => {}}
7121+
formatDateDisplay={intlFormatter}
7122+
/>,
7123+
);
7124+
7125+
const input = container.querySelector("input");
7126+
expect(input?.value).toBe("January 15, 2024 - ");
7127+
});
7128+
7129+
it("should still parse typed input using dateFormat", () => {
7130+
const onChange = jest.fn();
7131+
const { container } = render(
7132+
<DatePicker
7133+
selected={new Date("2024/01/15")}
7134+
onChange={onChange}
7135+
dateFormat="MM/dd/yyyy"
7136+
formatDateDisplay={intlFormatter}
7137+
/>,
7138+
);
7139+
7140+
const input = container.querySelector("input")!;
7141+
fireEvent.change(input, { target: { value: "02/20/2024" } });
7142+
7143+
expect(onChange).toHaveBeenCalled();
7144+
const receivedDate = onChange.mock.calls[0]?.[0] as Date;
7145+
expect(receivedDate.getMonth()).toBe(1);
7146+
expect(receivedDate.getDate()).toBe(20);
7147+
});
7148+
});
70777149
});

src/test/multiple_selected_dates.test.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,4 +134,43 @@ describe("Multiple Dates Selected", function () {
134134
expect(typeof receivedFormatDate).toBe("function");
135135
expect(receivedFormatDate(new Date("2024/01/01"))).toBe("01/01/2024");
136136
});
137+
138+
const shortDateFormatter = (date: Date) =>
139+
new Intl.DateTimeFormat("en-US", {
140+
year: "numeric",
141+
month: "short",
142+
day: "numeric",
143+
}).format(date);
144+
145+
it("should display multiple dates using formatDateDisplay", () => {
146+
const { container: datePicker } = getDatePicker({
147+
selectsMultiple: true,
148+
selectedDates: [
149+
new Date("2024/01/01"),
150+
new Date("2024/01/15"),
151+
new Date("2024/03/15"),
152+
],
153+
formatDateDisplay: shortDateFormatter,
154+
});
155+
156+
const input = datePicker.querySelector("input");
157+
158+
expect(input).not.toBeNull();
159+
expect(input?.value).toBe("Jan 1, 2024 (+2)");
160+
});
161+
162+
it("should use formatDateDisplay with formatMultipleDates", () => {
163+
const { container: datePicker } = getDatePicker({
164+
selectsMultiple: true,
165+
selectedDates: [new Date("2024/01/01"), new Date("2024/01/15")],
166+
formatDateDisplay: shortDateFormatter,
167+
formatMultipleDates: (dates, formatDate) =>
168+
dates.map(formatDate).join(" | "),
169+
});
170+
171+
const input = datePicker.querySelector("input");
172+
173+
expect(input).not.toBeNull();
174+
expect(input?.value).toBe("Jan 1, 2024 | Jan 15, 2024");
175+
});
137176
});

0 commit comments

Comments
 (0)