diff --git a/src/components/Fullscreen/Fullscreen.tsx b/src/components/Fullscreen/Fullscreen.tsx index 0546cc9931..c072d10b24 100644 --- a/src/components/Fullscreen/Fullscreen.tsx +++ b/src/components/Fullscreen/Fullscreen.tsx @@ -5,6 +5,7 @@ import {Button, Icon} from '@gravity-ui/uikit'; import {disableFullscreen} from '../../store/reducers/fullscreen'; import {cn} from '../../utils/cn'; import {useTypedDispatch, useTypedSelector} from '../../utils/hooks'; +import {useResizeObserverTrigger} from '../../utils/hooks/useResizeObserverTrigger'; import {Portal} from '../Portal/Portal'; import {useFullscreenContext} from './FullscreenContext'; @@ -62,14 +63,12 @@ export function Fullscreen({children, className}: FullscreenProps) { } else { ref.current?.appendChild(container); } - // Trigger recalculation for components relying on window resize - // Dispatch after DOM repaint to ensure correct measurements - requestAnimationFrame(() => { - window.dispatchEvent(new Event('resize')); - }); } }, [container, fullscreenRootRef, isFullscreen]); + // Trigger resize event when fullscreen state changes to force virtualization recalculation + useResizeObserverTrigger([isFullscreen]); + if (!container) { return null; } diff --git a/src/containers/Cluster/ClusterOverview/ClusterOverview.tsx b/src/containers/Cluster/ClusterOverview/ClusterOverview.tsx index 20d31e52bd..56c3769fb8 100644 --- a/src/containers/Cluster/ClusterOverview/ClusterOverview.tsx +++ b/src/containers/Cluster/ClusterOverview/ClusterOverview.tsx @@ -14,6 +14,7 @@ import {isClusterInfoV2, isClusterInfoV5} from '../../../types/api/cluster'; import type {TClusterInfo} from '../../../types/api/cluster'; import type {IResponseError} from '../../../types/api/error'; import {valueIsDefined} from '../../../utils'; +import {useResizeObserverTrigger} from '../../../utils/hooks/useResizeObserverTrigger'; import {useSetting} from '../../../utils/hooks/useSetting'; import {ClusterInfo} from '../ClusterInfo/ClusterInfo'; import i18n from '../i18n'; @@ -43,6 +44,9 @@ export function ClusterOverview(props: ClusterOverviewProps) { const [expandDashboard, setExpandDashboard] = useSetting( SETTING_KEYS.EXPAND_CLUSTER_DASHBOARD, ); + + //needs timeout to ensure layout has been recalculated after Disclosure animations + useResizeObserverTrigger([expandDashboard], 110); const bridgeModeEnabled = useBridgeModeEnabled(); const bridgePiles = React.useMemo(() => { diff --git a/src/containers/Tenant/utils/paneVisibilityToggleHelpers.tsx b/src/containers/Tenant/utils/paneVisibilityToggleHelpers.tsx index 3f8e92fc26..0b8ebb67be 100644 --- a/src/containers/Tenant/utils/paneVisibilityToggleHelpers.tsx +++ b/src/containers/Tenant/utils/paneVisibilityToggleHelpers.tsx @@ -4,6 +4,7 @@ import {ChevronsUp} from '@gravity-ui/icons'; import {ActionTooltip, Button, Icon} from '@gravity-ui/uikit'; import {cn} from '../../../utils/cn'; +import {useResizeObserverTrigger} from '../../../utils/hooks/useResizeObserverTrigger'; import './ToggleButton.scss'; @@ -82,6 +83,7 @@ export function PaneVisibilityToggleButtons({ initialDirection = 'top', className, }: ToggleButtonProps) { + useResizeObserverTrigger([isCollapsed]); return ( diff --git a/src/utils/hooks/useResizeObserverTrigger.ts b/src/utils/hooks/useResizeObserverTrigger.ts new file mode 100644 index 0000000000..30613734db --- /dev/null +++ b/src/utils/hooks/useResizeObserverTrigger.ts @@ -0,0 +1,43 @@ +import React from 'react'; + +/** + * Triggers a window resize event when dependencies change. + * Useful for forcing virtualized components to recalculate visible items + * after layout changes (e.g., when panels expand/collapse). + * + * Uses double requestAnimationFrame to ensure the resize event is dispatched + * after the browser has completed layout recalculation. Properly cleans up + * all pending RAF callbacks and timeouts on unmount or dependency changes. + * @param dependencies - Array of values to watch for changes + * @param timeout - Optional delay in milliseconds before dispatching the resize event (default: 0) + * @example + * ```typescript + * const [isExpanded, setIsExpanded] = useState(false); + * useResizeObserverTrigger([isExpanded]); + * ``` + */ +export function useResizeObserverTrigger(dependencies: React.DependencyList, timeout = 0): void { + React.useEffect(() => { + let rafId2: number | undefined; + let timeoutId: number | undefined; + + // Use double RAF to ensure layout has been recalculated after animation + const rafId1 = requestAnimationFrame(() => { + rafId2 = requestAnimationFrame(() => { + // Dispatch resize event to trigger virtualization recalculation + timeoutId = window.setTimeout(() => { + window.dispatchEvent(new Event('resize')); + }, timeout); + }); + }); + + return () => { + cancelAnimationFrame(rafId1); + if (rafId2 !== undefined) { + cancelAnimationFrame(rafId2); + } + clearTimeout(timeoutId); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, dependencies); +}