From 0dae71aa9275599ee6c4a47da634144cff47c97b Mon Sep 17 00:00:00 2001 From: Lebaud Antoine Date: Fri, 16 Jun 2023 13:49:47 +0200 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F(react)=20extract=20duplicate?= =?UTF-8?q?d=20utility=20functions=20and=20types?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DateRangePicker and DatePicker would share quite a lot of common logics. Make sure the code elements are factorised easily reusable in each components. --- .../src/components/Forms/DatePicker/types.ts | 2 + .../components/Forms/DatePicker/utils.spec.ts | 163 ++++++++++++++++++ .../src/components/Forms/DatePicker/utils.ts | 48 ++++++ 3 files changed, 213 insertions(+) create mode 100644 packages/react/src/components/Forms/DatePicker/types.ts create mode 100644 packages/react/src/components/Forms/DatePicker/utils.spec.ts create mode 100644 packages/react/src/components/Forms/DatePicker/utils.ts diff --git a/packages/react/src/components/Forms/DatePicker/types.ts b/packages/react/src/components/Forms/DatePicker/types.ts new file mode 100644 index 0000000..0adbb6b --- /dev/null +++ b/packages/react/src/components/Forms/DatePicker/types.ts @@ -0,0 +1,2 @@ +export type StringOrDate = string | Date; +export type StringsOrDateRange = [StringOrDate, StringOrDate]; diff --git a/packages/react/src/components/Forms/DatePicker/utils.spec.ts b/packages/react/src/components/Forms/DatePicker/utils.spec.ts new file mode 100644 index 0000000..b2d81e4 --- /dev/null +++ b/packages/react/src/components/Forms/DatePicker/utils.spec.ts @@ -0,0 +1,163 @@ +import { CalendarDate, DateValue, parseDate } from "@internationalized/date"; +import { + parseCalendarDate, + parseRangeCalendarDate, +} from ":/components/Forms/DatePicker/utils"; +import { StringOrDate } from ":/components/Forms/DatePicker/types"; + +const expectDateToBeEqual = ( + parsedDate: CalendarDate | 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); +}; + +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); + }); + + 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.each([undefined, ""])("parse an empty or null date", (date) => { + const parsedDate = parseCalendarDate(date); + expect(parsedDate).eq(undefined); + }); + + it.each([ + "35/04/2024", + "11 janvier 20O2", + "22.04.2022", + "22-4-2022", + "2022-04-1T00:00:00-00:00", + "2022-04-01 T00:00:00-00:00", + ])("parse a wrong date", (wrongFormattedDate) => { + expect(() => parseCalendarDate(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]); + expectDateToBeEqual(range?.start, 2023, 3, 22); + expectDateToBeEqual(range?.end, 2023, 4, 22); + }); + + it.each([ + ["", "2023-03-22"], + ["2023-03-22", ""], + ])( + "parse a partially null or empty date range", + (start: StringOrDate, end: StringOrDate) => { + expect(parseRangeCalendarDate([start, end])).eq(undefined); + } + ); + + it("parse an undefined date range", () => { + expect(parseRangeCalendarDate(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"]); + expectDateToBeEqual(range?.start, 2023, 5, 22); + expectDateToBeEqual(range?.end, 2023, 4, 22); + }); +}); diff --git a/packages/react/src/components/Forms/DatePicker/utils.ts b/packages/react/src/components/Forms/DatePicker/utils.ts new file mode 100644 index 0000000..43b7058 --- /dev/null +++ b/packages/react/src/components/Forms/DatePicker/utils.ts @@ -0,0 +1,48 @@ +import { + CalendarDate, + parseAbsoluteToLocal, + toCalendarDate, +} 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 => { + if (!rawDate) { + return undefined; + } + try { + const ISODateString = new Date(rawDate).toISOString(); + return toCalendarDate(parseAbsoluteToLocal(ISODateString)); + } catch (e) { + throw new Error( + "Invalid date format when initializing props on DatePicker component" + ); + } +}; + +export const parseRangeCalendarDate = ( + rawRange: StringsOrDateRange | undefined +): DateRange | undefined => { + if (!rawRange || !rawRange[0] || !rawRange[1]) { + return undefined; + } + return { + start: parseCalendarDate(rawRange[0])!, + end: parseCalendarDate(rawRange[1])!, + }; +}; + +export const getDefaultPickerOptions = (props: DatePickerAuxSubProps): any => ({ + minValue: parseCalendarDate(props.minValue), + maxValue: parseCalendarDate(props.maxValue), + shouldCloseOnSelect: true, + granularity: "day", + isDisabled: props.disabled, + label: props.label, +});