From 20652107fe661bcce10f65e55a35ac753ef77401 Mon Sep 17 00:00:00 2001 From: Noblet Ouways <91417411+Dsaquel@users.noreply.github.com> Date: Tue, 5 Aug 2025 16:28:26 +0200 Subject: [PATCH] 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 --- docs/en-US/component/date-picker.md | 2 +- docs/en-US/component/datetime-picker.md | 2 +- .../date-picker/__tests__/date-picker.test.ts | 92 ++++++++++++++++++- .../__tests__/date-time-picker.test.tsx | 50 +++++++++- .../src/composables/use-range-picker.ts | 12 ++- .../src/date-picker-com/panel-date-range.vue | 18 ++-- 6 files changed, 161 insertions(+), 15 deletions(-) diff --git a/docs/en-US/component/date-picker.md b/docs/en-US/component/date-picker.md index d02f2cd6c1..23a31ba1c4 100644 --- a/docs/en-US/component/date-picker.md +++ b/docs/en-US/component/date-picker.md @@ -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` | diff --git a/docs/en-US/component/datetime-picker.md b/docs/en-US/component/datetime-picker.md index f20a99beee..a47e35cf1c 100644 --- a/docs/en-US/component/datetime-picker.md +++ b/docs/en-US/component/datetime-picker.md @@ -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` | diff --git a/packages/components/date-picker/__tests__/date-picker.test.ts b/packages/components/date-picker/__tests__/date-picker.test.ts index 7c85dcec1d..8d0e2fad01 100644 --- a/packages/components/date-picker/__tests__/date-picker.test.ts +++ b/packages/components/date-picker/__tests__/date-picker.test.ts @@ -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( ` { />`, () => ({ 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( + ``, + () => ({ + 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> + 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( + ` + `, + () => ({ value: baseValue }), + { + methods: { + changeDate() { + spy() + this.value = newVal + }, + }, + } + ) + await nextTick() + const rangePanelWrapper = wrapper.findComponent( + '.el-date-range-picker' + ) as VueWrapper> + 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() diff --git a/packages/components/date-picker/__tests__/date-time-picker.test.tsx b/packages/components/date-picker/__tests__/date-time-picker.test.tsx index 3152f7d8b9..c96814d75d 100644 --- a/packages/components/date-picker/__tests__/date-time-picker.test.tsx +++ b/packages/components/date-picker/__tests__/date-time-picker.test.tsx @@ -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(() => ( )) + 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(() => ( + + )) + const input = wrapper.find('input') + await input.trigger('blur') + await input.trigger('focus') + + const rangePanelWrapper = wrapper.findComponent( + '.el-date-range-picker' + ) as VueWrapper> + 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) + }) }) diff --git a/packages/components/date-picker/src/composables/use-range-picker.ts b/packages/components/date-picker/src/composables/use-range-picker.ts index 3e643be643..b5dfb7e2ad 100644 --- a/packages/components/date-picker/src/composables/use-range-picker.ts +++ b/packages/components/date-picker/src/composables/use-range-picker.ts @@ -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, diff --git a/packages/components/date-picker/src/date-picker-com/panel-date-range.vue b/packages/components/date-picker/src/date-picker-com/panel-date-range.vue index 1a8040a237..ed3381f53c 100644 --- a/packages/components/date-picker/src/date-picker-com/panel-date-range.vue +++ b/packages/components/date-picker/src/date-picker-com/panel-date-range.vue @@ -438,6 +438,7 @@ const defaultValue = toRef(pickerBase.props, 'defaultValue') const { lang } = useLocale() const leftDate = ref(dayjs().locale(lang.value)) const rightDate = ref(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)