From 69c25bcea488e2da6d4bf019ba714ce325d45f48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=BF=E8=8F=9C=20Cai?= Date: Thu, 2 Jul 2026 17:27:09 +0800 Subject: [PATCH 01/12] =?UTF-8?q?fix(date-range-picker):=20=E9=80=89?= =?UTF-8?q?=E5=AE=8C=E6=97=A5=E6=9C=9F=E5=85=B3=E9=97=AD=E9=9D=A2=E6=9D=BF?= =?UTF-8?q?=E6=97=B6=E8=A7=A6=E5=8F=91=20onVisibleChange?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/components/date-picker/DateRangePicker.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/components/date-picker/DateRangePicker.tsx b/packages/components/date-picker/DateRangePicker.tsx index df422b85d4..d3af494cd0 100644 --- a/packages/components/date-picker/DateRangePicker.tsx +++ b/packages/components/date-picker/DateRangePicker.tsx @@ -11,6 +11,7 @@ import { DatePickerYearChangeTrigger, DatePickerMonthChangeTrigger, PickerDateRange, + TdDateRangePickerProps, } from './type'; import { RangeInputPopup as TRangeInputPopup } from '../range-input'; @@ -297,6 +298,9 @@ export default defineComponent({ activeIndex.value = nextIndex as 0 | 1; isFirstValueSelected.value = normalizedValue.some((v) => !!v); } else { + // 受控关闭面板时主动通知用户 + const userPopupProps = props.popupProps as TdDateRangePickerProps['popupProps']; + userPopupProps?.onVisibleChange?.(false, { e, trigger: 'trigger-element-close' }); popupVisible.value = false; } } From 19b088945de32fda9467bbbf341be14a3c054b7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=BF=E8=8F=9C=20Cai?= Date: Thu, 2 Jul 2026 17:40:50 +0800 Subject: [PATCH 02/12] fix(date-range-picker): remove unnecessary comment in DateRangePickerPanel --- packages/components/date-picker/DateRangePickerPanel.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/components/date-picker/DateRangePickerPanel.tsx b/packages/components/date-picker/DateRangePickerPanel.tsx index 1d81a22ead..f68dfdb8ad 100644 --- a/packages/components/date-picker/DateRangePickerPanel.tsx +++ b/packages/components/date-picker/DateRangePickerPanel.tsx @@ -218,7 +218,6 @@ export default defineComponent({ function onConfirmClick({ e }: { e: MouseEvent }) { const nextValue = [...(cacheValue.value as string[])]; - // 首次点击不关闭、确保两端都有有效值并且无时间选择器时点击后自动关闭 if (nextValue.length === 2 && isFirstValueSelected.value) { onRawChange?.( formatDate(nextValue, { From 7447fc09c03e3131c4c2906a6bf77e6ef54ee279 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=BF=E8=8F=9C=20Cai?= Date: Thu, 2 Jul 2026 21:51:38 +0800 Subject: [PATCH 03/12] fix(date-picker): trigger popupProps.onVisibleChange on close --- .../date-picker/DateRangePicker.tsx | 13 +- .../date-picker/DateRangePickerPanel.tsx | 3 +- .../__tests__/date-picker.test.tsx | 213 ++++++++++++++++++ .../date-picker/__tests__/useRange.test.tsx | 98 ++++++++ .../components/date-picker/hooks/useRange.tsx | 29 ++- 5 files changed, 341 insertions(+), 15 deletions(-) create mode 100644 packages/components/date-picker/__tests__/useRange.test.tsx diff --git a/packages/components/date-picker/DateRangePicker.tsx b/packages/components/date-picker/DateRangePicker.tsx index d3af494cd0..d893bb8123 100644 --- a/packages/components/date-picker/DateRangePicker.tsx +++ b/packages/components/date-picker/DateRangePicker.tsx @@ -11,7 +11,6 @@ import { DatePickerYearChangeTrigger, DatePickerMonthChangeTrigger, PickerDateRange, - TdDateRangePickerProps, } from './type'; import { RangeInputPopup as TRangeInputPopup } from '../range-input'; @@ -48,6 +47,7 @@ export default defineComponent({ isHoverCell, isFirstValueSelected, onRawChange, + closePopup, } = useRange(props); const isDisabled = useDisabled() as ComputedRef>; @@ -298,10 +298,7 @@ export default defineComponent({ activeIndex.value = nextIndex as 0 | 1; isFirstValueSelected.value = normalizedValue.some((v) => !!v); } else { - // 受控关闭面板时主动通知用户 - const userPopupProps = props.popupProps as TdDateRangePickerProps['popupProps']; - userPopupProps?.onVisibleChange?.(false, { e, trigger: 'trigger-element-close' }); - popupVisible.value = false; + closePopup({ e, trigger: 'trigger-element-close' }); } } @@ -452,7 +449,7 @@ export default defineComponent({ const notValidIndex = getInvalidIndex(nextValue); if (isSingleSideDisabled()) { - popupVisible.value = false; + closePopup({ e, trigger: 'trigger-element-close' }); } else if (!isFirstValueSelected.value) { // 首次点击不关闭、确保两端都有有效值并且无时间选择器时点击后自动关闭 let nextIndex = notValidIndex; @@ -460,7 +457,7 @@ export default defineComponent({ activeIndex.value = nextIndex as 0 | 1; isFirstValueSelected.value = nextValue.some((v) => !!v); } else if (nextValue.length === 2) { - popupVisible.value = false; + closePopup({ e, trigger: 'trigger-element-close' }); } } @@ -491,7 +488,7 @@ export default defineComponent({ inputValue.value = formatDate(presetValue, { format: formatRef.value.format, }); - popupVisible.value = false; + closePopup({ e: context.e, trigger: 'trigger-element-close' }); props.onPresetClick?.(context); } } diff --git a/packages/components/date-picker/DateRangePickerPanel.tsx b/packages/components/date-picker/DateRangePickerPanel.tsx index f68dfdb8ad..1efbbf2321 100644 --- a/packages/components/date-picker/DateRangePickerPanel.tsx +++ b/packages/components/date-picker/DateRangePickerPanel.tsx @@ -105,7 +105,7 @@ export default defineComponent({ // 有时间选择器走 confirm 逻辑 if (props.enableTimePicker) return; - // 首次点击不关闭、确保两端都有有效值并且无时间选择器时点击后自动关闭 + // 首次点击不提交 value,两端都有有效值且无时间选择器时第二次点击触发 onChange if (nextValue.length === 2 && isFirstValueSelected.value) { onRawChange?.( formatDate(nextValue, { @@ -218,6 +218,7 @@ export default defineComponent({ function onConfirmClick({ e }: { e: MouseEvent }) { const nextValue = [...(cacheValue.value as string[])]; + // 首次点击不提交 value,两端都有有效值且无时间选择器时第二次点击触发 onChange if (nextValue.length === 2 && isFirstValueSelected.value) { onRawChange?.( formatDate(nextValue, { diff --git a/packages/components/date-picker/__tests__/date-picker.test.tsx b/packages/components/date-picker/__tests__/date-picker.test.tsx index f8e2daf9fb..54dd1847a5 100644 --- a/packages/components/date-picker/__tests__/date-picker.test.tsx +++ b/packages/components/date-picker/__tests__/date-picker.test.tsx @@ -1017,4 +1017,217 @@ describe('DatePicker', () => { expect(onChange).toHaveBeenCalledTimes(1); expect(onConfirm).toHaveBeenCalledTimes(1); }); + + describe('DateRangePicker: popupProps.onVisibleChange', () => { + const createPopupProps = (attachClass: string, onVisibleChange: ReturnType) => { + const popupProps: Record = { attach: `.${attachClass}` }; + popupProps.onVisibleChange = onVisibleChange; + return popupProps; + }; + + const openRangePickerPopup = async (wrapper: ReturnType) => { + const trigger = wrapper.find('.t-input'); + await trigger.trigger('mousedown'); + await trigger.trigger('mouseup'); + await trigger.trigger('click'); + await nextTick(); + await new Promise((resolve) => setTimeout(resolve, 0)); + }; + + const getDateCells = (wrapper: ReturnType) => { + const cells = Array.from(wrapper.element.querySelectorAll('td.t-date-picker__cell')) as HTMLElement[]; + return cells.length > 0 + ? cells + : (Array.from(document.querySelectorAll('td.t-date-picker__cell')) as HTMLElement[]); + }; + + const findDateCell = (cells: HTMLElement[], day: string) => + cells.find((cell) => cell.textContent?.trim() === day && !cell.className.includes('--additional')); + + const selectRangeByCells = async (wrapper: ReturnType, startDay = '10', endDay = '20') => { + findDateCell(getDateCells(wrapper), startDay)?.click(); + await nextTick(); + await new Promise((resolve) => setTimeout(resolve, 0)); + findDateCell(getDateCells(wrapper), endDay)?.click(); + await nextTick(); + await new Promise((resolve) => setTimeout(resolve, 0)); + }; + + const findConfirmBtn = (wrapper: ReturnType) => + (wrapper.element.querySelector('.t-date-picker__footer button') as HTMLElement | null) || + (Array.from(document.querySelectorAll('button.t-button') as NodeListOf).find((b) => + b.textContent?.trim().includes('确定'), + ) as HTMLElement | null); + + const getVisibleChangeCloseCalls = (onVisibleChange: ReturnType) => + onVisibleChange.mock.calls.filter(([visible]) => visible === false); + + it('calls popupProps.onVisibleChange when second cell is selected without enableTimePicker', async () => { + const onVisibleChange = vi.fn(); + const attachClass = 'drp-visible-change-cell-attach'; + + const wrapper = mount({ + render() { + return ( +
+ +
+ ); + }, + }); + + await openRangePickerPopup(wrapper); + await selectRangeByCells(wrapper); + + expect(getVisibleChangeCloseCalls(onVisibleChange).length).toBeGreaterThan(0); + }); + + it('calls popupProps["on-visible-change"] when second cell is selected without enableTimePicker', async () => { + const onVisibleChange = vi.fn(); + const attachClass = 'drp-visible-change-kebab-attach'; + + const wrapper = mount({ + render() { + return ( +
+ +
+ ); + }, + }); + + await openRangePickerPopup(wrapper); + await selectRangeByCells(wrapper); + + expect(getVisibleChangeCloseCalls(onVisibleChange).length).toBeGreaterThan(0); + }); + + it('calls popupProps.onVisibleChange when popup reports document close', async () => { + const onVisibleChange = vi.fn(); + const attachClass = 'drp-visible-change-document-attach'; + + const wrapper = mount({ + render() { + return ( +
+ +
+ ); + }, + }); + + await openRangePickerPopup(wrapper); + + const popup = wrapper.findComponent({ name: 'TPopup' }); + expect(popup.exists()).toBe(true); + popup.props('onVisibleChange')?.(false, { + trigger: 'document', + e: new MouseEvent('mousedown', { bubbles: true }), + }); + await nextTick(); + + expect( + onVisibleChange.mock.calls.some(([visible, context]) => visible === false && context?.trigger === 'document'), + ).toBe(true); + }); + + it('calls popupProps.onVisibleChange when confirm button closes popup with enableTimePicker', async () => { + const onVisibleChange = vi.fn(); + const attachClass = 'drp-visible-change-confirm-attach'; + const value = ['2020-12-10 08:00:00', '2020-12-20 18:00:00']; + + const wrapper = mount({ + render() { + return ( +
+ +
+ ); + }, + }); + + await openRangePickerPopup(wrapper); + + const confirmBtn = findConfirmBtn(wrapper); + expect(confirmBtn).toBeTruthy(); + confirmBtn?.click(); + await nextTick(); + confirmBtn?.click(); + await nextTick(); + + expect(getVisibleChangeCloseCalls(onVisibleChange).length).toBeGreaterThan(0); + }); + + it('calls popupProps.onVisibleChange when clear button is clicked', async () => { + const onVisibleChange = vi.fn(); + const attachClass = 'drp-visible-change-clear-attach'; + const value = ['2020-12-10', '2020-12-20']; + + const wrapper = mount({ + render() { + return ( +
+ +
+ ); + }, + }); + + await openRangePickerPopup(wrapper); + await wrapper.find('.t-range-input').trigger('mouseenter'); + await nextTick(); + + const clearIcon = wrapper.find('[class*="suffix-clear"]'); + expect(clearIcon.exists()).toBe(true); + await clearIcon.trigger('click'); + await nextTick(); + + expect(getVisibleChangeCloseCalls(onVisibleChange).length).toBeGreaterThan(0); + }); + + it('calls popupProps.onVisibleChange when preset is clicked', async () => { + const onVisibleChange = vi.fn(); + const attachClass = 'drp-visible-change-preset-attach'; + const presets = { lastWeek: ['2020-12-21', '2020-12-27'] as [string, string] }; + + const wrapper = mount({ + render() { + return ( +
+ +
+ ); + }, + }); + + await openRangePickerPopup(wrapper); + + const presetButton = wrapper.findAll('.t-button').find((btn) => btn.text() === 'lastWeek'); + expect(presetButton).toBeTruthy(); + if (presetButton) await presetButton.trigger('click'); + await nextTick(); + + expect(getVisibleChangeCloseCalls(onVisibleChange).length).toBeGreaterThan(0); + }); + }); }); diff --git a/packages/components/date-picker/__tests__/useRange.test.tsx b/packages/components/date-picker/__tests__/useRange.test.tsx new file mode 100644 index 0000000000..b2451ba4b3 --- /dev/null +++ b/packages/components/date-picker/__tests__/useRange.test.tsx @@ -0,0 +1,98 @@ +// @ts-nocheck +import { defineComponent, reactive } from 'vue'; +import { mount } from '@vue/test-utils'; +import { useRange } from '../hooks/useRange'; + +describe('useRange closePopup', () => { + const mountUseRange = (props: Record = {}) => { + let api: ReturnType; + const hookProps = reactive({ + value: [], + format: 'YYYY-MM-DD', + ...props, + }); + + mount( + defineComponent({ + setup() { + api = useRange(hookProps as any); + return () => null; + }, + }), + ); + + return () => api!; + }; + + it('calls popupProps.onVisibleChange when closePopup is invoked', () => { + const onVisibleChange = vi.fn(); + const getApi = mountUseRange({ popupProps: { onVisibleChange } }); + const api = getApi(); + + api.popupVisible.value = true; + api.closePopup({ trigger: 'trigger-element-close' }); + + expect(onVisibleChange).toHaveBeenCalledWith(false, { trigger: 'trigger-element-close' }); + expect(api.popupVisible.value).toBe(false); + }); + + it('calls popupProps["on-visible-change"] when closePopup is invoked', () => { + const onVisibleChange = vi.fn(); + const getApi = mountUseRange({ popupProps: { 'on-visible-change': onVisibleChange } }); + const api = getApi(); + + api.popupVisible.value = true; + api.closePopup({ e: new MouseEvent('click'), trigger: 'trigger-element-close' }); + + expect(onVisibleChange).toHaveBeenCalledWith(false, expect.objectContaining({ trigger: 'trigger-element-close' })); + expect(api.popupVisible.value).toBe(false); + }); + + it('does not call onVisibleChange when popup is already closed', () => { + const onVisibleChange = vi.fn(); + const getApi = mountUseRange({ popupProps: { onVisibleChange } }); + const api = getApi(); + + api.closePopup(); + + expect(onVisibleChange).not.toHaveBeenCalled(); + }); + + it('notifies onVisibleChange when range input clear closes popup', async () => { + const onVisibleChange = vi.fn(); + const onRawChange = vi.fn(); + const getApi = mountUseRange({ + clearable: true, + popupProps: { onVisibleChange }, + onChange: onRawChange, + value: ['2020-12-10', '2020-12-20'], + }); + const api = getApi(); + + api.popupVisible.value = true; + const mouseEvent = new MouseEvent('click'); + await api.rangeInputProps.value.onClear(mouseEvent); + + expect(onVisibleChange).toHaveBeenCalledWith( + false, + expect.objectContaining({ trigger: 'trigger-element-close', e: mouseEvent }), + ); + expect(onRawChange).toHaveBeenCalled(); + }); + + it('notifies onVisibleChange when range input enter closes popup', async () => { + const onVisibleChange = vi.fn(); + const getApi = mountUseRange({ + popupProps: { onVisibleChange }, + value: ['2020-12-10', '2020-12-20'], + format: 'YYYY-MM-DD', + }); + const api = getApi(); + + api.popupVisible.value = true; + await api.rangeInputProps.value.onEnter(['2020-12-10', '2020-12-20']); + + expect(onVisibleChange).toHaveBeenCalledWith(false, expect.objectContaining({ trigger: 'trigger-element-close' })); + expect(api.popupVisible.value).toBe(false); + }); +}); diff --git a/packages/components/date-picker/hooks/useRange.tsx b/packages/components/date-picker/hooks/useRange.tsx index b109cab497..956f9a8621 100644 --- a/packages/components/date-picker/hooks/useRange.tsx +++ b/packages/components/date-picker/hooks/useRange.tsx @@ -5,6 +5,7 @@ import { omit } from 'lodash-es'; import { useConfig, useTNodeJSX, useReadonly, useGlobalIcon, usePrefixClass } from '@tdesign/shared-hooks'; import { TdDateRangePickerProps, DateValue, DateRangePickerPartial } from '../type'; +import type { PopupVisibleChangeContext } from '../../popup/type'; import { isValidDate, formatDate, getDefaultFormat, parseToDayjs } from '@tdesign/common-js/date-picker/format'; import { useRangeValue } from './useRangeValue'; @@ -36,6 +37,21 @@ export function useRange(props: TdDateRangePickerProps) { const inputValue = ref(formatDate(props.value, { format: formatRef.value.format })); // 未真正选中前可能不断变更输入框的内容 const isReadOnly = useReadonly(); + // 通知 popupProps 中的 visible 变化回调 + const notifyPopupVisibleChange = (visible: boolean, context: PopupVisibleChangeContext) => { + if (isReadOnly.value) return; + props.popupProps?.onVisibleChange?.(visible, context); + // @ts-ignore types only declare onVisibleChange,but not declare on-visible-change + props.popupProps?.['on-visible-change']?.(visible, context); + }; + + // 受控关闭面板,并主动触发 onVisibleChange + const closePopup = (context?: PopupVisibleChangeContext) => { + if (!popupVisible.value || isReadOnly.value) return; + notifyPopupVisibleChange(false, context ?? { trigger: 'trigger-element-close' }); + popupVisible.value = false; + }; + // input 设置 const rangeInputProps = computed(() => ({ ...props.rangeInputProps, @@ -63,7 +79,10 @@ export function useRange(props: TdDateRangePickerProps) { if (context instanceof MouseEvent) context.stopPropagation(); else context.e.stopPropagation(); - popupVisible.value = false; + closePopup({ + e: context instanceof MouseEvent ? context : context.e, + trigger: 'trigger-element-close', + }); onRawChange?.([], { dayjsValue: [], trigger: 'clear' }); }, onBlur: (newVal: string[], { e, position }: { e: MouseEvent; position: 'first' | 'second' }) => { @@ -102,7 +121,7 @@ export function useRange(props: TdDateRangePickerProps) { onEnter: (newVal: string[]) => { if (!isValidDate(newVal, formatRef.value.format) && !isValidDate(value.value, formatRef.value.format)) return; - popupVisible.value = false; + closePopup({ trigger: 'trigger-element-close' }); if (isValidDate(newVal, formatRef.value.format)) { onRawChange?.( formatDate(newVal, { @@ -135,10 +154,7 @@ export function useRange(props: TdDateRangePickerProps) { if (isReadOnly.value) return; // 这里劫持了进一步向 popup 传递的 onVisibleChange 事件,为了保证可以在 Datepicker 中使用 popupProps.onVisibleChange,故此处理 - props.popupProps?.onVisibleChange?.(visible, context); - // TODO - // @ts-ignore types only declare onVisibleChange,but not declare on-visible-change - props.popupProps?.['on-visible-change']?.(visible, context); + notifyPopupVisibleChange(visible, context); // 输入框点击不关闭面板 if (context.trigger === 'trigger-element-click') { @@ -203,5 +219,6 @@ export function useRange(props: TdDateRangePickerProps) { isFirstValueSelected, cacheValue, onRawChange, + closePopup, }; } From 2309a019fe9c619275b8534a1fc9943415f1cfbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=BF=E8=8F=9C=20Cai?= Date: Fri, 3 Jul 2026 10:02:25 +0800 Subject: [PATCH 04/12] test(date-picker): enhance useRange tests with improved hook props and error handling --- .../date-picker/__tests__/useRange.test.tsx | 89 ++++++++++++------- 1 file changed, 57 insertions(+), 32 deletions(-) diff --git a/packages/components/date-picker/__tests__/useRange.test.tsx b/packages/components/date-picker/__tests__/useRange.test.tsx index b2451ba4b3..3550e59c67 100644 --- a/packages/components/date-picker/__tests__/useRange.test.tsx +++ b/packages/components/date-picker/__tests__/useRange.test.tsx @@ -1,59 +1,86 @@ -// @ts-nocheck import { defineComponent, reactive } from 'vue'; import { mount } from '@vue/test-utils'; import { useRange } from '../hooks/useRange'; +import type { TdDateRangePickerProps } from '../type'; +import type { PopupVisibleChangeContext } from '../../popup/type'; + +type PopupPropsWithKebabVisibleChange = NonNullable & { + 'on-visible-change'?: (visible: boolean, context: PopupVisibleChangeContext) => void; +}; + +// 测试只用到少量 props,避免 Partial 触发类型递归过深 +type UseRangeTestProps = { + value?: TdDateRangePickerProps['value']; + format?: string; + mode?: TdDateRangePickerProps['mode']; + clearable?: boolean; + popupProps?: PopupPropsWithKebabVisibleChange; + onChange?: TdDateRangePickerProps['onChange']; +}; + +const defaultHookProps: UseRangeTestProps = { + value: [], + format: 'YYYY-MM-DD', + mode: 'date', +}; + +const createHookProps = (override: UseRangeTestProps = {}): TdDateRangePickerProps => + reactive({ + ...defaultHookProps, + ...override, + }) as unknown as TdDateRangePickerProps; describe('useRange closePopup', () => { - const mountUseRange = (props: Record = {}) => { - let api: ReturnType; - const hookProps = reactive({ - value: [], - format: 'YYYY-MM-DD', - ...props, - }); + const createRangeHook = (props: UseRangeTestProps = {}) => { + let rangeHook: ReturnType | undefined; + const hookProps = createHookProps(props); mount( defineComponent({ setup() { - api = useRange(hookProps as any); + rangeHook = useRange(hookProps); return () => null; }, }), ); - return () => api!; + if (!rangeHook) { + throw new Error('useRange hook failed to initialize'); + } + return rangeHook; }; it('calls popupProps.onVisibleChange when closePopup is invoked', () => { const onVisibleChange = vi.fn(); - const getApi = mountUseRange({ popupProps: { onVisibleChange } }); - const api = getApi(); + const rangeHook = createRangeHook({ popupProps: { onVisibleChange } }); - api.popupVisible.value = true; - api.closePopup({ trigger: 'trigger-element-close' }); + rangeHook.popupVisible.value = true; + rangeHook.closePopup({ trigger: 'trigger-element-close' }); expect(onVisibleChange).toHaveBeenCalledWith(false, { trigger: 'trigger-element-close' }); - expect(api.popupVisible.value).toBe(false); + expect(rangeHook.popupVisible.value).toBe(false); }); it('calls popupProps["on-visible-change"] when closePopup is invoked', () => { const onVisibleChange = vi.fn(); - const getApi = mountUseRange({ popupProps: { 'on-visible-change': onVisibleChange } }); - const api = getApi(); + const rangeHook = createRangeHook({ + popupProps: { + 'on-visible-change': onVisibleChange, + }, + }); - api.popupVisible.value = true; - api.closePopup({ e: new MouseEvent('click'), trigger: 'trigger-element-close' }); + rangeHook.popupVisible.value = true; + rangeHook.closePopup({ e: new MouseEvent('click'), trigger: 'trigger-element-close' }); expect(onVisibleChange).toHaveBeenCalledWith(false, expect.objectContaining({ trigger: 'trigger-element-close' })); - expect(api.popupVisible.value).toBe(false); + expect(rangeHook.popupVisible.value).toBe(false); }); it('does not call onVisibleChange when popup is already closed', () => { const onVisibleChange = vi.fn(); - const getApi = mountUseRange({ popupProps: { onVisibleChange } }); - const api = getApi(); + const rangeHook = createRangeHook({ popupProps: { onVisibleChange } }); - api.closePopup(); + rangeHook.closePopup(); expect(onVisibleChange).not.toHaveBeenCalled(); }); @@ -61,17 +88,16 @@ describe('useRange closePopup', () => { it('notifies onVisibleChange when range input clear closes popup', async () => { const onVisibleChange = vi.fn(); const onRawChange = vi.fn(); - const getApi = mountUseRange({ + const rangeHook = createRangeHook({ clearable: true, popupProps: { onVisibleChange }, onChange: onRawChange, value: ['2020-12-10', '2020-12-20'], }); - const api = getApi(); - api.popupVisible.value = true; + rangeHook.popupVisible.value = true; const mouseEvent = new MouseEvent('click'); - await api.rangeInputProps.value.onClear(mouseEvent); + await rangeHook.rangeInputProps.value.onClear(mouseEvent); expect(onVisibleChange).toHaveBeenCalledWith( false, @@ -82,17 +108,16 @@ describe('useRange closePopup', () => { it('notifies onVisibleChange when range input enter closes popup', async () => { const onVisibleChange = vi.fn(); - const getApi = mountUseRange({ + const rangeHook = createRangeHook({ popupProps: { onVisibleChange }, value: ['2020-12-10', '2020-12-20'], format: 'YYYY-MM-DD', }); - const api = getApi(); - api.popupVisible.value = true; - await api.rangeInputProps.value.onEnter(['2020-12-10', '2020-12-20']); + rangeHook.popupVisible.value = true; + await rangeHook.rangeInputProps.value.onEnter(['2020-12-10', '2020-12-20']); expect(onVisibleChange).toHaveBeenCalledWith(false, expect.objectContaining({ trigger: 'trigger-element-close' })); - expect(api.popupVisible.value).toBe(false); + expect(rangeHook.popupVisible.value).toBe(false); }); }); From ae7a7d292614e3b3131f5b6dcaf83f16de3ca2e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=BF=E8=8F=9C=20Cai?= Date: Fri, 3 Jul 2026 14:38:26 +0800 Subject: [PATCH 05/12] test(date-picker): update `createPopupProps` for better type safety --- .../date-picker/__tests__/date-picker.test.tsx | 16 +++++++++++----- .../date-picker/__tests__/useRange.test.tsx | 3 ++- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/components/date-picker/__tests__/date-picker.test.tsx b/packages/components/date-picker/__tests__/date-picker.test.tsx index 54dd1847a5..d3403b4ab1 100644 --- a/packages/components/date-picker/__tests__/date-picker.test.tsx +++ b/packages/components/date-picker/__tests__/date-picker.test.tsx @@ -2,8 +2,12 @@ import { mount } from '@vue/test-utils'; import { nextTick } from 'vue'; import MockDate from 'mockdate'; import DatePicker, { DateRangePicker, DatePickerPanel, DateRangePickerPanel } from '@tdesign/components/date-picker'; +import type { TdDateRangePickerProps } from '../type'; +import type { PopupVisibleChangeContext } from '../../popup/type'; import dayjs from 'dayjs'; +type DateRangePickerPopupProps = NonNullable; + // 固定时间,当使用 new Date() 时,返回固定时间,防止“当前时间”的副作用影响,导致 snapshot 变更,mockdate 插件见 https://github.com/boblauer/MockDate MockDate.set('2020-12-28'); @@ -1019,11 +1023,13 @@ describe('DatePicker', () => { }); describe('DateRangePicker: popupProps.onVisibleChange', () => { - const createPopupProps = (attachClass: string, onVisibleChange: ReturnType) => { - const popupProps: Record = { attach: `.${attachClass}` }; - popupProps.onVisibleChange = onVisibleChange; - return popupProps; - }; + const createPopupProps = ( + attachClass: string, + onVisibleChange: (visible: boolean, context: PopupVisibleChangeContext) => void, + ): DateRangePickerPopupProps => ({ + attach: `.${attachClass}`, + onVisibleChange, + }); const openRangePickerPopup = async (wrapper: ReturnType) => { const trigger = wrapper.find('.t-input'); diff --git a/packages/components/date-picker/__tests__/useRange.test.tsx b/packages/components/date-picker/__tests__/useRange.test.tsx index 3550e59c67..5fd9380c50 100644 --- a/packages/components/date-picker/__tests__/useRange.test.tsx +++ b/packages/components/date-picker/__tests__/useRange.test.tsx @@ -11,6 +11,7 @@ type PopupPropsWithKebabVisibleChange = NonNullable 触发类型递归过深 type UseRangeTestProps = { value?: TdDateRangePickerProps['value']; + defaultValue?: TdDateRangePickerProps['defaultValue']; format?: string; mode?: TdDateRangePickerProps['mode']; clearable?: boolean; @@ -19,7 +20,7 @@ type UseRangeTestProps = { }; const defaultHookProps: UseRangeTestProps = { - value: [], + defaultValue: [], format: 'YYYY-MM-DD', mode: 'date', }; From 5032fccbd04dd475e345f08091ea5abdada99031 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=BF=E8=8F=9C=20Cai?= Date: Tue, 7 Jul 2026 10:17:47 +0800 Subject: [PATCH 06/12] feat(date-picker): extract usePopupVisibleChange hook --- .../components/date-picker/DatePicker.tsx | 9 +-- .../hooks/usePopupVisibleChange.ts | 33 ++++++++++ .../components/date-picker/hooks/useRange.tsx | 32 ++++----- .../date-picker/hooks/useSingle.tsx | 15 +++-- packages/components/popup/popup.en-US.md | 34 +++++----- packages/components/popup/popup.md | 4 +- packages/components/popup/type.ts | 4 +- .../tree-select/tree-select.en-US.md | 66 +++++++++---------- .../components/tree-select/tree-select.md | 10 +-- 9 files changed, 121 insertions(+), 86 deletions(-) create mode 100644 packages/components/date-picker/hooks/usePopupVisibleChange.ts diff --git a/packages/components/date-picker/DatePicker.tsx b/packages/components/date-picker/DatePicker.tsx index dd67e2bfaf..3a012a5861 100644 --- a/packages/components/date-picker/DatePicker.tsx +++ b/packages/components/date-picker/DatePicker.tsx @@ -57,6 +57,7 @@ export default defineComponent({ time, inputRef, onChange, + closePopup, } = useSingle(props); const isDisabled = useDisabled() as ComputedRef; @@ -201,7 +202,7 @@ export default defineComponent({ trigger: 'pick', }, ); - popupVisible.value = false; + closePopup(); } props.onPick?.(date); @@ -263,7 +264,7 @@ export default defineComponent({ function onTagClearClick({ e }: { e: MouseEvent }) { e.stopPropagation(); - popupVisible.value = false; + closePopup({ e, trigger: 'trigger-element-close' }); onChange?.(props.multiple ? [] : '', { dayjsValue: dayjs(), trigger: 'clear' }); props.onClear?.({ e }); } @@ -365,7 +366,7 @@ export default defineComponent({ format: formatRef.value.format, }); } - popupVisible.value = false; + closePopup({ e, trigger: 'trigger-element-close' }); } // 预设 @@ -385,7 +386,7 @@ export default defineComponent({ inputValue.value = formatDate(presetVal, { format: formatRef.value.format, }); - popupVisible.value = false; + closePopup({ e: context.e, trigger: 'trigger-element-close' }); props.onPresetClick?.(context); } diff --git a/packages/components/date-picker/hooks/usePopupVisibleChange.ts b/packages/components/date-picker/hooks/usePopupVisibleChange.ts new file mode 100644 index 0000000000..25ad5e065d --- /dev/null +++ b/packages/components/date-picker/hooks/usePopupVisibleChange.ts @@ -0,0 +1,33 @@ +import type { PopupVisibleChangeContext } from '../../popup/type'; +import type { PopupProps } from '../../popup'; +import { ref } from 'vue'; + +/** + * 统一管理弹窗可见性变更的 Hook + * 解决受控关闭面板时未触发 popupProps.onVisibleChange 的问题 + */ +export function usePopupVisibleChange(popupProps?: PopupProps) { + const popupVisible = ref(false); + + // 通知 popupProps 中的 visible 变化回调 + // 兼容 kebab-case 写法 + const notifyPopupVisibleChange = (visible: boolean, context?: PopupVisibleChangeContext) => { + popupProps?.onVisibleChange?.(visible, context); + // @ts-ignore types only declare onVisibleChange,but not declare on-visible-change + popupProps?.['on-visible-change']?.(visible, context); + }; + + // 受控关闭面板,并主动触发 onVisibleChange + // 程序化关闭统一标记 trigger: 'trigger-element-close' + const closePopup = (context?: PopupVisibleChangeContext) => { + if (!popupVisible.value) return; + notifyPopupVisibleChange(false, context ?? { trigger: 'trigger-element-close' }); + popupVisible.value = false; + }; + + return { + popupVisible, + notifyPopupVisibleChange, + closePopup, + }; +} diff --git a/packages/components/date-picker/hooks/useRange.tsx b/packages/components/date-picker/hooks/useRange.tsx index 956f9a8621..c681965791 100644 --- a/packages/components/date-picker/hooks/useRange.tsx +++ b/packages/components/date-picker/hooks/useRange.tsx @@ -5,9 +5,10 @@ import { omit } from 'lodash-es'; import { useConfig, useTNodeJSX, useReadonly, useGlobalIcon, usePrefixClass } from '@tdesign/shared-hooks'; import { TdDateRangePickerProps, DateValue, DateRangePickerPartial } from '../type'; -import type { PopupVisibleChangeContext } from '../../popup/type'; import { isValidDate, formatDate, getDefaultFormat, parseToDayjs } from '@tdesign/common-js/date-picker/format'; import { useRangeValue } from './useRangeValue'; +import { usePopupVisibleChange } from './usePopupVisibleChange'; +import type { PopupVisibleChangeContext } from '../../popup/type'; export const PARTIAL_MAP: Record<'first' | 'second', DateRangePickerPartial> = { first: 'start', second: 'end' }; @@ -31,25 +32,24 @@ export function useRange(props: TdDateRangePickerProps) { }), ); - const popupVisible = ref(false); const isHoverCell = ref(false); const activeIndex = ref<0 | 1>(0); // 确定当前选中的输入框序号 const inputValue = ref(formatDate(props.value, { format: formatRef.value.format })); // 未真正选中前可能不断变更输入框的内容 const isReadOnly = useReadonly(); - // 通知 popupProps 中的 visible 变化回调 - const notifyPopupVisibleChange = (visible: boolean, context: PopupVisibleChangeContext) => { + // 使用公共 Hook 管理弹窗可见性 + const { popupVisible, notifyPopupVisibleChange, closePopup } = usePopupVisibleChange(props.popupProps); + + // 包装 closePopup,增加只读判断 + const closePopupWithCheck = (context?: PopupVisibleChangeContext) => { if (isReadOnly.value) return; - props.popupProps?.onVisibleChange?.(visible, context); - // @ts-ignore types only declare onVisibleChange,but not declare on-visible-change - props.popupProps?.['on-visible-change']?.(visible, context); + closePopup(context); }; - // 受控关闭面板,并主动触发 onVisibleChange - const closePopup = (context?: PopupVisibleChangeContext) => { - if (!popupVisible.value || isReadOnly.value) return; - notifyPopupVisibleChange(false, context ?? { trigger: 'trigger-element-close' }); - popupVisible.value = false; + // 包装 notifyPopupVisibleChange,增加只读判断 + const notifyPopupVisibleChangeWithCheck = (visible: boolean, context: PopupVisibleChangeContext) => { + if (isReadOnly.value) return; + notifyPopupVisibleChange(visible, context); }; // input 设置 @@ -79,7 +79,7 @@ export function useRange(props: TdDateRangePickerProps) { if (context instanceof MouseEvent) context.stopPropagation(); else context.e.stopPropagation(); - closePopup({ + closePopupWithCheck({ e: context instanceof MouseEvent ? context : context.e, trigger: 'trigger-element-close', }); @@ -121,7 +121,7 @@ export function useRange(props: TdDateRangePickerProps) { onEnter: (newVal: string[]) => { if (!isValidDate(newVal, formatRef.value.format) && !isValidDate(value.value, formatRef.value.format)) return; - closePopup({ trigger: 'trigger-element-close' }); + closePopupWithCheck({ trigger: 'trigger-element-close' }); if (isValidDate(newVal, formatRef.value.format)) { onRawChange?.( formatDate(newVal, { @@ -154,7 +154,7 @@ export function useRange(props: TdDateRangePickerProps) { if (isReadOnly.value) return; // 这里劫持了进一步向 popup 传递的 onVisibleChange 事件,为了保证可以在 Datepicker 中使用 popupProps.onVisibleChange,故此处理 - notifyPopupVisibleChange(visible, context); + notifyPopupVisibleChangeWithCheck(visible, context); // 输入框点击不关闭面板 if (context.trigger === 'trigger-element-click') { @@ -219,6 +219,6 @@ export function useRange(props: TdDateRangePickerProps) { isFirstValueSelected, cacheValue, onRawChange, - closePopup, + closePopup: closePopupWithCheck, }; } diff --git a/packages/components/date-picker/hooks/useSingle.tsx b/packages/components/date-picker/hooks/useSingle.tsx index 763e717b9d..4aae148645 100644 --- a/packages/components/date-picker/hooks/useSingle.tsx +++ b/packages/components/date-picker/hooks/useSingle.tsx @@ -13,6 +13,7 @@ import { parseToDayjs, } from '@tdesign/common-js/date-picker/format'; import { useSingleValue } from './useSingleValue'; +import { usePopupVisibleChange } from './usePopupVisibleChange'; export function useSingle(props: TdDatePickerProps) { const COMPONENT_NAME = usePrefixClass('date-picker'); @@ -32,7 +33,6 @@ export function useSingle(props: TdDatePickerProps) { }), ); - const popupVisible = ref(false); const isHoverCell = ref(false); // 未真正选中前可能不断变更输入框的内容 const inputValue = ref( @@ -41,6 +41,9 @@ export function useSingle(props: TdDatePickerProps) { : formatDate(value.value, { format: formatRef.value.format }), ); + // 使用公共 Hook 管理弹窗可见性 + const { popupVisible, notifyPopupVisibleChange, closePopup } = usePopupVisibleChange(props.popupProps); + // input 设置 const inputProps = computed(() => { const defaultInputProps = { @@ -88,13 +91,13 @@ export function useSingle(props: TdDatePickerProps) { onEnter: (val: string) => { if (!val) { onChange('', { dayjsValue: dayjs(), trigger: 'enter' }); - popupVisible.value = false; + closePopup({ trigger: 'trigger-element-close' }); return; } if (!isValidDate(val, formatRef.value.format) && !isValidDate(value.value, formatRef.value.format)) return; - popupVisible.value = false; + closePopup({ trigger: 'trigger-element-close' }); if (isValidDate(val, formatRef.value.format)) { onChange?.( formatDate(val, { @@ -127,10 +130,7 @@ export function useSingle(props: TdDatePickerProps) { onVisibleChange: (visible: boolean, context: any) => { if (disabled.value) return; // 这里劫持了进一步向 popup 传递的 onVisibleChange 事件,为了保证可以在 Datepicker 中使用 popupProps.onVisibleChange,故此处理 - props.popupProps?.onVisibleChange?.(visible, context); - // TODO - // @ts-ignore types only declare onVisibleChange,but not declare on-visible-change - props.popupProps?.['on-visible-change']?.(visible, context); + notifyPopupVisibleChange(visible, context); // 输入框点击不关闭面板 if (context.trigger === 'trigger-element-click') { popupVisible.value = true; @@ -165,5 +165,6 @@ export function useSingle(props: TdDatePickerProps) { cacheValue, isHoverCell, onChange, + closePopup, }; } diff --git a/packages/components/popup/popup.en-US.md b/packages/components/popup/popup.en-US.md index 4065cb0de3..7dadab9665 100644 --- a/packages/components/popup/popup.en-US.md +++ b/packages/components/popup/popup.en-US.md @@ -6,29 +6,29 @@ name | type | default | description | required -- | -- | -- | -- | -- -attach | String / Function | 'body' | Typescript:`AttachNode`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N -content | String / Slot / Function | - | Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N -default | String / Slot / Function | - | Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N -delay | Number / Array | - | delay to show or hide popover。Typescript:`number \| Array` | N +attach | String / Function | 'body' | Typescript: `AttachNode`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N +content | String / Slot / Function | - | Typescript: `string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N +default | String / Slot / Function | - | Typescript: `string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N +delay | Number / Array | - | delay to show or hide popover。Typescript: `number \| Array` | N destroyOnClose | Boolean | false | \- | N disabled | Boolean | - | \- | N hideEmptyPopup | Boolean | false | \- | N -overlayClassName | String / Object / Array | - | Typescript:`ClassName`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N -overlayInnerClassName | String / Object / Array | - | Typescript:`ClassName`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N -overlayInnerStyle | Boolean / Object / Function | - | Typescript:`Styles \| ((triggerElement: HTMLElement, popupElement: HTMLElement) => Styles)`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N -overlayStyle | Boolean / Object / Function | - | Typescript:`Styles \| ((triggerElement: HTMLElement, popupElement: HTMLElement) => Styles)`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N -placement | String | top | Typescript:`PopupPlacement` `type PopupPlacement = 'top'\|'left'\|'right'\|'bottom'\|'top-left'\|'top-right'\|'bottom-left'\|'bottom-right'\|'left-top'\|'left-bottom'\|'right-top'\|'right-bottom'`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/popup/type.ts) | N +overlayClassName | String / Object / Array | - | Typescript: `ClassName`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N +overlayInnerClassName | String / Object / Array | - | Typescript: `ClassName`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N +overlayInnerStyle | Boolean / Object / Function | - | Typescript: `Styles \| ((triggerElement: HTMLElement, popupElement: HTMLElement) => Styles)`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N +overlayStyle | Boolean / Object / Function | - | Typescript: `Styles \| ((triggerElement: HTMLElement, popupElement: HTMLElement) => Styles)`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N +placement | String | top | Typescript: `PopupPlacement` `type PopupPlacement = 'top'\|'left'\|'right'\|'bottom'\|'top-left'\|'top-right'\|'bottom-left'\|'bottom-right'\|'left-top'\|'left-bottom'\|'right-top'\|'right-bottom'`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/popup/type.ts) | N popperOptions | Object | - | popper initial options,details refer to https://popper.js.org/docs | N showArrow | Boolean | false | \- | N trigger | String | hover | options: hover/click/focus/mousedown/context-menu | N -triggerElement | String / Slot / Function | - | Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N -visible | Boolean | - | `v-model` and `v-model:visible` is supported。Typescript:`boolean` | N -defaultVisible | Boolean | - | uncontrolled property。Typescript:`boolean` | N +triggerElement | String / Slot / Function | - | Typescript: `string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N +visible | Boolean | - | `v-model` and `v-model:visible` is supported。Typescript: `boolean` | N +defaultVisible | Boolean | - | uncontrolled property。Typescript: `boolean` | N zIndex | Number | - | \- | N -onOverlayClick | Function | | Typescript:`(context: { e: MouseEvent }) => void`
trigger on popup content click | N -onScroll | Function | | Typescript:`(context: { e: WheelEvent }) => void`
| N -onScrollToBottom | Function | | Typescript:`(context: { e: WheelEvent }) => void`
| N -onVisibleChange | Function | | Typescript:`(visible: boolean, context: PopupVisibleChangeContext) => void`
[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/popup/type.ts)。
`interface PopupVisibleChangeContext { e?: PopupTriggerEvent; trigger?: PopupTriggerSource }`

`type PopupTriggerEvent = MouseEvent \| FocusEvent \| KeyboardEvent`

`type PopupTriggerSource = 'document' \| 'trigger-element-click' \| 'trigger-element-hover' \| 'trigger-element-blur' \| 'trigger-element-focus' \| 'trigger-element-mousedown' \| 'trigger-element-close' \| 'context-menu' \| 'keydown-esc'`
| N +onOverlayClick | Function | | Typescript: `(context: { e: MouseEvent }) => void`
trigger on popup content click | N +onScroll | Function | | Typescript: `(context: { e: WheelEvent }) => void`
| N +onScrollToBottom | Function | | Typescript: `(context: { e: WheelEvent }) => void`
| N +onVisibleChange | Function | | Typescript: `(visible: boolean, context: PopupVisibleChangeContext) => void`
[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/popup/type.ts)。
`interface PopupVisibleChangeContext { e?: PopupTriggerEvent; trigger?: PopupTriggerSource }`

`type PopupTriggerEvent = Event`

`type PopupTriggerSource = 'document' \| 'trigger-element-click' \| 'trigger-element-hover' \| 'trigger-element-blur' \| 'trigger-element-focus' \| 'trigger-element-mousedown' \| 'trigger-element-close' \| 'context-menu' \| 'keydown-esc'`
| N ### Popup Events @@ -37,7 +37,7 @@ name | params | description overlay-click | `(context: { e: MouseEvent })` | trigger on popup content click scroll | `(context: { e: WheelEvent })` | \- scroll-to-bottom | `(context: { e: WheelEvent })` | \- -visible-change | `(visible: boolean, context: PopupVisibleChangeContext)` | [see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/popup/type.ts)。
`interface PopupVisibleChangeContext { e?: PopupTriggerEvent; trigger?: PopupTriggerSource }`

`type PopupTriggerEvent = MouseEvent \| FocusEvent \| KeyboardEvent`

`type PopupTriggerSource = 'document' \| 'trigger-element-click' \| 'trigger-element-hover' \| 'trigger-element-blur' \| 'trigger-element-focus' \| 'trigger-element-mousedown' \| 'trigger-element-close' \| 'context-menu' \| 'keydown-esc'`
+visible-change | `(visible: boolean, context: PopupVisibleChangeContext)` | [see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/popup/type.ts)。
`interface PopupVisibleChangeContext { e?: PopupTriggerEvent; trigger?: PopupTriggerSource }`

`type PopupTriggerEvent = Event`

`type PopupTriggerSource = 'document' \| 'trigger-element-click' \| 'trigger-element-hover' \| 'trigger-element-blur' \| 'trigger-element-focus' \| 'trigger-element-mousedown' \| 'trigger-element-close' \| 'context-menu' \| 'keydown-esc'`
### PopupInstanceFunctions 组件实例方法 diff --git a/packages/components/popup/popup.md b/packages/components/popup/popup.md index f8247b46c1..3ae53f0728 100644 --- a/packages/components/popup/popup.md +++ b/packages/components/popup/popup.md @@ -64,7 +64,7 @@ zIndex | Number | - | 组件层级,Web 侧样式默认为 5500,移动端和 onOverlayClick | Function | | TS 类型:`(context: { e: MouseEvent }) => void`
内容面板点击时触发 | N onScroll | Function | | TS 类型:`(context: { e: WheelEvent }) => void`
下拉选项滚动事件 | N onScrollToBottom | Function | | TS 类型:`(context: { e: WheelEvent }) => void`
下拉滚动触底事件,常用于滚动到底执行具体业务逻辑 | N -onVisibleChange | Function | | TS 类型:`(visible: boolean, context: PopupVisibleChangeContext) => void`
当浮层隐藏或显示时触发,`trigger=document` 表示点击非浮层元素触发;`trigger=context-menu` 表示右击触发。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/popup/type.ts)。
`interface PopupVisibleChangeContext { e?: PopupTriggerEvent; trigger?: PopupTriggerSource }`

`type PopupTriggerEvent = MouseEvent \| FocusEvent \| KeyboardEvent`

`type PopupTriggerSource = 'document' \| 'trigger-element-click' \| 'trigger-element-hover' \| 'trigger-element-blur' \| 'trigger-element-focus' \| 'trigger-element-mousedown' \| 'trigger-element-close' \| 'context-menu' \| 'keydown-esc'`
| N +onVisibleChange | Function | | TS 类型:`(visible: boolean, context: PopupVisibleChangeContext) => void`
当浮层隐藏或显示时触发,`trigger=document` 表示点击非浮层元素触发;`trigger=context-menu` 表示右击触发。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/popup/type.ts)。
`interface PopupVisibleChangeContext { e?: PopupTriggerEvent; trigger?: PopupTriggerSource }`

`type PopupTriggerEvent = Event`

`type PopupTriggerSource = 'document' \| 'trigger-element-click' \| 'trigger-element-hover' \| 'trigger-element-blur' \| 'trigger-element-focus' \| 'trigger-element-mousedown' \| 'trigger-element-close' \| 'context-menu' \| 'keydown-esc'`
| N ### Popup Events @@ -73,7 +73,7 @@ onVisibleChange | Function | | TS 类型:`(visible: boolean, context: PopupVi overlay-click | `(context: { e: MouseEvent })` | 内容面板点击时触发 scroll | `(context: { e: WheelEvent })` | 下拉选项滚动事件 scroll-to-bottom | `(context: { e: WheelEvent })` | 下拉滚动触底事件,常用于滚动到底执行具体业务逻辑 -visible-change | `(visible: boolean, context: PopupVisibleChangeContext)` | 当浮层隐藏或显示时触发,`trigger=document` 表示点击非浮层元素触发;`trigger=context-menu` 表示右击触发。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/popup/type.ts)。
`interface PopupVisibleChangeContext { e?: PopupTriggerEvent; trigger?: PopupTriggerSource }`

`type PopupTriggerEvent = MouseEvent \| FocusEvent \| KeyboardEvent`

`type PopupTriggerSource = 'document' \| 'trigger-element-click' \| 'trigger-element-hover' \| 'trigger-element-blur' \| 'trigger-element-focus' \| 'trigger-element-mousedown' \| 'trigger-element-close' \| 'context-menu' \| 'keydown-esc'`
+visible-change | `(visible: boolean, context: PopupVisibleChangeContext)` | 当浮层隐藏或显示时触发,`trigger=document` 表示点击非浮层元素触发;`trigger=context-menu` 表示右击触发。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/popup/type.ts)。
`interface PopupVisibleChangeContext { e?: PopupTriggerEvent; trigger?: PopupTriggerSource }`

`type PopupTriggerEvent = Event`

`type PopupTriggerSource = 'document' \| 'trigger-element-click' \| 'trigger-element-hover' \| 'trigger-element-blur' \| 'trigger-element-focus' \| 'trigger-element-mousedown' \| 'trigger-element-close' \| 'context-menu' \| 'keydown-esc'`
### PopupInstanceFunctions 组件实例方法 diff --git a/packages/components/popup/type.ts b/packages/components/popup/type.ts index 5d48cdd6bc..f83d7382d1 100644 --- a/packages/components/popup/type.ts +++ b/packages/components/popup/type.ts @@ -5,7 +5,7 @@ * */ import { Instance } from '@popperjs/core'; -import { TNode, ClassName, Styles, AttachNode } from '../common'; +import type { TNode, ClassName, Styles, AttachNode } from '../common'; export interface TdPopupProps { /** @@ -151,7 +151,7 @@ export interface PopupVisibleChangeContext { trigger?: PopupTriggerSource; } -export type PopupTriggerEvent = MouseEvent | FocusEvent | KeyboardEvent; +export type PopupTriggerEvent = Event; export type PopupTriggerSource = | 'document' diff --git a/packages/components/tree-select/tree-select.en-US.md b/packages/components/tree-select/tree-select.en-US.md index d7a7dfcf40..b56bcf3d89 100644 --- a/packages/components/tree-select/tree-select.en-US.md +++ b/packages/components/tree-select/tree-select.en-US.md @@ -10,52 +10,52 @@ autoWidth | Boolean | false | \- | N autofocus | Boolean | false | \- | N borderless | Boolean | false | \- | N clearable | Boolean | false | \- | N -collapsedItems | Slot / Function | - | Typescript:`TNode<{ value: DataOption[]; collapsedSelectedItems: DataOption[]; count: number; onClose: (context: { index: number, e?: MouseEvent }) => void }>`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N -data | Array | [] | Typescript:`Array` | N +collapsedItems | Slot / Function | - | Typescript: `TNode<{ value: DataOption[]; collapsedSelectedItems: DataOption[]; count: number; onClose: (context: { index: number, e?: MouseEvent }) => void }>`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N +data | Array | [] | Typescript: `Array` | N disabled | Boolean | undefined | \- | N -empty | String / Slot / Function | - | Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N -filter | Function | - | Typescript:`(filterWords: string, option: DataOption) => boolean` | N +empty | String / Slot / Function | - | Typescript: `string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N +filter | Function | - | Typescript: `(filterWords: string, option: DataOption) => boolean` | N filterable | Boolean | false | \- | N -inputProps | Object | - | Typescript:`InputProps`,[Input API Documents](./input?tab=api)。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/tree-select/type.ts) | N -inputValue | String / Number | - | input value。`v-model:inputValue` is supported。Typescript:`string`,[Input API Documents](./input?tab=api)。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/tree-select/type.ts) | N -defaultInputValue | String / Number | - | input value。uncontrolled property。Typescript:`string`,[Input API Documents](./input?tab=api)。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/tree-select/type.ts) | N -keys | Object | - | alias filed name in data。Typescript:`TreeKeysType`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N -label | String / Slot / Function | - | Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N +inputProps | Object | - | Typescript: `InputProps`,[Input API Documents](./input?tab=api)。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/tree-select/type.ts) | N +inputValue | String / Number | - | input value。`v-model:inputValue` is supported。Typescript: `string` | N +defaultInputValue | String / Number | - | input value。uncontrolled property。Typescript: `string` | N +keys | Object | - | alias filed name in data。Typescript: `TreeKeysType`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N +label | String / Slot / Function | - | Typescript: `string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N loading | Boolean | false | \- | N -loadingText | String / Slot / Function | - | Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N +loadingText | String / Slot / Function | - | Typescript: `string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N max | Number | 0 | \- | N minCollapsedNum | Number | 0 | \- | N multiple | Boolean | false | \- | N -panelBottomContent | String / Slot / Function | - | Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N -panelTopContent | String / Slot / Function | - | Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N +panelBottomContent | String / Slot / Function | - | Typescript: `string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N +panelTopContent | String / Slot / Function | - | Typescript: `string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N placeholder | String | undefined | \- | N -popupProps | Object | - | Typescript:`PopupProps`,[Popup API Documents](./popup?tab=api)。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/tree-select/type.ts) | N +popupProps | Object | - | Typescript: `PopupProps`,[Popup API Documents](./popup?tab=api)。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/tree-select/type.ts) | N popupVisible | Boolean | undefined | show popup。`v-model:popupVisible` is supported | N defaultPopupVisible | Boolean | undefined | show popup。uncontrolled property | N -prefixIcon | Slot / Function | - | Typescript:`TNode`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N +prefixIcon | Slot / Function | - | Typescript: `TNode`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N readonly | Boolean | undefined | \- | N reserveKeyword | Boolean | false | \- | N -selectInputProps | Object | - | Typescript:`SelectInputProps`,[SelectInput API Documents](./select-input?tab=api)。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/tree-select/type.ts) | N +selectInputProps | Object | - | Typescript: `SelectInputProps`,[SelectInput API Documents](./select-input?tab=api)。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/tree-select/type.ts) | N size | String | medium | options: small/medium/large | N status | String | default | options: default/success/warning/error | N -suffix | String / Slot / Function | - | Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N -suffixIcon | Slot / Function | - | Typescript:`TNode`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N -tagProps | Object | - | Typescript:`TagProps`,[Tag API Documents](./tag?tab=api)。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/tree-select/type.ts) | N -tips | String / Slot / Function | - | Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N -treeProps | Object | - | Typescript:`TreeProps`,[Tree API Documents](./tree?tab=api)。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts)。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/tree-select/type.ts) | N -value | String / Number / Object / Array | - | `v-model` and `v-model:value` is supported。Typescript:`TreeValueType` `type TreeSelectValue = string \| number \| TreeOptionData \| Array`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts)。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/tree-select/type.ts) | N -defaultValue | String / Number / Object / Array | - | uncontrolled property。Typescript:`TreeValueType` `type TreeSelectValue = string \| number \| TreeOptionData \| Array`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts)。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/tree-select/type.ts) | N -valueDisplay | Slot / Function | - | Typescript:`string \| TNode<{ value: TreeOptionData \| TreeOptionData[]; onClose: (index: number) => void }>`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N +suffix | String / Slot / Function | - | Typescript: `string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N +suffixIcon | Slot / Function | - | Typescript: `TNode`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N +tagProps | Object | - | Typescript: `TagProps`,[Tag API Documents](./tag?tab=api)。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/tree-select/type.ts) | N +tips | String / Slot / Function | - | Typescript: `string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N +treeProps | Object | - | Typescript: `TreeProps`,[Tree API Documents](./tree?tab=api)。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts)。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/tree-select/type.ts) | N +value | String / Number / Object / Array | - | `v-model` and `v-model:value` is supported。Typescript: `TreeValueType` `type TreeSelectValue = string \| number \| TreeOptionData \| Array`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts)。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/tree-select/type.ts) | N +defaultValue | String / Number / Object / Array | - | uncontrolled property。Typescript: `TreeValueType` `type TreeSelectValue = string \| number \| TreeOptionData \| Array`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts)。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/tree-select/type.ts) | N +valueDisplay | Slot / Function | - | Typescript: `string \| TNode<{ value: TreeOptionData \| TreeOptionData[]; onClose: (index: number) => void }>`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N valueType | String | value | options: value/object | N -onBlur | Function | | Typescript:`(context: SelectInputBlurContext & { value: TreeSelectValue }) => void`
| N -onChange | Function | | Typescript:`(value: TreeValueType, context: TreeSelectChangeContext) => void`
[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/tree-select/type.ts)。
`interface TreeSelectChangeContext { node: TreeNodeModel; data: DataOption; index?: number; trigger: TreeSelectValueChangeTrigger; e?: MouseEvent \| KeyboardEvent \| Event }`

`type TreeSelectValueChangeTrigger = 'clear' \| 'tag-remove' \| 'backspace' \| 'check' \| 'uncheck'`
| N -onClear | Function | | Typescript:`(context: { e: MouseEvent }) => void`
| N -onEnter | Function | | Typescript:`(context: { inputValue: string; e: KeyboardEvent; value: TreeValueType }) => void`
| N -onFocus | Function | | Typescript:`(context: { value: TreeSelectValue; e: FocusEvent }) => void`
| N -onInputChange | Function | | Typescript:`(value: string, context: SelectInputValueChangeContext) => void`
| N -onPopupVisibleChange | Function | | Typescript:`(visible: boolean, context: TreeSelectPopupVisibleContext) => void`
[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/tree-select/type.ts)。
`import { PopupVisibleChangeContext, PopupTriggerEvent, PopupTriggerSource } from '@Popup'`

`interface TreeSelectPopupVisibleContext { e?: PopupTriggerEvent \| Event; node?: TreeNodeModel; trigger?: PopupTriggerSource \| 'clear'; }`
| N -onRemove | Function | | Typescript:`(options: RemoveOptions) => void`
[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/tree-select/type.ts)。
`export interface RemoveOptions { value: N; data: T; index: number; node: TreeNodeModel; e?: MouseEvent \| KeyboardEvent; trigger: 'tag-remove' \| 'backspace'; }`
| N -onSearch | Function | | Typescript:`(filterWords: string, context: { e: KeyboardEvent \| SelectInputValueChangeContext['e'] }) => void`
| N +onBlur | Function | | Typescript: `(context: SelectInputBlurContext & { value: TreeSelectValue }) => void`
| N +onChange | Function | | Typescript: `(value: TreeValueType, context: TreeSelectChangeContext) => void`
[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/tree-select/type.ts)。
`interface TreeSelectChangeContext { node: TreeNodeModel; data: DataOption; index?: number; trigger: TreeSelectValueChangeTrigger; e?: MouseEvent \| KeyboardEvent \| Event }`

`type TreeSelectValueChangeTrigger = 'clear' \| 'tag-remove' \| 'backspace' \| 'check' \| 'uncheck'`
| N +onClear | Function | | Typescript: `(context: { e: MouseEvent }) => void`
| N +onEnter | Function | | Typescript: `(context: { inputValue: string; e: KeyboardEvent; value: TreeValueType }) => void`
| N +onFocus | Function | | Typescript: `(context: { value: TreeSelectValue; e: FocusEvent }) => void`
| N +onInputChange | Function | | Typescript: `(value: string, context: SelectInputValueChangeContext) => void`
| N +onPopupVisibleChange | Function | | Typescript: `(visible: boolean, context: TreeSelectPopupVisibleContext) => void`
[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/tree-select/type.ts)。
`import { PopupVisibleChangeContext, PopupTriggerEvent, PopupTriggerSource } from '@Popup'`

`interface TreeSelectPopupVisibleContext { e?: PopupTriggerEvent; node?: TreeNodeModel; trigger?: PopupTriggerSource \| 'clear'; }`
| N +onRemove | Function | | Typescript: `(options: RemoveOptions) => void`
[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/tree-select/type.ts)。
`export interface RemoveOptions { value: N; data: T; index: number; node: TreeNodeModel; e?: MouseEvent \| KeyboardEvent; trigger: 'tag-remove' \| 'backspace'; }`
| N +onSearch | Function | | Typescript: `(filterWords: string, context: { e: KeyboardEvent \| SelectInputValueChangeContext['e'] }) => void`
| N ### TreeSelect Events @@ -67,6 +67,6 @@ clear | `(context: { e: MouseEvent })` | \- enter | `(context: { inputValue: string; e: KeyboardEvent; value: TreeValueType })` | \- focus | `(context: { value: TreeSelectValue; e: FocusEvent })` | \- input-change | `(value: string, context: SelectInputValueChangeContext)` | \- -popup-visible-change | `(visible: boolean, context: TreeSelectPopupVisibleContext)` | [see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/tree-select/type.ts)。
`import { PopupVisibleChangeContext, PopupTriggerEvent, PopupTriggerSource } from '@Popup'`

`interface TreeSelectPopupVisibleContext { e?: PopupTriggerEvent \| Event; node?: TreeNodeModel; trigger?: PopupTriggerSource \| 'clear'; }`
+popup-visible-change | `(visible: boolean, context: TreeSelectPopupVisibleContext)` | [see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/tree-select/type.ts)。
`import { PopupVisibleChangeContext, PopupTriggerEvent, PopupTriggerSource } from '@Popup'`

`interface TreeSelectPopupVisibleContext { e?: PopupTriggerEvent; node?: TreeNodeModel; trigger?: PopupTriggerSource \| 'clear'; }`
remove | `(options: RemoveOptions)` | [see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/tree-select/type.ts)。
`export interface RemoveOptions { value: N; data: T; index: number; node: TreeNodeModel; e?: MouseEvent \| KeyboardEvent; trigger: 'tag-remove' \| 'backspace'; }`
search | `(filterWords: string, context: { e: KeyboardEvent \| SelectInputValueChangeContext['e'] })` | \- diff --git a/packages/components/tree-select/tree-select.md b/packages/components/tree-select/tree-select.md index 9a61c92a8e..7a2b9c46d4 100644 --- a/packages/components/tree-select/tree-select.md +++ b/packages/components/tree-select/tree-select.md @@ -17,8 +17,8 @@ empty | String / Slot / Function | - | 当下拉列表为空时显示的内容 filter | Function | - | 过滤方法,用于对现有数据进行搜索过滤,判断是否过滤某一项数据。TS 类型:`(filterWords: string, option: DataOption) => boolean` | N filterable | Boolean | false | 是否可搜索 | N inputProps | Object | - | 透传给 输入框 Input 组件的全部属性。TS 类型:`InputProps`,[Input API Documents](./input?tab=api)。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/tree-select/type.ts) | N -inputValue | String / Number | - | 输入框的值。支持语法糖 `v-model:inputValue`。TS 类型:`string`,[Input API Documents](./input?tab=api)。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/tree-select/type.ts) | N -defaultInputValue | String / Number | - | 输入框的值。非受控属性。TS 类型:`string`,[Input API Documents](./input?tab=api)。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/tree-select/type.ts) | N +inputValue | String / Number | - | 输入框的值。支持语法糖 `v-model:inputValue`。TS 类型:`string` | N +defaultInputValue | String / Number | - | 输入框的值。非受控属性。TS 类型:`string` | N keys | Object | - | 用来定义 `value / label / disabled / children` 在 `data` 数据中对应的字段别名,示例:`{ value: 'key', label: 'name', children: 'list' }`。TS 类型:`TreeKeysType`。[通用类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N label | String / Slot / Function | - | 左侧文本。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N loading | Boolean | false | 是否正在加载数据 | N @@ -29,7 +29,7 @@ multiple | Boolean | false | 是否允许多选 | N panelBottomContent | String / Slot / Function | - | 面板内的底部内容。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N panelTopContent | String / Slot / Function | - | 面板内的顶部内容。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N placeholder | String | undefined | 占位符 | N -popupProps | Object | - | 透传给 popup 组件的全部属性。TS 类型:`PopupProps`,[Popup API Documents](./popup?tab=api)。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/tree-select/type.ts) | N +popupProps | Object | - | 透传 Popup 组件全部属性。TS 类型:`PopupProps`,[Popup API Documents](./popup?tab=api)。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/tree-select/type.ts) | N popupVisible | Boolean | undefined | 是否显示下拉框。支持语法糖 `v-model:popupVisible` | N defaultPopupVisible | Boolean | undefined | 是否显示下拉框。非受控属性 | N prefixIcon | Slot / Function | - | 组件前置图标。TS 类型:`TNode`。[通用类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N @@ -53,7 +53,7 @@ onClear | Function | | TS 类型:`(context: { e: MouseEvent }) => void`
onEnter | Function | | TS 类型:`(context: { inputValue: string; e: KeyboardEvent; value: TreeValueType }) => void`
回车键按下时触发。`inputValue` 表示输入框的值,`value` 表示选中值。泛型 `TreeValueType` 继承 `TreeSelectValue` | N onFocus | Function | | TS 类型:`(context: { value: TreeSelectValue; e: FocusEvent }) => void`
输入框获得焦点时触发 | N onInputChange | Function | | TS 类型:`(value: string, context: SelectInputValueChangeContext) => void`
输入框值发生变化时触发,`context.trigger` 表示触发输入框值变化的来源:文本输入触发、清除按钮触发、失去焦点等 | N -onPopupVisibleChange | Function | | TS 类型:`(visible: boolean, context: TreeSelectPopupVisibleContext) => void`
下拉框显示或隐藏时触发。单选场景,选中某个选项时触发关闭,此时需要添加参数 `node`。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/tree-select/type.ts)。
`import { PopupVisibleChangeContext, PopupTriggerEvent, PopupTriggerSource } from '@Popup'`

`interface TreeSelectPopupVisibleContext { e?: PopupTriggerEvent \| Event; node?: TreeNodeModel; trigger?: PopupTriggerSource \| 'clear'; }`
| N +onPopupVisibleChange | Function | | TS 类型:`(visible: boolean, context: TreeSelectPopupVisibleContext) => void`
下拉框显示或隐藏时触发。单选场景,选中某个选项时触发关闭,此时需要添加参数 `node`。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/tree-select/type.ts)。
`import { PopupVisibleChangeContext, PopupTriggerEvent, PopupTriggerSource } from '@Popup'`

`interface TreeSelectPopupVisibleContext { e?: PopupTriggerEvent; node?: TreeNodeModel; trigger?: PopupTriggerSource \| 'clear'; }`
| N onRemove | Function | | TS 类型:`(options: RemoveOptions) => void`
多选模式下,选中数据被移除时触发。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/tree-select/type.ts)。
`export interface RemoveOptions { value: N; data: T; index: number; node: TreeNodeModel; e?: MouseEvent \| KeyboardEvent; trigger: 'tag-remove' \| 'backspace'; }`
| N onSearch | Function | | TS 类型:`(filterWords: string, context: { e: KeyboardEvent \| SelectInputValueChangeContext['e'] }) => void`
输入值变化时,触发搜索事件。主要用于远程搜索新数据。设置 `filterable=true` 开启此功能。优先级高于本地数据搜索 `filter`,即一旦存在这个远程搜索事件 `filter` 失效 | N @@ -67,6 +67,6 @@ clear | `(context: { e: MouseEvent })` | 点击清除按钮时触发 enter | `(context: { inputValue: string; e: KeyboardEvent; value: TreeValueType })` | 回车键按下时触发。`inputValue` 表示输入框的值,`value` 表示选中值。泛型 `TreeValueType` 继承 `TreeSelectValue` focus | `(context: { value: TreeSelectValue; e: FocusEvent })` | 输入框获得焦点时触发 input-change | `(value: string, context: SelectInputValueChangeContext)` | 输入框值发生变化时触发,`context.trigger` 表示触发输入框值变化的来源:文本输入触发、清除按钮触发、失去焦点等 -popup-visible-change | `(visible: boolean, context: TreeSelectPopupVisibleContext)` | 下拉框显示或隐藏时触发。单选场景,选中某个选项时触发关闭,此时需要添加参数 `node`。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/tree-select/type.ts)。
`import { PopupVisibleChangeContext, PopupTriggerEvent, PopupTriggerSource } from '@Popup'`

`interface TreeSelectPopupVisibleContext { e?: PopupTriggerEvent \| Event; node?: TreeNodeModel; trigger?: PopupTriggerSource \| 'clear'; }`
+popup-visible-change | `(visible: boolean, context: TreeSelectPopupVisibleContext)` | 下拉框显示或隐藏时触发。单选场景,选中某个选项时触发关闭,此时需要添加参数 `node`。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/tree-select/type.ts)。
`import { PopupVisibleChangeContext, PopupTriggerEvent, PopupTriggerSource } from '@Popup'`

`interface TreeSelectPopupVisibleContext { e?: PopupTriggerEvent; node?: TreeNodeModel; trigger?: PopupTriggerSource \| 'clear'; }`
remove | `(options: RemoveOptions)` | 多选模式下,选中数据被移除时触发。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/tree-select/type.ts)。
`export interface RemoveOptions { value: N; data: T; index: number; node: TreeNodeModel; e?: MouseEvent \| KeyboardEvent; trigger: 'tag-remove' \| 'backspace'; }`
search | `(filterWords: string, context: { e: KeyboardEvent \| SelectInputValueChangeContext['e'] })` | 输入值变化时,触发搜索事件。主要用于远程搜索新数据。设置 `filterable=true` 开启此功能。优先级高于本地数据搜索 `filter`,即一旦存在这个远程搜索事件 `filter` 失效 From 2d65e8e493afe2bda8c0fb7b8ac94b6a9f9ebe99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=BF=E8=8F=9C=20Cai?= Date: Tue, 7 Jul 2026 10:48:04 +0800 Subject: [PATCH 07/12] test: add test --- .../__tests__/date-picker.test.tsx | 153 ++++++++++++++++++ .../date-picker/__tests__/useSingle.test.tsx | 101 ++++++++++++ 2 files changed, 254 insertions(+) create mode 100644 packages/components/date-picker/__tests__/useSingle.test.tsx diff --git a/packages/components/date-picker/__tests__/date-picker.test.tsx b/packages/components/date-picker/__tests__/date-picker.test.tsx index d3403b4ab1..3f905146c2 100644 --- a/packages/components/date-picker/__tests__/date-picker.test.tsx +++ b/packages/components/date-picker/__tests__/date-picker.test.tsx @@ -1236,4 +1236,157 @@ describe('DatePicker', () => { expect(getVisibleChangeCloseCalls(onVisibleChange).length).toBeGreaterThan(0); }); }); + + describe('DatePicker: popupProps.onVisibleChange', () => { + const createPopupProps = ( + attachClass: string, + onVisibleChange: (visible: boolean, context: PopupVisibleChangeContext) => void, + ): DateRangePickerPopupProps => ({ + attach: `.${attachClass}`, + onVisibleChange, + }); + + const openDatePickerPopup = async (wrapper: ReturnType) => { + const trigger = wrapper.find('.t-input'); + await trigger.trigger('mousedown'); + await trigger.trigger('mouseup'); + await trigger.trigger('click'); + await nextTick(); + await new Promise((resolve) => setTimeout(resolve, 0)); + }; + + const getDateCells = (wrapper: ReturnType) => { + const cells = Array.from(wrapper.element.querySelectorAll('td.t-date-picker__cell')) as HTMLElement[]; + return cells.length > 0 + ? cells + : (Array.from(document.querySelectorAll('td.t-date-picker__cell')) as HTMLElement[]); + }; + + const findDateCell = (cells: HTMLElement[], day: string) => + cells.find((cell) => cell.textContent?.trim() === day && !cell.className.includes('--additional')); + + const getVisibleChangeCloseCalls = (onVisibleChange: ReturnType) => + onVisibleChange.mock.calls.filter(([visible]) => visible === false); + + it('calls popupProps.onVisibleChange when cell is selected', async () => { + const onVisibleChange = vi.fn(); + const attachClass = 'dp-visible-change-cell-attach'; + + const wrapper = mount({ + render() { + return ( +
+ +
+ ); + }, + }); + + await openDatePickerPopup(wrapper); + + const cells = getDateCells(wrapper); + const targetCell = findDateCell(cells, '15'); + expect(targetCell).toBeTruthy(); + if (targetCell) targetCell.click(); + await nextTick(); + + expect(getVisibleChangeCloseCalls(onVisibleChange).length).toBeGreaterThan(0); + }); + + it('calls popupProps.onVisibleChange when clear button is clicked', async () => { + const onVisibleChange = vi.fn(); + const attachClass = 'dp-visible-change-clear-attach'; + const value = '2020-12-10'; + + const wrapper = mount({ + render() { + return ( +
+ +
+ ); + }, + }); + + await openDatePickerPopup(wrapper); + await wrapper.find('.t-input').trigger('mouseenter'); + await nextTick(); + + const clearIcon = wrapper.find('.t-input-clear'); + expect(clearIcon.exists()).toBe(true); + await clearIcon.trigger('click'); + await nextTick(); + + expect(getVisibleChangeCloseCalls(onVisibleChange).length).toBeGreaterThan(0); + }); + + it('calls popupProps.onVisibleChange when preset is clicked', async () => { + const onVisibleChange = vi.fn(); + const attachClass = 'dp-visible-change-preset-attach'; + const presets = { today: '2020-12-28' }; + + const wrapper = mount({ + render() { + return ( +
+ +
+ ); + }, + }); + + await openDatePickerPopup(wrapper); + + const presetButton = wrapper.findAll('.t-button').find((btn) => btn.text() === 'today'); + expect(presetButton).toBeTruthy(); + if (presetButton) await presetButton.trigger('click'); + await nextTick(); + + expect(getVisibleChangeCloseCalls(onVisibleChange).length).toBeGreaterThan(0); + }); + + it('calls popupProps.onVisibleChange when confirm button is clicked', async () => { + const onVisibleChange = vi.fn(); + const attachClass = 'dp-visible-change-confirm-attach'; + const value = '2020-12-10'; + + const wrapper = mount({ + render() { + return ( +
+ +
+ ); + }, + }); + + await openDatePickerPopup(wrapper); + + const findConfirmBtn = () => + (wrapper.element.querySelector('.t-date-picker__footer button') as HTMLElement | null) || + (Array.from(document.querySelectorAll('button.t-button') as NodeListOf).find((b) => + b.textContent?.trim().includes('确定'), + ) as HTMLElement | null); + + const confirmBtn = findConfirmBtn(); + expect(confirmBtn).toBeTruthy(); + confirmBtn?.click(); + await nextTick(); + + expect(getVisibleChangeCloseCalls(onVisibleChange).length).toBeGreaterThan(0); + }); + }); }); diff --git a/packages/components/date-picker/__tests__/useSingle.test.tsx b/packages/components/date-picker/__tests__/useSingle.test.tsx new file mode 100644 index 0000000000..c8a004d260 --- /dev/null +++ b/packages/components/date-picker/__tests__/useSingle.test.tsx @@ -0,0 +1,101 @@ +import { defineComponent, reactive } from 'vue'; +import { mount } from '@vue/test-utils'; +import { useSingle } from '../hooks/useSingle'; +import type { TdDatePickerProps } from '../type'; +import type { PopupVisibleChangeContext } from '../../popup/type'; + +type PopupPropsWithKebabVisibleChange = NonNullable & { + 'on-visible-change'?: (visible: boolean, context: PopupVisibleChangeContext) => void; +}; + +// 测试只用到少量 props,避免 Partial 触发类型递归过深 +type UseSingleTestProps = { + value?: TdDatePickerProps['value']; + defaultValue?: TdDatePickerProps['defaultValue']; + format?: string; + clearable?: boolean; + popupProps?: PopupPropsWithKebabVisibleChange; + onChange?: TdDatePickerProps['onChange']; +}; + +const defaultHookProps: UseSingleTestProps = { + defaultValue: '', + format: 'YYYY-MM-DD', +}; + +const createHookProps = (override: UseSingleTestProps = {}): TdDatePickerProps => + reactive({ + ...defaultHookProps, + ...override, + }) as unknown as TdDatePickerProps; + +describe('useSingle closePopup', () => { + const createSingleHook = (props: UseSingleTestProps = {}) => { + let singleHook: ReturnType | undefined; + const hookProps = createHookProps(props); + + mount( + defineComponent({ + setup() { + singleHook = useSingle(hookProps); + return () => null; + }, + }), + ); + + if (!singleHook) { + throw new Error('useSingle hook failed to initialize'); + } + return singleHook; + }; + + it('calls popupProps.onVisibleChange when closePopup is invoked', () => { + const onVisibleChange = vi.fn(); + const singleHook = createSingleHook({ popupProps: { onVisibleChange } }); + + singleHook.popupVisible.value = true; + singleHook.closePopup({ trigger: 'trigger-element-close' }); + + expect(onVisibleChange).toHaveBeenCalledWith(false, { trigger: 'trigger-element-close' }); + expect(singleHook.popupVisible.value).toBe(false); + }); + + it('calls popupProps["on-visible-change"] when closePopup is invoked', () => { + const onVisibleChange = vi.fn(); + const singleHook = createSingleHook({ + popupProps: { + 'on-visible-change': onVisibleChange, + }, + }); + + singleHook.popupVisible.value = true; + singleHook.closePopup({ e: new MouseEvent('click'), trigger: 'trigger-element-close' }); + + expect(onVisibleChange).toHaveBeenCalledWith(false, expect.objectContaining({ trigger: 'trigger-element-close' })); + expect(singleHook.popupVisible.value).toBe(false); + }); + + it('does not call onVisibleChange when popup is already closed', () => { + const onVisibleChange = vi.fn(); + const singleHook = createSingleHook({ popupProps: { onVisibleChange } }); + + singleHook.closePopup(); + + expect(onVisibleChange).not.toHaveBeenCalled(); + }); + + it('notifies onVisibleChange when inputProps onEnter closes popup', async () => { + const onVisibleChange = vi.fn(); + const singleHook = createSingleHook({ + popupProps: { onVisibleChange }, + value: '2020-12-10', + format: 'YYYY-MM-DD', + }); + + singleHook.popupVisible.value = true; + await singleHook.inputProps.value.onEnter('2020-12-15'); + + expect(onVisibleChange).toHaveBeenCalledWith(false, expect.objectContaining({ trigger: 'trigger-element-close' })); + expect(singleHook.popupVisible.value).toBe(false); + }); +}); From 3c696bfbd99a86e7ee0b2d6ac6806b68bc748315 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=BF=E8=8F=9C=20Cai?= Date: Tue, 7 Jul 2026 11:14:37 +0800 Subject: [PATCH 08/12] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E9=80=89?= =?UTF-8?q?=E6=8B=A9=E5=99=A8=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/components/date-picker/__tests__/date-picker.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/date-picker/__tests__/date-picker.test.tsx b/packages/components/date-picker/__tests__/date-picker.test.tsx index 3f905146c2..2d1378f0b4 100644 --- a/packages/components/date-picker/__tests__/date-picker.test.tsx +++ b/packages/components/date-picker/__tests__/date-picker.test.tsx @@ -1317,7 +1317,7 @@ describe('DatePicker', () => { await wrapper.find('.t-input').trigger('mouseenter'); await nextTick(); - const clearIcon = wrapper.find('.t-input-clear'); + const clearIcon = wrapper.find('[class*="suffix-clear"]'); expect(clearIcon.exists()).toBe(true); await clearIcon.trigger('click'); await nextTick(); From f2589195c1bf60afca8c21aa0bfc18280dd4fdff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=BF=E8=8F=9C=20Cai?= Date: Tue, 7 Jul 2026 11:39:06 +0800 Subject: [PATCH 09/12] =?UTF-8?q?test:=20=E4=BF=AE=E5=A4=8D=E5=85=B3?= =?UTF-8?q?=E9=97=AD=E6=B5=8B=E8=AF=95=E6=96=B9=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../date-picker/__tests__/useSingle.test.tsx | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/components/date-picker/__tests__/useSingle.test.tsx b/packages/components/date-picker/__tests__/useSingle.test.tsx index c8a004d260..6a1460a9b9 100644 --- a/packages/components/date-picker/__tests__/useSingle.test.tsx +++ b/packages/components/date-picker/__tests__/useSingle.test.tsx @@ -84,18 +84,21 @@ describe('useSingle closePopup', () => { expect(onVisibleChange).not.toHaveBeenCalled(); }); - it('notifies onVisibleChange when inputProps onEnter closes popup', async () => { + it('notifies onVisibleChange with event when closePopup is called with context', () => { const onVisibleChange = vi.fn(); - const singleHook = createSingleHook({ - popupProps: { onVisibleChange }, - value: '2020-12-10', - format: 'YYYY-MM-DD', - }); + const singleHook = createSingleHook({ popupProps: { onVisibleChange } }); singleHook.popupVisible.value = true; - await singleHook.inputProps.value.onEnter('2020-12-15'); - - expect(onVisibleChange).toHaveBeenCalledWith(false, expect.objectContaining({ trigger: 'trigger-element-close' })); + const mockEvent = new MouseEvent('click'); + singleHook.closePopup({ e: mockEvent, trigger: 'trigger-element-close' }); + + expect(onVisibleChange).toHaveBeenCalledWith( + false, + expect.objectContaining({ + trigger: 'trigger-element-close', + e: mockEvent, + }), + ); expect(singleHook.popupVisible.value).toBe(false); }); }); From 69ad3c7776b698bc339f1e05e0825ff76ac787a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=BF=E8=8F=9C=20Cai?= Date: Tue, 7 Jul 2026 15:27:50 +0800 Subject: [PATCH 10/12] refactor(popup): narrow PopupTriggerEvent type from Event to specific event types --- packages/components/popup/popup.en-US.md | 4 ++-- packages/components/popup/popup.md | 4 ++-- packages/components/popup/type.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/components/popup/popup.en-US.md b/packages/components/popup/popup.en-US.md index 7dadab9665..0b745412e8 100644 --- a/packages/components/popup/popup.en-US.md +++ b/packages/components/popup/popup.en-US.md @@ -28,7 +28,7 @@ zIndex | Number | - | \- | N onOverlayClick | Function | | Typescript: `(context: { e: MouseEvent }) => void`
trigger on popup content click | N onScroll | Function | | Typescript: `(context: { e: WheelEvent }) => void`
| N onScrollToBottom | Function | | Typescript: `(context: { e: WheelEvent }) => void`
| N -onVisibleChange | Function | | Typescript: `(visible: boolean, context: PopupVisibleChangeContext) => void`
[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/popup/type.ts)。
`interface PopupVisibleChangeContext { e?: PopupTriggerEvent; trigger?: PopupTriggerSource }`

`type PopupTriggerEvent = Event`

`type PopupTriggerSource = 'document' \| 'trigger-element-click' \| 'trigger-element-hover' \| 'trigger-element-blur' \| 'trigger-element-focus' \| 'trigger-element-mousedown' \| 'trigger-element-close' \| 'context-menu' \| 'keydown-esc'`
| N +onVisibleChange | Function | | Typescript: `(visible: boolean, context: PopupVisibleChangeContext) => void`
[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/popup/type.ts)。
`interface PopupVisibleChangeContext { e?: PopupTriggerEvent; trigger?: PopupTriggerSource }`

`type PopupTriggerEvent = MouseEvent \| FocusEvent \| KeyboardEvent`

`type PopupTriggerSource = 'document' \| 'trigger-element-click' \| 'trigger-element-hover' \| 'trigger-element-blur' \| 'trigger-element-focus' \| 'trigger-element-mousedown' \| 'trigger-element-close' \| 'context-menu' \| 'keydown-esc'`
| N ### Popup Events @@ -37,7 +37,7 @@ name | params | description overlay-click | `(context: { e: MouseEvent })` | trigger on popup content click scroll | `(context: { e: WheelEvent })` | \- scroll-to-bottom | `(context: { e: WheelEvent })` | \- -visible-change | `(visible: boolean, context: PopupVisibleChangeContext)` | [see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/popup/type.ts)。
`interface PopupVisibleChangeContext { e?: PopupTriggerEvent; trigger?: PopupTriggerSource }`

`type PopupTriggerEvent = Event`

`type PopupTriggerSource = 'document' \| 'trigger-element-click' \| 'trigger-element-hover' \| 'trigger-element-blur' \| 'trigger-element-focus' \| 'trigger-element-mousedown' \| 'trigger-element-close' \| 'context-menu' \| 'keydown-esc'`
+visible-change | `(visible: boolean, context: PopupVisibleChangeContext)` | [see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/popup/type.ts)。
`interface PopupVisibleChangeContext { e?: PopupTriggerEvent; trigger?: PopupTriggerSource }`

`type PopupTriggerEvent = MouseEvent \| FocusEvent \| KeyboardEvent`

`type PopupTriggerSource = 'document' \| 'trigger-element-click' \| 'trigger-element-hover' \| 'trigger-element-blur' \| 'trigger-element-focus' \| 'trigger-element-mousedown' \| 'trigger-element-close' \| 'context-menu' \| 'keydown-esc'`
### PopupInstanceFunctions 组件实例方法 diff --git a/packages/components/popup/popup.md b/packages/components/popup/popup.md index 3ae53f0728..f8247b46c1 100644 --- a/packages/components/popup/popup.md +++ b/packages/components/popup/popup.md @@ -64,7 +64,7 @@ zIndex | Number | - | 组件层级,Web 侧样式默认为 5500,移动端和 onOverlayClick | Function | | TS 类型:`(context: { e: MouseEvent }) => void`
内容面板点击时触发 | N onScroll | Function | | TS 类型:`(context: { e: WheelEvent }) => void`
下拉选项滚动事件 | N onScrollToBottom | Function | | TS 类型:`(context: { e: WheelEvent }) => void`
下拉滚动触底事件,常用于滚动到底执行具体业务逻辑 | N -onVisibleChange | Function | | TS 类型:`(visible: boolean, context: PopupVisibleChangeContext) => void`
当浮层隐藏或显示时触发,`trigger=document` 表示点击非浮层元素触发;`trigger=context-menu` 表示右击触发。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/popup/type.ts)。
`interface PopupVisibleChangeContext { e?: PopupTriggerEvent; trigger?: PopupTriggerSource }`

`type PopupTriggerEvent = Event`

`type PopupTriggerSource = 'document' \| 'trigger-element-click' \| 'trigger-element-hover' \| 'trigger-element-blur' \| 'trigger-element-focus' \| 'trigger-element-mousedown' \| 'trigger-element-close' \| 'context-menu' \| 'keydown-esc'`
| N +onVisibleChange | Function | | TS 类型:`(visible: boolean, context: PopupVisibleChangeContext) => void`
当浮层隐藏或显示时触发,`trigger=document` 表示点击非浮层元素触发;`trigger=context-menu` 表示右击触发。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/popup/type.ts)。
`interface PopupVisibleChangeContext { e?: PopupTriggerEvent; trigger?: PopupTriggerSource }`

`type PopupTriggerEvent = MouseEvent \| FocusEvent \| KeyboardEvent`

`type PopupTriggerSource = 'document' \| 'trigger-element-click' \| 'trigger-element-hover' \| 'trigger-element-blur' \| 'trigger-element-focus' \| 'trigger-element-mousedown' \| 'trigger-element-close' \| 'context-menu' \| 'keydown-esc'`
| N ### Popup Events @@ -73,7 +73,7 @@ onVisibleChange | Function | | TS 类型:`(visible: boolean, context: PopupVi overlay-click | `(context: { e: MouseEvent })` | 内容面板点击时触发 scroll | `(context: { e: WheelEvent })` | 下拉选项滚动事件 scroll-to-bottom | `(context: { e: WheelEvent })` | 下拉滚动触底事件,常用于滚动到底执行具体业务逻辑 -visible-change | `(visible: boolean, context: PopupVisibleChangeContext)` | 当浮层隐藏或显示时触发,`trigger=document` 表示点击非浮层元素触发;`trigger=context-menu` 表示右击触发。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/popup/type.ts)。
`interface PopupVisibleChangeContext { e?: PopupTriggerEvent; trigger?: PopupTriggerSource }`

`type PopupTriggerEvent = Event`

`type PopupTriggerSource = 'document' \| 'trigger-element-click' \| 'trigger-element-hover' \| 'trigger-element-blur' \| 'trigger-element-focus' \| 'trigger-element-mousedown' \| 'trigger-element-close' \| 'context-menu' \| 'keydown-esc'`
+visible-change | `(visible: boolean, context: PopupVisibleChangeContext)` | 当浮层隐藏或显示时触发,`trigger=document` 表示点击非浮层元素触发;`trigger=context-menu` 表示右击触发。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/popup/type.ts)。
`interface PopupVisibleChangeContext { e?: PopupTriggerEvent; trigger?: PopupTriggerSource }`

`type PopupTriggerEvent = MouseEvent \| FocusEvent \| KeyboardEvent`

`type PopupTriggerSource = 'document' \| 'trigger-element-click' \| 'trigger-element-hover' \| 'trigger-element-blur' \| 'trigger-element-focus' \| 'trigger-element-mousedown' \| 'trigger-element-close' \| 'context-menu' \| 'keydown-esc'`
### PopupInstanceFunctions 组件实例方法 diff --git a/packages/components/popup/type.ts b/packages/components/popup/type.ts index f83d7382d1..83606655a2 100644 --- a/packages/components/popup/type.ts +++ b/packages/components/popup/type.ts @@ -151,7 +151,7 @@ export interface PopupVisibleChangeContext { trigger?: PopupTriggerSource; } -export type PopupTriggerEvent = Event; +export type PopupTriggerEvent = MouseEvent | FocusEvent | KeyboardEvent; export type PopupTriggerSource = | 'document' From 073d01e91f28476d75079d2fe0fed5e2db27081b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=BF=E8=8F=9C=20Cai?= Date: Wed, 8 Jul 2026 03:05:22 +0800 Subject: [PATCH 11/12] fix(date-picker): pass MouseEvent to `closePopup` and update tests --- packages/components/date-picker/DatePicker.tsx | 4 ++-- .../date-picker/__tests__/date-picker.test.tsx | 12 +++++++++++- .../date-picker/hooks/usePopupVisibleChange.ts | 3 +-- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/components/date-picker/DatePicker.tsx b/packages/components/date-picker/DatePicker.tsx index 3a012a5861..3ae73cd99d 100644 --- a/packages/components/date-picker/DatePicker.tsx +++ b/packages/components/date-picker/DatePicker.tsx @@ -170,7 +170,7 @@ export default defineComponent({ } // 日期点击 - function onCellClick(date: Date) { + function onCellClick(date: Date, { e }: { e: MouseEvent }) { isHoverCell.value = false; // date 模式自动切换年月 if (props.mode === 'date') { @@ -202,7 +202,7 @@ export default defineComponent({ trigger: 'pick', }, ); - closePopup(); + closePopup({ e }); } props.onPick?.(date); diff --git a/packages/components/date-picker/__tests__/date-picker.test.tsx b/packages/components/date-picker/__tests__/date-picker.test.tsx index 2d1378f0b4..33a4ea7533 100644 --- a/packages/components/date-picker/__tests__/date-picker.test.tsx +++ b/packages/components/date-picker/__tests__/date-picker.test.tsx @@ -1290,7 +1290,17 @@ describe('DatePicker', () => { if (targetCell) targetCell.click(); await nextTick(); - expect(getVisibleChangeCloseCalls(onVisibleChange).length).toBeGreaterThan(0); + expect(getVisibleChangeCloseCalls(onVisibleChange)).toEqual( + expect.arrayContaining([ + [ + false, + expect.objectContaining({ + trigger: 'trigger-element-close', + e: expect.any(MouseEvent), + }), + ], + ]), + ); }); it('calls popupProps.onVisibleChange when clear button is clicked', async () => { diff --git a/packages/components/date-picker/hooks/usePopupVisibleChange.ts b/packages/components/date-picker/hooks/usePopupVisibleChange.ts index 25ad5e065d..7abd2987cd 100644 --- a/packages/components/date-picker/hooks/usePopupVisibleChange.ts +++ b/packages/components/date-picker/hooks/usePopupVisibleChange.ts @@ -20,8 +20,7 @@ export function usePopupVisibleChange(popupProps?: PopupProps) { // 受控关闭面板,并主动触发 onVisibleChange // 程序化关闭统一标记 trigger: 'trigger-element-close' const closePopup = (context?: PopupVisibleChangeContext) => { - if (!popupVisible.value) return; - notifyPopupVisibleChange(false, context ?? { trigger: 'trigger-element-close' }); + notifyPopupVisibleChange(false, { trigger: 'trigger-element-close', ...context }); popupVisible.value = false; }; From dceeae99f78b05a232b85d794ee72a0f15e9d50a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=BF=E8=8F=9C=20Cai?= Date: Wed, 8 Jul 2026 04:28:46 +0800 Subject: [PATCH 12/12] fix(date-picker): enhance `onVisibleChange` handling in hooks --- .../date-picker/__tests__/date-picker.test.tsx | 5 +++-- .../date-picker/hooks/usePopupVisibleChange.ts | 11 ++++++++--- packages/components/date-picker/hooks/useRange.tsx | 3 +++ packages/components/date-picker/hooks/useSingle.tsx | 3 +++ packages/components/popup/popup.tsx | 4 +++- 5 files changed, 20 insertions(+), 6 deletions(-) diff --git a/packages/components/date-picker/__tests__/date-picker.test.tsx b/packages/components/date-picker/__tests__/date-picker.test.tsx index 33a4ea7533..d4a2b9af30 100644 --- a/packages/components/date-picker/__tests__/date-picker.test.tsx +++ b/packages/components/date-picker/__tests__/date-picker.test.tsx @@ -1367,7 +1367,7 @@ describe('DatePicker', () => { it('calls popupProps.onVisibleChange when confirm button is clicked', async () => { const onVisibleChange = vi.fn(); const attachClass = 'dp-visible-change-confirm-attach'; - const value = '2020-12-10'; + const value = '2020-12-10 08:00:00'; const wrapper = mount({ render() { @@ -1375,7 +1375,8 @@ describe('DatePicker', () => {
diff --git a/packages/components/date-picker/hooks/usePopupVisibleChange.ts b/packages/components/date-picker/hooks/usePopupVisibleChange.ts index 7abd2987cd..49d0ce28df 100644 --- a/packages/components/date-picker/hooks/usePopupVisibleChange.ts +++ b/packages/components/date-picker/hooks/usePopupVisibleChange.ts @@ -2,6 +2,10 @@ import type { PopupVisibleChangeContext } from '../../popup/type'; import type { PopupProps } from '../../popup'; import { ref } from 'vue'; +type KebabPopupProps = PopupProps & { + 'on-visible-change'?: PopupProps['onVisibleChange']; +}; + /** * 统一管理弹窗可见性变更的 Hook * 解决受控关闭面板时未触发 popupProps.onVisibleChange 的问题 @@ -12,14 +16,15 @@ export function usePopupVisibleChange(popupProps?: PopupProps) { // 通知 popupProps 中的 visible 变化回调 // 兼容 kebab-case 写法 const notifyPopupVisibleChange = (visible: boolean, context?: PopupVisibleChangeContext) => { - popupProps?.onVisibleChange?.(visible, context); - // @ts-ignore types only declare onVisibleChange,but not declare on-visible-change - popupProps?.['on-visible-change']?.(visible, context); + const kebabHandler = (popupProps as KebabPopupProps | undefined)?.['on-visible-change']; + const handler = popupProps?.onVisibleChange ?? kebabHandler; + handler?.(visible, context); }; // 受控关闭面板,并主动触发 onVisibleChange // 程序化关闭统一标记 trigger: 'trigger-element-close' const closePopup = (context?: PopupVisibleChangeContext) => { + if (!popupVisible.value) return; notifyPopupVisibleChange(false, { trigger: 'trigger-element-close', ...context }); popupVisible.value = false; }; diff --git a/packages/components/date-picker/hooks/useRange.tsx b/packages/components/date-picker/hooks/useRange.tsx index c681965791..c4e508f653 100644 --- a/packages/components/date-picker/hooks/useRange.tsx +++ b/packages/components/date-picker/hooks/useRange.tsx @@ -152,6 +152,9 @@ export function useRange(props: TdDateRangePickerProps) { overlayClassName: [props.popupProps?.overlayClassName, `${COMPONENT_NAME.value}__panel-container`], onVisibleChange: (visible: boolean, context: any) => { if (isReadOnly.value) return; + if (!visible && context.trigger === 'trigger-element-close' && !context.e) { + return; + } // 这里劫持了进一步向 popup 传递的 onVisibleChange 事件,为了保证可以在 Datepicker 中使用 popupProps.onVisibleChange,故此处理 notifyPopupVisibleChangeWithCheck(visible, context); diff --git a/packages/components/date-picker/hooks/useSingle.tsx b/packages/components/date-picker/hooks/useSingle.tsx index 4aae148645..7194209997 100644 --- a/packages/components/date-picker/hooks/useSingle.tsx +++ b/packages/components/date-picker/hooks/useSingle.tsx @@ -129,6 +129,9 @@ export function useSingle(props: TdDatePickerProps) { overlayClassName: [props.popupProps?.overlayClassName, `${COMPONENT_NAME.value}__panel-container`], onVisibleChange: (visible: boolean, context: any) => { if (disabled.value) return; + if (!visible && context.trigger === 'trigger-element-close' && !context.e) { + return; + } // 这里劫持了进一步向 popup 传递的 onVisibleChange 事件,为了保证可以在 Datepicker 中使用 popupProps.onVisibleChange,故此处理 notifyPopupVisibleChange(visible, context); // 输入框点击不关闭面板 diff --git a/packages/components/popup/popup.tsx b/packages/components/popup/popup.tsx index e81b5663d8..ded695c598 100644 --- a/packages/components/popup/popup.tsx +++ b/packages/components/popup/popup.tsx @@ -363,7 +363,9 @@ export default defineComponent({ popper.state.elements.reference = triggerEl.value; popper.update(); } else { - setVisible(false, { trigger: getTriggerType({ type: 'mouseenter' } as MouseEvent) }); + // getTriggerType maps real event types to popup trigger sources. Hidden trigger cleanup has no + // user event, so avoid faking mouseenter and report it as a trigger-element close instead. + setVisible(false, { trigger: getTriggerType() }); } } if (props.showArrow) {