u
This commit is contained in:
1 parent
f572dce2f6
commit
e99a9fb274
356 files changed
+28877
-1055
No files matched your search
@@ -0,0 +1,116 @@
|
||||
<script setup lang="ts">
|
||||
import { inject, isVNode, type VNode } from 'vue'
|
||||
import { ChevronRight } from '@lucide/vue'
|
||||
import { useNamespace } from '../_composables/useNamespace'
|
||||
import G3Popover from '../popover/Popover.vue'
|
||||
import { dropdownActiveIndexKey, dropdownSelectKey } from './context'
|
||||
import type { DropdownItemData } from './types'
|
||||
|
||||
/**
|
||||
* 下拉菜单列表(内部组件)—— 把 items 数组渲染成分隔线 / 分组标签 / 子菜单 / 普通项。
|
||||
*/
|
||||
defineOptions({ name: 'G3DropdownMenu' })
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
/** 菜单项数组 */
|
||||
items?: DropdownItemData[]
|
||||
/** 是否参与主菜单的键盘 / 鼠标高亮联动;递归子菜单时关闭 */
|
||||
interactive?: boolean
|
||||
}>(),
|
||||
{ items: () => [], interactive: true },
|
||||
)
|
||||
|
||||
const { b, e, m, is } = useNamespace('dropdown')
|
||||
|
||||
const selectItem = inject(dropdownSelectKey, null)
|
||||
const activeIndex = inject(dropdownActiveIndexKey, null)
|
||||
|
||||
function isActive(index: number): boolean {
|
||||
return props.interactive && activeIndex !== null && activeIndex.value === index
|
||||
}
|
||||
|
||||
/** 鼠标移入时同步键盘高亮索引(子菜单不参与联动) */
|
||||
function setActive(index: number) {
|
||||
if (!props.interactive || activeIndex === null) return
|
||||
activeIndex.value = index
|
||||
}
|
||||
|
||||
function onItemClick(item: DropdownItemData) {
|
||||
if (item.disabled) return
|
||||
item.onClick?.(item)
|
||||
selectItem?.(item)
|
||||
}
|
||||
|
||||
/** 渲染函数组件:把 VNode 形态的图标原样渲染 */
|
||||
const VNodeRenderer = (renderProps: { node: VNode }) => renderProps.node
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-for="(item, index) in props.items" :key="item?.key ?? index">
|
||||
<!-- 分隔线 -->
|
||||
<div v-if="item?.divider" :class="e('divider')" role="separator" />
|
||||
|
||||
<!-- 分组标签 -->
|
||||
<div v-else-if="item?.type === 'label'" :class="e('label')" role="group">
|
||||
{{ item.label }}
|
||||
</div>
|
||||
|
||||
<!-- 子菜单:嵌套 Popover,hover 展开 -->
|
||||
<G3Popover
|
||||
v-else-if="item?.children?.length"
|
||||
trigger="hover"
|
||||
placement="right-start"
|
||||
:offset="0"
|
||||
:arrow="false"
|
||||
unwrapped
|
||||
>
|
||||
<template #trigger>
|
||||
<span
|
||||
:class="[
|
||||
e('item'),
|
||||
{ [is('item-active')]: isActive(index), [is('item-disabled')]: item.disabled },
|
||||
]"
|
||||
role="menuitem"
|
||||
aria-haspopup="menu"
|
||||
>
|
||||
<span v-if="item.icon" :class="e('item-icon')">
|
||||
<component :is="item.icon" v-if="!isVNode(item.icon)" />
|
||||
<VNodeRenderer v-else :node="(item.icon as VNode)" />
|
||||
</span>
|
||||
<span :class="e('item-label')">{{ item.label }}</span>
|
||||
<ChevronRight :class="e('item-caret')" aria-hidden="true" />
|
||||
</span>
|
||||
</template>
|
||||
<template #content>
|
||||
<div :class="e('menu')" role="menu">
|
||||
<G3DropdownMenu :items="item.children" :interactive="false" />
|
||||
</div>
|
||||
</template>
|
||||
</G3Popover>
|
||||
|
||||
<!-- 普通项 -->
|
||||
<div
|
||||
v-else-if="item"
|
||||
:class="[
|
||||
e('item'),
|
||||
{
|
||||
[is('item-active')]: isActive(index),
|
||||
[is('item-disabled')]: item.disabled,
|
||||
[is('item-danger')]: item.danger,
|
||||
},
|
||||
]"
|
||||
role="menuitem"
|
||||
:aria-disabled="item.disabled || undefined"
|
||||
@mouseenter="!item.disabled && setActive(index)"
|
||||
@click="onItemClick(item)"
|
||||
>
|
||||
<span v-if="item.icon" :class="e('item-icon')">
|
||||
<component :is="item.icon" v-if="!isVNode(item.icon)" />
|
||||
<VNodeRenderer v-else :node="(item.icon as VNode)" />
|
||||
</span>
|
||||
<span :class="e('item-label')">{{ item.label }}</span>
|
||||
<span v-if="item.shortcut" :class="e('item-shortcut')">{{ item.shortcut }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
Reference in new issue
Block a user