feat(dropdown): clsPrefix

This commit is contained in:
07akioni 2021-04-16 23:39:49 +08:00
parent 0af36ac95b
commit e7c0d7d269
9 changed files with 293 additions and 272 deletions

View File

@ -20,7 +20,7 @@ For some special case, you may want to manually position the dropdown. For examp
```
```js
import { defineComponent, ref } from 'vue'
import { defineComponent, ref, nextTick } from 'vue'
import { useMessage } from 'naive-ui'
const options = [

View File

@ -1,5 +1,5 @@
/* istanbul ignore file */
export { default as NDropdown } from './src/Dropdown'
export type { DropdownProps } from './src/Dropdown'
export type {
DropdownOption,
DropdownGroupOption,

View File

@ -8,14 +8,21 @@ import {
watch,
provide,
reactive,
CSSProperties
CSSProperties,
InjectionKey
} from 'vue'
import { createTreeMate, Key, TreeMateOptions, TreeNode } from 'treemate'
import { useMergedState, useKeyboard, useMemo } from 'vooks'
import { useTheme } from '../../_mixins'
import { useConfig, useTheme } from '../../_mixins'
import type { ThemeProps } from '../../_mixins'
import { NPopover, popoverBaseProps } from '../../popover'
import { keep, call, createKey, MaybeArray } from '../../_utils'
import {
keep,
call,
createKey,
MaybeArray,
ExtractPublicPropTypes
} from '../../_utils'
import { dropdownLight } from '../styles'
import type { DropdownTheme } from '../styles'
import NDropdownMenu from './DropdownMenu'
@ -57,7 +64,11 @@ export interface DropdownInjection {
doUpdateShow: (value: boolean) => void
}
const dropdownProps = {
export const dropdownInjectionKey: InjectionKey<DropdownInjection> = Symbol(
'dropdown'
)
const dropdownBaseProps = {
animated: {
type: Boolean,
default: true
@ -83,10 +94,6 @@ const dropdownProps = {
type: Array as PropType<DropdownMixedOption[]>,
default: () => []
},
containerClass: {
type: String,
default: 'n-dropdown'
},
// for menu
value: [String, Number] as PropType<Key | null>
} as const
@ -95,13 +102,17 @@ const popoverPropKeys = Object.keys(popoverBaseProps) as Array<
keyof typeof popoverBaseProps
>
const dropdownProps = {
...popoverBaseProps,
...dropdownBaseProps,
...(useTheme.props as ThemeProps<DropdownTheme>)
} as const
export type DropdownProps = ExtractPublicPropTypes<typeof dropdownProps>
export default defineComponent({
name: 'Dropdown',
props: {
...popoverBaseProps,
...dropdownProps,
...(useTheme.props as ThemeProps<DropdownTheme>)
},
props: dropdownProps,
setup (props) {
const uncontrolledShowRef = ref(false)
const mergedShowRef = useMergedState(
@ -171,16 +182,19 @@ export default defineComponent({
keyboardEnabledRef
)
const { mergedClsPrefix } = useConfig(props)
const themeRef = useTheme(
'Dropdown',
'Dropdown',
style,
dropdownLight,
props
props,
mergedClsPrefix
)
provide<DropdownInjection>(
'NDropdown',
provide(
dropdownInjectionKey,
reactive({
hoverKey: hoverKeyRef,
keyboardKey: keyboardKeyRef,
@ -278,6 +292,7 @@ export default defineComponent({
}
}
return {
cPrefix: mergedClsPrefix,
mergedTheme: themeRef,
// data
tmNodes: tmNodesRef,
@ -339,17 +354,19 @@ export default defineComponent({
NPopover,
keep(this.$props, popoverPropKeys, {
show: this.mergedShow,
'onUpdate:show': this.doUpdateShow,
onUpdateShow: this.doUpdateShow,
showArrow: false,
raw: true,
shadow: false,
theme: this.mergedTheme.peers.Popover,
themeOverrides: this.mergedTheme.peerOverrides.Popover
themeOverrides: this.mergedTheme.peerOverrides.Popover,
internalExtraClass: 'dropdown'
}),
{
trigger: this.$slots.default,
default: () => {
return h(NDropdownMenu, {
clsPrefix: this.cPrefix,
tmNodes: this.tmNodes,
style: this.cssVars as CSSProperties
})

View File

@ -2,7 +2,13 @@ import { h, defineComponent } from 'vue'
export default defineComponent({
name: 'DropdownDivider',
props: {
clsPrefix: {
type: String,
required: true
}
},
render () {
return <div class="n-dropdown-divider" />
return <div class={`${this.clsPrefix}-dropdown-divider`} />
}
})

View File

@ -1,4 +1,4 @@
import { defineComponent, Fragment, h, PropType, VNode } from 'vue'
import { defineComponent, Fragment, h, PropType } from 'vue'
import { TreeNode } from 'treemate'
import { warn } from '../../_utils'
import NDropdownOption from './DropdownOption'
@ -14,6 +14,10 @@ import {
export default defineComponent({
name: 'NDropdownGroup',
props: {
clsPrefix: {
type: String,
required: true
},
tmNode: {
type: Object as PropType<
TreeNode<DropdownOption, DropdownGroupOption, DropdownIgnoredOption>
@ -26,19 +30,19 @@ export default defineComponent({
}
},
render () {
const { tmNode, parentKey } = this
const { tmNode, parentKey, clsPrefix } = this
const { children } = tmNode
return h(
Fragment,
[
h(NDropdownGroupHeader, {
tmNode,
key: tmNode.key
})
].concat(
children?.map((child) => {
return (
<>
<NDropdownGroupHeader
clsPrefix={clsPrefix}
tmNode={tmNode}
key={tmNode.key}
/>
{children?.map((child) => {
if (isDividerNode(child.rawNode)) {
return h(NDropdownDivider, {
clsPrefix,
key: child.key
})
}
@ -50,12 +54,13 @@ export default defineComponent({
return null
}
return h(NDropdownOption, {
clsPrefix,
tmNode: child,
parentKey,
key: child.key
})
}) as VNode[]
)
})}
</>
)
}
})

View File

@ -1,72 +0,0 @@
import { defineComponent, h, inject } from 'vue'
import { render } from '../../_utils'
import { NDropdownMenuInjection } from './DropdownMenu'
export default defineComponent({
name: 'DropdownGroupHeader',
props: {
tmNode: {
type: Object,
required: true
}
},
setup () {
return {
NDropdownMenu: inject<NDropdownMenuInjection>(
'NDropdownMenu'
) as NDropdownMenuInjection
}
},
render () {
const { rawNode } = this.tmNode
return h(
'div',
{
class: 'n-dropdown-option'
},
[
h(
'div',
{
class: 'n-dropdown-option-body n-dropdown-option-body--group'
},
[
h(
'div',
{
class: [
'n-dropdown-option-body__prefix',
{
'n-dropdown-option-body__prefix--show-icon': this
.NDropdownMenu.showIcon
}
],
'n-dropdown-option': true
},
[h(render, { render: rawNode.icon })]
),
h(
'div',
{
class: 'n-dropdown-option-body__label',
'n-dropdown-option': true
},
// TODO: Workaround, menu campatible
[h(render, { render: rawNode.label ?? rawNode.title })]
),
h('div', {
class: [
'n-dropdown-option-body__suffix',
{
'n-dropdown-option-body__suffix--has-submenu': this
.NDropdownMenu.hasSubmenu
}
],
'n-dropdown-option': true
})
]
)
]
)
}
})

View File

@ -0,0 +1,59 @@
import { defineComponent, h, inject } from 'vue'
import { render } from '../../_utils'
import { dropdownMenuInjectionKey } from './DropdownMenu'
export default defineComponent({
name: 'DropdownGroupHeader',
props: {
clsPrefix: {
type: String,
required: true
},
tmNode: {
type: Object,
required: true
}
},
setup () {
return {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
NDropdownMenu: inject(dropdownMenuInjectionKey)!
}
},
render () {
const { clsPrefix } = this
const { rawNode } = this.tmNode
return (
<div class={`${clsPrefix}-dropdown-option`}>
<div
class={`${clsPrefix}-dropdown-option-body ${clsPrefix}-dropdown-option-body--group`}
>
<div
__dropdown-option
class={[
`${clsPrefix}-dropdown-option-body__prefix`,
this.NDropdownMenu.showIcon &&
`${clsPrefix}-dropdown-option-body__prefix--show-icon`
]}
>
{h(render, { render: rawNode.icon })}
</div>
<div
class={`${clsPrefix}-dropdown-option-body__label`}
__dropdown-option
>
{h(render, { render: rawNode.label ?? rawNode.title })}
</div>
<div
class={[
`${clsPrefix}-dropdown-option-body__suffix`,
this.NDropdownMenu.hasSubmenu &&
`${clsPrefix}-dropdown-option-body__suffix--has-submenu`
]}
__dropdown-option
/>
</div>
</div>
)
}
})

View File

@ -1,4 +1,12 @@
import { computed, defineComponent, h, PropType, provide, reactive } from 'vue'
import {
computed,
defineComponent,
h,
InjectionKey,
PropType,
provide,
reactive
} from 'vue'
import { TreeNode } from 'treemate'
import NDropdownOption from './DropdownOption'
import NDropdownDivider from './DropdownDivider'
@ -15,9 +23,17 @@ export interface NDropdownMenuInjection {
hasSubmenu: boolean
}
export const dropdownMenuInjectionKey: InjectionKey<NDropdownMenuInjection> = Symbol(
'dropdownMenu'
)
export default defineComponent({
name: 'DropdownMenu',
props: {
clsPrefix: {
type: String,
required: true
},
tmNodes: {
type: Array as PropType<
Array<
@ -32,8 +48,8 @@ export default defineComponent({
}
},
setup (props) {
provide<NDropdownMenuInjection>(
'NDropdownMenu',
provide(
dropdownMenuInjectionKey,
reactive({
showIcon: computed(() => {
return props.tmNodes.some((tmNode) => {
@ -61,31 +77,33 @@ export default defineComponent({
)
},
render () {
const { parentKey } = this
return h(
'div',
{
class: 'n-dropdown-menu'
},
this.tmNodes.map((tmNode) => {
if (isDividerNode(tmNode.rawNode)) {
return h(NDropdownDivider, {
key: tmNode.key
})
}
if (isGroupNode(tmNode.rawNode)) {
return h(NDropdownGroup, {
tmNode,
parentKey,
key: tmNode.key
})
}
return h(NDropdownOption, {
tmNode: tmNode,
parentKey,
key: tmNode.key
})
})
const { parentKey, clsPrefix } = this
return (
<div class={`${clsPrefix}-dropdown-menu`}>
{this.tmNodes.map((tmNode) => {
if (isDividerNode(tmNode.rawNode)) {
return <NDropdownDivider clsPrefix={clsPrefix} key={tmNode.key} />
}
if (isGroupNode(tmNode.rawNode)) {
return (
<NDropdownGroup
clsPrefix={clsPrefix}
tmNode={tmNode}
parentKey={parentKey}
key={tmNode.key}
/>
)
}
return (
<NDropdownOption
clsPrefix={clsPrefix}
tmNode={tmNode}
parentKey={parentKey}
key={tmNode.key}
/>
)
})}
</div>
)
}
})

View File

@ -7,7 +7,8 @@ import {
defineComponent,
provide,
reactive,
PropType
PropType,
InjectionKey
} from 'vue'
import { VBinder, VTarget, VFollower, FollowerPlacement } from 'vueuc'
import { useMemo } from 'vooks'
@ -15,8 +16,8 @@ import { ChevronRightIcon } from '../../_internal/icons'
import { useDeferredTrue } from '../../_utils/composable'
import { render } from '../../_utils'
import { NIcon } from '../../icon'
import NDropdownMenu, { NDropdownMenuInjection } from './DropdownMenu'
import { DropdownInjection } from './Dropdown'
import NDropdownMenu, { dropdownMenuInjectionKey } from './DropdownMenu'
import { dropdownInjectionKey } from './Dropdown'
import { isSubmenuNode } from './utils'
import { TreeNode } from 'treemate'
import {
@ -29,9 +30,17 @@ interface NDropdownOptionInjection {
enteringSubmenu: boolean
}
const dropdownOptionInjectionKey: InjectionKey<NDropdownOptionInjection> = Symbol(
'dropdown-option'
)
export default defineComponent({
name: 'DropdownOption',
props: {
clsPrefix: {
type: String,
required: true
},
tmNode: {
type: Object as PropType<
TreeNode<DropdownOption, DropdownGroupOption, DropdownIgnoredOption>
@ -48,16 +57,11 @@ export default defineComponent({
}
},
setup (props) {
const NDropdown = inject<DropdownInjection>(
'NDropdown'
) as DropdownInjection
const NDropdownOption = inject<NDropdownOptionInjection | null>(
'NDropdownOption',
null
)
const NDropdownMenu = inject<NDropdownMenuInjection>(
'NDropdownMenu'
) as NDropdownMenuInjection
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const NDropdown = inject(dropdownInjectionKey)!
const NDropdownOption = inject(dropdownOptionInjectionKey, null)
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const NDropdownMenu = inject(dropdownMenuInjectionKey)!
const rawNodeRef = computed(() => props.tmNode.rawNode)
const hasSubmenuRef = computed(() => {
return isSubmenuNode(props.tmNode.rawNode)
@ -93,8 +97,8 @@ export default defineComponent({
return !!NDropdownOption?.enteringSubmenu
})
const enteringSubmenuRef = ref(false)
provide<NDropdownOptionInjection>(
'NDropdownOption',
provide(
dropdownOptionInjectionKey,
reactive({
enteringSubmenu: enteringSubmenuRef
})
@ -124,7 +128,7 @@ export default defineComponent({
const { relatedTarget } = e
if (
relatedTarget &&
!(relatedTarget as HTMLElement).getAttribute('n-dropdown-option')
!(relatedTarget as HTMLElement).hasAttribute('__dropdown-option')
) {
NDropdown.hoverKey = null
}
@ -171,136 +175,120 @@ export default defineComponent({
const {
NDropdown: { animated },
rawNode,
mergedShowSubmenu
mergedShowSubmenu,
clsPrefix
} = this
const submenuVNode = mergedShowSubmenu
? h(NDropdownMenu, {
clsPrefix,
tmNodes: this.tmNode.children,
parentKey: this.tmNode.key
})
: null
return h(
'div',
{
class: 'n-dropdown-option'
},
[
h(
'div',
{
class: [
'n-dropdown-option-body',
{
'n-dropdown-option-body--pending': this.pending,
'n-dropdown-option-body--active': this.active
}
],
onMousemove: this.handleMouseMove,
onMouseenter: this.handleMouseEnter,
onMouseleave: this.handleMouseLeave,
onClick: this.handleClick
},
[
h(
'div',
{
class: [
'n-dropdown-option-body__prefix',
{
'n-dropdown-option-body__prefix--show-icon': this
.NDropdownMenu.showIcon
}
],
'n-dropdown-option': true
},
[h(render, { render: rawNode.icon })]
),
h(
'div',
{
class: 'n-dropdown-option-body__label',
'n-dropdown-option': true
},
// TODO: Workaround, menu campatible
[h(render, { render: rawNode.label ?? rawNode.title })]
),
h(
'div',
{
class: [
'n-dropdown-option-body__suffix',
{
'n-dropdown-option-body__suffix--has-submenu': this
.NDropdownMenu.hasSubmenu
}
],
'n-dropdown-option': true
},
[
this.hasSubmenu
? h(NIcon, null, {
default: () => h(ChevronRightIcon)
})
: null
]
)
]
),
this.hasSubmenu
? h(VBinder, null, {
default: () => {
return h(VTarget, null, {
default: () => {
return h(
'div',
{
class: 'n-dropdown-offset-container'
},
[
h(
VFollower,
{
show: this.mergedShowSubmenu,
teleportDisabled: true,
placement: this.placement
},
{
default: () => {
return h(
'div',
{
class: 'n-dropdown-menu-wrapper'
},
[
animated
? h(
Transition,
{
onBeforeEnter: this
.handleSubmenuBeforeEnter,
onAfterEnter: this
.handleSubmenuAfterEnter,
name: 'n-fade-in-scale-up-transition',
appear: true
},
{
default: () => submenuVNode
}
)
: submenuVNode
]
)
}
}
)
]
)
}
})
return (
<div class={`${clsPrefix}-dropdown-option`}>
<div
class={[
`${clsPrefix}-dropdown-option-body`,
{
[`${clsPrefix}-dropdown-option-body--pending`]: this.pending,
[`${clsPrefix}-dropdown-option-body--active`]: this.active
}
})
: null
]
]}
onMousemove={this.handleMouseMove}
onMouseenter={this.handleMouseEnter}
onMouseleave={this.handleMouseLeave}
onClick={this.handleClick}
>
<div
__dropdown-option
class={[
`${clsPrefix}-dropdown-option-body__prefix`,
{
[`${clsPrefix}-dropdown-option-body__prefix--show-icon`]: this
.NDropdownMenu.showIcon
}
]}
>
{h(render, { render: rawNode.icon })}
</div>
<div
__dropdown-option
class={`${clsPrefix}-dropdown-option-body__label`}
>
{/* TODO: Workaround, menu campatible */}
{h(render, { render: rawNode.label ?? rawNode.title })}
</div>
<div
__dropdown-option
class={[
`${clsPrefix}-dropdown-option-body__suffix`,
{
[`${clsPrefix}-dropdown-option-body__suffix--has-submenu`]: this
.NDropdownMenu.hasSubmenu
}
]}
>
{this.hasSubmenu ? (
<NIcon>
{{
default: () => <ChevronRightIcon />
}}
</NIcon>
) : null}
</div>
</div>
{this.hasSubmenu ? (
<VBinder>
{{
default: () => [
<VTarget>
{{
default: () => (
<div class={`${clsPrefix}-dropdown-offset-container`}>
<VFollower
show={this.mergedShowSubmenu}
placement={this.placement}
teleportDisabled
>
{{
default: () => {
return (
<div
class={`${clsPrefix}-dropdown-menu-wrapper`}
>
{animated ? (
<Transition
onBeforeEnter={
this.handleSubmenuBeforeEnter
}
onAfterEnter={
this.handleSubmenuAfterEnter
}
name="n-fade-in-scale-up-transition"
appear
>
{{
default: () => submenuVNode
}}
</Transition>
) : (
submenuVNode
)}
</div>
)
}
}}
</VFollower>
</div>
)
}}
</VTarget>
]
}}
</VBinder>
) : null}
</div>
)
}
})