diff --git a/packages/react/src/PageLayout/usePaneWidth.test.ts b/packages/react/src/PageLayout/usePaneWidth.test.ts index 69d478d0019..1f0bd9d7941 100644 --- a/packages/react/src/PageLayout/usePaneWidth.test.ts +++ b/packages/react/src/PageLayout/usePaneWidth.test.ts @@ -635,15 +635,24 @@ describe('usePaneWidth', () => { vi.stubGlobal('innerWidth', 1000) window.dispatchEvent(new Event('resize')) - // At this point, attribute is applied but timing depends on throttle behavior - // The key is that it gets cleaned up after + // Attribute should be applied immediately on first resize + expect(refs.paneRef.current?.hasAttribute('data-dragging')).toBe(true) + expect(refs.contentRef.current?.hasAttribute('data-dragging')).toBe(true) - // Wait for throttle to complete via rAF + // Fire another resize event immediately (simulating continuous resize) + vi.stubGlobal('innerWidth', 900) + window.dispatchEvent(new Event('resize')) + + // Attribute should still be present (containment stays on during continuous resize) + expect(refs.paneRef.current?.hasAttribute('data-dragging')).toBe(true) + expect(refs.contentRef.current?.hasAttribute('data-dragging')).toBe(true) + + // Wait for the debounce timeout (150ms) to complete after resize stops await act(async () => { - await vi.runAllTimersAsync() + await vi.advanceTimersByTimeAsync(150) }) - // Attribute should be removed after throttle completes + // Attribute should be removed after debounce completes expect(refs.paneRef.current?.hasAttribute('data-dragging')).toBe(false) expect(refs.contentRef.current?.hasAttribute('data-dragging')).toBe(false) @@ -669,7 +678,11 @@ describe('usePaneWidth', () => { vi.stubGlobal('innerWidth', 1000) window.dispatchEvent(new Event('resize')) - // Unmount immediately (may or may not have attributes depending on throttle timing) + // Attribute should be applied + expect(refs.paneRef.current?.hasAttribute('data-dragging')).toBe(true) + expect(refs.contentRef.current?.hasAttribute('data-dragging')).toBe(true) + + // Unmount immediately (before debounce timer fires) unmount() // Attribute should be cleaned up on unmount regardless of timing diff --git a/packages/react/src/PageLayout/usePaneWidth.ts b/packages/react/src/PageLayout/usePaneWidth.ts index 5df8ae23b78..b96dd2c389a 100644 --- a/packages/react/src/PageLayout/usePaneWidth.ts +++ b/packages/react/src/PageLayout/usePaneWidth.ts @@ -269,9 +269,11 @@ export function usePaneWidth({ // Throttle approach for window resize - provides immediate visual feedback for small DOMs // while still limiting update frequency const THROTTLE_MS = 16 // ~60fps + const DEBOUNCE_MS = 150 // Delay before removing containment after resize stops let lastUpdateTime = 0 let pendingUpdate = false let rafId: number | null = null + let debounceId: ReturnType | null = null let isResizing = false // Apply containment during resize to reduce layout thrashing on large DOMs @@ -291,13 +293,13 @@ export function usePaneWidth({ } const handleResize = () => { + // Apply containment on first resize event (stays applied until resize stops) startResizeOptimizations() const now = Date.now() if (now - lastUpdateTime >= THROTTLE_MS) { lastUpdateTime = now syncAll() - endResizeOptimizations() } else if (!pendingUpdate) { pendingUpdate = true rafId = requestAnimationFrame(() => { @@ -305,15 +307,22 @@ export function usePaneWidth({ rafId = null lastUpdateTime = Date.now() syncAll() - endResizeOptimizations() }) } + + // Debounce the cleanup — remove containment after resize stops + if (debounceId !== null) clearTimeout(debounceId) + debounceId = setTimeout(() => { + debounceId = null + endResizeOptimizations() + }, DEBOUNCE_MS) } // eslint-disable-next-line github/prefer-observers -- Uses window resize events instead of ResizeObserver to avoid INP issues. ResizeObserver on document.documentElement fires on any content change (typing, etc), while window resize only fires on actual viewport changes. window.addEventListener('resize', handleResize) return () => { if (rafId !== null) cancelAnimationFrame(rafId) + if (debounceId !== null) clearTimeout(debounceId) endResizeOptimizations() window.removeEventListener('resize', handleResize) }