Unable to drag React bootstrap tabs - React Beautiful dnd - reactjs

I would like to drag drop tabs from react-bootstrap and I am using react-beautiful-dnd to achieve it. But for some reason, I am not able to get it. No tabs are appearing.
Here is my code:
import React from "react";
import { Tabs, Tab } from "react-bootstrap";
import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd";
export default function App() {
const getItems = count =>
Array.from({ length: count }, (v, k) => k).map(k => ({
id: `item-${k}`,
content: `item ${k}`
}));
const [selectedTab, setSelectedTab] = React.useState("item-0");
const [items, setItems] = React.useState(getItems(6));
const reorder = (list, startIndex, endIndex) => {
const result = Array.from(list);
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
return result;
};
const onDragEnd = result => {
// dropped outside the list
if (!result.destination) {
return;
}
const items = reorder(
this.state.items,
result.source.index,
result.destination.index
);
setItems(items);
};
return (
<div className="App">
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="tab-drop" direction="horizontal">
{droppable => (
<React.Fragment>
<Tabs
ref={droppable.innerRef}
activeKey={selectedTab}
onSelect={chartId => {
if (chartId === this.state.selectedTab) {
return;
}
setSelectedTab(chartId);
}}
{...droppable.droppableProps}
>
{items.map((item, index) => (
<Draggable
key={`tab-${item.id}-order-${index}`}
draggableId={`${item.id}-order-${index}`}
index={index}
>
{(draggable, snapshot) => (
<Tab
ref={draggable.innerRef}
eventKey={item.id}
title={<div>{item.content}</div>}
{...draggable.draggableProps}
{...draggable.dragHandleProps}
style={getItemStyle(
draggable.draggableProps.style,
snapshot.isDragging
)}
/>
)}
</Draggable>
))}
<Tab title={<button>+</button>} />
<Tab
tabClassName="ml-auto active tabBottomBorder"
title={<button>Format</button>}
/>
<Tab
tabClassName="active tabBottomBorder"
title={<button>Edit</button>}
/>
</Tabs>
{droppable.placeholder}
</React.Fragment>
)}
</Droppable>
</DragDropContext>
</div>
);
}
This is the example which works without DND:
import React from "react";
import "./styles.css";
import { Tabs, Tab } from "react-bootstrap";
export default function App() {
const getItems = (count) =>
Array.from({ length: count }, (v, k) => k).map((k) => ({
id: `item-${k}`,
content: `item ${k}`
}));
const items = getItems(6);
const [selectedTab, setSelectedTab] = React.useState("item-0");
return (
<div className="App">
<Tabs
activeKey={selectedTab}
onSelect={(chartId) => {
if (chartId === selectedTab) {
return;
}
setSelectedTab(chartId);
}}
>
{items.map((item, index) => (
<Tab eventKey={item.id} title={<div>{item.content}</div>} />
))}
<Tab title={<button>+</button>} />
<Tab
tabClassName="ml-auto active tabBottomBorder"
title={<button>Format</button>}
/>
<Tab
tabClassName="active tabBottomBorder"
title={<button>Edit</button>}
/>
</Tabs>
</div>
);
}
Link with DND: https://stackblitz.com/edit/react-u1cts7?file=src%2FApp.js
Link without DND: https://codesandbox.io/s/pedantic-minsky-7zelb?file=/src/App.js:0-1045
I want to achieve a similar functionality to : https://codesandbox.io/s/mmrp44okvj?file=/index.js:0-2939
Please note only the tabs before the "+" button should be draggable. The + button tab and last two tabs shouldnt be.
Please advice on a way to fix this. Any help is appreciated.

Related

Jest testing with props

React, typescript, and jest are all new to me. I have a component, 'myContacts,' displaying tabs and tab panels with contacts; an address book.
Here is a portion of myContacts.
import ... as needed ...
export default function myContacts( props ){
const { data, link } = props;
const [limit, setLimit] = useState<number>(data.limit);
(handleTabsChange and various other logic)
useEffect(() => {
const offset = (page - 1) * limit;
setContactsPerPage(
entries[currentTab?.activeKey]?.slice(offset, offset + limit)
);
setLimit(viewData.limit);
}, [entries, currentTab?.activeKey, page, limit, data.limit]);
if (currentTab?.activeKey == null) {
return null;
}
return (
<div>
<Tabs
index={tabIndex}
onChange={handleTabsChange}
>
<TabList>
{tabs.map((item: string) => (
<Tab key={item}>{item}</Tab>
))}
</TabList>
<TabPanels>
{tabs.map((item: string) => (
<TabPanel key={item}>
... do some stuff ...
/>
))}
</TabPanel>
))}
</TabPanels>
</Tabs>
<Container>
<div className="w-full md:w-8/12">
<Pagination
totalEntries={entries[currentTab?.activeKey]?.length}
limit={data.limit}
page={page}
setPage={setPage}
/>
</div>
</Container>
</div>
);
}
Here is the test I am working on. Very simple. Check that myContacts has rendered.
import { render, screen } from "#testing-library/react";
import myContacts from "../myContacts";
test("render contacts", () => {
render(<myContacts />);
expect(screen.getByText(/component myContacts/i)).toBeInTheDocument();
});
However, I am getting the following error.
TypeError: Cannot read properties of undefined (reading 'limit')
23 | const { link, route, viewData } = props;
24 |
> 25 | const [limit, setLimit] = useState<number>(viewData.limit);
My initial thought was to do the following:
test("render contacts", () => {
const limit = 5;
render(<myContacts limit={limit} />);
expect(screen.getByText(/component myContacts/i)).toBeInTheDocument();
});
but this resulted in the same error.
I know this error is because 'limit' is not defined in the test, but I am at a loss on how to define and pass it in the test.

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

Mui TabList with react-beautiful-dnd not auto scrolling on drag

I have added drag & drop feature to the MUI Tabs List using react-beautiful-dnd.
Code -
import * as React from 'react';
import TabContext from '#mui/lab/TabContext';
import TabList from '#mui/lab/TabList';
import TabPanel from '#mui/lab/TabPanel';
import Tab from '#mui/material/Tab';
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
import { Draggable } from 'react-beautiful-dnd';
import { styled } from '#mui/material/styles';
import { Stack } from '#mui/material';
function DraggableTab(props) {
return (
<Draggable
draggableId={`${props.index}`}
index={props.index}
disableInteractiveElementBlocking
>
{(draggableProvided) => (
<div
ref={draggableProvided.innerRef}
{...draggableProvided.draggableProps}
>
{React.cloneElement(props.child, {
...props,
...draggableProvided.dragHandleProps,
})}
</div>
)}
</Draggable>
);
}
const StyledTabList = styled(TabList)();
const StyledTab = styled(Tab)();
export default function DraggableTabsList() {
const [value, setValue] = React.useState('1');
const handleChange = (event, newValue) => {
setValue(newValue);
};
const [tabs, setTabs] = React.useState(
[...Array(55)].map((_, index) => ({
id: `tab${index + 1}`,
label: `Tab ${index + 1}`,
value: `${index + 1}`,
content: `Content ${index + 1}`,
}))
);
const onDragEnd = (result) => {
const newTabs = Array.from(tabs);
const draggedTab = newTabs.splice(result.source.index, 1)[0];
newTabs.splice(result.destination?.index, 0, draggedTab);
setTabs(newTabs);
};
const _renderTabList = (droppableProvided) => (
<StyledTabList onChange={handleChange} variant="scrollable">
{tabs.map((tab, index) => {
const child = (
<StyledTab
label={tab.label}
value={tab.value}
key={index}
/>
);
return (
<DraggableTab
label={tab.label}
value={tab.value}
index={index}
key={index}
child={child}
/>
);
})}
{droppableProvided ? droppableProvided.placeholder : null}
</StyledTabList>
);
const _renderTabListWrappedInDroppable = () => (
<DragDropContext onDragEnd={onDragEnd}>
<div>
<Droppable droppableId="1" direction="horizontal">
{(droppableProvided) => (
<div
ref={droppableProvided.innerRef}
{...droppableProvided.droppableProps}
>
{_renderTabList(droppableProvided)}
</div>
)}
</Droppable>
</div>
</DragDropContext>
);
return (
<TabContext value={value}>
<Stack>{_renderTabListWrappedInDroppable()}</Stack>
{tabs.map((tab, index) => (
<TabPanel value={tab.value} key={index}>
{tab.content}
</TabPanel>
))}
</TabContext>
);
}
Working codesandbox example - https://codesandbox.io/s/mui-tab-list-drag-and-drop-jceqnz
I am facing trouble making the tab list auto scroll while a tab is being dragged to the end of the visible list.
Also, I tried some hacks to make it work - see this, but I'm losing out on the scroll buttons which is a feature loss and not wanted. As auto scrolling is a standard d&d feature I'm hoping there must be some solution. Could you please help?
Thanks!
First of all thank you for such good quality code. I was searching for implementation of MUI tabs with react-beautiful-dnd and found your implementation really helpful.
To make it scrollable i along with dragable I removed unnecessary div and it worked perfectly for me
Here's a screen shot of my code.
And Heres the picture of dragable tab with scroll
Also I'm importing tabs from mui directly like this

How to get rid of weird id error using react-beautiful-dnd?

Right now I have a page that looks like this:
Im displaying some ClosedExerciseComp-components basically small cards with their respective id on them. When I press the Add exercise-button a new exercise card will emerge.
Using react-beautiful-dnd the cards are fully drag-and-droppable.However sometimes when I drop an Item I get this error:
It seems to occur at random when I drop an Item but I can get the error to trigger more reliable if I spam drag-and-drop in the small gap between the cards. I demonstrate here.
My code looks like this (I know it's a tad long) (simplified aesthetically):
import React, { useState } from "react";
import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd";
import ExerciseComp from "../components/ExerciseComp/ExerciseComp";
const EditorMainPage = () => {
const [exercises, setExercises] = useState([]);
const addExercise = () => {
const exercise = {
id: "exercise-" + Date.now(),
...,
};
setExercises((exercises) => [...exercises, exercise]);
};
const handleDragEnd = (result) => {
if (!result.destination) return;
if (result.destination.index === result.source.index) return;
const items = [...exercises];
const [reorderedItem] = items.splice(result.source.index, 1);
items.splice(result.destination.index, 0, reorderedItem);
setExercises(items);
};
const Exercises = () => {
return (
<DragDropContext onDragEnd={handleDragEnd}>
<Droppable droppableId="exercises-dnd">
{(provided) => (
<div
ref={provided.innerRef}
{...provided.droppableProps}
>
{exercises.map((exercise, index) => (
<Draggable
key={exercise.id}
draggableId={exercise.id}
index={index}
>
{(provided) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<ExerciseComp
id={exercise.id}
/>
</div>
)}
</Draggable>
))}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
);
};
return (
<div>
<Exercises />
<button onClick={() => addExercise()}>Add exercise</button>
</div>
);
};
export default EditorMainPage;
I hope you can help me understand why suddenly react decides that an existing draggable doesn't exist???

React Material-UI menu anchor lost because of re-rendered by react-window

I'm building an infinite loading list of users with react-window. In the list, every item has an icon button from Material-UI for further action.
But I can't mount the menu near the icon as the icon button would be re-rendered when setting anchorEl for the menu to be opened. A gif clip:
The question is related to React Material-UI menu anchor broken by react-window list but has more HOC. The code is listed here. I wish I could use my codesandbox for demonstration but the react-measure keeps growing height.
function App() {
const [anchorEl, setAnchorEl] = useState(null);
const openMenu = React.useCallback(e => {
e.stopPropagation();
setAnchorEl(e.currentTarget);
console.log("target", e.currentTarget);
}, []);
const handleClose = () => {
setAnchorEl(null);
};
const [items, setItems] = React.useState([]);
const isItemLoaded = index => {
const c = index < items.length;
// console.log("isItemLoaded", index, c);
return c;
};
const loadMoreItems = (startIndex, stopIndex) => {
console.log("loadMoreItems", startIndex, items);
setItems(items.concat(Array(10).fill({ name: "1", size: startIndex })));
};
const innerET = React.forwardRef((props, ref) => (
<div ref={ref} {...props} />
));
const Row = React.useCallback(
({ index, style }) => {
console.log("Row", items, index);
return items[index] ? (
<ListItem style={style} key={index}>
<Button variant="contained" color="primary" onClick={openMenu}>
Row {index}: {items[index].size}
</Button>
</ListItem>
) : null;
},
[items, openMenu]
);
const innerListType = React.forwardRef((props, ref) => (
<List ref={ref} {...props} />
));
return (
<div className="App">
<div className="ceiling">Something at top</div>
<div className="interest">
<Menu anchorEl={anchorEl} onClose={handleClose} />
<Measure bounds offset>
{({ measureRef, contentRect }) => {
const height = Math.min(
contentRect && contentRect.offset
? document.getElementById("root").getBoundingClientRect()
.height - contentRect.offset.top
: itemSize * items.length,
itemSize * items.length
);
console.log(
"bounds",
height,
contentRect.bounds,
contentRect.offset
);
return (
<div>
<div />
<div ref={measureRef} className="measurement">
<InfiniteLoader
isItemLoaded={isItemLoaded}
itemCount={itemCount}
loadMoreItems={loadMoreItems}
>
{({ onItemsRendered, ref }) => (
<FixedSizeList
height={height}
width={
contentRect.bounds !== undefined &&
contentRect.bounds.width !== undefined
? contentRect.bounds.width
: -1
}
itemCount={itemCount}
itemSize={itemSize}
onItemsRendered={onItemsRendered}
ref={ref}
innerElementType={innerET}
>
{Row}
</FixedSizeList>
)}
</InfiniteLoader>
</div>
</div>
);
}}
</Measure>
</div>
</div>
);
}
As far as I understand, the ripple effect would trigger a re-render in the box with the first click. Moreover, the second click after the re-render upon clicking would not trigger a re-render. That feels even more peculiar to me.
EDIT: I fixed the first sandbox. And by using Material UI's list, this issue is reproducible. https://codesandbox.io/s/blissful-butterfly-qn3g7
So the problem lies in using innerElementType property.
It turns out that a hook is needed.
const innerListType = React.useMemo(() => {
return React.forwardRef((props, ref) => (
<List component="div" ref={ref} {...props} />
));
}, []);
To fix my problems, hooks for handling events are needed to be handled more carefully.

Resources