feat: add slot-listener for dropdown and others
This commit is contained in:
@@ -1,18 +1,24 @@
|
||||
<template>
|
||||
<div class="inline-flex relative" ref="wrapper">
|
||||
<div class="inline-flex items-center">
|
||||
<slot name="trigger" :show="onShow" :hide="onHide" :toggle="onToggle">
|
||||
<Button @click="onToggle">
|
||||
{{ text }}
|
||||
<template #suffix>
|
||||
<svg class="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>
|
||||
</template>
|
||||
</Button>
|
||||
</slot>
|
||||
<slot-listener @click="onToggle">
|
||||
<slot name="trigger">
|
||||
<Button>
|
||||
{{ text }}
|
||||
<template #suffix>
|
||||
<svg class="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||
</svg>
|
||||
</template>
|
||||
</Button>
|
||||
</slot>
|
||||
</slot-listener>
|
||||
|
||||
</div>
|
||||
<transition :name="transitionName">
|
||||
<div ref="content" v-if="visible" :style="contentStyles" :class="[contentClasses]">
|
||||
<slot />
|
||||
<slot/>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
@@ -24,6 +30,7 @@ import type { DropdownPlacement } from './types'
|
||||
import { useDropdownClasses } from './composables/useDropdownClasses'
|
||||
import Button from '../Button/Button.vue'
|
||||
import { onClickOutside } from '@vueuse/core'
|
||||
import SlotListener from '@/components/utils/SlotListener/SlotListener.vue'
|
||||
|
||||
const visible = ref(false)
|
||||
|
||||
@@ -37,7 +44,7 @@ const props = defineProps({
|
||||
default: 'bottom',
|
||||
},
|
||||
text: {
|
||||
type: String ,
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
transition: {
|
||||
@@ -54,7 +61,7 @@ const placementTransitionMap: Record<DropdownPlacement, string> = {
|
||||
}
|
||||
|
||||
const transitionName = computed(() => {
|
||||
if(props.transition === null) return placementTransitionMap[props.placement]
|
||||
if (props.transition === null) return placementTransitionMap[props.placement]
|
||||
return props.transition
|
||||
})
|
||||
|
||||
@@ -68,7 +75,7 @@ const { contentClasses, contentStyles } = useDropdownClasses({
|
||||
})
|
||||
|
||||
onClickOutside(wrapper, () => {
|
||||
if(!visible.value) return
|
||||
if (!visible.value) return
|
||||
visible.value = false
|
||||
})
|
||||
</script>
|
||||
|
||||
132
src/components/utils/SlotListener/SlotListener.vue
Normal file
132
src/components/utils/SlotListener/SlotListener.vue
Normal file
@@ -0,0 +1,132 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import type { VNode, PropType } from 'vue'
|
||||
import type { SlotListenerTrigger, TriggerEventHandlers } from '@/components/utils/SlotListener/types'
|
||||
import { getFirstSlotVNode } from '@/utils/getFirstSlotNode'
|
||||
import pick from 'lodash/pick'
|
||||
|
||||
// inspired from https://github.com/TuSimple/naive-ui/blob/main/src/popover/src/Popover.tsx
|
||||
|
||||
const triggerEventMap: Record<SlotListenerTrigger, string[]> = {
|
||||
focus: ['onFocus', 'onBlur'],
|
||||
click: ['onClick'],
|
||||
hover: ['onMouseenter', 'onMouseleave'],
|
||||
}
|
||||
|
||||
function appendEvents(
|
||||
vNode: VNode,
|
||||
events: TriggerEventHandlers,
|
||||
): void {
|
||||
Object.entries(triggerEventMap).forEach(([, eventNames]) => {
|
||||
eventNames.forEach((eventName) => {
|
||||
if (!vNode.props) vNode.props = {}
|
||||
else {
|
||||
vNode.props = Object.assign({}, vNode.props)
|
||||
}
|
||||
const originalHandler = vNode.props[eventName]
|
||||
const handler = events[eventName as keyof typeof events]
|
||||
if (!originalHandler) vNode.props[eventName] = handler
|
||||
else {
|
||||
vNode.props[eventName] = (...args: unknown[]) => {
|
||||
originalHandler(...args)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
;(handler as any)(...args)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'SlotListener',
|
||||
emits: ['click', 'focus', 'blur', 'mouseenter', 'mouseleave'],
|
||||
props: {
|
||||
trigger: {
|
||||
type: String as PropType<SlotListenerTrigger>,
|
||||
default: 'click',
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const handleFocus = (e: FocusEvent) => {
|
||||
emit('focus', e)
|
||||
}
|
||||
const handleBlur = (e: FocusEvent) => {
|
||||
emit('blur', e)
|
||||
}
|
||||
const handleClick = (e: MouseEvent) => {
|
||||
emit('click', e)
|
||||
}
|
||||
const handleMouseEnter = (e: MouseEvent) => {
|
||||
emit('mouseenter', e)
|
||||
}
|
||||
const handleMouseLeave = (e: MouseEvent) => {
|
||||
emit('mouseleave', e)
|
||||
}
|
||||
|
||||
return {
|
||||
handleClick,
|
||||
handleBlur,
|
||||
handleFocus,
|
||||
handleMouseLeave,
|
||||
handleMouseEnter,
|
||||
}
|
||||
},
|
||||
render() {
|
||||
const {
|
||||
$slots,
|
||||
} = this
|
||||
|
||||
const handlers = {
|
||||
onClick: this.handleClick,
|
||||
onMouseenter: this.handleMouseEnter,
|
||||
onMouseleave: this.handleMouseLeave,
|
||||
onFocus: this.handleFocus,
|
||||
onBlur: this.handleBlur,
|
||||
}
|
||||
|
||||
let triggerVNode = getFirstSlotVNode($slots, 'default')
|
||||
|
||||
const ascendantAndCurrentHandlers: TriggerEventHandlers[] = [
|
||||
handlers,
|
||||
]
|
||||
if (triggerVNode?.props)
|
||||
ascendantAndCurrentHandlers.push(pick(triggerVNode.props, 'onClick', 'onMouseenter', 'onMouseleave', 'onFocus', 'onBlur'))
|
||||
|
||||
const mergedHandlers: TriggerEventHandlers = {
|
||||
onBlur: (e: FocusEvent) => {
|
||||
ascendantAndCurrentHandlers.forEach((_handlers) => {
|
||||
_handlers?.onBlur?.(e)
|
||||
})
|
||||
},
|
||||
onFocus: (e: FocusEvent) => {
|
||||
ascendantAndCurrentHandlers.forEach((_handlers) => {
|
||||
_handlers?.onFocus?.(e)
|
||||
})
|
||||
},
|
||||
onClick: (e: MouseEvent) => {
|
||||
ascendantAndCurrentHandlers.forEach((_handlers) => {
|
||||
_handlers?.onClick?.(e)
|
||||
})
|
||||
},
|
||||
onMouseenter: (e: MouseEvent) => {
|
||||
ascendantAndCurrentHandlers.forEach((_handlers) => {
|
||||
_handlers?.onMouseenter?.(e)
|
||||
})
|
||||
},
|
||||
onMouseleave: (e: MouseEvent) => {
|
||||
ascendantAndCurrentHandlers.forEach((_handlers) => {
|
||||
_handlers?.onMouseleave?.(e)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
if (triggerVNode)
|
||||
appendEvents(
|
||||
triggerVNode,
|
||||
mergedHandlers,
|
||||
)
|
||||
|
||||
return triggerVNode
|
||||
},
|
||||
})
|
||||
</script>
|
||||
10
src/components/utils/SlotListener/types.ts
Normal file
10
src/components/utils/SlotListener/types.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export type SlotListenerTrigger = 'click' | 'focus' | 'hover'
|
||||
|
||||
|
||||
export type TriggerEventHandlers = {
|
||||
onClick: (e: MouseEvent) => void
|
||||
onMouseenter: (e: MouseEvent) => void
|
||||
onMouseleave: (e: MouseEvent) => void
|
||||
onFocus: (e: FocusEvent) => void
|
||||
onBlur: (e: FocusEvent) => void
|
||||
}
|
||||
Reference in New Issue
Block a user