diff --git a/lib/DraggableCore.tsx b/lib/DraggableCore.tsx index c7f80043..91e434dc 100644 --- a/lib/DraggableCore.tsx +++ b/lib/DraggableCore.tsx @@ -30,6 +30,9 @@ const eventsFor = { // Default to mouse events. let dragEventFor = eventsFor.mouse; +// ReactDOM.findDOMNode was removed in React 19 +const legacyReactDOM = ReactDOM as unknown as {findDOMNode?: (instance: React.Component) => HTMLElement | null}; + export type DraggableCoreDefaultProps = { allowAnyClick: boolean, allowMobileScroll: boolean, @@ -269,6 +272,8 @@ export default class DraggableCore extends React.Component HTMLElement | null}; if (typeof legacyReactDOM.findDOMNode === 'function') { return legacyReactDOM.findDOMNode(this); } - // In React 19+, nodeRef is required - log a warning via our log utility - log( - 'react-draggable: ReactDOM.findDOMNode is not available in React 19+. ' + - 'You must provide a nodeRef prop. See: https://github.com/react-grid-layout/react-draggable#noderef' - ); + // In React 19+, nodeRef is required. Warn once per instance; `log` is a no-op + // unless DRAGGABLE_DEBUG is set, so users would never see it. + if (!this.warnedMissingNodeRef) { + this.warnedMissingNodeRef = true; + // eslint-disable-next-line no-console + console.warn( + 'react-draggable: ReactDOM.findDOMNode is not available in React 19+. ' + + 'You must provide a nodeRef prop. See: https://github.com/react-grid-layout/react-draggable#using-noderef' + ); + } return null; } @@ -324,6 +332,12 @@ export default class DraggableCore extends React.Component has no DOM node on DragStart: no nodeRef prop was provided, and ReactDOM.findDOMNode ' + + 'is not available in React 19+. See: https://github.com/react-grid-layout/react-draggable#using-noderef' + ); + } throw new Error(' not mounted on DragStart!'); } const {ownerDocument} = thisNode; diff --git a/test/Draggable.test.jsx b/test/Draggable.test.jsx index 54601988..e835311a 100644 --- a/test/Draggable.test.jsx +++ b/test/Draggable.test.jsx @@ -431,4 +431,53 @@ describe('Draggable', () => { expect(container.firstChild).toBeTruthy(); }); }); + + // These run on React 19, which has no ReactDOM.findDOMNode. + describe('missing nodeRef', () => { + it('should warn once on mount when nodeRef is not provided', () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + render( + +
+ + ); + + expect(warnSpy).toHaveBeenCalledTimes(1); + expect(warnSpy.mock.calls[0][0]).toContain('nodeRef'); + expect(warnSpy.mock.calls[0][0]).toContain('#using-noderef'); + warnSpy.mockRestore(); + }); + + it('should warn only once under StrictMode', () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + render( + + +
+ + + ); + + expect(warnSpy).toHaveBeenCalledTimes(1); + warnSpy.mockRestore(); + }); + + it('should not warn when nodeRef is provided', () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + const { container } = render( + +
+ + ); + act(() => { + simulateDrag(container.firstChild, { from: { x: 0, y: 0 }, to: { x: 100, y: 100 } }); + }); + + expect(warnSpy).not.toHaveBeenCalled(); + warnSpy.mockRestore(); + }); + }); }); diff --git a/test/DraggableCore.test.jsx b/test/DraggableCore.test.jsx index 11ed75cd..2dacf53b 100644 --- a/test/DraggableCore.test.jsx +++ b/test/DraggableCore.test.jsx @@ -620,5 +620,36 @@ describe('DraggableCore', () => { expect(threw).toBe(true); errorSpy.mockRestore(); }); + + it('should name nodeRef in the drag start error when nodeRef is not provided', () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const coreRef = React.createRef(); + render( + +
+ + ); + + // React 19 has no ReactDOM.findDOMNode, so without nodeRef there is no node to drag. + expect(() => coreRef.current.handleDragStart(new MouseEvent('mousedown'))).toThrow(/nodeRef/); + // The warning from mount is not repeated on drag start. + expect(warnSpy).toHaveBeenCalledTimes(1); + warnSpy.mockRestore(); + }); + + it('should keep the old drag start error when nodeRef is provided but not attached', () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const coreRef = React.createRef(); + const nodeRef = React.createRef(); + render( + +
+ + ); + + expect(() => coreRef.current.handleDragStart(new MouseEvent('mousedown'))).toThrow(' not mounted on DragStart!'); + expect(warnSpy).not.toHaveBeenCalled(); + warnSpy.mockRestore(); + }); }); });