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) }