This commit is contained in:
oneao committed 2026-10-10 17:33:59 +08:00
1 parent 5f28a10d89
commit 32d423b759
59 files changed
+5101 -1098

No files matched your search

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { inject, isVNode, type VNode } from 'vue'
import { inject, isVNode, provide, ref, type Ref, type VNode } from 'vue'
import { ChevronRight } from '@lucide/vue'
import { useNamespace } from '../_composables/useNamespace'
import G3Popover from '../popover/Popover.vue'
@@ -15,7 +15,7 @@ const props = withDefaults(
defineProps<{
/** 菜单项数组 */
items?: DropdownItemData[]
/** 是否参与主菜单的键盘 / 鼠标高亮联动;递归子菜单时关闭 */
/** 根层菜单:与 Dropdown 的键盘导航联动;子菜单为 false */
interactive?: boolean
}>(),
{ items: () => [], interactive: true },
@@ -24,18 +24,45 @@ const props = withDefaults(
const { b, e, m, is } = useNamespace('dropdown')
const selectItem = inject(dropdownSelectKey, null)
const activeIndex = inject(dropdownActiveIndexKey, null)
const injectedActiveIndex = inject(dropdownActiveIndexKey, null)
/**
* 本层生效的高亮索引(Ref<number>)。
*
* 根层(interactive=true):用 Dropdown 注入的那个 —— 键盘 ↑↓ 改的就是它,
* 鼠标划过也要回写同一个,两者才能联动(这是原有行为,必须保住)。
*
* 子菜单(interactive=false):**自持一个**,不碰注入值。原因见 setActive。
*/
const localActiveIndex = ref(-1)
const activeIndex: Ref<number> =
props.interactive && injectedActiveIndex ? injectedActiveIndex : localActiveIndex
function isActive(index: number): boolean {
return props.interactive && activeIndex !== null && activeIndex.value === index
return activeIndex.value === index
}
/** 鼠标移入时同步键盘高亮索引(子菜单不参与联动) */
function setActive(index: number) {
if (!props.interactive || activeIndex === null) return
/**
* 鼠标移入时更新本层高亮。
*
* 子菜单这里曾经是空操作(interactive=false 直接 return),导致子项完全没有 hover 反馈;
* 而且根层那个共享索引的残留值会命中子项索引(两层都从 0 开始),
* 表现为 hover 子项时灰底「一闪即消」—— 面板自身的 mouseenter 会触发
* setOpen(true) → onOpenChange 把根层 activeIndex 重置为 -1,子项又无力重新置位。
* 现在子菜单自持索引,面板重入再也冲不掉它。
*/
function setActive(index: number): void {
activeIndex.value = index
}
function clearActive(): void {
activeIndex.value = -1
}
/** 把这层的高亮索引 provide 给子菜单(子菜单会再 provide 自己那层覆盖掉) */
provide(dropdownActiveIndexKey, activeIndex)
function onItemClick(item: DropdownItemData) {
if (item.disabled) return
item.onClick?.(item)
@@ -69,10 +96,16 @@ const VNodeRenderer = (renderProps: { node: VNode }) => renderProps.node
<span
:class="[
e('item'),
{ [is('item-active')]: isActive(index), [is('item-disabled')]: item.disabled },
{
[is('item-active')]: isActive(index),
[is('item-selected')]: item.selected,
[is('item-disabled')]: item.disabled,
},
]"
role="menuitem"
aria-haspopup="menu"
:aria-current="item.selected ? 'page' : undefined"
@mouseenter="!item.disabled && setActive(index)"
>
<span v-if="item.icon" :class="e('item-icon')">
<component :is="item.icon" v-if="!isVNode(item.icon)" />
@@ -83,7 +116,7 @@ const VNodeRenderer = (renderProps: { node: VNode }) => renderProps.node
</span>
</template>
<template #content>
<div :class="e('menu')" role="menu">
<div :class="e('menu')" role="menu" @mouseleave="clearActive">
<G3DropdownMenu :items="item.children" :interactive="false" />
</div>
</template>
@@ -96,12 +129,14 @@ const VNodeRenderer = (renderProps: { node: VNode }) => renderProps.node
e('item'),
{
[is('item-active')]: isActive(index),
[is('item-selected')]: item.selected,
[is('item-disabled')]: item.disabled,
[is('item-danger')]: item.danger,
},
]"
role="menuitem"
:aria-disabled="item.disabled || undefined"
:aria-current="item.selected ? 'page' : undefined"
@mouseenter="!item.disabled && setActive(index)"
@click="onItemClick(item)"
>