Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 81 additions & 7 deletions packages/react/src/PageLayout/PageLayout.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<PageLayout>
<PageLayout.Pane resizable>
Expand All @@ -249,15 +249,89 @@ describe('PageLayout', async () => {
const pane = container.querySelector<HTMLElement>('[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(
<PageLayout>
<PageLayout.Pane resizable>
<Placeholder height={320} label="Pane" />
</PageLayout.Pane>
<PageLayout.Content>
<Placeholder height={1200} label="Content" />
</PageLayout.Content>
</PageLayout>,
)

const content = container.querySelector<HTMLElement>('[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(
<PageLayout>
<PageLayout.Pane resizable>
<Placeholder height={320} label="Pane" />
</PageLayout.Pane>
<PageLayout.Content>
<Placeholder height={640} label="Content" />
</PageLayout.Content>
</PageLayout>,
)

const content = container.querySelector<HTMLElement>('[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('')
})
})

Expand Down
52 changes: 44 additions & 8 deletions packages/react/src/PageLayout/paneUtils.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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`
}
}

/**
Expand All @@ -32,21 +43,46 @@ 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 */
export function removeDraggingStyles({handle, pane, content}: DraggingStylesParams) {
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 = ''
}
}