Skip to content

Commit aafbf98

Browse files
RSS1102uyarntdesign-bot
authored
fix(menu): fix the issue where submenus do not collapse correctly after the parent menu is closed (#6121)
* fix(Submenu): add popupVisible to TdSubMenuInterface and update provide logic * fix(Submenu): refactor imports and clean up unused code * fix(Submenu): improve popup visibility handling and add current popup check * fix(Submenu): refactor popup visibility logic and improve timer handling * fix(Submenu): remove popupVisible from TdSubMenuInterface * fix(Submenu): optimize popup visibility handling and clean up timers * fix(Submenu): enhance popup closing logic to prevent premature closure * fix(Submenu): add cancelHideTimer method to TdSubMenuInterface for better timer management * fix(Submenu): remove redundant loopInPopup function to simplify popup logic * fix(husky): remove unnecessary check for terminal input * chore: import order * fix(Submenu): refactor timer management to use clearTimers function and add cleanup on unmount * chore: stash changelog [ci skip] --------- Co-authored-by: wū yāng <[email protected]> Co-authored-by: tdesign-bot <[email protected]>
1 parent 765cba1 commit aafbf98

File tree

3 files changed

+96
-16
lines changed

3 files changed

+96
-16
lines changed

packages/components/menu/submenu.tsx

Lines changed: 89 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
ref,
66
provide,
77
onMounted,
8+
onBeforeUnmount,
89
getCurrentInstance,
910
watch,
1011
Slots,
@@ -13,13 +14,15 @@ import {
1314
nextTick,
1415
Transition,
1516
} from 'vue';
17+
import { isFunction } from 'lodash-es';
18+
import { useRipple, useContent, useTNodeJSX, usePrefixClass, useCollapseAnimation } from '@tdesign/shared-hooks';
19+
1620
import props from './submenu-props';
1721
import { TdMenuInterface, TdSubMenuInterface, TdMenuItem } from './types';
1822
import FakeArrow from '../common-components/fake-arrow';
19-
import { useRipple, useContent, useTNodeJSX, usePrefixClass, useCollapseAnimation } from '@tdesign/shared-hooks';
2023

2124
import { Popup, PopupPlacement } from '../popup';
22-
import { isFunction } from 'lodash-es';
25+
2326
import { TdSubmenuProps } from './type';
2427

2528
export default defineComponent({
@@ -36,7 +39,7 @@ export default defineComponent({
3639
const { theme, activeValues, expandValues, isHead, open } = menu;
3740

3841
const submenu = inject<TdSubMenuInterface>('TdSubmenu', {});
39-
const { setSubPopup, closeParentPopup } = submenu;
42+
const { setSubPopup, closeParentPopup, cancelHideTimer } = submenu;
4043

4144
const mode = computed(() => attrs.expandType || menu.mode.value);
4245

@@ -59,6 +62,21 @@ export default defineComponent({
5962
const transitionClass = usePrefixClass('slide-down');
6063
useRipple(submenuRef, rippleColor);
6164

65+
// 存储 setTimeout 的 timer ID,用于清除定时器
66+
const showTimer = ref<ReturnType<typeof setTimeout> | null>(null);
67+
const hideTimer = ref<ReturnType<typeof setTimeout> | null>(null);
68+
69+
const clearTimers = () => {
70+
if (showTimer.value !== null) {
71+
clearTimeout(showTimer.value);
72+
showTimer.value = null;
73+
}
74+
if (hideTimer.value !== null) {
75+
clearTimeout(hideTimer.value);
76+
hideTimer.value = null;
77+
}
78+
};
79+
6280
const classes = computed(() => [
6381
`${classPrefix.value}-submenu`,
6482
{
@@ -114,9 +132,33 @@ export default defineComponent({
114132
subPopupRef.value = ref;
115133
},
116134
closeParentPopup: (e: MouseEvent) => {
117-
const related = e.relatedTarget as HTMLElement;
118-
if (loopInPopup(related)) return;
119-
handleMouseLeavePopup(e);
135+
// 不再检查 relatedTarget,直接触发父级的延迟隐藏
136+
// 如果鼠标真的停留在父级,父级的 handleEnterPopup 会取消定时器
137+
// 如果鼠标继续离开,定时器会触发隐藏
138+
139+
clearTimers();
140+
141+
// 设置父级的延迟隐藏
142+
hideTimer.value = setTimeout(() => {
143+
popupVisible.value = false;
144+
hideTimer.value = null;
145+
}, 100);
146+
147+
// 继续通知上级父级
148+
if (isFunction(closeParentPopup)) {
149+
closeParentPopup(e);
150+
}
151+
},
152+
cancelHideTimer: () => {
153+
// 取消当前级别的隐藏定时器
154+
if (hideTimer.value !== null) {
155+
clearTimeout(hideTimer.value);
156+
hideTimer.value = null;
157+
}
158+
// 递归取消所有父级的隐藏定时器
159+
if (isFunction(cancelHideTimer)) {
160+
cancelHideTimer();
161+
}
120162
},
121163
}),
122164
);
@@ -130,7 +172,15 @@ export default defineComponent({
130172
// methods
131173
const handleMouseEnter = () => {
132174
if (props.disabled) return;
133-
setTimeout(() => {
175+
176+
clearTimers();
177+
178+
// 通知父级取消隐藏定时器
179+
if (isFunction(cancelHideTimer)) {
180+
cancelHideTimer();
181+
}
182+
183+
showTimer.value = setTimeout(() => {
134184
if (!popupVisible.value) {
135185
open(props.value);
136186

@@ -140,22 +190,22 @@ export default defineComponent({
140190
});
141191
}
142192
popupVisible.value = true;
193+
showTimer.value = null;
143194
}, 0);
144195
};
145196

146197
const targetInPopup = (el: HTMLElement) => el?.classList.contains(`${classPrefix.value}-menu__popup`);
147-
const loopInPopup = (el: HTMLElement): boolean => {
148-
if (!el) return false;
149-
return targetInPopup(el) || loopInPopup(el.parentElement);
150-
};
151198

152199
const handleMouseLeave = (e: MouseEvent) => {
153-
setTimeout(() => {
200+
clearTimers();
201+
202+
hideTimer.value = setTimeout(() => {
154203
const inPopup = targetInPopup(e.relatedTarget as HTMLElement);
155204

156205
if (isCursorInPopup.value || inPopup) return;
157206
popupVisible.value = false;
158-
}, 0);
207+
hideTimer.value = null;
208+
}, 100);
159209
};
160210

161211
const handleMouseLeavePopup = (e: any) => {
@@ -172,13 +222,31 @@ export default defineComponent({
172222
isCursorInPopup.value = false;
173223

174224
if (!isSubmenu(target)) {
175-
popupVisible.value = false;
176-
}
225+
clearTimers();
177226

178-
closeParentPopup?.(e);
227+
// 使用延迟隐藏,避免在子项之间移动时闪烁
228+
hideTimer.value = setTimeout(() => {
229+
popupVisible.value = false;
230+
hideTimer.value = null;
231+
}, 100);
232+
233+
// 立即通知父级也开始延迟隐藏
234+
closeParentPopup?.(e);
235+
}
179236
};
180237
const handleEnterPopup = () => {
181238
isCursorInPopup.value = true;
239+
240+
// 进入 popup 时清除隐藏定时器
241+
if (hideTimer.value !== null) {
242+
clearTimeout(hideTimer.value);
243+
hideTimer.value = null;
244+
}
245+
246+
// 通知父级取消隐藏定时器
247+
if (isFunction(cancelHideTimer)) {
248+
cancelHideTimer();
249+
}
182250
};
183251

184252
const handleSubmenuItemClick = () => {
@@ -330,6 +398,11 @@ export default defineComponent({
330398
}
331399
});
332400

401+
// Cleanup timers on unmount to prevent memory leaks
402+
onBeforeUnmount(() => {
403+
clearTimers();
404+
});
405+
333406
return () => {
334407
let child = null;
335408
let events = {};

packages/components/menu/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,5 @@ export interface TdSubMenuInterface {
2828
addMenuItem?: (item: TdMenuItem) => void;
2929
setSubPopup?: (popupRef: HTMLElement) => void;
3030
closeParentPopup?: (e: MouseEvent) => void;
31+
cancelHideTimer?: () => void;
3132
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
pr_number: 6121
3+
contributor: RSS1102
4+
---
5+
6+
- fix(menu): 修复快速操作菜单时,父菜单关闭后子菜单未正确收起的问题 @RSS1102 ([#6121](https://github.com/Tencent/tdesign-vue-next/pull/6121))

0 commit comments

Comments
 (0)