Fix incorrect tooltip position after window resize
This commit fixes an issue where the tooltip position becomes inaccurate after resizing the window. The solution uses `autoUpdate` functionality of `floating-ui` to update the position automatically on resize events. This function depends on browser APIs: `IntersectionObserver` and `ResizeObserver`. The official documentation recommends polyfilling those to support old browsers. Polyfilling `ResizeObserver` is already part of the codebase, used by `SizeObserver.vue`. This commit refactors polyfill logic to be reusable across different components, and reuses it on `TooltipWrapper.vue`. Polyfilling `IntersectionObserver` is ignored due to this API being older and more widely supported.
This commit is contained in:
@@ -0,0 +1,27 @@
|
|||||||
|
import { onMounted } from 'vue';
|
||||||
|
import { AsyncLazy } from '@/infrastructure/Threading/AsyncLazy';
|
||||||
|
|
||||||
|
// AsyncLazy ensures single load of the ResizeObserver polyfill,
|
||||||
|
// even when multiple calls are made simultaneously.
|
||||||
|
const polyfillLoader = new AsyncLazy(async () => {
|
||||||
|
if ('ResizeObserver' in window) {
|
||||||
|
return window.ResizeObserver;
|
||||||
|
}
|
||||||
|
const module = await import('@juggle/resize-observer');
|
||||||
|
globalThis.window.ResizeObserver = module.ResizeObserver;
|
||||||
|
return module.ResizeObserver;
|
||||||
|
});
|
||||||
|
|
||||||
|
async function polyfillResizeObserver(): Promise<typeof ResizeObserver> {
|
||||||
|
return polyfillLoader.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useResizeObserverPolyfill() {
|
||||||
|
const resizeObserverReady = new Promise<void>((resolve) => {
|
||||||
|
onMounted(async () => {
|
||||||
|
await polyfillResizeObserver();
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return { resizeObserverReady };
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
import {
|
import {
|
||||||
defineComponent, ref, onMounted, onBeforeUnmount,
|
defineComponent, ref, onMounted, onBeforeUnmount,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
|
import { useResizeObserverPolyfill } from '@/presentation/components/Shared/Hooks/UseResizeObserverPolyfill';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
emits: {
|
emits: {
|
||||||
@@ -18,18 +19,22 @@ export default defineComponent({
|
|||||||
/* eslint-enable @typescript-eslint/no-unused-vars */
|
/* eslint-enable @typescript-eslint/no-unused-vars */
|
||||||
},
|
},
|
||||||
setup(_, { emit }) {
|
setup(_, { emit }) {
|
||||||
|
const { resizeObserverReady } = useResizeObserverPolyfill();
|
||||||
|
|
||||||
const containerElement = ref<HTMLElement>();
|
const containerElement = ref<HTMLElement>();
|
||||||
|
|
||||||
let width = 0;
|
let width = 0;
|
||||||
let height = 0;
|
let height = 0;
|
||||||
let observer: ResizeObserver;
|
let observer: ResizeObserver;
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(() => {
|
||||||
width = containerElement.value.offsetWidth;
|
width = containerElement.value.offsetWidth;
|
||||||
height = containerElement.value.offsetHeight;
|
height = containerElement.value.offsetHeight;
|
||||||
|
|
||||||
observer = await initializeResizeObserver(updateSize);
|
resizeObserverReady.then(() => {
|
||||||
observer.observe(containerElement.value);
|
observer = new ResizeObserver(updateSize);
|
||||||
|
observer.observe(containerElement.value);
|
||||||
|
});
|
||||||
|
|
||||||
fireChangeEvents();
|
fireChangeEvents();
|
||||||
});
|
});
|
||||||
@@ -38,16 +43,6 @@ export default defineComponent({
|
|||||||
observer?.disconnect();
|
observer?.disconnect();
|
||||||
});
|
});
|
||||||
|
|
||||||
async function initializeResizeObserver(
|
|
||||||
callback: ResizeObserverCallback,
|
|
||||||
): Promise<ResizeObserver> {
|
|
||||||
if ('ResizeObserver' in window) {
|
|
||||||
return new window.ResizeObserver(callback);
|
|
||||||
}
|
|
||||||
const module = await import('@juggle/resize-observer');
|
|
||||||
return new module.ResizeObserver(callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateSize() {
|
function updateSize() {
|
||||||
let sizeChanged = false;
|
let sizeChanged = false;
|
||||||
if (isWidthChanged()) {
|
if (isWidthChanged()) {
|
||||||
|
|||||||
@@ -24,9 +24,10 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
useFloating, arrow, shift, flip, Placement, offset, Side, Coords,
|
useFloating, arrow, shift, flip, Placement, offset, Side, Coords, autoUpdate,
|
||||||
} from '@floating-ui/vue';
|
} from '@floating-ui/vue';
|
||||||
import { defineComponent, ref, computed } from 'vue';
|
import { defineComponent, ref, computed } from 'vue';
|
||||||
|
import { useResizeObserverPolyfill } from '@/presentation/components/Shared/Hooks/UseResizeObserverPolyfill';
|
||||||
import type { CSSProperties } from 'vue/types/jsx'; // In Vue 3.0 import from 'vue'
|
import type { CSSProperties } from 'vue/types/jsx'; // In Vue 3.0 import from 'vue'
|
||||||
|
|
||||||
const GAP_BETWEEN_TOOLTIP_AND_TRIGGER_IN_PX = 2;
|
const GAP_BETWEEN_TOOLTIP_AND_TRIGGER_IN_PX = 2;
|
||||||
@@ -40,6 +41,8 @@ export default defineComponent({
|
|||||||
const arrowElement = ref<HTMLElement | undefined>();
|
const arrowElement = ref<HTMLElement | undefined>();
|
||||||
const placement = ref<Placement>('top');
|
const placement = ref<Placement>('top');
|
||||||
|
|
||||||
|
useResizeObserverPolyfill();
|
||||||
|
|
||||||
const { floatingStyles, middlewareData } = useFloating(
|
const { floatingStyles, middlewareData } = useFloating(
|
||||||
triggeringElement,
|
triggeringElement,
|
||||||
tooltipDisplayElement,
|
tooltipDisplayElement,
|
||||||
@@ -56,6 +59,7 @@ export default defineComponent({
|
|||||||
flip(),
|
flip(),
|
||||||
arrow({ element: arrowElement }),
|
arrow({ element: arrowElement }),
|
||||||
],
|
],
|
||||||
|
whileElementsMounted: autoUpdate,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const arrowStyles = computed<CSSProperties>(() => {
|
const arrowStyles = computed<CSSProperties>(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user