All checks were successful
ci/woodpecker/push/push Pipeline was successful
Co-authored-by: Madison Grubb <madison@elastiflow.com> Reviewed-on: #5
96 lines
2.5 KiB
Vue
96 lines
2.5 KiB
Vue
<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>
|