diff --git a/__tests__/Resizable.test.js b/__tests__/Resizable.test.js index 2107cc26..9ad2c71a 100644 --- a/__tests__/Resizable.test.js +++ b/__tests__/Resizable.test.js @@ -404,6 +404,107 @@ describe('render Resizable', () => { }); }); }); + + describe('onResizeStop with stale props', () => { + // This tests the fix for a bug where onResizeStop would report stale size data + // because React's batched state updates mean props.width/height haven't updated yet + // when onResizeStop fires. The fix stores the last size from onResize and uses it + // in onResizeStop. See: https://github.com/react-grid-layout/react-grid-layout/pull/2224 + + test('onResizeStop reports correct size even when props are stale', () => { + const onResizeStop = jest.fn(); + const onResize = jest.fn(); + const resizableRef = React.createRef(); + render( + + {resizableBoxChildren} + + ); + + // Simulate onResizeStart + const startHandler = resizableRef.current.resizeHandler('onResizeStart', 'se'); + startHandler(mockEvent, { node, deltaX: 0, deltaY: 0 }); + + // Simulate dragging - this calls onResize with the new size + const dragHandler = resizableRef.current.resizeHandler('onResize', 'se'); + dragHandler(mockEvent, { node, deltaX: 20, deltaY: 30 }); + expect(onResize).toHaveBeenLastCalledWith( + mockEvent, + expect.objectContaining({ + size: { width: 70, height: 80 }, + }) + ); + + // Now simulate onResizeStop. In a real app, React may not have re-rendered yet, + // so props.width/height would still be 50. The deltaX/deltaY from DraggableCore's + // onStop is typically 0 or very small since the mouse hasn't moved since the last + // drag event. Without the fix, this would incorrectly report size: {width: 50, height: 50}. + const stopHandler = resizableRef.current.resizeHandler('onResizeStop', 'se'); + stopHandler(mockEvent, { node, deltaX: 0, deltaY: 0 }); + + // With the fix, onResizeStop should report the same size as the last onResize + expect(onResizeStop).toHaveBeenCalledWith( + mockEvent, + expect.objectContaining({ + size: { width: 70, height: 80 }, + }) + ); + }); + + test('onResizeStop reports correct size for west handle with stale props', () => { + const onResizeStop = jest.fn(); + const onResize = jest.fn(); + const resizableRef = React.createRef(); + const testMockClientRect = { left: 0, top: 0 }; + const testNode = document.createElement('div'); + testNode.getBoundingClientRect = () => ({ ...testMockClientRect }); + + render( + + {resizableBoxChildren} + + ); + + // Simulate onResizeStart - this sets lastHandleRect to {left: 0, top: 0} + const startHandler = resizableRef.current.resizeHandler('onResizeStart', 'w'); + startHandler(mockEvent, { node: testNode, deltaX: 0, deltaY: 0 }); + + // Simulate dragging west (left) + // deltaX = -15 from drag, plus position adjustment of -15 (handle moved from 0 to -15) + // Total deltaX = -30, reversed for 'w' = +30, so width = 50 + 30 = 80 + const dragHandler = resizableRef.current.resizeHandler('onResize', 'w'); + testMockClientRect.left = -15; + dragHandler(mockEvent, { node: testNode, deltaX: -15, deltaY: 0 }); + expect(onResize).toHaveBeenLastCalledWith( + mockEvent, + expect.objectContaining({ + size: { width: 80, height: 50 }, + }) + ); + + // Continue dragging - element moves further left + // position adjustment: -25 - (-15) = -10, deltaX becomes -10 + (-10) = -20 + // reversed for 'w' = +20, width = 50 + 20 = 70 + testMockClientRect.left = -25; + dragHandler(mockEvent, { node: testNode, deltaX: -10, deltaY: 0 }); + expect(onResize).toHaveBeenLastCalledWith( + mockEvent, + expect.objectContaining({ + size: { width: 70, height: 50 }, + }) + ); + + // onResizeStop with stale props - should use stored lastSize (70x50 from last onResize) + const stopHandler = resizableRef.current.resizeHandler('onResizeStop', 'w'); + stopHandler(mockEvent, { node: testNode, deltaX: 0, deltaY: 0 }); + expect(onResizeStop).toHaveBeenCalledWith( + mockEvent, + expect.objectContaining({ + size: { width: 70, height: 50 }, + }) + ); + }); + }); }); // ============================================ @@ -588,14 +689,17 @@ describe('render Resizable', () => { const node = document.createElement('div'); node.getBoundingClientRect = () => ({ left: 0, top: 0 }); - // First start + // Start resize const startHandler = resizableRef.current.resizeHandler('onResizeStart', 'se'); startHandler(mockEvent, { node, deltaX: 0, deltaY: 0 }); - // Then stop with a delta - Resizable is stateless so size is calculated from - // props.width/height + delta at time of stop + // Drag to resize - this stores the size for onResizeStop to use + const dragHandler = resizableRef.current.resizeHandler('onResize', 'se'); + dragHandler(mockEvent, { node, deltaX: 10, deltaY: 10 }); + + // Stop resize - uses the stored size from the last onResize const stopHandler = resizableRef.current.resizeHandler('onResizeStop', 'se'); - stopHandler(mockEvent, { node, deltaX: 10, deltaY: 10 }); + stopHandler(mockEvent, { node, deltaX: 0, deltaY: 0 }); expect(props.onResizeStop).toHaveBeenCalledWith( mockEvent, diff --git a/lib/Resizable.js b/lib/Resizable.js index a2eede3d..fb62f8cf 100644 --- a/lib/Resizable.js +++ b/lib/Resizable.js @@ -24,13 +24,14 @@ export default class Resizable extends React.Component { handleRefs: {[key: ResizeHandleAxis]: ReactRef} = {}; lastHandleRect: ?ClientRect = null; slack: ?[number, number] = null; + lastSize: ?{width: number, height: number} = null; componentWillUnmount() { this.resetData(); } resetData() { - this.lastHandleRect = this.slack = null; + this.lastHandleRect = this.slack = this.lastSize = null; } // Clamp width and height within provided constraints @@ -132,8 +133,20 @@ export default class Resizable extends React.Component { // Run user-provided constraints. [width, height] = this.runConstraints(width, height); + // For onResizeStop, use the last size from onResize rather than recalculating. + // This avoids issues where props.width/height are stale due to React's batched updates. + if (handlerName === 'onResizeStop' && this.lastSize) { + ({width, height} = this.lastSize); + } + const dimensionsChanged = width !== this.props.width || height !== this.props.height; + // Store the size for use in onResizeStop. We do this after the onResizeStop check + // above so we don't overwrite the stored value with a potentially stale calculation. + if (handlerName !== 'onResizeStop') { + this.lastSize = {width, height}; + } + // Call user-supplied callback if present. const cb = typeof this.props[handlerName] === 'function' ? this.props[handlerName] : null; // Don't call 'onResize' if dimensions haven't changed.