Co-authored-by: Camille Moussu <cmoussu@linagora.com>
This commit is contained in:
@@ -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/);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")}`;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user