Skip to content

Commit 3d53acb

Browse files
Merge pull request #6206 from Hacker0x01/fix/timezone-date-selection-6193
fix: correct timezone handling for date selection and display
2 parents e1ce245 + c5766e0 commit 3d53acb

3 files changed

Lines changed: 222 additions & 5 deletions

File tree

src/date_utils.ts

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -439,19 +439,38 @@ export function formatDate(
439439
* Safely formats a date.
440440
*
441441
* @param date - The date.
442-
* @param options - An object containing the dateFormat and locale.
442+
* @param options - An object containing the dateFormat, locale, and optional timeZone.
443443
* @returns - The formatted date or an empty string.
444444
*/
445445
export function safeDateFormat(
446446
date: Date | null | undefined,
447-
{ dateFormat, locale }: { dateFormat: string | string[]; locale?: Locale },
447+
{
448+
dateFormat,
449+
locale,
450+
timeZone,
451+
}: { dateFormat: string | string[]; locale?: Locale; timeZone?: TimeZone },
448452
): string {
449453
const formatStr = (
450454
Array.isArray(dateFormat) && dateFormat.length > 0
451455
? dateFormat[0]
452456
: dateFormat
453457
) as string; // Cast to string because it's impossible to get `string | string[] | undefined` here and typescript doesn't know that
454-
return (date && formatDate(date, formatStr, locale)) || "";
458+
459+
if (!date) {
460+
return "";
461+
}
462+
463+
// Use timezone-aware formatting if timeZone is specified
464+
if (timeZone) {
465+
// Resolve locale string to locale object for formatInTimeZone
466+
// Cast to DateFnsLocale since LocaleObj is a compatible subset
467+
const localeObj = (
468+
locale ? getLocaleObject(locale) : getLocaleObject(getDefaultLocale())
469+
) as DateFnsLocale | undefined;
470+
return formatInTimeZone(date, formatStr, timeZone, localeObj);
471+
}
472+
473+
return formatDate(date, formatStr, locale) || "";
455474
}
456475

457476
/**
@@ -474,6 +493,7 @@ export function safeDateRangeFormat(
474493
dateFormat: string | string[];
475494
locale?: Locale;
476495
rangeSeparator?: string;
496+
timeZone?: TimeZone;
477497
},
478498
): string {
479499
if (!startDate && !endDate) {
@@ -496,7 +516,11 @@ export function safeDateRangeFormat(
496516
*/
497517
export function safeMultipleDatesFormat(
498518
dates: Date[],
499-
props: { dateFormat: string | string[]; locale?: Locale },
519+
props: {
520+
dateFormat: string | string[];
521+
locale?: Locale;
522+
timeZone?: TimeZone;
523+
},
500524
): string {
501525
if (!dates?.length) {
502526
return "";

src/index.tsx

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -534,6 +534,7 @@ export class DatePicker extends Component<DatePickerProps, DatePickerState> {
534534
selectsRange,
535535
formatMultipleDates,
536536
value,
537+
timeZone,
537538
} = this.props;
538539
const dateFormat =
539540
this.props.dateFormat ?? DatePicker.defaultProps.dateFormat;
@@ -549,21 +550,24 @@ export class DatePicker extends Component<DatePickerProps, DatePickerState> {
549550
dateFormat,
550551
locale,
551552
rangeSeparator,
553+
timeZone,
552554
});
553555
} else if (selectsMultiple) {
554556
if (formatMultipleDates) {
555557
const formatDateFn = (date: Date) =>
556-
safeDateFormat(date, { dateFormat, locale });
558+
safeDateFormat(date, { dateFormat, locale, timeZone });
557559
return formatMultipleDates(selectedDates ?? [], formatDateFn);
558560
}
559561
return safeMultipleDatesFormat(selectedDates ?? [], {
560562
dateFormat,
561563
locale,
564+
timeZone,
562565
});
563566
}
564567
return safeDateFormat(selected, {
565568
dateFormat,
566569
locale,
570+
timeZone,
567571
});
568572
};
569573

@@ -1730,6 +1734,23 @@ export class DatePicker extends Component<DatePickerProps, DatePickerState> {
17301734
if (!this.props.inline && !this.isCalendarOpen()) {
17311735
return null;
17321736
}
1737+
1738+
const { timeZone, selected, startDate, endDate, selectedDates } =
1739+
this.props;
1740+
1741+
// Convert dates to zoned time for calendar display when timeZone is specified
1742+
// This ensures the calendar highlights the correct day in the target timezone
1743+
const zonedSelected =
1744+
selected && timeZone ? toZonedTime(selected, timeZone) : selected;
1745+
const zonedStartDate =
1746+
startDate && timeZone ? toZonedTime(startDate, timeZone) : startDate;
1747+
const zonedEndDate =
1748+
endDate && timeZone ? toZonedTime(endDate, timeZone) : endDate;
1749+
const zonedSelectedDates =
1750+
selectedDates && timeZone
1751+
? selectedDates.map((date) => toZonedTime(date, timeZone))
1752+
: selectedDates;
1753+
17331754
return (
17341755
<Calendar
17351756
showMonthYearDropdown={undefined}
@@ -1738,6 +1759,11 @@ export class DatePicker extends Component<DatePickerProps, DatePickerState> {
17381759
}}
17391760
{...this.props}
17401761
{...this.state}
1762+
// Override date props with zoned time versions for correct display
1763+
selected={zonedSelected}
1764+
startDate={zonedStartDate}
1765+
endDate={zonedEndDate}
1766+
selectedDates={zonedSelectedDates}
17411767
setOpen={this.setOpen}
17421768
dateFormat={
17431769
this.props.dateFormatCalendar ??

src/test/timezone_test.test.tsx

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -688,6 +688,173 @@ describe("DatePicker with timeZone prop", () => {
688688
expect(changedStartDate).toBeInstanceOf(Date);
689689
expect(changedEndDate).toBeNull();
690690
});
691+
692+
// Test for issue #6193: Date selection with extreme timezone difference
693+
it("should correctly select date when timezone differs significantly from browser timezone (issue #6193)", () => {
694+
// Simulate: browser in UTC, timezone prop set to Pacific/Kiritimati (UTC+14)
695+
// When user clicks Dec 26 in the calendar, onChange should receive Dec 26 in Kiritimati
696+
// which is Dec 25 10:00 UTC - and the input should still display Dec 26
697+
698+
// Start with a date that represents Dec 26 00:00 in Kiritimati (= Dec 25 10:00 UTC)
699+
const initialUtcDate = new Date("2025-12-25T10:00:00Z");
700+
let selectedDate: Date | null = initialUtcDate;
701+
702+
const { container, rerender } = render(
703+
<DatePicker
704+
selected={selectedDate}
705+
onChange={(date) => {
706+
selectedDate = date;
707+
}}
708+
timeZone="Pacific/Kiritimati"
709+
dateFormat="yyyy-MM-dd"
710+
/>,
711+
);
712+
713+
// The input should display Dec 26 (the date in Kiritimati timezone)
714+
const input = container.querySelector("input");
715+
expect(input?.value).toBe("2025-12-26");
716+
717+
// Open the calendar
718+
if (input) {
719+
fireEvent.focus(input);
720+
}
721+
722+
// Find and click Dec 26 in the calendar
723+
const days = container.querySelectorAll(".react-datepicker__day");
724+
const dayToClick = Array.from(days).find(
725+
(day) =>
726+
!day.classList.contains("react-datepicker__day--outside-month") &&
727+
day.textContent === "26",
728+
);
729+
expect(dayToClick).toBeTruthy();
730+
731+
// The day 26 should be marked as selected before clicking
732+
expect(
733+
dayToClick?.classList.contains("react-datepicker__day--selected"),
734+
).toBe(true);
735+
736+
if (dayToClick) {
737+
fireEvent.click(dayToClick);
738+
}
739+
740+
// After clicking, re-render with the new selected date
741+
rerender(
742+
<DatePicker
743+
selected={selectedDate}
744+
onChange={(date) => {
745+
selectedDate = date;
746+
}}
747+
timeZone="Pacific/Kiritimati"
748+
dateFormat="yyyy-MM-dd"
749+
/>,
750+
);
751+
752+
// The input should still display Dec 26 (same date user clicked)
753+
expect(input?.value).toBe("2025-12-26");
754+
755+
// The onChange should have been called with a UTC date that represents Dec 26 in Kiritimati
756+
// Dec 26 00:00 Kiritimati = Dec 25 10:00 UTC
757+
expect(selectedDate).not.toBeNull();
758+
expect(selectedDate?.getUTCFullYear()).toBe(2025);
759+
expect(selectedDate?.getUTCMonth()).toBe(11); // December
760+
expect(selectedDate?.getUTCDate()).toBe(25);
761+
expect(selectedDate?.getUTCHours()).toBe(10);
762+
});
763+
764+
// Test that clicking a different date works correctly with timezone
765+
it("should correctly change date when clicking different day with timezone (issue #6193)", () => {
766+
// Start with Dec 26 00:00 Kiritimati (= Dec 25 10:00 UTC)
767+
const initialUtcDate = new Date("2025-12-25T10:00:00Z");
768+
let selectedDate: Date | null = initialUtcDate;
769+
770+
const { container, rerender } = render(
771+
<DatePicker
772+
selected={selectedDate}
773+
onChange={(date) => {
774+
selectedDate = date;
775+
}}
776+
timeZone="Pacific/Kiritimati"
777+
dateFormat="yyyy-MM-dd"
778+
/>,
779+
);
780+
781+
const input = container.querySelector("input");
782+
expect(input?.value).toBe("2025-12-26");
783+
784+
// Open the calendar
785+
if (input) {
786+
fireEvent.focus(input);
787+
}
788+
789+
// Click Dec 27 instead
790+
const days = container.querySelectorAll(".react-datepicker__day");
791+
const dayToClick = Array.from(days).find(
792+
(day) =>
793+
!day.classList.contains("react-datepicker__day--outside-month") &&
794+
day.textContent === "27",
795+
);
796+
expect(dayToClick).toBeTruthy();
797+
798+
if (dayToClick) {
799+
fireEvent.click(dayToClick);
800+
}
801+
802+
// Re-render with new selected date
803+
rerender(
804+
<DatePicker
805+
selected={selectedDate}
806+
onChange={(date) => {
807+
selectedDate = date;
808+
}}
809+
timeZone="Pacific/Kiritimati"
810+
dateFormat="yyyy-MM-dd"
811+
/>,
812+
);
813+
814+
// The input should now display Dec 27
815+
expect(input?.value).toBe("2025-12-27");
816+
817+
// The UTC date should represent Dec 27 00:00 Kiritimati = Dec 26 10:00 UTC
818+
expect(selectedDate?.getUTCDate()).toBe(26);
819+
});
820+
821+
// Test for selectsMultiple with timezone
822+
it("should correctly display and select multiple dates with timezone", () => {
823+
// Dec 26 00:00 Kiritimati = Dec 25 10:00 UTC
824+
// Dec 27 00:00 Kiritimati = Dec 26 10:00 UTC
825+
const initialDates = [
826+
new Date("2025-12-25T10:00:00Z"),
827+
new Date("2025-12-26T10:00:00Z"),
828+
];
829+
let selectedDates: Date[] = initialDates;
830+
831+
const { container } = render(
832+
<DatePicker
833+
selectedDates={selectedDates}
834+
selectsMultiple
835+
onChange={(dates) => {
836+
selectedDates = dates as Date[];
837+
}}
838+
timeZone="Pacific/Kiritimati"
839+
dateFormat="yyyy-MM-dd"
840+
inline
841+
// Set openToDate to ensure calendar shows December 2025
842+
openToDate={new Date("2025-12-25T10:00:00Z")}
843+
/>,
844+
);
845+
846+
// Both Dec 26 and Dec 27 should be marked as selected in the calendar
847+
// Filter to only days in current month (not outside-month days)
848+
const selectedDays = container.querySelectorAll(
849+
".react-datepicker__day--selected:not(.react-datepicker__day--outside-month)",
850+
);
851+
expect(selectedDays.length).toBe(2);
852+
853+
// Check that the correct days are highlighted
854+
const dayTexts = Array.from(selectedDays).map((d) => d.textContent);
855+
expect(dayTexts).toContain("26");
856+
expect(dayTexts).toContain("27");
857+
});
691858
});
692859

693860
describe("Timezone fallback behavior (when date-fns-tz is not installed)", () => {

0 commit comments

Comments
 (0)