@@ -404,6 +404,107 @@ describe('render Resizable', () => {
404404 } ) ;
405405 } ) ;
406406 } ) ;
407+
408+ describe ( 'onResizeStop with stale props' , ( ) => {
409+ // This tests the fix for a bug where onResizeStop would report stale size data
410+ // because React's batched state updates mean props.width/height haven't updated yet
411+ // when onResizeStop fires. The fix stores the last size from onResize and uses it
412+ // in onResizeStop. See: https://github.com/react-grid-layout/react-grid-layout/pull/2224
413+
414+ test ( 'onResizeStop reports correct size even when props are stale' , ( ) => {
415+ const onResizeStop = jest . fn ( ) ;
416+ const onResize = jest . fn ( ) ;
417+ const resizableRef = React . createRef ( ) ;
418+ render (
419+ < Resizable { ...customProps } onResize = { onResize } onResizeStop = { onResizeStop } ref = { resizableRef } >
420+ { resizableBoxChildren }
421+ </ Resizable >
422+ ) ;
423+
424+ // Simulate onResizeStart
425+ const startHandler = resizableRef . current . resizeHandler ( 'onResizeStart' , 'se' ) ;
426+ startHandler ( mockEvent , { node, deltaX : 0 , deltaY : 0 } ) ;
427+
428+ // Simulate dragging - this calls onResize with the new size
429+ const dragHandler = resizableRef . current . resizeHandler ( 'onResize' , 'se' ) ;
430+ dragHandler ( mockEvent , { node, deltaX : 20 , deltaY : 30 } ) ;
431+ expect ( onResize ) . toHaveBeenLastCalledWith (
432+ mockEvent ,
433+ expect . objectContaining ( {
434+ size : { width : 70 , height : 80 } ,
435+ } )
436+ ) ;
437+
438+ // Now simulate onResizeStop. In a real app, React may not have re-rendered yet,
439+ // so props.width/height would still be 50. The deltaX/deltaY from DraggableCore's
440+ // onStop is typically 0 or very small since the mouse hasn't moved since the last
441+ // drag event. Without the fix, this would incorrectly report size: {width: 50, height: 50}.
442+ const stopHandler = resizableRef . current . resizeHandler ( 'onResizeStop' , 'se' ) ;
443+ stopHandler ( mockEvent , { node, deltaX : 0 , deltaY : 0 } ) ;
444+
445+ // With the fix, onResizeStop should report the same size as the last onResize
446+ expect ( onResizeStop ) . toHaveBeenCalledWith (
447+ mockEvent ,
448+ expect . objectContaining ( {
449+ size : { width : 70 , height : 80 } ,
450+ } )
451+ ) ;
452+ } ) ;
453+
454+ test ( 'onResizeStop reports correct size for west handle with stale props' , ( ) => {
455+ const onResizeStop = jest . fn ( ) ;
456+ const onResize = jest . fn ( ) ;
457+ const resizableRef = React . createRef ( ) ;
458+ const testMockClientRect = { left : 0 , top : 0 } ;
459+ const testNode = document . createElement ( 'div' ) ;
460+ testNode . getBoundingClientRect = ( ) => ( { ...testMockClientRect } ) ;
461+
462+ render (
463+ < Resizable { ...customProps } onResize = { onResize } onResizeStop = { onResizeStop } ref = { resizableRef } >
464+ { resizableBoxChildren }
465+ </ Resizable >
466+ ) ;
467+
468+ // Simulate onResizeStart - this sets lastHandleRect to {left: 0, top: 0}
469+ const startHandler = resizableRef . current . resizeHandler ( 'onResizeStart' , 'w' ) ;
470+ startHandler ( mockEvent , { node : testNode , deltaX : 0 , deltaY : 0 } ) ;
471+
472+ // Simulate dragging west (left)
473+ // deltaX = -15 from drag, plus position adjustment of -15 (handle moved from 0 to -15)
474+ // Total deltaX = -30, reversed for 'w' = +30, so width = 50 + 30 = 80
475+ const dragHandler = resizableRef . current . resizeHandler ( 'onResize' , 'w' ) ;
476+ testMockClientRect . left = - 15 ;
477+ dragHandler ( mockEvent , { node : testNode , deltaX : - 15 , deltaY : 0 } ) ;
478+ expect ( onResize ) . toHaveBeenLastCalledWith (
479+ mockEvent ,
480+ expect . objectContaining ( {
481+ size : { width : 80 , height : 50 } ,
482+ } )
483+ ) ;
484+
485+ // Continue dragging - element moves further left
486+ // position adjustment: -25 - (-15) = -10, deltaX becomes -10 + (-10) = -20
487+ // reversed for 'w' = +20, width = 50 + 20 = 70
488+ testMockClientRect . left = - 25 ;
489+ dragHandler ( mockEvent , { node : testNode , deltaX : - 10 , deltaY : 0 } ) ;
490+ expect ( onResize ) . toHaveBeenLastCalledWith (
491+ mockEvent ,
492+ expect . objectContaining ( {
493+ size : { width : 70 , height : 50 } ,
494+ } )
495+ ) ;
496+
497+ // onResizeStop with stale props - should use stored lastSize (70x50 from last onResize)
498+ const stopHandler = resizableRef . current . resizeHandler ( 'onResizeStop' , 'w' ) ;
499+ stopHandler ( mockEvent , { node : testNode , deltaX : 0 , deltaY : 0 } ) ;
500+ expect ( onResizeStop ) . toHaveBeenCalledWith (
501+ mockEvent ,
502+ expect . objectContaining ( {
503+ size : { width : 70 , height : 50 } ,
504+ } )
505+ ) ;
506+ } ) ;
507+ } ) ;
407508 } ) ;
408509
409510 // ============================================
@@ -588,14 +689,17 @@ describe('render Resizable', () => {
588689 const node = document . createElement ( 'div' ) ;
589690 node . getBoundingClientRect = ( ) => ( { left : 0 , top : 0 } ) ;
590691
591- // First start
692+ // Start resize
592693 const startHandler = resizableRef . current . resizeHandler ( 'onResizeStart' , 'se' ) ;
593694 startHandler ( mockEvent , { node, deltaX : 0 , deltaY : 0 } ) ;
594695
595- // Then stop with a delta - Resizable is stateless so size is calculated from
596- // props.width/height + delta at time of stop
696+ // Drag to resize - this stores the size for onResizeStop to use
697+ const dragHandler = resizableRef . current . resizeHandler ( 'onResize' , 'se' ) ;
698+ dragHandler ( mockEvent , { node, deltaX : 10 , deltaY : 10 } ) ;
699+
700+ // Stop resize - uses the stored size from the last onResize
597701 const stopHandler = resizableRef . current . resizeHandler ( 'onResizeStop' , 'se' ) ;
598- stopHandler ( mockEvent , { node, deltaX : 10 , deltaY : 10 } ) ;
702+ stopHandler ( mockEvent , { node, deltaX : 0 , deltaY : 0 } ) ;
599703
600704 expect ( props . onResizeStop ) . toHaveBeenCalledWith (
601705 mockEvent ,
0 commit comments