(react) restrict inputs formats in date picker components

Enforces a standardized approach for end consumers using the
component's API. Consumers are now required to provide a date as an
ISO string in either UTC or with a UTC offset. The support for native
Date objects has been removed to ensure consistent and reliable
behavior across all implementations. By adopting this uniform input
format, we can simplify usage and avoid potential inconsistencies in
date handling.
This commit is contained in:
Lebaud Antoine
2023-07-12 22:51:32 +02:00
committed by aleb_the_flash
parent 8cf8e1eba2
commit 0dc46d1144
11 changed files with 403 additions and 324 deletions

View File

@@ -0,0 +1,5 @@
---
"@openfun/cunningham-react": minor
---
Restrict input formats of date picker components to IS0 strings

View File

@@ -8,7 +8,7 @@ import { Button } from ":/components/Button";
vi.mock("@internationalized/date", async () => { vi.mock("@internationalized/date", async () => {
const mod = await vi.importActual<typeof import("@internationalized/date")>( const mod = await vi.importActual<typeof import("@internationalized/date")>(
"@internationalized/date" "@internationalized/date",
); );
return { return {
...mod, ...mod,
@@ -193,7 +193,7 @@ describe("<DatePicker/>", () => {
<DatePicker <DatePicker
label="Pick a date" label="Pick a date"
name="datepicker" name="datepicker"
defaultValue="2023-04-25" defaultValue="2023-04-25T00:00:00.000Z"
/> />
</CunninghamProvider>, </CunninghamProvider>,
); );
@@ -282,7 +282,7 @@ describe("<DatePicker/>", () => {
<DatePicker <DatePicker
label="Pick a date" label="Pick a date"
name="datepicker" name="datepicker"
defaultValue="2023-04-05" defaultValue="2023-04-05T00:00:00.000Z"
/> />
</CunninghamProvider>, </CunninghamProvider>,
); );
@@ -317,8 +317,8 @@ describe("<DatePicker/>", () => {
<DatePicker <DatePicker
label="Pick a date" label="Pick a date"
name="datepicker" name="datepicker"
defaultValue="2023-04-05" defaultValue="2023-04-05T10:00:00.000Z"
minValue="2022-12-03" minValue="2022-12-03T10:00:00.000Z"
/> />
</CunninghamProvider>, </CunninghamProvider>,
); );
@@ -350,12 +350,9 @@ describe("<DatePicker/>", () => {
}); });
it.each([ it.each([
"2022-05-25", "2022-05-25T23:59:59.000Z",
"2023-06-01T13:50", "2023-06-01T00:00:00.000Z",
"2025-03-25", "2025-03-25T12:30:00.000Z",
"Mar 25 2025",
"25 Mar 2025",
new Date("2025-04-23"),
])("has a default value", async (defaultValue) => { ])("has a default value", async (defaultValue) => {
render( render(
<CunninghamProvider> <CunninghamProvider>
@@ -373,8 +370,6 @@ describe("<DatePicker/>", () => {
}); });
it("has an uncontrolled a controlled value", async () => { it("has an uncontrolled a controlled value", async () => {
const defaultValue = new Date("2023-05-24");
const value = new Date("2023-05-24");
vi.spyOn(console, "error").mockImplementation(() => undefined); vi.spyOn(console, "error").mockImplementation(() => undefined);
expect(() => expect(() =>
render( render(
@@ -382,8 +377,8 @@ describe("<DatePicker/>", () => {
<DatePicker <DatePicker
label="Pick a date" label="Pick a date"
name="datepicker" name="datepicker"
defaultValue={defaultValue} defaultValue="2023-05-24T00:00:00.000Z"
value={value} value="2023-05-24T00:00:00.000Z"
/> />
</CunninghamProvider>, </CunninghamProvider>,
), ),
@@ -413,14 +408,13 @@ describe("<DatePicker/>", () => {
); );
it("clears date", async () => { it("clears date", async () => {
const defaultValue = new Date("2023-05-24");
const user = userEvent.setup(); const user = userEvent.setup();
render( render(
<CunninghamProvider> <CunninghamProvider>
<DatePicker <DatePicker
label="Pick a date" label="Pick a date"
name="datepicker" name="datepicker"
defaultValue={defaultValue} defaultValue="2023-05-24T00:00:00.000Z"
/> />
</CunninghamProvider>, </CunninghamProvider>,
); );
@@ -435,7 +429,7 @@ describe("<DatePicker/>", () => {
expect(dateFieldContent).eq("mm/dd/yyyy"); expect(dateFieldContent).eq("mm/dd/yyyy");
const isGridCellSelected = screen const isGridCellSelected = screen
.getByRole("gridcell", { name: `${defaultValue.getDate()}` })! .getByRole("gridcell", { name: "24" })!
.getAttribute("aria-selected"); .getAttribute("aria-selected");
expect(isGridCellSelected).toBeNull(); expect(isGridCellSelected).toBeNull();
@@ -453,8 +447,8 @@ describe("<DatePicker/>", () => {
<DatePicker <DatePicker
label="Pick a date" label="Pick a date"
name="datepicker" name="datepicker"
defaultValue="2023-01-01" defaultValue="2023-01-01T00:00:00.000Z"
minValue="2023-02-01" minValue="2023-02-01T00:00:00.000Z"
/> />
</CunninghamProvider>, </CunninghamProvider>,
); );
@@ -467,8 +461,8 @@ describe("<DatePicker/>", () => {
<DatePicker <DatePicker
label="Pick a date" label="Pick a date"
name="datepicker" name="datepicker"
defaultValue="2023-03-01" defaultValue="2023-03-01T00:00:00.000Z"
maxValue="2023-02-01" maxValue="2023-02-01T00:00:00.000Z"
/> />
</CunninghamProvider>, </CunninghamProvider>,
); );
@@ -482,9 +476,9 @@ describe("<DatePicker/>", () => {
<DatePicker <DatePicker
label="Pick a date" label="Pick a date"
name="datepicker" name="datepicker"
defaultValue="2023-03-01" defaultValue="2023-03-01T00:00:00.000Z"
maxValue="2023-04-01" maxValue="2023-04-01T00:00:00.000Z"
minValue="2023-05-01" minValue="2023-05-01T00:00:00.000Z"
/> />
</CunninghamProvider>, </CunninghamProvider>,
); );
@@ -494,7 +488,9 @@ describe("<DatePicker/>", () => {
it("works controlled", async () => { it("works controlled", async () => {
const user = userEvent.setup(); const user = userEvent.setup();
const Wrapper = () => { const Wrapper = () => {
const [value, setValue] = useState<string | null>("2023-04-25"); const [value, setValue] = useState<string | null>(
"2023-04-25T00:00:00.000Z",
);
return ( return (
<CunninghamProvider> <CunninghamProvider>
<div> <div>
@@ -513,11 +509,11 @@ describe("<DatePicker/>", () => {
render(<Wrapper />); render(<Wrapper />);
// Make sure value is selected. // Make sure value is selected.
screen.getByText("Value = 2023-04-25|"); screen.getByText("Value = 2023-04-25T00:00:00.000Z|");
// Make sure value is initially render in the date field component. // Make sure value is initially render in the date field component.
const dateFieldContent = screen.getByRole("presentation").textContent; const dateFieldContent = screen.getByRole("presentation").textContent;
expectDatesToBeEqual("2023-04-25", dateFieldContent); expectDatesToBeEqual("2023-04-25T00:00:00.000Z", dateFieldContent);
// Open the calendar grid. // Open the calendar grid.
const toggleButton = (await screen.findAllByRole("button"))![1]; const toggleButton = (await screen.findAllByRole("button"))![1];
@@ -525,7 +521,7 @@ describe("<DatePicker/>", () => {
expectCalendarToBeOpen(); expectCalendarToBeOpen();
const gridCell = within( const gridCell = within(
await screen.getByRole("gridcell", { name: "12" }), screen.getByRole("gridcell", { name: "12" }),
).getByRole("button")!; ).getByRole("button")!;
// Select a new value in the calendar grid. // Select a new value in the calendar grid.
@@ -533,7 +529,7 @@ describe("<DatePicker/>", () => {
expectCalendarToBeClosed(); expectCalendarToBeClosed();
// Make sure value is selected. // Make sure value is selected.
screen.getByText(`Value = 2023-04-11T22:00:00.000Z|`); screen.getByText(`Value = 2023-04-12T00:00:00.000Z|`);
// Clear value. // Clear value.
const clearButton = screen.getByRole("button", { const clearButton = screen.getByRole("button", {
@@ -552,7 +548,7 @@ describe("<DatePicker/>", () => {
<DatePicker <DatePicker
label="Pick a date" label="Pick a date"
name="datepicker" name="datepicker"
defaultValue="2023-03-01" defaultValue="2023-03-01T00:00:00.000Z"
disabled={true} disabled={true}
/> />
</CunninghamProvider>, </CunninghamProvider>,
@@ -584,7 +580,7 @@ describe("<DatePicker/>", () => {
<DatePicker <DatePicker
label="Pick a date" label="Pick a date"
name="datepicker" name="datepicker"
defaultValue="2023-03-01" defaultValue="2023-03-01T00:00:00.000Z"
/> />
</CunninghamProvider>, </CunninghamProvider>,
); );
@@ -649,6 +645,7 @@ describe("<DatePicker/>", () => {
expectDateFieldToBeDisplayed(); expectDateFieldToBeDisplayed();
// Make sure form's value matches. // Make sure form's value matches.
// It should be equal the 12th of May at midnight in local timezone.
expect(formData).toEqual({ expect(formData).toEqual({
datepicker: "2023-05-11T22:00:00.000Z", datepicker: "2023-05-11T22:00:00.000Z",
}); });
@@ -663,7 +660,7 @@ describe("<DatePicker/>", () => {
// Submit the form being empty. // Submit the form being empty.
await user.click(submitButton); await user.click(submitButton);
// Date field disappears when the user click outside the comppnent. // Date field disappears when the user click outside the component.
expectDateFieldToBeHidden(); expectDateFieldToBeHidden();
// Make sure form's value is null. // Make sure form's value is null.
@@ -672,15 +669,118 @@ describe("<DatePicker/>", () => {
}); });
}); });
it("submits forms data with a default value", async () => {
let formData: any;
const Wrapper = () => {
const onSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const data = new FormData(e.currentTarget);
formData = {
datepicker: data.get("datepicker"),
};
};
return (
<CunninghamProvider>
<div>
<form onSubmit={onSubmit}>
<DatePicker
label="Pick a date"
name="datepicker"
defaultValue="2023-04-25T12:00:00.000Z"
/>
<Button>Submit</Button>
</form>
</div>
</CunninghamProvider>
);
};
render(<Wrapper />);
const user = userEvent.setup();
const submitButton = screen.getByRole("button", {
name: "Submit",
});
// Submit the form with the default value.
// Time should be equal to the initial one in UTC.
await user.click(submitButton);
expect(formData).toEqual({
datepicker: "2023-04-25T12:00:00.000Z",
});
// Open calendar
const toggleButton = (await screen.findAllByRole("button"))![1];
await user.click(toggleButton);
const monthSegment = await screen.getByRole("spinbutton", {
name: /month/,
});
// Select the first segment, month one.
await user.click(monthSegment);
expect(monthSegment).toHaveFocus();
// Type date's value.
await user.keyboard("{5}{1}{2}{2}{0}{2}{3}");
// Submit form being filled with a date.
await user.click(submitButton);
expectCalendarToBeClosed();
expectDateFieldToBeDisplayed();
// Make sure form's value matches.
// Selection should keep the default time passed to the component.
// Thus, component output should still be at noon UTC.
expect(formData).toEqual({
datepicker: "2023-05-12T12:00:00.000Z",
});
// Clear picked date.
// We lose info about the initial time.
const clearButton = screen.getByRole("button", {
name: "Clear date",
});
await user.click(clearButton);
expectDateFieldToBeDisplayed();
// Submit the form being empty.
await user.click(submitButton);
// Date field disappears when the user click outside the component.
expectDateFieldToBeHidden();
// Make sure form's value is null.
expect(formData).toEqual({
datepicker: "",
});
// Select the first segment, month one.
await user.click(monthSegment);
expect(monthSegment).toHaveFocus();
// Type a new date's value, that would be selected at midnight local timezone.
await user.keyboard("{5}{1}{2}{2}{0}{2}{3}");
// Submit form being filled with a date.
await user.click(submitButton);
expectCalendarToBeClosed();
expectDateFieldToBeDisplayed();
// Make sure form's value matches.
// It should be equal the 2023-05-12 at midnight in local timezone.
expect(formData).toEqual({
datepicker: "2023-05-11T22:00:00.000Z",
});
});
it("clicks next and previous focused month", async () => { it("clicks next and previous focused month", async () => {
const defaultValue = new Date("2023-05-24");
const user = userEvent.setup(); const user = userEvent.setup();
render( render(
<CunninghamProvider> <CunninghamProvider>
<DatePicker <DatePicker
label="Pick a date" label="Pick a date"
name="datepicker" name="datepicker"
defaultValue={defaultValue} defaultValue="2023-05-24T00:00:00.000Z"
/> />
</CunninghamProvider>, </CunninghamProvider>,
); );
@@ -711,14 +811,13 @@ describe("<DatePicker/>", () => {
}); });
it("clicks next and previous focused year", async () => { it("clicks next and previous focused year", async () => {
const defaultValue = new Date("2023-05-24");
const user = userEvent.setup(); const user = userEvent.setup();
render( render(
<CunninghamProvider> <CunninghamProvider>
<DatePicker <DatePicker
label="Pick a date" label="Pick a date"
name="datepicker" name="datepicker"
defaultValue={defaultValue} defaultValue="2023-05-24T00:00:00.000Z"
/> />
</CunninghamProvider>, </CunninghamProvider>,
); );
@@ -749,18 +848,15 @@ describe("<DatePicker/>", () => {
}); });
it("renders disabled next and previous month", async () => { it("renders disabled next and previous month", async () => {
const defaultValue = new Date("2023-05-24");
const minValue = new Date("2023-05-22");
const maxValue = new Date("2023-05-26");
const user = userEvent.setup(); const user = userEvent.setup();
render( render(
<CunninghamProvider> <CunninghamProvider>
<DatePicker <DatePicker
label="Pick a date" label="Pick a date"
name="datepicker" name="datepicker"
defaultValue={defaultValue} defaultValue="2023-05-24T00:00:00.000Z"
minValue={minValue} minValue="2023-05-22T00:00:00.000Z"
maxValue={maxValue} maxValue="2023-05-26T00:00:00.000Z"
/> />
</CunninghamProvider>, </CunninghamProvider>,
); );
@@ -782,18 +878,15 @@ describe("<DatePicker/>", () => {
}); });
it("renders disabled next and previous year", async () => { it("renders disabled next and previous year", async () => {
const defaultValue = new Date("2023-05-24");
const minValue = new Date("2023-05-22");
const maxValue = new Date("2023-05-26");
const user = userEvent.setup(); const user = userEvent.setup();
render( render(
<CunninghamProvider> <CunninghamProvider>
<DatePicker <DatePicker
label="Pick a date" label="Pick a date"
name="datepicker" name="datepicker"
defaultValue={defaultValue} defaultValue="2023-05-24T00:00:00.000Z"
minValue={minValue} minValue="2023-05-22T00:00:00.000Z"
maxValue={maxValue} maxValue="2023-05-26T00:00:00.000Z"
/> />
</CunninghamProvider>, </CunninghamProvider>,
); );
@@ -816,16 +909,16 @@ describe("<DatePicker/>", () => {
it("renders partially disabled next and previous month", async () => { it("renders partially disabled next and previous month", async () => {
const user = userEvent.setup(); const user = userEvent.setup();
const minValue = new Date("2023-04-23"); const minValue = new Date("2023-04-23T00:00:00.000z");
const maxValue = new Date("2023-06-23"); const maxValue = new Date("2023-06-23T00:00:00.000z");
render( render(
<CunninghamProvider> <CunninghamProvider>
<DatePicker <DatePicker
label="Pick a date" label="Pick a date"
name="datepicker" name="datepicker"
defaultValue="2023-05-23" defaultValue="2023-05-23T00:00:00.000Z"
minValue={minValue} minValue={minValue.toISOString()}
maxValue={maxValue} maxValue={maxValue.toISOString()}
/> />
</CunninghamProvider>, </CunninghamProvider>,
); );
@@ -915,13 +1008,12 @@ describe("<DatePicker/>", () => {
it("selects a focused month", async () => { it("selects a focused month", async () => {
const user = userEvent.setup(); const user = userEvent.setup();
const defaultValue = new Date("2023-05-23");
render( render(
<CunninghamProvider> <CunninghamProvider>
<DatePicker <DatePicker
label="Pick a date" label="Pick a date"
name="datepicker" name="datepicker"
defaultValue={defaultValue} defaultValue="2023-05-23T00:00:00.000Z"
/> />
</CunninghamProvider>, </CunninghamProvider>,
); );
@@ -952,7 +1044,7 @@ describe("<DatePicker/>", () => {
// Select a month option. // Select a month option.
const option: HTMLLIElement = screen.getByRole("option", { const option: HTMLLIElement = screen.getByRole("option", {
name: "September", name: "August",
}); });
await user.click(option); await user.click(option);
@@ -961,18 +1053,17 @@ describe("<DatePicker/>", () => {
// Make sure focused month has properly updated. // Make sure focused month has properly updated.
focusedMonth = monthDropdown.textContent?.replace("arrow_drop_down", ""); focusedMonth = monthDropdown.textContent?.replace("arrow_drop_down", "");
expect(focusedMonth).eq("Sep"); expect(focusedMonth).eq("Aug");
}); });
it("selects a focused year", async () => { it("selects a focused year", async () => {
const user = userEvent.setup(); const user = userEvent.setup();
const defaultValue = new Date("2023-05-23");
render( render(
<CunninghamProvider> <CunninghamProvider>
<DatePicker <DatePicker
label="Pick a date" label="Pick a date"
name="datepicker" name="datepicker"
defaultValue={defaultValue} defaultValue="2023-05-23T00:00:00.000Z"
/> />
</CunninghamProvider>, </CunninghamProvider>,
); );
@@ -1013,13 +1104,13 @@ describe("<DatePicker/>", () => {
it("renders only cell within the focused month", async () => { it("renders only cell within the focused month", async () => {
const user = userEvent.setup(); const user = userEvent.setup();
const defaultValue = new Date("2023-05-23"); const defaultValue = new Date("2023-05-23T00:00:00.000Z");
render( render(
<CunninghamProvider> <CunninghamProvider>
<DatePicker <DatePicker
label="Pick a date" label="Pick a date"
name="datepicker" name="datepicker"
defaultValue={defaultValue} defaultValue={defaultValue.toISOString()}
/> />
</CunninghamProvider>, </CunninghamProvider>,
); );
@@ -1053,13 +1144,12 @@ describe("<DatePicker/>", () => {
it("navigate previous focused month with keyboard", async () => { it("navigate previous focused month with keyboard", async () => {
const user = userEvent.setup(); const user = userEvent.setup();
const defaultValue = new Date("2023-05-01");
render( render(
<CunninghamProvider> <CunninghamProvider>
<DatePicker <DatePicker
label="Pick a date" label="Pick a date"
name="datepicker" name="datepicker"
defaultValue={defaultValue} defaultValue="2023-05-01T00:00:00.000Z"
/> />
</CunninghamProvider>, </CunninghamProvider>,
); );
@@ -1086,13 +1176,12 @@ describe("<DatePicker/>", () => {
it("navigate next focused month with keyboard", async () => { it("navigate next focused month with keyboard", async () => {
const user = userEvent.setup(); const user = userEvent.setup();
const defaultValue = new Date("2023-05-31");
render( render(
<CunninghamProvider> <CunninghamProvider>
<DatePicker <DatePicker
label="Pick a date" label="Pick a date"
name="datepicker" name="datepicker"
defaultValue={defaultValue} defaultValue="2023-05-31T00:00:00.000Z"
/> />
</CunninghamProvider>, </CunninghamProvider>,
); );
@@ -1123,7 +1212,7 @@ describe("<DatePicker/>", () => {
<DatePicker <DatePicker
label="Pick a date" label="Pick a date"
locale="hi-IN-u-ca-indian" locale="hi-IN-u-ca-indian"
defaultValue="2023-06-25" defaultValue="2023-06-25T00:00:00.000Z"
/> />
</CunninghamProvider>, </CunninghamProvider>,
); );
@@ -1169,7 +1258,10 @@ describe("<DatePicker/>", () => {
const user = userEvent.setup(); const user = userEvent.setup();
render( render(
<CunninghamProvider currentLocale="fr-FR"> <CunninghamProvider currentLocale="fr-FR">
<DatePicker label="Pick a date" defaultValue="2023-06-25" /> <DatePicker
label="Pick a date"
defaultValue="2023-06-25T00:00:00.000Z"
/>
</CunninghamProvider>, </CunninghamProvider>,
); );
@@ -1216,7 +1308,7 @@ describe("<DatePicker/>", () => {
<CunninghamProvider currentLocale="fr-FR"> <CunninghamProvider currentLocale="fr-FR">
<DatePicker <DatePicker
label="Pick a date" label="Pick a date"
defaultValue="2023-06-25" defaultValue="2023-06-25T00:00:00.000Z"
locale="hi-IN-u-ca-indian" locale="hi-IN-u-ca-indian"
/> />
</CunninghamProvider>, </CunninghamProvider>,
@@ -1271,4 +1363,51 @@ describe("<DatePicker/>", () => {
), ),
).toThrow("Incorrect locale information provided"); ).toThrow("Incorrect locale information provided");
}); });
it("keeps time component while selecting a date", async () => {
const user = userEvent.setup();
const Wrapper = () => {
const [value, setValue] = useState<string | null>(
"2023-04-25T12:00:00.000Z",
);
return (
<CunninghamProvider>
<div>
<div>Value = {value}|</div>
<Button onClick={() => setValue(null)}>Clear</Button>
<DatePicker
label="Pick a date"
name="datepicker"
value={value}
onChange={(e: string | null) => setValue(e)}
/>
</div>
</CunninghamProvider>
);
};
render(<Wrapper />);
// Make sure initial value is printed.
screen.getByText("Value = 2023-04-25T12:00:00.000Z|");
// Make sure value is initially rendered in the date field component.
const dateFieldContent = screen.getByRole("presentation").textContent;
expectDatesToBeEqual("2023-04-25", dateFieldContent);
// Open the calendar grid.
const toggleButton = (await screen.findAllByRole("button"))![1];
await user.click(toggleButton);
expectCalendarToBeOpen();
const gridCell = within(
screen.getByRole("gridcell", { name: "12" }),
).getByRole("button")!;
// Select a new value in the calendar grid.
await user.click(gridCell);
expectCalendarToBeClosed();
// Make sure value is selected, with the same time as the initial value.
screen.getByText(`Value = 2023-04-12T12:00:00.000Z|`);
});
}); });

View File

@@ -10,17 +10,16 @@ import DatePickerAux, {
} from ":/components/Forms/DatePicker/DatePickerAux"; } from ":/components/Forms/DatePicker/DatePickerAux";
import { Calendar } from ":/components/Forms/DatePicker/Calendar"; import { Calendar } from ":/components/Forms/DatePicker/Calendar";
import DateFieldBox from ":/components/Forms/DatePicker/DateField"; import DateFieldBox from ":/components/Forms/DatePicker/DateField";
import { StringOrDate } from ":/components/Forms/DatePicker/types";
import { import {
convertDateValueToString, convertDateValueToString,
getDefaultPickerOptions, getDefaultPickerOptions,
parseCalendarDate, parseDateValue,
} from ":/components/Forms/DatePicker/utils"; } from ":/components/Forms/DatePicker/utils";
export type DatePickerProps = DatePickerAuxSubProps & { export type DatePickerProps = DatePickerAuxSubProps & {
value?: null | StringOrDate; value?: null | string;
label: string; label: string;
defaultValue?: StringOrDate; defaultValue?: string;
onChange?: (value: string | null) => void | undefined; onChange?: (value: string | null) => void | undefined;
}; };
@@ -39,8 +38,8 @@ export const DatePicker = (props: DatePickerProps) => {
// Force clear the component's value when passing null or an empty string. // Force clear the component's value when passing null or an empty string.
props.value === "" || props.value === null props.value === "" || props.value === null
? null ? null
: parseCalendarDate(props.value), : parseDateValue(props.value),
defaultValue: parseCalendarDate(props.defaultValue), defaultValue: parseDateValue(props.defaultValue),
onChange: (value: DateValue | null) => { onChange: (value: DateValue | null) => {
props.onChange?.(convertDateValueToString(value)); props.onChange?.(convertDateValueToString(value));
}, },

View File

@@ -16,7 +16,6 @@ import { Button } from ":/components/Button";
import { Popover } from ":/components/Popover"; import { Popover } from ":/components/Popover";
import { Field, FieldProps } from ":/components/Forms/Field"; import { Field, FieldProps } from ":/components/Forms/Field";
import { useCunningham } from ":/components/Provider"; import { useCunningham } from ":/components/Provider";
import { StringOrDate } from ":/components/Forms/DatePicker/types";
import { import {
Calendar, Calendar,
CalendarRange, CalendarRange,
@@ -25,8 +24,8 @@ import { convertDateValueToString } from ":/components/Forms/DatePicker/utils";
export type DatePickerAuxSubProps = FieldProps & { export type DatePickerAuxSubProps = FieldProps & {
label?: string; label?: string;
minValue?: StringOrDate; minValue?: string;
maxValue?: StringOrDate; maxValue?: string;
disabled?: boolean; disabled?: boolean;
name?: string; name?: string;
locale?: string; locale?: string;

View File

@@ -8,7 +8,7 @@ import { Button } from ":/components/Button";
vi.mock("@internationalized/date", async () => { vi.mock("@internationalized/date", async () => {
const mod = await vi.importActual<typeof import("@internationalized/date")>( const mod = await vi.importActual<typeof import("@internationalized/date")>(
"@internationalized/date" "@internationalized/date",
); );
return { return {
...mod, ...mod,
@@ -151,9 +151,8 @@ describe("<DateRangePicker/>", () => {
}); });
it.each([ it.each([
["2022-05-25", "2022-05-26"], ["2022-05-25T00:00:00.000Z", "2022-05-26T00:00:00.000Z"],
["25 Mar 2025", "2029-05-26"], ["2023-06-01T00:00:00.000Z", "2024-05-26T00:00:00.000Z"],
["2023-06-01T13:50", "2024-05-26"],
])("has a default value", async (start: string, end: string) => { ])("has a default value", async (start: string, end: string) => {
render( render(
<CunninghamProvider> <CunninghamProvider>
@@ -180,7 +179,10 @@ describe("<DateRangePicker/>", () => {
startLabel="Start date" startLabel="Start date"
endLabel="End date" endLabel="End date"
name="datepicker" name="datepicker"
defaultValue={["2023-04-25", "2023-05-25"]} defaultValue={[
"2023-04-25T00:00:00.000Z",
"2023-05-25T00:00:00.000Z",
]}
/> />
</CunninghamProvider>, </CunninghamProvider>,
); );
@@ -375,7 +377,10 @@ describe("<DateRangePicker/>", () => {
<DateRangePicker <DateRangePicker
startLabel="Start date" startLabel="Start date"
endLabel="End date" endLabel="End date"
defaultValue={["2023-01-01", "2023-01-01"]} defaultValue={[
"2023-01-01T00:00:00.000Z",
"2023-01-01T00:00:00.000Z",
]}
name="datepicker" name="datepicker"
/> />
</CunninghamProvider>, </CunninghamProvider>,
@@ -437,7 +442,10 @@ describe("<DateRangePicker/>", () => {
startLabel="Start date" startLabel="Start date"
endLabel="End date" endLabel="End date"
name="datepicker" name="datepicker"
defaultValue={["2023-01-01", "2023-01-01"]} defaultValue={[
"2023-01-01T00:00:00.000Z",
"2023-01-01T00:00:00.000Z",
]}
/> />
</CunninghamProvider>, </CunninghamProvider>,
); );
@@ -486,7 +494,10 @@ describe("<DateRangePicker/>", () => {
startLabel="Start date" startLabel="Start date"
endLabel="End date" endLabel="End date"
name="datepicker" name="datepicker"
defaultValue={["2023-01-01", "2023-01-01"]} defaultValue={[
"2023-01-01T00:00:00.000Z",
"2023-01-01T00:00:00.000Z",
]}
/> />
</CunninghamProvider>, </CunninghamProvider>,
); );
@@ -657,8 +668,11 @@ describe("<DateRangePicker/>", () => {
startLabel="Start date" startLabel="Start date"
endLabel="End date" endLabel="End date"
name="datepicker" name="datepicker"
defaultValue={["2022-05-25", "2022-05-26"]} defaultValue={[
value={["2022-05-25", "2022-05-26"]} "2022-05-25T00:00:00.000Z",
"2022-05-26T00:00:00.000Z",
]}
value={["2022-05-25T00:00:00.000Z", "2022-05-26T00:00:00.000Z"]}
/> />
</CunninghamProvider>, </CunninghamProvider>,
), ),
@@ -668,9 +682,9 @@ describe("<DateRangePicker/>", () => {
}); });
it.each([ it.each([
["this_not_a_valid_date", "2022-05-12"], ["this_not_a_valid_date", "2022-05-12T00:00:00.000Z"],
["2022-05-12", "2023-13-13"], ["2022-05-12T00:00:00.000Z", "2023-13-13"],
["2025-25-05", "2022-05-12"], ["2025-25-05T00:00:00.000Z", "2022-05-12T00:00:00.000Z"],
])("has not a valid range value", async (start: string, end: string) => { ])("has not a valid range value", async (start: string, end: string) => {
vi.spyOn(console, "error").mockImplementation(() => undefined); vi.spyOn(console, "error").mockImplementation(() => undefined);
expect(() => expect(() =>
@@ -697,16 +711,17 @@ describe("<DateRangePicker/>", () => {
startLabel="Start date" startLabel="Start date"
endLabel="End date" endLabel="End date"
name="datepicker" name="datepicker"
defaultValue={["2024-05-25", "2022-05-26"]} defaultValue={[
"2024-05-25T00:00:00.000Z",
"2022-05-26T00:00:00.000Z",
]}
/> />
</CunninghamProvider>, </CunninghamProvider>,
); );
await expectDateRangePickerStateToBe("invalid"); await expectDateRangePickerStateToBe("invalid");
}); });
it.each([[new Date(2022, 5, 25), new Date(2022, 5, 27)]])( it("clears date", async () => {
"clears date",
async (start: Date, end: Date) => {
const user = userEvent.setup(); const user = userEvent.setup();
render( render(
<CunninghamProvider> <CunninghamProvider>
@@ -714,7 +729,10 @@ describe("<DateRangePicker/>", () => {
startLabel="Start date" startLabel="Start date"
endLabel="End date" endLabel="End date"
name="datepicker" name="datepicker"
defaultValue={[start, end]} defaultValue={[
"2023-05-25T00:00:00.000Z",
"2023-05-27T00:00:00.000Z",
]}
/> />
</CunninghamProvider>, </CunninghamProvider>,
); );
@@ -726,14 +744,12 @@ describe("<DateRangePicker/>", () => {
expectCalendarToBeOpen(); expectCalendarToBeOpen();
// Date field's value should be set to a placeholder value. // Date field's value should be set to a placeholder value.
const [startInput, endInput] = await screen.queryAllByRole( const [startInput, endInput] = await screen.queryAllByRole("presentation");
"presentation",
);
expect(startInput.textContent).eq("mm/dd/yyyy"); expect(startInput.textContent).eq("mm/dd/yyyy");
expect(endInput.textContent).eq("mm/dd/yyyy"); expect(endInput.textContent).eq("mm/dd/yyyy");
const startGridCell = screen.getByRole("gridcell", { const startGridCell = screen.getByRole("gridcell", {
name: `${start.getDate()}`, name: "25",
})!; })!;
// Make sure start grid-cell is not selected anymore. // Make sure start grid-cell is not selected anymore.
@@ -746,7 +762,7 @@ describe("<DateRangePicker/>", () => {
// Make sure end grid-cell is not selected anymore. // Make sure end grid-cell is not selected anymore.
const endGridCell = screen.getByRole("gridcell", { const endGridCell = screen.getByRole("gridcell", {
name: `${end.getDate()}`, name: "27",
})!; })!;
expect(endGridCell.getAttribute("aria-selected")).toBeNull(); expect(endGridCell.getAttribute("aria-selected")).toBeNull();
expect( expect(
@@ -761,12 +777,11 @@ describe("<DateRangePicker/>", () => {
// Make sure the empty date field is hidden when closing the calendar. // Make sure the empty date field is hidden when closing the calendar.
await expectDateFieldsToBeHidden(); await expectDateFieldsToBeHidden();
}, });
);
it.each([ it.each([
["2023-01-01", "2023-01-01"], ["2023-01-01T00:00:00.000Z", "2023-01-01T00:00:00.000Z"],
["2023-01-01", "2023-03-01"], ["2023-01-01T00:00:00.000Z", "2023-03-01T00:00:00.000Z"],
])( ])(
"has a start or a end date inferior to minValue", "has a start or a end date inferior to minValue",
async (start: string, end: string) => { async (start: string, end: string) => {
@@ -777,7 +792,7 @@ describe("<DateRangePicker/>", () => {
endLabel="End date" endLabel="End date"
name="datepicker" name="datepicker"
defaultValue={[start, end]} defaultValue={[start, end]}
minValue="2023-02-01" minValue="2023-02-01T00:00:00.000Z"
/> />
</CunninghamProvider>, </CunninghamProvider>,
); );
@@ -786,8 +801,8 @@ describe("<DateRangePicker/>", () => {
); );
it.each([ it.each([
["2023-01-01", "2023-03-01"], ["2023-01-01T00:00:00.000Z", "2023-03-01T00:00:00.000Z"],
["2023-03-01", "2023-03-01"], ["2023-03-01T00:00:00.000Z", "2023-03-01T00:00:00.000Z"],
])( ])(
"has a start or a end date superior to maxValue", "has a start or a end date superior to maxValue",
async (start: string, end: string) => { async (start: string, end: string) => {
@@ -798,7 +813,7 @@ describe("<DateRangePicker/>", () => {
endLabel="End date" endLabel="End date"
name="datepicker" name="datepicker"
defaultValue={[start, end]} defaultValue={[start, end]}
maxValue="2023-02-01" maxValue="2023-02-01T00:00:00.000Z"
/> />
</CunninghamProvider>, </CunninghamProvider>,
); );
@@ -813,7 +828,10 @@ describe("<DateRangePicker/>", () => {
startLabel="Start date" startLabel="Start date"
endLabel="End date" endLabel="End date"
name="datepicker" name="datepicker"
defaultValue={["2023-01-01", "2023-01-01"]} defaultValue={[
"2023-01-01T00:00:00.000Z",
"2023-01-01T00:00:00.000Z",
]}
disabled={true} disabled={true}
/> />
</CunninghamProvider>, </CunninghamProvider>,
@@ -846,7 +864,10 @@ describe("<DateRangePicker/>", () => {
startLabel="Start date" startLabel="Start date"
endLabel="End date" endLabel="End date"
name="datepicker" name="datepicker"
defaultValue={["2023-01-01", "2023-01-01"]} defaultValue={[
"2023-01-01T00:00:00.000Z",
"2023-01-01T00:00:00.000Z",
]}
/> />
</CunninghamProvider>, </CunninghamProvider>,
); );
@@ -864,7 +885,10 @@ describe("<DateRangePicker/>", () => {
startLabel="Start date" startLabel="Start date"
endLabel="End date" endLabel="End date"
name="datepicker" name="datepicker"
defaultValue={["2023-01-01", "2023-01-10"]} defaultValue={[
"2023-01-01T00:00:00.000Z",
"2023-01-10T00:00:00.000Z",
]}
/> />
</CunninghamProvider>, </CunninghamProvider>,
); );
@@ -903,8 +927,8 @@ describe("<DateRangePicker/>", () => {
const user = userEvent.setup(); const user = userEvent.setup();
const Wrapper = () => { const Wrapper = () => {
const [value, setValue] = useState<[string, string] | null>([ const [value, setValue] = useState<[string, string] | null>([
"2023-04-25", "2023-04-25T00:00:00.000Z",
"2023-04-26", "2023-04-26T00:00:00.000Z",
]); ]);
return ( return (
<CunninghamProvider> <CunninghamProvider>
@@ -924,7 +948,9 @@ describe("<DateRangePicker/>", () => {
render(<Wrapper />); render(<Wrapper />);
// Make sure value is selected. // Make sure value is selected.
screen.getByText("Value = 2023-04-25 2023-04-26|"); screen.getByText(
"Value = 2023-04-25T00:00:00.000Z 2023-04-26T00:00:00.000Z|",
);
// Make sure value is initially render in the date field component. // Make sure value is initially render in the date field component.
const [startInput, endInput] = await screen.queryAllByRole("presentation"); const [startInput, endInput] = await screen.queryAllByRole("presentation");
@@ -951,7 +977,7 @@ describe("<DateRangePicker/>", () => {
// Make sure value is selected. // Make sure value is selected.
screen.getByText( screen.getByText(
`Value = 2023-04-11T22:00:00.000Z 2023-04-13T22:00:00.000Z|` `Value = 2023-04-12T00:00:00.000Z 2023-04-14T00:00:00.000Z|`,
); );
// Clear value. // Clear value.

View File

@@ -4,24 +4,24 @@ import {
DateRangePickerStateOptions, DateRangePickerStateOptions,
useDateRangePickerState, useDateRangePickerState,
} from "@react-stately/datepicker"; } from "@react-stately/datepicker";
import { useDateRangePicker, DateRange } from "@react-aria/datepicker"; import { useDateRangePicker } from "@react-aria/datepicker";
import { DateRange } from "react-aria";
import { CalendarRange } from ":/components/Forms/DatePicker/Calendar"; import { CalendarRange } from ":/components/Forms/DatePicker/Calendar";
import DatePickerAux, { import DatePickerAux, {
DatePickerAuxSubProps, DatePickerAuxSubProps,
} from ":/components/Forms/DatePicker/DatePickerAux"; } from ":/components/Forms/DatePicker/DatePickerAux";
import DateFieldBox from ":/components/Forms/DatePicker/DateField"; import DateFieldBox from ":/components/Forms/DatePicker/DateField";
import { StringsOrDateRange } from ":/components/Forms/DatePicker/types";
import { import {
convertDateValueToString, convertDateValueToString,
getDefaultPickerOptions, getDefaultPickerOptions,
parseRangeCalendarDate, parseRangeDateValue,
} from ":/components/Forms/DatePicker/utils"; } from ":/components/Forms/DatePicker/utils";
export type DateRangePickerProps = DatePickerAuxSubProps & { export type DateRangePickerProps = DatePickerAuxSubProps & {
startLabel: string; startLabel: string;
endLabel: string; endLabel: string;
value?: null | StringsOrDateRange; value?: null | [string, string];
defaultValue?: StringsOrDateRange; defaultValue?: [string, string];
onChange?: (value: [string, string] | null) => void; onChange?: (value: [string, string] | null) => void;
}; };
@@ -40,8 +40,8 @@ export const DateRangePicker = ({
const options: DateRangePickerStateOptions<DateValue> = { const options: DateRangePickerStateOptions<DateValue> = {
...getDefaultPickerOptions(props), ...getDefaultPickerOptions(props),
value: props.value === null ? null : parseRangeCalendarDate(props.value), value: props.value === null ? null : parseRangeDateValue(props.value),
defaultValue: parseRangeCalendarDate(props.defaultValue), defaultValue: parseRangeDateValue(props.defaultValue),
onChange: (value: DateRange) => { onChange: (value: DateRange) => {
props.onChange?.( props.onChange?.(
value?.start && value.end value?.start && value.end

View File

@@ -4,10 +4,6 @@ import { CunninghamProvider } from ":/components/Provider";
import { Button } from ":/components/Button"; import { Button } from ":/components/Button";
import { DateRangePicker } from ":/components/Forms/DatePicker/DateRangePicker"; import { DateRangePicker } from ":/components/Forms/DatePicker/DateRangePicker";
import { DatePicker } from ":/components/Forms/DatePicker/DatePicker"; import { DatePicker } from ":/components/Forms/DatePicker/DatePicker";
import {
StringOrDate,
StringsOrDateRange,
} from ":/components/Forms/DatePicker/types";
export default { export default {
title: "Components/Forms/DatePicker", title: "Components/Forms/DatePicker",
@@ -35,18 +31,18 @@ export const Disabled = {
export const DefaultValue = { export const DefaultValue = {
render: Template, render: Template,
args: { defaultValue: "2023-05-24" }, args: { defaultValue: "2023-05-24T00:00:00.000+00:00" },
}; };
export const DisabledValue = { export const DisabledValue = {
render: Template, render: Template,
args: { disabled: true, defaultValue: "2023-05-24" }, args: { disabled: true, defaultValue: "2023-05-24T00:00:00.000+00:00" },
}; };
export const Error = { export const Error = {
render: Template, render: Template,
args: { args: {
defaultValue: "2023-05-24", defaultValue: "2023-05-24T00:00:00.000+00:00",
state: "error", state: "error",
text: "Something went wrong", text: "Something went wrong",
}, },
@@ -55,7 +51,7 @@ export const Error = {
export const Success = { export const Success = {
render: Template, render: Template,
args: { args: {
defaultValue: "2023-05-24", defaultValue: "2023-05-24T00:00:00.000+00:00",
state: "success", state: "success",
text: "Well done", text: "Well done",
}, },
@@ -64,25 +60,25 @@ export const Success = {
export const MinMaxValue = { export const MinMaxValue = {
render: Template, render: Template,
args: { args: {
defaultValue: "2023-05-24", defaultValue: "2023-05-24T00:00:00.000+00:00",
minValue: "2023-04-23", minValue: "2023-04-23T00:00:00.000+00:00",
maxValue: "2023-06-23", maxValue: "2023-06-23T00:00:00.000+00:00",
}, },
}; };
export const InvalidValue = { export const InvalidValue = {
render: Template, render: Template,
args: { args: {
defaultValue: "2023-02-24", defaultValue: "2023-02-24T00:00:00.000+00:00",
minValue: "2023-04-23", minValue: "2023-04-23T00:00:00.000+00:00",
maxValue: "2023-06-23", maxValue: "2023-06-23T00:00:00.000+00:00",
}, },
}; };
export const WithText = { export const WithText = {
render: Template, render: Template,
args: { args: {
defaultValue: "2023-05-24", defaultValue: "2023-05-24T00:00:00.000+00:00",
text: "This is a text, you can display anything you want here like warnings, information or errors.", text: "This is a text, you can display anything you want here like warnings, information or errors.",
}, },
}; };
@@ -90,7 +86,7 @@ export const WithText = {
export const Fullwidth = { export const Fullwidth = {
render: Template, render: Template,
args: { args: {
defaultValue: "2023-05-24", defaultValue: "2023-05-24T00:00:00.000+00:00",
fullWidth: true, fullWidth: true,
}, },
}; };
@@ -101,7 +97,7 @@ export const CustomLocale = () => (
<DatePicker <DatePicker
label="Pick a date" label="Pick a date"
locale="hi-IN-u-ca-indian" locale="hi-IN-u-ca-indian"
defaultValue="2023-06-25" defaultValue="2023-06-25T00:00:00.000+00:00"
/> />
</CunninghamProvider> </CunninghamProvider>
</div> </div>
@@ -110,13 +106,16 @@ export const CustomLocale = () => (
export const CunninghamLocale = () => ( export const CunninghamLocale = () => (
<div style={{ minHeight: "400px" }}> <div style={{ minHeight: "400px" }}>
<CunninghamProvider currentLocale="fr-FR"> <CunninghamProvider currentLocale="fr-FR">
<DatePicker label="Pick a date" defaultValue="2023-06-25" /> <DatePicker
label="Pick a date"
defaultValue="2023-06-25T00:00:00.000+00:00"
/>
</CunninghamProvider> </CunninghamProvider>
</div> </div>
); );
export const Controlled = () => { export const Controlled = () => {
const [value, setValue] = useState<StringOrDate | null>("2023-05-26"); const [value, setValue] = useState<string | null>("2023-04-25T12:00:00.000Z");
return ( return (
<CunninghamProvider> <CunninghamProvider>
<div> <div>
@@ -152,16 +151,19 @@ export const RangeDefaultValue = () => {
<DateRangePicker <DateRangePicker
startLabel="Start date" startLabel="Start date"
endLabel="Due date" endLabel="Due date"
defaultValue={["2023-05-23", "2023-06-23"]} defaultValue={[
"2023-05-23T00:00:00.000+00:00",
"2023-06-23T00:00:00.000+00:00",
]}
/> />
</CunninghamProvider> </CunninghamProvider>
); );
}; };
export const RangeControlled = () => { export const RangeControlled = () => {
const [value, setValue] = useState<StringsOrDateRange | null>([ const [value, setValue] = useState<[string, string] | null>([
"2023-05-23", "2023-05-23T13:37:00.000+02:00",
"2023-06-23", "2023-06-23T13:37:00.000+02:00",
]); ]);
return ( return (
<div> <div>
@@ -170,8 +172,8 @@ export const RangeControlled = () => {
<DateRangePicker <DateRangePicker
startLabel="Start date" startLabel="Start date"
endLabel="Due date" endLabel="Due date"
minValue="2023-01-23" minValue="2023-01-23T00:00:00.000+00:00"
maxValue="2023-08-23" maxValue="2023-08-23T00:00:00.000+00:00"
value={value} value={value}
onChange={(e) => setValue(e)} onChange={(e) => setValue(e)}
/> />

View File

@@ -1,5 +1,4 @@
export * from "./DatePicker"; export * from "./DatePicker";
export * from "./DateRangePicker"; export * from "./DateRangePicker";
export * from "./types";
export * from "./utils"; export * from "./utils";

View File

@@ -1,2 +0,0 @@
export type StringOrDate = string | Date;
export type StringsOrDateRange = [StringOrDate, StringOrDate];

View File

@@ -1,27 +1,21 @@
import { import { DateValue, parseAbsolute, parseDate } from "@internationalized/date";
CalendarDate,
DateValue,
parseAbsolute,
parseDate,
} from "@internationalized/date";
import { vi } from "vitest"; import { vi } from "vitest";
import { import {
convertDateValueToString, convertDateValueToString,
parseCalendarDate, parseDateValue,
parseRangeCalendarDate, parseRangeDateValue,
} from ":/components/Forms/DatePicker/utils"; } from ":/components/Forms/DatePicker/utils";
import { StringOrDate } from ":/components/Forms/DatePicker/types";
const expectDateToBeEqual = ( const expectDateToBeEqual = (
parsedDate: CalendarDate | DateValue | undefined, parsedDate: DateValue | undefined,
expectedYear: number, expectedYear: number,
expectedMonth: number, expectedMonth: number,
expectedDay: number, expectedDay: number,
) => { ) => {
expect(parsedDate).not.eq(undefined); expect(parsedDate).not.eq(undefined);
expect(parsedDate?.year === expectedYear); expect(parsedDate?.year).eq(expectedYear);
expect(parsedDate?.month === expectedMonth); expect(parsedDate?.month).eq(expectedMonth);
expect(parsedDate?.day === expectedDay); expect(parsedDate?.day).eq(expectedDay);
}; };
vi.mock("@internationalized/date", async () => { vi.mock("@internationalized/date", async () => {
@@ -36,124 +30,46 @@ vi.mock("@internationalized/date", async () => {
}; };
}); });
describe("parseCalendarDate", () => { describe("parseDateValue", () => {
it.each([ it("parse a 'YYYY-MM-DDThh:mm:ssZ' date", () => {
[2023, 4, 12], const parsedDate = parseDateValue("2023-05-11T00:00:00.000Z");
[2022, 1, 1], expectDateToBeEqual(parsedDate, 2023, 5, 11);
[2022, 12, 31], expect(parsedDate?.hour).eq(0);
[2022, 5, 2],
])("parse an iso string date", (year: number, month: number, day: number) => {
const d = new Date(year, month, day);
const parsedDate = parseCalendarDate(d.toISOString());
expectDateToBeEqual(parsedDate, year, month, day);
}); });
it.each([ it("parse a 'YYYY-MM-DDThh:mm:ss±hh:mm' date", () => {
[2023, 4, 12], const parsedDate = parseDateValue("2023-05-11T00:00:00.000+00:00");
[2022, 1, 1], expectDateToBeEqual(parsedDate, 2023, 5, 11);
[2022, 12, 31], expect(parsedDate?.hour).eq(0);
[2022, 5, 2],
])(
"parse a 'YYYY-MM-DD' date",
(year: number, month: number, day: number) => {
const stringDate = `${year}-${month}-${day}`;
const parsedDate = parseCalendarDate(stringDate);
expectDateToBeEqual(parsedDate, year, month, day);
},
);
it.each([
[2023, 4, 12],
[2022, 1, 1],
[2022, 12, 31],
[2022, 5, 2],
])("parse a datetime date", (year: number, month: number, day: number) => {
const date = new Date(year, month, day);
const parsedDate = parseCalendarDate(date);
expectDateToBeEqual(parsedDate, year, month, day);
}); });
it.each([undefined, ""])("parse an empty or null date", (date) => { it.each([undefined, ""])("parse an empty or null date", (date) => {
const parsedDate = parseCalendarDate(date); const parsedDate = parseDateValue(date);
expect(parsedDate).eq(undefined); expect(parsedDate).eq(undefined);
}); });
it.each([ it.each([
"35/04/2024", "35/04/2024",
"2023-05-11",
"11 janvier 20O2", "11 janvier 20O2",
"22.04.2022", "22.04.2022",
"22-4-2022", "22-4-2022",
"2022-04-1T00:00:00-00:00", "2022-04-1T00:00:00-00:00",
"2022-04-01 T00:00:00-00:00", "2022-04-01 T00:00:00-00:00",
"2022-04-01T00:00:00.000",
])("parse a wrong date", (wrongFormattedDate) => { ])("parse a wrong date", (wrongFormattedDate) => {
expect(() => parseCalendarDate(wrongFormattedDate)).toThrow( expect(() => parseDateValue(wrongFormattedDate)).toThrow(
"Invalid date format when initializing props on DatePicker component", "Invalid date format when initializing props on DatePicker component",
); );
}); });
it.each([
[4, "2023-04-22"],
[5, "2023-04-30"],
[7, "2023-04-22"],
])(
"parse date to locale timezone, converted to day before",
(offset: number, dateString: string) => {
// Get the local offset
const localOffset = new Date().getTimezoneOffset() / 60;
const formattedOffset = offset.toLocaleString("en-US", {
minimumIntegerDigits: 2,
}); });
// Create an ISO string in a timezone that is the day after in local timezone describe("parseRangeDateValue", () => {
const offsetISODate = `${dateString}T${ it("parse a date range", () => {
24 - (offset - localOffset - 1) const range = parseRangeDateValue([
}:00:00-${formattedOffset}:00`; "2023-03-22T00:00:00.000Z",
"2023-04-22T00:00:00.000Z",
// Parse this ISO string, that should be converted to local timezone ]);
const parsedDate = parseCalendarDate(offsetISODate);
// Make sure the ISO string have been converted to the local timezone
const nextDay = parseDate(dateString).add({ days: 1 });
expect(parsedDate?.compare(nextDay)).eq(0);
},
);
it.each([
[4, "2023-04-22"],
[5, "2023-04-30"],
[7, "2023-04-22"],
])(
"parse date to locale timezone, converted to same day",
(offset: number, dateString: string) => {
// Get the local offset
const localOffset = new Date().getTimezoneOffset() / 60;
const formattedOffset = offset.toLocaleString("en-US", {
minimumIntegerDigits: 2,
});
// Create an ISO string in a timezone that is the day after in local timezone
const offsetISODate = `${dateString}T${
24 - (offset - localOffset + 2)
}:00:00-${formattedOffset}:00`;
// Parse this ISO string, that should be converted to local timezone
const parsedDate = parseCalendarDate(offsetISODate);
const sameDay = parseDate(dateString);
// Make sure the ISO string have been converted to the local timezone
expect(parsedDate?.compare(sameDay)).eq(0);
},
);
});
describe("parseRangeCalendarDate", () => {
it.each([
["2023-03-22", "2023-04-22"],
[new Date(2023, 3, 22), "2023-04-22"],
["2023-03-22", new Date(2023, 4, 22)],
["2022-03-22T00:00:00-00:00", "2023-04-22"],
])("parse a date range", (start: string | Date, end: string | Date) => {
const range = parseRangeCalendarDate([start, end]);
expectDateToBeEqual(range?.start, 2023, 3, 22); expectDateToBeEqual(range?.start, 2023, 3, 22);
expectDateToBeEqual(range?.end, 2023, 4, 22); expectDateToBeEqual(range?.end, 2023, 4, 22);
}); });
@@ -163,19 +79,22 @@ describe("parseRangeCalendarDate", () => {
["2023-03-22", ""], ["2023-03-22", ""],
])( ])(
"parse a partially null or empty date range", "parse a partially null or empty date range",
(start: StringOrDate, end: StringOrDate) => { (start: string, end: string) => {
expect(parseRangeCalendarDate([start, end])).eq(undefined); expect(parseRangeDateValue([start, end])).eq(undefined);
}, },
); );
it("parse an undefined date range", () => { it("parse an undefined date range", () => {
expect(parseRangeCalendarDate(undefined)).eq(undefined); expect(parseRangeDateValue(undefined)).eq(undefined);
}); });
it("parse an inverted date range", () => { it("parse an inverted date range", () => {
// Utils function accepts start date superior to the end date // Utils function accepts start date superior to the end date
// However, DateRangePicker will trigger an error with the parsed range. // However, DateRangePicker will trigger an error with the parsed range.
const range = parseRangeCalendarDate(["2023-05-22", "2023-04-22"]); const range = parseRangeDateValue([
"2023-05-22T00:00:00.000Z",
"2023-04-22T00:00:00.000Z",
]);
expectDateToBeEqual(range?.start, 2023, 5, 22); expectDateToBeEqual(range?.start, 2023, 5, 22);
expectDateToBeEqual(range?.end, 2023, 4, 22); expectDateToBeEqual(range?.end, 2023, 4, 22);
}); });

View File

@@ -1,28 +1,21 @@
import { import {
CalendarDate,
DateValue, DateValue,
parseAbsoluteToLocal, parseAbsoluteToLocal,
toCalendarDate,
ZonedDateTime, ZonedDateTime,
toZoned, toZoned,
getLocalTimeZone, getLocalTimeZone,
} from "@internationalized/date"; } from "@internationalized/date";
import { DateRange } from "react-aria"; import { DateRange } from "react-aria";
import {
StringOrDate,
StringsOrDateRange,
} from ":/components/Forms/DatePicker/types";
import { DatePickerAuxSubProps } from ":/components/Forms/DatePicker/DatePickerAux"; import { DatePickerAuxSubProps } from ":/components/Forms/DatePicker/DatePickerAux";
export const parseCalendarDate = ( export const parseDateValue = (
rawDate: StringOrDate | undefined, rawDate: string | undefined,
): undefined | CalendarDate => { ): undefined | ZonedDateTime => {
if (!rawDate) { if (!rawDate) {
return undefined; return undefined;
} }
try { try {
const ISODateString = new Date(rawDate).toISOString(); return parseAbsoluteToLocal(rawDate);
return toCalendarDate(parseAbsoluteToLocal(ISODateString));
} catch (e) { } catch (e) {
throw new Error( throw new Error(
"Invalid date format when initializing props on DatePicker component", "Invalid date format when initializing props on DatePicker component",
@@ -30,15 +23,15 @@ export const parseCalendarDate = (
} }
}; };
export const parseRangeCalendarDate = ( export const parseRangeDateValue = (
rawRange: StringsOrDateRange | undefined, rawRange: [string, string] | undefined,
): DateRange | undefined => { ): DateRange | undefined => {
if (!rawRange || !rawRange[0] || !rawRange[1]) { if (!rawRange || !rawRange[0] || !rawRange[1]) {
return undefined; return undefined;
} }
return { return {
start: parseCalendarDate(rawRange[0])!, start: parseDateValue(rawRange[0])!,
end: parseCalendarDate(rawRange[1])!, end: parseDateValue(rawRange[1])!,
}; };
}; };
@@ -50,14 +43,14 @@ export const convertDateValueToString = (date: DateValue | null): string => {
return date ? toZoned(date, localTimezone).toAbsoluteString() : ""; return date ? toZoned(date, localTimezone).toAbsoluteString() : "";
} catch (e) { } catch (e) {
throw new Error( throw new Error(
"Invalid date format when converting date value on DatePicker component" "Invalid date format when converting date value on DatePicker component",
); );
} }
}; };
export const getDefaultPickerOptions = (props: DatePickerAuxSubProps): any => ({ export const getDefaultPickerOptions = (props: DatePickerAuxSubProps): any => ({
minValue: parseCalendarDate(props.minValue), minValue: parseDateValue(props.minValue),
maxValue: parseCalendarDate(props.maxValue), maxValue: parseDateValue(props.maxValue),
shouldCloseOnSelect: true, shouldCloseOnSelect: true,
granularity: "day", granularity: "day",
isDisabled: props.disabled, isDisabled: props.disabled,