Styling selected TreeItem inside a TreeView

Viewed 8489

I'm trying to override the style of the selected TreeItem inside a Material-UI TreeView component. According to the CSS API docs, there is a selected selector, but when I use that, I see the entire subtree getting styled, and not just the selected item. What is the correct selector to use to only style the selected tree item?

Code Sandbox: https://codesandbox.io/s/nostalgic-flower-e85cd?file=/src/App.js

import React from "react";
import { makeStyles } from "@material-ui/core/styles";
import TreeView from "@material-ui/lab/TreeView";
import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
import ChevronRightIcon from "@material-ui/icons/ChevronRight";
import MuiTreeItem from "@material-ui/lab/TreeItem";
import { withStyles } from "@material-ui/core/styles";

const useStyles = makeStyles({
  root: {
    height: 240,
    flexGrow: 1,
    maxWidth: 400
  }
});

const TreeItem = withStyles({
  selected: {
    color: "red"
  }
})(MuiTreeItem);

export default function FileSystemNavigator() {
  const classes = useStyles();

  return (
    <TreeView
      className={classes.root}
      defaultCollapseIcon={<ExpandMoreIcon />}
      defaultExpandIcon={<ChevronRightIcon />}
    >
      <TreeItem nodeId="1" label="Applications">
        <TreeItem nodeId="2" label="Calendar" />
        <TreeItem nodeId="3" label="Chrome" />
        <TreeItem nodeId="4" label="Webstorm" />
      </TreeItem>
      <TreeItem nodeId="5" label="Documents">
        <TreeItem nodeId="10" label="OSS" />
        <TreeItem nodeId="6" label="Material-UI">
          <TreeItem nodeId="7" label="src">
            <TreeItem nodeId="8" label="index.js" />
            <TreeItem nodeId="9" label="tree-view.js" />
          </TreeItem>
        </TreeItem>
      </TreeItem>
    </TreeView>
  );
}
3 Answers

When you have questions about how to override the default Material-UI styles, the best resource is to look at how the default styles are defined.

Below are the most relevant portions of the default styles for TreeItem:

export const styles = (theme) => ({
  /* Styles applied to the root element. */
  root: {
    '&$selected > $content $label': {
      backgroundColor: fade(theme.palette.primary.main, theme.palette.action.selectedOpacity),
    },
    '&$selected > $content $label:hover, &$selected:focus > $content $label': {
      backgroundColor: fade(
        theme.palette.primary.main,
        theme.palette.action.selectedOpacity + theme.palette.action.hoverOpacity,
      ),
      // Reset on touch devices, it doesn't add specificity
      '@media (hover: none)': {
        backgroundColor: 'transparent',
      },
    },
  },
  /* Pseudo-class applied to the root element when selected. */
  selected: {},
  /* Styles applied to the tree node content. */
  content: {
    width: '100%',
    display: 'flex',
    alignItems: 'center',
    cursor: 'pointer',
  },
  /* Styles applied to the label element. */
  label: {
    '&:hover': {
      backgroundColor: theme.palette.action.hover,
      // Reset on touch devices, it doesn't add specificity
      '@media (hover: none)': {
        backgroundColor: 'transparent',
      },
    },
  },
});

The overall structure of TreeItem (which can also be found by looking at the source code) is as follows (simplified slightly):

<li className={clsx(classes.root, className, {[classes.selected]: selected})}>
   <div className={classes.content}>
      <div className={classes.iconContainer}>
         {icon}
      </div>
      <Typography component="div" className={classes.label}>
         {label}
      </Typography>
   </div>
   {children}
</li>

In the structure above, you can see that the selected class is on the root element which is an <li> which surrounds both the content specific to this TreeItem as well as its children. The TreeItem's own content is in the <div> with the content class and that div contains the icon and label.

You can target the content div as follows:

const TreeItem = withStyles({
  root: {
    "&.Mui-selected > .MuiTreeItem-content": {
      color: "red"
    }
  }
})(MuiTreeItem);

Edit selected TreeItem

which is equivalent to:

const TreeItem = withStyles({
  root: {
    "&$selected > $content": {
      color: "red"
    }
  },
  selected: {},
  content: {}
})(MuiTreeItem);

If you don't want to include the icon in the styling, then you can just target the label within the content:

const TreeItem = withStyles({
  root: {
    "&.Mui-selected > .MuiTreeItem-content .MuiTreeItem-label": {
      color: "red"
    }
  }
})(MuiTreeItem);

Edit selected TreeItem

which is equivalent to:

const TreeItem = withStyles({
  root: {
    "&$selected > $content $label": {
      color: "red"
    }
  },
  selected: {},
  content: {},
  label: {}
})(MuiTreeItem);

If you want to change the background color, you need to pay attention to a few more details in the default styling (since the default styling does a lot with background color) in order to deal appropriately with the hover and focused states.

import MuiTreeItem from "@material-ui/lab/TreeItem";
import { withStyles } from "@material-ui/core/styles";

const TreeItem = withStyles({
  root: {
    "&.Mui-selected > .MuiTreeItem-content": {
      background: "#89CFF0"
    },
    "&.MuiTreeItem-root > .MuiTreeItem-content:hover": {
      background: "gray",
    },
    "&.MuiTreeItem-root > .MuiTreeItem-content:hover > .MuiTreeItem-label": {
      background: "#89CFF0",
    },
    '@media (hover: none)': {
      backgroundColor: 'transparent',
    }
  }
})(MuiTreeItem);

The answers so far use withStyles from MUI v4 because that was the current version when they were posted. I'm pretty sure there is a MUI v5 approach, but I gave up on trying to get that to work.

The technique used doesn't really matter as much, anyway: The problem is getting all the selectors right.

I ended up adding this to index.css which seems to disable any most focused / hover / selected styles (some edge cases seem to persist):

.MuiTreeItem-root:hover {
  background: transparent
}

.MuiTreeItem-root > .MuiTreeItem-content:hover {
  background: transparent
}

.MuiTreeItem-root > .MuiTreeItem-content:hover > .MuiTreeItem-label {
  background: transparent
}

.MuiTreeItem-root > .MuiTreeItem-content.Mui-selected {
  background: transparent
}

.MuiTreeItem-root > .MuiTreeItem-content.Mui-selected:hover {
  background: transparent
}

.MuiTreeItem-root > .MuiTreeItem-content.Mui-selected > .MuiTreeItem-label {
  background: transparent
}

.MuiTreeItem-root > .MuiTreeItem-content.Mui-selected.Mui-focused {
  background: transparent
}

Yes, as far as I can tell, they are all required. Probably still a few more.

NOTE: That's focused (with one s), using e.g. MUI-focussed fails silently.

Related