Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 21 additions & 7 deletions lib/DraggableCore.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -269,6 +272,8 @@ export default class DraggableCore extends React.Component<Partial<DraggableCore

mounted: boolean = false;

warnedMissingNodeRef: boolean = false;

componentDidMount() {
this.mounted = true;
// Touch handlers must be added with {passive: false} to be cancelable.
Expand Down Expand Up @@ -301,16 +306,19 @@ export default class DraggableCore extends React.Component<Partial<DraggableCore
if (this.props?.nodeRef) {
return this.props.nodeRef.current;
}
// ReactDOM.findDOMNode was removed in React 19
const legacyReactDOM = ReactDOM as unknown as {findDOMNode?: (instance: 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;
}

Expand All @@ -324,6 +332,12 @@ export default class DraggableCore extends React.Component<Partial<DraggableCore
// Get nodes. Be sure to grab relative document (could be iframed)
const thisNode = this.findDOMNode();
if (!thisNode || !thisNode.ownerDocument || !thisNode.ownerDocument.body) {
if (!this.props.nodeRef && typeof legacyReactDOM.findDOMNode !== 'function') {
throw new Error(
'<DraggableCore> 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('<DraggableCore> not mounted on DragStart!');
}
const {ownerDocument} = thisNode;
Expand Down
49 changes: 49 additions & 0 deletions test/Draggable.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<Draggable>
<div />
</Draggable>
);

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(
<React.StrictMode>
<Draggable>
<div />
</Draggable>
</React.StrictMode>
);

expect(warnSpy).toHaveBeenCalledTimes(1);
warnSpy.mockRestore();
});

it('should not warn when nodeRef is provided', () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});

const { container } = render(
<DraggableWrapper>
<div />
</DraggableWrapper>
);
act(() => {
simulateDrag(container.firstChild, { from: { x: 0, y: 0 }, to: { x: 100, y: 100 } });
});

expect(warnSpy).not.toHaveBeenCalled();
warnSpy.mockRestore();
});
});
});
31 changes: 31 additions & 0 deletions test/DraggableCore.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<DraggableCore ref={coreRef}>
<div />
</DraggableCore>
);

// 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(
<DraggableCore ref={coreRef} nodeRef={nodeRef}>
<div />
</DraggableCore>
);

expect(() => coreRef.current.handleDragStart(new MouseEvent('mousedown'))).toThrow('<DraggableCore> not mounted on DragStart!');
expect(warnSpy).not.toHaveBeenCalled();
warnSpy.mockRestore();
});
});
});