[#268] changed timezone offset calculation (#306)

Co-authored-by: Camille Moussu <cmoussu@linagora.com>
This commit is contained in:
Camille Moussu
2025-11-14 08:52:02 +01:00
committed by GitHub
parent 917954f7a5
commit 1bc9b0c160
6 changed files with 83 additions and 29 deletions
@@ -216,7 +216,7 @@ describe("EventDisplayPreview - Timezone Display", () => {
it("displays correct timezone offset for different timezones", async () => { it("displays correct timezone offset for different timezones", async () => {
const timezones = [ const timezones = [
{ tz: "America/New_York", expectedOffset: /UTC-[45]/ }, { tz: "America/New_York", expectedOffset: /UTC[-][45]/ },
{ tz: "Europe/Paris", expectedOffset: /UTC\+[12]/ }, { tz: "Europe/Paris", expectedOffset: /UTC\+[12]/ },
{ tz: "Asia/Tokyo", expectedOffset: /UTC\+9/ }, { tz: "Asia/Tokyo", expectedOffset: /UTC\+9/ },
{ tz: "Australia/Sydney", expectedOffset: /UTC\+1[01]/ }, { tz: "Australia/Sydney", expectedOffset: /UTC\+1[01]/ },
@@ -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 { TimezoneSelector } from "../../../src/components/Calendar/TimezoneSelector";
import { renderWithProviders } from "../../utils/Renderwithproviders"; import { renderWithProviders } from "../../utils/Renderwithproviders";
@@ -11,17 +11,25 @@ describe("TimezoneSelector", () => {
it("renders with initial timezone value", () => { it("renders with initial timezone value", () => {
renderWithProviders( renderWithProviders(
<TimezoneSelector value="America/New_York" onChange={mockOnChange} /> <TimezoneSelector
referenceDate={new Date()}
value="America/New_York"
onChange={mockOnChange}
/>
); );
const button = screen.getByRole("button"); const button = screen.getByRole("button");
expect(button).toBeInTheDocument(); 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 () => { it("opens popover when button is clicked", async () => {
renderWithProviders( renderWithProviders(
<TimezoneSelector value="Europe/Paris" onChange={mockOnChange} /> <TimezoneSelector
referenceDate={new Date()}
value="Europe/Paris"
onChange={mockOnChange}
/>
); );
const button = screen.getByRole("button"); const button = screen.getByRole("button");
@@ -34,7 +42,11 @@ describe("TimezoneSelector", () => {
it("calls onChange when a new timezone is selected", async () => { it("calls onChange when a new timezone is selected", async () => {
renderWithProviders( renderWithProviders(
<TimezoneSelector value="Europe/Paris" onChange={mockOnChange} /> <TimezoneSelector
referenceDate={new Date()}
value="Europe/Paris"
onChange={mockOnChange}
/>
); );
const button = screen.getByRole("button"); const button = screen.getByRole("button");
@@ -56,7 +68,11 @@ describe("TimezoneSelector", () => {
it("closes popover after timezone selection", async () => { it("closes popover after timezone selection", async () => {
renderWithProviders( renderWithProviders(
<TimezoneSelector value="Europe/Paris" onChange={mockOnChange} /> <TimezoneSelector
referenceDate={new Date()}
value="Europe/Paris"
onChange={mockOnChange}
/>
); );
const button = screen.getByRole("button"); const button = screen.getByRole("button");
@@ -79,10 +95,40 @@ describe("TimezoneSelector", () => {
it("displays timezones with half-hour offsets correctly", () => { it("displays timezones with half-hour offsets correctly", () => {
renderWithProviders( renderWithProviders(
<TimezoneSelector value="Asia/Kolkata" onChange={mockOnChange} /> <TimezoneSelector
referenceDate={new Date()}
value="Asia/Kolkata"
onChange={mockOnChange}
/>
); );
const button = screen.getByRole("button"); const button = screen.getByRole("button");
expect(button).toHaveTextContent("UTC+5:30"); // India offset 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(
<TimezoneSelector
value="Europe/Paris"
onChange={mockOnChange}
referenceDate={summerDate}
/>
);
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(
<TimezoneSelector
value="Europe/Paris"
onChange={mockOnChange}
referenceDate={winterDate}
/>
);
button = screen.getByRole("button");
expect(button).toHaveTextContent(/UTC\+1\b/);
});
}); });
+1
View File
@@ -407,6 +407,7 @@ export default function CalendarApp({
</div> </div>
<TimezoneSelector <TimezoneSelector
value={timezone} value={timezone}
referenceDate={calendarRef.current?.getDate() ?? new Date()}
onChange={(newTimezone: string) => onChange={(newTimezone: string) =>
dispatch(setTimeZone(newTimezone)) dispatch(setTimeZone(newTimezone))
} }
+23 -18
View File
@@ -1,5 +1,6 @@
import { Button, Popover } from "@mui/material"; import { Button, Popover } from "@mui/material";
import { useI18n } from "cozy-ui/transpiled/react/providers/I18n"; import { useI18n } from "cozy-ui/transpiled/react/providers/I18n";
import moment from "moment";
import { MouseEvent, useMemo, useState } from "react"; import { MouseEvent, useMemo, useState } from "react";
import { TIMEZONES } from "../../utils/timezone-data"; import { TIMEZONES } from "../../utils/timezone-data";
import { TimezoneAutocomplete } from "../Timezone/TimezoneAutocomplete"; import { TimezoneAutocomplete } from "../Timezone/TimezoneAutocomplete";
@@ -7,9 +8,14 @@ import { TimezoneAutocomplete } from "../Timezone/TimezoneAutocomplete";
interface TimezoneSelectProps { interface TimezoneSelectProps {
value: string; value: string;
onChange: (value: string) => void; onChange: (value: string) => void;
referenceDate: Date;
} }
export function TimezoneSelector({ value, onChange }: TimezoneSelectProps) { export function TimezoneSelector({
value,
onChange,
referenceDate,
}: TimezoneSelectProps) {
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null); const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
const timezoneList = useMemo(() => { const timezoneList = useMemo(() => {
@@ -24,7 +30,7 @@ export function TimezoneSelector({ value, onChange }: TimezoneSelectProps) {
const effectiveTimezone = value const effectiveTimezone = value
? resolveTimezone(value) ? resolveTimezone(value)
: timezoneList.browserTz; : timezoneList.browserTz;
const selectedOffset = getTimezoneOffset(effectiveTimezone); const selectedOffset = getTimezoneOffset(effectiveTimezone, referenceDate);
const handleOpen = (event: MouseEvent<HTMLElement>) => { const handleOpen = (event: MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget); setAnchorEl(event.currentTarget);
@@ -76,7 +82,9 @@ export function TimezoneSelector({ value, onChange }: TimezoneSelectProps) {
value={effectiveTimezone} value={effectiveTimezone}
onChange={onChange} onChange={onChange}
zones={timezoneList.zones} zones={timezoneList.zones}
getTimezoneOffset={getTimezoneOffset} getTimezoneOffset={(tzName: string) =>
getTimezoneOffset(tzName, referenceDate)
}
autoFocus={true} autoFocus={true}
showIcon={true} showIcon={true}
inputFontSize="14px" inputFontSize="14px"
@@ -99,20 +107,17 @@ export function resolveTimezone(tzName: string): string {
return tzName; return tzName;
} }
export function getTimezoneOffset(tzName: string): string { export function getTimezoneOffset(
const resolvedTz = resolveTimezone(tzName); tzName: string,
const tzData = TIMEZONES.zones[resolvedTz]; date: Date = new Date()
if (!tzData) return ""; ): string {
const fmt = new Intl.DateTimeFormat(undefined, {
timeZone: tzName,
timeZoneName: "shortOffset",
});
const icsMatch = tzData.ics.match(/TZOFFSETTO:([+-]\d{4})/); const currentDate = moment(date).isValid() ? date : new Date();
if (!icsMatch) return ""; const parts = fmt.formatToParts(currentDate);
const offsetPart = parts.find((p) => p.type === "timeZoneName");
const offset = icsMatch[1]; return offsetPart?.value.replace("GMT", "UTC") ?? "";
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")}`;
} }
+4 -2
View File
@@ -90,7 +90,7 @@ interface EventFormFieldsProps {
timezoneList: { timezoneList: {
zones: string[]; zones: string[];
browserTz: string; browserTz: string;
getTimezoneOffset: (tzName: string) => string; getTimezoneOffset: (tzName: string, date: Date) => string;
}; };
// Event handlers // Event handlers
@@ -534,7 +534,9 @@ export default function EventFormFields({
value={timezone} value={timezone}
onChange={setTimezone} onChange={setTimezone}
zones={timezoneList.zones} zones={timezoneList.zones}
getTimezoneOffset={timezoneList.getTimezoneOffset} getTimezoneOffset={(tzName: string) =>
timezoneList.getTimezoneOffset(tzName, new Date(start))
}
showIcon={true} showIcon={true}
width={240} width={240}
size="small" size="small"
+1 -1
View File
@@ -304,7 +304,7 @@ export default function EventPreviewModal({
{formatDate(event.start, t, event.allday)} {formatDate(event.start, t, event.allday)}
{event.end && {event.end &&
formatEnd(event.start, event.end, t, event.allday) && 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)) : ""}`}
</Typography> </Typography>
</> </>
} }