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:
07akioni 2021-11-21 23:13:04 +08:00 committed by GitHub
parent bb452d421d
commit 79635320a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 211 additions and 70 deletions

View File

@ -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

View File

@ -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

View File

@ -3,6 +3,6 @@ export type { DropdownProps } from './src/Dropdown'
export type {
DropdownOption,
DropdownGroupOption,
DropdownIgnoredOption,
DropdownDividerOption
DropdownDividerOption,
DropdownRenderOption
} from './src/interface'

View File

@ -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, {

View File

@ -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}
/>
)
})}

View File

@ -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)}

View File

@ -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,

View File

@ -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!** |

View File

@ -40,6 +40,15 @@ const menuOptions = [
key: 'go-back-home',
icon: renderIcon(HomeIcon)
},
{
key: 'divider-1',
type: 'divider',
props: {
style: {
marginLeft: '32px'
}
}
},
{
label: () =>
h(

View File

@ -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'` | 菜单项的类型,**必填!** |

View File

@ -40,6 +40,15 @@ const menuOptions = [
key: 'go-back-home',
icon: renderIcon(HomeIcon)
},
{
key: 'divider-1',
type: 'divider',
props: {
style: {
marginLeft: '32px'
}
}
},
{
label: () =>
h(

View File

@ -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'

View File

@ -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,

View 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`} />
)
}
})

View File

@ -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: {

View File

@ -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),

View File

@ -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[] {

View File

@ -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
})

View File

@ -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')
}
}

View File

@ -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'
}
]
}
]}
/>
)
})
})