Bring draggable div to the front in React.js when user clicks on it
Asked Answered
P

2

10

I want to bring a draggable box to the front when I click on it or drag it.

I don't know in advance the maximum number of such boxes because I have a button that creates a new draggable box when the user clicks on the button.

The button should always be on top no matter how many boxes there are, so its z-index should always be the greatest.

So these are my problems:

  1. How to bring a box to the front when the user clicks on it.
  2. How to make the button stay at the front.

And I am new to ReactJS.

This is what I currently have in MyComponent.js

import React, { useState }  from 'react';
import Draggable from 'react-draggable';

function MyComponent() {
    const [currZIndex, setZIndex] = useState(0);

    const bringForward = () => {
        setZIndex(currZIndex + 1);
    }

    return (
        <Draggable onMouseDown={bringForward}>
            <div className="mydiv" style={{zIndex: currZIndex}}></div>
        </Draggable>
    );
}

The problem of my current implementation is that each component knows only its own z-index, but does not know what is the current highest z-index. And z-index increases whenever I click on any div, so this makes the z-indexes unnecessarily large.

If div1 has a z-index of 6 and div2 has a z-index of 2, I have to click div2 5 times to make its z-index become 7 in order to bring div2 to the front.

I still haven't come up with an idea on how to deal with the z-index of the button.

Fyr this is what I have in App.js

import React, { useState } from 'react';
import MyComponent from './MyComponent';

function App() {
  const [componentList, setComponentList] = useState([]);

  function addNewComponent() {
    setComponentList(componentList.concat(<MyComponent key={componentList.length} />));
  }

  return (
    <div>
      <button onClick={addNewComponent}>New Component</button>
      {componentList}
    </div>
  );
}
Pastorate answered 14/2, 2023 at 17:10 Comment(0)
M
7

If you only care about the "stack level" of the item being dragged and not the others then set a z-index — any z-index — on that item only. Items without z-index (or with identical z-index) have their stack level determined by their source order (specs). So you would:

  • Create a stacking context on the container (e.g. by adding position: relative; z-index: 1)
  • Set z-index: 1 on the element on drag-start and remove it on drag-end
  • Set z-index: 2 on the button

StackBlitz


If you do care about the stack level of all items then there is absolutely no need to worry about "unnecessarily large" values — there is no restriction in CSS. You just need to keep track of the current highest z-index:

StackBlitz


If you really want to restrict the values for z-index, then you need to:

  • identify the item that is being dragged
  • subtract 1 from all items that have a z-index higher than that item
  • assign the largest value to that item

StackBlitz

Maurili answered 4/12, 2023 at 12:3 Comment(0)
H
6

Let's address the problems one by one. First the easy one:

1. How to make the button stay at the top no matter how many boxes there are, so its z-index should always be the greatest?

One way to achieve this requirement is:

  1. Wrap the <button> and MyComponent in a separate div.
  2. Provide the z-index for <button> div higher than that of MyComponent div.
  3. Use position: relative on both button container(div) and MyComponent container(div) to establish separate stacking contexts. You can refer this article to read more about it.

Following the above steps the button will always be at the top no matter how many draggable elements you create.

return (
    <div>
      <div style={{ position: "relative", zIndex: 2 }}>
        <button onClick={addNewComponent}>New Component</button>
      </div>
      <div style={{ position: "relative", zIndex: 1 }}>
        {componentList.map((item) => (
          <MyComponent
            key={item.id}
            zIndex={item.zIndex}
            bringToFront={() => bringToFront(item.id)}
          />
        ))}
      </div>
    </div>
  );

2. How to bring the draggable box to the top(but behind button) when user clicks/drags it?

Now, to solve this there can be multiple ways. One way which I would use is to play with the number of draggable component created in view. Let me elaborate by breaking it into steps.

  1. Maintain a state in parent component which will keep track of total number of draggable components.

    const [componentList, setComponentList] = useState([]);
    
  2. Make <MyComponent />(logically every draggable box) to accept two props; zIndex and bringToFront.

  3. On click of button add a new entry inside componentList(i.e. setState) with the object:

     {
        id: prevComponentList.length, 
        zIndex: 0,
      }
    
  4. Now, when a particular component is clicked or dragged(mousedown event is triggered) and bringToFront callback function is invoked with id of that box. Now, map through the componentList and find the id of currently dragged/clicked element and increase it's z-index value to max length of elements available in prevComponentList else(if the id does not matches the element id) decrease the current z-index by 1.

     function bringToFront(id) {
      setComponentList((prevComponentList) =>
        prevComponentList.map((item) =>
          item.id === id
            ? { ...item, zIndex: prevComponentList.length }
            : { ...item, zIndex: item.zIndex - 1 },
        ),
      );
    }
    

EDIT (h/t Salman A):

The above solution works for your current case. However, as you can notice the z-index is decremented unconditionally. To fix that we can put a condition on the zIndex that it should either be clamped to 0 or should lie between 1 to total number of draggable boxes in view(the second one looks more desirable). To achieve the aforementioned, you will need to edit the bringToFront function something like below:

//... Rest of the code
{
        id: prevComponentList.length,
        zIndex: prevComponentList.length + 1, 
      },
// ...Rest
function bringToFront(id) {
    setComponentList((prevComponentList) => {
      const tempZidx = prevComponentList.find((item) => item.id === id).zIndex;

      return prevComponentList.map((item) => {
        if (item.id === id) {
          return { ...item, zIndex: prevComponentList.length };
        } else if (item.zIndex > tempZidx) {
          return { ...item, zIndex: item.zIndex - 1 };
        }
        return item;
      });
    });
  }

DEMO IMPLEMENTATION

Suggestions and further reading:

  1. 4 reasons your z-index is not working
  2. Using Styled components for styling
Housebound answered 2/12, 2023 at 10:26 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.