Reputation: 153
I am trying to override pseudo-classes in Stepper component using makeStyles:
const useStyles = makeStyles((theme) => ({
active: {
color: theme.palette.primary.main,
},
completed: {
color: theme.palette.goodyear.status.positive,
},
root: {
color: theme.palette.goodyear.grey.medium,
fontWeight: 500,
},
text: {
color: theme.palette.text.titles,
},
iconContainer: {
transform: 'scale(1.667)',
},
label: {
fontSize: '1.2rem',
fontWeight: 500,
},
}));
const StepLabel = (props) => {
const classes = useStyles();
return (
<MaterialStepLabel
classes={{
iconContainer: classes.iconContainer,
label: classes.label,
}}
StepIconProps={{
classes: {
active: classes.active,
completed: classes.completed,
root: classes.root,
text: classes.text,
},
}}
{...props}
/>
);
};
Unfortunately in the browser the results look like that:
The classes that were created by makeStyles are there, but are overridden by default because it's more specific? You can also see that the completed class is also below the root class, which would be strange, since root is the element in general state, and the completed pseudo should override that styles.
What could be the problem here and how should I use that classes correctly?
Upvotes: 4
Views: 2834
Reputation: 80966
Below is the definition of the default styles for StepIcon:
export const styles = (theme) => ({
/* Styles applied to the root element. */
root: {
display: 'block',
color: theme.palette.text.disabled,
'&$completed': {
color: theme.palette.primary.main,
},
'&$active': {
color: theme.palette.primary.main,
},
'&$error': {
color: theme.palette.error.main,
},
},
/* Styles applied to the SVG text element. */
text: {
fill: theme.palette.primary.contrastText,
fontSize: theme.typography.caption.fontSize,
fontFamily: theme.typography.fontFamily,
},
/* Pseudo-class applied to the root element if `active={true}`. */
active: {},
/* Pseudo-class applied to the root element if `completed={true}`. */
completed: {},
/* Pseudo-class applied to the root element if `error={true}`. */
error: {},
});
The key to understanding the problems you are experiencing is to better understand how CSS specificity works.
In the styles above, you can see that all the states other than the default are applied via a declaration with two CSS class names. The &
refers back to root
and then $completed
and $active
refer to the corresponding rules defined via completed: {}
and active: {}
. As you saw when inspecting the styles, &$completed
resolves eventually to be .MuiStepIcon-root.MuiStepIcon-completed
.
The styles in a CSS declaration with two class selectors (e.g. .MuiStepIcon-root.MuiStepIcon-completed
) will always win over styles in a CSS declaration with a single class selector (as is the case with all of your styles). When specificity is the same, such as with your makeStyles-root-x
and makeStyles-completed-x
, then the one declared last will win. You declared your root
class after your completed
class (and this relative ordering carries through to the stylesheet in the <head>
generated for your makeStyles
call), so your root
class wins.
For your style overrides to work, you should use the same specificity as used in the default styles in Material-UI. I would recommend defining your root
and completed
styles as follows:
const useStyles = makeStyles((theme) => ({
root: {
color: theme.palette.goodyear.grey.medium,
fontWeight: 500,
"&.MuiStepIcon-completed": {
color: theme.palette.goodyear.status.positive,
},
},
}));
With this approach you don't need to specify anything for completed
within the classes
prop -- just root
.
Below is a full working example based on one of the demos (the stepIconRoot
class being the most relevant portion):
import React from "react";
import { makeStyles } from "@material-ui/core/styles";
import Stepper from "@material-ui/core/Stepper";
import Step from "@material-ui/core/Step";
import StepLabel from "@material-ui/core/StepLabel";
import Button from "@material-ui/core/Button";
import Typography from "@material-ui/core/Typography";
const useStyles = makeStyles((theme) => ({
root: {
width: "100%"
},
button: {
marginRight: theme.spacing(1)
},
instructions: {
marginTop: theme.spacing(1),
marginBottom: theme.spacing(1)
},
stepIconRoot: {
color: "orange",
"&.MuiStepIcon-active": {
color: "purple"
},
"&.MuiStepIcon-completed": {
color: "green"
}
}
}));
function getSteps() {
return ["Select campaign settings", "Create an ad group", "Create an ad"];
}
function getStepContent(step) {
switch (step) {
case 0:
return "Select campaign settings...";
case 1:
return "What is an ad group anyways?";
case 2:
return "This is the bit I really care about!";
default:
return "Unknown step";
}
}
export default function HorizontalLinearStepper() {
const classes = useStyles();
const [activeStep, setActiveStep] = React.useState(0);
const [skipped, setSkipped] = React.useState(new Set());
const steps = getSteps();
const isStepOptional = (step) => {
return step === 1;
};
const isStepSkipped = (step) => {
return skipped.has(step);
};
const handleNext = () => {
let newSkipped = skipped;
if (isStepSkipped(activeStep)) {
newSkipped = new Set(newSkipped.values());
newSkipped.delete(activeStep);
}
setActiveStep((prevActiveStep) => prevActiveStep + 1);
setSkipped(newSkipped);
};
const handleBack = () => {
setActiveStep((prevActiveStep) => prevActiveStep - 1);
};
const handleSkip = () => {
if (!isStepOptional(activeStep)) {
// You probably want to guard against something like this,
// it should never occur unless someone's actively trying to break something.
throw new Error("You can't skip a step that isn't optional.");
}
setActiveStep((prevActiveStep) => prevActiveStep + 1);
setSkipped((prevSkipped) => {
const newSkipped = new Set(prevSkipped.values());
newSkipped.add(activeStep);
return newSkipped;
});
};
const handleReset = () => {
setActiveStep(0);
};
return (
<div className={classes.root}>
<Stepper activeStep={activeStep}>
{steps.map((label, index) => {
const stepProps = {};
const labelProps = {
StepIconProps: { classes: { root: classes.stepIconRoot } }
};
if (isStepOptional(index)) {
labelProps.optional = (
<Typography variant="caption">Optional</Typography>
);
}
if (isStepSkipped(index)) {
stepProps.completed = false;
}
return (
<Step key={label} {...stepProps}>
<StepLabel {...labelProps}>{label}</StepLabel>
</Step>
);
})}
</Stepper>
<div>
{activeStep === steps.length ? (
<div>
<Typography className={classes.instructions}>
All steps completed - you're finished
</Typography>
<Button onClick={handleReset} className={classes.button}>
Reset
</Button>
</div>
) : (
<div>
<Typography className={classes.instructions}>
{getStepContent(activeStep)}
</Typography>
<div>
<Button
disabled={activeStep === 0}
onClick={handleBack}
className={classes.button}
>
Back
</Button>
{isStepOptional(activeStep) && (
<Button
variant="contained"
color="primary"
onClick={handleSkip}
className={classes.button}
>
Skip
</Button>
)}
<Button
variant="contained"
color="primary"
onClick={handleNext}
className={classes.button}
>
{activeStep === steps.length - 1 ? "Finish" : "Next"}
</Button>
</div>
</div>
)}
</div>
</div>
);
}
Upvotes: 3