mui / mui-x

MUI X: Build complex and data-rich applications using a growing list of advanced React components, like the Data Grid, Date and Time Pickers, Charts, and more!
https://mui.com/x/
4.11k stars 1.27k forks source link

[tree view] `RichTreeView` custom component doesn't seem to be updating #14774

Open royassis opened 1 day ago

royassis commented 1 day ago

Steps to reproduce

  1. Use this code for a file browser as an example.
  2. Set a custom property,loading, on each tree element. Which is boolean.
  3. Set a spinner when the property is true. Example:
    
    // This is the example code, modified.
    // Search for this line  {other?.loading && <CircularProgress size={20}/>}

import * as React from "react"; import clsx from "clsx"; import { animated, useSpring } from "@react-spring/web"; import { styled, alpha } from "@mui/material/styles";

import Box from "@mui/material/Box"; import Collapse from "@mui/material/Collapse"; import Typography from "@mui/material/Typography"; import ArticleIcon from "@mui/icons-material/Article"; import DeleteIcon from "@mui/icons-material/Delete"; import FolderOpenIcon from "@mui/icons-material/FolderOpen"; import FolderRounded from "@mui/icons-material/FolderRounded"; import ImageIcon from "@mui/icons-material/Image"; import PictureAsPdfIcon from "@mui/icons-material/PictureAsPdf"; import VideoCameraBackIcon from "@mui/icons-material/VideoCameraBack"; import { treeItemClasses } from "@mui/x-tree-view/TreeItem"; import { useTreeItem2 } from "@mui/x-tree-view/useTreeItem2"; import { TreeItem2Checkbox, TreeItem2Content, TreeItem2IconContainer, TreeItem2Label, TreeItem2Root, } from "@mui/x-tree-view/TreeItem2"; import { TreeItem2Icon } from "@mui/x-tree-view/TreeItem2Icon"; import { TreeItem2Provider } from "@mui/x-tree-view/TreeItem2Provider"; import { TreeItem2DragAndDropOverlay } from "@mui/x-tree-view/TreeItem2DragAndDropOverlay";

import CircularProgress from '@mui/material/CircularProgress';

function DotIcon() { return ( <Box sx={{ width: 6, height: 6, borderRadius: "70%", bgcolor: "warning.main", display: "inline-block", verticalAlign: "middle", zIndex: 1, mx: 1, }} /> ); }

const StyledTreeItemRoot = styled(TreeItem2Root)(({ theme }) => ({ color: theme.palette.grey[400], position: "relative",

marginLeft: theme.spacing(3.5),

}, ...theme.applyStyles("light", { color: theme.palette.grey[800], }), }));

const CustomTreeItemContent = styled(TreeItem2Content)(({ theme }) => ({ flexDirection: "row-reverse", borderRadius: theme.spacing(0.7), marginBottom: theme.spacing(0.5), marginTop: theme.spacing(0.5), padding: theme.spacing(0.5), paddingRight: theme.spacing(1), fontWeight: 500,

"&:not(.Mui-focused, .Mui-selected, .Mui-selected.Mui-focused) .labelIcon":
  {
    color: theme.palette.primary.dark,
    ...theme.applyStyles("light", {
      color: theme.palette.primary.main,
    }),
  },
"&::before": {
  content: '""',
  display: "block",
  position: "absolute",
  left: "16px",
  top: "44px",
  height: "calc(100% - 48px)",
  width: "1.5px",
  backgroundColor: theme.palette.grey[700],
  ...theme.applyStyles("light", {
    backgroundColor: theme.palette.grey[300],
  }),
},

}, "&:hover": { backgroundColor: alpha(theme.palette.primary.main, 0.1), color: "white", ...theme.applyStyles("light", { color: theme.palette.primary.main, }), },

backgroundColor: theme.palette.primary.dark,
color: theme.palette.primary.contrastText,
...theme.applyStyles("light", {
  backgroundColor: theme.palette.primary.main,
}),

}, }));

const AnimatedCollapse = animated(Collapse);

function TransitionComponent(props) { const style = useSpring({ to: { opacity: props.in ? 1 : 0, transform: translate3d(0,${props.in ? 0 : 20}px,0), }, });

return <AnimatedCollapse style={style} {...props} />; }

const StyledTreeItemLabelText = styled(Typography)({ color: "inherit", fontFamily: "General Sans", fontWeight: 500, });

function CustomLabel({ icon: Icon, expandable, children, ...other }) { console.log("other.loading",other.loading) return ( <TreeItem2Label {...other} sx={{ display: "flex", alignItems: "center", }}

{Icon && ( <Box component={Icon} className="labelIcon" color="inherit" sx={{ mr: 1, fontSize: "1.2rem" }} /> )}

  <StyledTreeItemLabelText variant="body2">
    {children}
  </StyledTreeItemLabelText>
  {expandable && <DotIcon />}
  {other?.loading && <CircularProgress size={20}/>}
</TreeItem2Label>

); }

const isExpandable = (reactChildren) => { if (Array.isArray(reactChildren)) { return reactChildren.length > 0 && reactChildren.some(isExpandable); } return Boolean(reactChildren); };

const getIconFromFileType = (fileType) => { switch (fileType) { case "file": return ImageIcon; case "pdf": return PictureAsPdfIcon; case "doc": return ArticleIcon; case "video": return VideoCameraBackIcon; case "directory": return FolderRounded; case "pinned": return FolderOpenIcon; case "trash": return DeleteIcon; default: return ArticleIcon; } };

export const CustomTreeItem = React.forwardRef( function CustomTreeItem(props, ref) { const { id, itemId, label, disabled, children, ...other } = props;

const {
  getRootProps,
  getContentProps,
  getIconContainerProps,
  getCheckboxProps,
  getLabelProps,
  getGroupTransitionProps,
  getDragAndDropOverlayProps,
  status,
  publicAPI,
} = useTreeItem2({ id, itemId, children, label, disabled, rootRef: ref });

const item = publicAPI.getItem(itemId);
const expandable = isExpandable(children);
const isLoading = item?.loading ?? false
let icon;
if (expandable) {
  icon = FolderRounded;
} else if (item.type) {
  icon = getIconFromFileType(item.type);
}

return (
  <TreeItem2Provider itemId={itemId}>
    <StyledTreeItemRoot {...getRootProps(other)}>
      <CustomTreeItemContent
        {...getContentProps({
          className: clsx("content", {
            "Mui-expanded": status.expanded,
            "Mui-selected": status.selected,
            "Mui-focused": status.focused,
            "Mui-disabled": status.disabled,
          }),
        })}
      >
        <TreeItem2IconContainer {...getIconContainerProps()}>
          <TreeItem2Icon status={status} />
        </TreeItem2IconContainer>
        <TreeItem2Checkbox {...getCheckboxProps()} />
        <CustomLabel
          {...getLabelProps({
            icon,
            expandable: expandable && status.expanded,
            loading: isLoading
          })}
        />
        <TreeItem2DragAndDropOverlay {...getDragAndDropOverlayProps()} />
      </CustomTreeItemContent>
      {children && <TransitionComponent {...getGroupTransitionProps()} />}
    </StyledTreeItemRoot>
  </TreeItem2Provider>
);

} );

  1. Update the tree items using setState, and change the loading property for some of the items.

Current behavior

loading property is not updating and spinner is not showing.

Expected behavior

loading property is updating and spinner is showing.

Context

I am using the Rich Tree View. I want to display a spinner when I am loading additional tree children from my backend. I want to render a spinner on a parent tree element when it is loading.

Your environment

npx @mui/envinfo ``` System: OS: Windows 11 10.0.22631 Binaries: Node: 20.13.1 - C:\Program Files\nodejs\node.EXE npm: 10.8.3 - C:\Program Files\nodejs\npm.CMD pnpm: Not Found Browsers: Chrome: Not Found Edge: Chromium (127.0.2651.74) npmPackages: @emotion/react: ^11.13.3 => 11.13.3 @emotion/styled: ^11.13.0 => 11.13.0 @mui/core-downloads-tracker: 6.1.1 @mui/icons-material: ^6.1.1 => 6.1.1 @mui/material: ^6.1.1 => 6.1.1 @mui/private-theming: 6.1.1 @mui/styled-engine: 6.1.1 @mui/system: 6.1.1 @mui/types: 7.2.17 @mui/utils: 6.1.1 @mui/x-internals: 7.18.0 @mui/x-tree-view: ^7.18.0 => 7.18.0 @types/react: 18.3.8 react: ^18.3.1 => 18.3.1 react-dom: ^18.3.1 => 18.3.1 typescript: 4.9.5 ```

Using Chrome

Search keywords: update treeview richtreeview rich tree

noraleonte commented 23 hours ago

Hey @royassis πŸ‘‹

Thanks for opening the issue. I think I will need an exact reproduction case for what you are trying to do πŸ€” You should technically be able to conditionally set a loading state for your items doing something like this. I simplified the demo a bit, but the way you do it depends on your specific example

royassis commented 20 hours ago

Thanks @noraleonte !

Works like a charm. Got any idea why the snippet I added that use useTreeItem2 and TreeItem2Provider won't work?

noraleonte commented 20 hours ago

Happy to help @royassis πŸ˜„

Not sure why your snippet didn't work πŸ€” You should be able to do the same thing even if you build the custom item with useTreeItem2 and TreeItem2Provider πŸ€” I think it depends on how you update the state (the logic is missing from your snippet)

Here's an example with the file explorer demo :)

royassis commented 20 hours ago

Hey @noraleonte

I updated the snippet to use the same logic. Sadly it still won't work. I'm probably missing something. I use this though:

  <CustomLabel
      {...getLabelProps({
        icon,
        expandable: expandable && status.expanded,
        loading: isLoading(props.itemId)
      })}
    />
noraleonte commented 20 hours ago

@royassis Would you be able to provide a CodeSandbox or Stackblitz example with what you are trying to achieve exactly? Otherwise I'm not sure I can figure out the problem πŸ˜…

royassis commented 20 hours ago

@noraleonte sure thing, i'll send a link to CodeSandbox

royassis commented 20 hours ago

@noraleonte https://codesandbox.io/p/sandbox/mui-tree-test-3llkm5

src\components\steps\AccordionContent\FileExplorer\CustomTreeItem2.jsx

michelengelen commented 18 hours ago

@noraleonte could it be that the props spreading is the issue here?

noraleonte commented 1 hour ago

@michelengelen it shouldn't be a problem πŸ€”

@royassis In this case I think the RichTreeView is using a different TreeItem, not th eone with the spinning. TreeView.tsx line 126

image

That should fix it I think πŸ€”

royassis commented 39 minutes ago

Hey @noraleonte it does, at least I think it does. Look at line 146 in CustomTreeItem.jsx