Skip to content

Commit 9f53dbb

Browse files
fix(DatePicker): reset time after clearing value (#2636)
* fix(DatePicker): reset time after clearing value * docs(DatePicker): document resetTime helper * fix: add Include millisecond field in time reset packages/core/src/DatePicker/DatePickerRoot.vue Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * test(DatePicker): cover ZonedDateTime reset * fix(DatePicker): reset stale placeholder time --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent 5bceab6 commit 9f53dbb

2 files changed

Lines changed: 141 additions & 3 deletions

File tree

packages/core/src/DatePicker/DatePicker.test.ts

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ function getTimeSegments(getByTestId: (...args: any[]) => HTMLElement) {
2626
}
2727
}
2828

29-
function setup(props: { datePickerProps?: DatePickerRootProps, emits?: { 'onUpdate:modelValue'?: (data: DateValue) => void } } = {}) {
29+
function setup(props: { datePickerProps?: DatePickerRootProps, emits?: { 'onUpdate:modelValue'?: (data: DateValue | undefined) => void } } = {}) {
3030
const user = userEvent.setup()
3131
const returned = render(DatePicker, { props })
3232
const month = returned.getByTestId('month')
@@ -252,6 +252,128 @@ describe('datePicker', async () => {
252252
expect(calendar.querySelector('[data-selected]')).not.toBeInTheDocument()
253253
})
254254

255+
it('resets stale placeholder time when selecting a date after the model value is cleared', async () => {
256+
const emittedValues: (DateValue | undefined)[] = []
257+
const { user, trigger, getByTestId, rerender } = setup({
258+
datePickerProps: {
259+
modelValue: calendarDateTime,
260+
granularity: 'minute',
261+
},
262+
emits: {
263+
'onUpdate:modelValue': value => emittedValues.push(value),
264+
},
265+
})
266+
267+
await rerender({
268+
datePickerProps: {
269+
modelValue: undefined,
270+
granularity: 'minute',
271+
},
272+
emits: {
273+
'onUpdate:modelValue': value => emittedValues.push(value),
274+
},
275+
})
276+
277+
await user.click(trigger)
278+
await user.click(getByTestId('date-1-1'))
279+
280+
const selectedValue = emittedValues.at(-1)
281+
expect(selectedValue).toBeInstanceOf(CalendarDateTime)
282+
expect((selectedValue as CalendarDateTime).hour).toBe(0)
283+
expect((selectedValue as CalendarDateTime).minute).toBe(0)
284+
expect((selectedValue as CalendarDateTime).second).toBe(0)
285+
expect((selectedValue as CalendarDateTime).millisecond).toBe(0)
286+
})
287+
288+
it('resets stale ZonedDateTime placeholder time when selecting a date after the model value is cleared', async () => {
289+
const emittedValues: (DateValue | undefined)[] = []
290+
const zonedValue = toZoned(new CalendarDateTime(1980, 1, 20, 12, 30, 45, 123), 'America/New_York')
291+
const { user, trigger, getByTestId, rerender } = setup({
292+
datePickerProps: {
293+
modelValue: zonedValue,
294+
granularity: 'second',
295+
},
296+
emits: {
297+
'onUpdate:modelValue': value => emittedValues.push(value),
298+
},
299+
})
300+
301+
await rerender({
302+
datePickerProps: {
303+
modelValue: undefined,
304+
granularity: 'second',
305+
},
306+
emits: {
307+
'onUpdate:modelValue': value => emittedValues.push(value),
308+
},
309+
})
310+
311+
await user.click(trigger)
312+
await user.click(getByTestId('date-1-1'))
313+
314+
const selectedValue = emittedValues.at(-1)
315+
expect(selectedValue).toHaveProperty('timeZone', 'America/New_York')
316+
expect(selectedValue?.hour).toBe(0)
317+
expect(selectedValue?.minute).toBe(0)
318+
expect(selectedValue?.second).toBe(0)
319+
expect(selectedValue?.millisecond).toBe(0)
320+
})
321+
322+
it('preserves typed time when typing a date after the model value is cleared', async () => {
323+
const emittedValues: (DateValue | undefined)[] = []
324+
let rerender: ReturnType<typeof setup>['rerender']
325+
const handleUpdateModelValue = (value: DateValue | undefined) => {
326+
emittedValues.push(value)
327+
return rerender({
328+
datePickerProps: {
329+
modelValue: value,
330+
granularity: 'minute',
331+
},
332+
emits: {
333+
'onUpdate:modelValue': handleUpdateModelValue,
334+
},
335+
})
336+
}
337+
338+
const view = setup({
339+
datePickerProps: {
340+
modelValue: calendarDateTime,
341+
granularity: 'minute',
342+
},
343+
emits: {
344+
'onUpdate:modelValue': handleUpdateModelValue,
345+
},
346+
})
347+
rerender = view.rerender
348+
349+
await rerender({
350+
datePickerProps: {
351+
modelValue: undefined,
352+
granularity: 'minute',
353+
},
354+
emits: {
355+
'onUpdate:modelValue': handleUpdateModelValue,
356+
},
357+
})
358+
359+
await view.user.click(view.month)
360+
await view.user.keyboard('{2}')
361+
await view.user.click(view.day)
362+
await view.user.keyboard('{3}')
363+
await view.user.click(view.year)
364+
await view.user.keyboard('{2020}')
365+
await view.user.click(view.getByTestId('hour'))
366+
await view.user.keyboard('{9}')
367+
await view.user.click(view.getByTestId('minute'))
368+
await view.user.keyboard('{45}')
369+
370+
expect(view.month).toHaveTextContent('2')
371+
expect(view.day).toHaveTextContent('3')
372+
expect(view.year).toHaveTextContent('2020')
373+
expect(view.getByTestId('hour')).toHaveTextContent('9')
374+
expect(view.getByTestId('minute')).toHaveTextContent('45')
375+
})
376+
255377
it('should close the picker on select when `closeOnSelect` is true', async () => {
256378
const { user, trigger, getByTestId } = setup({
257379
datePickerProps: {

packages/core/src/DatePicker/DatePickerRoot.vue

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,10 +140,23 @@ const open = useVModel(props, 'open', emits, {
140140
141141
const dateFieldRef = ref<InstanceType<typeof DateFieldRoot> | undefined>()
142142
143+
/**
144+
* Reset time fields on DateValue instances that support time granularity.
145+
*/
146+
function resetTime(date: DateValue) {
147+
if (!('hour' in date))
148+
return date
149+
150+
return date.set({ hour: 0, minute: 0, second: 0, millisecond: 0 })
151+
}
152+
143153
watch(modelValue, (value) => {
144154
if (value && value.compare(placeholder.value) !== 0) {
145155
placeholder.value = value.copy()
146156
}
157+
else if (!value && 'hour' in placeholder.value) {
158+
placeholder.value = resetTime(placeholder.value)
159+
}
147160
if (closeOnSelect.value) {
148161
open.value = false
149162
}
@@ -178,8 +191,11 @@ provideDatePickerRootContext({
178191
dir,
179192
step,
180193
onDateChange(date: DateValue | undefined) {
181-
if (!date || !modelValue.value) {
182-
modelValue.value = date?.copy() ?? undefined
194+
if (!date) {
195+
modelValue.value = undefined
196+
}
197+
else if (!modelValue.value) {
198+
modelValue.value = date.copy()
183199
}
184200
else if (!preventDeselect.value && date && modelValue.value.compare(date) === 0) {
185201
modelValue.value = undefined

0 commit comments

Comments
 (0)