PronounsPage/components/Popover.vue
Valentyne Stigloher 8ec65b87d4 (style) fix popovers for rtl locales also in production
/* */ comments get stripped out by scss in compressed format: https://sass-lang.com/documentation/syntax/comments/,
so /*! */ must be used so that rtlcss can process the directive: https://rtlcss.com/learn/getting-started/why-rtlcss/#processing-directives
adjustment to 1f88c83a96e16e1166da14c3f0c6f4a1906d14f5
2024-05-20 18:43:52 +02:00

180 lines
5.4 KiB
Vue

<template>
<span
ref="reference"
@mouseenter="show"
@mouseleave="hide"
><slot></slot><span
v-if="visible && hasContent"
ref="floating"
class="popover"
:style="floatingStyles"
><span
ref="arrow"
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></span></span>
</template>
<script lang="ts">
import type { PropType } from 'vue';
import { arrow, autoUpdate, computePosition, flip, offset, shift, size } from '@floating-ui/dom';
import Vue from 'vue';
import type { CSSProperties } from 'vue/types/jsx';
import type { ComputePositionReturn, Placement } from '@floating-ui/dom';
const getDPR = (element: Element): number => {
if (typeof window === 'undefined') {
return 1;
}
const win = element.ownerDocument.defaultView || window;
return win.devicePixelRatio || 1;
};
const roundByDPR = (element: Element, value: number): number => {
const dpr = getDPR(element);
return Math.round(value * dpr) / dpr;
};
const remToPx = (valueInRem: number): number => {
return valueInRem * parseFloat(getComputedStyle(document.documentElement).fontSize);
};
interface Data {
visible: boolean;
position: ComputePositionReturn | null;
cleanup: (() => void) | null;
}
interface Refs {
reference: HTMLSpanElement | undefined;
floating: HTMLSpanElement | undefined;
content: HTMLSpanElement | undefined;
arrow: HTMLSpanElement | undefined;
}
export default Vue.extend({
props: {
placement: { default: 'bottom', type: String as PropType<Placement> },
resize: { type: Boolean },
},
data(): Data {
return {
visible: false,
position: null,
cleanup: null,
};
},
computed: {
$tRefs(): Refs {
return this.$refs as unknown as Refs;
},
hasContent(): boolean {
return !!this.$slots.content?.[0];
},
floatingStyles(): CSSProperties | undefined {
if (!this.position || !this.$tRefs.floating) {
return;
}
const xVal = roundByDPR(this.$tRefs.floating, this.position.x);
const yVal = roundByDPR(this.$tRefs.floating, this.position.y);
return {
transform: `translate(${xVal}px, ${yVal}px)`,
...getDPR(this.$tRefs.floating) >= 1.5 && {
willChange: 'transform',
},
};
},
arrowStyles(): CSSProperties | undefined {
if (!this.position) {
return;
}
const arrowData = this.position.middlewareData.arrow;
const staticSite = {
top: 'bottom',
right: 'left',
bottom: 'top',
left: 'right',
}[this.position.placement.split('-')[0]]!;
return {
left: typeof arrowData?.x === 'number' ? `${arrowData.x}px` : '',
top: typeof arrowData?.y === 'number' ? `${arrowData.y}px` : '',
[staticSite]: '-0.25rem',
};
},
},
beforeDestroy() {
this.hide();
},
methods: {
async show() {
this.visible = true;
// floating element will be rendered on next tick
await this.$nextTick();
if (!this.$tRefs.reference || !this.$tRefs.floating) {
return;
}
// remove title from reference element to prevent a double tooltip
this.$tRefs.reference.removeAttribute('title');
this.cleanup = autoUpdate(this.$tRefs.reference, this.$tRefs.floating, () => this.update());
},
hide() {
this.visible = false;
if (this.cleanup) {
this.cleanup();
this.cleanup = null;
}
},
async update() {
if (!this.$tRefs.reference || !this.$tRefs.floating || !this.$tRefs.arrow) {
return;
}
const middleware = [offset(remToPx(0.25)), flip(), shift()];
if (this.resize) {
middleware.push(size({
apply: ({ availableWidth, availableHeight }) => {
if (!this.$tRefs.content) {
return;
}
Object.assign(this.$tRefs.content.style, {
maxWidth: `${availableWidth}px`,
maxHeight: `${availableHeight}px`,
});
},
}));
}
middleware.push(arrow({ element: this.$tRefs.arrow }));
this.position = await computePosition(this.$tRefs.reference, this.$tRefs.floating, {
middleware,
placement: this.placement as Placement,
});
},
},
});
</script>
<style lang="scss">
.popover {
position: absolute;
top: 0;
/*! rtl:ignore: popovers are always positioned absolutely from the upper-left corner */
left: 0;
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>