Files
web/packages/design-system/lib/components/Form/Date/index.tsx
Anton Gunnarsson 66b7af877a Merged in fix/loy-514-fix-validation-tracking-in-signup-form (pull request #3445)
fix(LOY-514): Fix validation error tracking in SignupForm

* Fix issue with form submit handling

* Fix

* Remove browser validation

* Add automatic tracking of validatione rrors


Approved-by: Rasmus Langvad
Approved-by: Matilda Landström
2026-01-19 08:34:24 +00:00

188 lines
5.4 KiB
TypeScript

"use client"
import { parseDate } from "@internationalized/date"
import { useEffect } from "react"
import { useController, useFormContext, useWatch } from "react-hook-form"
import { useMediaQuery } from "usehooks-ts"
import { dt } from "@scandic-hotels/common/dt"
import { logger } from "@scandic-hotels/common/logger"
import { getLocalizedMonthName } from "@scandic-hotels/common/utils/dateFormatting"
import { rangeArray } from "@scandic-hotels/common/utils/rangeArray"
import { ErrorMessage } from "../../Form/ErrorMessage"
import { Select } from "../../Select"
import { DateName, type DateProps } from "./date"
import styles from "./date.module.css"
export default function DateSelect({
labels,
lang,
name,
registerOptions = {},
}: DateProps) {
const isDesktop = useMediaQuery("(min-width: 768px)", {
initializeWithValue: false,
})
const { control, setValue, formState, watch } = useFormContext()
const { field, fieldState } = useController({
control,
name,
rules: registerOptions,
})
const currentDateValue: string = useWatch({ name })
const year = watch(DateName.year)
const month = watch(DateName.month)
const day = watch(DateName.day)
const minAgeDate = dt().subtract(18, "year").toDate() // age 18
const minAgeYear = minAgeDate.getFullYear()
const minAgeMonth = year === minAgeYear ? minAgeDate.getMonth() + 1 : null
const minAgeDay =
Number(year) === minAgeYear && Number(month) === minAgeMonth
? minAgeDate.getDate()
: null
const months = rangeArray(1, minAgeMonth ?? 12).map((month) => ({
value: month,
label: getLocalizedMonthName(month, lang),
}))
const years = rangeArray(1900, minAgeYear)
.reverse()
.map((year) => ({ value: year, label: year.toString() }))
// Calculate available days based on selected year and month
const daysInMonth = getDaysInMonth(
year ? Number(year) : null,
month ? Number(month) - 1 : null
)
const days = rangeArray(1, minAgeDay ?? daysInMonth).map((day) => ({
value: day,
label: `${day}`,
}))
useEffect(() => {
if (formState.isSubmitting) return
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)
}
}
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 {
/**
* parseDate throws when its not a valid
* date, but we can't check isNan since
* we recieve the date as "1999-01-01"
*/
dateValue = dt(currentDateValue).isValid()
? parseDate(currentDateValue)
: null
} catch (error) {
logger.warn("Known error for parse date in DateSelect: ", error)
}
useEffect(() => {
if (formState.isSubmitting) return
if (!(day && month && year) && dateValue) {
setValue(DateName.day, Number(dateValue.day))
setValue(DateName.month, Number(dateValue.month))
setValue(DateName.year, Number(dateValue.year))
}
}, [setValue, formState.isSubmitting, dateValue, day, month, year])
return (
<>
<div className={styles.container}>
<div className={styles.segment}>
<Select
items={days}
label={labels.day}
name={DateName.day}
onSelectionChange={(key) => setValue(DateName.day, Number(key))}
enableFiltering={isDesktop}
isInvalid={fieldState.invalid}
onBlur={field.onBlur}
defaultSelectedKey={dateValue?.day}
inputMode="numeric"
data-testid={DateName.day}
/>
</div>
<div className={styles.segment}>
<Select
items={months}
label={labels.month}
name={DateName.month}
onSelectionChange={(key) => setValue(DateName.month, Number(key))}
enableFiltering={isDesktop}
isInvalid={fieldState.invalid}
onBlur={field.onBlur}
defaultSelectedKey={dateValue?.month}
data-testid={DateName.month}
/>
</div>
<div className={styles.segment}>
<Select
items={years}
label={labels.year}
name={DateName.year}
onSelectionChange={(key) => setValue(DateName.year, Number(key))}
enableFiltering={isDesktop}
isInvalid={fieldState.invalid}
onBlur={field.onBlur}
defaultSelectedKey={dateValue?.year}
inputMode="numeric"
data-testid={DateName.year}
/>
</div>
</div>
<ErrorMessage
errors={formState.errors}
name={field.name}
messageLabel={labels.errorMessage}
/>
</>
)
}
function getDaysInMonth(year: number | null, month: number | null): number {
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()
}