71 lines
1.8 KiB
Vue
71 lines
1.8 KiB
Vue
<script setup>
|
|
import { inject, ref, computed } from "vue";
|
|
|
|
defineOptions({ name: "DropdownContent" });
|
|
|
|
const { isOpen, position, triggerRef } = inject("dropdown");
|
|
const contentRef = ref(null);
|
|
|
|
const adjustedPosition = computed(() => {
|
|
if (!contentRef.value || !triggerRef.value) return position.value;
|
|
|
|
const content = contentRef.value;
|
|
const trigger = triggerRef.value;
|
|
const contentRect = content.getBoundingClientRect();
|
|
const triggerRect = trigger.getBoundingClientRect();
|
|
const windowHeight = window.innerHeight;
|
|
const windowWidth = window.innerWidth;
|
|
|
|
let { x, y } = position.value;
|
|
|
|
// Check if dropdown should appear above instead of below
|
|
const shouldShowAbove = y + contentRect.height > windowHeight;
|
|
if (shouldShowAbove) {
|
|
y = triggerRect.top - contentRect.height - 4; // 4px gap
|
|
}
|
|
|
|
// Ensure horizontal alignment stays within bounds
|
|
if (x + contentRect.width > windowWidth) {
|
|
// Align to right edge of trigger
|
|
x = triggerRect.right - contentRect.width;
|
|
}
|
|
|
|
// Prevent going off-screen left
|
|
x = Math.max(8, x); // 8px minimum margin
|
|
|
|
// Prevent going off-screen top
|
|
y = Math.max(8, y); // 8px minimum margin
|
|
|
|
return { x, y };
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<Transition name="dropdown">
|
|
<div
|
|
v-if="isOpen"
|
|
ref="contentRef"
|
|
class="fixed z-[100] min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md"
|
|
:style="{
|
|
top: `${adjustedPosition.y}px`,
|
|
left: `${adjustedPosition.x}px`,
|
|
}"
|
|
>
|
|
<slot />
|
|
</div>
|
|
</Transition>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.dropdown-enter-active,
|
|
.dropdown-leave-active {
|
|
transition: opacity 0.2s ease, transform 0.2s ease;
|
|
}
|
|
|
|
.dropdown-enter-from,
|
|
.dropdown-leave-to {
|
|
opacity: 0;
|
|
transform: scale(0.95);
|
|
}
|
|
</style>
|