how to make react-bootstrap modal draggable
Asked Answered
M

4

7

I've tried making this to work, but this is what happened.

  1. Using react-draggable npm package, I was able to make contents draggable and droppable. But the back of whole dialog stays in place, and it looks broken afterwards.

I also found this online https://gist.github.com/burgalon/870a68de68c5ed15c416fab63589d503,

import { Modal } from 'react-bootstrap'
import ModalDialog from 'react-bootstrap/lib/ModalDialog'
import Draggable from 'react-draggable'

class DraggableModalDialog extends React.Component {
    render() {
        return <Draggable handle=".modal-title"><ModalDialog 
{...this.props} /></Draggable>
    }
}

// enforceForce=false causes recursion exception otherwise....
export default ({titleIconClass, modalClass, children, title,...props}) =>
<Modal dialogComponent={DraggableModalDialog} show={true} enforceFocus={false} backdrop="static" {...props}>
    <Modal.Header closeButton>
        <Modal.Title>
            {title}
        </Modal.Title>
    </Modal.Header>
    <Modal.Body>
        {children}
    </Modal.Body>
</Modal>

This code I got from the searching around, I can't actually get this to work.


Especially this,

<ModalDialog {...this.props} />

, I do not get why the props are sent in and what kind of props are sent in.

And

<Modal dialogComponent={DraggableModalDialog} show={true} enforceFocus={false} backdrop="static" {...props}>

<------ {...props} what does that do? it doesn't seem like it's giving props to Modal. what is the purpose of it? Is it relevant to

"<ModalDialog {...this.props} />"

?

Can anyone give me a hint how those above two questions can work if this is a valid work?

Thank you!

Markswoman answered 28/8, 2017 at 23:32 Comment(12)
Did you read the entire gist? The last comment there is that you should import Draggable from 'react-draggable';Willard
yes I have that installed and imported as well..Markswoman
So update the question with the relevant updated code and add the error you getWillard
But you didn't include the error you got :( how can someone help?Willard
T___T.. I didn't get any error. It just showed me nothing. I tried to console.log out to see if it even works, but It didn't. I'm so confused with this code I got from online. Im having hard time understanding itMarkswoman
Create a jsfiddle with a working example, otherwise it's really hard to understand what/where is the problemWillard
I see. Let me try it. Thank you so much for your guide.Markswoman
I couldn't get that code working T__T... I think I have to understand what those props being sent in without prop name is doing first. I've never seen that kind of passing in.Markswoman
It will pass all the props to the componentWillard
why is it necessary to do it to <ModalDialog {...this.props} />? This is component called from react-bootstrap.Markswoman
Because if you don't know all the props that might come but you want to pass-on all of them - this is the way to do so.Willard
My understanding to this code's work flow is I call the export default function, then dialogComponent calls the class="draggableModalDialog". Then the class returns the draggable dialog component that is imported from ModalDialog from 'react-bootstrap/lib/ModalDialog'. But why need to add in props for that imported component like this <ModalDialog {...this.props} />?Markswoman
M
8

For anyone who might be still struggling with the latest version of react-bootstrap ( mine is 1.0.0-beta.5 at the writing time). Here is the modified version of (https://gist.github.com/burgalon/870a68de68c5ed15c416fab63589d503)

import React, { Component } from "react";
import Modal from "react-bootstrap/Modal";
import Draggable from 'react-draggable';
import ModalDialog from 'react-bootstrap/ModalDialog';

class DraggableModalDialog extends React.Component {
    render() {
        return <Draggable handle=".modal-title"><ModalDialog {...this.props} /> 
   </Draggable>
    }
}

export default class BSModal extends Component {

render() {
    return (
        <Modal
                dialogAs={DraggableModalDialog} 
                show={this.props.show} 
                onHide={this.props.close}>
            <Modal.Header>
                <Modal.Title>{this.props.title}</Modal.Title>
            </Modal.Header>
            <Modal.Body>
                {this.props.children}
            </Modal.Body>
            <Modal.Footer >
            </Modal.Footer>
        </Modal>
    );
}
}
Manhole answered 10/5, 2019 at 17:11 Comment(3)
is there any way to make the whole header draggable, yet still have the exit-button functionality, since it is included in the header?Lactescent
@YoungScooter I guess, it's gonna be based.modal-title class so if one quick way is to add width:100% to .modal-title.Manhole
You can use .modal-header insteadSkardol
S
1

I had a warning that findDOMNode is deprecated in StrictMode

I was able to get rid of this by using nodeRef instead of handle. My function component/typescript DraggableModalDialog looks like this:

/* eslint-disable react/jsx-props-no-spreading */
import React, { RefObject } from 'react';
import Draggable from 'react-draggable';
import ModalDialog, { ModalDialogProps } from 'react-bootstrap/ModalDialog';

export interface DraggableModalDialogProps extends ModalDialogProps {
  nodeRef?: RefObject<HTMLElement>;
}

function DraggableModalDialog({ nodeRef, ...props }: DraggableModalDialogProps) {
  return (
    <Draggable nodeRef={nodeRef}>
      <ModalDialog {...props} />
    </Draggable>
  );
}

export default DraggableModalDialog;

Modal component is like this (Simplified it here a bit):

import React, { useRef } from 'react';
import { Modal, Button } from 'react-bootstrap';
import DraggableModalDialog from 'components/DraggableModalDialog';

export interface MyModalProps {
  visible: boolean;
  onClose: () => void;
}

function MyModal({ visible, onClose }: MyModalProps) {
  const nodeRef = useRef<HTMLDivElement>(null);
  return (
    <Modal
      nodeRef={nodeRef}
      dialogAs={DraggableModalDialog}
      show={visible}
      centered
      size="lg"
      backdrop="static"
      onHide={onClose}
    >
      <Modal.Header ref={nodeRef}>
        <Modal.Title>TITLE</Modal.Title>
      </Modal.Header>
      <Modal.Body>BODY</Modal.Body>
      <Modal.Footer>
        <Button variant="primary" onClick={onClose}>CLOSE</Button>
      </Modal.Footer>
    </Modal>
  );
}

export default MyModal;
Stingo answered 3/1, 2023 at 14:18 Comment(0)
B
0

I think this solution is more simple and you could use the whole modal header to drag the modal. However I use reactstrap!

import { Modal, ModalHeader, ModalBody } from "reactstrap";
import Draggable from "react-draggable";

...

<Draggable handle=".handle">
  <Modal size="lg" toggle={function noRefCheck(){}}>
    <ModalHeader toggle={function noRefCheck(){}} className="handle">
      Modal Title
    </ModalHeader>
    <ModalBody>
      Modal Body
    </ModalBody>
  </Modal>
</Draggable>
Boren answered 13/4, 2022 at 12:26 Comment(4)
Not sure if this works because the Modal is not the same as ModalDialog. At least I couldn't drag the whole Modal. But I used the .modal-header as the handle in my DraggableModalDialog so the whole header can be used as handleStingo
@Stingo What do you mean with ModalDialog? Reactstrap has only the component Modal.Boren
my apologies, I was too hasty and didn't notice your solution was not for react-bootstrap. If react-bootstrap is not mandatory, your reactstrap solution is more simple I think.Stingo
OP was asking about react-bootstrap.Lacielacing
L
0

For anyone still struggling with this, i found a solution without using the draggable library that works smoothly.

Create 4 state variables for your modal. Two for the Modal header, and two for the main Component. Create a ref for the header aswell.

const [headerLeft, setHeaderLeft] = useState(0);
const [left, setLeft] = useState('0');
const [headerTop, setHeaderTop] = useState(0);
const [top, setTop] = useState('0');
const modalHeader = useRef<HTMLDivElement>(null);

Using the ref you created for the header, add a listener on the modal header OnMouseDown. Then add a listener on the document to implement your dragging mechanism.

modalHeader.current?.addEventListener("mousedown", () => {
    document.addEventListener("mousemove", onDrag);
});
    

Then create your dragging function. I've added an id on my modal header to be able to get the HTMLElement using getElementById. Then i'm calculating the new position based on top & left vars and update my state.

function onDrag(e) {
    const modalHeaderElement = document.getElementById('modal-header');
    if (modalHeaderElement) {
        const getStyle = window.getComputedStyle(modalHeaderElement);
        const leftVal = parseInt(getStyle.left);
        const topVal = parseInt(getStyle.top);
    
        setHeaderLeft(e.movementX + leftVal);
        setHeaderTop(e.movementY + topVal);
        setLeft(`${leftVal + e.movementX}px`);
        setTop(`${topVal + e.movementY}px`);
    }
}

Then all you need to do is update the style prop on the modal & modal header components

<Modal style={{ left: left, top: top }} >
     <Modal.Header id='modal-header' ref={modalHeader}  style={{ left: headerLeft, top: headerTop}}>A Header </Modal.Header>
<Modal.Body></Modal.Body>
</Modal>

Finally add another listener on the document for the mouseUp to stop dragging

document.addEventListener("mouseup", () => {
   document.removeEventListener("mousemove", onDrag);
});

In my use-case, i've placed the addEventListeners and OnDrag function inside a useImperativeHandle hook because that's how i utilize my Modal (I'm wrapping it with a forwardRef). But i'm guessing you can do that inside a useEffect and work just as well.

Edit: You can also init the state variables to not keep your modal in last position that you dragged it.

Edit 2: Posting the code below to make it a custom hook and re-use it in your code. The idea is that the custom hook will return the state variables to update the coordinates of your modal. The input of the custom hook is the current of your modalHeader ref , the id you've provided in the Modal.Header element and the show state variable that shows or hides your modal.

    export function useDraggable(modalHeader: HTMLDivElement | null, modalHeaderId: string, show: boolean) {
        const [headerLeft, setHeaderLeft] = useState(0);
        const [left, setLeft] = useState('0px');
        const [headerTop, setHeaderTop] = useState(0);
        const [top, setTop] = useState('0px');
    
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        const onDrag = useCallback((e: any) => {
            const modalHeaderElement = document.getElementById(modalHeaderId);
            if (modalHeaderElement) {
                const getStyle = window.getComputedStyle(modalHeaderElement);
                const leftVal = parseInt(getStyle.left);
                const topVal = parseInt(getStyle.top);
    
                setHeaderLeft(_prevState => e.movementX + leftVal);
                setHeaderTop(_prevState => e.movementY + topVal);
                setLeft(_prevState => `${leftVal + e.movementX}px`);
                setTop(_prevState => `${topVal + e.movementY}px`);
            }
        }, []);
    
        const onMouseUp = useCallback(() => {
            document.removeEventListener("mousemove", onDrag);
            const modalHeaderElement = document.getElementById(modalHeaderId);
            if (modalHeaderElement) {
                modalHeaderElement.classList.remove("active");
            }
        }, []);
    
        useEffect(() => { 
            const modalHeaderElement = document.getElementById(modalHeaderId);
            if (modalHeaderElement && show) {
                modalHeaderElement.addEventListener("mousedown", () => {
                    modalHeaderElement.classList.add("active");
                    document.addEventListener("mousemove", onDrag);
                });
    
                document.addEventListener("mouseup", onMouseUp);
            }
    
            if (!show) {
                if (modalHeader) {
                    document.removeEventListener("mouseup", onMouseUp);
                }
                setLeft('0px');
                setTop('0px');
                setHeaderLeft(0);
                setHeaderTop(0);
            }
    
        }, [modalHeaderId, modalHeader, show, onMouseUp, onDrag])
    
        return {
            headerTop,
            headerLeft,
            top,
            left
        };
    }
Lawn answered 12/1, 2024 at 12:13 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.