From 478b162a5ba3e2f4f7fb12241881599ecdce5747 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D1=82=D1=83=D0=B0=D0=BD?= Date: Wed, 6 May 2026 13:23:34 +0300 Subject: [PATCH 1/4] fix position popover on scroll or window resize --- .../src/vue/components/popover/usePopover.ts | 52 ++++++++++++++++++- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/@codexteam/ui/src/vue/components/popover/usePopover.ts b/@codexteam/ui/src/vue/components/popover/usePopover.ts index 00f6b6f9..ff45a9f2 100644 --- a/@codexteam/ui/src/vue/components/popover/usePopover.ts +++ b/@codexteam/ui/src/vue/components/popover/usePopover.ts @@ -1,6 +1,7 @@ import { reactive, ref, shallowRef } from 'vue'; import { createSharedComposable } from '@vueuse/core'; import type { PopoverContent, PopoverShowParams } from './Popover.types'; +import { throttle } from '../../utils'; /** * Shared composable for the Popover component @@ -56,6 +57,16 @@ export const usePopover = createSharedComposable(() => { */ const targetElement = ref(null); + /** + * Last alignment config, stored for recalculating position on scroll/resize + */ + let lastAlign: PopoverShowParams['align'] = { vertically: 'below', horizontally: 'left' }; + + /** + * Last width config, stored for recalculating position on scroll/resize + */ + let lastWidthConfig: PopoverShowParams['width'] = 'auto'; + /** * Move popover to the target element * Also, align and set width @@ -86,11 +97,11 @@ export const usePopover = createSharedComposable(() => { switch (align.horizontally) { case 'left': - left = `${rect.left}px`; + left = `${rect.left + window.scrollX}px`; transformX = '0'; break; case 'right': - left = `${rect.right}px`; + left = `${rect.right + window.scrollX}px`; transformX = '-100'; break; } @@ -109,11 +120,45 @@ export const usePopover = createSharedComposable(() => { position.transform = `translate(${transformX}%, ${transformY})`; } + /** + * Recalculate popover position using stored target and alignment + * Called on scroll/resize to keep popover anchored to the target element + */ + function updatePosition(): void { + if (!isOpen.value || !targetElement.value) { + return; + } + + move(targetElement.value, lastAlign, lastWidthConfig); + } + + /** + * Throttled handler for scroll/resize events + */ + const onRepositionThrottled = throttle(updatePosition, 16); + + /** + * Start listening for scroll/resize to reposition popover + */ + function startRepositionListeners(): void { + window.addEventListener('scroll', onRepositionThrottled, true); + window.addEventListener('resize', onRepositionThrottled); + } + + /** + * Stop listening for scroll/resize + */ + function stopRepositionListeners(): void { + window.removeEventListener('scroll', onRepositionThrottled, true); + window.removeEventListener('resize', onRepositionThrottled); + } + /** * Show popover */ function show(): void { isOpen.value = true; + startRepositionListeners(); } /** @@ -134,6 +179,8 @@ export const usePopover = createSharedComposable(() => { */ function showPopover(params: PopoverShowParams): void { targetElement.value = params.targetEl; + lastAlign = params.align; + lastWidthConfig = params.width; move(params.targetEl, params.align, params.width); mountComponent(params.with.component, params.with.props); show(); @@ -143,6 +190,7 @@ export const usePopover = createSharedComposable(() => { * Empty content, position and hide popover */ function resetPopover(): void { + stopRepositionListeners(); targetElement.value = null; content.value = null; position.left = '0px'; From 90a924a26ccc25ae227b5d7b11d2c82609915770 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D1=82=D1=83=D0=B0=D0=BD?= Date: Wed, 6 May 2026 13:32:33 +0300 Subject: [PATCH 2/4] eslint fixed --- @codexteam/ui/src/vue/components/popover/usePopover.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/@codexteam/ui/src/vue/components/popover/usePopover.ts b/@codexteam/ui/src/vue/components/popover/usePopover.ts index ff45a9f2..81546d2e 100644 --- a/@codexteam/ui/src/vue/components/popover/usePopover.ts +++ b/@codexteam/ui/src/vue/components/popover/usePopover.ts @@ -60,7 +60,8 @@ export const usePopover = createSharedComposable(() => { /** * Last alignment config, stored for recalculating position on scroll/resize */ - let lastAlign: PopoverShowParams['align'] = { vertically: 'below', horizontally: 'left' }; + let lastAlign: PopoverShowParams['align'] = { vertically: 'below', + horizontally: 'left' }; /** * Last width config, stored for recalculating position on scroll/resize @@ -132,10 +133,15 @@ export const usePopover = createSharedComposable(() => { move(targetElement.value, lastAlign, lastWidthConfig); } + /** + * Delay in milliseconds for throttling scroll/resize reposition (~60fps) + */ + const REPOSITION_THROTTLE_DELAY_MS = 16; + /** * Throttled handler for scroll/resize events */ - const onRepositionThrottled = throttle(updatePosition, 16); + const onRepositionThrottled = throttle(updatePosition, REPOSITION_THROTTLE_DELAY_MS); /** * Start listening for scroll/resize to reposition popover From fc603124a89d95a971476543d508451e7554894d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D1=82=D1=83=D0=B0=D0=BD?= Date: Wed, 6 May 2026 14:25:03 +0300 Subject: [PATCH 3/4] address comment review --- .../ui/src/vue/components/popover/usePopover.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/@codexteam/ui/src/vue/components/popover/usePopover.ts b/@codexteam/ui/src/vue/components/popover/usePopover.ts index 81546d2e..8e83fa74 100644 --- a/@codexteam/ui/src/vue/components/popover/usePopover.ts +++ b/@codexteam/ui/src/vue/components/popover/usePopover.ts @@ -1,4 +1,4 @@ -import { reactive, ref, shallowRef } from 'vue'; +import { onScopeDispose, reactive, ref, shallowRef } from 'vue'; import { createSharedComposable } from '@vueuse/core'; import type { PopoverContent, PopoverShowParams } from './Popover.types'; import { throttle } from '../../utils'; @@ -147,18 +147,27 @@ export const usePopover = createSharedComposable(() => { * Start listening for scroll/resize to reposition popover */ function startRepositionListeners(): void { - window.addEventListener('scroll', onRepositionThrottled, true); - window.addEventListener('resize', onRepositionThrottled); + window.addEventListener('scroll', onRepositionThrottled, { + capture: true, + passive: true, + }); + window.addEventListener('resize', onRepositionThrottled, { passive: true }); } /** * Stop listening for scroll/resize */ function stopRepositionListeners(): void { - window.removeEventListener('scroll', onRepositionThrottled, true); + window.removeEventListener('scroll', onRepositionThrottled, { capture: true }); window.removeEventListener('resize', onRepositionThrottled); } + /** + * Safety net: clean up window listeners if the Vue scope is disposed + * while the popover is still open (e.g. last consumer unmounts) + */ + onScopeDispose(stopRepositionListeners); + /** * Show popover */ From f0f6d78b334c5c27a015842f7db792df9372d48c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D1=82=D1=83=D0=B0=D0=BD?= Date: Wed, 6 May 2026 14:44:28 +0300 Subject: [PATCH 4/4] fix eslint --- @codexteam/ui/src/vue/components/popover/usePopover.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/@codexteam/ui/src/vue/components/popover/usePopover.ts b/@codexteam/ui/src/vue/components/popover/usePopover.ts index 8e83fa74..9e428ed6 100644 --- a/@codexteam/ui/src/vue/components/popover/usePopover.ts +++ b/@codexteam/ui/src/vue/components/popover/usePopover.ts @@ -60,8 +60,10 @@ export const usePopover = createSharedComposable(() => { /** * Last alignment config, stored for recalculating position on scroll/resize */ - let lastAlign: PopoverShowParams['align'] = { vertically: 'below', - horizontally: 'left' }; + let lastAlign: PopoverShowParams['align'] = { + vertically: 'below', + horizontally: 'left', + }; /** * Last width config, stored for recalculating position on scroll/resize