disable transform settling reflow when panning the graph (#6186)

## Summary

- disable pan tracking in `useTransformSettling` so we stop wiring
high-frequency pointer listeners during canvas drags
- the post-navigation-interaction forced reflow is only necessary when
zooming since it is for fixing pixel stretch that results from `scale`
(which doesn't happen during panning/`translate`)
- extend settle delay to 512ms to reduce unnecessary reflow while
preserving post-zoom pixel fix

After this PR, there should be 0 reflows when panning the graph.

First PR in series to address:

- https://github.com/Comfy-Org/ComfyUI_frontend/issues/6151

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6186-disable-transform-settling-reflow-when-panning-the-graph-2936d73d365081c2b357e3c72d711439)
by [Unito](https://www.unito.io)

---------

Co-authored-by: DrJKL <DrJKL@users.noreply.github.com>
This commit is contained in:
Christian Byrne
2025-10-22 14:33:05 -07:00
committed by GitHub
parent 2f00893b27
commit dc5d41642d
5 changed files with 24 additions and 200 deletions

View File

@@ -67,41 +67,8 @@ describe('useTransformSettling', () => {
expect(isTransforming.value).toBe(false)
})
it('should track pan events when trackPan is enabled', async () => {
const { isTransforming } = useTransformSettling(element, {
trackPan: true,
settleDelay: 200
})
// Pointer down should start transform
element.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }))
await nextTick()
expect(isTransforming.value).toBe(true)
// Pointer move should keep it active
vi.advanceTimersByTime(100)
element.dispatchEvent(new PointerEvent('pointermove', { bubbles: true }))
await nextTick()
// Should still be transforming
expect(isTransforming.value).toBe(true)
// Pointer up
element.dispatchEvent(new PointerEvent('pointerup', { bubbles: true }))
await nextTick()
// Should still be transforming until settle delay
expect(isTransforming.value).toBe(true)
// Advance past settle delay
vi.advanceTimersByTime(200)
expect(isTransforming.value).toBe(false)
})
it('should not track pan events when trackPan is disabled', async () => {
const { isTransforming } = useTransformSettling(element, {
trackPan: false
})
it('should not track pan events', async () => {
const { isTransforming } = useTransformSettling(element)
// Pointer events should not trigger transform
element.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }))
@@ -111,26 +78,6 @@ describe('useTransformSettling', () => {
expect(isTransforming.value).toBe(false)
})
it('should handle pointer cancel events', async () => {
const { isTransforming } = useTransformSettling(element, {
trackPan: true,
settleDelay: 200
})
// Start panning
element.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }))
await nextTick()
expect(isTransforming.value).toBe(true)
// Cancel instead of up
element.dispatchEvent(new PointerEvent('pointercancel', { bubbles: true }))
await nextTick()
// Should still settle normally
vi.advanceTimersByTime(200)
expect(isTransforming.value).toBe(false)
})
it('should work with ref target', async () => {
const targetRef = ref<HTMLElement | null>(null)
const { isTransforming } = useTransformSettling(targetRef)
@@ -177,44 +124,13 @@ describe('useTransformSettling', () => {
element.removeEventListener('wheel', bubbleHandler, false)
})
it('should throttle pointer move events', async () => {
const { isTransforming } = useTransformSettling(element, {
trackPan: true,
pointerMoveThrottle: 50,
settleDelay: 100
})
// Start panning
element.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }))
await nextTick()
// Fire many pointer move events rapidly
for (let i = 0; i < 10; i++) {
element.dispatchEvent(new PointerEvent('pointermove', { bubbles: true }))
vi.advanceTimersByTime(5) // 5ms between events
}
await nextTick()
// Should still be transforming
expect(isTransforming.value).toBe(true)
// End panning
element.dispatchEvent(new PointerEvent('pointerup', { bubbles: true }))
// Advance past settle delay
vi.advanceTimersByTime(100)
expect(isTransforming.value).toBe(false)
})
it('should clean up event listeners when component unmounts', async () => {
const removeEventListenerSpy = vi.spyOn(element, 'removeEventListener')
// Create a test component
const TestComponent = {
setup() {
const { isTransforming } = useTransformSettling(element, {
trackPan: true
})
const { isTransforming } = useTransformSettling(element)
return { isTransforming }
},
template: '<div>{{ isTransforming }}</div>'
@@ -226,52 +142,26 @@ describe('useTransformSettling', () => {
// Unmount component
wrapper.unmount()
// Should have removed all event listeners
// Should have removed wheel event listener
expect(removeEventListenerSpy).toHaveBeenCalledWith(
'wheel',
expect.any(Function),
expect.objectContaining({ capture: true })
)
expect(removeEventListenerSpy).toHaveBeenCalledWith(
'pointerdown',
expect.any(Function),
expect.objectContaining({ capture: true })
)
expect(removeEventListenerSpy).toHaveBeenCalledWith(
'pointermove',
expect.any(Function),
expect.objectContaining({ capture: true })
)
expect(removeEventListenerSpy).toHaveBeenCalledWith(
'pointerup',
expect.any(Function),
expect.objectContaining({ capture: true })
)
expect(removeEventListenerSpy).toHaveBeenCalledWith(
'pointercancel',
expect.any(Function),
expect.objectContaining({ capture: true })
)
})
it('should use passive listeners when specified', async () => {
const addEventListenerSpy = vi.spyOn(element, 'addEventListener')
useTransformSettling(element, {
passive: true,
trackPan: true
passive: true
})
// Check that passive option was used for appropriate events
// Check that passive option was used for wheel event
expect(addEventListenerSpy).toHaveBeenCalledWith(
'wheel',
expect.any(Function),
expect.objectContaining({ passive: true, capture: true })
)
expect(addEventListenerSpy).toHaveBeenCalledWith(
'pointermove',
expect.any(Function),
expect.objectContaining({ passive: true, capture: true })
)
})
})