Files
kestrelos/app/components/AppDropdown.vue
Madison Grubb 4e51ca5509
All checks were successful
ci/woodpecker/pr/pr Pipeline was successful
new nav system
2026-02-14 22:47:05 -05:00

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>