Skip to content
Snippets Groups Projects
Unverified Commit 9e49a370 authored by Anton Kulyk's avatar Anton Kulyk Committed by GitHub
Browse files

Browsing | Refactor `Tree` component (#21287)

parent 0e8d4570
No related branches found
No related tags found
No related merge requests found
import styled from "@emotion/styled";
import { color } from "metabase/lib/colors";
import { Tree } from "metabase/components/tree";
import { color, lighten } from "metabase/lib/colors";
export const FilterableTreeRoot = styled.div`
display: flex;
......@@ -24,3 +25,14 @@ export const ItemGroupsDivider = styled.hr`
export const EmptyStateContainer = styled.div`
margin-top: 6.25rem;
`;
export const AdminTreeNode = styled(Tree.Node)`
color: ${props => (props.isSelected ? color("white") : color("text-medium"))};
background-color: ${props => (props.isSelected ? color("accent7") : "unset")};
&:hover {
background-color: ${props =>
props.isSelected ? color("accent7") : lighten(color("accent7"), 0.6)};
}
`;
......@@ -12,6 +12,7 @@ import {
FilterableTreeRoot,
FilterInputContainer,
ItemGroupsDivider,
AdminTreeNode,
} from "./FilterableTree.styled";
import { searchItems } from "./utils";
import { ITreeNodeItem } from "metabase/components/tree/types";
......@@ -63,10 +64,10 @@ export const FilterableTree = ({
<FilterableTreeContainer>
{filteredList && (
<Tree
colorScheme="admin"
data={filteredList}
selectedId={selectedId}
onSelect={onSelect}
TreeNode={AdminTreeNode}
emptyState={
<EmptyStateContainer>
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
......@@ -85,10 +86,10 @@ export const FilterableTree = ({
return (
<React.Fragment key={index}>
<Tree
colorScheme="admin"
data={items}
selectedId={selectedId}
onSelect={onSelect}
TreeNode={AdminTreeNode}
/>
{!isLastGroup && <ItemGroupsDivider />}
</React.Fragment>
......
import React, { useState, useCallback } from "react";
import { TreeNodeList } from "./TreeNodeList";
import { TreeNode as DefaultTreeNode } from "./TreeNode";
import { getInitialExpandedIds } from "./utils";
import { ColorScheme, ITreeNodeItem } from "./types";
import { ITreeNodeItem, TreeNodeComponent } from "./types";
interface TreeProps {
data: ITreeNodeItem[];
onSelect: (item: ITreeNodeItem) => void;
selectedId?: ITreeNodeItem["id"];
colorScheme?: ColorScheme;
emptyState?: React.ReactNode;
onSelect?: (item: ITreeNodeItem) => void;
TreeNode?: TreeNodeComponent;
}
export function Tree({
function BaseTree({
data,
onSelect,
selectedId,
colorScheme = "default",
emptyState = null,
onSelect,
TreeNode = DefaultTreeNode,
}: TreeProps) {
const [expandedIds, setExpandedIds] = useState(
new Set(selectedId != null ? getInitialExpandedIds(selectedId, data) : []),
......@@ -39,13 +40,17 @@ export function Tree({
return (
<TreeNodeList
colorScheme={colorScheme}
items={data}
onSelect={onSelect}
onToggleExpand={handleToggleExpand}
TreeNode={TreeNode}
expandedIds={expandedIds}
selectedId={selectedId}
depth={0}
onSelect={onSelect}
onToggleExpand={handleToggleExpand}
/>
);
}
export const Tree = Object.assign(BaseTree, {
Node: DefaultTreeNode,
});
......@@ -2,34 +2,17 @@ import styled from "@emotion/styled";
import { css } from "@emotion/react";
import colors, { lighten } from "metabase/lib/colors";
import Icon, { IconProps } from "metabase/components/Icon";
import { ColorScheme } from "./types";
const COLOR_SCHEMES = {
admin: {
text: () => colors["text-medium"],
background: () => colors["accent7"],
},
default: {
text: () => colors["brand"],
background: () => colors["brand"],
},
};
interface TreeNodeRootProps {
isSelected: boolean;
depth: number;
colorScheme: ColorScheme;
}
export const TreeNodeRoot = styled.li<TreeNodeRootProps>`
display: flex;
align-items: center;
color: ${props =>
props.isSelected
? colors["white"]
: COLOR_SCHEMES[props.colorScheme].text()};
background-color: ${props =>
props.isSelected ? COLOR_SCHEMES[props.colorScheme].background() : "unset"};
color: ${props => (props.isSelected ? colors["white"] : colors["brand"])};
background-color: ${props => (props.isSelected ? colors["brand"] : "unset")};
padding-left: ${props => props.depth + 0.5}rem;
padding-right: 0.5rem;
cursor: pointer;
......@@ -37,9 +20,7 @@ export const TreeNodeRoot = styled.li<TreeNodeRootProps>`
&:hover {
background-color: ${props =>
props.isSelected
? COLOR_SCHEMES[props.colorScheme].background()
: lighten(COLOR_SCHEMES[props.colorScheme].background(), 0.6)};
props.isSelected ? colors["brand"] : lighten(colors["brand"], 0.6)};
}
`;
......
......@@ -11,67 +11,56 @@ import {
NameContainer,
IconContainer,
} from "./TreeNode.styled";
import { ITreeNodeItem } from "./types";
export interface TreeNodeProps {
item: ITreeNodeItem;
depth: number;
hasChildren: boolean;
isExpanded: boolean;
isSelected: boolean;
colorScheme: "default" | "admin";
onSelect: (item: ITreeNodeItem) => void;
onToggleExpand: (id: ITreeNodeItem["id"]) => void;
}
import { TreeNodeProps } from "./types";
// eslint-disable-next-line react/display-name
export const TreeNode = React.memo(
const BaseTreeNode = React.memo(
React.forwardRef<HTMLLIElement, TreeNodeProps>(function TreeNode(
{
item,
depth,
isExpanded,
isSelected,
hasChildren,
onToggleExpand,
onSelect,
depth,
item,
colorScheme,
onToggleExpand,
...props
},
ref,
) {
const { name, icon, id } = item;
const { name, icon } = item;
const iconProps = _.isObject(icon) ? icon : { name: icon };
const handleSelect = () => {
onSelect(item);
onToggleExpand(id);
};
function onClick() {
onSelect?.();
onToggleExpand();
}
const handleKeyDown: React.KeyboardEventHandler = ({ key }) => {
switch (key) {
case "Enter":
onSelect(item);
onSelect?.();
break;
case "ArrowRight":
!isExpanded && onToggleExpand(id);
!isExpanded && onToggleExpand();
break;
case "ArrowLeft":
isExpanded && onToggleExpand(id);
isExpanded && onToggleExpand();
break;
}
};
return (
<TreeNodeRoot
ref={ref}
role="menuitem"
tabIndex={0}
colorScheme={colorScheme}
onClick={onClick}
{...props}
depth={depth}
onClick={handleSelect}
isSelected={isSelected}
onKeyDown={handleKeyDown}
ref={ref}
>
<ExpandToggleButton hidden={!hasChildren}>
<ExpandToggleIcon
......@@ -91,3 +80,11 @@ export const TreeNode = React.memo(
);
}),
);
export const TreeNode = Object.assign(BaseTreeNode, {
Root: TreeNodeRoot,
ExpandToggleButton,
ExpandToggleIcon,
NameContainer,
IconContainer,
});
import React from "react";
import { useScrollOnMount } from "metabase/hooks/use-scroll-on-mount";
import { ColorScheme, ITreeNodeItem } from "./types";
import { TreeNode } from "./TreeNode";
import { ITreeNodeItem, TreeNodeComponent } from "./types";
interface TreeNodeListProps {
items: ITreeNodeItem[];
onToggleExpand: (id: ITreeNodeItem["id"]) => void;
onSelect: (item: ITreeNodeItem) => void;
expandedIds: Set<ITreeNodeItem["id"]>;
selectedId?: ITreeNodeItem["id"];
depth: number;
colorScheme: ColorScheme;
onToggleExpand: (id: ITreeNodeItem["id"]) => void;
onSelect?: (item: ITreeNodeItem) => void;
TreeNode: TreeNodeComponent;
}
export function TreeNodeList({
items,
onToggleExpand,
onSelect,
expandedIds,
selectedId,
depth,
colorScheme,
onSelect,
onToggleExpand,
TreeNode,
}: TreeNodeListProps) {
const selectedRef = useScrollOnMount();
......@@ -31,15 +30,17 @@ export function TreeNodeList({
const hasChildren =
Array.isArray(item.children) && item.children.length > 0;
const isExpanded = hasChildren && expandedIds.has(item.id);
const onItemSelect =
typeof onSelect === "function" ? () => onSelect(item) : undefined;
const onItemToggle = () => onToggleExpand(item.id);
return (
<React.Fragment key={item.id}>
<TreeNode
ref={isSelected ? selectedRef : null}
colorScheme={colorScheme}
item={item}
onToggleExpand={onToggleExpand}
onSelect={onSelect}
onSelect={onItemSelect}
onToggleExpand={onItemToggle}
isSelected={isSelected}
isExpanded={isExpanded}
hasChildren={hasChildren}
......@@ -47,14 +48,14 @@ export function TreeNodeList({
/>
{isExpanded && (
<TreeNodeList
colorScheme={colorScheme}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
items={item.children!}
onToggleExpand={onToggleExpand}
onSelect={onSelect}
expandedIds={expandedIds}
selectedId={selectedId}
depth={depth + 1}
onSelect={onSelect}
onToggleExpand={onToggleExpand}
TreeNode={TreeNode}
/>
)}
</React.Fragment>
......
import React from "react";
import { IconProps } from "../Icon";
export interface ITreeNodeItem {
......@@ -7,4 +8,16 @@ export interface ITreeNodeItem {
children?: ITreeNodeItem[];
}
export type ColorScheme = "admin" | "default";
export interface TreeNodeProps {
item: ITreeNodeItem;
depth: number;
hasChildren: boolean;
isExpanded: boolean;
isSelected: boolean;
onSelect?: () => void;
onToggleExpand: () => void;
}
export type TreeNodeComponent = React.ComponentType<
TreeNodeProps & React.RefAttributes<HTMLLIElement>
>;
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment