So I have a custom component for my sidebar navigation that isn't completely functional. I'm trying to figure out how to dynamically add isSelected to the CustomItem component by location.
Edit: I'm assuming that useLocation from react-router-dom is my best bet for the first part.
Here's the code for the Dashboard:
/** #jsx jsx */
import React, { forwardRef } from 'react';
import {
Main,
Content,
LeftSidebar,
} from '#atlaskit/page-layout';
import {
Link,
Route,
Switch,
useParams,
useRouteMatch } from 'react-router-dom';
import { css, jsx } from '#emotion/core';
import { Section } from '#atlaskit/menu';
import {
Header,
NavigationHeader,
NestableNavigationContent,
SideNavigation,
CustomItem,
CustomItemComponentProps
} from '#atlaskit/side-navigation';
type CustomProps = CustomItemComponentProps & {
href: string;
};
const CustomLink = forwardRef<HTMLAnchorElement, CustomProps>(
(props: CustomProps, ref) => {
const { href, children, ...rest } = props;
return (
<Link to={href} ref={ref} {...rest}>
{children}
</Link>
);
},
);
function SideNavigationWrapper(props: { children: React.ReactNode }) {
return (
<div
css={css`
height: 100%;
& nav {
min-width: 20px;
overflow-x: hidden;
}
`}
>
{props.children}
</div>
);
}
function Components() {
let { component } = useParams();
if (component === 'assets') {
return (
<Assets />
);
}
if (component === 'orders') {
return (
<Orders />
);
}
return (
<div></div>
);
}
export default function Dashboard() {
let { path } = useRouteMatch();
return (
<Content>
<LeftSidebar
isFixed={true}
id="dash-navigation"
>
<SideNavigationWrapper>
<SideNavigationContent />
</SideNavigationWrapper>
</LeftSidebar>
<Main>
<div
style={{
marginLeft: 40,
marginRight: 40,
marginTop: 20,
}}
>
<Switch>
<Route exact path={path}>
<Overview />
</Route>
<Route path={`${path}/:component`}>
<Components />
</Route>
</Switch>
</div>
</Main>
</Content>
);
}
const SideNavigationContent = () => {
return (
<SideNavigation label="DashboardNav">
<NavigationHeader>
<Header>Dashboard</Header>
</NavigationHeader>
<NestableNavigationContent initialStack={[]}>
<Section>
<CustomItem
isSelected
component={CustomLink}
href="/dashboard"
iconBefore={<PortfolioIcon label="" />}
>
Portfolio
</CustomItem>
<CustomItem
component={CustomLink}
href="/dashboard/assets"
iconBefore={<SuitcaseIcon label="" />}
>
Assets
</CustomItem>
<CustomItem
component={CustomLink}
href="/dashboard/orders"
iconBefore={<RoadmapIcon label="" />}
>
Orders
</CustomItem>
</Section>
</NestableNavigationContent>
</SideNavigation>
);
};
It's also worth nothing that my approach has slightly messed up the styling as shown below:
Which should appear like this naturally without me having to hover over it.
Edit: I'm also assuming that I would have to override the styling with cssFn as per the documentation.
Any guidance on either two issues would be helpful. I've gave myself a headache trying to redo the entire apps routing. Figured I'd try my best to wrap this up tonight so I can move onto greater issues
You should assign boolean instead of putting props
const SideNavigationContent = props => {
//Add logic to control variable
//isPortfolioSelected, isAssetsSelect, isOrdersSelected, then assign to each item,
//Ex: isPortfolioSelected = props.selected['Portfolio']
return (
<SideNavigation label="DashboardNav">
<NavigationHeader>
<Header>Dashboard</Header>
</NavigationHeader>
<NestableNavigationContent initialStack={[]}>
<Section>
<CustomItem
isSelected={isPortfolioSelected}
component={CustomLink}
href="/dashboard"
iconBefore={<PortfolioIcon label="" />}
>
Portfolio
</CustomItem>
<CustomItem
isSelected={isAssetsSelect}
component={CustomLink}
href="/dashboard/assets"
iconBefore={<SuitcaseIcon label="" />}
>
Assets
</CustomItem>
<CustomItem
isSelecled={isOrdersSelected}
component={CustomLink}
href="/dashboard/orders"
iconBefore={<RoadmapIcon label="" />}
>
Orders
</CustomItem>
</Section>
</NestableNavigationContent>
</SideNavigation>
);
};
Related
How to correctly pass callbacks and states to the Layout so that they can be used elsewhere? When I share this as below, I have errors and a white screen:
class Menu extends Component {
constructor(props) {
super(props);
this.onSearchF = this.onSearchF.bind(this)
}
state = {
searchBlock: false,
};
onSearchF = (keyword) => {
const filtered = this.state.data.filter((entry) =>
Object.values(entry).some(
(val) => typeof val === "string" && val.toLowerCase().includes(keyword.toLowerCase())
)
);
};
render() {
return (
<div className="content">
<Routes>
<Route path="/" element={<Layout searchBlock={this.state.searchBlock} onSearch={()=>this.onSearchF()}/>}>
<Route
index
element={
<Home data={this.state.data} num={this.state.data.length} />
}
/>
</Route>
</Routes>
</div>
);
}
}
export default Menu;
Here I pass the callback to the Header that I previously passed to the Layout:
const Layout = () => {
return (
<>
<Header sblock={this.props.searchBlock} onS = {this.props.onSearch}/>
</>
);
};
export default Layout;
I want to use the callback here:
function Header() {
return (
<header className="header">
<button onClick={()=>console.log(this.props.sblock)}>button</button>
</header>
);
}
export default Header;
Your Layout is a functional component, and you are trying to use this.props in it; this is incorrect. Get the props as part of arguments instead, like so:
import { Outlet } from "react-router-dom";
const Layout = ({searchBlock,onSearch}) => {
return (
<>
<Header sblock={searchBlock} onS={onSearch}/>
<Outlet/>
</>
);
};
export default Layout;
Issues
The Layout component isn't accepting any props.
The Layout component isn't rendering an Outlet for nested routes.
Solution
It seems that Layout only exists to render the Header component. I'd suggest rendering Header directly in the Main component.
Example:
class Menu extends Component {
state = {
data: [],
searchBlock: false,
};
onSearch = (keyword) => {
const filtered = this.state.data.filter((entry) =>
Object.values(entry).some((val) =>
typeof val === "string"
&& val.toLowerCase().includes(keyword.toLowerCase())
)
);
... do something with filtered ...
};
render() {
const { data, searchBlock } = this.state;
return (
<div className="content">
<Header sblock={searchBlock} onS={this.onSearch} />
<Routes>
<Route
path="/"
element={<Home data={data} num={data.length} />}
/>
</Routes>
</div>
);
}
}
export default Menu;
I have written below routes in App.js -
function App() {
return (
<>
<BrowserRouter>
<Switch>
<Route path="/" exact component={Dashboard} ></Route>
<Route path="/details/:index" exact component={ItemDetails} ></Route>
<Dashboard></Dashboard>
</Switch>
</BrowserRouter>
</>
);
}
export default App;
I have another component - Items which has Card (Reactstrap). Each card is having a Link -
function Items(props) {
console.log(props.index.index)
return (
<Link to={{pathname:"/details",param1:props.index.index}}>
<Card tag="a" key={props.index.index} style={{display:'flex',width:'25%',flexWrap:'nowrap',float:'left',cursor: "pointer"}}
>
<CardBody>
<CardTitle> {props.card.card.value} </CardTitle>
</CardBody>
</Card>
</Link>
)
}
export default Items
Within Link tag , to attribute , I have mentioned -
to={{pathname:"/details",param1:props.index.index}}
By this I am expecting , upon clicking of card , component - ItemDetails should get rendered.
But I cannot see , ItemDetails has got rendered.
Do I need to add anything else within my current code ?
You can use the useHistory hook which solve this problem easily
import React from 'react'
import {useHistory} from 'react-router-dom'
function Items({index, card}) {
const history = useHistory()
function navigateTo(){
history.push(`/details/${index.index}`)
}
return (
<Card onClick={navigateTo} tag="a" key={props.index.index} style={{display:'flex',width:'25%',flexWrap:'nowrap',float:'left',cursor: "pointer"}}
>
<CardBody>
<CardTitle> {card.card.value} </CardTitle>
</CardBody>
</Card>
)
}
export default Items
You should add exact attribute to your "/" route. And component is given in Route not in Switch block
function App() {
return (
<>
<BrowserRouter>
<Switch>
<Route exact path="/" exact component={Dashboard} />
<Route path="/details/:index" exact component={ItemDetails} />
</Switch>
</BrowserRouter>
</>
);
}
export default App;
You may change the path param with Template literals
<Link to={`/details/${param1:props.index.index}`}></Link>
Complete code is something like this
function Items(props) {
return (
<Link to={`/details/${param1:props.index.index}`}>
<Card tag="a" key={props.index.index} style={{display:'flex',width:'25%',flexWrap:'nowrap',float:'left',cursor: "pointer"}}
>
<CardBody>
<CardTitle> {props.card.card.value} </CardTitle>
</CardBody>
</Card>
</Link>
)
}
export default Items
Use NavLink and change the Card tag from a to something else. Otherwise you will have nested a tags.
import { NavLink } from 'react-router-dom'
function Items(props) {
const linkTo = `/details/${props.index.index}`;
return (
<NavLink to={linkTo}>
<Card tag="span" key={props.index.index} style={{display:'flex',width:'25%',flexWrap:'nowrap',float:'left',cursor: "pointer"}}
>
<CardBody>
<CardTitle> {props.card.card.value} </CardTitle>
</CardBody>
</Card>
</NavLink>
)
}
I'm trying to add some animations and smoothness to an app with Framer-motion and I'm struggling to make it all work.
Using react-router 6, I want to trigger some exit animations on route sub-components when the url changes. Following this tutorial, here is what I got for the main layout :
export default function MainWrapper() {
const location = useLocation();
return (
<Main>
<AnimatePresence exitBeforeEnter initial={false}>
<Routes key={location.pathname}>
<Route path="/" element={<Dashboard />} />
<Route path="project/:id/*" element={<Project />} />
</Routes>
</AnimatePresence>
</Main>
);
}
The pages Dashboard and Project are build using some composition of Antd's Row and Col system. I want to animate Row's children to appear one after the other on mount, and to disappear one after the other on unmount :
import React, { ReactNode } from "react";
import { Card, Col, Row } from "antd";
import { motion } from "framer-motion";
// Section
type SectionProps = {
title?: ReactNode;
extra?: ReactNode;
children?: ReactNode;
span?: number;
};
export default function Section({
title,
extra,
children,
span = 24
}: SectionProps) {
return (
<MotionCol span={span} variants={colVariant}>
<Card title={title} extra={extra}>
{children}
</Card>
</MotionCol>
);
}
// Section.Group
type GroupProps = {
children?: ReactNode;
};
Section.Group = function({ children }: GroupProps) {
return (
<MotionRow
gutter={[24, 24]}
variants={rowVariant}
initial="hidden"
animate="show"
exit="close"
>
{children}
</MotionRow>
);
};
// Framer stuff
const MotionRow = motion(Row);
const MotionCol = motion(Col);
const transition = { duration: 0.4, ease: [0.43, 0.13, 0.23, 0.96] };
const rowVariant = {
hidden: {},
show: {
transition: {
staggerChildren: 0.1
}
},
close: {}
};
const colVariant = {
hidden: { opacity: 0, x: 20, transition },
show: { opacity: 1, x: 0, transition },
close: {
opacity: 0,
x: -20,
transition
}
};
Dashboard is then built using these blocks :
<Section.Group>
<Section>
First section...
</Section>
<Section>
Second section...
</Section>
</Section.Group>
The issue : Only hidden and show work. Not close. There is no exit-animation when leaving a page. How could I solve this ? Thank you.
Things I found wrong:
For custom components the motion function requires you to forward the ref
Docs: https://www.framer.com/api/motion/component/#custom-components
const ForwardedAntdRow = React.forwardRef((props, ref) => (
<Row ref={ref} {...props} />
));
const MotionRow = motion(ForwardedAntdRow);
not
const MotionRow = motion(Row);
Route doesn't have an element prop
<Route path="/page1">
<Page1 />
</Route>
is pretty standard notation as far as I know (I don't work with react-router often)
I created a working example here: https://codesandbox.io/s/framer-motion-animate-react-router-transition-kczeg?file=/src/index.js:1308-1372
I can answer any other questions you have when I am online tomorrow. Let me know
I can see two potential problems with your code:
1.
Note: Child motion components must each have a unique key prop so
AnimatePresence can track their presence in the tree.
Note: The custom component being removed from the DOM must still be a
direct descendant of AnimatePresence for the exit animation(s) it
contains to trigger.
source: https://www.framer.com/api/motion/animate-presence/
So your code would become:
export default function MainWrapper() {
const location = useLocation();
return (
<Main>
<Routes key={location.pathname}>
<AnimatePresence exitBeforeEnter initial={false}>
<Route key="dashboard" path="/" element={<Dashboard />} />
<Route key="project" path="project/:id/*" element={<Project />} />
</AnimatePresence>
</Routes>
</Main>
);
}
I'm a little confused on how to set up a theme wrapped around my index.js components, while using a switch state in my header.js that triggers light or dark theme.
trying to streamline this as much as possible to require less coding. However I'm a React Noob and would like some guidance on how I can set up my code properly to get everything working.
EDIT: Thanks to Diyorbek, I've managed to understand how to get this working. I'm now getting a theme is not defined for my index.js file
index.js:
import { ThemeProvider, CssBaseline } from "#material-ui/core";
import { createMuiTheme } from "#material-ui/core";
const MyThemeContext = React.createContext({});
export function useMyThemeContext() {
return useContext(MyThemeContext);
}
function MyThemeProvider(props) {
const [isDarkMode, setIsDarkMode] = useState(false);
const theme = useMemo(
() =>
createMuiTheme({
palette: {
type: isDarkMode ? 'dark' : 'light',
},
}),
[isDarkMode]
);
return (
<ThemeProvider theme={theme}>
<MyThemeContext.Provider value={{ isDarkMode, setIsDarkMode }}>
{props.children}
</MyThemeContext.Provider>
</ThemeProvider>
);
}
const routing = (
<Router>
<React.StrictMode>
<ThemeProvider theme={theme}>
<CssBaseline />
<Header />
<Switch>
<Route exact path="/" component={App} />
</Switch>
<Footer />
</ThemeProvider>
</React.StrictMode>
</Router>
);
ReactDOM.render(routing, document.getElementById('root'));
header.js:
import { useMyThemeContext } from '../index';
const useStyles = makeStyles((theme) => ({
appBar: {
borderBottom: `1px solid ${theme.palette.divider}`,
},
link: {
margin: theme.spacing(1, 1.5),
},
toolbarTitle: {
flexGrow: 1,
},
}));
function Header() {
const classes = useStyles();
const [isDarkMode, setIsDarkMode] = useMyThemeContext();
return (
<React.Fragment>
<CssBaseline />
<AppBar
position="static"
color="default"
elevation={0}
className={classes.appBar}
>
<Toolbar className={classes.toolbar}>
<Switch
checked={isDarkMode}
onChange={() => setIsDarkMode(!isDarkMode)}
/>
</Toolbar>
</AppBar>
</React.Fragment>
);
}
export default Header;
Thank you in advance for the help :)
You should make use of React Context.
This is how I would approach this case:
const MyThemeContext = createContext({});
export function useMyThemeContext() {
return useContext(MyThemeContext);
}
function MyThemeProvider(props) {
const [isDarkMode, setIsDarkMode] = useState(false);
const theme = useMemo(
() =>
createMuiTheme({
palette: {
type: isDarkMode ? 'dark' : 'light',
},
}),
[isDarkMode]
);
return (
<ThemeProvider theme={theme}>
<MyThemeContext.Provider value={{ isDarkMode, setIsDarkMode }}>
{props.children}
</MyThemeContext.Provider>
</ThemeProvider>
);
}
const routing = (
<Router>
<React.StrictMode>
<MyThemeProvider>
<CssBaseline />
<Header />
<Switch>
<Route exact path="/" component={App} />
</Switch>
<Footer />
</MyThemeProvider>
</React.StrictMode>
</Router>
);
import { useMyThemeContext } from "...."
function Header() {
const classes = useStyles();
const {isDarkMode, setIsDarkMode} = useMyThemeContext();
return (
<React.Fragment>
<CssBaseline />
<AppBar
position="static"
color="default"
elevation={0}
className={classes.appBar}
>
<Toolbar className={classes.toolbar}>
<Switch
checked={isDarkMode}
onChange={() => setIsDarkMode(!isDarkMode)}
/>
</Toolbar>
</AppBar>
</React.Fragment>
);
}
When working with react, if you want to change global variables that act like states, you'll want to use a Context. This allows you to access data and render components based on changes to this data throughout every page and component. Consult the Context documentation for more information & guides on usage.
I'm struggling to add <Link/> component to my material-ui AppBar
This is my navigation class:
class Navigation extends Component {
constructor(props) {
super(props)
}
render() {
var styles = {
appBar: {
flexWrap: 'wrap'
},
tabs: {
width: '100%'
}
}
return (
<AppBar showMenuIconButton={false} style={styles.appBar}>
<Tabs style={styles.tabs}>
<Tab label='Most popular ideas'/>
<Tab label='Latest ideas' />
<Tab label='My ideas' />
</Tabs>
</AppBar>
)
}
}
Which looks okay:
Tabs are clickable, have fluid animations, that's cool. But how do I wire them up together with react-router and its' <Link/> component?
I've tried adding onChange listener like that:
<Tab
label='My ideas'
onChange={<Link to='/myPath'></Link>}
/>
However I'm getting following error:
Uncaught Invariant Violation: Expected onChange listener to be a function, instead got type object
If I try to wrap <Tab/> component into <Link/> component, I'm getting error that <Tabs/> component accepts only <Tab/> component.
This doesn't work either (no error is being produced, but clicking on Tab does not bring me to the path):
<Tab label='Most popular ideas'>
<Link to='/popular'/>
</Tab>
How do I make <Link/> component work together with <Tabs> and <AppBar>? If that's not possible, I can use any other component from material-ui library to form a proper menu.
For Material UI 1.0 with Typescript: see this post by #ogglas below.
For Material-UI 1.0 with plain JS:
<Tabs value={value} onChange={this.handleChange}>
{
this.props.tabs.map(
({label, path})=><Tab key={label}
label={label}
className={classes.tabLink}
component={Link}
to={path} />
)
}
</Tabs>
And classes.tabLink is defined as:
tabLink : {
display:"flex",
alignItems:"center",
justifyContent:"center"
}
How this works?
All the mui 1.0 components inheriting from ButtonBase, support a component prop, see ButtonBase. The idea is to allow you to control what the component renders as its wrapper/root element. Tab also has this feature although at the time of writing this answer this prop is not documented explicitly, but as Tab inherits from ButtonBase, all its props carry over (and the documentation does cover this).
Another feature of ButtonBase is that all the extra props, not in use by ButtonBase or inherited component, are spread over the specified component. We have used this behavior to send the to prop used by Link by giving it to Tab control. You can send any additional props in the same way. Note that this is documented explicitly for both ButtonBase and Tab.
Thanks #josh-l for asking this to be added.
here's how you can do it now:
<Tabs onChange={this.changeTab} value={value}>
<Tab value={0} label="first" containerElement={<Link to="/first"/>} />
<Tab value={1} label="second" containerElement={<Link to="/second"/>}/>
<Tab value={2} label="third" containerElement={<Link to="/third"/>} />
</Tabs>
You can try this simple method
<Tab label='Most popular ideas' to='/myPath' component={Link} />
This is solved using the <Link /> from material-ui instead of directly using the <Link /> or <NavLink /> from react-router. The example for the same can be found in the documentation here.
https://material-ui.com/components/links/
Also <Button /> tag has a component prop to achieve this
<Button color="inherit" component={Link} to={"/logout"}>Logout</Button>
An extensive discussion on this can be found here
https://github.com/mui-org/material-ui/issues/850
Since we are using TypeScript I could not use #hazardous solutions. This is how we implemented routing for material-ui v1.0.0-beta.16 and react-router 4.2.0. The reason why we are splitting this.props.history.location.pathname is because we need to access /renewals/123 for example. If we did not do this we would get the following warning and no tab would be displayed as active: Warning: Material-UI: the value provided '/renewals/123' is invalid
Complete code with imports:
import * as React from "react";
import * as ReactDOM from "react-dom";
import * as ReactRouter from "react-router";
import * as PropTypes from "prop-types";
import { Switch, Route, Redirect, Link } from "react-router-dom";
import { Cases } from './../Cases';
import { SidePane } from './../SidePane';
import { withStyles, WithStyles } from 'material-ui/styles';
import Paper from 'material-ui/Paper';
import Tabs, { Tab } from 'material-ui/Tabs';
import { withRouter } from "react-router-dom";
import Badge from 'material-ui/Badge';
import Grid from 'material-ui/Grid';
import { Theme } from 'material-ui/styles';
import SimpleLineIcons from '../../Shared/SimpleLineIcons'
interface IState {
userName: string;
}
interface IProps {
history?: any
}
const styles = (theme: Theme) => ({
root: theme.typography.display1,
badge: {
right: '-28px',
color: theme.palette.common.white,
},
imageStyle:{
float: 'left',
height: '40px',
paddingTop: '10px'
},
myAccount: {
float: 'right'
},
topMenuAccount: {
marginLeft: '0.5em',
cursor: 'pointer'
}
});
type WithStyleProps = 'root' | 'badge' | 'imageStyle' | 'myAccount' | 'topMenuAccount';
class Menu extends React.Component<IProps & WithStyles<WithStyleProps>, IState> {
constructor(props: IProps & WithStyles<WithStyleProps>) {
super(props);
this.state = {
userName: localStorage.userName ? 'userName ' + localStorage.userName : ""
}
}
componentDidMount() {
this.setState({ userName: localStorage.userName ? localStorage.userName : "" })
}
logout(event: any) {
localStorage.removeItem('token');
window.location.href = "/"
}
handleChange = (event: any, value: any) => {
this.props.history.push(value);
};
render() {
const classes = this.props.classes;
let route = '/' + this.props.history.location.pathname.split('/')[1];
return (
<div>
<Grid container spacing={24}>
<Grid item xs={12} className={classes.root}>
<img src="/Features/Client/Menu/logo.png" alt="Logo" className={classes.imageStyle} />
<div className={this.props.classes.myAccount}>
<span><span className={this.props.classes.topMenuAccount}>MY ACCOUNT</span><span className={classes.topMenuAccount}><SimpleLineIcons iconName={'user'} />▾</span></span>
<span onClick={this.logout} className={classes.topMenuAccount}><SimpleLineIcons iconName={'logout'} /></span>
</div>
</Grid>
<Grid item xs={12} >
<div className="route-list">
<Tabs
value={route}
onChange={this.handleChange}
indicatorColor="primary"
textColor="primary"
>
<Tab label="Overview" value="/" />
<Tab label={<Badge classes={{ badge: classes.badge }} badgeContent={this.props.caseRenewalCount} color="primary">
Renewals
</Badge>} value="/renewals" />
</Tabs>
</div>
</Grid>
</Grid>
</div>
);
}
}
export default withStyles(styles)(withRouter(Menu))
TypeScript implementation of the router-driven tabs.
For those who look for the TypeScript implementation. Easy configurable. Driven by tabs configuration.
interface ITabsPageProps {
match: match<{page: string}>;
history: History;
}
const tabs = [{
label: 'Fist Tab',
link: 'fist-tab',
component: <FirstTabContent/>
}, {
label: 'Second Tab',
link: 'second-tab',
component: <SecondTabContent/>
}, {
label: 'Third Tab',
link: 'third-tab',
component: <ThirdTabContent/>
}];
export class TabsPage extends React.Component<ITabsPageProps> {
handleChange(tabLink: string) {
this.props.history.push(`/tabs-page/${tabLink}`);
}
render() {
const currentTab = this.props.match.params.page;
const selectedTab = tabs.find(tab => currentTab === tab.link);
return (
<Fragment>
<Tabs
value={currentTab}
onChange={(event, value) => this.handleChange(value)}
>
{tabs.map(tab => (
<Tab
key={tab.link}
value={tab.link}
label={tab.label}
/>
))}
</Tabs>
{selectedTab && selectedTab.component}
</Fragment>
);
}
}
Here's another implementation of React with hooks, Material-UI with tabs, React Router with Link, and TypeScript.
import * as React from "react";
import { BrowserRouter as Router, Route, Redirect, Switch, Link, LinkProps } from 'react-router-dom';
import AppBar from '#material-ui/core/AppBar';
import Tabs from '#material-ui/core/Tabs';
import { default as Tab, TabProps } from '#material-ui/core/Tab';
import Home from './Home';
import ProductManagement from './ProductManagement';
import Development from './Development';
import HomeIcon from '#material-ui/icons/Home';
import CodeIcon from '#material-ui/icons/Code';
import TimelineIcon from '#material-ui/icons/Timeline';
const LinkTab: React.ComponentType<TabProps & LinkProps> = Tab as React.ComponentType<TabProps & LinkProps>;
function NavBar() {
const [value, setValue] = React.useState(0);
const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => {
setValue(newValue);
};
return (
<div >
<AppBar position="static" >
<Tabs value={value} onChange={handleChange} centered>
<LinkTab label='Home' icon={ <HomeIcon />} component={Link} to="/" />
<LinkTab label='Development' icon={<CodeIcon />} component={Link} to="/dev" />
<LinkTab label='Product Management' icon={<TimelineIcon />} component={Link} to="/pm" />
</Tabs>
</AppBar>
</div>
)
};
export default function App() {
return (
<Router>
<div>
<NavBar />
<Switch>
<Route exact path="/" component={ Home } />
<Route exact path="/dev" component={ Development } />
<Route exact path="/pm" component={ ProductManagement } />
<Redirect from="/" to="/" />
</Switch>
</div>
</Router>
)
}
So my work-around for this solution has been quite reliable, though it may be more manual of a solution than what you're looking to do.
The strategy that I've been using is to actually not even use the Link Component. Instead, you'll utilize the Tabs onChange property as a callback that can respond to Tab clicks, and track location manually with Props on the Parent.
You can import a utility called History from react-router that will allow you to manually push locations. While using React-Router, your component tree will have access to Location prop that has a pathname key with the string of your current location.
We will manually parse this string into the components that make up your current URL, then use a Switch statement to decide both which tab is currently selected and also where to link to when a tab is clicked. (This gives you a fair amount of control over navigation)
( e.g. ['', 'latest'] )
Here is a mock up of what your component MAY look like after integrating this solution.
import React from 'react';
import {History} from 'react-router';
function parseLocation(location) {
if (String(location)) {
var locationArray = location.split('/');
return locationArray;
} else {
return false;
}
};
function filterPath(path) {
let locationArray = parseLocation(path);
return locationArray[locationArray.length - 1];
};
var Navigation = React.createClass({
mixins: [History],
getPage() {
if (this.props.location.pathname) {
let pathname = this.props.location.pathname;
let pageName = filterPath(pathname);
return pageName;
} else {
return false;
}
},
decideContent() {
let page = this.getPage();
let content;
switch(page) {
case 'popular':
content = 0;
case 'latest':
content = 1;
case 'myideas':
content = 2;
default:
content = 0;
}
return content;
},
handleTabChange(value) {
let location = false;
switch (value) {
case 0:
location = 'popular';
break;
case 1:
location = 'latest';
break;
case 2:
location = 'myideas';
break;
}
if (location && location !== this.getPage()) {
this.history.pushState(null, '/'+location);
}
},
render() {
var styles = {
appBar: {
flexWrap: 'wrap'
},
tabs: {
width: '100%'
}
};
let content = this.decideContent();
let tabs = <Tabs
onChange={this.handleTabChange}
value={content}
>
<Tab label="Most Popular Ideas" value={0} />
<Tab label="Latest Ideas" value={1} />
<Tab label="My Ideas" value={2} />
</Tabs>;
return (
<AppBar showMenuIconButton={false} style={styles.appBar}>
{tabs}
</AppBar>
);
}
});
Check this link, I implemented the solution and worked for me
Composition in material UI
If you use NextJs, you can do it like that, and create your own component.
*i didn`t wrap the Tab with 'a' tag, because it added automatically
const WrapTab = (props) => {
const { href } = props
return (
<Link href={href} style={{ width: "100%" }}>
<Tab {...props} />
</Link>
)}
and then this is your return
<Tabs
value={value}
indicatorColor="primary"
textColor="primary"
onChange={handleChange}
variant="fullWidth"
>
<WrapTab href="/testPage/?tab=0" icon={<MenuIcon />} />
<WrapTab href="/testPage/?tab=1" icon={<StampIcon2 />} />
<WrapTab href="/testPage/?tab=2" icon={<ShopIcon />} />
<WrapTab href="/testPage/?tab=3" icon={<PenIcon />} />
<WrapTab href="/testPage/?tab=4" icon={<ProfileIcon />} />
</Tabs>
link to material-ui docs:
https://material-ui.com/guides/composition/
This seems to work for me
import { Link as RouterLink } from 'react-router-dom';
import Link from '#mui/material/Link';
<Link to={menuItem.url} component={RouterLink} aria-current="page">
{menuItem.label}
</Link>
For anyone looking to wrap Material-ui Link with Next.js Link
import Link from "next/link"
import MuiLink from "#material-ui/core/Link"
const CustomNextLink = ({href, alt}) => ({children, ...rest}) => (
<Link href={href} alt={alt}>
<MuiLink {...rest}>
{children}
</MuiLink>
</Link>)
Then pass it to you Tab component
<Tab
key={...}
label={title}
icon={icon}
component={CustomNextLink({href: to, alt: title})}
style={...}
className={...}
classes={{selected: ...}}
{...a11yProps(index)}
/>
Use the href="" option as shown below:
<Tab
href="/en/getting-started"
label="Contact US"
style={{ color: "white", textDecoration: "none" }}
/>
To remove the ripple effect on clicking, use the option disableRipple