refactor(dropdown): ts

This commit is contained in:
07akioni 2021-01-15 01:17:20 +08:00
parent 88db9bf48f
commit 2eda4312ae
17 changed files with 479 additions and 387 deletions

View File

@ -1,2 +0,0 @@
/* istanbul ignore file */
export { default as NDropdown } from './src/Dropdown.js'

2
src/dropdown/index.ts Normal file
View File

@ -0,0 +1,2 @@
/* istanbul ignore file */
export { default as NDropdown } from './src/Dropdown'

View File

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

View File

@ -2,7 +2,7 @@
<div class="n-dropdown-divider" />
</template>
<script>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({

View File

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

View File

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

View File

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

View File

@ -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 () {

View File

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

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

View File

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

View File

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

View File

@ -1,2 +0,0 @@
export { default as dropdownDark } from './dark.js'
export { default as dropdownLight } from './light.js'

View File

@ -0,0 +1,3 @@
export { default as dropdownDark } from './dark'
export { default as dropdownLight } from './light'
export type { DropdownThemeVars } from './light'

View File

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