dmarr
dmarr

Reputation: 490

Styling selected TreeItem inside a TreeView

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

Upvotes: 4

Views: 14981

Answers (3)

Ryan Cogswell
Ryan Cogswell

Reputation: 81036

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 default styles for TreeItemContent:

const StyledTreeItemContent = styled(TreeItemContent, {
  name: 'MuiTreeItem',
  slot: 'Content',
  overridesResolver: (props, styles) => {
    return [
      styles.content,
      styles.iconContainer && {
        [`& .${treeItemClasses.iconContainer}`]: styles.iconContainer,
      },
      styles.label && {
        [`& .${treeItemClasses.label}`]: styles.label,
      },
    ];
  },
})(({ theme }) => ({
  padding: '0 8px',
  width: '100%',
  display: 'flex',
  alignItems: 'center',
  cursor: 'pointer',
  WebkitTapHighlightColor: 'transparent',
  '&:hover': {
    backgroundColor: (theme.vars || theme).palette.action.hover,
    // Reset on touch devices, it doesn't add specificity
    '@media (hover: none)': {
      backgroundColor: 'transparent',
    },
  },
  [`&.${treeItemClasses.disabled}`]: {
    opacity: (theme.vars || theme).palette.action.disabledOpacity,
    backgroundColor: 'transparent',
  },
  [`&.${treeItemClasses.focused}`]: {
    backgroundColor: (theme.vars || theme).palette.action.focus,
  },
  [`&.${treeItemClasses.selected}`]: {
    backgroundColor: theme.vars
      ? `rgba(${theme.vars.palette.primary.mainChannel} / ${theme.vars.palette.action.selectedOpacity})`
      : alpha(theme.palette.primary.main, theme.palette.action.selectedOpacity),
    '&:hover': {
      backgroundColor: theme.vars
        ? `rgba(${theme.vars.palette.primary.mainChannel} / calc(${theme.vars.palette.action.selectedOpacity} + ${theme.vars.palette.action.hoverOpacity}))`
        : alpha(
            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: theme.vars
          ? `rgba(${theme.vars.palette.primary.mainChannel} / ${theme.vars.palette.action.selectedOpacity})`
          : alpha(theme.palette.primary.main, theme.palette.action.selectedOpacity),
      },
    },
    [`&.${treeItemClasses.focused}`]: {
      backgroundColor: theme.vars
        ? `rgba(${theme.vars.palette.primary.mainChannel} / calc(${theme.vars.palette.action.selectedOpacity} + ${theme.vars.palette.action.focusOpacity}))`
        : alpha(
            theme.palette.primary.main,
            theme.palette.action.selectedOpacity + theme.palette.action.focusOpacity,
          ),
    },
  },
  [`& .${treeItemClasses.iconContainer}`]: {
    marginRight: 4,
    width: 15,
    display: 'flex',
    flexShrink: 0,
    justifyContent: 'center',
    '& svg': {
      fontSize: 18,
    },
  },
  [`& .${treeItemClasses.label}`]: {
    width: '100%',
    // fixes overflow - see https://github.com/mui/material-ui/issues/27372
    minWidth: 0,
    paddingLeft: 4,
    position: 'relative',
    ...theme.typography.body1,
  },
}));

The overall structure of TreeItem can be found by looking at TreeItem and TreeItemContent and is as follows (simplified slightly):

<li className="MuiTreeItem-root">
   <div className="MuiTreeItem-content">
      <div className="MuiTreeItem-iconContainer">
         {icon}
      </div>
      <div className="MuiTreeItem-label">
         {label}
      </div>
   </div>
   {children}
</li>

Additionally, the Mui-selected, Mui-expanded, Mui-focused, and Mui-disabled classes get added to the MuiTreeItem-content div when applicable.

You can target the MuiTreeItem-content div as follows:

const TreeItem = styled(MuiTreeItem)`
  & > .MuiTreeItem-content.Mui-selected {
    color: red;
  }
`;

Edit TreeView customization

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

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

Edit TreeView customization

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.

Upvotes: 10

ssc
ssc

Reputation: 9883

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.

Upvotes: 2

Vishala Ramasamy
Vishala Ramasamy

Reputation: 367

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);

Upvotes: 0

Related Questions