From af80fa9bae6b0eefc4be2d060e93be1b1e48ab2f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 26 Dec 2025 13:35:25 +0000 Subject: [PATCH 1/3] Initial plan From 873f524102e5c377d0a0ae1798e8f1c1aac6005e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 26 Dec 2025 13:47:16 +0000 Subject: [PATCH 2/3] Refine drag/resize performance optimizations in paneUtils.ts - Remove will-change: width from pane (only helps transform/opacity) - Remove content-visibility from pane (pane always visible during drag) - Add TALL_CONTENT_THRESHOLD (1000px) constant - Apply content-visibility only to tall content (>1000px) - Use actual offsetHeight instead of hardcoded 500px - Update setDraggingStyles to apply minimal containment to pane - Update setDraggingStyles to conditionally apply content-visibility to content - Update setContainmentOptimizations for conditional content-visibility - Update tests to reflect new behavior - Add tests for tall content threshold Co-authored-by: mattcosta7 <8616962+mattcosta7@users.noreply.github.com> --- .../react/src/PageLayout/PageLayout.test.tsx | 88 +++++++++++++++++-- packages/react/src/PageLayout/paneUtils.ts | 52 +++++++++-- 2 files changed, 125 insertions(+), 15 deletions(-) diff --git a/packages/react/src/PageLayout/PageLayout.test.tsx b/packages/react/src/PageLayout/PageLayout.test.tsx index 5d841b799de..2bdf5136539 100644 --- a/packages/react/src/PageLayout/PageLayout.test.tsx +++ b/packages/react/src/PageLayout/PageLayout.test.tsx @@ -234,7 +234,7 @@ describe('PageLayout', async () => { expect(content!.style.getPropertyValue('contain')).toBe('') }) - it('should add will-change during drag for optimized updates', async () => { + it('should apply containment optimizations during drag', async () => { const {container} = render( @@ -249,15 +249,89 @@ describe('PageLayout', async () => { const pane = container.querySelector('[class*="Pane"][data-resizable]') const divider = await screen.findByRole('slider') - // Before drag - no will-change - expect(pane!.style.willChange).toBe('') + // Before drag - no containment + expect(pane!.style.contain).toBe('') + expect(pane!.style.pointerEvents).toBe('') - // Start drag - will-change is added + // Start drag - containment is added fireEvent.pointerDown(divider, {clientX: 300, clientY: 200, pointerId: 1}) - expect(pane!.style.willChange).toBe('width') - // End drag - will-change is removed + expect(pane!.style.contain).toBe('layout style paint') + expect(pane!.style.pointerEvents).toBe('none') + + // End drag - containment is removed + fireEvent.lostPointerCapture(divider, {pointerId: 1}) + expect(pane!.style.contain).toBe('') + expect(pane!.style.pointerEvents).toBe('') + }) + + it('should apply content-visibility only for tall content during drag', async () => { + const {container} = render( + + + + + + + + , + ) + + const content = container.querySelector('[class*="Content"]') + const divider = await screen.findByRole('slider') + + // Mock offsetHeight for tall content (>1000px threshold) + Object.defineProperty(content, 'offsetHeight', { + configurable: true, + value: 1200, + }) + + // Before drag - no content-visibility + expect(content!.style.contentVisibility).toBe('') + expect(content!.style.containIntrinsicSize).toBe('') + + // Start drag - content-visibility is added for tall content + fireEvent.pointerDown(divider, {clientX: 300, clientY: 200, pointerId: 1}) + expect(content!.style.contentVisibility).toBe('auto') + expect(content!.style.containIntrinsicSize).toBe('auto 1200px') + + // End drag - content-visibility is removed + fireEvent.lostPointerCapture(divider, {pointerId: 1}) + expect(content!.style.contentVisibility).toBe('') + expect(content!.style.containIntrinsicSize).toBe('') + }) + + it('should not apply content-visibility for short content during drag', async () => { + const {container} = render( + + + + + + + + , + ) + + const content = container.querySelector('[class*="Content"]') + const divider = await screen.findByRole('slider') + + // Mock offsetHeight for short content (<1000px threshold) + Object.defineProperty(content, 'offsetHeight', { + configurable: true, + value: 640, + }) + + // Start drag + fireEvent.pointerDown(divider, {clientX: 300, clientY: 200, pointerId: 1}) + + // content-visibility should NOT be applied for short content + expect(content!.style.contentVisibility).toBe('') + expect(content!.style.containIntrinsicSize).toBe('') + // But basic containment should still be applied + expect(content!.style.contain).toBe('layout style paint') + + // End drag fireEvent.lostPointerCapture(divider, {pointerId: 1}) - expect(pane!.style.willChange).toBe('') }) }) diff --git a/packages/react/src/PageLayout/paneUtils.ts b/packages/react/src/PageLayout/paneUtils.ts index 4ac2ebca89b..9ebfa8483dd 100644 --- a/packages/react/src/PageLayout/paneUtils.ts +++ b/packages/react/src/PageLayout/paneUtils.ts @@ -1,3 +1,9 @@ +/** + * Height threshold (in pixels) above which content-visibility optimizations are applied. + * Avoids overhead on small content that doesn't benefit from rendering optimizations. + */ +const TALL_CONTENT_THRESHOLD = 1000 + /** * Apply CSS containment optimizations to isolate an element during resize/drag. * - contain: limits layout/paint recalc to this subtree @@ -8,9 +14,14 @@ export function setContainmentOptimizations(element: HTMLElement | null) { if (!element) return element.style.contain = 'layout style paint' - element.style.contentVisibility = 'auto' - element.style.containIntrinsicSize = 'auto 500px' element.style.pointerEvents = 'none' + + // Only apply content-visibility for tall content to avoid overhead on small elements + const height = element.offsetHeight + if (height > TALL_CONTENT_THRESHOLD) { + element.style.contentVisibility = 'auto' + element.style.containIntrinsicSize = `auto ${height}px` + } } /** @@ -32,13 +43,29 @@ type DraggingStylesParams = { /** Apply visual feedback and performance optimizations during drag */ export function setDraggingStyles({handle, pane, content}: DraggingStylesParams) { + // Handle visual feedback handle?.style.setProperty('background-color', 'var(--bgColor-accent-emphasis)') handle?.style.setProperty('--draggable-handle--drag-opacity', '1') // Disable transition for instant visual feedback during drag handle?.style.setProperty('--draggable-handle--transition', 'none') - pane?.style.setProperty('will-change', 'width') - setContainmentOptimizations(content) - setContainmentOptimizations(pane) + + // Pane: minimal containment (always visible during drag) + if (pane) { + pane.style.contain = 'layout style paint' + pane.style.pointerEvents = 'none' + } + + // Content: containment + conditional content-visibility for tall content + if (content) { + content.style.contain = 'layout style paint' + content.style.pointerEvents = 'none' + + const height = content.offsetHeight + if (height > TALL_CONTENT_THRESHOLD) { + content.style.contentVisibility = 'auto' + content.style.containIntrinsicSize = `auto ${height}px` + } + } } /** Remove drag styles and restore normal state */ @@ -46,7 +73,16 @@ export function removeDraggingStyles({handle, pane, content}: DraggingStylesPara handle?.style.removeProperty('background-color') handle?.style.removeProperty('--draggable-handle--drag-opacity') handle?.style.removeProperty('--draggable-handle--transition') - pane?.style.removeProperty('will-change') - removeContainmentOptimizations(content) - removeContainmentOptimizations(pane) + + if (pane) { + pane.style.contain = '' + pane.style.pointerEvents = '' + } + + if (content) { + content.style.contain = '' + content.style.pointerEvents = '' + content.style.contentVisibility = '' + content.style.containIntrinsicSize = '' + } } From d4132f9651f316e245b81c57dfd0b484c0bc6017 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Dec 2025 13:13:14 -0500 Subject: [PATCH 3/3] Simplify PageLayout drag optimizations by removing height threshold (#7375) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: mattcosta7 <8616962+mattcosta7@users.noreply.github.com> --- .../react/src/PageLayout/PageLayout.test.tsx | 97 ++++++------------- packages/react/src/PageLayout/paneUtils.ts | 56 ++--------- 2 files changed, 37 insertions(+), 116 deletions(-) diff --git a/packages/react/src/PageLayout/PageLayout.test.tsx b/packages/react/src/PageLayout/PageLayout.test.tsx index 2bdf5136539..34a25206971 100644 --- a/packages/react/src/PageLayout/PageLayout.test.tsx +++ b/packages/react/src/PageLayout/PageLayout.test.tsx @@ -247,91 +247,50 @@ describe('PageLayout', async () => { ) const pane = container.querySelector('[class*="Pane"][data-resizable]') + const content = container.querySelector('[class*="PageLayoutContent"]') const divider = await screen.findByRole('slider') + // Mock offsetHeight for testing + Object.defineProperty(pane, 'offsetHeight', { + configurable: true, + value: 320, + }) + Object.defineProperty(content, 'offsetHeight', { + configurable: true, + value: 640, + }) + // Before drag - no containment expect(pane!.style.contain).toBe('') expect(pane!.style.pointerEvents).toBe('') + expect(pane!.style.contentVisibility).toBe('') + expect(pane!.style.containIntrinsicSize).toBe('') + expect(content!.style.contain).toBe('') + expect(content!.style.pointerEvents).toBe('') + expect(content!.style.contentVisibility).toBe('') + expect(content!.style.containIntrinsicSize).toBe('') - // Start drag - containment is added + // Start drag - containment and content-visibility are added to both pane and content fireEvent.pointerDown(divider, {clientX: 300, clientY: 200, pointerId: 1}) expect(pane!.style.contain).toBe('layout style paint') expect(pane!.style.pointerEvents).toBe('none') + expect(pane!.style.contentVisibility).toBe('auto') + expect(pane!.style.containIntrinsicSize).toBe('auto 320px') + expect(content!.style.contain).toBe('layout style paint') + expect(content!.style.pointerEvents).toBe('none') + expect(content!.style.contentVisibility).toBe('auto') + expect(content!.style.containIntrinsicSize).toBe('auto 640px') // End drag - containment is removed fireEvent.lostPointerCapture(divider, {pointerId: 1}) expect(pane!.style.contain).toBe('') expect(pane!.style.pointerEvents).toBe('') - }) - - it('should apply content-visibility only for tall content during drag', async () => { - const {container} = render( - - - - - - - - , - ) - - const content = container.querySelector('[class*="Content"]') - const divider = await screen.findByRole('slider') - - // Mock offsetHeight for tall content (>1000px threshold) - Object.defineProperty(content, 'offsetHeight', { - configurable: true, - value: 1200, - }) - - // Before drag - no content-visibility - expect(content!.style.contentVisibility).toBe('') - expect(content!.style.containIntrinsicSize).toBe('') - - // Start drag - content-visibility is added for tall content - fireEvent.pointerDown(divider, {clientX: 300, clientY: 200, pointerId: 1}) - expect(content!.style.contentVisibility).toBe('auto') - expect(content!.style.containIntrinsicSize).toBe('auto 1200px') - - // End drag - content-visibility is removed - fireEvent.lostPointerCapture(divider, {pointerId: 1}) - expect(content!.style.contentVisibility).toBe('') - expect(content!.style.containIntrinsicSize).toBe('') - }) - - it('should not apply content-visibility for short content during drag', async () => { - const {container} = render( - - - - - - - - , - ) - - const content = container.querySelector('[class*="Content"]') - const divider = await screen.findByRole('slider') - - // Mock offsetHeight for short content (<1000px threshold) - Object.defineProperty(content, 'offsetHeight', { - configurable: true, - value: 640, - }) - - // Start drag - fireEvent.pointerDown(divider, {clientX: 300, clientY: 200, pointerId: 1}) - - // content-visibility should NOT be applied for short content + expect(pane!.style.contentVisibility).toBe('') + expect(pane!.style.containIntrinsicSize).toBe('') + expect(content!.style.contain).toBe('') + expect(content!.style.pointerEvents).toBe('') expect(content!.style.contentVisibility).toBe('') expect(content!.style.containIntrinsicSize).toBe('') - // But basic containment should still be applied - expect(content!.style.contain).toBe('layout style paint') - - // End drag - fireEvent.lostPointerCapture(divider, {pointerId: 1}) }) }) diff --git a/packages/react/src/PageLayout/paneUtils.ts b/packages/react/src/PageLayout/paneUtils.ts index 9ebfa8483dd..caca5bf983d 100644 --- a/packages/react/src/PageLayout/paneUtils.ts +++ b/packages/react/src/PageLayout/paneUtils.ts @@ -1,27 +1,16 @@ -/** - * Height threshold (in pixels) above which content-visibility optimizations are applied. - * Avoids overhead on small content that doesn't benefit from rendering optimizations. - */ -const TALL_CONTENT_THRESHOLD = 1000 - /** * Apply CSS containment optimizations to isolate an element during resize/drag. * - contain: limits layout/paint recalc to this subtree - * - content-visibility: skip rendering off-screen content (valuable for large DOMs) - * - contain-intrinsic-size: prevents layout thrashing from size estimation when using content-visibility + * - content-visibility: skip rendering off-screen content + * - contain-intrinsic-size: uses actual element height to prevent layout shift * - pointer-events: skip hit-testing large child trees */ export function setContainmentOptimizations(element: HTMLElement | null) { if (!element) return element.style.contain = 'layout style paint' + element.style.contentVisibility = 'auto' + element.style.containIntrinsicSize = `auto ${element.offsetHeight}px` element.style.pointerEvents = 'none' - - // Only apply content-visibility for tall content to avoid overhead on small elements - const height = element.offsetHeight - if (height > TALL_CONTENT_THRESHOLD) { - element.style.contentVisibility = 'auto' - element.style.containIntrinsicSize = `auto ${height}px` - } } /** @@ -43,29 +32,12 @@ type DraggingStylesParams = { /** Apply visual feedback and performance optimizations during drag */ export function setDraggingStyles({handle, pane, content}: DraggingStylesParams) { - // Handle visual feedback handle?.style.setProperty('background-color', 'var(--bgColor-accent-emphasis)') handle?.style.setProperty('--draggable-handle--drag-opacity', '1') - // Disable transition for instant visual feedback during drag handle?.style.setProperty('--draggable-handle--transition', 'none') - - // Pane: minimal containment (always visible during drag) - if (pane) { - pane.style.contain = 'layout style paint' - pane.style.pointerEvents = 'none' - } - - // Content: containment + conditional content-visibility for tall content - if (content) { - content.style.contain = 'layout style paint' - content.style.pointerEvents = 'none' - - const height = content.offsetHeight - if (height > TALL_CONTENT_THRESHOLD) { - content.style.contentVisibility = 'auto' - content.style.containIntrinsicSize = `auto ${height}px` - } - } + // No will-change: width - doesn't help layout properties + setContainmentOptimizations(pane) + setContainmentOptimizations(content) } /** Remove drag styles and restore normal state */ @@ -73,16 +45,6 @@ export function removeDraggingStyles({handle, pane, content}: DraggingStylesPara handle?.style.removeProperty('background-color') handle?.style.removeProperty('--draggable-handle--drag-opacity') handle?.style.removeProperty('--draggable-handle--transition') - - if (pane) { - pane.style.contain = '' - pane.style.pointerEvents = '' - } - - if (content) { - content.style.contain = '' - content.style.pointerEvents = '' - content.style.contentVisibility = '' - content.style.containIntrinsicSize = '' - } + removeContainmentOptimizations(pane) + removeContainmentOptimizations(content) }