mirror of
https://github.com/tusen-ai/naive-ui.git
synced 2025-03-07 13:48:31 +08:00
feat(menu): option support divider type (#1636)
* feat(menu): Add divider into `Menu` (like `DropdownDivider`) * [wip] fix(menu): rename MenuOptionDivider to MenuDivider * refactor * remove comment Co-authored-by: 07akioni <07akioni2@gmail.com> * fix types * it compiled now * remove 'submenu' from menu option that was used on dropdown * fixes * feat(menu): option support divider type * fix(dropdown): export render option * fix(dropdown): DropdownIntersectionOption type * docs(menu): divider enus * fix(menu): group option children type * fix(menu): option children type Co-authored-by: Rafael Hengles <rhengles@gmail.com>
This commit is contained in:
parent
bb452d421d
commit
79635320a4
@ -22,6 +22,7 @@
|
||||
- Add `n-tab` component, closes [#1630](https://github.com/TuSimple/naive-ui/issues/1630).
|
||||
- `n-switch` add `round` prop, closes [#1469](https://github.com/TuSimple/naive-ui/issues/1469).
|
||||
- `n-step` add `title` slot.
|
||||
- `n-menu` support `divider` type option.
|
||||
|
||||
### Fixes
|
||||
|
||||
|
@ -22,6 +22,7 @@
|
||||
- 新增 `n-tab` 组件,关闭 [#1630](https://github.com/TuSimple/naive-ui/issues/1630)
|
||||
- `n-switch` 新增 `round` 属性,关闭 [#1469](https://github.com/TuSimple/naive-ui/issues/1469)
|
||||
- `n-step` 新增 `title` 插槽
|
||||
- `n-menu` 支持 `divider` 类型的选项
|
||||
|
||||
### Fixes
|
||||
|
||||
|
@ -3,6 +3,6 @@ export type { DropdownProps } from './src/Dropdown'
|
||||
export type {
|
||||
DropdownOption,
|
||||
DropdownGroupOption,
|
||||
DropdownIgnoredOption,
|
||||
DropdownDividerOption
|
||||
DropdownDividerOption,
|
||||
DropdownRenderOption
|
||||
} from './src/interface'
|
||||
|
@ -34,10 +34,11 @@ import type { DropdownTheme } from '../styles'
|
||||
import NDropdownMenu from './DropdownMenu'
|
||||
import style from './styles/index.cssr'
|
||||
import {
|
||||
DropdownGroupOption,
|
||||
DropdownIgnoredOption,
|
||||
DropdownOption,
|
||||
DropdownRenderOption,
|
||||
DropdownGroupOption,
|
||||
DropdownMixedOption,
|
||||
DropdownIgnoredOption,
|
||||
OnUpdateValue,
|
||||
OnUpdateValueImpl,
|
||||
RenderLabel,
|
||||
@ -132,7 +133,7 @@ export default defineComponent({
|
||||
const treemateRef = computed(() => {
|
||||
const { keyField, childrenField } = props
|
||||
return createTreeMate<
|
||||
DropdownOption,
|
||||
DropdownOption | DropdownRenderOption,
|
||||
DropdownGroupOption,
|
||||
DropdownIgnoredOption
|
||||
>(props.options, {
|
||||
|
@ -94,7 +94,8 @@ export default defineComponent({
|
||||
return (
|
||||
<div class={`${clsPrefix}-dropdown-menu`}>
|
||||
{this.tmNodes.map((tmNode) => {
|
||||
if (isRenderNode(tmNode.rawNode)) {
|
||||
const { rawNode } = tmNode
|
||||
if (isRenderNode(rawNode)) {
|
||||
return (
|
||||
<NDropdownRenderOption
|
||||
tmNode={tmNode as unknown as TreeNode<DropdownRenderOption>}
|
||||
@ -102,10 +103,10 @@ export default defineComponent({
|
||||
/>
|
||||
)
|
||||
}
|
||||
if (isDividerNode(tmNode.rawNode)) {
|
||||
if (isDividerNode(rawNode)) {
|
||||
return <NDropdownDivider clsPrefix={clsPrefix} key={tmNode.key} />
|
||||
}
|
||||
if (isGroupNode(tmNode.rawNode)) {
|
||||
if (isGroupNode(rawNode)) {
|
||||
return (
|
||||
<NDropdownGroup
|
||||
clsPrefix={clsPrefix}
|
||||
@ -121,7 +122,7 @@ export default defineComponent({
|
||||
tmNode={tmNode}
|
||||
parentKey={parentKey}
|
||||
key={tmNode.key}
|
||||
props={tmNode.rawNode.props}
|
||||
props={rawNode.props}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
|
@ -24,8 +24,7 @@ import { TreeNode } from 'treemate'
|
||||
import {
|
||||
DropdownGroupOption,
|
||||
DropdownIgnoredOption,
|
||||
DropdownOption,
|
||||
DropdownOptionProps
|
||||
DropdownOption
|
||||
} from './interface'
|
||||
|
||||
interface NDropdownOptionInjection {
|
||||
@ -56,7 +55,7 @@ export default defineComponent({
|
||||
type: String as PropType<FollowerPlacement>,
|
||||
default: 'right-start'
|
||||
},
|
||||
props: Object as PropType<DropdownOptionProps>
|
||||
props: Object as PropType<HTMLAttributes>
|
||||
},
|
||||
setup (props) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
@ -250,7 +249,7 @@ export default defineComponent({
|
||||
__dropdown-option
|
||||
class={`${clsPrefix}-dropdown-option-body__label`}
|
||||
>
|
||||
{/* TODO: Workaround, menu campatible */}
|
||||
{/* TODO: Workaround, menu compatible */}
|
||||
{renderLabel
|
||||
? renderLabel(rawNode)
|
||||
: render(rawNode[this.labelField] ?? rawNode.title)}
|
||||
|
@ -1,46 +1,29 @@
|
||||
import { TreeNode } from 'treemate'
|
||||
import { VNodeChild, HTMLAttributes } from 'vue'
|
||||
import { MenuOption, MenuGroupOption } from '../../menu/src/interface'
|
||||
import { VNodeChild } from 'vue'
|
||||
import {
|
||||
MenuOption,
|
||||
MenuGroupOption,
|
||||
MenuDividerOption,
|
||||
MenuIgnoredOption,
|
||||
MenuRenderOption
|
||||
} from '../../menu/src/interface'
|
||||
|
||||
export type Key = string | number
|
||||
|
||||
export type DropdownOptionProps = HTMLAttributes
|
||||
|
||||
// Aligned with MenuOption props, has some redundant fields
|
||||
export type DropdownOption = MenuOption & { props?: HTMLAttributes }
|
||||
export type DropdownGroupOption = MenuGroupOption & {
|
||||
props?: HTMLAttributes
|
||||
}
|
||||
export interface DropdownIgnoredOption {
|
||||
key?: Key
|
||||
type: 'render' | 'divider'
|
||||
props?: HTMLAttributes
|
||||
render?: () => VNodeChild
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export type DropdownOption = MenuOption
|
||||
export type DropdownGroupOption = MenuGroupOption
|
||||
export type DropdownDividerOption = MenuDividerOption
|
||||
export type DropdownRenderOption = MenuRenderOption
|
||||
export type DropdownMixedOption =
|
||||
| DropdownOption
|
||||
| DropdownGroupOption
|
||||
| DropdownIgnoredOption
|
||||
| DropdownDividerOption
|
||||
| DropdownRenderOption
|
||||
|
||||
export type DropdownIntersectionOption = DropdownOption &
|
||||
DropdownGroupOption &
|
||||
DropdownIgnoredOption
|
||||
export type DropdownIgnoredOption = MenuIgnoredOption
|
||||
|
||||
export interface DropdownRenderOption {
|
||||
key?: Key
|
||||
type: 'render'
|
||||
props?: HTMLAttributes
|
||||
render?: () => VNodeChild
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export interface DropdownDividerOption {
|
||||
key?: Key
|
||||
type: 'divider'
|
||||
[key: string]: unknown
|
||||
}
|
||||
export type DropdownIntersectionOption = DropdownOption & DropdownGroupOption
|
||||
|
||||
export type TmNode = TreeNode<
|
||||
DropdownOption,
|
||||
|
@ -43,7 +43,7 @@ customize-field
|
||||
| inverted | `boolean` | `false` | Use inverted style. |
|
||||
| key-field | `string` | `'key'` | Field name of key. |
|
||||
| label-field | `string` | `'label'` | Field name of label. |
|
||||
| options | `Array<MenuOption \| MenuGroupOption>` | `[]` | Items data of menu. |
|
||||
| options | `Array<MenuOption \| MenuDividerOption \| MenuGroupOption>` | `[]` | Items data of menu. |
|
||||
| mode | `'vertical' \| 'horizontal'` | `'vertical'` | Menu layout. |
|
||||
| render-extra | `(option: MenuOption \| MenuGroupOption) => VNodeChild` | `undefined` | Render function that renders all extras. |
|
||||
| render-icon | `(option: MenuOption) => VNodeChild` | `undefined` | Render function that renders all icons. |
|
||||
@ -69,7 +69,15 @@ customize-field
|
||||
|
||||
| Name | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| children | `Array<MenuOption \| MenuGroupOption>` | Group items. **required!** |
|
||||
| children | `Array<MenuOption \| MenuGroupOption>` | Group items, **required!** |
|
||||
| key | `string` | The indentifier of the menu group. |
|
||||
| label | `string \| (() => VNodeChild)` | The label of the menu item. |
|
||||
| type | `'group'` | The type of the menu item, **required!** |
|
||||
|
||||
### MenuDividerOption Properties
|
||||
|
||||
| Name | Type | Description |
|
||||
| ----- | ---------------- | ---------------------------------------- |
|
||||
| key | `string` | The indentifier of the menu group. |
|
||||
| props | `HTMLAttributes` | Attributes of the divider. |
|
||||
| type | `'divider'` | The type of the menu item, **required!** |
|
||||
|
@ -40,6 +40,15 @@ const menuOptions = [
|
||||
key: 'go-back-home',
|
||||
icon: renderIcon(HomeIcon)
|
||||
},
|
||||
{
|
||||
key: 'divider-1',
|
||||
type: 'divider',
|
||||
props: {
|
||||
style: {
|
||||
marginLeft: '32px'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
|
@ -43,7 +43,7 @@ customize-field
|
||||
| inverted | `boolean` | `false` | 使用反转样式 |
|
||||
| key-field | `string` | `'key'` | key 的字段名 |
|
||||
| label-field | `string` | `'label'` | label 的字段名 |
|
||||
| options | `Array<MenuOption \| MenuGroupOption>` | `[]` | 菜单的数据 |
|
||||
| options | `Array<MenuOption \| MenuDividerOption \| MenuGroupOption>` | `[]` | 菜单的数据 |
|
||||
| mode | `'vertical' \| 'horizontal'` | `'vertical'` | 菜单的布局方式 |
|
||||
| render-extra | `(option: MenuOption \| MenuGroupOption) => VNodeChild` | `undefined` | 批量处理菜单额外部分渲染 |
|
||||
| render-icon | `(option: MenuOption) => VNodeChild` | `undefined` | 批量处理菜单图标渲染 |
|
||||
@ -73,3 +73,11 @@ customize-field
|
||||
| key | `string` | 菜单项的标识符 |
|
||||
| label | `string \| (() => VNodeChild)` | 菜单项的内容 |
|
||||
| type | `'group'` | 菜单项的类型,**必填!** |
|
||||
|
||||
### MenuDividerOption Properties
|
||||
|
||||
| 名称 | 类型 | 说明 |
|
||||
| ----- | ---------------- | ------------------------ |
|
||||
| key | `string` | 菜单项的标识符 |
|
||||
| props | `HTMLAttributes` | 分割线的属性 |
|
||||
| type | `'divider'` | 菜单项的类型,**必填!** |
|
||||
|
@ -40,6 +40,15 @@ const menuOptions = [
|
||||
key: 'go-back-home',
|
||||
icon: renderIcon(HomeIcon)
|
||||
},
|
||||
{
|
||||
key: 'divider-1',
|
||||
type: 'divider',
|
||||
props: {
|
||||
style: {
|
||||
marginLeft: '32px'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
|
@ -1,3 +1,7 @@
|
||||
export { default as NMenu } from './src/Menu'
|
||||
export type { MenuProps } from './src/Menu'
|
||||
export type { MenuOption, MenuGroupOption } from './src/interface'
|
||||
export type {
|
||||
MenuOption,
|
||||
MenuGroupOption,
|
||||
MenuDividerOption
|
||||
} from './src/interface'
|
||||
|
@ -27,6 +27,8 @@ import style from './styles/index.cssr'
|
||||
import {
|
||||
MenuOption,
|
||||
MenuGroupOption,
|
||||
MenuIgnoredOption,
|
||||
MenuMixedOption,
|
||||
OnUpdateValue,
|
||||
OnUpdateKeys,
|
||||
OnUpdateValueImpl,
|
||||
@ -40,7 +42,7 @@ import { DropdownProps } from '../../dropdown'
|
||||
const menuProps = {
|
||||
...(useTheme.props as ThemeProps<MenuTheme>),
|
||||
options: {
|
||||
type: Array as PropType<Array<MenuOption | MenuGroupOption>>,
|
||||
type: Array as PropType<MenuMixedOption[]>,
|
||||
default: () => []
|
||||
},
|
||||
collapsed: {
|
||||
@ -165,11 +167,11 @@ export default defineComponent({
|
||||
|
||||
const treeMateRef = computed(() => {
|
||||
const { keyField, childrenField } = props
|
||||
return createTreeMate<MenuOption, MenuGroupOption>(
|
||||
return createTreeMate<MenuOption, MenuGroupOption, MenuIgnoredOption>(
|
||||
props.items || props.options,
|
||||
{
|
||||
getChildren (node) {
|
||||
return node[childrenField] as any
|
||||
return node[childrenField]
|
||||
},
|
||||
getKey (node) {
|
||||
return (node[keyField] as Key) ?? node.name
|
||||
@ -314,9 +316,15 @@ export default defineComponent({
|
||||
common: { cubicBezierEaseInOut },
|
||||
self
|
||||
} = themeRef.value
|
||||
const { borderRadius, borderColorHorizontal, fontSize, itemHeight } =
|
||||
self
|
||||
const {
|
||||
borderRadius,
|
||||
borderColorHorizontal,
|
||||
fontSize,
|
||||
itemHeight,
|
||||
dividerColor
|
||||
} = self
|
||||
const vars: any = {
|
||||
'--divider-color': dividerColor,
|
||||
'--bezier': cubicBezierEaseInOut,
|
||||
'--font-size': fontSize,
|
||||
'--border-color-horizontal': borderColorHorizontal,
|
||||
|
15
src/menu/src/MenuDivider.tsx
Normal file
15
src/menu/src/MenuDivider.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { h, defineComponent, inject } from 'vue'
|
||||
import { menuInjectionKey } from './Menu'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'MenuDivider',
|
||||
setup () {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const NMenu = inject(menuInjectionKey)!
|
||||
const { mergedClsPrefixRef, isHorizontalRef } = NMenu
|
||||
return () =>
|
||||
isHorizontalRef.value ? null : (
|
||||
<div class={`${mergedClsPrefixRef.value}-menu-divider`} />
|
||||
)
|
||||
}
|
||||
})
|
@ -16,18 +16,17 @@ import NMenuOptionContent from './MenuOptionContent'
|
||||
import { itemRenderer } from './utils'
|
||||
import { useMenuChild, useMenuChildProps } from './use-menu-child'
|
||||
import type { SubmenuInjection } from './use-menu-child'
|
||||
import { TreeNode } from 'treemate'
|
||||
import { MenuGroupOption, MenuOption, TmNode } from './interface'
|
||||
import { MenuMixedOption, TmNode } from './interface'
|
||||
import { menuItemGroupInjectionKey } from './MenuOptionGroup'
|
||||
|
||||
export const submenuProps = {
|
||||
...useMenuChildProps,
|
||||
rawNodes: {
|
||||
type: Array as PropType<Array<MenuOption | MenuGroupOption>>,
|
||||
type: Array as PropType<MenuMixedOption[]>,
|
||||
default: () => []
|
||||
},
|
||||
tmNodes: {
|
||||
type: Array as PropType<Array<TreeNode<MenuOption, MenuGroupOption>>>,
|
||||
type: Array as PropType<TmNode[]>,
|
||||
default: () => []
|
||||
},
|
||||
tmNode: {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { TreeNode } from 'treemate'
|
||||
import { VNodeChild } from 'vue'
|
||||
import { VNodeChild, HTMLAttributes } from 'vue'
|
||||
|
||||
export type Key = string | number
|
||||
|
||||
@ -7,16 +7,37 @@ export interface MenuOptionSharedPart {
|
||||
key?: Key
|
||||
disabled?: boolean
|
||||
icon?: () => VNodeChild
|
||||
children?: Array<MenuOption | MenuGroupOption>
|
||||
children?: Array<MenuOption | MenuGroupOption | MenuDividerOption>
|
||||
extra?: string | (() => VNodeChild)
|
||||
props?: HTMLAttributes
|
||||
[key: string]: unknown
|
||||
/** @deprecated */
|
||||
titleExtra?: string | (() => VNodeChild)
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export type MenuIgnoredOption = MenuDividerOption | MenuRenderOption
|
||||
|
||||
export interface MenuDividerOption {
|
||||
type: 'divider'
|
||||
key?: Key
|
||||
props?: HTMLAttributes
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export interface MenuRenderOption {
|
||||
type: 'render'
|
||||
key?: Key
|
||||
props?: HTMLAttributes
|
||||
render?: () => VNodeChild
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export interface MenuGroupOptionBase extends MenuOptionSharedPart {
|
||||
type: 'group'
|
||||
children?: Array<MenuOption | MenuGroupOption>
|
||||
children: Array<MenuOption | MenuDividerOption>
|
||||
}
|
||||
|
||||
export type MenuOption =
|
||||
@ -33,7 +54,9 @@ export type MenuGroupOption =
|
||||
})
|
||||
| (MenuGroupOptionBase & { label?: string | (() => VNodeChild) })
|
||||
|
||||
export type TmNode = TreeNode<MenuOption, MenuGroupOption>
|
||||
export type MenuMixedOption = MenuDividerOption | MenuOption | MenuGroupOption
|
||||
|
||||
export type TmNode = TreeNode<MenuOption, MenuGroupOption, MenuIgnoredOption>
|
||||
|
||||
export type OnUpdateValue = (
|
||||
value: string & number & (string | number),
|
||||
|
@ -286,7 +286,13 @@ export default c([
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
`)
|
||||
])
|
||||
]),
|
||||
cB('menu-divider', `
|
||||
transition: background-color .3s var(--bezier);
|
||||
background-color: var(--divider-color);
|
||||
height: 1px;
|
||||
margin: 6px 18px;
|
||||
`)
|
||||
])
|
||||
|
||||
function hoverStyle (props: CProperties, children: CNodeChildren): CNode[] {
|
||||
|
@ -4,19 +4,46 @@ import { keep, keysOf } from '../../_utils'
|
||||
import NMenuOptionGroup, { menuItemGroupProps } from './MenuOptionGroup'
|
||||
import NSubmenu, { submenuProps } from './Submenu'
|
||||
import NMenuOption, { menuItemProps } from './MenuOption'
|
||||
import { MenuOption, MenuGroupOption } from './interface'
|
||||
import NMenuDivider from './MenuDivider'
|
||||
import {
|
||||
MenuOption,
|
||||
MenuGroupOption,
|
||||
MenuIgnoredOption,
|
||||
MenuMixedOption
|
||||
} from './interface'
|
||||
import { MenuSetupProps } from './Menu'
|
||||
|
||||
const groupPropKeys = keysOf(menuItemGroupProps)
|
||||
const itemPropKeys = keysOf(menuItemProps)
|
||||
const submenuPropKeys = keysOf(submenuProps)
|
||||
|
||||
export function isIgnoredNode (
|
||||
rawNode: MenuMixedOption
|
||||
): rawNode is MenuIgnoredOption {
|
||||
return rawNode.type === 'divider' || rawNode.type === 'render'
|
||||
}
|
||||
|
||||
export function isDividerNode (
|
||||
rawNode: MenuMixedOption
|
||||
): rawNode is MenuIgnoredOption {
|
||||
return rawNode.type === 'divider' || rawNode.type === 'render'
|
||||
}
|
||||
|
||||
export function itemRenderer (
|
||||
tmNode: TreeNode<MenuOption, MenuGroupOption>,
|
||||
tmNode: TreeNode<MenuOption, MenuGroupOption, MenuIgnoredOption>,
|
||||
menuProps: MenuSetupProps
|
||||
): VNode {
|
||||
): VNode | undefined {
|
||||
const { rawNode } = tmNode
|
||||
|
||||
if (isIgnoredNode(rawNode)) {
|
||||
if (isDividerNode(rawNode)) {
|
||||
return <NMenuDivider key={tmNode.key} {...rawNode.props} />
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
const { labelField } = menuProps
|
||||
const { rawNode, key, level, isGroup } = tmNode
|
||||
const { key, level, isGroup } = tmNode
|
||||
const props = {
|
||||
...rawNode,
|
||||
title: (rawNode.title || rawNode[labelField]) as
|
||||
@ -42,7 +69,7 @@ export function itemRenderer (
|
||||
NSubmenu,
|
||||
keep(props, submenuPropKeys, {
|
||||
key,
|
||||
rawNodes: tmNode.rawNode[menuProps.childrenField] as any,
|
||||
rawNodes: rawNode[menuProps.childrenField] as any,
|
||||
tmNodes: tmNode.children,
|
||||
tmNode
|
||||
})
|
@ -35,7 +35,8 @@ export const self = (vars: ThemeCommonVars) => {
|
||||
textColor2,
|
||||
primaryColorHover,
|
||||
textColor1,
|
||||
fontSize
|
||||
fontSize,
|
||||
dividerColor
|
||||
} = vars
|
||||
return {
|
||||
borderRadius: borderRadius,
|
||||
@ -62,6 +63,7 @@ export const self = (vars: ThemeCommonVars) => {
|
||||
itemColorActiveCollapsedInverted: primaryColor,
|
||||
borderColorHorizontal: '#0000',
|
||||
fontSize,
|
||||
dividerColor,
|
||||
...createPartialInvertedVars('#BBB', '#FFF', '#AAA')
|
||||
}
|
||||
}
|
||||
|
@ -346,4 +346,41 @@ describe('n-menu', () => {
|
||||
})
|
||||
expect(wrapper.find('.n-submenu-children').element.children.length).toBe(3)
|
||||
})
|
||||
|
||||
it('accepts proper options', () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const menu = (
|
||||
<NMenu
|
||||
options={[
|
||||
{
|
||||
type: 'divider'
|
||||
},
|
||||
{
|
||||
type: 'group',
|
||||
key: 'foo'
|
||||
},
|
||||
{
|
||||
key: 'blabla',
|
||||
label: 'kirby'
|
||||
},
|
||||
{
|
||||
key: 'xxxx',
|
||||
children: [
|
||||
{
|
||||
type: 'divider'
|
||||
},
|
||||
{
|
||||
type: 'group',
|
||||
key: 'foo1'
|
||||
},
|
||||
{
|
||||
key: 'blabla1',
|
||||
label: 'kirby'
|
||||
}
|
||||
]
|
||||
}
|
||||
]}
|
||||
/>
|
||||
)
|
||||
})
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user