This commit is contained in:
95
app/components/AppDropdown.vue
Normal file
95
app/components/AppDropdown.vue
Normal file
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div class="relative">
|
||||
<div ref="triggerRef">
|
||||
<slot />
|
||||
</div>
|
||||
<Teleport
|
||||
v-if="teleport"
|
||||
to="body"
|
||||
>
|
||||
<Transition
|
||||
enter-active-class="transition duration-100 ease-out"
|
||||
enter-from-class="opacity-0 scale-95"
|
||||
enter-to-class="opacity-100 scale-100"
|
||||
leave-active-class="transition duration-75 ease-in"
|
||||
leave-from-class="opacity-100 scale-100"
|
||||
leave-to-class="opacity-0 scale-95"
|
||||
>
|
||||
<div
|
||||
v-if="open && placement"
|
||||
ref="menuRef"
|
||||
role="menu"
|
||||
class="fixed z-[100] min-w-[6rem] rounded border border-kestrel-border bg-kestrel-surface py-1 shadow-glow shadow-glow-dropdown"
|
||||
:style="menuStyle"
|
||||
>
|
||||
<slot name="menu" />
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
<Transition
|
||||
v-else
|
||||
name="dropdown"
|
||||
>
|
||||
<div
|
||||
v-if="open"
|
||||
ref="menuRef"
|
||||
role="menu"
|
||||
class="absolute right-0 top-full z-[2001] mt-1 min-w-[160px] rounded border border-kestrel-border bg-kestrel-surface py-1 shadow-glow"
|
||||
>
|
||||
<slot name="menu" />
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, nextTick, onMounted, onBeforeUnmount } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
open: { type: Boolean, default: false },
|
||||
teleport: { type: Boolean, default: false },
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
const triggerRef = ref(null)
|
||||
const menuRef = ref(null)
|
||||
const placement = ref(null)
|
||||
const menuStyle = computed(() => {
|
||||
if (!placement.value) return undefined
|
||||
const p = placement.value
|
||||
return { top: p.top + 'px', left: p.left + 'px', minWidth: p.minWidth + 'px' }
|
||||
})
|
||||
|
||||
watch(() => props.open, (open) => {
|
||||
if (open && triggerRef.value && props.teleport) {
|
||||
nextTick(() => {
|
||||
const rect = triggerRef.value.getBoundingClientRect()
|
||||
placement.value = {
|
||||
top: rect.bottom + 4,
|
||||
left: rect.left,
|
||||
minWidth: Math.max(rect.width, 96),
|
||||
}
|
||||
})
|
||||
}
|
||||
else {
|
||||
placement.value = null
|
||||
}
|
||||
})
|
||||
|
||||
function onDocumentClick(e) {
|
||||
if (!props.open) return
|
||||
const trigger = triggerRef.value
|
||||
const menu = menuRef.value
|
||||
const inTrigger = trigger && trigger.contains(e.target)
|
||||
const inMenu = menu && menu.contains(e.target)
|
||||
if (!inTrigger && !inMenu) emit('close')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', onDocumentClick)
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('click', onDocumentClick)
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user