fix(SW-649): let react-hook-form do date state handling
This commit is contained in:
@@ -23,7 +23,7 @@ export const joinDetailsSchema = baseDetailsSchema.merge(
|
|||||||
z.object({
|
z.object({
|
||||||
join: z.literal(true),
|
join: z.literal(true),
|
||||||
zipCode: z.string().min(1, { message: "Zip code is required" }),
|
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, {
|
termsAccepted: z.literal(true, {
|
||||||
errorMap: (err, ctx) => {
|
errorMap: (err, ctx) => {
|
||||||
switch (err.code) {
|
switch (err.code) {
|
||||||
|
|||||||
@@ -18,3 +18,12 @@
|
|||||||
.year {
|
.year {
|
||||||
grid-area: 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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { RegisterOptions } from "react-hook-form"
|
|||||||
|
|
||||||
export const enum DateName {
|
export const enum DateName {
|
||||||
date = "date",
|
date = "date",
|
||||||
|
day = "day",
|
||||||
month = "month",
|
month = "month",
|
||||||
year = "year",
|
year = "year",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { parseDate } from "@internationalized/date"
|
import { parseDate } from "@internationalized/date"
|
||||||
import { useState } from "react"
|
import { useEffect } from "react"
|
||||||
import { DateInput, DatePicker, Group } from "react-aria-components"
|
import { DateInput, DatePicker, Group } from "react-aria-components"
|
||||||
import { useController, useFormContext, useWatch } from "react-hook-form"
|
import { useController, useFormContext, useWatch } from "react-hook-form"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
@@ -21,37 +21,24 @@ import type { DateProps } from "./date"
|
|||||||
|
|
||||||
export default function DateSelect({ name, registerOptions = {} }: DateProps) {
|
export default function DateSelect({ name, registerOptions = {} }: DateProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const currentValue = useWatch({ name })
|
const { control, setValue, trigger, formState, watch } = useFormContext()
|
||||||
const { control, setValue, trigger, formState } = useFormContext()
|
const { field, fieldState } = useController({
|
||||||
const { field } = useController({
|
|
||||||
control,
|
control,
|
||||||
name,
|
name,
|
||||||
rules: registerOptions,
|
rules: registerOptions,
|
||||||
})
|
})
|
||||||
|
|
||||||
const dayLabel = intl.formatMessage({ id: "Day" })
|
const currentDateValue = useWatch({ name })
|
||||||
const monthLabel = intl.formatMessage({ id: "Month" })
|
const year = watch(DateName.year)
|
||||||
const yearLabel = intl.formatMessage({ id: "Year" })
|
const month = watch(DateName.month)
|
||||||
|
const day = watch(DateName.day)
|
||||||
const initialDate = dt(currentValue)
|
|
||||||
|
|
||||||
const [selectedYear, setSelectedYear] = useState<number | null>(
|
|
||||||
initialDate.isValid() ? initialDate.year() : null
|
|
||||||
)
|
|
||||||
const [selectedMonth, setSelectedMonth] = useState<number | null>(
|
|
||||||
initialDate.isValid() ? initialDate.month() : null
|
|
||||||
)
|
|
||||||
const [selectedDay, setSelectedDay] = useState<number | null>(
|
|
||||||
initialDate.isValid() ? initialDate.date() : null
|
|
||||||
)
|
|
||||||
|
|
||||||
const currentYear = new Date().getFullYear()
|
|
||||||
|
|
||||||
const months = rangeArray(1, 12).map((month) => ({
|
const months = rangeArray(1, 12).map((month) => ({
|
||||||
value: month,
|
value: month,
|
||||||
label: `${month}`,
|
label: `${month}`,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
const currentYear = new Date().getFullYear()
|
||||||
const years = rangeArray(1900, currentYear - 18)
|
const years = rangeArray(1900, currentYear - 18)
|
||||||
.reverse()
|
.reverse()
|
||||||
.map((year) => ({ value: year, label: year.toString() }))
|
.map((year) => ({ value: year, label: year.toString() }))
|
||||||
@@ -60,58 +47,61 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
|
|||||||
if (month === null) {
|
if (month === null) {
|
||||||
return 31
|
return 31
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If month is February and no year selected, return minimum.
|
||||||
|
if (month === 1 && !year) {
|
||||||
|
return 28
|
||||||
|
}
|
||||||
|
|
||||||
const yearToUse = year ?? new Date().getFullYear()
|
const yearToUse = year ?? new Date().getFullYear()
|
||||||
return dt(`${yearToUse}-${month + 1}-01`).daysInMonth()
|
return dt(`${yearToUse}-${month + 1}-01`).daysInMonth()
|
||||||
}
|
}
|
||||||
|
|
||||||
const days = rangeArray(1, getDaysInMonth(selectedYear, selectedMonth)).map(
|
// Calculate available days based on selected year and month
|
||||||
(day) => ({
|
const daysInMonth = getDaysInMonth(
|
||||||
value: day,
|
year ? Number(year) : null,
|
||||||
label: `${day}`,
|
month ? Number(month) - 1 : null
|
||||||
})
|
|
||||||
)
|
)
|
||||||
|
|
||||||
function handleSegmentChange(selector: DateName, value: number) {
|
const days = rangeArray(1, daysInMonth).map((day) => ({
|
||||||
let newYear = selectedYear
|
value: day,
|
||||||
let newMonth = selectedMonth
|
label: `${day}`,
|
||||||
let newDay = selectedDay
|
}))
|
||||||
|
|
||||||
switch (selector) {
|
const dayLabel = intl.formatMessage({ id: "Day" })
|
||||||
case DateName.year:
|
const monthLabel = intl.formatMessage({ id: "Month" })
|
||||||
newYear = value
|
const yearLabel = intl.formatMessage({ id: "Year" })
|
||||||
setSelectedYear(newYear)
|
|
||||||
break
|
useEffect(() => {
|
||||||
/**
|
if (formState.isSubmitting) return
|
||||||
* Months are 0 index based and therefore we
|
|
||||||
* must subtract by 1 to get the selected month
|
if (month && day) {
|
||||||
*/
|
const maxDays = getDaysInMonth(
|
||||||
case DateName.month:
|
year ? Number(year) : null,
|
||||||
newMonth = value - 1
|
Number(month) - 1
|
||||||
setSelectedMonth(newMonth)
|
)
|
||||||
if (selectedDay) {
|
const adjustedDay = Number(day) > maxDays ? maxDays : Number(day)
|
||||||
const maxDays = getDaysInMonth(newYear, newMonth)
|
|
||||||
if (selectedDay > maxDays) {
|
if (adjustedDay !== Number(day)) {
|
||||||
newDay = maxDays
|
setValue(DateName.day, adjustedDay)
|
||||||
setSelectedDay(newDay)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break
|
|
||||||
case DateName.date:
|
|
||||||
newDay = value
|
|
||||||
setSelectedDay(newDay)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if all segments are set and update form value.
|
if (year && month && day) {
|
||||||
if (newYear && newMonth !== null && newDay) {
|
const newDate = dt()
|
||||||
const newDate = dt().year(newYear).month(newMonth).date(newDay)
|
.year(Number(year))
|
||||||
|
.month(Number(month) - 1)
|
||||||
|
.date(Number(day))
|
||||||
|
|
||||||
if (newDate.isValid()) {
|
if (newDate.isValid()) {
|
||||||
setValue(name, newDate.format("YYYY-MM-DD"))
|
setValue(name, newDate.format("YYYY-MM-DD"), {
|
||||||
trigger(name)
|
shouldDirty: true,
|
||||||
}
|
shouldTouch: true,
|
||||||
|
shouldValidate: true,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}, [year, month, day, setValue, name, formState.isSubmitting])
|
||||||
|
|
||||||
let dateValue = null
|
let dateValue = null
|
||||||
try {
|
try {
|
||||||
@@ -120,7 +110,9 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
|
|||||||
* date, but we can't check isNan since
|
* date, but we can't check isNan since
|
||||||
* we recieve the date as "1999-01-01"
|
* we recieve the date as "1999-01-01"
|
||||||
*/
|
*/
|
||||||
dateValue = dt(currentValue).isValid() ? parseDate(currentValue) : null
|
dateValue = dt(currentDateValue).isValid()
|
||||||
|
? parseDate(currentDateValue)
|
||||||
|
: null
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("Known error for parse date in DateSelect: ", error)
|
console.warn("Known error for parse date in DateSelect: ", error)
|
||||||
}
|
}
|
||||||
@@ -129,6 +121,7 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
|
|||||||
<DatePicker
|
<DatePicker
|
||||||
aria-label={intl.formatMessage({ id: "Select date of birth" })}
|
aria-label={intl.formatMessage({ id: "Select date of birth" })}
|
||||||
isRequired={!!registerOptions.required}
|
isRequired={!!registerOptions.required}
|
||||||
|
isInvalid={!formState.isValid}
|
||||||
name={name}
|
name={name}
|
||||||
ref={field.ref}
|
ref={field.ref}
|
||||||
value={dateValue}
|
value={dateValue}
|
||||||
@@ -140,63 +133,60 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
|
|||||||
switch (segment.type) {
|
switch (segment.type) {
|
||||||
case "day":
|
case "day":
|
||||||
return (
|
return (
|
||||||
<div className={styles.day}>
|
<div
|
||||||
|
className={`${styles.day} ${fieldState.invalid ? styles.invalid : ""}`}
|
||||||
|
>
|
||||||
<Select
|
<Select
|
||||||
aria-label={dayLabel}
|
aria-label={dayLabel}
|
||||||
items={days}
|
items={days}
|
||||||
label={dayLabel}
|
label={dayLabel}
|
||||||
name={DateName.date}
|
name={DateName.day}
|
||||||
onSelect={(select: Key) =>
|
onSelect={(key: Key) =>
|
||||||
handleSegmentChange(DateName.date, Number(select))
|
setValue(DateName.day, Number(key))
|
||||||
}
|
}
|
||||||
placeholder="DD"
|
placeholder={dayLabel}
|
||||||
required
|
required
|
||||||
tabIndex={3}
|
tabIndex={3}
|
||||||
defaultSelectedKey={
|
|
||||||
segment.isPlaceholder ? undefined : segment.value
|
|
||||||
}
|
|
||||||
value={segment.isPlaceholder ? undefined : segment.value}
|
value={segment.isPlaceholder ? undefined : segment.value}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
case "month":
|
case "month":
|
||||||
return (
|
return (
|
||||||
<div className={styles.month}>
|
<div
|
||||||
|
className={`${styles.month} ${fieldState.invalid ? styles.invalid : ""}`}
|
||||||
|
>
|
||||||
<Select
|
<Select
|
||||||
aria-label={monthLabel}
|
aria-label={monthLabel}
|
||||||
items={months}
|
items={months}
|
||||||
label={monthLabel}
|
label={monthLabel}
|
||||||
name={DateName.month}
|
name={DateName.month}
|
||||||
onSelect={(select: Key) =>
|
onSelect={(key: Key) =>
|
||||||
handleSegmentChange(DateName.month, Number(select))
|
setValue(DateName.month, Number(key))
|
||||||
}
|
}
|
||||||
placeholder="MM"
|
placeholder={monthLabel}
|
||||||
required
|
required
|
||||||
tabIndex={2}
|
tabIndex={2}
|
||||||
defaultSelectedKey={
|
|
||||||
segment.isPlaceholder ? undefined : segment.value
|
|
||||||
}
|
|
||||||
value={segment.isPlaceholder ? undefined : segment.value}
|
value={segment.isPlaceholder ? undefined : segment.value}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
case "year":
|
case "year":
|
||||||
return (
|
return (
|
||||||
<div className={styles.year}>
|
<div
|
||||||
|
className={`${styles.year} ${fieldState.invalid ? styles.invalid : ""}`}
|
||||||
|
>
|
||||||
<Select
|
<Select
|
||||||
aria-label={yearLabel}
|
aria-label={yearLabel}
|
||||||
items={years}
|
items={years}
|
||||||
label={yearLabel}
|
label={yearLabel}
|
||||||
name={DateName.year}
|
name={DateName.year}
|
||||||
onSelect={(select: Key) =>
|
onSelect={(key: Key) =>
|
||||||
handleSegmentChange(DateName.year, Number(select))
|
setValue(DateName.year, Number(key))
|
||||||
}
|
}
|
||||||
placeholder="YYYY"
|
placeholder={yearLabel}
|
||||||
required
|
required
|
||||||
tabIndex={1}
|
tabIndex={1}
|
||||||
defaultSelectedKey={
|
|
||||||
segment.isPlaceholder ? undefined : segment.value
|
|
||||||
}
|
|
||||||
value={segment.isPlaceholder ? undefined : segment.value}
|
value={segment.isPlaceholder ? undefined : segment.value}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user