Files
web/components/TempDesignSystem/Form/Date/index.tsx
2024-10-16 11:15:00 +02:00

185 lines
5.7 KiB
TypeScript

"use client"
import { parseDate } from "@internationalized/date"
import { useState } from "react"
import { DateInput, DatePicker, Group } from "react-aria-components"
import { useController, useFormContext, useWatch } from "react-hook-form"
import { useIntl } from "react-intl"
import { dt } from "@/lib/dt"
import Select from "@/components/TempDesignSystem/Select"
import { rangeArray } from "@/utils/rangeArray"
import { DateName } from "./date"
import styles from "./date.module.css"
import type { Key } from "react-aria-components"
import type { DateProps } from "./date"
export default function DateSelect({ name, registerOptions = {} }: DateProps) {
const intl = useIntl()
const d = useWatch({ name })
const { control, setValue } = useFormContext()
const { field } = useController({
control,
name,
rules: registerOptions,
})
const [dateSegments, setDateSegment] = useState<{
year: number | null
month: number | null
date: number | null
daysInMonth: number
}>({
year: null,
month: null,
date: null,
daysInMonth: 31,
})
const currentYear = new Date().getFullYear()
const months = rangeArray(1, 12).map((month) => ({
value: month,
label: `${month}`,
}))
const years = rangeArray(1900, currentYear - 18)
.reverse()
.map((year) => ({ value: year, label: `${year}` }))
function createOnSelect(selector: DateName) {
/**
* Months are 0 index based and therefore we
* must subtract by 1 to get the selected month
*/
return (select: Key) => {
const value =
selector === DateName.month ? Number(select) - 1 : Number(select)
const newSegments = { ...dateSegments, [selector]: value }
/**
* Update daysInMonth when year or month changes
* to ensure the user can't select a date that doesn't exist.
*/
if (selector === DateName.year || selector === DateName.month) {
const year = selector === DateName.year ? value : newSegments.year
const month = selector === DateName.month ? value : newSegments.month
if (year !== null && month !== null) {
newSegments.daysInMonth = dt().year(year).month(month).daysInMonth()
}
}
if (Object.values(newSegments).every((val) => val !== null)) {
const newDate = dt()
.utc()
.set("year", newSegments.year!)
.set("month", newSegments.month!)
.set("date", Math.min(newSegments.date!, newSegments.daysInMonth))
setValue(name, newDate.format("YYYY-MM-DD"))
}
setDateSegment(newSegments)
}
}
const dayLabel = intl.formatMessage({ id: "Day" })
const monthLabel = intl.formatMessage({ id: "Month" })
const yearLabel = intl.formatMessage({ id: "Year" })
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(d).isValid() ? parseDate(d) : null
} catch (error) {
console.error(error)
}
return (
<DatePicker
aria-label={intl.formatMessage({ id: "Select date of birth" })}
isRequired={!!registerOptions.required}
name={name}
ref={field.ref}
value={dateValue}
data-testid={name}
>
<Group>
<DateInput className={styles.container}>
{(segment) => {
switch (segment.type) {
case "day":
const maxDays = dateSegments.daysInMonth
const days = rangeArray(1, maxDays).map((day) => ({
value: day,
label: `${day}`,
}))
return (
<div className={styles.day}>
<Select
aria-label={dayLabel}
items={days}
label={dayLabel}
name={DateName.date}
onSelect={createOnSelect(DateName.date)}
placeholder="DD"
required
tabIndex={3}
defaultSelectedKey={
segment.isPlaceholder ? undefined : segment.value
}
/>
</div>
)
case "month":
return (
<div className={styles.month}>
<Select
aria-label={monthLabel}
items={months}
label={monthLabel}
name={DateName.month}
onSelect={createOnSelect(DateName.month)}
placeholder="MM"
required
tabIndex={2}
defaultSelectedKey={
segment.isPlaceholder ? undefined : segment.value
}
/>
</div>
)
case "year":
return (
<div className={styles.year}>
<Select
aria-label={yearLabel}
items={years}
label={yearLabel}
name={DateName.year}
onSelect={createOnSelect(DateName.year)}
placeholder="YYYY"
required
tabIndex={1}
defaultSelectedKey={
segment.isPlaceholder ? undefined : segment.value
}
/>
</div>
)
default:
/** DateInput forces return of ReactElement */
return <></>
}
}}
</DateInput>
</Group>
</DatePicker>
)
}