feat: bugfix
This commit is contained in:
142
src/hooks/useDelayedMenu.js
Normal file
142
src/hooks/useDelayedMenu.js
Normal file
@@ -0,0 +1,142 @@
|
||||
// src/hooks/useDelayedMenu.js
|
||||
// 导航菜单延迟关闭 Hook - 优化 hover 和 click 交互体验
|
||||
|
||||
import { useState, useRef, useCallback } from 'react';
|
||||
|
||||
/**
|
||||
* 自定义 Hook:提供带延迟关闭功能的菜单控制
|
||||
*
|
||||
* 解决问题:
|
||||
* 1. 用户快速移动鼠标导致菜单意外关闭
|
||||
* 2. Hover 和 Click 状态冲突
|
||||
* 3. 从 MenuButton 移动到 MenuList 时菜单闪烁
|
||||
*
|
||||
* 功能特性:
|
||||
* - ✅ Hover 进入:立即打开菜单
|
||||
* - ✅ Hover 离开:延迟关闭(默认 200ms)
|
||||
* - ✅ Click 切换:支持点击切换打开/关闭状态
|
||||
* - ✅ 智能取消:再次 hover 进入时取消关闭定时器
|
||||
*
|
||||
* @param {Object} options - 配置选项
|
||||
* @param {number} options.closeDelay - 延迟关闭时间(毫秒),默认 200ms
|
||||
* @returns {Object} 菜单控制对象
|
||||
*/
|
||||
export function useDelayedMenu({ closeDelay = 200 } = {}) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const closeTimerRef = useRef(null);
|
||||
const isClickedRef = useRef(false); // 追踪是否通过点击打开
|
||||
|
||||
/**
|
||||
* 打开菜单
|
||||
* - 立即打开,无延迟
|
||||
* - 清除任何待执行的关闭定时器
|
||||
*/
|
||||
const onOpen = useCallback(() => {
|
||||
// 清除待执行的关闭定时器
|
||||
if (closeTimerRef.current) {
|
||||
clearTimeout(closeTimerRef.current);
|
||||
closeTimerRef.current = null;
|
||||
}
|
||||
setIsOpen(true);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 延迟关闭菜单
|
||||
* - 设置定时器,延迟后关闭
|
||||
* - 如果在延迟期间再次 hover 进入,会被 onOpen 取消
|
||||
*/
|
||||
const onDelayedClose = useCallback(() => {
|
||||
// 如果是点击打开的,hover 离开时不自动关闭
|
||||
if (isClickedRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 清除之前的定时器(防止重复设置)
|
||||
if (closeTimerRef.current) {
|
||||
clearTimeout(closeTimerRef.current);
|
||||
}
|
||||
|
||||
// 设置延迟关闭定时器
|
||||
closeTimerRef.current = setTimeout(() => {
|
||||
setIsOpen(false);
|
||||
closeTimerRef.current = null;
|
||||
}, closeDelay);
|
||||
}, [closeDelay]);
|
||||
|
||||
/**
|
||||
* 立即关闭菜单
|
||||
* - 无延迟,立即关闭
|
||||
* - 清除所有定时器和状态标记
|
||||
*/
|
||||
const onClose = useCallback(() => {
|
||||
// 清除定时器
|
||||
if (closeTimerRef.current) {
|
||||
clearTimeout(closeTimerRef.current);
|
||||
closeTimerRef.current = null;
|
||||
}
|
||||
setIsOpen(false);
|
||||
isClickedRef.current = false;
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 切换菜单状态(用于点击)
|
||||
* - 如果关闭 → 打开,并标记为点击打开
|
||||
* - 如果打开 → 关闭,并清除点击标记
|
||||
*/
|
||||
const onToggle = useCallback(() => {
|
||||
if (isOpen) {
|
||||
// 当前已打开 → 关闭
|
||||
onClose();
|
||||
} else {
|
||||
// 当前已关闭 → 打开
|
||||
onOpen();
|
||||
isClickedRef.current = true; // 标记为点击打开
|
||||
}
|
||||
}, [isOpen, onOpen, onClose]);
|
||||
|
||||
/**
|
||||
* Hover 进入处理
|
||||
* - 打开菜单
|
||||
* - 清除点击标记(允许 hover 离开时自动关闭)
|
||||
*/
|
||||
const handleMouseEnter = useCallback(() => {
|
||||
onOpen();
|
||||
isClickedRef.current = false; // 清除点击标记,允许 hover 控制
|
||||
}, [onOpen]);
|
||||
|
||||
/**
|
||||
* Hover 离开处理
|
||||
* - 延迟关闭菜单
|
||||
*/
|
||||
const handleMouseLeave = useCallback(() => {
|
||||
onDelayedClose();
|
||||
}, [onDelayedClose]);
|
||||
|
||||
/**
|
||||
* 点击处理
|
||||
* - 切换菜单状态
|
||||
*/
|
||||
const handleClick = useCallback(() => {
|
||||
onToggle();
|
||||
}, [onToggle]);
|
||||
|
||||
// 组件卸载时清理定时器
|
||||
const cleanup = useCallback(() => {
|
||||
if (closeTimerRef.current) {
|
||||
clearTimeout(closeTimerRef.current);
|
||||
closeTimerRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
isOpen,
|
||||
onOpen,
|
||||
onClose,
|
||||
onDelayedClose,
|
||||
onToggle,
|
||||
handleMouseEnter,
|
||||
handleMouseLeave,
|
||||
handleClick,
|
||||
cleanup
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user