Skip to content

Commit d6cf716

Browse files
committed
fix(date-time-picker): make hour/minute/period updates persist across repeated selections
1 parent 34d9259 commit d6cf716

1 file changed

Lines changed: 148 additions & 18 deletions

File tree

‎src/components/date-time-picker/index.tsx‎

Lines changed: 148 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,69 @@ function normalizeMinuteStep(step?: number): number {
5656
return Math.min(30, Math.max(1, Math.floor(step ?? 1)))
5757
}
5858

59+
function getDateValueTime(value?: DateValue): { hour: number; minute: number } | null {
60+
if (!value || typeof value !== 'object') {
61+
return null
62+
}
63+
64+
if (!('hour' in value) || !('minute' in value)) {
65+
return null
66+
}
67+
68+
if (typeof value.hour !== 'number' || typeof value.minute !== 'number') {
69+
return null
70+
}
71+
72+
return {
73+
hour: value.hour,
74+
minute: value.minute,
75+
}
76+
}
77+
78+
function getDateKey(value?: DateValue): string | null {
79+
if (!value || typeof value !== 'object') {
80+
return null
81+
}
82+
83+
if (!('year' in value) || !('month' in value) || !('day' in value)) {
84+
return null
85+
}
86+
87+
if (
88+
typeof value.year !== 'number' ||
89+
typeof value.month !== 'number' ||
90+
typeof value.day !== 'number'
91+
) {
92+
return null
93+
}
94+
95+
return `${value.year}-${value.month}-${value.day}`
96+
}
97+
98+
function getDateParts(value?: DateValue): { year: number; month: number; day: number } | null {
99+
if (!value || typeof value !== 'object') {
100+
return null
101+
}
102+
103+
if (!('year' in value) || !('month' in value) || !('day' in value)) {
104+
return null
105+
}
106+
107+
if (
108+
typeof value.year !== 'number' ||
109+
typeof value.month !== 'number' ||
110+
typeof value.day !== 'number'
111+
) {
112+
return null
113+
}
114+
115+
return {
116+
year: value.year,
117+
month: value.month,
118+
day: value.day,
119+
}
120+
}
121+
59122
function DayViewTable(props: {
60123
weeks: DateValue[][]
61124
weekDays: { short: string }[]
@@ -151,6 +214,8 @@ function TimePanel(props: {
151214
minuteStep: number
152215
activeRangeIndex: number
153216
onActiveRangeIndexChange: (index: number) => void
217+
resolveTimeForValue: (value?: DateValue) => { hour: number; minute: number } | null
218+
setTimeForValue: (value: DateValue, time: { hour: number; minute: number }) => void
154219
}) {
155220
const minuteOptions = createMemo(() => {
156221
const values: number[] = []
@@ -173,31 +238,45 @@ function TimePanel(props: {
173238
{api => {
174239
const selectedIndex = () => (api().selectionMode === 'range' ? props.activeRangeIndex : 0)
175240
const fallbackDate = () => new Date()
176-
const selectedDate = () => api().valueAsDate[selectedIndex()]
177-
const selectedHour = () => selectedDate()?.getHours() ?? fallbackDate().getHours()
178-
const selectedMinute = () => selectedDate()?.getMinutes() ?? fallbackDate().getMinutes()
241+
const selectedValue = () => api().value[selectedIndex()]
242+
const selectedTime = () => props.resolveTimeForValue(selectedValue())
243+
const selectedHour = () => selectedTime()?.hour ?? fallbackDate().getHours()
244+
const selectedMinute = () => selectedTime()?.minute ?? fallbackDate().getMinutes()
245+
const nowAsCalendarDateTime = () => {
246+
const now = new Date()
247+
248+
return new CalendarDateTime(
249+
now.getFullYear(),
250+
now.getMonth() + 1,
251+
now.getDate(),
252+
now.getHours(),
253+
now.getMinutes(),
254+
)
255+
}
179256

180257
const setTime = (hour: number, minute: number) => {
181258
const index = selectedIndex()
182259
const current = api().value[index]
183-
const fallback = api().focusedValue
260+
const focusedValue = api().focusedValue
261+
const fallback = focusedValue ?? nowAsCalendarDateTime()
262+
const base = current ?? focusedValue ?? fallback
184263

185-
if (!current && !fallback) {
186-
return
187-
}
188-
189-
const base = current ?? fallback
190264
const nextValue =
191265
base instanceof CalendarDateTime
192266
? base.set({ hour, minute })
193267
: new CalendarDateTime(base.year, base.month, base.day, hour, minute)
194268

195269
const nextValues = [...api().value]
196-
if (api().selectionMode === 'range' && index === 1 && !nextValues[0] && fallback) {
197-
nextValues[0] = fallback
270+
if (api().selectionMode === 'range' && index === 1 && !nextValues[0] && focusedValue) {
271+
nextValues[0] = focusedValue
198272
}
199273
nextValues[index] = nextValue
200-
api().setValue(nextValues)
274+
props.setTimeForValue(nextValue, { hour, minute })
275+
276+
api().setValue([])
277+
queueMicrotask(() => {
278+
api().setValue(nextValues)
279+
})
201280
}
202281

203282
const selectHour = (hourOption: number) => {
@@ -238,7 +317,7 @@ function TimePanel(props: {
238317
}
239318

240319
const isHourSelected = (hour: number) => {
241-
if (!selectedDate()) {
320+
if (!selectedValue()) {
242321
return false
243322
}
244323

@@ -250,7 +329,10 @@ function TimePanel(props: {
250329
}
251330

252331
return (
253-
<div data-slot="date-time-picker-time-panel" class="w-full min-w-[13.5rem] space-y-2 sm:h-[320px] overflow-hidden">
332+
<div
333+
data-slot="date-time-picker-time-panel"
334+
class="w-full min-w-[13.5rem] space-y-2 sm:h-[320px] overflow-hidden"
335+
>
254336
<Show when={api().selectionMode === 'range'}>
255337
<div class="grid grid-cols-2 gap-1">
256338
<button
@@ -277,7 +359,10 @@ function TimePanel(props: {
277359
</Show>
278360

279361
<div
280-
class={cx('grid gap-2 h-full', props.hourCycle === 12 ? 'grid-cols-3' : 'grid-cols-2')}
362+
class={cx(
363+
'grid gap-2 h-full',
364+
props.hourCycle === 12 ? 'grid-cols-3' : 'grid-cols-2',
365+
)}
281366
>
282367
<ScrollArea class="rounded-component border">
283368
<div class="grid gap-1 p-1">
@@ -307,7 +392,7 @@ function TimePanel(props: {
307392
type="button"
308393
class={buttonVariants({
309394
variant:
310-
selectedDate() && selectedMinute() === minute ? 'default' : 'ghost',
395+
selectedValue() && selectedMinute() === minute ? 'default' : 'ghost',
311396
size: 'icon',
312397
class: 'h-8 w-full',
313398
})}
@@ -329,7 +414,7 @@ function TimePanel(props: {
329414
type="button"
330415
class={buttonVariants({
331416
variant:
332-
selectedDate() &&
417+
selectedValue() &&
333418
((period === 'AM' && selectedHour() < 12) ||
334419
(period === 'PM' && selectedHour() >= 12))
335420
? 'default'
@@ -382,7 +467,19 @@ export const DateTimePicker = (props: DateTimePickerProps) => {
382467
)
383468

384469
const defaultFormat = (date: DateValue) => {
385-
const parsedDate = new Date(Date.parse(date.toString()))
470+
const dateParts = getDateParts(date)
471+
if (!dateParts) {
472+
return date.toString()
473+
}
474+
475+
const time = resolveTimeForValue(date) ?? { hour: 0, minute: 0 }
476+
const parsedDate = new Date(
477+
dateParts.year,
478+
dateParts.month - 1,
479+
dateParts.day,
480+
time.hour,
481+
time.minute,
482+
)
386483

387484
return new Intl.DateTimeFormat(local.locale ?? globalLocale.locale ?? 'en-US', {
388485
year: 'numeric',
@@ -395,6 +492,35 @@ export const DateTimePicker = (props: DateTimePickerProps) => {
395492
}
396493

397494
const [activeRangeIndex, setActiveRangeIndex] = createSignal(0)
495+
const [timeByDateKey, setTimeByDateKey] = createSignal<
496+
Record<string, { hour: number; minute: number }>
497+
>({})
498+
499+
const resolveTimeForValue = (value?: DateValue) => {
500+
const valueTime = getDateValueTime(value)
501+
if (valueTime) {
502+
return valueTime
503+
}
504+
505+
const key = getDateKey(value)
506+
if (!key) {
507+
return null
508+
}
509+
510+
return timeByDateKey()[key] ?? null
511+
}
512+
513+
const setTimeForValue = (value: DateValue, time: { hour: number; minute: number }) => {
514+
const key = getDateKey(value)
515+
if (!key) {
516+
return
517+
}
518+
519+
setTimeByDateKey(prev => ({
520+
...prev,
521+
[key]: time,
522+
}))
523+
}
398524

399525
return (
400526
<DatePicker
@@ -458,6 +584,8 @@ export const DateTimePicker = (props: DateTimePickerProps) => {
458584
onActiveRangeIndexChange={index => {
459585
setActiveRangeIndex(index)
460586
}}
587+
resolveTimeForValue={resolveTimeForValue}
588+
setTimeForValue={setTimeForValue}
461589
/>
462590
</div>
463591
</DatePickerContent>
@@ -475,6 +603,8 @@ export const DateTimePicker = (props: DateTimePickerProps) => {
475603
onActiveRangeIndexChange={index => {
476604
setActiveRangeIndex(index)
477605
}}
606+
resolveTimeForValue={resolveTimeForValue}
607+
setTimeForValue={setTimeForValue}
478608
/>
479609
</div>
480610
</DatePickerContent>

0 commit comments

Comments
 (0)