mirror of
https://github.com/tusen-ai/naive-ui.git
synced 2025-01-12 12:25:16 +08:00
refactor(dropdown): ts
This commit is contained in:
parent
88db9bf48f
commit
2eda4312ae
@ -1,2 +0,0 @@
|
||||
/* istanbul ignore file */
|
||||
export { default as NDropdown } from './src/Dropdown.js'
|
2
src/dropdown/index.ts
Normal file
2
src/dropdown/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
/* istanbul ignore file */
|
||||
export { default as NDropdown } from './src/Dropdown'
|
@ -4,32 +4,51 @@ import {
|
||||
computed,
|
||||
ref,
|
||||
toRef,
|
||||
getCurrentInstance
|
||||
PropType,
|
||||
watch,
|
||||
provide,
|
||||
reactive
|
||||
} from 'vue'
|
||||
import { TreeMate } from 'treemate'
|
||||
import { RawNode, TreeMate } from 'treemate'
|
||||
import {
|
||||
useMergedState,
|
||||
useFalseUntilTruthy,
|
||||
// useFalseUntilTruthy,
|
||||
useKeyboard,
|
||||
useMemo
|
||||
} from 'vooks'
|
||||
import { useTheme } from '../../_mixins'
|
||||
import { NPopover } from '../../popover'
|
||||
import { NPopover, popoverProps } from '../../popover'
|
||||
import { keep, call, createKey } from '../../_utils'
|
||||
import { dropdownLight } from '../styles'
|
||||
import NDropdownMenu from './DropdownMenu.js'
|
||||
import style from './styles/index.cssr.js'
|
||||
import type { DropdownThemeVars } from '../styles'
|
||||
import NDropdownMenu from './DropdownMenu'
|
||||
import style from './styles/index.cssr'
|
||||
|
||||
type Key = string | number
|
||||
type OnSelect = (key: Key, rawNode: RawNode) => void
|
||||
|
||||
const treemateOptions = {
|
||||
getKey (node) {
|
||||
return node.key
|
||||
getKey (node: RawNode) {
|
||||
return node.key as Key
|
||||
},
|
||||
getDisabled (node) {
|
||||
getDisabled (node: RawNode) {
|
||||
if (node.type === 'divider') return true
|
||||
return node.disabled === true
|
||||
}
|
||||
}
|
||||
|
||||
export interface DropdownInjection {
|
||||
hoverKey: Key | null
|
||||
keyboardKey: Key | null
|
||||
lastToggledSubmenuKey: Key | null
|
||||
pendingKeyPath: Key[]
|
||||
activeKeyPath: Key[]
|
||||
animated: boolean
|
||||
mergedShow: boolean
|
||||
doSelect: OnSelect
|
||||
doUpdateShow: (value: boolean) => void
|
||||
}
|
||||
|
||||
const dropdownProps = {
|
||||
animated: {
|
||||
type: Boolean,
|
||||
@ -40,9 +59,7 @@ const dropdownProps = {
|
||||
default: true
|
||||
},
|
||||
size: {
|
||||
validator (value) {
|
||||
return ['small', 'medium', 'large', 'huge'].includes(value)
|
||||
},
|
||||
type: String as PropType<'small' | 'medium' | 'large' | 'huge'>,
|
||||
default: 'medium'
|
||||
},
|
||||
submenuWidth: {
|
||||
@ -54,11 +71,11 @@ const dropdownProps = {
|
||||
default: null
|
||||
},
|
||||
onSelect: {
|
||||
type: [Function, Array],
|
||||
type: [Function, Array] as PropType<OnSelect | OnSelect[]>,
|
||||
default: undefined
|
||||
},
|
||||
options: {
|
||||
type: Array,
|
||||
type: Array as PropType<RawNode[]>,
|
||||
required: true
|
||||
},
|
||||
containerClass: {
|
||||
@ -70,20 +87,16 @@ const dropdownProps = {
|
||||
type: String,
|
||||
default: undefined
|
||||
}
|
||||
}
|
||||
} as const
|
||||
|
||||
const popoverPropKeys = Object.keys(NPopover.props)
|
||||
const popoverPropKeys = Object.keys(
|
||||
popoverProps
|
||||
) as (keyof typeof popoverProps)[]
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Dropdown',
|
||||
provide () {
|
||||
return {
|
||||
NDropdown: this
|
||||
}
|
||||
},
|
||||
props: {
|
||||
...useTheme.props,
|
||||
...NPopover.props,
|
||||
...popoverProps,
|
||||
...dropdownProps
|
||||
},
|
||||
setup (props) {
|
||||
@ -92,32 +105,20 @@ export default defineComponent({
|
||||
toRef(props, 'show'),
|
||||
uncontrolledShowRef
|
||||
)
|
||||
const dataNeededRef = useFalseUntilTruthy(mergedShowRef)
|
||||
|
||||
// const dataNeededRef = useFalseUntilTruthy(mergedShowRef)
|
||||
const treemateRef = computed(() => {
|
||||
if (dataNeededRef.value) return TreeMate(props.options, treemateOptions)
|
||||
return null
|
||||
return TreeMate(props.options, treemateOptions)
|
||||
})
|
||||
const tmNodesRef = computed(() => {
|
||||
if (dataNeededRef.value) return treemateRef.value.treeNodes
|
||||
return null
|
||||
return treemateRef.value.treeNodes
|
||||
})
|
||||
const tmNodeMap = computed(() => {
|
||||
if (dataNeededRef.value) return treemateRef.value.treeNodeMap
|
||||
return null
|
||||
})
|
||||
const getPathRef = computed(() => {
|
||||
if (dataNeededRef.value) return treemateRef.value.getPath
|
||||
return null
|
||||
})
|
||||
const getFirstAvailableNodeRef = computed(() => {
|
||||
if (dataNeededRef.value) return treemateRef.value.getFirstAvailableNode
|
||||
return null
|
||||
const tmNodeMapRef = computed(() => {
|
||||
return treemateRef.value.treeNodeMap
|
||||
})
|
||||
|
||||
const hoverKeyRef = ref(null)
|
||||
const keyboardKeyRef = ref(null)
|
||||
const lastToggledSubmenuKeyRef = ref(null)
|
||||
const hoverKeyRef = ref<Key | null>(null)
|
||||
const keyboardKeyRef = ref<Key | null>(null)
|
||||
const lastToggledSubmenuKeyRef = ref<Key | null>(null)
|
||||
const pendingKeyRef = computed(() => {
|
||||
return (
|
||||
hoverKeyRef.value ??
|
||||
@ -126,74 +127,160 @@ export default defineComponent({
|
||||
null
|
||||
)
|
||||
})
|
||||
|
||||
const pendingKeyPathRef = computed(
|
||||
() => getPathRef.value(pendingKeyRef.value).keyPath
|
||||
() => treemateRef.value.getPath(pendingKeyRef.value).keyPath
|
||||
)
|
||||
|
||||
const activeKeyPathRef = computed(
|
||||
() => getPathRef.value(props.value).keyPath
|
||||
() => treemateRef.value.getPath(props.value).keyPath
|
||||
)
|
||||
|
||||
const keyboardEnabledRef = useMemo(() => {
|
||||
return props.keyboard && mergedShowRef.value
|
||||
})
|
||||
|
||||
const vm = getCurrentInstance().proxy
|
||||
useKeyboard(
|
||||
{
|
||||
keydown: {
|
||||
ArrowUp: {
|
||||
prevent: true,
|
||||
handler: () => vm.handleKeyDownUp()
|
||||
handler: handleKeyDownUp
|
||||
},
|
||||
ArrowRight: {
|
||||
prevent: true,
|
||||
handler: () => vm.handleKeyDownRight()
|
||||
handler: handleKeyDownRight
|
||||
},
|
||||
ArrowDown: {
|
||||
prevent: true,
|
||||
handler: () => vm.handleKeyDownDown()
|
||||
handler: handleKeyDownDown
|
||||
},
|
||||
ArrowLeft: {
|
||||
prevent: true,
|
||||
handler: () => vm.handleKeyDownLeft()
|
||||
handler: handleKeyDownLeft
|
||||
},
|
||||
Escape: () => vm.handleKeyDownEsc()
|
||||
Escape: handleKeyDownEsc
|
||||
},
|
||||
keyup: {
|
||||
Enter: () => vm.handleKeyUpEnter()
|
||||
Enter: handleKeyUpEnter
|
||||
}
|
||||
},
|
||||
keyboardEnabledRef
|
||||
)
|
||||
|
||||
const themeRef = useTheme(
|
||||
const themeRef = useTheme<DropdownThemeVars>(
|
||||
'Dropdown',
|
||||
'Dropdown',
|
||||
style,
|
||||
dropdownLight,
|
||||
props
|
||||
)
|
||||
|
||||
provide<DropdownInjection>(
|
||||
'NDropdown',
|
||||
reactive({
|
||||
hoverKey: hoverKeyRef,
|
||||
keyboardKey: keyboardKeyRef,
|
||||
lastToggledSubmenuKey: lastToggledSubmenuKeyRef,
|
||||
pendingKeyPath: pendingKeyPathRef,
|
||||
activeKeyPath: activeKeyPathRef,
|
||||
animated: toRef(props, 'animated'),
|
||||
mergedShow: mergedShowRef,
|
||||
doSelect,
|
||||
doUpdateShow
|
||||
})
|
||||
)
|
||||
// watch
|
||||
watch(mergedShowRef, (value) => {
|
||||
if (!value) clearPendingState()
|
||||
})
|
||||
// methods
|
||||
function doSelect (key: Key, node: RawNode) {
|
||||
const { onSelect } = props
|
||||
if (onSelect) call(onSelect, key, node)
|
||||
}
|
||||
function doUpdateShow (value: boolean) {
|
||||
const { 'onUpdate:show': onUpdateShow } = props
|
||||
if (onUpdateShow) call(onUpdateShow, value)
|
||||
uncontrolledShowRef.value = value
|
||||
}
|
||||
function clearPendingState () {
|
||||
hoverKeyRef.value = null
|
||||
keyboardKeyRef.value = null
|
||||
lastToggledSubmenuKeyRef.value = null
|
||||
}
|
||||
function handleKeyDownEsc () {
|
||||
doUpdateShow(false)
|
||||
}
|
||||
function handleKeyDownLeft () {
|
||||
handleKeyDown('left')
|
||||
}
|
||||
function handleKeyDownRight () {
|
||||
handleKeyDown('right')
|
||||
}
|
||||
function handleKeyDownUp () {
|
||||
handleKeyDown('up')
|
||||
}
|
||||
function handleKeyDownDown () {
|
||||
handleKeyDown('down')
|
||||
}
|
||||
function handleKeyUpEnter () {
|
||||
const pendingNode = getPendingNode()
|
||||
if (pendingNode && pendingNode.isLeaf) {
|
||||
doSelect(pendingNode.key, pendingNode.rawNode)
|
||||
doUpdateShow(false)
|
||||
}
|
||||
}
|
||||
function getPendingNode () {
|
||||
const { value: tmNodeMap } = tmNodeMapRef
|
||||
const { value: pendingKey } = pendingKeyRef
|
||||
if (!tmNodeMap || pendingKey === null) return null
|
||||
return tmNodeMap.get(pendingKey) ?? null
|
||||
}
|
||||
function handleKeyDown (direction: 'up' | 'right' | 'down' | 'left') {
|
||||
const { value: pendingKey } = pendingKeyRef
|
||||
const {
|
||||
value: { getFirstAvailableNode }
|
||||
} = treemateRef
|
||||
let nextKeyboardKey = null
|
||||
if (pendingKey === null) {
|
||||
const firstNode = getFirstAvailableNode()
|
||||
if (firstNode !== null) {
|
||||
nextKeyboardKey = firstNode.key
|
||||
}
|
||||
} else {
|
||||
const currentNode = getPendingNode()
|
||||
if (currentNode) {
|
||||
let nextNode
|
||||
switch (direction) {
|
||||
case 'down':
|
||||
nextNode = currentNode.getNext()
|
||||
break
|
||||
case 'up':
|
||||
nextNode = currentNode.getPrev()
|
||||
break
|
||||
case 'right':
|
||||
nextNode = currentNode.getChild()
|
||||
break
|
||||
case 'left':
|
||||
nextNode = currentNode.getParent()
|
||||
break
|
||||
}
|
||||
if (nextNode) nextKeyboardKey = nextNode.key
|
||||
}
|
||||
}
|
||||
if (nextKeyboardKey !== null) {
|
||||
hoverKeyRef.value = null
|
||||
keyboardKeyRef.value = nextKeyboardKey
|
||||
}
|
||||
}
|
||||
return {
|
||||
// data
|
||||
tm: treemateRef,
|
||||
tmNodes: tmNodesRef,
|
||||
tmNodeMap: tmNodeMap,
|
||||
// pending state
|
||||
pendingKeyPath: pendingKeyPathRef,
|
||||
hoverKey: hoverKeyRef,
|
||||
keyboardKey: keyboardKeyRef,
|
||||
lastToggledSubmenuKey: lastToggledSubmenuKeyRef,
|
||||
pendingKey: pendingKeyRef,
|
||||
keyboardHandlerRegistered: ref(false),
|
||||
// active state
|
||||
activeKeyPath: activeKeyPathRef,
|
||||
// show
|
||||
uncontrolledShow: uncontrolledShowRef,
|
||||
mergedShow: mergedShowRef,
|
||||
// methods
|
||||
getPath: getPathRef,
|
||||
getFirstAvailableNode: getFirstAvailableNodeRef,
|
||||
doUpdateShow,
|
||||
cssVars: computed(() => {
|
||||
const { size } = props
|
||||
const {
|
||||
@ -216,8 +303,7 @@ export default defineComponent({
|
||||
[createKey('optionPrefixWidth', size)]: optionPrefixWidth,
|
||||
[createKey('fontSize', size)]: fontSize,
|
||||
[createKey('optionHeight', size)]: optionHeight,
|
||||
[createKey('optionIconSize', size)]: optionIconSize,
|
||||
[createKey('groupHeaderFontSize', size)]: groupHeaderFontSize
|
||||
[createKey('optionIconSize', size)]: optionIconSize
|
||||
}
|
||||
} = themeRef.value
|
||||
return {
|
||||
@ -239,93 +325,11 @@ export default defineComponent({
|
||||
'--prefix-color': prefixColor,
|
||||
'--suffix-color': suffixColor,
|
||||
'--group-header-text-color': groupHeaderTextColor,
|
||||
'--group-header-font-size': groupHeaderFontSize,
|
||||
'--option-icon-size': optionIconSize
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
mergedShow (value) {
|
||||
if (!value) this.clearPendingState()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
doSelect (...args) {
|
||||
const { onSelect } = this
|
||||
if (onSelect) call(onSelect, ...args)
|
||||
},
|
||||
doUpdateShow (value) {
|
||||
const { onUpdateShow } = this
|
||||
if (onUpdateShow) call(onUpdateShow, value)
|
||||
this.uncontrolledShow = value
|
||||
},
|
||||
clearPendingState () {
|
||||
this.hoverKey = null
|
||||
this.keyboardKey = null
|
||||
this.lastToggledSubmenuKey = null
|
||||
},
|
||||
handleKeyDownEsc () {
|
||||
this.doUpdateShow(false)
|
||||
},
|
||||
handleKeyDownLeft () {
|
||||
this.handleKeyDown('left')
|
||||
},
|
||||
handleKeyDownRight () {
|
||||
this.handleKeyDown('right')
|
||||
},
|
||||
handleKeyDownUp () {
|
||||
this.handleKeyDown('up')
|
||||
},
|
||||
handleKeyDownDown () {
|
||||
this.handleKeyDown('down')
|
||||
},
|
||||
handleKeyUpEnter () {
|
||||
const pendingNode = this.getPendingNode()
|
||||
if (pendingNode && pendingNode.isLeaf) {
|
||||
this.doSelect(pendingNode.key)
|
||||
this.doUpdateShow(false)
|
||||
}
|
||||
},
|
||||
getPendingNode () {
|
||||
const { pendingKey, tmNodeMap } = this
|
||||
return tmNodeMap.get(pendingKey) ?? null
|
||||
},
|
||||
handleKeyDown (direction) {
|
||||
const { pendingKey, getFirstAvailableNode } = this
|
||||
let nextKeyboardKey = null
|
||||
if (pendingKey === null) {
|
||||
const firstNode = getFirstAvailableNode()
|
||||
if (firstNode !== null) {
|
||||
nextKeyboardKey = firstNode.key
|
||||
}
|
||||
} else {
|
||||
const currentNode = this.getPendingNode()
|
||||
if (currentNode) {
|
||||
let nextNode
|
||||
switch (direction) {
|
||||
case 'down':
|
||||
nextNode = currentNode.getNext()
|
||||
break
|
||||
case 'up':
|
||||
nextNode = currentNode.getPrev()
|
||||
break
|
||||
case 'right':
|
||||
nextNode = currentNode.getChild()
|
||||
break
|
||||
case 'left':
|
||||
nextNode = currentNode.getParent()
|
||||
break
|
||||
}
|
||||
if (nextNode) nextKeyboardKey = nextNode.key
|
||||
}
|
||||
}
|
||||
if (nextKeyboardKey !== null) {
|
||||
this.hoverKey = null
|
||||
this.keyboardKey = nextKeyboardKey
|
||||
}
|
||||
}
|
||||
},
|
||||
render () {
|
||||
return h(
|
||||
NPopover,
|
@ -2,7 +2,7 @@
|
||||
<div class="n-dropdown-divider" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { defineComponent, Fragment, h } from 'vue'
|
||||
import { defineComponent, Fragment, h, PropType, VNode } from 'vue'
|
||||
import { TreeNode } from 'treemate'
|
||||
import { warn } from '../../_utils'
|
||||
import NDropdownOption from './DropdownOption'
|
||||
import NDropdownDivider from './DropdownDivider.vue'
|
||||
@ -9,7 +10,7 @@ export default defineComponent({
|
||||
name: 'NDropdownGroup',
|
||||
props: {
|
||||
tmNode: {
|
||||
type: Object,
|
||||
type: Object as PropType<TreeNode>,
|
||||
required: true
|
||||
},
|
||||
parentKey: {
|
||||
@ -28,7 +29,7 @@ export default defineComponent({
|
||||
key: tmNode.key
|
||||
})
|
||||
].concat(
|
||||
children.map((child) => {
|
||||
children?.map((child) => {
|
||||
if (isDividerNode(child.rawNode)) {
|
||||
return h(NDropdownDivider, {
|
||||
key: child.key
|
||||
@ -46,7 +47,7 @@ export default defineComponent({
|
||||
parentKey,
|
||||
key: child.key
|
||||
})
|
||||
})
|
||||
}) as VNode[]
|
||||
)
|
||||
)
|
||||
}
|
@ -1,15 +1,22 @@
|
||||
import { defineComponent, h } from 'vue'
|
||||
import { defineComponent, h, inject } from 'vue'
|
||||
import { render } from '../../_utils'
|
||||
import { NDropdownMenuInjection } from './DropdownMenu'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'DropdownGroupHeader',
|
||||
inject: ['NDropdown', 'NDropdownMenu'],
|
||||
props: {
|
||||
tmNode: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
setup () {
|
||||
return {
|
||||
NDropdownMenu: inject<NDropdownMenuInjection>(
|
||||
'NDropdownMenu'
|
||||
) as NDropdownMenuInjection
|
||||
}
|
||||
},
|
||||
render () {
|
||||
const { rawNode } = this.tmNode
|
||||
return h(
|
@ -1,20 +1,20 @@
|
||||
import { defineComponent, h } from 'vue'
|
||||
import { computed, defineComponent, h, PropType, provide, reactive } from 'vue'
|
||||
import { TreeNode } from 'treemate'
|
||||
import NDropdownOption from './DropdownOption'
|
||||
import NDropdownDivider from './DropdownDivider.vue'
|
||||
import NDropdownGroup from './DropdownGroup'
|
||||
import { isSubmenuNode, isGroupNode, isDividerNode } from './utils'
|
||||
|
||||
export interface NDropdownMenuInjection {
|
||||
showIcon: boolean
|
||||
hasSubmenu: boolean
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'DropdownMenu',
|
||||
inject: ['NDropdown'],
|
||||
provide () {
|
||||
return {
|
||||
NDropdownMenu: this
|
||||
}
|
||||
},
|
||||
props: {
|
||||
tmNodes: {
|
||||
type: Array,
|
||||
type: Array as PropType<TreeNode[]>,
|
||||
required: true
|
||||
},
|
||||
parentKey: {
|
||||
@ -22,25 +22,32 @@ export default defineComponent({
|
||||
default: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
showIcon () {
|
||||
return this.tmNodes.some((tmNode) => {
|
||||
const { rawNode } = tmNode
|
||||
if (isGroupNode(rawNode)) {
|
||||
return !!rawNode.children?.some((rawChild) => rawChild.icon)
|
||||
}
|
||||
return rawNode.icon
|
||||
setup (props) {
|
||||
provide<NDropdownMenuInjection>(
|
||||
'NDropdownMenu',
|
||||
reactive({
|
||||
showIcon: computed(() => {
|
||||
return props.tmNodes.some((tmNode) => {
|
||||
const { rawNode } = tmNode
|
||||
if (isGroupNode(rawNode)) {
|
||||
return !!rawNode.children?.some((rawChild) => rawChild.icon)
|
||||
}
|
||||
return rawNode.icon
|
||||
})
|
||||
}),
|
||||
hasSubmenu: computed(() => {
|
||||
return props.tmNodes.some((tmNode) => {
|
||||
const { rawNode } = tmNode
|
||||
if (isGroupNode(rawNode)) {
|
||||
return !!rawNode.children?.some((rawChild) =>
|
||||
isSubmenuNode(rawChild)
|
||||
)
|
||||
}
|
||||
return isSubmenuNode(rawNode)
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
hasSubmenu () {
|
||||
return this.tmNodes.some((tmNode) => {
|
||||
const { rawNode } = tmNode
|
||||
if (isGroupNode(rawNode)) {
|
||||
return !!rawNode.children?.some((rawChild) => isSubmenuNode(rawChild))
|
||||
}
|
||||
return isSubmenuNode(rawNode)
|
||||
})
|
||||
}
|
||||
)
|
||||
},
|
||||
render () {
|
||||
const { parentKey } = this
|
@ -1,21 +1,30 @@
|
||||
import { h, computed, inject, ref, Transition, defineComponent } from 'vue'
|
||||
import { VBinder, VTarget, VFollower } from 'vueuc'
|
||||
import {
|
||||
h,
|
||||
computed,
|
||||
inject,
|
||||
ref,
|
||||
Transition,
|
||||
defineComponent,
|
||||
provide,
|
||||
reactive,
|
||||
PropType
|
||||
} from 'vue'
|
||||
import { VBinder, VTarget, VFollower, FollowerPlacement } from 'vueuc'
|
||||
import { useMemo } from 'vooks'
|
||||
import { ChevronRightIcon } from '../../_base/icons'
|
||||
import { useDeferredTrue } from '../../_utils/composable'
|
||||
import { render } from '../../_utils'
|
||||
import { NIcon } from '../../icon'
|
||||
import NDropdownMenu, { NDropdownMenuInjection } from './DropdownMenu'
|
||||
import { DropdownInjection } from './Dropdown'
|
||||
import { isSubmenuNode } from './utils'
|
||||
import NDropdownMenu from './DropdownMenu'
|
||||
|
||||
interface NDropdownOptionInjection {
|
||||
enteringSubmenu: boolean
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'DropdownOption',
|
||||
provide () {
|
||||
return {
|
||||
NDropdownOption: this
|
||||
}
|
||||
},
|
||||
inject: ['NDropdown', 'NDropdownMenu'],
|
||||
props: {
|
||||
tmNode: {
|
||||
type: Object,
|
||||
@ -26,13 +35,21 @@ export default defineComponent({
|
||||
default: null
|
||||
},
|
||||
placement: {
|
||||
type: String,
|
||||
type: String as PropType<FollowerPlacement>,
|
||||
default: 'right-start'
|
||||
}
|
||||
},
|
||||
setup (props) {
|
||||
const NDropdown = inject('NDropdown')
|
||||
const NDropdownOption = inject('NDropdownOption', null)
|
||||
const NDropdown = inject<DropdownInjection>(
|
||||
'NDropdown'
|
||||
) as DropdownInjection
|
||||
const NDropdownOption = inject<NDropdownOptionInjection | null>(
|
||||
'NDropdownOption',
|
||||
null
|
||||
)
|
||||
const NDropdownMenu = inject<NDropdownMenuInjection>(
|
||||
'NDropdownMenu'
|
||||
) as NDropdownMenuInjection
|
||||
const rawNodeRef = computed(() => props.tmNode.rawNode)
|
||||
const hasSubmenuRef = computed(() => {
|
||||
return isSubmenuNode(props.tmNode.rawNode)
|
||||
@ -67,8 +84,55 @@ export default defineComponent({
|
||||
const parentEnteringSubmenuRef = computed(() => {
|
||||
return !!(NDropdownOption && NDropdownOption.enteringSubmenu)
|
||||
})
|
||||
const enteringSubmenuRef = ref(false)
|
||||
provide<NDropdownOptionInjection>(
|
||||
'NDropdownOption',
|
||||
reactive({
|
||||
enteringSubmenu: enteringSubmenuRef
|
||||
})
|
||||
)
|
||||
// methods
|
||||
function handleSubmenuBeforeEnter () {
|
||||
enteringSubmenuRef.value = true
|
||||
}
|
||||
function handleSubmenuAfterEnter () {
|
||||
enteringSubmenuRef.value = false
|
||||
}
|
||||
function handleMouseEnter () {
|
||||
const { parentKey, tmNode } = props
|
||||
if (!NDropdown.mergedShow) return
|
||||
NDropdown.lastToggledSubmenuKey = parentKey
|
||||
NDropdown.keyboardKey = null
|
||||
NDropdown.hoverKey = tmNode.key
|
||||
}
|
||||
function handleMouseMove () {
|
||||
const { tmNode } = props
|
||||
if (!NDropdown.mergedShow) return
|
||||
if (NDropdown.hoverKey === tmNode.key) return
|
||||
handleMouseEnter()
|
||||
}
|
||||
function handleMouseLeave (e: MouseEvent) {
|
||||
if (!NDropdown.mergedShow) return
|
||||
const { relatedTarget } = e
|
||||
if (
|
||||
relatedTarget &&
|
||||
!(relatedTarget as HTMLElement).getAttribute('n-dropdown-option')
|
||||
) {
|
||||
NDropdown.hoverKey = null
|
||||
}
|
||||
}
|
||||
function handleClick () {
|
||||
const { value: hasSubmenu } = hasSubmenuRef
|
||||
const { tmNode } = props
|
||||
if (!NDropdown.mergedShow) return
|
||||
if (!hasSubmenu && !tmNode.disabled) {
|
||||
NDropdown.doSelect(tmNode.key, tmNode.r)
|
||||
NDropdown.doUpdateShow(false)
|
||||
}
|
||||
}
|
||||
return {
|
||||
enteringSubmenu: ref(false),
|
||||
NDropdown,
|
||||
NDropdownMenu,
|
||||
mergedShowSubmenu: computed(() => {
|
||||
return delayedSubmenuRef.value && !parentEnteringSubmenuRef.value
|
||||
}),
|
||||
@ -84,44 +148,12 @@ export default defineComponent({
|
||||
const { key } = props.tmNode
|
||||
return activeKeyPath.includes(key)
|
||||
}),
|
||||
NDropdownOption
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleSubmenuBeforeEnter () {
|
||||
this.enteringSubmenu = true
|
||||
},
|
||||
handleSubmenuAfterEnter () {
|
||||
this.enteringSubmenu = false
|
||||
},
|
||||
handleMouseEnter () {
|
||||
const { NDropdown, parentKey, tmNode } = this
|
||||
if (!NDropdown.mergedShow) return
|
||||
NDropdown.lastToggledSubmenuKey = parentKey
|
||||
NDropdown.keyboardKey = null
|
||||
NDropdown.hoverKey = tmNode.key
|
||||
},
|
||||
handleMouseMove (e) {
|
||||
const { NDropdown, tmNode } = this
|
||||
if (!NDropdown.mergedShow) return
|
||||
if (NDropdown.hoverKey === tmNode.key) return
|
||||
this.handleMouseEnter(e)
|
||||
},
|
||||
handleMouseLeave (e) {
|
||||
const { NDropdown } = this
|
||||
if (!NDropdown.mergedShow) return
|
||||
const { relatedTarget } = e
|
||||
if (relatedTarget && !relatedTarget.getAttribute('n-dropdown-option')) {
|
||||
NDropdown.hoverKey = null
|
||||
}
|
||||
},
|
||||
handleClick () {
|
||||
const { NDropdown, hasSubmenu, tmNode } = this
|
||||
if (!NDropdown.mergedShow) return
|
||||
if (!hasSubmenu && !tmNode.disabled) {
|
||||
NDropdown.doSelect(tmNode.key)
|
||||
NDropdown.doUpdateShow(false)
|
||||
}
|
||||
handleClick,
|
||||
handleMouseMove,
|
||||
handleMouseEnter,
|
||||
handleMouseLeave,
|
||||
handleSubmenuBeforeEnter,
|
||||
handleSubmenuAfterEnter
|
||||
}
|
||||
},
|
||||
render () {
|
@ -1,130 +0,0 @@
|
||||
import { cB, cM, cE } from '../../../_utils/cssr'
|
||||
import fadeInScaleUpTransition from '../../../_styles/transitions/fade-in-scale-up'
|
||||
|
||||
// vars:
|
||||
// --bezier
|
||||
// --font-size
|
||||
// --option-color-hover
|
||||
// --divider-color
|
||||
// --color
|
||||
// --padding
|
||||
// --border-radius
|
||||
// --box-shadow
|
||||
// --option-height
|
||||
// --option-prefix-width
|
||||
// --option-icon-prefix-width
|
||||
// --option-suffix-width
|
||||
// --option-icon-suffix-width
|
||||
// --option-text-color
|
||||
// --option-text-color-active
|
||||
// --prefix-color
|
||||
// --suffix-color
|
||||
// --option-icon-size
|
||||
export default cB('dropdown-menu', {
|
||||
transformOrigin: 'inherit',
|
||||
padding: 'var(--padding)',
|
||||
backgroundColor: 'var(--color)',
|
||||
borderRadius: 'var(--border-radius)',
|
||||
boxShadow: 'var(--box-shadow)',
|
||||
transition: `
|
||||
background-color .3s var(--bezier),
|
||||
box-shadow .3s var(--bezier)
|
||||
`
|
||||
}, [
|
||||
fadeInScaleUpTransition(),
|
||||
cB('dropdown-option', {
|
||||
position: 'relative'
|
||||
}, [
|
||||
cB('dropdown-option-body', {
|
||||
display: 'flex',
|
||||
cursor: 'default',
|
||||
height: 'var(--option-height)',
|
||||
lineHeight: 'var(--option-height)',
|
||||
fontSize: 'var(--font-size)',
|
||||
color: 'var(--option-text-color)',
|
||||
transition: 'color .3s var(--bezier)'
|
||||
}, [
|
||||
cM('pending', {
|
||||
backgroundColor: 'var(--option-color-hover)'
|
||||
}),
|
||||
cM('active', {
|
||||
color: 'var(--option-text-color-active)'
|
||||
}, [
|
||||
cE('prefix, suffix', {
|
||||
color: 'var(--option-text-color-active)'
|
||||
})
|
||||
]),
|
||||
cM('group', {
|
||||
fontSize: 'calc(var(--font-size) - 1px)',
|
||||
color: 'var(--group-header-text-color)'
|
||||
}, [
|
||||
cE('prefix', {
|
||||
width: 'calc(var(--option-prefix-width) / 2)'
|
||||
}, [
|
||||
cM('show-icon', {
|
||||
width: 'calc(var(--option-icon-prefix-width) / 2)'
|
||||
})
|
||||
])
|
||||
]),
|
||||
cE('prefix', {
|
||||
width: 'var(--option-prefix-width)',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
color: 'var(--prefix-color)',
|
||||
transition: 'color .3s var(--bezier)'
|
||||
}, [
|
||||
cM('show-icon', {
|
||||
width: 'var(--option-icon-prefix-width)'
|
||||
}),
|
||||
cB('icon', {
|
||||
fontSize: 'var(--option-icon-size)'
|
||||
})
|
||||
]),
|
||||
cE('label', {
|
||||
whiteSpace: 'nowrap',
|
||||
flex: 1
|
||||
}),
|
||||
cE('suffix', {
|
||||
boxSizing: 'border-box',
|
||||
flexGrow: 0,
|
||||
flexShrink: 0,
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
minWidth: 'var(--option-suffix-width)',
|
||||
padding: '0 8px',
|
||||
transition: 'color .3s var(--bezier)',
|
||||
color: 'var(--suffix-color)'
|
||||
}, [
|
||||
cM('has-submenu', {
|
||||
width: 'var(--option-icon-suffix-width)'
|
||||
}),
|
||||
cB('icon', {
|
||||
fontSize: 'var(--option-icon-size)'
|
||||
})
|
||||
]),
|
||||
cB('dropdown-menu', {
|
||||
pointerEvents: 'all'
|
||||
})
|
||||
]),
|
||||
cB('dropdown-offset-container', {
|
||||
pointerEvents: 'none',
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: '-4px',
|
||||
bottom: '-4px'
|
||||
})
|
||||
]),
|
||||
cB('dropdown-divider', {
|
||||
transition: 'background-color .3s var(--bezier)',
|
||||
backgroundColor: 'var(--divider-color)',
|
||||
height: '1px',
|
||||
margin: '4px 0'
|
||||
}),
|
||||
cB('dropdown-menu-wrapper', {
|
||||
transformOrigin: 'inherit',
|
||||
width: 'fit-content'
|
||||
})
|
||||
])
|
162
src/dropdown/src/styles/index.cssr.ts
Normal file
162
src/dropdown/src/styles/index.cssr.ts
Normal file
@ -0,0 +1,162 @@
|
||||
import { cB, cM, cE } from '../../../_utils/cssr'
|
||||
import fadeInScaleUpTransition from '../../../_styles/transitions/fade-in-scale-up'
|
||||
|
||||
// vars:
|
||||
// --bezier
|
||||
// --font-size
|
||||
// --option-color-hover
|
||||
// --divider-color
|
||||
// --color
|
||||
// --padding
|
||||
// --border-radius
|
||||
// --box-shadow
|
||||
// --option-height
|
||||
// --option-prefix-width
|
||||
// --option-icon-prefix-width
|
||||
// --option-suffix-width
|
||||
// --option-icon-suffix-width
|
||||
// --option-text-color
|
||||
// --option-text-color-active
|
||||
// --prefix-color
|
||||
// --suffix-color
|
||||
// --option-icon-size
|
||||
export default cB(
|
||||
'dropdown-menu',
|
||||
{
|
||||
transformOrigin: 'inherit',
|
||||
padding: 'var(--padding)',
|
||||
backgroundColor: 'var(--color)',
|
||||
borderRadius: 'var(--border-radius)',
|
||||
boxShadow: 'var(--box-shadow)',
|
||||
transition: `
|
||||
background-color .3s var(--bezier),
|
||||
box-shadow .3s var(--bezier)
|
||||
`
|
||||
},
|
||||
[
|
||||
fadeInScaleUpTransition(),
|
||||
cB(
|
||||
'dropdown-option',
|
||||
{
|
||||
position: 'relative'
|
||||
},
|
||||
[
|
||||
cB(
|
||||
'dropdown-option-body',
|
||||
{
|
||||
display: 'flex',
|
||||
cursor: 'default',
|
||||
height: 'var(--option-height)',
|
||||
lineHeight: 'var(--option-height)',
|
||||
fontSize: 'var(--font-size)',
|
||||
color: 'var(--option-text-color)',
|
||||
transition: 'color .3s var(--bezier)'
|
||||
},
|
||||
[
|
||||
cM('pending', {
|
||||
backgroundColor: 'var(--option-color-hover)'
|
||||
}),
|
||||
cM(
|
||||
'active',
|
||||
{
|
||||
color: 'var(--option-text-color-active)'
|
||||
},
|
||||
[
|
||||
cE('prefix, suffix', {
|
||||
color: 'var(--option-text-color-active)'
|
||||
})
|
||||
]
|
||||
),
|
||||
cM(
|
||||
'group',
|
||||
{
|
||||
fontSize: 'calc(var(--font-size) - 1px)',
|
||||
color: 'var(--group-header-text-color)'
|
||||
},
|
||||
[
|
||||
cE(
|
||||
'prefix',
|
||||
{
|
||||
width: 'calc(var(--option-prefix-width) / 2)'
|
||||
},
|
||||
[
|
||||
cM('show-icon', {
|
||||
width: 'calc(var(--option-icon-prefix-width) / 2)'
|
||||
})
|
||||
]
|
||||
)
|
||||
]
|
||||
),
|
||||
cE(
|
||||
'prefix',
|
||||
{
|
||||
width: 'var(--option-prefix-width)',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
color: 'var(--prefix-color)',
|
||||
transition: 'color .3s var(--bezier)'
|
||||
},
|
||||
[
|
||||
cM('show-icon', {
|
||||
width: 'var(--option-icon-prefix-width)'
|
||||
}),
|
||||
cB('icon', {
|
||||
fontSize: 'var(--option-icon-size)'
|
||||
})
|
||||
]
|
||||
),
|
||||
cE('label', {
|
||||
whiteSpace: 'nowrap',
|
||||
flex: 1
|
||||
}),
|
||||
cE(
|
||||
'suffix',
|
||||
{
|
||||
boxSizing: 'border-box',
|
||||
flexGrow: 0,
|
||||
flexShrink: 0,
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
minWidth: 'var(--option-suffix-width)',
|
||||
padding: '0 8px',
|
||||
transition: 'color .3s var(--bezier)',
|
||||
color: 'var(--suffix-color)'
|
||||
},
|
||||
[
|
||||
cM('has-submenu', {
|
||||
width: 'var(--option-icon-suffix-width)'
|
||||
}),
|
||||
cB('icon', {
|
||||
fontSize: 'var(--option-icon-size)'
|
||||
})
|
||||
]
|
||||
),
|
||||
cB('dropdown-menu', {
|
||||
pointerEvents: 'all'
|
||||
})
|
||||
]
|
||||
),
|
||||
cB('dropdown-offset-container', {
|
||||
pointerEvents: 'none',
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: '-4px',
|
||||
bottom: '-4px'
|
||||
})
|
||||
]
|
||||
),
|
||||
cB('dropdown-divider', {
|
||||
transition: 'background-color .3s var(--bezier)',
|
||||
backgroundColor: 'var(--divider-color)',
|
||||
height: '1px',
|
||||
margin: '4px 0'
|
||||
}),
|
||||
cB('dropdown-menu-wrapper', {
|
||||
transformOrigin: 'inherit',
|
||||
width: 'fit-content'
|
||||
})
|
||||
]
|
||||
)
|
@ -1,14 +0,0 @@
|
||||
export function isSubmenuNode (rawNode) {
|
||||
return (
|
||||
rawNode.type === 'submenu' ||
|
||||
(rawNode.type === undefined && rawNode.children)
|
||||
)
|
||||
}
|
||||
|
||||
export function isGroupNode (rawNode) {
|
||||
return rawNode.type === 'group'
|
||||
}
|
||||
|
||||
export function isDividerNode (rawNode) {
|
||||
return rawNode.type === 'divider'
|
||||
}
|
16
src/dropdown/src/utils.ts
Normal file
16
src/dropdown/src/utils.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { RawNode } from 'treemate'
|
||||
|
||||
export function isSubmenuNode (rawNode: RawNode): boolean {
|
||||
return (
|
||||
rawNode.type === 'submenu' ||
|
||||
(rawNode.type === undefined && rawNode.children !== undefined)
|
||||
)
|
||||
}
|
||||
|
||||
export function isGroupNode (rawNode: RawNode): boolean {
|
||||
return rawNode.type === 'group'
|
||||
}
|
||||
|
||||
export function isDividerNode (rawNode: RawNode): boolean {
|
||||
return rawNode.type === 'divider'
|
||||
}
|
@ -1,10 +1,12 @@
|
||||
import { commonDark } from '../../_styles/new-common'
|
||||
import type { ThemeCommonVars } from '../../_styles/new-common'
|
||||
import type { DropdownThemeVars } from './light'
|
||||
import commonVariables from './_common'
|
||||
|
||||
export default {
|
||||
name: 'Dropdown',
|
||||
common: commonDark,
|
||||
self (vars) {
|
||||
self (vars: ThemeCommonVars): DropdownThemeVars {
|
||||
const {
|
||||
primaryColor,
|
||||
textColor2,
|
@ -1,2 +0,0 @@
|
||||
export { default as dropdownDark } from './dark.js'
|
||||
export { default as dropdownLight } from './light.js'
|
3
src/dropdown/styles/index.ts
Normal file
3
src/dropdown/styles/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export { default as dropdownDark } from './dark'
|
||||
export { default as dropdownLight } from './light'
|
||||
export type { DropdownThemeVars } from './light'
|
@ -1,10 +1,11 @@
|
||||
import commonVariables from './_common'
|
||||
import { commonLight } from '../../_styles/new-common'
|
||||
import type { ThemeCommonVars } from '../../_styles/new-common'
|
||||
import commonVariables from './_common'
|
||||
|
||||
export default {
|
||||
const dropdownLight = {
|
||||
name: 'Dropdown',
|
||||
common: commonLight,
|
||||
self (vars) {
|
||||
self (vars: ThemeCommonVars) {
|
||||
const {
|
||||
primaryColor,
|
||||
textColor2,
|
||||
@ -46,3 +47,6 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default dropdownLight
|
||||
export type DropdownThemeVars = ReturnType<typeof dropdownLight.self>
|
Loading…
Reference in New Issue
Block a user