diff --git a/__test__/features/Calendars/TimezoneInGrid.test.tsx b/__test__/features/Calendars/TimezoneInGrid.test.tsx index 5fc1e9d..53391b9 100644 --- a/__test__/features/Calendars/TimezoneInGrid.test.tsx +++ b/__test__/features/Calendars/TimezoneInGrid.test.tsx @@ -216,7 +216,7 @@ describe("EventDisplayPreview - Timezone Display", () => { it("displays correct timezone offset for different timezones", async () => { const timezones = [ - { tz: "America/New_York", expectedOffset: /UTC-[45]/ }, + { tz: "America/New_York", expectedOffset: /UTC[-−][45]/ }, { tz: "Europe/Paris", expectedOffset: /UTC\+[12]/ }, { tz: "Asia/Tokyo", expectedOffset: /UTC\+9/ }, { tz: "Australia/Sydney", expectedOffset: /UTC\+1[01]/ }, diff --git a/__test__/features/Calendars/TimezoneSelector.test.tsx b/__test__/features/Calendars/TimezoneSelector.test.tsx index 1814d91..0036a17 100644 --- a/__test__/features/Calendars/TimezoneSelector.test.tsx +++ b/__test__/features/Calendars/TimezoneSelector.test.tsx @@ -1,4 +1,4 @@ -import { screen, fireEvent, waitFor } from "@testing-library/react"; +import { screen, fireEvent, waitFor, cleanup } from "@testing-library/react"; import { TimezoneSelector } from "../../../src/components/Calendar/TimezoneSelector"; import { renderWithProviders } from "../../utils/Renderwithproviders"; @@ -11,17 +11,25 @@ describe("TimezoneSelector", () => { it("renders with initial timezone value", () => { renderWithProviders( - + ); const button = screen.getByRole("button"); expect(button).toBeInTheDocument(); - expect(button).toHaveTextContent("UTC-4"); // New York offset + expect(button).toHaveTextContent(/UTC[-−][45]/i); // New York offset }); it("opens popover when button is clicked", async () => { renderWithProviders( - + ); const button = screen.getByRole("button"); @@ -34,7 +42,11 @@ describe("TimezoneSelector", () => { it("calls onChange when a new timezone is selected", async () => { renderWithProviders( - + ); const button = screen.getByRole("button"); @@ -56,7 +68,11 @@ describe("TimezoneSelector", () => { it("closes popover after timezone selection", async () => { renderWithProviders( - + ); const button = screen.getByRole("button"); @@ -79,10 +95,40 @@ describe("TimezoneSelector", () => { it("displays timezones with half-hour offsets correctly", () => { renderWithProviders( - + ); const button = screen.getByRole("button"); expect(button).toHaveTextContent("UTC+5:30"); // India offset }); + + it("shows correct offset for Europe/Paris depending on daylight saving time", () => { + // Summer date (DST on) + const summerDate = new Date("2025-07-15T12:00:00Z"); + renderWithProviders( + + ); + let button = screen.getByRole("button"); + expect(button).toHaveTextContent(/UTC\+2\b/); + cleanup(); + // Rerender with a winter date (DST off) + const winterDate = new Date("2025-01-15T12:00:00Z"); + renderWithProviders( + + ); + button = screen.getByRole("button"); + expect(button).toHaveTextContent(/UTC\+1\b/); + }); }); diff --git a/src/components/Calendar/Calendar.tsx b/src/components/Calendar/Calendar.tsx index 7864525..9c46665 100644 --- a/src/components/Calendar/Calendar.tsx +++ b/src/components/Calendar/Calendar.tsx @@ -407,6 +407,7 @@ export default function CalendarApp({ dispatch(setTimeZone(newTimezone)) } diff --git a/src/components/Calendar/TimezoneSelector.tsx b/src/components/Calendar/TimezoneSelector.tsx index 1714c0d..471cc11 100644 --- a/src/components/Calendar/TimezoneSelector.tsx +++ b/src/components/Calendar/TimezoneSelector.tsx @@ -1,5 +1,6 @@ import { Button, Popover } from "@mui/material"; import { useI18n } from "cozy-ui/transpiled/react/providers/I18n"; +import moment from "moment"; import { MouseEvent, useMemo, useState } from "react"; import { TIMEZONES } from "../../utils/timezone-data"; import { TimezoneAutocomplete } from "../Timezone/TimezoneAutocomplete"; @@ -7,9 +8,14 @@ import { TimezoneAutocomplete } from "../Timezone/TimezoneAutocomplete"; interface TimezoneSelectProps { value: string; onChange: (value: string) => void; + referenceDate: Date; } -export function TimezoneSelector({ value, onChange }: TimezoneSelectProps) { +export function TimezoneSelector({ + value, + onChange, + referenceDate, +}: TimezoneSelectProps) { const [anchorEl, setAnchorEl] = useState(null); const timezoneList = useMemo(() => { @@ -24,7 +30,7 @@ export function TimezoneSelector({ value, onChange }: TimezoneSelectProps) { const effectiveTimezone = value ? resolveTimezone(value) : timezoneList.browserTz; - const selectedOffset = getTimezoneOffset(effectiveTimezone); + const selectedOffset = getTimezoneOffset(effectiveTimezone, referenceDate); const handleOpen = (event: MouseEvent) => { setAnchorEl(event.currentTarget); @@ -76,7 +82,9 @@ export function TimezoneSelector({ value, onChange }: TimezoneSelectProps) { value={effectiveTimezone} onChange={onChange} zones={timezoneList.zones} - getTimezoneOffset={getTimezoneOffset} + getTimezoneOffset={(tzName: string) => + getTimezoneOffset(tzName, referenceDate) + } autoFocus={true} showIcon={true} inputFontSize="14px" @@ -99,20 +107,17 @@ export function resolveTimezone(tzName: string): string { return tzName; } -export function getTimezoneOffset(tzName: string): string { - const resolvedTz = resolveTimezone(tzName); - const tzData = TIMEZONES.zones[resolvedTz]; - if (!tzData) return ""; +export function getTimezoneOffset( + tzName: string, + date: Date = new Date() +): string { + const fmt = new Intl.DateTimeFormat(undefined, { + timeZone: tzName, + timeZoneName: "shortOffset", + }); - const icsMatch = tzData.ics.match(/TZOFFSETTO:([+-]\d{4})/); - if (!icsMatch) return ""; - - const offset = icsMatch[1]; - const hours = parseInt(offset.slice(0, 3)); - const minutes = parseInt(offset.slice(3)); - - if (minutes === 0) { - return `UTC${hours >= 0 ? "+" : ""}${hours}`; - } - return `UTC${hours >= 0 ? "+" : ""}${hours}:${Math.abs(minutes).toString().padStart(2, "0")}`; + const currentDate = moment(date).isValid() ? date : new Date(); + const parts = fmt.formatToParts(currentDate); + const offsetPart = parts.find((p) => p.type === "timeZoneName"); + return offsetPart?.value.replace("GMT", "UTC") ?? ""; } diff --git a/src/components/Event/EventFormFields.tsx b/src/components/Event/EventFormFields.tsx index 4d561d7..1564764 100644 --- a/src/components/Event/EventFormFields.tsx +++ b/src/components/Event/EventFormFields.tsx @@ -90,7 +90,7 @@ interface EventFormFieldsProps { timezoneList: { zones: string[]; browserTz: string; - getTimezoneOffset: (tzName: string) => string; + getTimezoneOffset: (tzName: string, date: Date) => string; }; // Event handlers @@ -534,7 +534,9 @@ export default function EventFormFields({ value={timezone} onChange={setTimezone} zones={timezoneList.zones} - getTimezoneOffset={timezoneList.getTimezoneOffset} + getTimezoneOffset={(tzName: string) => + timezoneList.getTimezoneOffset(tzName, new Date(start)) + } showIcon={true} width={240} size="small" diff --git a/src/features/Events/EventDisplayPreview.tsx b/src/features/Events/EventDisplayPreview.tsx index 4790a57..07d5a79 100644 --- a/src/features/Events/EventDisplayPreview.tsx +++ b/src/features/Events/EventDisplayPreview.tsx @@ -304,7 +304,7 @@ export default function EventPreviewModal({ {formatDate(event.start, t, event.allday)} {event.end && formatEnd(event.start, event.end, t, event.allday) && - ` – ${formatEnd(event.start, event.end, t, event.allday)} ${!event.allday ? getTimezoneOffset(timezone) : ""}`} + ` – ${formatEnd(event.start, event.end, t, event.allday)} ${!event.allday ? getTimezoneOffset(timezone, new Date(event.start)) : ""}`} }