diff --git a/packages/react/src/components/Forms/DatePicker/DateRangePicker.spec.tsx b/packages/react/src/components/Forms/DatePicker/DateRangePicker.spec.tsx
index 69a22e0..a860d1b 100644
--- a/packages/react/src/components/Forms/DatePicker/DateRangePicker.spec.tsx
+++ b/packages/react/src/components/Forms/DatePicker/DateRangePicker.spec.tsx
@@ -698,9 +698,7 @@ describe("
", () => {
/>
,
),
- ).toThrow(
- "Invalid date format when initializing props on DatePicker component",
- );
+ ).toThrow(/Failed to parse date value:/);
});
it("has not a valid range value", async () => {
@@ -991,6 +989,50 @@ describe("
", () => {
screen.getByText("Value = |");
});
+ it("has a timezone", async () => {
+ const user = userEvent.setup();
+ const Wrapper = () => {
+ const [value, setValue] = useState<[string, string] | null>(null);
+ return (
+
+
+
Value = {value?.join(" ")}|
+
+
setValue(e)}
+ timezone="America/Sao_Paulo"
+ />
+
+
+ );
+ };
+ render(
);
+
+ // Make sure any value is selected.
+ screen.getByText("Value = |");
+
+ const allSegments = await screen.getAllByRole("spinbutton");
+ const startMonthSegment = allSegments![0];
+
+ // Select the first segment, month one.
+ await user.click(startMonthSegment);
+ expect(startMonthSegment).toHaveFocus();
+
+ // Type start date's value.
+ await user.keyboard("{5}{1}{0}{2}{0}{2}{3}");
+
+ // Type end date's value.
+ await user.keyboard("{5}{1}{2}{2}{0}{2}{3}");
+
+ // Make sure values is selected at midnight on America/Sao_Paulo.
+ screen.getByText(
+ `Value = 2023-05-10T03:00:00.000Z 2023-05-12T03:00:00.000Z|`,
+ );
+ });
+
it("submits forms data", async () => {
let formData: any;
const Wrapper = () => {
diff --git a/packages/react/src/components/Forms/DatePicker/DateRangePicker.tsx b/packages/react/src/components/Forms/DatePicker/DateRangePicker.tsx
index 1d02b86..21544e3 100644
--- a/packages/react/src/components/Forms/DatePicker/DateRangePicker.tsx
+++ b/packages/react/src/components/Forms/DatePicker/DateRangePicker.tsx
@@ -40,14 +40,17 @@ export const DateRangePicker = ({
const options: DateRangePickerStateOptions
= {
...getDefaultPickerOptions(props),
- value: props.value === null ? null : parseRangeDateValue(props.value),
- defaultValue: parseRangeDateValue(props.defaultValue),
+ value:
+ props.value === null
+ ? null
+ : parseRangeDateValue(props.value, props.timezone),
+ defaultValue: parseRangeDateValue(props.defaultValue, props.timezone),
onChange: (value: DateRange) => {
props.onChange?.(
value?.start && value.end
? [
- convertDateValueToString(value.start),
- convertDateValueToString(value.end),
+ convertDateValueToString(value.start, props.timezone),
+ convertDateValueToString(value.end, props.timezone),
]
: null,
);
diff --git a/packages/react/src/components/Forms/DatePicker/utils.spec.ts b/packages/react/src/components/Forms/DatePicker/utils.spec.ts
index 9fc01dd..d581433 100644
--- a/packages/react/src/components/Forms/DatePicker/utils.spec.ts
+++ b/packages/react/src/components/Forms/DatePicker/utils.spec.ts
@@ -2,6 +2,7 @@ import { DateValue, parseAbsolute, parseDate } from "@internationalized/date";
import { vi } from "vitest";
import {
convertDateValueToString,
+ isValidTimeZone,
parseDateValue,
parseRangeDateValue,
} from ":/components/Forms/DatePicker/utils";
@@ -43,6 +44,15 @@ describe("parseDateValue", () => {
expect(parsedDate?.hour).eq(0);
});
+ it("should parse time to the right timezone", async () => {
+ const parsedDate = parseDateValue(
+ "2023-05-11T00:00:00.000Z",
+ "America/Sao_Paulo",
+ );
+ expectDateToBeEqual(parsedDate, 2023, 5, 10);
+ expect(parsedDate?.hour).eq(21);
+ });
+
it.each([undefined, ""])("parse an empty or null date", (date) => {
const parsedDate = parseDateValue(date);
expect(parsedDate).eq(undefined);
@@ -59,9 +69,15 @@ describe("parseDateValue", () => {
"2022-04-01T00:00:00.000",
])("parse a wrong date", (wrongFormattedDate) => {
expect(() => parseDateValue(wrongFormattedDate)).toThrow(
- "Invalid date format when initializing props on DatePicker component",
+ /Failed to parse date value:/,
);
});
+
+ it("should raise an error when timezone is invalid", async () => {
+ expect(() =>
+ parseDateValue("2023-05-11T00:00:00.000Z", "Invalid/Timezone"),
+ ).toThrow(/Failed to parse date value:/);
+ });
});
describe("parseRangeDateValue", () => {
@@ -119,4 +135,44 @@ describe("convertDateValueToString", () => {
const result = convertDateValueToString(date);
expect(result).eq("2023-05-24T22:00:00.000Z");
});
+
+ it("should convert time to the right timezone", async () => {
+ const date = parseDate("2023-05-25");
+ const result = convertDateValueToString(date, "America/Sao_Paulo");
+ expect(result).eq("2023-05-25T03:00:00.000Z");
+ });
+
+ it("should raise an error when timezone is invalid", async () => {
+ const date = parseDate("2023-05-25");
+ expect(() => convertDateValueToString(date, "Invalid/Timezone")).toThrow(
+ /Failed to convert date value to string:/,
+ );
+ });
+});
+
+describe("isValidTimeZone", () => {
+ it.each(["UTC", "Europe/Paris", "America/Sao_Paulo"])(
+ "should return true when timezone is valid",
+ (timezone) => {
+ const isValid = isValidTimeZone(timezone);
+ expect(isValid).toBe(true);
+ },
+ );
+
+ it("should return false when timezone is invalid", () => {
+ const isNotValid = isValidTimeZone("Invalid/Timezone");
+ expect(isNotValid).toBe(false);
+ });
+
+ it("should return false when Intl or time zones are not available", () => {
+ // Mock Intl to simulate the absence of Intl or time zones support
+ const originalDateTimeFormat = Intl.DateTimeFormat;
+ vi.spyOn(Intl, "DateTimeFormat").mockImplementation(() => {
+ throw new Error("Time zones are not available");
+ });
+ const result = isValidTimeZone("Europe/Paris");
+ expect(result).toBe(false);
+ // Restore the original implementation after the test
+ (Intl as any).DateTimeFormat = originalDateTimeFormat;
+ });
});
diff --git a/packages/react/src/components/Forms/DatePicker/utils.ts b/packages/react/src/components/Forms/DatePicker/utils.ts
index 5099aaf..d2f906f 100644
--- a/packages/react/src/components/Forms/DatePicker/utils.ts
+++ b/packages/react/src/components/Forms/DatePicker/utils.ts
@@ -4,53 +4,83 @@ import {
ZonedDateTime,
toZoned,
getLocalTimeZone,
+ parseAbsolute,
} from "@internationalized/date";
import { DateRange } from "react-aria";
import { DatePickerAuxSubProps } from ":/components/Forms/DatePicker/DatePickerAux";
+export const isValidTimeZone = (timezone: string) => {
+ try {
+ // Check if Intl is available and supports time zones
+ if (!Intl || !Intl.DateTimeFormat().resolvedOptions().timeZone) {
+ throw new Error("Time zones are not available in this environment");
+ }
+
+ // Test if the provided time zone is valid
+ Intl.DateTimeFormat(undefined, { timeZone: timezone });
+
+ return true;
+ } catch (error) {
+ // If an error occurs, it could be due to an invalid time zone or lack of Intl support
+ return false;
+ }
+};
+
export const parseDateValue = (
rawDate: string | undefined,
+ timezone?: string,
): undefined | ZonedDateTime => {
if (!rawDate) {
return undefined;
}
try {
- return parseAbsoluteToLocal(rawDate);
+ if (timezone && !isValidTimeZone(timezone)) {
+ throw new Error("Invalid timezone provided.");
+ }
+ return timezone
+ ? parseAbsolute(rawDate, timezone)
+ : parseAbsoluteToLocal(rawDate);
} catch (e) {
- throw new Error(
- "Invalid date format when initializing props on DatePicker component",
- );
+ const errorMessage = e instanceof Error ? ": " + e.message : ".";
+ throw new Error("Failed to parse date value" + errorMessage);
}
};
export const parseRangeDateValue = (
rawRange: [string, string] | undefined,
+ timezone?: string,
): DateRange | undefined => {
if (!rawRange || !rawRange[0] || !rawRange[1]) {
return undefined;
}
return {
- start: parseDateValue(rawRange[0])!,
- end: parseDateValue(rawRange[1])!,
+ start: parseDateValue(rawRange[0], timezone)!,
+ end: parseDateValue(rawRange[1], timezone)!,
};
};
-export const convertDateValueToString = (date: DateValue | null): string => {
+export const convertDateValueToString = (
+ date: DateValue | null,
+ timezone?: string,
+): string => {
try {
- const localTimezone = getLocalTimeZone();
- // If timezone is already set, it would be kept, else the selection is set at midnight
- // on the local timezone, then converted to a UTC offset.
- return date ? toZoned(date, localTimezone).toAbsoluteString() : "";
+ if (!date) {
+ return "";
+ }
+ const localTimezone = timezone || getLocalTimeZone();
+ if (!isValidTimeZone(localTimezone)) {
+ throw new Error("Invalid timezone provided.");
+ }
+ return toZoned(date, localTimezone).toAbsoluteString();
} catch (e) {
- throw new Error(
- "Invalid date format when converting date value on DatePicker component",
- );
+ const errorMessage = e instanceof Error ? ": " + e.message : ".";
+ throw new Error("Failed to convert date value to string" + errorMessage);
}
};
export const getDefaultPickerOptions = (props: DatePickerAuxSubProps): any => ({
- minValue: parseDateValue(props.minValue),
- maxValue: parseDateValue(props.maxValue),
+ minValue: parseDateValue(props.minValue, props.timezone),
+ maxValue: parseDateValue(props.maxValue, props.timezone),
shouldCloseOnSelect: true,
granularity: "day",
isDisabled: props.disabled,