fix(components): [date-picker] keep datetimerange close on value change (#21625)

* fix(components): [date-picker] keep datetimerange close on value change

* chore: review past solution

* test: add test case

* docs: update onChange event

* test: test
This commit is contained in:
Noblet Ouways
2025-08-05 16:28:26 +02:00
committed by GitHub
parent 6cf0a1d48f
commit 20652107fe
6 changed files with 161 additions and 15 deletions

View File

@@ -201,7 +201,7 @@ Note, date time locale (month name, first day of the week ...) are also configur
| Name | Description | Type |
| --------------- | --------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| change | triggers when user confirms the value | ^[Function]`(val: typeof v-model) => void` |
| change | triggers when user confirms the value or click outside | ^[Function]`(val: typeof v-model) => void` |
| blur | triggers when Input blurs | ^[Function]`(e: FocusEvent) => void` |
| focus | triggers when Input focuses | ^[Function]`(e: FocusEvent) => void` |
| clear ^(2.7.7) | triggers when the clear icon is clicked in a clearable DatePicker | ^[Function]`() => void` |

View File

@@ -122,7 +122,7 @@ datetime-picker/custom-icon
| Name | Description | Parameters |
| --------------- | --------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| change | triggers when user confirms the value | ^[Function]`(val: typeof v-model) => void` |
| change | triggers when user confirms the value or click outside | ^[Function]`(val: typeof v-model) => void` |
| blur | triggers when Input blurs | ^[Function]`(e: FocusEvent) => void` |
| focus | triggers when Input focuses | ^[Function]`(e: FocusEvent) => void` |
| clear ^(2.7.7) | triggers when the clear icon is clicked in a clearable DateTimePicker | ^[Function]`() => void` |

View File

@@ -1,6 +1,6 @@
// @ts-nocheck
import { nextTick } from 'vue'
import { mount } from '@vue/test-utils'
import { flushPromises, mount } from '@vue/test-utils'
import { afterEach, describe, expect, it, vi } from 'vitest'
import dayjs from 'dayjs'
import { rAF } from '@element-plus/test-utils/tick'
@@ -17,6 +17,8 @@ import { EVENT_CODE } from '@element-plus/constants'
import { ElFormItem } from '@element-plus/components/form'
import DatePicker from '../src/date-picker'
import type DatePickerRange from '../src/date-picker-com/panel-date-range.vue'
const _mount = (template: string, data = () => ({}), otherObj?) =>
mount(
{
@@ -1970,7 +1972,7 @@ describe('DateRangePicker', () => {
})
it('range, shows weekNumber', async () => {
_mount(
const wrapper = _mount(
`<el-date-picker
v-model="value"
type="daterange"
@@ -1978,6 +1980,10 @@ describe('DateRangePicker', () => {
/>`,
() => ({ value: [new Date(2025, 0, 1), new Date(2025, 1, 1)] })
)
const input = wrapper.find('input')
input.trigger('blur')
input.trigger('focus')
await nextTick()
const weeks = document.querySelectorAll('td.week')
expect(weeks.length).toBe(12)
@@ -1996,6 +2002,85 @@ describe('DateRangePicker', () => {
'10',
])
})
it('should not be visible after input two dates', async () => {
const onChange = vi.fn()
const onUpdateModelValue = vi.fn()
const wrapper = _mount(
`<el-date-picker
v-model="value"
type="daterange"
@change="onChange"
@update:modelValue="onUpdateModelValue"
/>`,
() => ({
value: [new Date(2025, 0, 1), new Date(2025, 1, 1)],
onChange,
onUpdateModelValue,
})
)
const input = wrapper.find('input')
await input.trigger('blur')
await input.trigger('focus')
const rangePanelWrapper = wrapper.findComponent(
'.el-date-range-picker'
) as VueWrapper<InstanceType<typeof DatePickerRange>>
expect(rangePanelWrapper.exists()).toBe(true)
expect(rangePanelWrapper.vm.visible).toBe(true)
const cells = document.querySelectorAll('.available .el-date-table-cell')
;(cells[0] as HTMLElement).click()
await flushPromises()
expect(onChange).not.toHaveBeenCalled()
expect(onUpdateModelValue).not.toHaveBeenCalled()
;(cells[1] as HTMLElement).click()
await flushPromises()
expect(onChange).toHaveBeenCalledOnce()
expect(onUpdateModelValue).toHaveBeenCalledOnce()
expect(rangePanelWrapper.vm.visible).toBe(false)
})
it('should not trigger popper dropdown on dynamic assignment', async () => {
const spy = vi.fn()
const baseValue = [new Date(2025, 0, 1), new Date(2025, 0, 2)]
const newVal = [new Date(2025, 0, 3), new Date(2025, 0, 4)]
const wrapper = _mount(
`<el-date-picker
v-model="value"
type="daterange"
/>
<button @click="changeDate">click</button>`,
() => ({ value: baseValue }),
{
methods: {
changeDate() {
spy()
this.value = newVal
},
},
}
)
await nextTick()
const rangePanelWrapper = wrapper.findComponent(
'.el-date-range-picker'
) as VueWrapper<InstanceType<typeof DatePickerRange>>
const inputRange = wrapper.findAll('.el-range-input')
expect(rangePanelWrapper.exists()).toBe(true)
expect(rangePanelWrapper.vm.visible).toBe(false)
expect(inputRange[0].element.value).toBe('2025-01-01')
expect(inputRange[1].element.value).toBe('2025-01-02')
await wrapper.find('button').trigger('click')
await nextTick()
expect(spy).toHaveBeenCalled()
expect(inputRange[0].element.value).toBe('2025-01-03')
expect(inputRange[1].element.value).toBe('2025-01-04')
expect(rangePanelWrapper.vm.parsedValue.map((s) => s.toDate())).toEqual(
newVal
)
expect(rangePanelWrapper.vm.visible).toBe(false)
})
})
describe('MonthRange', () => {
@@ -2035,6 +2120,9 @@ describe('MonthRange', () => {
// input text is something like date string
expect(inputs[0].element.value.length).toBe(7)
expect(inputs[1].element.value.length).toBe(7)
inputs[0].trigger('blur')
inputs[0].trigger('focus')
await nextTick()
// reverse selection
p1.click()
await nextTick()

View File

@@ -1,5 +1,5 @@
import { nextTick, ref } from 'vue'
import { mount } from '@vue/test-utils'
import { flushPromises, mount } from '@vue/test-utils'
import { afterEach, describe, expect, it, vi } from 'vitest'
import dayjs from 'dayjs'
import triggerEvent from '@element-plus/test-utils/trigger-event'
@@ -958,9 +958,13 @@ describe('Datetimerange', () => {
it('shows weekNumber', async () => {
const value = ref([new Date(2025, 0, 1), new Date(2025, 1, 1)])
_mount(() => (
const wrapper = _mount(() => (
<DatePicker v-model={value.value} type="datetimerange" show-week-number />
))
const input = wrapper.find('input')
input.trigger('blur')
input.trigger('focus')
await nextTick()
const weeks = document.querySelectorAll('td.week')
expect(weeks.length).toBe(12)
@@ -1033,4 +1037,46 @@ describe('Datetimerange', () => {
expect(document.querySelector('.el-picker-panel__footer')).toBeNull()
})
})
it('should datetimerange visibility not be trapped by setting new values', async () => {
const values = ref(['2025-08-02', '2025-08-02'])
const spy = vi.fn()
const onChange = () => {
values.value = ['2025-08-02', '2025-08-02']
spy()
}
const wrapper = _mount(() => (
<DatePicker
v-model={values.value}
type="datetimerange"
valueFormat="YYYY-MM-DD"
//@ts-expect-error
onChange={onChange}
/>
))
const input = wrapper.find('input')
await input.trigger('blur')
await input.trigger('focus')
const rangePanelWrapper = wrapper.findComponent(
'.el-date-range-picker'
) as VueWrapper<InstanceType<typeof DatePickerRange>>
expect(rangePanelWrapper.exists()).toBe(true)
expect(rangePanelWrapper.vm.visible).toBe(true)
const cells = document.querySelectorAll('.available .el-date-table-cell')
;(cells[0] as HTMLElement).click()
await nextTick()
;(cells[1] as HTMLElement).click()
await nextTick()
const button = document.querySelectorAll(
'.el-picker-panel__footer button'
)![1] as HTMLButtonElement
button.click()
await flushPromises()
expect(spy).toHaveBeenCalledOnce()
expect(rangePanelWrapper.vm.visible).toBe(false)
})
})

View File

@@ -128,13 +128,23 @@ export const useRangePicker = (
watch(
() => props.parsedValue,
(parsedValue) => {
if (!props.visible || !parsedValue?.length) {
if (!parsedValue?.length) {
onReset(parsedValue)
}
},
{ immediate: true }
)
watch(
() => props.visible,
() => {
if (props.visible) {
onReset(props.parsedValue)
}
},
{ immediate: true }
)
return {
minDate,
maxDate,

View File

@@ -438,6 +438,7 @@ const defaultValue = toRef(pickerBase.props, 'defaultValue')
const { lang } = useLocale()
const leftDate = ref<Dayjs>(dayjs().locale(lang.value))
const rightDate = ref<Dayjs>(dayjs().locale(lang.value).add(1, unit))
let shouldBeVisible = true
const {
minDate,
@@ -688,16 +689,17 @@ const handleRangePick = (
maxDate.value = maxDate_
minDate.value = minDate_
handleRangeConfirm(close)
if (!showTime.value && close) {
close = !minDate_ || !maxDate_
}
shouldBeVisible = close
}
watch(
[maxDate, minDate],
([min, max]) => {
if (min && max) handleRangeConfirm(true)
},
{ flush: 'post' }
)
watch([maxDate, minDate], ([max, min]) => {
if (max && min) {
handleRangeConfirm(shouldBeVisible)
}
})
const minTimePickerVisible = ref(false)
const maxTimePickerVisible = ref(false)