diff --git a/docs-site/src/components/Examples/config.tsx b/docs-site/src/components/Examples/config.tsx index 4baac1b7f..b08cb022a 100644 --- a/docs-site/src/components/Examples/config.tsx +++ b/docs-site/src/components/Examples/config.tsx @@ -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"; @@ -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, diff --git a/docs-site/src/examples/ts/intlDateFormat.tsx b/docs-site/src/examples/ts/intlDateFormat.tsx new file mode 100644 index 000000000..8ea88813e --- /dev/null +++ b/docs-site/src/examples/ts/intlDateFormat.tsx @@ -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(new Date()); + + return ( + formatter.format(date)} + selected={selectedDate} + onChange={setSelectedDate} + /> + ); +}; + +render(IntlDateFormat); diff --git a/src/date_utils.ts b/src/date_utils.ts index 7fba0d960..f44589536 100644 --- a/src/date_utils.ts +++ b/src/date_utils.ts @@ -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}`; } diff --git a/src/index.tsx b/src/index.tsx index 05fde7b81..0f9d315a3 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -31,7 +31,6 @@ import { parseDateForNavigation, formatDate, safeDateFormat, - safeDateRangeFormat, getHighLightDaysMap, getYear, getMonth, @@ -172,6 +171,8 @@ export type DatePickerProps = OmitUnion< className?: string; customInput?: Parameters[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; @@ -524,6 +525,7 @@ export class DatePicker extends Component { getInputValue = (): string => { const { + formatDateDisplay, locale, startDate, endDate, @@ -545,30 +547,34 @@ export class DatePicker extends Component { 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 => { diff --git a/src/test/date_utils_test.test.ts b/src/test/date_utils_test.test.ts index ee5b98047..a1e486443 100644 --- a/src/test/date_utils_test.test.ts +++ b/src/test/date_utils_test.test.ts @@ -48,6 +48,7 @@ import { getWeek, safeDateRangeFormat, safeDateFormat, + safeMultipleDatesFormat, getHolidaysMap, arraysAreEqual, startOfMinute, @@ -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", () => { diff --git a/src/test/datepicker_test.test.tsx b/src/test/datepicker_test.test.tsx index 31b239b37..c6f736fbb 100644 --- a/src/test/datepicker_test.test.tsx +++ b/src/test/datepicker_test.test.tsx @@ -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( + {}} + formatDateDisplay={intlFormatter} + />, + ); + + const input = container.querySelector("input"); + expect(input?.value).toBe("January 15, 2024"); + }); + + it("should render date range using formatDateDisplay", () => { + const { container } = render( + {}} + 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( + {}} + 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( + , + ); + + 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); + }); + }); }); diff --git a/src/test/multiple_selected_dates.test.tsx b/src/test/multiple_selected_dates.test.tsx index f37c62ed6..6d1314564 100644 --- a/src/test/multiple_selected_dates.test.tsx +++ b/src/test/multiple_selected_dates.test.tsx @@ -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"); + }); });