PronounsPage/components/Popover.vue

142 lines
3.6 KiB
Vue

<script setup lang="ts">
import { arrow, autoUpdate, flip, shift, size, useFloating } from '@floating-ui/vue';
import type { Placement } from '@floating-ui/vue';
import type { CSSProperties } from 'vue';
const props = withDefaults(defineProps<{
tag?: string;
placement?: Placement;
resize?: boolean;
}>(), {
tag: 'span',
placement: 'bottom',
});
const reference = useTemplateRef<HTMLSpanElement>('reference');
const floating = useTemplateRef<HTMLSpanElement>('floating');
const floatingArrow = useTemplateRef<HTMLSpanElement>('floatingArrow');
const content = useTemplateRef<HTMLDivElement>('content');
const middleware = computed(() => {
const middleware = [flip(), shift()];
if (props.resize) {
middleware.push(size({
apply: ({ availableWidth, availableHeight }) => {
if (!content.value) {
return;
}
Object.assign(content.value.style, {
maxWidth: `${availableWidth}px`,
maxHeight: `${availableHeight}px`,
});
},
}));
}
middleware.push(arrow({ element: floatingArrow }));
return middleware;
});
const { floatingStyles, placement: calculatedPlacement, middlewareData } = useFloating(reference, floating, {
middleware,
placement: props.placement,
whileElementsMounted: autoUpdate,
});
const visible = ref(false);
const slots = defineSlots<{
default(): VNode;
content(): VNode[];
}>();
const hasContent = computed((): boolean => {
return !!slots.content()[0];
});
const arrowStyles = computed((): CSSProperties | undefined => {
const arrowData = middlewareData.value.arrow;
const staticSite = {
top: 'bottom',
right: 'left',
bottom: 'top',
left: 'right',
}[calculatedPlacement.value.split('-')[0]]!;
return {
left: typeof arrowData?.x === 'number' ? `${arrowData.x}px` : '',
top: typeof arrowData?.y === 'number' ? `${arrowData.y}px` : '',
[staticSite]: 0,
};
});
const hoverHandle = ref<ReturnType<typeof setTimeout> | null>(null);
const onEnter = () => {
visible.value = true;
if (hoverHandle.value) {
clearTimeout(hoverHandle.value);
hoverHandle.value = null;
}
};
const onLeave = () => {
hoverHandle.value = setTimeout(() => {
visible.value = false;
hoverHandle.value = null;
}, 50);
};
onMounted(() => {
// remove title from reference element to prevent a double tooltip
reference.value?.removeAttribute('title');
});
</script>
<template>
<component
:is="tag"
v-bind="$attrs"
ref="reference"
@mouseenter="onEnter()"
@mouseleave="onLeave()"
>
<slot></slot>
</component>
<Teleport to="#teleports">
<div
v-if="visible && hasContent"
ref="floating"
class="popover p-1"
:style="floatingStyles"
@mouseenter="onEnter()"
@mouseleave="onLeave()"
>
<span
ref="floatingArrow"
class="popover-arrow bg-dark text-white"
:style="arrowStyles"
></span>
<div
ref="content"
class="overflow-auto bg-dark text-white px-2 py-1 rounded"
>
<slot name="content"></slot>
</div>
</div>
</Teleport>
</template>
<style lang="scss">
.popover {
font-weight: normal;
font-style: normal;
font-size: .85rem;
z-index: 999;
}
.popover-arrow {
position: absolute;
width: 0.5rem;
height: 0.5rem;
transform: rotate(45deg);
}
</style>