In the code below the tabs are overflowing past the visible width after wrapping in the TabsWrapper. I have a use case where the tabs will always be rendered in a similar wrapper. Is it possible to override some styles in ScrollableTabs to achieve the default behaviour?
I'm getting the feeling that there should be a fix as this seems like a common use case w.r.t to CSS Grid and Flex.
Just in case you aren't familiar with the default behaviour, try rendering ScrollableTabs without it being wrapped in the TabsWrapper.
import React from "react";
import { TabContext, TabPanel, TabList } from "#mui/lab";
import { Box, Tab, Stack } from "#mui/material";
/** Cannot be changed */
function TabsWrapper(props) {
return (
<div style={{ display: "grid", gridAutoFlow: "row" }}>{props.children}</div>
);
}
/** Cannot be changed */
function MyTabList(props) {
return (
<Stack direction="row">
<TabList
orientation="horizontal"
variant="scrollable"
onChange={props.onChange}
>
{props.children}
</TabList>
</Stack>
);
}
/** Can be changed */
export default function ScrollableTabs() {
const tabValues = [...Array(30)].map((_, index) => String(index + 1));
const [tab, setTab] = React.useState("1");
const handleChange = (_, newValue) => {
setTab(newValue);
};
return (
<TabsWrapper>
<TabContext value={tab}>
<MyTabList onChange={handleChange}>
{tabValues.map((value) => (
<Tab label={`Tab ${value}`} value={value} key={value} />
))}
</MyTabList>
<Box>
{tabValues.map((value) => (
<TabPanel value={value} key={value}>
Tab content {value}
</TabPanel>
))}
</Box>
</TabContext>
</TabsWrapper>
);
}
Working codesandbox - https://codesandbox.io/s/mui-custom-scrollable-tabs-2mfcjr
Thanks!
Wrapping TabContext in <div style={{ overflow: "hidden" }}> fixes the issue.
The ScrollableTabs jsx should look like -
export default function ScrollableTabs() {
const tabValues = [...Array(30)].map((_, index) => String(index + 1));
const [tab, setTab] = React.useState("1");
const handleChange = (_, newValue) => {
setTab(newValue);
};
return (
<TabsWrapper>
<div style={{ overflow: "hidden" }}>
<TabContext value={tab}>
<MyTabList onChange={handleChange}>
{tabValues.map((value) => (
<Tab label={`Tab ${value}`} value={value} key={value} />
))}
</MyTabList>
<Box>
{tabValues.map((value) => (
<TabPanel value={value} key={value}>
Tab content {value}
</TabPanel>
))}
</Box>
</TabContext>
</div>
</TabsWrapper>
);
}
Working codesandbox - https://codesandbox.io/s/mui-custom-scrollable-tabs-forked-6ycqfv
Related
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>
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
This is either very simple or I am doing it completely wrong. I am a novice so please advise.
I am trying to show different components inside different tabs using Material UI using array map. The tabs are showing fine but the components do not render. Basically if the array label is 'Welcome', the tab name should be 'Welcome' and the Welcome component should show up and so on. Please help!
return (
<Box sx={{ width: '100%' }}>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs value={value} onChange={handleChange} aria-label="basic tabs example">
{fetchedCategories.map((category) => (
<Tab key={category.label} label={category.label} />
))}
</Tabs>
</Box>
{fetchedCategories.map((category, index) => {
const Component=myComponents[category.label];
})}
{fetchedCategories.map((category, index) => (
<TabPanel key={category.label} value={value} index={index}>
<Component label={category.label} />
</TabPanel>
))}
</Box>
);
Here is my props & Component function:
interface ComponentProps {
label: string;
value?: number;
}
function Component (props: ComponentProps)
{
const {label, value} = props;
return myComponents[label];
}
const myComponents = {
'Welcome': Welcome,
'Salad/Soup': Welcome
}
Try something like:
function Component({ label, value }: ComponentProps) {
const [Comp, setComponent] = useState(<div />);
React.useEffect(() => {
const LabelComp = myComponents[label];
if (label && LabelComp) {
setComponent(<LabelComp value={value} />); // <-- if you want to pass value to you component
}
}, [value, label]);
return Comp;
}
And you will use it like:
const App = () => {
return <Component label={"myComponentLabel"} value={"some value"} />;
};
I am building a an app that has a map and side-tabs.
Every time a user clicks on the map a marker appears and the coordinates are stored in a used state array.
I want every time a new marker appears to show it as a list or an accordion item in my side-tabs.
My side-tabs component and my addmarker component have the App as a parent.
How can I pass the usestate array from my addmarker component to my sidebar component every time I click on the map ?
ADD MARKER COMPONENT
function AddMarker(callbackFunction){
const [coord, setPosition] = useState([]);
const map = useMapEvents({
click: (e) => {
setPosition([...coord,e.latlng])
const mark = e
//console.log(mark)
//setInfo(`${e.latlng}`)
},
SIDE-BAR COMPONENT
export default function VerticalTabs() {
const [value, setValue] = React.useState(0);
const handleChange = (event, newValue) => {
setValue(newValue);
};
return (
<Box
sx={{ flexGrow: 1, bgcolor: 'background.paper', display: 'flex', height: 224 }}
>
<Tabs
orientation="vertical"
value={value}
onChange={handleChange}
aria-label="Vertical tabs"
sx={{ borderRight: 1, borderColor: 'divider' }}
>
<Tab label="Waypoints" {...a11yProps(0)} />
<Tab label="Sorting" {...a11yProps(1)} />
</Tabs>
<TabPanel value={value} index={0}>
</TabPanel>
<TabPanel value={value} index={1}>
Sorting
</TabPanel>
</Box>
);
}
APP.JS
function App() {
return (
<div className="App" >
<Sidetabs/>
<MapContainer center={[40.44695, -345.23437]} zoom={3}>
..............
<AddMarker />
</MapContainer>
</div>
)
}
Here, always lifting one step up, helps everytime, i have lifted your state up, and now both children have access to it, https://reactjs.org/docs/lifting-state-up.html
the similar implementation could be look like this,
as both siblings are nothing but children to parent App
function App() {
const [coord, setPosition] = useState([]);
return (
<div className="App" >
<Sidetabs coord={coord} setPosition={setPosition}/>
<MapContainer center={[40.44695, -345.23437]} zoom={3}>
<AddMarker coord={coord} setPosition={setPosition}/>
</MapContainer>
</div>
)
}
then extract out using props,
function AddMarker(props){
const {coord, setPosition} = props;
}
I need to create bunch of Tab nodes in a Tabs. I thought that map a array would be easier to manage it. But I was kind of don't know how to make it works with MATERIAL UI Taps components.
My target is when I click the tab, the TabPanel supposed to show the correct components pending on the index.
The Tabs part works just fine, and it will be siwtch components properly if I keep the TabPanel one by one. But it won't be work if I map the array to create the TabPanel.
Please advise how to fix it.
//TODO set the router for each tab, wondering if it could be done in an array and map it
const tab_item = [
{
index: 1,
label: 'Purchase',
path: '/Linx_Homeline/Purchase',
tabPanel_comp:<LawyerPurchase />
},
{
index: 2,
label: 'Refinance',
path: '/Linx_Homeline/Refinance',
tabPanel_comp:<Refinance />
},
// {},
]
function TabPanel(props) {
const { children, value, index } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`wrapped-tabpanel-${index}`}
>
{value === index && (
<Box p={3}>
<Typography component={'div'}>{children}</Typography>
</Box>
)}
</div>
);
}
// TabPanel.propTypes = {
// // children: PropTypes.node,
// index: PropTypes.any,
// value: PropTypes.any,
// };
const useStyles = makeStyles((theme) => ({
root: {
backgroundColor: theme.palette.background.paper,
},
item: {
minWidth: '0px'
}
}));
export default function TabsWrappedLabel() {
const classes = useStyles();
const [value, setValue] = React.useState(false);
const updateNotes = useContext(NotesUpdate);
const history = useHistory();
const handleChange = (event, newValue) => {
setValue(newValue);
};
const clean_notes_push = (item) => {
//comments
updateNotes.setCondition('');
updateNotes.setFunNotes('');
updateNotes.setBusNotes('');
history.push(item.path);
}
return (
<div className={classes.root}>
<AppBar position="static">
<Tabs
value={value}
onChange={handleChange}
variant='fullWidth'
TabIndicatorProps={{ style: { background: '#00ff33' } }}
>
{tab_item.map((item) => ( // The Tab works fine here.
<Tab
wrapped
key={item.index}
index={item.index}
label={item.label}
onClick={() => (clean_notes_push(item))}
/>
))}
</Tabs>
</AppBar>
{/* <TabPanel value={value} index={0}> // It works if I put the TabPanel one by one, but I'm trying to map the tab_item array to generate them, problem is I don't know how to make it works.
<LawyerPurchase/>
</TabPanel>
<TabPanel value={value} index={1}>
Item Two
</TabPanel>
<TabPanel value={value} index={2}>
Item Two
</TabPanel>
*/}
{tab_item.map((item) => ( // Not working here, not even generate a TabPanel
<TabPanel
key={item.index}
value={value}
index={1}
>
{item.tabPanel_comp}
</TabPanel>
))}
</div>
);
}
You may need to update the tab_item object by:
//TODO Declare the function to render the component in a tab pane
const tab_item = [
{
index: 1,
label: 'Purchase',
path: '/Linx_Homeline/Purchase',
tabPanel_comp: () => <LawyerPurchase /> // function returns the component
},
{
index: 2,
label: 'Refinance',
path: '/Linx_Homeline/Refinance',
tabPanel_comp: () => <Refinance />
},
]
And replace the TabPanel render map function by:
{tab_item.map((item) => ( // Not working here, not even generate a TabPanel
<TabPanel
key={item.index}
value={value}
index={1}
>
{item.tabPanel_comp()} //calls the function to render the component
</TabPanel>
))}