124 lines
3.3 KiB
Vue
124 lines
3.3 KiB
Vue
<script setup>
|
|
import { inject, ref, computed, onBeforeUnmount } from "vue";
|
|
|
|
defineOptions({ name: "HoverCardContent" });
|
|
|
|
const props = defineProps({
|
|
side: {
|
|
type: String,
|
|
default: "bottom",
|
|
validator: (value) => ["top", "right", "bottom", "left"].includes(value),
|
|
},
|
|
align: {
|
|
type: String,
|
|
default: "center",
|
|
validator: (value) => ["start", "center", "end"].includes(value),
|
|
},
|
|
});
|
|
|
|
const { isOpen, position, triggerRef, hide, setIsHoveringContent } = inject("hover-card");
|
|
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 there's room below
|
|
const hasSpaceBelow = triggerRect.bottom + contentRect.height <= windowHeight;
|
|
|
|
// If no space below, show above
|
|
if (!hasSpaceBelow && props.side === "bottom") {
|
|
y = triggerRect.top - contentRect.height - 8;
|
|
}
|
|
|
|
// Adjust based on side with safe area consideration
|
|
if (props.side === "top") {
|
|
y = triggerRect.top - contentRect.height - 8;
|
|
} else if (props.side === "right") {
|
|
x = triggerRect.right + 8;
|
|
y = Math.min(y, triggerRect.bottom - 20);
|
|
} else if (props.side === "left") {
|
|
x = triggerRect.left - contentRect.width - 8;
|
|
y = Math.min(y, triggerRect.bottom - 20);
|
|
} else {
|
|
x = x - 20;
|
|
}
|
|
|
|
// Adjust based on alignment
|
|
if (props.align === "start") {
|
|
// No adjustment needed
|
|
} else if (props.align === "center") {
|
|
x = triggerRect.left + triggerRect.width / 2 - contentRect.width / 2;
|
|
} else if (props.align === "end") {
|
|
x = triggerRect.right - contentRect.width;
|
|
}
|
|
|
|
// Ensure the content stays within viewport
|
|
if (x + contentRect.width > windowWidth) {
|
|
x = windowWidth - contentRect.width - 8;
|
|
}
|
|
if (x < 8) x = 8;
|
|
if (y < 8) y = 8;
|
|
if (y + contentRect.height > windowHeight) {
|
|
y = windowHeight - contentRect.height - 8;
|
|
}
|
|
|
|
return { x, y };
|
|
});
|
|
|
|
const handleMouseEnter = () => {
|
|
setIsHoveringContent(true);
|
|
};
|
|
|
|
const handleMouseLeave = () => {
|
|
setIsHoveringContent(false);
|
|
};
|
|
|
|
</script>
|
|
|
|
<template>
|
|
<Transition name="hover-card">
|
|
<div
|
|
v-if="isOpen"
|
|
ref="contentRef"
|
|
class="absolute z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none animate-in zoom-in-90"
|
|
:class="{
|
|
'data-[side=bottom]:slide-in-from-top-2': side === 'bottom',
|
|
'data-[side=left]:slide-in-from-right-2': side === 'left',
|
|
'data-[side=right]:slide-in-from-left-2': side === 'right',
|
|
'data-[side=top]:slide-in-from-bottom-2': side === 'top',
|
|
}"
|
|
:style="{
|
|
position: 'fixed',
|
|
top: `${adjustedPosition.y}px`,
|
|
left: `${adjustedPosition.x}px`,
|
|
}"
|
|
@mouseenter="handleMouseEnter"
|
|
@mouseleave="handleMouseLeave"
|
|
>
|
|
<slot />
|
|
</div>
|
|
</Transition>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.hover-card-enter-active,
|
|
.hover-card-leave-active {
|
|
transition: opacity 0.2s ease, transform 0.2s ease;
|
|
}
|
|
|
|
.hover-card-enter-from,
|
|
.hover-card-leave-to {
|
|
opacity: 0;
|
|
transform: scale(0.95);
|
|
}
|
|
</style>
|