diff --git a/docs/timezone.md b/docs/timezone.md index 667343299..5f800b054 100644 --- a/docs/timezone.md +++ b/docs/timezone.md @@ -105,6 +105,26 @@ function TokyoCalendar() { - **Consistency**: When using the `timeZone` prop, ensure all date-related props (like `minDate`, `maxDate`, `excludeDates`, etc.) are provided in a consistent manner. +### Using with Vite (or other bundlers that don't support dynamic require) + +If you're using Vite or another bundler that doesn't support dynamic `require()`, you'll need to explicitly provide the `date-fns-tz` module using `setDateFnsTzModule`: + +```jsx +import DatePicker, { setDateFnsTzModule } from "react-datepicker"; +import * as dateFnsTz from "date-fns-tz"; + +// Call once at app initialization (e.g., in your main entry point) +setDateFnsTzModule(dateFnsTz); + +function MyComponent() { + const [startDate, setStartDate] = useState(new Date()); + + return setStartDate(date)} timeZone="America/New_York" />; +} +``` + +This workaround is necessary because react-datepicker normally loads `date-fns-tz` dynamically using `require()` to keep it as an optional dependency. Since Vite doesn't support `require()` in ES modules or dynamic `require()`, you need to import and provide the module explicitly. + --- ## The "Date is One Day Off" Problem diff --git a/src/date_utils.ts b/src/date_utils.ts index 7fba0d960..8415bf49a 100644 --- a/src/date_utils.ts +++ b/src/date_utils.ts @@ -99,9 +99,32 @@ export function __setDateFnsTzNull(): void { dateFnsTzLoadAttempted = true; } +/** + * Sets the date-fns-tz module externally. + * This is useful for environments where dynamic require doesn't work (e.g., Vite). + * + * @example + * ```typescript + * import * as dateFnsTz from 'date-fns-tz'; + * import { setDateFnsTzModule } from 'react-datepicker'; + * + * // Call once at app initialization + * setDateFnsTzModule(dateFnsTz); + * ``` + * + * @param module - The date-fns-tz module containing toZonedTime, fromZonedTime, and formatInTimeZone + */ +export function setDateFnsTzModule(module: DateFnsTz): void { + dateFnsTz = module; + dateFnsTzLoadAttempted = true; +} + /** * Attempts to load date-fns-tz module. * Returns null if the module is not installed. + * + * If the module was set externally via setDateFnsTzModule(), that will be used. + * Otherwise, attempts to load via dynamic require. */ function getDateFnsTz(): DateFnsTz | null { if (dateFnsTzLoadAttempted) { diff --git a/src/index.tsx b/src/index.tsx index 05fde7b81..58c531d43 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -52,6 +52,7 @@ import { isSameMinute, toZonedTime, fromZonedTime, + setDateFnsTzModule, safeToDate, type HighlightDate, type HolidayItem, @@ -67,7 +68,12 @@ import type { ClickOutsideHandler } from "./click_outside_wrapper"; export { default as CalendarContainer } from "./calendar_container"; -export { registerLocale, setDefaultLocale, getDefaultLocale }; +export { + registerLocale, + setDefaultLocale, + getDefaultLocale, + setDateFnsTzModule, +}; export { ReactDatePickerCustomHeaderProps, diff --git a/src/test/timezone_test.test.tsx b/src/test/timezone_test.test.tsx index 86333a3eb..e3c3253d8 100644 --- a/src/test/timezone_test.test.tsx +++ b/src/test/timezone_test.test.tsx @@ -1,5 +1,6 @@ import React from "react"; import { render, fireEvent } from "@testing-library/react"; +import * as realDateFnsTz from "date-fns-tz"; import DatePicker from "../index"; import * as dateUtils from "../date_utils"; @@ -10,6 +11,7 @@ const { nowInTimeZone, __resetDateFnsTzCache, __setDateFnsTzNull, + setDateFnsTzModule, } = dateUtils; describe("Timezone utility functions", () => { @@ -928,3 +930,98 @@ describe("Timezone fallback behavior (when date-fns-tz is not installed)", () => consoleSpy.mockRestore(); }); }); + +describe("setDateFnsTzModule - for environments where dynamic require doesn't work (e.g., Vite)", () => { + beforeEach(() => { + __resetDateFnsTzCache(); + }); + + afterEach(() => { + __resetDateFnsTzCache(); + }); + + it("should use the externally provided module for toZonedTime", () => { + const testDate = new Date("2024-06-15T12:00:00Z"); + + // Create a mock module that returns a predictable result + const mockModule = { + toZonedTime: jest.fn().mockReturnValue(new Date("2024-06-15T08:00:00")), + fromZonedTime: jest.fn(), + formatInTimeZone: jest.fn(), + }; + + setDateFnsTzModule(mockModule); + + const result = toZonedTime(testDate, "America/New_York"); + + expect(mockModule.toZonedTime).toHaveBeenCalledWith( + testDate, + "America/New_York", + ); + expect(result.getHours()).toBe(8); + }); + + it("should use the externally provided module for fromZonedTime", () => { + const testDate = new Date("2024-06-15T08:00:00"); + + const mockModule = { + toZonedTime: jest.fn(), + fromZonedTime: jest + .fn() + .mockReturnValue(new Date("2024-06-15T12:00:00Z")), + formatInTimeZone: jest.fn(), + }; + + setDateFnsTzModule(mockModule); + + const result = fromZonedTime(testDate, "America/New_York"); + + expect(mockModule.fromZonedTime).toHaveBeenCalledWith( + testDate, + "America/New_York", + ); + expect(result.getUTCHours()).toBe(12); + }); + + it("should use the externally provided module for formatInTimeZone", () => { + const testDate = new Date("2024-06-15T12:00:00Z"); + + const mockModule = { + toZonedTime: jest.fn(), + fromZonedTime: jest.fn(), + formatInTimeZone: jest.fn().mockReturnValue("08:00"), + }; + + setDateFnsTzModule(mockModule); + + const result = formatInTimeZone(testDate, "HH:mm", "America/New_York"); + + expect(mockModule.formatInTimeZone).toHaveBeenCalledWith( + testDate, + "America/New_York", + "HH:mm", + { locale: undefined }, + ); + expect(result).toBe("08:00"); + }); + + it("should work with the real date-fns-tz module when provided", () => { + // Import the real module + setDateFnsTzModule(realDateFnsTz); + + const testDate = new Date("2024-06-15T12:00:00Z"); + + // Test toZonedTime + const zonedResult = toZonedTime(testDate, "America/New_York"); + expect(zonedResult.getHours()).toBe(8); // 12:00 UTC is 08:00 EDT + + // Test fromZonedTime + const nyDate = new Date("2024-06-15T08:00:00"); + const utcResult = fromZonedTime(nyDate, "America/New_York"); + expect(utcResult.getUTCHours()).toBe(12); + + // Test formatInTimeZone + const formatted = formatInTimeZone(testDate, "HH:mm", "America/New_York"); + expect(formatted).toBe("08:00"); + }); +});