Search code examples
reactjsmaterial-uitabsdrag-and-dropdnd-kit

Need help creating Drag and Drop Material UI Tabs


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;

Solution

  • 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>