185 lines
5.7 KiB
TypeScript
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>
|
|
)
|
|
}
|