Skip to content

Commit 7fb56ef

Browse files
Copilotmattcosta7
andauthored
Refactor PageLayout drag/resize optimizations to use CSS attribute selectors (#7385)
Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: mattcosta7 <[email protected]>
1 parent 4fb8c0b commit 7fb56ef

File tree

5 files changed

+50
-64
lines changed

5 files changed

+50
-64
lines changed

packages/react/src/PageLayout/PageLayout.module.css

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -725,3 +725,15 @@
725725
.DraggableHandle:hover::before {
726726
opacity: 1;
727727
}
728+
729+
/**
730+
* OPTIMIZATION: CSS containment during drag/resize
731+
* Direct attribute selectors are O(1) - only the attributed element is invalidated
732+
* (Unlike descendant selectors which require O(n) traversal)
733+
*/
734+
.Pane[data-dragging='true'],
735+
.ContentWrapper[data-dragging='true'] {
736+
contain: layout style paint;
737+
pointer-events: none;
738+
content-visibility: auto;
739+
}

packages/react/src/PageLayout/PageLayout.test.tsx

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -195,15 +195,15 @@ describe('PageLayout', async () => {
195195
const content = container.querySelector<HTMLElement>('[class*="PageLayoutContent"]')
196196
const divider = await screen.findByRole('slider')
197197

198-
// Before drag - no contain property
199-
expect(content!.style.getPropertyValue('contain')).toBe('')
198+
// Before drag - no data-dragging attribute
199+
expect(content).not.toHaveAttribute('data-dragging')
200200

201-
// Start drag - optimization properties are set
201+
// Start drag - optimization attribute is set
202202
fireEvent.pointerDown(divider, {clientX: 300, clientY: 200, pointerId: 1})
203-
expect(content!.style.getPropertyValue('contain')).toBe('layout style paint')
204-
// End drag - pointer capture lost ends the drag and removes optimization properties
203+
expect(content).toHaveAttribute('data-dragging', 'true')
204+
// End drag - pointer capture lost ends the drag and removes optimization attribute
205205
fireEvent.lostPointerCapture(divider, {pointerId: 1})
206-
expect(content!.style.getPropertyValue('contain')).toBe('')
206+
expect(content).not.toHaveAttribute('data-dragging')
207207
})
208208

209209
it('should set optimization styles during keyboard resize', async () => {
@@ -221,17 +221,17 @@ describe('PageLayout', async () => {
221221
const content = container.querySelector<HTMLElement>('[class*="PageLayoutContent"]')
222222
const divider = await screen.findByRole('slider')
223223

224-
// Before interaction - no contain property
225-
expect(content!.style.getPropertyValue('contain')).toBe('')
224+
// Before interaction - no data-dragging attribute
225+
expect(content).not.toHaveAttribute('data-dragging')
226226

227227
// Start keyboard resize (focus first)
228228
fireEvent.focus(divider)
229229
fireEvent.keyDown(divider, {key: 'ArrowRight'})
230-
expect(content!.style.getPropertyValue('contain')).toBe('layout style paint')
230+
expect(content).toHaveAttribute('data-dragging', 'true')
231231

232-
// End keyboard resize - removes optimization properties
232+
// End keyboard resize - removes optimization attribute
233233
fireEvent.keyUp(divider, {key: 'ArrowRight'})
234-
expect(content!.style.getPropertyValue('contain')).toBe('')
234+
expect(content).not.toHaveAttribute('data-dragging')
235235
})
236236

237237
it('should not add will-change during drag', async () => {
Lines changed: 10 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,29 @@
1-
/**
2-
* Apply CSS containment optimizations to isolate an element during resize/drag.
3-
* - contain: limits layout/paint recalc to this subtree
4-
* - content-visibility: skip rendering off-screen content (valuable for large DOMs)
5-
* - contain-intrinsic-size: prevents layout thrashing from size estimation when using content-visibility
6-
* - pointer-events: skip hit-testing large child trees
7-
*/
8-
export function setContainmentOptimizations(element: HTMLElement | null) {
9-
if (!element) return
10-
element.style.contain = 'layout style paint'
11-
element.style.contentVisibility = 'auto'
12-
element.style.containIntrinsicSize = `auto ${element.offsetHeight}px`
13-
element.style.pointerEvents = 'none'
14-
}
15-
16-
/**
17-
* Remove CSS containment optimizations after resize/drag completes.
18-
*/
19-
export function removeContainmentOptimizations(element: HTMLElement | null) {
20-
if (!element) return
21-
element.style.contain = ''
22-
element.style.contentVisibility = ''
23-
element.style.containIntrinsicSize = ''
24-
element.style.pointerEvents = ''
25-
}
26-
271
type DraggingStylesParams = {
282
handle: HTMLElement | null
293
pane: HTMLElement | null
304
content: HTMLElement | null
315
}
326

7+
const DATA_DRAGGING_ATTR = 'data-dragging'
8+
339
/** Apply visual feedback and performance optimizations during drag */
3410
export function setDraggingStyles({handle, pane, content}: DraggingStylesParams) {
11+
// Handle visual feedback (must be inline for instant response)
3512
handle?.style.setProperty('background-color', 'var(--bgColor-accent-emphasis)')
3613
handle?.style.setProperty('--draggable-handle--drag-opacity', '1')
37-
// Disable transition for instant visual feedback during drag
3814
handle?.style.setProperty('--draggable-handle--transition', 'none')
39-
setContainmentOptimizations(content)
40-
setContainmentOptimizations(pane)
15+
16+
// Set attribute for CSS containment (O(1) direct selector, not descendant)
17+
pane?.setAttribute(DATA_DRAGGING_ATTR, 'true')
18+
content?.setAttribute(DATA_DRAGGING_ATTR, 'true')
4119
}
4220

4321
/** Remove drag styles and restore normal state */
4422
export function removeDraggingStyles({handle, pane, content}: DraggingStylesParams) {
4523
handle?.style.removeProperty('background-color')
4624
handle?.style.removeProperty('--draggable-handle--drag-opacity')
4725
handle?.style.removeProperty('--draggable-handle--transition')
48-
removeContainmentOptimizations(content)
49-
removeContainmentOptimizations(pane)
26+
27+
pane?.removeAttribute(DATA_DRAGGING_ATTR)
28+
content?.removeAttribute(DATA_DRAGGING_ATTR)
5029
}

packages/react/src/PageLayout/usePaneWidth.test.ts

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -612,7 +612,7 @@ describe('usePaneWidth', () => {
612612
addEventListenerSpy.mockRestore()
613613
})
614614

615-
it('should apply and remove containment styles during resize', async () => {
615+
it('should apply and remove containment attributes during resize', async () => {
616616
vi.useFakeTimers()
617617
vi.stubGlobal('innerWidth', 1280)
618618
const refs = createMockRefs()
@@ -627,32 +627,30 @@ describe('usePaneWidth', () => {
627627
}),
628628
)
629629

630-
// Initially no containment
631-
expect(refs.paneRef.current?.style.contain).toBe('')
632-
expect(refs.contentRef.current?.style.contain).toBe('')
630+
// Initially no data-dragging attribute
631+
expect(refs.paneRef.current?.hasAttribute('data-dragging')).toBe(false)
632+
expect(refs.contentRef.current?.hasAttribute('data-dragging')).toBe(false)
633633

634634
// Fire resize
635635
vi.stubGlobal('innerWidth', 1000)
636636
window.dispatchEvent(new Event('resize'))
637637

638-
// At this point, containment is applied but timing depends on throttle behavior
638+
// At this point, attribute is applied but timing depends on throttle behavior
639639
// The key is that it gets cleaned up after
640640

641641
// Wait for throttle to complete via rAF
642642
await act(async () => {
643643
await vi.runAllTimersAsync()
644644
})
645645

646-
// Containment should be removed after throttle completes
647-
expect(refs.paneRef.current?.style.contain).toBe('')
648-
expect(refs.paneRef.current?.style.contentVisibility).toBe('')
649-
expect(refs.contentRef.current?.style.contain).toBe('')
650-
expect(refs.contentRef.current?.style.contentVisibility).toBe('')
646+
// Attribute should be removed after throttle completes
647+
expect(refs.paneRef.current?.hasAttribute('data-dragging')).toBe(false)
648+
expect(refs.contentRef.current?.hasAttribute('data-dragging')).toBe(false)
651649

652650
vi.useRealTimers()
653651
})
654652

655-
it('should cleanup containment styles on unmount during resize', async () => {
653+
it('should cleanup containment attributes on unmount during resize', async () => {
656654
vi.useFakeTimers()
657655
vi.stubGlobal('innerWidth', 1280)
658656
const refs = createMockRefs()
@@ -671,14 +669,12 @@ describe('usePaneWidth', () => {
671669
vi.stubGlobal('innerWidth', 1000)
672670
window.dispatchEvent(new Event('resize'))
673671

674-
// Unmount immediately (may or may not have styles depending on throttle timing)
672+
// Unmount immediately (may or may not have attributes depending on throttle timing)
675673
unmount()
676674

677-
// Containment should be cleaned up on unmount regardless of timing
678-
expect(refs.paneRef.current?.style.contain).toBe('')
679-
expect(refs.paneRef.current?.style.contentVisibility).toBe('')
680-
expect(refs.contentRef.current?.style.contain).toBe('')
681-
expect(refs.contentRef.current?.style.contentVisibility).toBe('')
675+
// Attribute should be cleaned up on unmount regardless of timing
676+
expect(refs.paneRef.current?.hasAttribute('data-dragging')).toBe(false)
677+
expect(refs.contentRef.current?.hasAttribute('data-dragging')).toBe(false)
682678

683679
vi.useRealTimers()
684680
})

packages/react/src/PageLayout/usePaneWidth.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import React, {startTransition} from 'react'
22
import {canUseDOM} from '../utils/environment'
33
import useIsomorphicLayoutEffect from '../utils/useIsomorphicLayoutEffect'
44
import cssExports from './PageLayout.module.css'
5-
import {setContainmentOptimizations, removeContainmentOptimizations} from './paneUtils'
65

76
// ----------------------------------------------------------------------------
87
// Types
@@ -280,15 +279,15 @@ export function usePaneWidth({
280279
const startResizeOptimizations = () => {
281280
if (isResizing) return
282281
isResizing = true
283-
setContainmentOptimizations(paneRef.current)
284-
setContainmentOptimizations(contentRef.current)
282+
paneRef.current?.setAttribute('data-dragging', 'true')
283+
contentRef.current?.setAttribute('data-dragging', 'true')
285284
}
286285

287286
const endResizeOptimizations = () => {
288287
if (!isResizing) return
289288
isResizing = false
290-
removeContainmentOptimizations(paneRef.current)
291-
removeContainmentOptimizations(contentRef.current)
289+
paneRef.current?.removeAttribute('data-dragging')
290+
contentRef.current?.removeAttribute('data-dragging')
292291
}
293292

294293
const handleResize = () => {

0 commit comments

Comments
 (0)