Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tree Item doesn't expand full leading to selection "breaking off" when user scrolls horizontally #291

Closed
lkp-k opened this issue Jan 17, 2025 · 1 comment

Comments

@lkp-k
Copy link

lkp-k commented Jan 17, 2025

When there are long Tree Items, I want to be able to show them in a "selected" state where they have a background color applied to them. However, the background color does not extend fully.

Maybe pictures would help to explain the issue best -

Nothing selected - Note the first item is long and extends
Image

Scrolled to the end horizontally, and selected the first item
Image

Now, I've selected a child, and scrolled midway. You can see how the selection background color is cut off
Image

Is there a way to force the items to expand to the full width?

Here's my code:

import { Box, Typography } from "@mui/material";
import { blue, grey } from "@mui/material/colors";
import {
  FolderIcon,
  FolderOpenIcon,
  PackageIcon,
  TagIcon,
  UsersIcon,
} from "lucide-react";
import { useMemo } from "react";
import { Tree, NodeRendererProps, NodeApi } from "react-arborist";
import useResizeObserver from "use-resize-observer";
import { ResourceCategoryDTO, ResourceDTO } from "@shared-types";

import useGetResources from "../../../../../hooks/api/useGetResourcesQuery";

interface TreeNode {
  id: string;
  name: string;
  children?: TreeNode[];
  type: "category" | "resource";
  dto: ResourceCategoryDTO | ResourceDTO;
}

const buildTree = (
  categories: ResourceCategoryDTO[],
  resources: ResourceDTO[]
): TreeNode[] => {
  const categoryMap: { [key: string]: TreeNode } = {};

  categories.forEach((category) => {
    categoryMap[category.id] = {
      id: category.id,
      name: category.name,
      type: "category",
      children: [],
      dto: category,
    };
  });

  resources.forEach((resource) => {
    if (categoryMap[resource.categoryId]) {
      categoryMap[resource.categoryId].children?.push({
        id: resource.id,
        name: resource.name,
        type: "resource",
        dto: resource,
      });
    }
  });

  const tree: TreeNode[] = [];
  Object.values(categoryMap).forEach((category) => {
    if (!categories.find((cat) => cat.id === category.id)?.parentId) {
      tree.push(category);
    } else {
      const parentId = categories.find(
        (cat) => cat.id === category.id
      )?.parentId;
      if (parentId && categoryMap[parentId]) {
        categoryMap[parentId].children?.push(category);
      }
    }
  });

  return tree;
};

const ResourceTree = ({ organizationId }: { organizationId: string }) => {
  const { data: rcrData } = useGetResources(organizationId);

  const treeData = useMemo(
    () => buildTree(rcrData?.categories || [], rcrData?.resources || []),
    [rcrData]
  );

  const renderNodeLabel = (node: NodeApi<TreeNode>) => {
    return (
      <Box
        sx={{
          display: "flex",
          alignItems: "center",
          gap: 1.5,
          flexShrink: 0,
        }}
      >
        {node.data.type === "category" ? (
          (node.children?.length || 0) > 0 && node.isOpen ? (
            <FolderOpenIcon size={20} />
          ) : (
            <FolderIcon size={20} />
          )
        ) : node.data.type === "resource" ? (
          (node.data.dto as ResourceDTO).type === "labor" ? (
            <UsersIcon size={20} />
          ) : (node.data.dto as ResourceDTO).type === "non-labor" ? (
            <TagIcon size={20} />
          ) : (
            <PackageIcon size={20} />
          )
        ) : null}
        <Typography
          sx={{
            fontWeight: node.isFocused ? 500 : "",
            color: node.isFocused ? "text.primary" : grey[800],
            whiteSpace: "nowrap",
            textOverflow: "ellipsis",
            overflow: "hidden",
          }}
        >
          {node.data.name}
        </Typography>
      </Box>
    );
  };

  const renderNode = ({
    node,
    style,
    dragHandle,
  }: NodeRendererProps<TreeNode>) => {
    return (
      <Box
        style={style}
        sx={{
          bgcolor: node.isFocused ? blue[50] : "",
          display: "flex",
          alignItems: "center",
          paddingX: 1,
          minWidth: "max-content",
        }}
        ref={dragHandle}
        onClick={node.toggle}
      >
        {renderNodeLabel(node)}
      </Box>
    );
  };

  const { ref, width, height } = useResizeObserver();

  return (
    <Box ref={ref} sx={{ flex: 1, overflowX: "auto", display: "flex" }}>
      <Box sx={{ minWidth: "100%", width: "100%" }}>
        <Tree data={treeData} height={height} width={width}>
          {renderNode}
        </Tree>
      </Box>
    </Box>
  );
};

export default ResourceTree;
@jameskerr
Copy link
Member

I think you might need to also handle rendering the "row" in addition to the "node".

React arborist render's stuff like this.

  <list>
    <row>
      <node />
    </row>
  </list>

By default, the library renders the row for you, but you can also render it yourself. Here's the default row renderer for reference.

https://github.com/brimdata/react-arborist/blob/main/modules/react-arborist/src/components/default-row.tsx

And some docs about the props

https://github.com/brimdata/react-arborist/blob/main/README.md#row-component-props

And you may enjoy reading this about "overflow" in html and css.

https://css-tricks.com/almanac/properties/o/overflow/

Good luck!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants