Cragmorton
Cragmorton

Reputation: 153

Material-ui makeStyles overwritten by default

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:

screenshot of app

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

Answers (1)

Ryan Cogswell
Ryan Cogswell

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&apos;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>
  );
}

Edit StepIcon overrides

Upvotes: 3

Related Questions