feat: refactor date-and-time-picker to use custom dayjs utility and add timezone support (#15101)

This commit is contained in:
Wu Tianwei
2025-03-06 16:24:03 +08:00
committed by GitHub
parent 6101733232
commit 5be8fbab56
7 changed files with 102 additions and 38 deletions

View File

@@ -1,10 +1,16 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import dayjs, { type Dayjs } from 'dayjs'
import { RiCalendarLine, RiCloseCircleFill } from '@remixicon/react'
import cn from '@/utils/classnames'
import type { DatePickerProps, Period } from '../types'
import { ViewType } from '../types'
import { cloneTime, getDaysInMonth, getHourIn12Hour } from '../utils'
import type { Dayjs } from 'dayjs'
import dayjs, {
clearMonthMapCache,
cloneTime,
getDateWithTimezone,
getDaysInMonth,
getHourIn12Hour,
} from '../utils/dayjs'
import {
PortalToFollowElem,
PortalToFollowElemContent,
@@ -22,6 +28,7 @@ import { useTranslation } from 'react-i18next'
const DatePicker = ({
value,
timezone,
onChange,
onClear,
placeholder,
@@ -32,12 +39,15 @@ const DatePicker = ({
const [isOpen, setIsOpen] = useState(false)
const [view, setView] = useState(ViewType.date)
const containerRef = useRef<HTMLDivElement>(null)
const isInitial = useRef(true)
const inputValue = useRef(value ? value.tz(timezone) : undefined).current
const defaultValue = useRef(getDateWithTimezone({ timezone })).current
const [currentDate, setCurrentDate] = useState(value || dayjs())
const [selectedDate, setSelectedDate] = useState(value)
const [currentDate, setCurrentDate] = useState(inputValue || defaultValue)
const [selectedDate, setSelectedDate] = useState(inputValue)
const [selectedMonth, setSelectedMonth] = useState((value || dayjs()).month())
const [selectedYear, setSelectedYear] = useState((value || dayjs()).year())
const [selectedMonth, setSelectedMonth] = useState((inputValue || defaultValue).month())
const [selectedYear, setSelectedYear] = useState((inputValue || defaultValue).year())
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
@@ -50,6 +60,25 @@ const DatePicker = ({
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [])
useEffect(() => {
if (isInitial.current) {
isInitial.current = false
return
}
clearMonthMapCache()
if (value) {
const newValue = getDateWithTimezone({ date: value, timezone })
setCurrentDate(newValue)
setSelectedDate(newValue)
onChange(newValue)
}
else {
setCurrentDate(prev => getDateWithTimezone({ date: prev, timezone }))
setSelectedDate(prev => prev ? getDateWithTimezone({ date: prev, timezone }) : undefined)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [timezone])
const handleClickTrigger = (e: React.MouseEvent) => {
e.stopPropagation()
if (isOpen) {
@@ -58,15 +87,15 @@ const DatePicker = ({
}
setView(ViewType.date)
setIsOpen(true)
if (value) {
setCurrentDate(value)
setSelectedDate(value)
}
}
const handleClear = (e: React.MouseEvent) => {
const newDate = dayjs()
e.stopPropagation()
setSelectedDate(undefined)
setCurrentDate(prev => prev || newDate)
setSelectedMonth(prev => prev || newDate.month())
setSelectedYear(prev => prev || newDate.year())
if (!isOpen)
onClear()
}
@@ -84,13 +113,13 @@ const DatePicker = ({
}, [currentDate])
const handleDateSelect = useCallback((day: Dayjs) => {
const newDate = cloneTime(day, selectedDate || dayjs())
const newDate = cloneTime(day, selectedDate || getDateWithTimezone({ timezone }))
setCurrentDate(newDate)
setSelectedDate(newDate)
}, [selectedDate])
}, [selectedDate, timezone])
const handleSelectCurrentDate = () => {
const newDate = dayjs()
const newDate = getDateWithTimezone({ timezone })
setCurrentDate(newDate)
setSelectedDate(newDate)
onChange(newDate)
@@ -119,19 +148,19 @@ const DatePicker = ({
}
const handleSelectHour = useCallback((hour: string) => {
const selectedTime = selectedDate || dayjs()
const selectedTime = selectedDate || getDateWithTimezone({ timezone })
handleTimeSelect(hour, selectedTime.minute().toString().padStart(2, '0'), selectedTime.format('A') as Period)
}, [selectedDate])
}, [selectedDate, timezone])
const handleSelectMinute = useCallback((minute: string) => {
const selectedTime = selectedDate || dayjs()
const selectedTime = selectedDate || getDateWithTimezone({ timezone })
handleTimeSelect(getHourIn12Hour(selectedTime).toString().padStart(2, '0'), minute, selectedTime.format('A') as Period)
}, [selectedDate])
}, [selectedDate, timezone])
const handleSelectPeriod = useCallback((period: Period) => {
const selectedTime = selectedDate || dayjs()
const selectedTime = selectedDate || getDateWithTimezone({ timezone })
handleTimeSelect(getHourIn12Hour(selectedTime).toString().padStart(2, '0'), selectedTime.minute().toString().padStart(2, '0'), period)
}, [selectedDate])
}, [selectedDate, timezone])
const handleOpenYearMonthPicker = () => {
setSelectedMonth(currentDate.month())
@@ -156,15 +185,13 @@ const DatePicker = ({
}, [])
const handleYearMonthConfirm = () => {
setCurrentDate((prev) => {
return prev ? prev.clone().month(selectedMonth).year(selectedYear) : dayjs().month(selectedMonth).year(selectedYear)
})
setCurrentDate(prev => prev.clone().month(selectedMonth).year(selectedYear))
setView(ViewType.date)
}
const timeFormat = needTimePicker ? 'MMMM D, YYYY hh:mm A' : 'MMMM D, YYYY'
const displayValue = value?.format(timeFormat) || ''
const displayTime = (selectedDate || dayjs().startOf('day')).format('hh:mm A')
const displayTime = selectedDate?.format('hh:mm A') || '--:-- --'
const placeholderDate = isOpen && selectedDate ? selectedDate.format(timeFormat) : (placeholder || t('time.defaultPlaceholder'))
return (