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)) : ""}`}
>
}