✨(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:
committed by
aleb_the_flash
parent
8cf8e1eba2
commit
0dc46d1144
5
.changeset/smart-tables-kiss.md
Normal file
5
.changeset/smart-tables-kiss.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@openfun/cunningham-react": minor
|
||||
---
|
||||
|
||||
Restrict input formats of date picker components to IS0 strings
|
||||
@@ -8,7 +8,7 @@ import { Button } from ":/components/Button";
|
||||
|
||||
vi.mock("@internationalized/date", async () => {
|
||||
const mod = await vi.importActual<typeof import("@internationalized/date")>(
|
||||
"@internationalized/date"
|
||||
"@internationalized/date",
|
||||
);
|
||||
return {
|
||||
...mod,
|
||||
@@ -193,7 +193,7 @@ describe("<DatePicker/>", () => {
|
||||
<DatePicker
|
||||
label="Pick a date"
|
||||
name="datepicker"
|
||||
defaultValue="2023-04-25"
|
||||
defaultValue="2023-04-25T00:00:00.000Z"
|
||||
/>
|
||||
</CunninghamProvider>,
|
||||
);
|
||||
@@ -282,7 +282,7 @@ describe("<DatePicker/>", () => {
|
||||
<DatePicker
|
||||
label="Pick a date"
|
||||
name="datepicker"
|
||||
defaultValue="2023-04-05"
|
||||
defaultValue="2023-04-05T00:00:00.000Z"
|
||||
/>
|
||||
</CunninghamProvider>,
|
||||
);
|
||||
@@ -317,8 +317,8 @@ describe("<DatePicker/>", () => {
|
||||
<DatePicker
|
||||
label="Pick a date"
|
||||
name="datepicker"
|
||||
defaultValue="2023-04-05"
|
||||
minValue="2022-12-03"
|
||||
defaultValue="2023-04-05T10:00:00.000Z"
|
||||
minValue="2022-12-03T10:00:00.000Z"
|
||||
/>
|
||||
</CunninghamProvider>,
|
||||
);
|
||||
@@ -350,12 +350,9 @@ describe("<DatePicker/>", () => {
|
||||
});
|
||||
|
||||
it.each([
|
||||
"2022-05-25",
|
||||
"2023-06-01T13:50",
|
||||
"2025-03-25",
|
||||
"Mar 25 2025",
|
||||
"25 Mar 2025",
|
||||
new Date("2025-04-23"),
|
||||
"2022-05-25T23:59:59.000Z",
|
||||
"2023-06-01T00:00:00.000Z",
|
||||
"2025-03-25T12:30:00.000Z",
|
||||
])("has a default value", async (defaultValue) => {
|
||||
render(
|
||||
<CunninghamProvider>
|
||||
@@ -373,8 +370,6 @@ describe("<DatePicker/>", () => {
|
||||
});
|
||||
|
||||
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);
|
||||
expect(() =>
|
||||
render(
|
||||
@@ -382,8 +377,8 @@ describe("<DatePicker/>", () => {
|
||||
<DatePicker
|
||||
label="Pick a date"
|
||||
name="datepicker"
|
||||
defaultValue={defaultValue}
|
||||
value={value}
|
||||
defaultValue="2023-05-24T00:00:00.000Z"
|
||||
value="2023-05-24T00:00:00.000Z"
|
||||
/>
|
||||
</CunninghamProvider>,
|
||||
),
|
||||
@@ -413,14 +408,13 @@ describe("<DatePicker/>", () => {
|
||||
);
|
||||
|
||||
it("clears date", async () => {
|
||||
const defaultValue = new Date("2023-05-24");
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<CunninghamProvider>
|
||||
<DatePicker
|
||||
label="Pick a date"
|
||||
name="datepicker"
|
||||
defaultValue={defaultValue}
|
||||
defaultValue="2023-05-24T00:00:00.000Z"
|
||||
/>
|
||||
</CunninghamProvider>,
|
||||
);
|
||||
@@ -435,7 +429,7 @@ describe("<DatePicker/>", () => {
|
||||
expect(dateFieldContent).eq("mm/dd/yyyy");
|
||||
|
||||
const isGridCellSelected = screen
|
||||
.getByRole("gridcell", { name: `${defaultValue.getDate()}` })!
|
||||
.getByRole("gridcell", { name: "24" })!
|
||||
.getAttribute("aria-selected");
|
||||
expect(isGridCellSelected).toBeNull();
|
||||
|
||||
@@ -453,8 +447,8 @@ describe("<DatePicker/>", () => {
|
||||
<DatePicker
|
||||
label="Pick a date"
|
||||
name="datepicker"
|
||||
defaultValue="2023-01-01"
|
||||
minValue="2023-02-01"
|
||||
defaultValue="2023-01-01T00:00:00.000Z"
|
||||
minValue="2023-02-01T00:00:00.000Z"
|
||||
/>
|
||||
</CunninghamProvider>,
|
||||
);
|
||||
@@ -467,8 +461,8 @@ describe("<DatePicker/>", () => {
|
||||
<DatePicker
|
||||
label="Pick a date"
|
||||
name="datepicker"
|
||||
defaultValue="2023-03-01"
|
||||
maxValue="2023-02-01"
|
||||
defaultValue="2023-03-01T00:00:00.000Z"
|
||||
maxValue="2023-02-01T00:00:00.000Z"
|
||||
/>
|
||||
</CunninghamProvider>,
|
||||
);
|
||||
@@ -482,9 +476,9 @@ describe("<DatePicker/>", () => {
|
||||
<DatePicker
|
||||
label="Pick a date"
|
||||
name="datepicker"
|
||||
defaultValue="2023-03-01"
|
||||
maxValue="2023-04-01"
|
||||
minValue="2023-05-01"
|
||||
defaultValue="2023-03-01T00:00:00.000Z"
|
||||
maxValue="2023-04-01T00:00:00.000Z"
|
||||
minValue="2023-05-01T00:00:00.000Z"
|
||||
/>
|
||||
</CunninghamProvider>,
|
||||
);
|
||||
@@ -494,7 +488,9 @@ describe("<DatePicker/>", () => {
|
||||
it("works controlled", async () => {
|
||||
const user = userEvent.setup();
|
||||
const Wrapper = () => {
|
||||
const [value, setValue] = useState<string | null>("2023-04-25");
|
||||
const [value, setValue] = useState<string | null>(
|
||||
"2023-04-25T00:00:00.000Z",
|
||||
);
|
||||
return (
|
||||
<CunninghamProvider>
|
||||
<div>
|
||||
@@ -513,11 +509,11 @@ describe("<DatePicker/>", () => {
|
||||
render(<Wrapper />);
|
||||
|
||||
// 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.
|
||||
const dateFieldContent = screen.getByRole("presentation").textContent;
|
||||
expectDatesToBeEqual("2023-04-25", dateFieldContent);
|
||||
expectDatesToBeEqual("2023-04-25T00:00:00.000Z", dateFieldContent);
|
||||
|
||||
// Open the calendar grid.
|
||||
const toggleButton = (await screen.findAllByRole("button"))![1];
|
||||
@@ -525,7 +521,7 @@ describe("<DatePicker/>", () => {
|
||||
expectCalendarToBeOpen();
|
||||
|
||||
const gridCell = within(
|
||||
await screen.getByRole("gridcell", { name: "12" }),
|
||||
screen.getByRole("gridcell", { name: "12" }),
|
||||
).getByRole("button")!;
|
||||
|
||||
// Select a new value in the calendar grid.
|
||||
@@ -533,7 +529,7 @@ describe("<DatePicker/>", () => {
|
||||
expectCalendarToBeClosed();
|
||||
|
||||
// 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.
|
||||
const clearButton = screen.getByRole("button", {
|
||||
@@ -552,7 +548,7 @@ describe("<DatePicker/>", () => {
|
||||
<DatePicker
|
||||
label="Pick a date"
|
||||
name="datepicker"
|
||||
defaultValue="2023-03-01"
|
||||
defaultValue="2023-03-01T00:00:00.000Z"
|
||||
disabled={true}
|
||||
/>
|
||||
</CunninghamProvider>,
|
||||
@@ -584,7 +580,7 @@ describe("<DatePicker/>", () => {
|
||||
<DatePicker
|
||||
label="Pick a date"
|
||||
name="datepicker"
|
||||
defaultValue="2023-03-01"
|
||||
defaultValue="2023-03-01T00:00:00.000Z"
|
||||
/>
|
||||
</CunninghamProvider>,
|
||||
);
|
||||
@@ -649,6 +645,7 @@ describe("<DatePicker/>", () => {
|
||||
expectDateFieldToBeDisplayed();
|
||||
|
||||
// Make sure form's value matches.
|
||||
// It should be equal the 12th of May at midnight in local timezone.
|
||||
expect(formData).toEqual({
|
||||
datepicker: "2023-05-11T22:00:00.000Z",
|
||||
});
|
||||
@@ -663,7 +660,7 @@ describe("<DatePicker/>", () => {
|
||||
// Submit the form being empty.
|
||||
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();
|
||||
|
||||
// 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 () => {
|
||||
const defaultValue = new Date("2023-05-24");
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<CunninghamProvider>
|
||||
<DatePicker
|
||||
label="Pick a date"
|
||||
name="datepicker"
|
||||
defaultValue={defaultValue}
|
||||
defaultValue="2023-05-24T00:00:00.000Z"
|
||||
/>
|
||||
</CunninghamProvider>,
|
||||
);
|
||||
@@ -711,14 +811,13 @@ describe("<DatePicker/>", () => {
|
||||
});
|
||||
|
||||
it("clicks next and previous focused year", async () => {
|
||||
const defaultValue = new Date("2023-05-24");
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<CunninghamProvider>
|
||||
<DatePicker
|
||||
label="Pick a date"
|
||||
name="datepicker"
|
||||
defaultValue={defaultValue}
|
||||
defaultValue="2023-05-24T00:00:00.000Z"
|
||||
/>
|
||||
</CunninghamProvider>,
|
||||
);
|
||||
@@ -749,18 +848,15 @@ describe("<DatePicker/>", () => {
|
||||
});
|
||||
|
||||
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();
|
||||
render(
|
||||
<CunninghamProvider>
|
||||
<DatePicker
|
||||
label="Pick a date"
|
||||
name="datepicker"
|
||||
defaultValue={defaultValue}
|
||||
minValue={minValue}
|
||||
maxValue={maxValue}
|
||||
defaultValue="2023-05-24T00:00:00.000Z"
|
||||
minValue="2023-05-22T00:00:00.000Z"
|
||||
maxValue="2023-05-26T00:00:00.000Z"
|
||||
/>
|
||||
</CunninghamProvider>,
|
||||
);
|
||||
@@ -782,18 +878,15 @@ describe("<DatePicker/>", () => {
|
||||
});
|
||||
|
||||
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();
|
||||
render(
|
||||
<CunninghamProvider>
|
||||
<DatePicker
|
||||
label="Pick a date"
|
||||
name="datepicker"
|
||||
defaultValue={defaultValue}
|
||||
minValue={minValue}
|
||||
maxValue={maxValue}
|
||||
defaultValue="2023-05-24T00:00:00.000Z"
|
||||
minValue="2023-05-22T00:00:00.000Z"
|
||||
maxValue="2023-05-26T00:00:00.000Z"
|
||||
/>
|
||||
</CunninghamProvider>,
|
||||
);
|
||||
@@ -816,16 +909,16 @@ describe("<DatePicker/>", () => {
|
||||
|
||||
it("renders partially disabled next and previous month", async () => {
|
||||
const user = userEvent.setup();
|
||||
const minValue = new Date("2023-04-23");
|
||||
const maxValue = new Date("2023-06-23");
|
||||
const minValue = new Date("2023-04-23T00:00:00.000z");
|
||||
const maxValue = new Date("2023-06-23T00:00:00.000z");
|
||||
render(
|
||||
<CunninghamProvider>
|
||||
<DatePicker
|
||||
label="Pick a date"
|
||||
name="datepicker"
|
||||
defaultValue="2023-05-23"
|
||||
minValue={minValue}
|
||||
maxValue={maxValue}
|
||||
defaultValue="2023-05-23T00:00:00.000Z"
|
||||
minValue={minValue.toISOString()}
|
||||
maxValue={maxValue.toISOString()}
|
||||
/>
|
||||
</CunninghamProvider>,
|
||||
);
|
||||
@@ -915,13 +1008,12 @@ describe("<DatePicker/>", () => {
|
||||
|
||||
it("selects a focused month", async () => {
|
||||
const user = userEvent.setup();
|
||||
const defaultValue = new Date("2023-05-23");
|
||||
render(
|
||||
<CunninghamProvider>
|
||||
<DatePicker
|
||||
label="Pick a date"
|
||||
name="datepicker"
|
||||
defaultValue={defaultValue}
|
||||
defaultValue="2023-05-23T00:00:00.000Z"
|
||||
/>
|
||||
</CunninghamProvider>,
|
||||
);
|
||||
@@ -952,7 +1044,7 @@ describe("<DatePicker/>", () => {
|
||||
|
||||
// Select a month option.
|
||||
const option: HTMLLIElement = screen.getByRole("option", {
|
||||
name: "September",
|
||||
name: "August",
|
||||
});
|
||||
await user.click(option);
|
||||
|
||||
@@ -961,18 +1053,17 @@ describe("<DatePicker/>", () => {
|
||||
|
||||
// Make sure focused month has properly updated.
|
||||
focusedMonth = monthDropdown.textContent?.replace("arrow_drop_down", "");
|
||||
expect(focusedMonth).eq("Sep");
|
||||
expect(focusedMonth).eq("Aug");
|
||||
});
|
||||
|
||||
it("selects a focused year", async () => {
|
||||
const user = userEvent.setup();
|
||||
const defaultValue = new Date("2023-05-23");
|
||||
render(
|
||||
<CunninghamProvider>
|
||||
<DatePicker
|
||||
label="Pick a date"
|
||||
name="datepicker"
|
||||
defaultValue={defaultValue}
|
||||
defaultValue="2023-05-23T00:00:00.000Z"
|
||||
/>
|
||||
</CunninghamProvider>,
|
||||
);
|
||||
@@ -1013,13 +1104,13 @@ describe("<DatePicker/>", () => {
|
||||
|
||||
it("renders only cell within the focused month", async () => {
|
||||
const user = userEvent.setup();
|
||||
const defaultValue = new Date("2023-05-23");
|
||||
const defaultValue = new Date("2023-05-23T00:00:00.000Z");
|
||||
render(
|
||||
<CunninghamProvider>
|
||||
<DatePicker
|
||||
label="Pick a date"
|
||||
name="datepicker"
|
||||
defaultValue={defaultValue}
|
||||
defaultValue={defaultValue.toISOString()}
|
||||
/>
|
||||
</CunninghamProvider>,
|
||||
);
|
||||
@@ -1053,13 +1144,12 @@ describe("<DatePicker/>", () => {
|
||||
|
||||
it("navigate previous focused month with keyboard", async () => {
|
||||
const user = userEvent.setup();
|
||||
const defaultValue = new Date("2023-05-01");
|
||||
render(
|
||||
<CunninghamProvider>
|
||||
<DatePicker
|
||||
label="Pick a date"
|
||||
name="datepicker"
|
||||
defaultValue={defaultValue}
|
||||
defaultValue="2023-05-01T00:00:00.000Z"
|
||||
/>
|
||||
</CunninghamProvider>,
|
||||
);
|
||||
@@ -1086,13 +1176,12 @@ describe("<DatePicker/>", () => {
|
||||
|
||||
it("navigate next focused month with keyboard", async () => {
|
||||
const user = userEvent.setup();
|
||||
const defaultValue = new Date("2023-05-31");
|
||||
render(
|
||||
<CunninghamProvider>
|
||||
<DatePicker
|
||||
label="Pick a date"
|
||||
name="datepicker"
|
||||
defaultValue={defaultValue}
|
||||
defaultValue="2023-05-31T00:00:00.000Z"
|
||||
/>
|
||||
</CunninghamProvider>,
|
||||
);
|
||||
@@ -1123,7 +1212,7 @@ describe("<DatePicker/>", () => {
|
||||
<DatePicker
|
||||
label="Pick a date"
|
||||
locale="hi-IN-u-ca-indian"
|
||||
defaultValue="2023-06-25"
|
||||
defaultValue="2023-06-25T00:00:00.000Z"
|
||||
/>
|
||||
</CunninghamProvider>,
|
||||
);
|
||||
@@ -1169,7 +1258,10 @@ describe("<DatePicker/>", () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<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>,
|
||||
);
|
||||
|
||||
@@ -1216,7 +1308,7 @@ describe("<DatePicker/>", () => {
|
||||
<CunninghamProvider currentLocale="fr-FR">
|
||||
<DatePicker
|
||||
label="Pick a date"
|
||||
defaultValue="2023-06-25"
|
||||
defaultValue="2023-06-25T00:00:00.000Z"
|
||||
locale="hi-IN-u-ca-indian"
|
||||
/>
|
||||
</CunninghamProvider>,
|
||||
@@ -1271,4 +1363,51 @@ describe("<DatePicker/>", () => {
|
||||
),
|
||||
).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|`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,17 +10,16 @@ import DatePickerAux, {
|
||||
} from ":/components/Forms/DatePicker/DatePickerAux";
|
||||
import { Calendar } from ":/components/Forms/DatePicker/Calendar";
|
||||
import DateFieldBox from ":/components/Forms/DatePicker/DateField";
|
||||
import { StringOrDate } from ":/components/Forms/DatePicker/types";
|
||||
import {
|
||||
convertDateValueToString,
|
||||
getDefaultPickerOptions,
|
||||
parseCalendarDate,
|
||||
parseDateValue,
|
||||
} from ":/components/Forms/DatePicker/utils";
|
||||
|
||||
export type DatePickerProps = DatePickerAuxSubProps & {
|
||||
value?: null | StringOrDate;
|
||||
value?: null | string;
|
||||
label: string;
|
||||
defaultValue?: StringOrDate;
|
||||
defaultValue?: string;
|
||||
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.
|
||||
props.value === "" || props.value === null
|
||||
? null
|
||||
: parseCalendarDate(props.value),
|
||||
defaultValue: parseCalendarDate(props.defaultValue),
|
||||
: parseDateValue(props.value),
|
||||
defaultValue: parseDateValue(props.defaultValue),
|
||||
onChange: (value: DateValue | null) => {
|
||||
props.onChange?.(convertDateValueToString(value));
|
||||
},
|
||||
|
||||
@@ -16,7 +16,6 @@ import { Button } from ":/components/Button";
|
||||
import { Popover } from ":/components/Popover";
|
||||
import { Field, FieldProps } from ":/components/Forms/Field";
|
||||
import { useCunningham } from ":/components/Provider";
|
||||
import { StringOrDate } from ":/components/Forms/DatePicker/types";
|
||||
import {
|
||||
Calendar,
|
||||
CalendarRange,
|
||||
@@ -25,8 +24,8 @@ import { convertDateValueToString } from ":/components/Forms/DatePicker/utils";
|
||||
|
||||
export type DatePickerAuxSubProps = FieldProps & {
|
||||
label?: string;
|
||||
minValue?: StringOrDate;
|
||||
maxValue?: StringOrDate;
|
||||
minValue?: string;
|
||||
maxValue?: string;
|
||||
disabled?: boolean;
|
||||
name?: string;
|
||||
locale?: string;
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Button } from ":/components/Button";
|
||||
|
||||
vi.mock("@internationalized/date", async () => {
|
||||
const mod = await vi.importActual<typeof import("@internationalized/date")>(
|
||||
"@internationalized/date"
|
||||
"@internationalized/date",
|
||||
);
|
||||
return {
|
||||
...mod,
|
||||
@@ -151,9 +151,8 @@ describe("<DateRangePicker/>", () => {
|
||||
});
|
||||
|
||||
it.each([
|
||||
["2022-05-25", "2022-05-26"],
|
||||
["25 Mar 2025", "2029-05-26"],
|
||||
["2023-06-01T13:50", "2024-05-26"],
|
||||
["2022-05-25T00:00:00.000Z", "2022-05-26T00:00:00.000Z"],
|
||||
["2023-06-01T00:00:00.000Z", "2024-05-26T00:00:00.000Z"],
|
||||
])("has a default value", async (start: string, end: string) => {
|
||||
render(
|
||||
<CunninghamProvider>
|
||||
@@ -180,7 +179,10 @@ describe("<DateRangePicker/>", () => {
|
||||
startLabel="Start date"
|
||||
endLabel="End date"
|
||||
name="datepicker"
|
||||
defaultValue={["2023-04-25", "2023-05-25"]}
|
||||
defaultValue={[
|
||||
"2023-04-25T00:00:00.000Z",
|
||||
"2023-05-25T00:00:00.000Z",
|
||||
]}
|
||||
/>
|
||||
</CunninghamProvider>,
|
||||
);
|
||||
@@ -375,7 +377,10 @@ describe("<DateRangePicker/>", () => {
|
||||
<DateRangePicker
|
||||
startLabel="Start 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"
|
||||
/>
|
||||
</CunninghamProvider>,
|
||||
@@ -437,7 +442,10 @@ describe("<DateRangePicker/>", () => {
|
||||
startLabel="Start date"
|
||||
endLabel="End date"
|
||||
name="datepicker"
|
||||
defaultValue={["2023-01-01", "2023-01-01"]}
|
||||
defaultValue={[
|
||||
"2023-01-01T00:00:00.000Z",
|
||||
"2023-01-01T00:00:00.000Z",
|
||||
]}
|
||||
/>
|
||||
</CunninghamProvider>,
|
||||
);
|
||||
@@ -486,7 +494,10 @@ describe("<DateRangePicker/>", () => {
|
||||
startLabel="Start date"
|
||||
endLabel="End date"
|
||||
name="datepicker"
|
||||
defaultValue={["2023-01-01", "2023-01-01"]}
|
||||
defaultValue={[
|
||||
"2023-01-01T00:00:00.000Z",
|
||||
"2023-01-01T00:00:00.000Z",
|
||||
]}
|
||||
/>
|
||||
</CunninghamProvider>,
|
||||
);
|
||||
@@ -657,8 +668,11 @@ describe("<DateRangePicker/>", () => {
|
||||
startLabel="Start date"
|
||||
endLabel="End date"
|
||||
name="datepicker"
|
||||
defaultValue={["2022-05-25", "2022-05-26"]}
|
||||
value={["2022-05-25", "2022-05-26"]}
|
||||
defaultValue={[
|
||||
"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>,
|
||||
),
|
||||
@@ -668,9 +682,9 @@ describe("<DateRangePicker/>", () => {
|
||||
});
|
||||
|
||||
it.each([
|
||||
["this_not_a_valid_date", "2022-05-12"],
|
||||
["2022-05-12", "2023-13-13"],
|
||||
["2025-25-05", "2022-05-12"],
|
||||
["this_not_a_valid_date", "2022-05-12T00:00:00.000Z"],
|
||||
["2022-05-12T00:00:00.000Z", "2023-13-13"],
|
||||
["2025-25-05T00:00:00.000Z", "2022-05-12T00:00:00.000Z"],
|
||||
])("has not a valid range value", async (start: string, end: string) => {
|
||||
vi.spyOn(console, "error").mockImplementation(() => undefined);
|
||||
expect(() =>
|
||||
@@ -697,76 +711,77 @@ describe("<DateRangePicker/>", () => {
|
||||
startLabel="Start date"
|
||||
endLabel="End date"
|
||||
name="datepicker"
|
||||
defaultValue={["2024-05-25", "2022-05-26"]}
|
||||
defaultValue={[
|
||||
"2024-05-25T00:00:00.000Z",
|
||||
"2022-05-26T00:00:00.000Z",
|
||||
]}
|
||||
/>
|
||||
</CunninghamProvider>,
|
||||
);
|
||||
await expectDateRangePickerStateToBe("invalid");
|
||||
});
|
||||
|
||||
it.each([[new Date(2022, 5, 25), new Date(2022, 5, 27)]])(
|
||||
"clears date",
|
||||
async (start: Date, end: Date) => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<CunninghamProvider>
|
||||
<DateRangePicker
|
||||
startLabel="Start date"
|
||||
endLabel="End date"
|
||||
name="datepicker"
|
||||
defaultValue={[start, end]}
|
||||
/>
|
||||
</CunninghamProvider>,
|
||||
);
|
||||
it("clears date", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<CunninghamProvider>
|
||||
<DateRangePicker
|
||||
startLabel="Start date"
|
||||
endLabel="End date"
|
||||
name="datepicker"
|
||||
defaultValue={[
|
||||
"2023-05-25T00:00:00.000Z",
|
||||
"2023-05-27T00:00:00.000Z",
|
||||
]}
|
||||
/>
|
||||
</CunninghamProvider>,
|
||||
);
|
||||
|
||||
const clearButton = screen.getByRole("button", {
|
||||
name: "Clear date",
|
||||
});
|
||||
await user.click(clearButton);
|
||||
expectCalendarToBeOpen();
|
||||
const clearButton = screen.getByRole("button", {
|
||||
name: "Clear date",
|
||||
});
|
||||
await user.click(clearButton);
|
||||
expectCalendarToBeOpen();
|
||||
|
||||
// Date field's value should be set to a placeholder value.
|
||||
const [startInput, endInput] = await screen.queryAllByRole(
|
||||
"presentation",
|
||||
);
|
||||
expect(startInput.textContent).eq("mm/dd/yyyy");
|
||||
expect(endInput.textContent).eq("mm/dd/yyyy");
|
||||
// Date field's value should be set to a placeholder value.
|
||||
const [startInput, endInput] = await screen.queryAllByRole("presentation");
|
||||
expect(startInput.textContent).eq("mm/dd/yyyy");
|
||||
expect(endInput.textContent).eq("mm/dd/yyyy");
|
||||
|
||||
const startGridCell = screen.getByRole("gridcell", {
|
||||
name: `${start.getDate()}`,
|
||||
})!;
|
||||
const startGridCell = screen.getByRole("gridcell", {
|
||||
name: "25",
|
||||
})!;
|
||||
|
||||
// Make sure start grid-cell is not selected anymore.
|
||||
expect(startGridCell.getAttribute("aria-selected")).toBeNull();
|
||||
expect(
|
||||
startGridCell.classList.contains(
|
||||
"c__calendar__wrapper__grid__week-row__background--range--start",
|
||||
),
|
||||
).toBe(false);
|
||||
// Make sure start grid-cell is not selected anymore.
|
||||
expect(startGridCell.getAttribute("aria-selected")).toBeNull();
|
||||
expect(
|
||||
startGridCell.classList.contains(
|
||||
"c__calendar__wrapper__grid__week-row__background--range--start",
|
||||
),
|
||||
).toBe(false);
|
||||
|
||||
// Make sure end grid-cell is not selected anymore.
|
||||
const endGridCell = screen.getByRole("gridcell", {
|
||||
name: `${end.getDate()}`,
|
||||
})!;
|
||||
expect(endGridCell.getAttribute("aria-selected")).toBeNull();
|
||||
expect(
|
||||
endGridCell.classList.contains(
|
||||
"c__calendar__wrapper__grid__week-row__background--range--end",
|
||||
),
|
||||
).toBe(false);
|
||||
// Make sure end grid-cell is not selected anymore.
|
||||
const endGridCell = screen.getByRole("gridcell", {
|
||||
name: "27",
|
||||
})!;
|
||||
expect(endGridCell.getAttribute("aria-selected")).toBeNull();
|
||||
expect(
|
||||
endGridCell.classList.contains(
|
||||
"c__calendar__wrapper__grid__week-row__background--range--end",
|
||||
),
|
||||
).toBe(false);
|
||||
|
||||
// Close the calendar.
|
||||
const toggleButton = (await screen.findAllByRole("button"))![1];
|
||||
await user.click(toggleButton);
|
||||
// Close the calendar.
|
||||
const toggleButton = (await screen.findAllByRole("button"))![1];
|
||||
await user.click(toggleButton);
|
||||
|
||||
// Make sure the empty date field is hidden when closing the calendar.
|
||||
await expectDateFieldsToBeHidden();
|
||||
},
|
||||
);
|
||||
// Make sure the empty date field is hidden when closing the calendar.
|
||||
await expectDateFieldsToBeHidden();
|
||||
});
|
||||
|
||||
it.each([
|
||||
["2023-01-01", "2023-01-01"],
|
||||
["2023-01-01", "2023-03-01"],
|
||||
["2023-01-01T00:00:00.000Z", "2023-01-01T00:00:00.000Z"],
|
||||
["2023-01-01T00:00:00.000Z", "2023-03-01T00:00:00.000Z"],
|
||||
])(
|
||||
"has a start or a end date inferior to minValue",
|
||||
async (start: string, end: string) => {
|
||||
@@ -777,7 +792,7 @@ describe("<DateRangePicker/>", () => {
|
||||
endLabel="End date"
|
||||
name="datepicker"
|
||||
defaultValue={[start, end]}
|
||||
minValue="2023-02-01"
|
||||
minValue="2023-02-01T00:00:00.000Z"
|
||||
/>
|
||||
</CunninghamProvider>,
|
||||
);
|
||||
@@ -786,8 +801,8 @@ describe("<DateRangePicker/>", () => {
|
||||
);
|
||||
|
||||
it.each([
|
||||
["2023-01-01", "2023-03-01"],
|
||||
["2023-03-01", "2023-03-01"],
|
||||
["2023-01-01T00:00:00.000Z", "2023-03-01T00:00:00.000Z"],
|
||||
["2023-03-01T00:00:00.000Z", "2023-03-01T00:00:00.000Z"],
|
||||
])(
|
||||
"has a start or a end date superior to maxValue",
|
||||
async (start: string, end: string) => {
|
||||
@@ -798,7 +813,7 @@ describe("<DateRangePicker/>", () => {
|
||||
endLabel="End date"
|
||||
name="datepicker"
|
||||
defaultValue={[start, end]}
|
||||
maxValue="2023-02-01"
|
||||
maxValue="2023-02-01T00:00:00.000Z"
|
||||
/>
|
||||
</CunninghamProvider>,
|
||||
);
|
||||
@@ -813,7 +828,10 @@ describe("<DateRangePicker/>", () => {
|
||||
startLabel="Start date"
|
||||
endLabel="End date"
|
||||
name="datepicker"
|
||||
defaultValue={["2023-01-01", "2023-01-01"]}
|
||||
defaultValue={[
|
||||
"2023-01-01T00:00:00.000Z",
|
||||
"2023-01-01T00:00:00.000Z",
|
||||
]}
|
||||
disabled={true}
|
||||
/>
|
||||
</CunninghamProvider>,
|
||||
@@ -846,7 +864,10 @@ describe("<DateRangePicker/>", () => {
|
||||
startLabel="Start date"
|
||||
endLabel="End date"
|
||||
name="datepicker"
|
||||
defaultValue={["2023-01-01", "2023-01-01"]}
|
||||
defaultValue={[
|
||||
"2023-01-01T00:00:00.000Z",
|
||||
"2023-01-01T00:00:00.000Z",
|
||||
]}
|
||||
/>
|
||||
</CunninghamProvider>,
|
||||
);
|
||||
@@ -864,7 +885,10 @@ describe("<DateRangePicker/>", () => {
|
||||
startLabel="Start date"
|
||||
endLabel="End date"
|
||||
name="datepicker"
|
||||
defaultValue={["2023-01-01", "2023-01-10"]}
|
||||
defaultValue={[
|
||||
"2023-01-01T00:00:00.000Z",
|
||||
"2023-01-10T00:00:00.000Z",
|
||||
]}
|
||||
/>
|
||||
</CunninghamProvider>,
|
||||
);
|
||||
@@ -903,8 +927,8 @@ describe("<DateRangePicker/>", () => {
|
||||
const user = userEvent.setup();
|
||||
const Wrapper = () => {
|
||||
const [value, setValue] = useState<[string, string] | null>([
|
||||
"2023-04-25",
|
||||
"2023-04-26",
|
||||
"2023-04-25T00:00:00.000Z",
|
||||
"2023-04-26T00:00:00.000Z",
|
||||
]);
|
||||
return (
|
||||
<CunninghamProvider>
|
||||
@@ -924,7 +948,9 @@ describe("<DateRangePicker/>", () => {
|
||||
render(<Wrapper />);
|
||||
|
||||
// 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.
|
||||
const [startInput, endInput] = await screen.queryAllByRole("presentation");
|
||||
@@ -951,7 +977,7 @@ describe("<DateRangePicker/>", () => {
|
||||
|
||||
// Make sure value is selected.
|
||||
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.
|
||||
|
||||
@@ -4,24 +4,24 @@ import {
|
||||
DateRangePickerStateOptions,
|
||||
useDateRangePickerState,
|
||||
} 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 DatePickerAux, {
|
||||
DatePickerAuxSubProps,
|
||||
} from ":/components/Forms/DatePicker/DatePickerAux";
|
||||
import DateFieldBox from ":/components/Forms/DatePicker/DateField";
|
||||
import { StringsOrDateRange } from ":/components/Forms/DatePicker/types";
|
||||
import {
|
||||
convertDateValueToString,
|
||||
getDefaultPickerOptions,
|
||||
parseRangeCalendarDate,
|
||||
parseRangeDateValue,
|
||||
} from ":/components/Forms/DatePicker/utils";
|
||||
|
||||
export type DateRangePickerProps = DatePickerAuxSubProps & {
|
||||
startLabel: string;
|
||||
endLabel: string;
|
||||
value?: null | StringsOrDateRange;
|
||||
defaultValue?: StringsOrDateRange;
|
||||
value?: null | [string, string];
|
||||
defaultValue?: [string, string];
|
||||
onChange?: (value: [string, string] | null) => void;
|
||||
};
|
||||
|
||||
@@ -40,8 +40,8 @@ export const DateRangePicker = ({
|
||||
|
||||
const options: DateRangePickerStateOptions<DateValue> = {
|
||||
...getDefaultPickerOptions(props),
|
||||
value: props.value === null ? null : parseRangeCalendarDate(props.value),
|
||||
defaultValue: parseRangeCalendarDate(props.defaultValue),
|
||||
value: props.value === null ? null : parseRangeDateValue(props.value),
|
||||
defaultValue: parseRangeDateValue(props.defaultValue),
|
||||
onChange: (value: DateRange) => {
|
||||
props.onChange?.(
|
||||
value?.start && value.end
|
||||
|
||||
@@ -4,10 +4,6 @@ import { CunninghamProvider } from ":/components/Provider";
|
||||
import { Button } from ":/components/Button";
|
||||
import { DateRangePicker } from ":/components/Forms/DatePicker/DateRangePicker";
|
||||
import { DatePicker } from ":/components/Forms/DatePicker/DatePicker";
|
||||
import {
|
||||
StringOrDate,
|
||||
StringsOrDateRange,
|
||||
} from ":/components/Forms/DatePicker/types";
|
||||
|
||||
export default {
|
||||
title: "Components/Forms/DatePicker",
|
||||
@@ -35,18 +31,18 @@ export const Disabled = {
|
||||
|
||||
export const DefaultValue = {
|
||||
render: Template,
|
||||
args: { defaultValue: "2023-05-24" },
|
||||
args: { defaultValue: "2023-05-24T00:00:00.000+00:00" },
|
||||
};
|
||||
|
||||
export const DisabledValue = {
|
||||
render: Template,
|
||||
args: { disabled: true, defaultValue: "2023-05-24" },
|
||||
args: { disabled: true, defaultValue: "2023-05-24T00:00:00.000+00:00" },
|
||||
};
|
||||
|
||||
export const Error = {
|
||||
render: Template,
|
||||
args: {
|
||||
defaultValue: "2023-05-24",
|
||||
defaultValue: "2023-05-24T00:00:00.000+00:00",
|
||||
state: "error",
|
||||
text: "Something went wrong",
|
||||
},
|
||||
@@ -55,7 +51,7 @@ export const Error = {
|
||||
export const Success = {
|
||||
render: Template,
|
||||
args: {
|
||||
defaultValue: "2023-05-24",
|
||||
defaultValue: "2023-05-24T00:00:00.000+00:00",
|
||||
state: "success",
|
||||
text: "Well done",
|
||||
},
|
||||
@@ -64,25 +60,25 @@ export const Success = {
|
||||
export const MinMaxValue = {
|
||||
render: Template,
|
||||
args: {
|
||||
defaultValue: "2023-05-24",
|
||||
minValue: "2023-04-23",
|
||||
maxValue: "2023-06-23",
|
||||
defaultValue: "2023-05-24T00:00:00.000+00:00",
|
||||
minValue: "2023-04-23T00:00:00.000+00:00",
|
||||
maxValue: "2023-06-23T00:00:00.000+00:00",
|
||||
},
|
||||
};
|
||||
|
||||
export const InvalidValue = {
|
||||
render: Template,
|
||||
args: {
|
||||
defaultValue: "2023-02-24",
|
||||
minValue: "2023-04-23",
|
||||
maxValue: "2023-06-23",
|
||||
defaultValue: "2023-02-24T00:00:00.000+00:00",
|
||||
minValue: "2023-04-23T00:00:00.000+00:00",
|
||||
maxValue: "2023-06-23T00:00:00.000+00:00",
|
||||
},
|
||||
};
|
||||
|
||||
export const WithText = {
|
||||
render: Template,
|
||||
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.",
|
||||
},
|
||||
};
|
||||
@@ -90,7 +86,7 @@ export const WithText = {
|
||||
export const Fullwidth = {
|
||||
render: Template,
|
||||
args: {
|
||||
defaultValue: "2023-05-24",
|
||||
defaultValue: "2023-05-24T00:00:00.000+00:00",
|
||||
fullWidth: true,
|
||||
},
|
||||
};
|
||||
@@ -101,7 +97,7 @@ export const CustomLocale = () => (
|
||||
<DatePicker
|
||||
label="Pick a date"
|
||||
locale="hi-IN-u-ca-indian"
|
||||
defaultValue="2023-06-25"
|
||||
defaultValue="2023-06-25T00:00:00.000+00:00"
|
||||
/>
|
||||
</CunninghamProvider>
|
||||
</div>
|
||||
@@ -110,13 +106,16 @@ export const CustomLocale = () => (
|
||||
export const CunninghamLocale = () => (
|
||||
<div style={{ minHeight: "400px" }}>
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
|
||||
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 (
|
||||
<CunninghamProvider>
|
||||
<div>
|
||||
@@ -152,16 +151,19 @@ export const RangeDefaultValue = () => {
|
||||
<DateRangePicker
|
||||
startLabel="Start 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>
|
||||
);
|
||||
};
|
||||
|
||||
export const RangeControlled = () => {
|
||||
const [value, setValue] = useState<StringsOrDateRange | null>([
|
||||
"2023-05-23",
|
||||
"2023-06-23",
|
||||
const [value, setValue] = useState<[string, string] | null>([
|
||||
"2023-05-23T13:37:00.000+02:00",
|
||||
"2023-06-23T13:37:00.000+02:00",
|
||||
]);
|
||||
return (
|
||||
<div>
|
||||
@@ -170,8 +172,8 @@ export const RangeControlled = () => {
|
||||
<DateRangePicker
|
||||
startLabel="Start date"
|
||||
endLabel="Due date"
|
||||
minValue="2023-01-23"
|
||||
maxValue="2023-08-23"
|
||||
minValue="2023-01-23T00:00:00.000+00:00"
|
||||
maxValue="2023-08-23T00:00:00.000+00:00"
|
||||
value={value}
|
||||
onChange={(e) => setValue(e)}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export * from "./DatePicker";
|
||||
export * from "./DateRangePicker";
|
||||
|
||||
export * from "./types";
|
||||
export * from "./utils";
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export type StringOrDate = string | Date;
|
||||
export type StringsOrDateRange = [StringOrDate, StringOrDate];
|
||||
@@ -1,27 +1,21 @@
|
||||
import {
|
||||
CalendarDate,
|
||||
DateValue,
|
||||
parseAbsolute,
|
||||
parseDate,
|
||||
} from "@internationalized/date";
|
||||
import { DateValue, parseAbsolute, parseDate } from "@internationalized/date";
|
||||
import { vi } from "vitest";
|
||||
import {
|
||||
convertDateValueToString,
|
||||
parseCalendarDate,
|
||||
parseRangeCalendarDate,
|
||||
parseDateValue,
|
||||
parseRangeDateValue,
|
||||
} from ":/components/Forms/DatePicker/utils";
|
||||
import { StringOrDate } from ":/components/Forms/DatePicker/types";
|
||||
|
||||
const expectDateToBeEqual = (
|
||||
parsedDate: CalendarDate | DateValue | undefined,
|
||||
parsedDate: DateValue | undefined,
|
||||
expectedYear: number,
|
||||
expectedMonth: number,
|
||||
expectedDay: number,
|
||||
) => {
|
||||
expect(parsedDate).not.eq(undefined);
|
||||
expect(parsedDate?.year === expectedYear);
|
||||
expect(parsedDate?.month === expectedMonth);
|
||||
expect(parsedDate?.day === expectedDay);
|
||||
expect(parsedDate?.year).eq(expectedYear);
|
||||
expect(parsedDate?.month).eq(expectedMonth);
|
||||
expect(parsedDate?.day).eq(expectedDay);
|
||||
};
|
||||
|
||||
vi.mock("@internationalized/date", async () => {
|
||||
@@ -36,124 +30,46 @@ vi.mock("@internationalized/date", async () => {
|
||||
};
|
||||
});
|
||||
|
||||
describe("parseCalendarDate", () => {
|
||||
it.each([
|
||||
[2023, 4, 12],
|
||||
[2022, 1, 1],
|
||||
[2022, 12, 31],
|
||||
[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);
|
||||
describe("parseDateValue", () => {
|
||||
it("parse a 'YYYY-MM-DDThh:mm:ssZ' date", () => {
|
||||
const parsedDate = parseDateValue("2023-05-11T00:00:00.000Z");
|
||||
expectDateToBeEqual(parsedDate, 2023, 5, 11);
|
||||
expect(parsedDate?.hour).eq(0);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[2023, 4, 12],
|
||||
[2022, 1, 1],
|
||||
[2022, 12, 31],
|
||||
[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("parse a 'YYYY-MM-DDThh:mm:ss±hh:mm' date", () => {
|
||||
const parsedDate = parseDateValue("2023-05-11T00:00:00.000+00:00");
|
||||
expectDateToBeEqual(parsedDate, 2023, 5, 11);
|
||||
expect(parsedDate?.hour).eq(0);
|
||||
});
|
||||
|
||||
it.each([undefined, ""])("parse an empty or null date", (date) => {
|
||||
const parsedDate = parseCalendarDate(date);
|
||||
const parsedDate = parseDateValue(date);
|
||||
expect(parsedDate).eq(undefined);
|
||||
});
|
||||
|
||||
it.each([
|
||||
"35/04/2024",
|
||||
"2023-05-11",
|
||||
"11 janvier 20O2",
|
||||
"22.04.2022",
|
||||
"22-4-2022",
|
||||
"2022-04-1T00:00:00-00:00",
|
||||
"2022-04-01 T00:00:00-00:00",
|
||||
"2022-04-01T00:00:00.000",
|
||||
])("parse a wrong date", (wrongFormattedDate) => {
|
||||
expect(() => parseCalendarDate(wrongFormattedDate)).toThrow(
|
||||
expect(() => parseDateValue(wrongFormattedDate)).toThrow(
|
||||
"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
|
||||
const offsetISODate = `${dateString}T${
|
||||
24 - (offset - localOffset - 1)
|
||||
}:00:00-${formattedOffset}:00`;
|
||||
|
||||
// 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]);
|
||||
describe("parseRangeDateValue", () => {
|
||||
it("parse a date range", () => {
|
||||
const range = parseRangeDateValue([
|
||||
"2023-03-22T00:00:00.000Z",
|
||||
"2023-04-22T00:00:00.000Z",
|
||||
]);
|
||||
expectDateToBeEqual(range?.start, 2023, 3, 22);
|
||||
expectDateToBeEqual(range?.end, 2023, 4, 22);
|
||||
});
|
||||
@@ -163,19 +79,22 @@ describe("parseRangeCalendarDate", () => {
|
||||
["2023-03-22", ""],
|
||||
])(
|
||||
"parse a partially null or empty date range",
|
||||
(start: StringOrDate, end: StringOrDate) => {
|
||||
expect(parseRangeCalendarDate([start, end])).eq(undefined);
|
||||
(start: string, end: string) => {
|
||||
expect(parseRangeDateValue([start, end])).eq(undefined);
|
||||
},
|
||||
);
|
||||
|
||||
it("parse an undefined date range", () => {
|
||||
expect(parseRangeCalendarDate(undefined)).eq(undefined);
|
||||
expect(parseRangeDateValue(undefined)).eq(undefined);
|
||||
});
|
||||
|
||||
it("parse an inverted date range", () => {
|
||||
// Utils function accepts start date superior to the end date
|
||||
// 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?.end, 2023, 4, 22);
|
||||
});
|
||||
|
||||
@@ -1,28 +1,21 @@
|
||||
import {
|
||||
CalendarDate,
|
||||
DateValue,
|
||||
parseAbsoluteToLocal,
|
||||
toCalendarDate,
|
||||
ZonedDateTime,
|
||||
toZoned,
|
||||
getLocalTimeZone,
|
||||
} from "@internationalized/date";
|
||||
import { DateRange } from "react-aria";
|
||||
import {
|
||||
StringOrDate,
|
||||
StringsOrDateRange,
|
||||
} from ":/components/Forms/DatePicker/types";
|
||||
import { DatePickerAuxSubProps } from ":/components/Forms/DatePicker/DatePickerAux";
|
||||
|
||||
export const parseCalendarDate = (
|
||||
rawDate: StringOrDate | undefined,
|
||||
): undefined | CalendarDate => {
|
||||
export const parseDateValue = (
|
||||
rawDate: string | undefined,
|
||||
): undefined | ZonedDateTime => {
|
||||
if (!rawDate) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
const ISODateString = new Date(rawDate).toISOString();
|
||||
return toCalendarDate(parseAbsoluteToLocal(ISODateString));
|
||||
return parseAbsoluteToLocal(rawDate);
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
"Invalid date format when initializing props on DatePicker component",
|
||||
@@ -30,15 +23,15 @@ export const parseCalendarDate = (
|
||||
}
|
||||
};
|
||||
|
||||
export const parseRangeCalendarDate = (
|
||||
rawRange: StringsOrDateRange | undefined,
|
||||
export const parseRangeDateValue = (
|
||||
rawRange: [string, string] | undefined,
|
||||
): DateRange | undefined => {
|
||||
if (!rawRange || !rawRange[0] || !rawRange[1]) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
start: parseCalendarDate(rawRange[0])!,
|
||||
end: parseCalendarDate(rawRange[1])!,
|
||||
start: parseDateValue(rawRange[0])!,
|
||||
end: parseDateValue(rawRange[1])!,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -50,14 +43,14 @@ export const convertDateValueToString = (date: DateValue | null): string => {
|
||||
return date ? toZoned(date, localTimezone).toAbsoluteString() : "";
|
||||
} catch (e) {
|
||||
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 => ({
|
||||
minValue: parseCalendarDate(props.minValue),
|
||||
maxValue: parseCalendarDate(props.maxValue),
|
||||
minValue: parseDateValue(props.minValue),
|
||||
maxValue: parseDateValue(props.maxValue),
|
||||
shouldCloseOnSelect: true,
|
||||
granularity: "day",
|
||||
isDisabled: props.disabled,
|
||||
|
||||
Reference in New Issue
Block a user