I am quite new to React and I need to make a dashboard, where I will have a table with some data.
One of the column in the table is clickable and when I click on a particular cell, it will take that cell value and display more details about that item "in a new tab".
Now this is easy with a browser, where you open a new tab on link click. But this is an app i am making on chromium. more of a desktop app.
But I do not want a new window whole together. I need a new tab panel to open with the previous table still there in one tab, and the details in the new tab.
so when I go back to the previous tab and click on another item, it opens a third tab with the details of this item.
Example below (Sorry I am not allowed to insert pictures yet. Please click the link to see them.)
1st picture with initial table.
First Picture With the initial table
Now, if I click on Accountant, a new tab should appear as in second image:
Second image with a new tab opened
I think I found a solution to this. I am pasting it here for someone looking for a solution.
import React, { useState, useCallback, useEffect } from "react";
import PropTypes from "prop-types";
import { withStyles } from "#material-ui/core/styles";
import AppBar from "#material-ui/core/AppBar";
import { Tabs, Tab, IconButton } from "#material-ui/core";
import CloseIcon from "#material-ui/icons/Close";
import TabContainer from "../TabContainer/index.jsx";
const styles = theme => ({
root: {
flexGrow: 1,
backgroundColor: theme.palette.background.paper
},
colorPrimary: {
color: "red"
}
});
export const TabsDemo = ({
tabs,
selectedTab = 1,
onClose,
tabsProps = { indicatorColor: "primary" },
...rest
}) => {
const [activeTab, setActiveTab] = useState(selectedTab);
const [activetabs, setActiveTabs] = useState([]);
// useEffect(() => {
// if (activeTab !== selectedTab) setActiveTab(selectedTab);
// }, [setActiveTab, selectedTab, activeTab]);
// const handleChange = useCallback(event => setActiveTab(event.target.value), [
// setActiveTab,
// ]);
useEffect(() => {
setActiveTabs(tabs);
}, [tabs]);
const handleChange = useCallback((event, activeTab) => {
setActiveTab(activeTab);
}, []);
const handleClose = useCallback(
(event, tabToDelete) => {
event.stopPropagation();
const tabToDeleteIndex = activetabs.findIndex(
tab => tab.id === tabToDelete.id
);
const updatedTabs = activetabs.filter((tab, index) => {
return index !== tabToDeleteIndex;
});
const previousTab =
activetabs[tabToDeleteIndex - 1] ||
activetabs[tabToDeleteIndex + 1] ||
{};
setActiveTabs(updatedTabs);
setActiveTab(previousTab.id);
},
[activetabs]
);
return (
<>
<div>
<Tabs value={activeTab} onChange={handleChange}>
{activetabs.map(tab => (
<Tab
key={tab.id}
value={tab.id}
label={
typeof tab.label === "string" ? (
<span>
{" "}
tab.label
{tab.closeable && (
<IconButton
component="div"
onClick={event => handleClose(event, tab)}
>
<CloseIcon />
</IconButton>
)}
</span>
) : (
tab.label
)
}
/>
))}
</Tabs>
{activetabs.map(tab =>
activeTab === tab.id ? (
<TabContainer key={tab.id}>{tab.component}</TabContainer>
) : null
)}
</div>
</>
);
};
TabsDemo.propTypes = {
classes: PropTypes.object.isRequired,
tabs: PropTypes.arrayOf(
PropTypes.shape({
label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired,
id: PropTypes.number.isRequired,
component: PropTypes.object.isRequired,
closeable: PropTypes.bool.isRequired
}).isRequired
).isRequired
};
export default withStyles(styles)(TabsDemo);
Below is the link of a code sandbox that you can use to understand this fully :
https://codesandbox.io/s/mui-closeable-tab-gw2hw?file=/src/components/tabsdemo/index.jsx:0-3123
Related
I'm rendering from database over 300 choices to the MUI textfield select. I used the controller component from react hook forms how do I solve this?
import { Menu, MenuItem, TextField } from "#mui/material";
import React from "react";
import { Controller } from "react-hook-form";
import { useSelector } from "react-redux";
const DropdownState = ({ controller, textField, type, isLabelAsId = true }) => {
const position = useSelector((state) => state.position);
let data = [];
if (type === "position") {
data = position.positions.map((x) => ({
id: x.PosID,
label: x.Positn,
}));
} else {
console.error(
'Please choose a type between "position" and "test" '
);
return;
}
if (isLabelAsId) data = data.map((x) => ({ ...x, id: x?.label }));
return (
<Controller
{...controller}
render={({ field, fieldState }) => (
<TextField
select
size="small"
margin="normal"
sx={{
flex: 1,
"& fieldset": {
borderRadius: 3,
},
}}
{...field}
{...textField}
error={!!fieldState.error}
helperText={fieldState.error?.message}
>
{data
.sort((a, b) =>
a.label.toLowerCase() < b.label.toLowerCase()
? -1
: a.label.toLowerCase() > b.label.toLowerCase()
? 1
: 0
)
.map((x, index) => (
<MenuItem key={index} value={x.id}>
{x.label}
</MenuItem>
))}
</TextField>
)}
/>
);
};
export default DropdownState;
What I want is to avoid getting this warning message from the console and stop the laggy animation when clicking the select input field "Position"
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
while building my react app for deployment, I am getting this error
TypeError: Cannot read property '0' of undefined
when I am rending on port3000 I did not see this error but only get it while building the app.
Can anyone assist to resolve this?
import { useState } from "react";
import styles from "./Tabs.module.css"
const Tabs = ({ children}) => {
const [activeTab, setActiveTab] = useState (children [0].props.label);
const handleClick =( e, newActiveTab ) => {
e.preventDefault();
setActiveTab(newActiveTab);
}
return (
<div>
<ul className= {styles.tabs}>
{children.map ((tab) => {
const label = tab.props.label;
return (
<li
className= {label == activeTab ? styles.current : ""}
key= {label}
>
<a href="#" onClick={(e) => handleClick (e, label)}>{label}
</a>
</li>
)
})}
</ul>
{children.map ((tabcontent1) => {
if (tabcontent1.props.label == activeTab)
return (
<div key= {tabcontent1.props.label} className= {styles.content}>{tabcontent1.props.children}
</div>
);
})}
</div>
);
}
export default Tabs ;
In next js, when you don't put export const getServerSideProps = () => {} in your page then that page is automatically subjected to static side rendering. On development mode, you may see a lightening symbol on bottom-right. Anyway you can read the docs on data-fetching on nextjs. However, your issue on this situation can be easily fixed by setting the children through useEffect.
// handle null on your active tab render function
const [activeTab, setActiveTab] = useState(null);
useEffect(() => {
if(children.length)
children[0].props.label
}, [children])
Another Code Sample:
*A simple change in code structure and the way you are trying to do. It's on react but kind of same in next as well *
import React from "react";
const Tabs = ({ tabsData }) => {
const [activeTabIndex, setActiveTabIndex] = React.useState(0);
const switchTabs = (index) => setActiveTabIndex(index);
return (
<div style={{ display: "flex", gap: 20, cursor: "pointer" }}>
{/* Here active tab is given a green color and non actives grey */}
{tabsData.map((x, i) => (
<div
key={i}
style={{ color: activeTabIndex === i ? "green" : "#bbb" }}
onClick={() => switchTabs(i)}
>
{x.label}
</div>
))}
{/* Show Active Tab Content */}
{tabsData[activeTabIndex].content}
</div>
);
};
export default function App() {
// You can place it inside tabs also in this case
// but lets say you have some states on this component
const tabsData = React.useMemo(() => {
return [
// content can be any component or React Element
{ label: "Profile", content: <p>Verify all Input</p> },
{ label: "Settings", content: <p>Settings Input</p> },
{ label: "Info", content: <p>INput info</p> }
];
}, []);
return (
<div className="App">
<Tabs tabsData={tabsData} />
</div>
);
}
and here is also a example sandbox https://codesandbox.io/s/serverless-night-ufqr5?file=/src/App.js:0-1219
I'm using Primereact's tree along with context menu. After performing an action (a click event on one of the menu items), context menu is not hiding. If I click anywhere on the screen the context menu hides. Here is the code. I'm following the same pattern provided by primereact. Can you help me figure out how to fix this?
DemoPrimeReact.js
import React, {useContext, useEffect, useRef, useState} from 'react';
import 'primereact/resources/themes/saga-blue/theme.css';
import 'primereact/resources/primereact.css';
import 'primeflex/primeflex.css';
import {Tree} from 'primereact/tree';
// other imports
const DemoPrimeReact = () => {
// .... other lines skipped
const [selectedNodeKey, setSelectedNodeKey] = useState(null);
const contextMenu = useRef(null);
return (
<>
<DemoContextMenu
contextMenu={contextMenu} setSelectedNodeKey={setSelectedNodeKey}
//... other props
/>
<Tree
value={[withOrWithoutSearchNodes[0]]}
selectionMode="single" selectionKeys={selectedKey} onSelectionChange={handleSelectionChange}
onSelect={onNodeSelect} onUnselect={onNodeUnselect}
expandedKeys={expandedKeys} onToggle={(event) => setExpandedKeys(event.value)}
contextMenuSelectionKey={selectedNodeKey}
onContextMenuSelectionChange={(event) => setSelectedNodeKey(event.value)}
onContextMenu={(event) => contextMenu.current.show(event.originalEvent)}
dragdropScope="catalog-dnd" onDragDrop={(event) => {
handleCatalogDragAndDrop(event, setLoading, setCanPaste, loadChildrenOnExpand,
setSelectedKey, setExpandedKeys, onNodeSelect, setNodes, expandedKeys);
}}
onExpand={loadChildrenOnExpand} className={classes.primeTree} nodeTemplate={nodeTemplate}
/>
</>
);
}
export default DemoPrimeReact;
DemoContextMenu.js
import React, {useState} from 'react';
import {ContextMenu} from "primereact/contextmenu";
const DemoContextMenu = (props) => {
const { contextMenu, setSelectedNodeKey } = props;
const menu = [
{
label: "Create Library",
template: (item, options) => {
return (
<>
{
nodeInfo && (nodeInfo.node.value.entryType === 'M' ||
nodeInfo.node.value.entryType === 'L') ?
<span
style={{padding: '0 0 0 15px'}}
className={options.className}
>
<Button
onClick={(event) => handleCatalogAction(event, 'createLibrary')}
className={classes.button}
>
<CreateLibraryIcon style={{color: '#68de86'}} />
<span className={classes.item}>Create Library</span>
</Button>
</span> : null
}
</>
);
},
}
];
return (
<>
<ContextMenu
model={menu}
ref={contextMenu}
onHide={() => setSelectedNodeKey(null)}
className={classes.contextMenu}
/>
</>
);
}
export default DemoContextMenu
I think you can hide the context menu with contextMenu ref using contextMenu.current.state.visible = false;.
You can put this line in handleCatalogAction(...) function at starting. Before you add the above line, you can do console.log(contextMenu); to check for current.state.visible value.
As described in the official documentation for react-select, I'm trying to use ref and focus() to manually set the focus into the control input field. In most instances it works, but not immediately after selecting an Option from the dropdown.
After selecting an option from the dropdown, the control gets the focus but the cursor doesn't appear. It only appears if you start typing (including hitting the Esc key). On subsequent openings of the menu, the cursor appears along with the focus of the entire control field. Any ideas how to get this working?
I've created a sample code in codesandbox.io here
This is the code:
import React, { Component } from "react";
import ReactDOM from "react-dom";
import Select from "react-select";
import styled from "styled-components";
import { stateOptions } from "./data.js";
class PopoutExample extends Component {
selectRef = React.createRef();
state = {
isOpen: false,
option: undefined,
};
toggleOpen = () => {
const isOpening = !this.state.isOpen;
this.setState(
{
isOpen: isOpening,
},
() => isOpening && setTimeout(() => this.selectRef.focus(), 400),
);
};
onSelectChange = option => {
this.toggleOpen();
this.setState({ option });
};
render() {
const { isOpen, option } = this.state;
return (
<Dropdown
target={
<MainButton onClick={this.toggleOpen}>
{option ? option.label : "Select a State"}
</MainButton>
}
>
<Select
menuIsOpen
ref={ref => {
this.selectRef = ref;
}}
styles={{
container: provided => ({
...provided,
display: isOpen ? "block" : "none",
}),
}}
onChange={this.onSelectChange}
options={stateOptions}
value={option}
controlShouldRenderValue={false}
/>
</Dropdown>
);
}
}
const MainButton = styled.button`
padding: 10px;
background-color: aqua;
width: 100%;
`;
const Dropdown = ({ children, target }) => (
<div>
{target}
{children}
</div>
);
ReactDOM.render(<PopoutExample />, document.getElementById("root"));
You can notice that the bug also exists in the official react-select examples. Even clicking on the blur button after the selection is not solving the problem.
There's probably a small different in the code when user closes the menu and saves + automatically closes action.
I saw you've opened an issue on github. Let's keep an eye on it.
If I can offer an alternative to the behaviour you're trying to achieve, instead of hiding the Select with css why don't just mount / unmount it ?
class PopoutExample extends Component {
state = {
isOpen: false,
option: undefined
};
toggleOpen = () => {
this.setState({
isOpen: !this.state.isOpen
});
};
onSelectChange = option => {
this.setState({ option, isOpen: !this.state.isOpen });
};
render() {
const { isOpen, option } = this.state;
return (
<Dropdown
target={
<MainButton onClick={this.toggleOpen}>
{option ? option.label : "Select a State"}
</MainButton>
}
>
{isOpen && (
<Select
autoFocus
menuIsOpen
onChange={this.onSelectChange}
options={stateOptions}
value={option}
controlShouldRenderValue={false}
/>
)}
</Dropdown>
);
}
}
Here a live example of my solution.