Need help creating Drag and Drop Material UI Tabs

1.1k Views Asked by At

I've got a simple setup for some MUI tabs that I'm working to implement some drag and drop functionality to. The issue I'm running into is that when the SortableContext is nested inside of the TabList component, drag and drop works but the values no longer work for the respective tabs. When I move the SortableContext outside of the TabList component the values work again, but the drag and drop doesn't. If anybody has any guidance here that would be greatly appreciated!

Here is a link to a CodeSandbox using the code below: https://codesandbox.io/s/material-ui-tabs-with-drag-n-drop-functionality-05ktf3

Below is my code snippet:

import { Box } from "@mui/material";
import { useState } from "react";
import { DndContext, closestCenter } from "@dnd-kit/core";
import { arrayMove, SortableContext, rectSortingStrategy } from "@dnd-kit/sortable";
import SortableTab from "./SortableTab";
import { TabContext, TabList } from "@mui/lab";

const App = () => {
    const [items, setItems] = useState(["Item One", "Item Two", "Item Three", "Item Four", "Item Five"]);
    const [activeTab, setActiveTab] = useState("0");

    const handleDragEnd = (event) => {
        const { active, over } = event;
        console.log("Drag End Called");

        if (active.id !== over.id) {
            setItems((items) => {
                const oldIndex = items.indexOf(active.id);
                const newIndex = items.indexOf(over.id);
                return arrayMove(items, oldIndex, newIndex);
            });
        }
    };

    const handleChange = (event, newValue) => {
        setActiveTab(newValue);
    };

    return (
        <div>
            <Box sx={{ width: "100%" }}>
                <Box sx={{ borderBottom: 1, borderColor: "divider" }}>
                    <TabContext value={activeTab}>
                        <DndContext collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
                            <SortableContext items={items} strategy={rectSortingStrategy}>
                                <TabList onChange={handleChange} aria-label="basic tabs example">
                                    {items.map((item, index) => (
                                        <SortableTab value={index.toString()} key={item} id={item} index={index} label={item} />
                                    ))}
                                </TabList>
                            </SortableContext>
                        </DndContext>
                    </TabContext>
                </Box>
            </Box>
        </div>
    );
};

export default App;
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { Tab, IconButton } from "@mui/material";
import DragIndicatorIcon from "@mui/icons-material/DragIndicator";

const SortableTab = (props) => {
    const { attributes, listeners, setNodeRef, transform, transition, setActivatorNodeRef } = useSortable({
        id: props.id,
    });

    const style = {
        transform: CSS.Transform.toString(transform),
        transition,
    };

    return (
        <div ref={setNodeRef} {...attributes} style={style}>
            <Tab {...props} />
            <IconButton ref={setActivatorNodeRef} {...listeners} size="small">
                <DragIndicatorIcon fontSize="5px" />
            </IconButton>
        </div>
    );
};

export default SortableTab;
2

There are 2 best solutions below

0
On BEST ANSWER

So in order for this to work I ended up figuring out that without a draghandle on the Tab the click event would only either fire for the drag event or setting the value depending on where the SortableContext was placed. This was my solution:

SortableTab

import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { Tab, IconButton } from "@mui/material";
import MoreVertRoundedIcon from "@mui/icons-material/MoreVertRounded";

const SortableTab = (props) => {
    const { attributes, listeners, setNodeRef, transform, transition, setActivatorNodeRef } = useSortable({
        id: props.id,
    });

    const style = {
        transform: CSS.Transform.toString(transform),
        transition,
    };

    return (
        <div ref={setNodeRef} {...attributes} style={style}>
            <Tab {...props} />
            <IconButton ref={setActivatorNodeRef} {...listeners} size="small">
                <MoreVertRoundedIcon fontSize="5px" />
            </IconButton>
        </div>
    );
};

export default SortableTab;

DndContext code chunk:

const renderedTab = selectedScenario ? (
        scenarioModels.map((item, index) => <SortableTab key={item} label={models.data[item].model} id={item} index={index} value={index + 1} onClick={() => handleModelClick(item)} />)
    ) : (
        <Tab label="Pick a Scenario" value={0} />
    );

<DndContext collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
                    <SortableContext items={scenarioModels} strategy={horizontalListSortingStrategy}>
                        <Tabs value={selectedTab} onChange={handleChange} centered>
                            {selectedScenario ? <Tab label="Source Data" /> : ""}
                            {renderedTab}
                        </Tabs>
                    </SortableContext>
                </DndContext>
0
On

You can make drag and drop tabs work by moving <SortableContext> inside the <TabList> component something like the below. Then change sorting started to horizontalListSortingStrategy.

In this case, your Dnd will work, but you will lose all MUI transitions/animations. Because now DndContext is overriding MuiContext. The best solution to create something like this is to create custom Tabs components where your context does not depend on TabContext.

I hope it helps.

     <TabContext value={activeTab}>
        <DndContext
          collisionDetection={closestCenter}
          onDragEnd={handleDragEnd}
        >
          <TabList onChange={handleChange} aria-label="basic tabs example">
            <SortableContext
              items={items}
              strategy={horizontalListSortingStrategy}
            >
              {items.map((item, index) => (
                <SortableTab
                  value={index.toString()}
                  key={item}
                  id={item}
                  index={index}
                  label={item}
                />
              ))}
            </SortableContext>
          </TabList>
        </DndContext>
      </TabContext>