From 8763346f9d3520b7d74bf435a9ba2724771217be Mon Sep 17 00:00:00 2001 From: Chuma McPhoy Date: Wed, 30 Oct 2024 13:03:23 +0100 Subject: [PATCH] fix(SW-649): let react-hook-form do date state handling --- .../EnterDetails/Details/schema.ts | 2 +- .../Form/Date/date.module.css | 9 + components/TempDesignSystem/Form/Date/date.ts | 1 + .../TempDesignSystem/Form/Date/index.tsx | 160 ++++++++---------- 4 files changed, 86 insertions(+), 86 deletions(-) diff --git a/components/HotelReservation/EnterDetails/Details/schema.ts b/components/HotelReservation/EnterDetails/Details/schema.ts index 2e01eefe3..e6bd0dd41 100644 --- a/components/HotelReservation/EnterDetails/Details/schema.ts +++ b/components/HotelReservation/EnterDetails/Details/schema.ts @@ -23,7 +23,7 @@ export const joinDetailsSchema = baseDetailsSchema.merge( z.object({ join: z.literal(true), zipCode: z.string().min(1, { message: "Zip code is required" }), - dateOfBirth: z.string(), + dateOfBirth: z.string().min(1, { message: "Date of birth is required" }), termsAccepted: z.literal(true, { errorMap: (err, ctx) => { switch (err.code) { diff --git a/components/TempDesignSystem/Form/Date/date.module.css b/components/TempDesignSystem/Form/Date/date.module.css index 6c0549313..fde0b7e03 100644 --- a/components/TempDesignSystem/Form/Date/date.module.css +++ b/components/TempDesignSystem/Form/Date/date.module.css @@ -18,3 +18,12 @@ .year { grid-area: year; } + +/* TODO: Handle this in Select component. + - out of scope for now. +*/ +.day.invalid > div > div, +.month.invalid > div > div, +.year.invalid > div > div { + border-color: var(--Scandic-Red-60); +} diff --git a/components/TempDesignSystem/Form/Date/date.ts b/components/TempDesignSystem/Form/Date/date.ts index 25eff31c7..8f17d4607 100644 --- a/components/TempDesignSystem/Form/Date/date.ts +++ b/components/TempDesignSystem/Form/Date/date.ts @@ -2,6 +2,7 @@ import type { RegisterOptions } from "react-hook-form" export const enum DateName { date = "date", + day = "day", month = "month", year = "year", } diff --git a/components/TempDesignSystem/Form/Date/index.tsx b/components/TempDesignSystem/Form/Date/index.tsx index c2754bb13..36a0ff44a 100644 --- a/components/TempDesignSystem/Form/Date/index.tsx +++ b/components/TempDesignSystem/Form/Date/index.tsx @@ -1,6 +1,6 @@ "use client" import { parseDate } from "@internationalized/date" -import { useState } from "react" +import { useEffect } from "react" import { DateInput, DatePicker, Group } from "react-aria-components" import { useController, useFormContext, useWatch } from "react-hook-form" import { useIntl } from "react-intl" @@ -21,37 +21,24 @@ import type { DateProps } from "./date" export default function DateSelect({ name, registerOptions = {} }: DateProps) { const intl = useIntl() - const currentValue = useWatch({ name }) - const { control, setValue, trigger, formState } = useFormContext() - const { field } = useController({ + const { control, setValue, trigger, formState, watch } = useFormContext() + const { field, fieldState } = useController({ control, name, rules: registerOptions, }) - const dayLabel = intl.formatMessage({ id: "Day" }) - const monthLabel = intl.formatMessage({ id: "Month" }) - const yearLabel = intl.formatMessage({ id: "Year" }) - - const initialDate = dt(currentValue) - - const [selectedYear, setSelectedYear] = useState( - initialDate.isValid() ? initialDate.year() : null - ) - const [selectedMonth, setSelectedMonth] = useState( - initialDate.isValid() ? initialDate.month() : null - ) - const [selectedDay, setSelectedDay] = useState( - initialDate.isValid() ? initialDate.date() : null - ) - - const currentYear = new Date().getFullYear() + const currentDateValue = useWatch({ name }) + const year = watch(DateName.year) + const month = watch(DateName.month) + const day = watch(DateName.day) const months = rangeArray(1, 12).map((month) => ({ value: month, label: `${month}`, })) + const currentYear = new Date().getFullYear() const years = rangeArray(1900, currentYear - 18) .reverse() .map((year) => ({ value: year, label: year.toString() })) @@ -60,58 +47,61 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) { if (month === null) { return 31 } + + // If month is February and no year selected, return minimum. + if (month === 1 && !year) { + return 28 + } + const yearToUse = year ?? new Date().getFullYear() return dt(`${yearToUse}-${month + 1}-01`).daysInMonth() } - const days = rangeArray(1, getDaysInMonth(selectedYear, selectedMonth)).map( - (day) => ({ - value: day, - label: `${day}`, - }) + // Calculate available days based on selected year and month + const daysInMonth = getDaysInMonth( + year ? Number(year) : null, + month ? Number(month) - 1 : null ) - function handleSegmentChange(selector: DateName, value: number) { - let newYear = selectedYear - let newMonth = selectedMonth - let newDay = selectedDay + const days = rangeArray(1, daysInMonth).map((day) => ({ + value: day, + label: `${day}`, + })) - switch (selector) { - case DateName.year: - newYear = value - setSelectedYear(newYear) - break - /** - * Months are 0 index based and therefore we - * must subtract by 1 to get the selected month - */ - case DateName.month: - newMonth = value - 1 - setSelectedMonth(newMonth) - if (selectedDay) { - const maxDays = getDaysInMonth(newYear, newMonth) - if (selectedDay > maxDays) { - newDay = maxDays - setSelectedDay(newDay) - } - } - break - case DateName.date: - newDay = value - setSelectedDay(newDay) - break - } + const dayLabel = intl.formatMessage({ id: "Day" }) + const monthLabel = intl.formatMessage({ id: "Month" }) + const yearLabel = intl.formatMessage({ id: "Year" }) - // Check if all segments are set and update form value. - if (newYear && newMonth !== null && newDay) { - const newDate = dt().year(newYear).month(newMonth).date(newDay) + useEffect(() => { + if (formState.isSubmitting) return - if (newDate.isValid()) { - setValue(name, newDate.format("YYYY-MM-DD")) - trigger(name) + if (month && day) { + const maxDays = getDaysInMonth( + year ? Number(year) : null, + Number(month) - 1 + ) + const adjustedDay = Number(day) > maxDays ? maxDays : Number(day) + + if (adjustedDay !== Number(day)) { + setValue(DateName.day, adjustedDay) } } - } + + if (year && month && day) { + const newDate = dt() + .year(Number(year)) + .month(Number(month) - 1) + .date(Number(day)) + + if (newDate.isValid()) { + setValue(name, newDate.format("YYYY-MM-DD"), { + shouldDirty: true, + shouldTouch: true, + shouldValidate: true, + }) + } + } + }, [year, month, day, setValue, name, formState.isSubmitting]) let dateValue = null try { @@ -120,7 +110,9 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) { * date, but we can't check isNan since * we recieve the date as "1999-01-01" */ - dateValue = dt(currentValue).isValid() ? parseDate(currentValue) : null + dateValue = dt(currentDateValue).isValid() + ? parseDate(currentDateValue) + : null } catch (error) { console.warn("Known error for parse date in DateSelect: ", error) } @@ -129,6 +121,7 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) { +
- handleSegmentChange(DateName.month, Number(select)) + onSelect={(key: Key) => + setValue(DateName.month, Number(key)) } - placeholder="MM" + placeholder={monthLabel} required tabIndex={2} - defaultSelectedKey={ - segment.isPlaceholder ? undefined : segment.value - } value={segment.isPlaceholder ? undefined : segment.value} />
) case "year": return ( -
+