Overriding MUI Stepper styles in React 2022 - reactjs

I've been struggling with the proper way to override styles for a React MUI Stepper component in 2022. More specifically in the active, and completed states for the label as well as the circle icon.
My Code
import { ThemeProvider } from '#mui/styles';
import { createTheme } from '#mui/system';
const theme = createTheme({
overrides: {
MuiStepIcon: {
root: {
'&$completed': {
color: 'pink',
},
'&$active': {
color: 'red',
},
},
active: {},
completed: {},
},
MuiStepLabel: {
root: {
color: 'red'
}
}
}
})
<ThemeProvider theme={theme}>
<Stepper activeStep={activeStep} className='serviceReferralWizardStepper'>
{steps.map((label) => {
const stepProps = {};
const labelProps = {};
return (
<Step className='stepper-holder' style={{ margin: '0 1.8rem' }} key={label} {...stepProps}>
<StepLabel {...labelProps}><span className='serviceReferralWizardStepper__label'>{label}</span></StepLabel>
</Step>
);
})}
</Stepper>
Currently I see no styles at all applied. Any advice would be apprecieted.

I recommend looking at MUI's documentation example: https://v4.mui.com/components/steppers/#CustomizedSteppers.js
You will need to create custom components to represent the styles you want.
const CustomeConnector = withStyles({
active: {
'& $line': {
borderColor: '#784af4',
},
},
completed: {
'& $line': {
borderColor: '#784af4',
},
},
})(StepConnector);
function CustomeStepIcon(props) {
const classes = // create styles
const { active, completed } = props;
return (
<div
className={clsx(classes.root, {
[classes.active]: active,
})}
>
{completed ? <Check className={classes.completed} /> : <div className={classes.circle} />}
</div>
);
}
<Stepper alternativeLabel activeStep={activeStep} connector={<CustomConnector />}>
{steps.map((label) => (
<Step key={label}>
<StepLabel StepIconComponent={CustomStepIcon}>{label}</StepLabel>
</Step>
))}
</Stepper>
UPDATE
Here is an updated link to the v5 implementation: https://mui.com/components/steppers/#customized-horizontal-stepper

Related

Path aware side navigation using collapsible nested lists in React/NextJS

I've coded a collapsible nested navigation for my website. A menu object is iterated over to create the sidebar as it appears in the image. Items with children can be collapsed and this is also performing flawlessly. I am having trouble with implementing a feature which would automatically collapse all parents of an item if a user navigates to it. So in the image I added, if a user navigates to "/nastava/studiranje" from the main meenu or the "menu" on the right side of the creen (or receives a direct link to that location) "Nastava" and "Studiranje" lists should be collapsed.
I thought the right approach is to get the current route by using pathname method on the router object, passing the route to MultiLevel component and checking if it matches to the current passed item link. But it just doesn't seem to work that way.
Here is the code so far.
const SideMenu = () => {
const router = useRouter();
//menu is imported and sorted in createDataTree function
const [menu, setMenu] = useState([]);
const [openItems, setOpenItems] = useState();
const routerPathname = router.pathname;
useEffect(() => {
const pathnames = routerPathname.split('/').filter((x) => x);
setOpenItems(pathnames);
const theMenu = createDataTree(mainMenu.nodes);
setMenu(theMenu.filter((item) => item.url === '/' + pathnames[0]));
}, [routerPathname]);
return (
<Box>
//menu[0] to get from main menu object into "Nastava" child
{menu[0]?.childNodes.map((item, key) => (
<MenuItem key={key} item={item} openItems={openItems} />
))}
</Box>
);
};
export default SideMenu;
MenuItem.jsx
import MultiLevel from './MultiLevel';
import SingleLevel from './SingleLevel';
const hasChildren = (item) => {
const { childNodes: children } = item;
if (children === undefined) {
return false;
}
if (children.constructor !== Array) {
return false;
}
if (children.length === 0) {
return false;
}
return true;
};
const MenuItem = ({ item }) => {
const Component = hasChildren(item) ? MultiLevel : SingleLevel;
return <Component item={item} />;
};
export default MenuItem;
SingleLevel.jsx
import { ListItem } from '#mui/material';
import HeaderLink from '../../Elements/HeaderLink/HeaderLink';
const SingleLevel = ({ item, active }) => {
return (
<ListItem sx={{ listStyle: 'none', lineHeight: '0.5rem', borderColor: 'text.default' }}>
<HeaderLink href={item.url} sx={{ color: 'text.primary', '&:hover': { paddingLeft: '5px' }, fontWeight: '600' }}>
{item.label}
</HeaderLink>
</ListItem>
);
};
export default SingleLevel;
MultiLevel.jsx
import { Box, Collapse, List, ListItem } from '#mui/material';
import HeaderLink from '../../Elements/HeaderLink/HeaderLink';
import ExpandLess from '#mui/icons-material/ExpandLess';
import ExpandMore from '#mui/icons-material/ExpandMore';
import MenuItem from './MenuItem';
import { useState } from 'react';
const MultiLevel = ({ item }) => {
const { childNodes: children } = item;
const [open, setOpen] = useState(false);
const handleClick = () => {
setOpen((prev) => !prev);
};
return (
<>
<ListItem button sx={{ listStyle: 'none', lineHeight: '0.5rem', borderColor: 'text.default' }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', maxWidth: '200px', alignItems: 'center' }}>
<HeaderLink href={item.url} sx={{ color: 'text.primary', '&:hover': { paddingLeft: '5px' }, fontWeight: '600' }}>
{item.label}
</HeaderLink>
{open ? <ExpandLess onClick={handleClick} /> : <ExpandMore onClick={handleClick} />}
</Box>
</ListItem>
<Collapse in={open} timeout="auto" unmountOnExit>
<List component="div" disablePadding>
{children.map((child, key) => (
<MenuItem key={key} item={child} />
))}
</List>
</Collapse>
</>
);
};
export default MultiLevel;
This is an approximation of the menu object.
const menu = [
{
label: 'Nastava',
url:'/nastava',
childNodes: [
{
label:'Studiranje',
url:'/nastava/studiranje'
childNodes: [
{
label: 'Raspored',
url: '/nastava/studiranje/raspored'
},
{
label: 'Konzultacije',
url: '/nastava/studiranje/konzultacije'
},
......
],
{
label:'Upisi'
url:'/nastava/upisi'
},
{
label:'Studiji',
url:'/nastava/studiji',
childNodes:[
{
label:'Preddiplomski studiji',
url:'/nastava/studiji/preddiplomski-studiji'
},
{
label:'Diplomski studiji',
url:'/nastava/studiji/diplomski-studiji'
}
]
}
},
]
}

Dynamic URL in React

I'm working on a React project with Redux and I'm consuming a Rest API, I need to implement a functionality where when I select a project from a list and I need to load the project ID in the URL and direct to another screen where a sidebar with the options is loaded. navigation of this project.
Example: Layout
I managed to load the project's Id in the URL and retrieve this ID in the project's home screen, the problem is to store the project's Id and set this ID in the next selected URLs, for example:
path: '/project/:id/companies'
path: '/project/:id/currencies'
path: '/project/:id/settings'
List of projects:
Capture the project id and arrow the url:
href={`#/project/${row.id}/main`}
Routes:
path: '/project/:id/main',
exact: true,
name: 'projectMain',
component: RequireAuth(ProjectMain),
Retrieve ID in main
import { useParams } from 'react-router-dom';
...
const { id } = useParams();
The problem is in the sidebar, where I load a list of items with the path, I'm not able to pass the project id in this list.
Complementando a pergunta
In Sidebar I'm using useHistory(), the problem is that the path comes static by 'props' through importing a file into my template, as you can see below:
Template
import React from 'react';
import { Grid, makeStyles } from '#material-ui/core';
import {
AppContent,
AppHeader,
SidebarApp,
} from '../components/index';
import itemsProject from '../components/itemsSidebar/itemsProject';
const useStyles = makeStyles(theme => ({
appContent: {
paddingLeft: 240,
width: '100%',
backgroundColor: theme.palette.background.paper,
},
}));
const ProjectLayout = () => {
const classes = useStyles();
return (
<div className={classes.appContent}>
<AppHeader />
<Grid container direction="row">
<SidebarApp items={itemsProject} />
<AppContent />
</Grid>
</div>
);
};
export default ProjectLayout;
Sidebar:
/* eslint-disable react/jsx-no-duplicate-props */
import React from 'react';
import List from '#material-ui/core/List';
import ListItem from '#material-ui/core/ListItem';
import Divider from '#material-ui/core/Divider';
import ExpandMoreIcon from '#material-ui/icons/ExpandMore';
import ExpandLessIcon from '#material-ui/icons/ExpandLess';
import Collapse from '#material-ui/core/Collapse';
import {
alpha,
Box,
Card,
ListSubheader,
makeStyles,
Typography,
} from '#material-ui/core';
import { useHistory, useLocation, useParams } from 'react-router-dom';
import translate from '../providers/i18n/translate';
const useStyles = makeStyles(theme => ({
sidebar: {
background: theme.palette.background.dark,
width: 240,
height: '100vh',
border: '1px solid rgba(0, 0, 0, 0.1)',
display: 'flex',
flexDirection: 'column',
position: 'absolute',
paddingTop: 64,
top: 0,
left: 0,
},
sidebarItem: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
},
sidebarItemContent: {
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
overflow: 'hidden',
display: 'flex',
alignItems: 'center',
width: '100%',
},
sidebarItemIcon: {
marginRight: 6,
},
sidebarItemText: {
width: '100%',
},
sidebarItemExpandArrow: {
fontSize: '1.2rem !important',
},
sidebarItemExpandArrowExpanded: {
fontSize: '1.2rem !important',
color: theme.palette.primary.main,
fontWeight: 'bold',
},
active: {
background: alpha(theme.palette.primary.light, 0.2),
},
}));
function SidebarItem({ depthStep = 10, depth = 0, expanded, item, ...rest }) {
const [collapsed, setCollapsed] = React.useState(true);
const { label, items, Icon, onClick: onClickProp } = item;
const classes = useStyles();
const history = useHistory();
const location = useLocation();
function toggleCollapse() {
setCollapsed(prevValue => !prevValue);
}
function onClick(e) {
if (Array.isArray(items)) {
toggleCollapse();
}
if (onClickProp) {
onClickProp(e, item);
history.push(item.path);
}
}
let expandIcon;
if (Array.isArray(items) && items.length) {
expandIcon = !collapsed ? (
<>
<ExpandLessIcon className={classes.sidebarItemExpandArrowExpanded} />
</>
) : (
<ExpandMoreIcon className={classes.sidebarItemExpandArrow} />
);
}
return (
<>
<ListItem
className={classes.sidebarItem}
onClick={onClick}
button
dense
className={location.pathname === item.path ? classes.active : null}
{...rest}
>
<div
style={{ paddingLeft: depth * depthStep }}
className={classes.sidebarItemContent}
>
{Icon && (
<Icon
className={classes.sidebarItemIcon}
fontSize="small"
color="primary"
/>
)}
<div className={classes.sidebarItemText}>{label}</div>
</div>
{expandIcon}
</ListItem>
<Collapse in={!collapsed} timeout="auto" unmountOnExit>
{Array.isArray(items) ? (
<List disablePadding dense>
{items.map((subItem, index) => (
<React.Fragment key={`${subItem.name}${index}`}>
{subItem === 'divider' ? (
<Divider style={{ margin: '6px 0' }} />
) : (
<SidebarItem
depth={depth + 1}
depthStep={depthStep}
item={subItem}
/>
)}
</React.Fragment>
))}
</List>
) : null}
</Collapse>
</>
);
}
function Sidebar({ items, depthStep, depth, expanded }) {
const classes = useStyles();
const { key } = useParams();
return (
<Card elevation={0} className={classes.sidebar}>
<List
disablePadding
dense
subheader={
<ListSubheader component="div" id="nested-list-subheader">
{translate('sidebarMenuSettings')}
<Typography>
<Box>{key}</Box>
</Typography>
</ListSubheader>
}
>
{items.map((sidebarItem, index) => (
<React.Fragment key={`${sidebarItem.name}${index}`}>
{sidebarItem === 'divider' ? (
<Divider style={{ margin: '6px 0' }} />
) : (
<SidebarItem
depthStep={depthStep}
depth={depth}
expanded={expanded}
item={sidebarItem}
/>
)}
</React.Fragment>
))}
</List>
</Card>
);
}
export default Sidebar;
Sidebar list items
function onClick(e, item) {}
const itemsProject = [
{
name: 'companies',
label: translate('sidebarProjectCompanies'),
Icon: CompanyIcon,
path: '/project/:id/companies',
onClick,
}
{
name: 'currencies',
label: translate('sidebarProjectCurrencies'),
Icon: CurrencyIcon,
path: '/project/:id/currencies',
onClick,
}
];
export default itemsProject;
How can I pass the ID variable on the Sidebar list items?
I thank you for your help!
You can use ES6 template literals as follows.
path: `/project/${id}/companies`
Since you already defined your path, you just need to use useHistory and navigate to the new link
import { useHistory } from 'react-router';
...
const history = useHistory();
...
// call this whenever you want to navigate
history.push(`/project/${id}/currencies`);

How do customise a component's CSS selectors in FluentUI

I am trying to customise the behavior of an FluentUI component when it is hovered over (A Primary Button in this case). How do I customise CSS selectors when I am using Microsoft's React FluentUI library.
I tried this initially (This approach is deprecated now, in favor of the method where you add selectors as siblings)...
export const MyButton = (props: IButtonProps) => {
const styles: IButtonStyles = {
root: {
backgroundColor: '#000000',
selectors : {
':hover': {
backgroundColor: '#0000ff'
}
}
},
}
return (
<PrimaryButton styles={styles} {...props} />
);
}
Then I tried this:
export const MyButton = (props: IButtonProps) => {
const styles: IButtonStyles = {
root: {
backgroundColor: '#000000',
':hover': {
backgroundColor: '#0000ff'
}
},
}
return (
<PrimaryButton styles={styles} {...props} />
);
}
Both approaches do not seem to be working. Am I missing something?
With new FluentUI you can modify styles trough specific props based on button state:
export const MyButton = (props: IButtonProps) => {
const styles: IButtonStyles = {
root: {
backgroundColor: '#000000',
},
rootHovered: {
backgroundColor: '#0000ff',
},
}
return (
<PrimaryButton styles={styles} {...props} />
);
}
Codepen working example.
On this link you have IButtonStyles interface.

How do you give material-ui drawer component a switchable theme?

I am making a react project but I have a problem getting my material-ui drawer to match with my dark/light switchable theme. I currently have given the drawer styling using makeStyles, but I can't for the life of me get it sync with ThemeProvider.
I have tried doing it all in ThemeProvider but the drawer required other methods. Then I tried doing it all with makeStyles and not using ThemeProvider at all but that didn't work either. I also tried putting a conditional ternary statement in makeStyles for this, but that didn't work.
Any help or advice that leads me in the right direction would be greatly appreciated!
function App() {
const dark_theme = createMuiTheme({
palette: {
primary: {
main: '#e78f23',
secondary: {
main: '#463d3d',
}
},
background: {
default: "#222222",
},
text: {
primary: "#ffffff"
},
}
})
const light_theme = createMuiTheme({
palete: {
primary: {
main: '#4892BC',
},
secondary: {
main: '#EEF3FF',
},
background: {
default: "#e4f0e2"
},
text: {
primary: "#000000"
},
}
});
const useStyles = makeStyles((theme) => ({
drawer: {
background : '#e78f23',
},
switchTrack: {
backgroundColor: "#000"
},
switchBase: {
color: "#000",
"&.Mui-checked": {
color: "white"
},
"&.Mui-checked + .MuiSwitch-track": {
backgroundColor: "white"
},
},
}));
const classes = useStyles()
const [light, setLight] = useState(true);
return (
<ThemeProvider theme={light ? light_theme : dark_theme}>
<CssBaseline />
<Router>
<main>
<Drawer id="Drawer" PaperProps={{className:classes.drawer}} open={drawerOpen} onClose={CloseDrawer}>
<List id="ListID">
<Button>
<ListItem button onClick={() => setLight(prev => !prev)} color="primary" class="themeButton">
<FormControl>
<FormLabel>Dark/Light</FormLabel>
<FormGroup>
<FormControlLabel
id="formControlLabel"
control = {
<Switch2
color="default"
classes={{
track: classes.switchTrack,
switchBase: classes.switchBase,
}}
size="medium"
position="center"
checked={checked}
onChange={toggleChecked}
labelPlacement="end"
/>
}
/>
</FormGroup>
</FormControl>
</ListItem>
</Button>

How do you change the Stepper color on React Material UI?

In the screenshot above, I am trying to change the step color to either: green for correct, yellow for in-progress and red for incorrect.
How could I do this?
In case anyone is still looking for this question, for MUI 5 it is through the sx property or styled.
Checkout what are the classes of step, stepIcon so you can customize the styles.
<Box sx={{ width: '100%' }}>
<Stepper activeStep={currentStep} alternativeLabel>
{Object.keys(steps).map((stepNumber) => (
<Step
key={stepNumber}
sx={{
'& .MuiStepLabel-root .Mui-completed': {
color: 'secondary.dark', // circle color (COMPLETED)
},
'& .MuiStepLabel-label.Mui-completed.MuiStepLabel-alternativeLabel':
{
color: 'grey.500', // Just text label (COMPLETED)
},
'& .MuiStepLabel-root .Mui-active': {
color: 'secondary.main', // circle color (ACTIVE)
},
'& .MuiStepLabel-label.Mui-active.MuiStepLabel-alternativeLabel':
{
color: 'common.white', // Just text label (ACTIVE)
},
'& .MuiStepLabel-root .Mui-active .MuiStepIcon-text': {
fill: 'black', // circle's number (ACTIVE)
},
}}>
<StepLabel>{steps[stepNumber].label}</StepLabel>
</Step>
))}
</Stepper>
</Box>
Update: Here is correct way for latest version 3. You just need to add the overrides correctly to your theme by referencing MuiStepIcon:
const theme = createMuiTheme({
overrides: {
MuiStepIcon: {
root: {
'&$completed': {
color: 'pink',
},
'&$active': {
color: 'red',
},
},
active: {},
completed: {},
},
palette: {
...
}
})
Old question but in case anyone is looking.
You need to edit the theme and wrap it in getMuiTheme
import getMuiTheme from 'material-ui/styles/getMuiTheme'
const muiTheme = getMuiTheme({
stepper: {
iconColor: 'green' // or logic to change color
}
})
<MuiThemeProvider muiTheme={muiTheme}>
<Stepper>
...
</Stepper>
</MuiThemeProvider>
See https://github.com/callemall/material-ui/blob/master/src/styles/getMuiTheme.js for full list of components and their default color schmemes.
You will see you can override colors on a per component basis and/or change the overall theme colors.
This is a way I used to override it using classes overrides - all other properties remain the same.
const styles = theme => ({
labelContainer: {
"& $alternativeLabel": {
marginTop: 0
}
},
step: {
"& $completed": {
color: "lightgreen"
},
"& $active": {
color: "pink"
},
"& $disabled": {
color: "red"
}
},
alternativeLabel: {},
active: {}, //needed so that the &$active tag works
completed: {},
disabled: {},
labelContainer: {
"& $alternativeLabel": {
marginTop: 0
}
},
});
class myStepper extends Component {
render() {
const { classes } = this.props;
return(
<Stepper
activeStep={activeStep}
alternativeLabel
connector={connector}
classes={{
root: classes.root
}}
>
{this.state.numberTasks.map(label => {
return (
<Step
key={label}
classes={{
root: classes.step,
completed: classes.completed,
active: classes.active
}}
>
<StepLabel
classes={{
alternativeLabel: classes.alternativeLabel,
labelContainer: classes.labelContainer
}}
StepIconProps={{
classes: {
root: classes.step,
completed: classes.completed,
active: classes.active,
disabled: classes.disabled
}
}}
>
{this.state.labels[label - 1]} //label value here
</StepLabel>
</Step>
);
})}
</Stepper>
);
}
export default withStyles(styles)(myStepper);
You can set the classes property for the StepIconProps property:
part of JavaScript styles
...
icon:{
fill:"green",
},
text:{
fill:"white",
},
...
<Step key="someKey">
<StepLabel StepIconProps={{
classes: {
active: classes.icon,
text: classes.text,
}
}}>
Some Text
</StepLabel>
</Step>
Then your style should overwrite the default theme color by using the !important CSS rule:
const styles = theme => ({
icon: {
color: "red !important"
},
});
Wish I could comment the answer by "#Piotr O", in regards to keeping the step numbers but do not have enough rep yet.
You need to set the icon prop to the index of the Step to keep the numbers.
<Stepper activeStep={activeStep}>
{steps.map((label, index) => {
return (
<Step>
<StepLabel icon={index+1} />
</Step>
);
})}
</Stepper>
If you were to use different icons like you mentioned, you'd need some conditional logic to swap the icon via the icon prop or another possibility is to add the className prop to <StepLabel /> when a condition is met and then style it with CSS.
I included an example with both concepts here: https://codesandbox.io/s/l5m570jq0l
I did this via the sx property on the MobileStepper. I specifically wanted to change the colour of the dots indicating progress
<MobileStepper
variant='dots'
steps={maxSteps}
...
sx={{
'.MuiMobileStepper-dot': { backgroundColor: '#CCCCCC' },
'.MuiMobileStepper-dotActive': { backgroundColor: '#666666' },
}}
...
/>
You need to change props icon of a StepLabel component as below:
<StepLabel
icon={<WarningIcon color={red500} />}
style={{color: red500}}
>
Random label
</StepLabel>
We need to pass the class to StepperLabel props . For example if we need to change alternativeLabel class then try the below:-
<StepLabel
StepIconComponent={stepperIcon}
classes={{
alternativeLabel: classes.alternativeLabel,
}}
>
<span className={cn(classes.label, labelClass)}>
{'label'}
</span>
</StepLabel>

Resources