datenel-react/src/components/SingleDatePicker.tsx

143 lines
6.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import LeftArrowIcon from '@/assets/icons/left-arrow.svg'
import RightArrowIcon from '@/assets/icons/right-arrow.svg'
import { useEffect, useState } from 'react'
import { getCalendarDates, getL10Weekday } from '../utils'
interface Props {
/**
* Control the selected
* date programmatically, including situations like provide a default value or control the selected
* date by parent component.
*/
value?: Date | { year: number, month: number, day: number }
/**
* A callback function that will be called when a date is selected inside the panel.
* @param date - The date user selected.
* @returns
*/
onSelect?: (date: {
year: number,
month: number,
day: number
}) => void
/**
* The language code that will be used to localize the panel.
* Accept standard ISO 639-1 language code, such as 'zh-CN', 'en-US', 'ja-JP', etc. Default to the
* language of the users browser. Note that it will not effect to the screen reader, but the screen
* reader will still read the date in the users language.
*/
localization?: string
}
/**
* A panel that allows users to select a date.
*
* @component
*
* @param {Props} props
*/
export default ({ value, onSelect, localization }: Props) => {
const [currentMonth, setCurrentMonth] = useState(new Date().getMonth())
const [currentYear, setCurrentYear] = useState(new Date().getFullYear())
const [selectedDate, setSelectedDate] = useState(new Date())
const [dates, setDates] = useState<Date[]>([])
const [l10nDays, setL10nDays] = useState<string[]>([])
const [selectMonth, setSelectMonth] = useState(false)
useEffect(() => {
setDates(getCalendarDates(currentMonth, currentYear))
}, [currentMonth, currentYear])
useEffect(() => {
if (!value) return
const date = value instanceof Date ? value : new Date(value.year, value.month - 1, value.day)
setSelectedDate(date)
setCurrentMonth(date.getMonth())
setCurrentYear(date.getFullYear())
}, [value])
useEffect(() => {
const i18n = localization || navigator.language
setL10nDays(getL10Weekday(i18n))
}, [localization])
function selectDate(date: Date) {
setSelectedDate(date)
onSelect?.({
year: date.getFullYear(),
month: date.getMonth() + 1,
day: date.getDate()
})
}
function skipToLastMonth() {
if (currentMonth === 0) {
setCurrentMonth(11)
setCurrentYear(currentYear - 1)
}
else setCurrentMonth(currentMonth - 1)
}
function skipToNextMonth() {
if (currentMonth === 11) {
setCurrentMonth(0)
setCurrentYear(currentYear + 1)
}
else setCurrentMonth(currentMonth + 1)
}
if (selectMonth) return (
<div className='datenel-component' role="dialog" aria-label="Date selection panel, you are now at month and year quick-select">
<div className='header'>
<button className='stepper' onClick={() => {
if (currentYear === 0) return
setCurrentYear(currentYear - 1)
}} aria-label={`Go to last year, ${currentYear - 1}, you are now at year ${currentYear}`}><img src={LeftArrowIcon} /></button>
<input className='indicator'
value={currentYear}
onChange={e => setCurrentYear(parseInt(e.target.value))}
aria-label="Year input, type a year to go to that year"
/>
<button className='stepper' onClick={() => {
if (currentYear === 9999) return
setCurrentYear(currentYear + 1)
}} aria-label={`Go to next year, ${currentYear + 1}, you are now at year ${currentYear}`}><img src={RightArrowIcon} /></button>
</div>
<div className='month-selector-body'>
{Array.from({ length: 12 }).map((_, index) => <button className={`item`} key={index} onClick={() => {
setCurrentMonth(index)
setSelectMonth(false)
}} aria-label={`Go to ${new Date(currentYear, index).toLocaleString(localization || navigator.language, { month: 'long' })} of the year ${currentYear}`}>
{new Date(currentYear, index).toLocaleString(localization || navigator.language, { month: 'long' })}
</button>)}
</div>
</div>
)
else return (
<div className='datenel-component' role="dialog" aria-label="Date selection panel">
<div className='header'>
<button className='stepper' onClick={skipToLastMonth} aria-label={`Go to last month, ${new Date(currentYear, currentMonth - 1).toLocaleString(localization || navigator.language, { month: 'long' })}`}><img src={LeftArrowIcon} /></button>
<button className='indicator' onClick={() => setSelectMonth(true)} aria-label={`You are now at ${new Date(currentYear, currentMonth).toLocaleString(localization || navigator.language, { month: 'long', year: 'numeric' })}. Click here to quick-select month or year.`}>
{new Date(currentYear, currentMonth).toLocaleString(localization || navigator.language, { month: 'long', year: 'numeric' })}
</button>
<button className='stepper' onClick={skipToNextMonth} aria-label={`Go to next month, ${new Date(currentYear, currentMonth + 1).toLocaleString(localization || navigator.language, { month: 'long' })}`}><img src={RightArrowIcon} /></button>
</div>
<div className='calendar-view-body' aria-live="polite">
{l10nDays.map((day, index) => <div className='item day-indicator' key={index}>{day}</div>)}
{dates.map(date => <button
className={`item date ${currentMonth !== date.getMonth() && 'extra-month'} ${selectedDate.toDateString() === date.toDateString() && 'active'}`}
key={date.toISOString()}
onClick={() => selectDate(date)}
aria-label={`${date.toLocaleString(localization || navigator.language, { dateStyle: 'full' })}${date.toDateString() === new Date().toDateString() ? ", this is today" : ""}, click to select this date`}
tabIndex={currentMonth !== date.getMonth() ? -1 : 0}
aria-hidden={currentMonth !== date.getMonth()}
>
{date.getDate()}
{date.toDateString() === new Date().toDateString() && <svg xmlns="http://www.w3.org/2000/svg" className='today-indicator' viewBox="0 0 24 24" fill="currentColor"><path d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z"></path></svg>}
</button>)}
</div>
</div>
)
}