Skip to content
Snippets Groups Projects
Unverified Commit 9bc8dc0d authored by Alexander Lesnenko's avatar Alexander Lesnenko Committed by GitHub
Browse files

Convert Tree component to Typescript (#20692)

* Convert Tree component to Typescript

* fix types

* review
parent 88a4e178
No related branches found
No related tags found
No related merge requests found
import React, { useState, useCallback } from "react";
import PropTypes from "prop-types";
import { TreeNodeList } from "./TreeNodeList";
import { TreeNode } from "./TreeNode";
import { getInitialExpandedIds } from "./utils";
import { ColorScheme, ITreeNodeItem } from "./types";
const propTypes = {
TreeNodeComponent: PropTypes.object,
data: PropTypes.array.isRequired,
onSelect: PropTypes.func.isRequired,
colorScheme: PropTypes.oneOf(["default", "admin"]),
selectedId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
emptyState: PropTypes.node,
};
interface TreeProps {
data: ITreeNodeItem[];
onSelect: (item: ITreeNodeItem) => void;
selectedId?: ITreeNodeItem["id"];
colorScheme?: ColorScheme;
emptyState?: React.ReactNode;
}
export function Tree({
TreeNodeComponent = TreeNode,
data,
onSelect,
selectedId,
colorScheme = "default",
emptyState = null,
}) {
}: TreeProps) {
const [expandedIds, setExpandedIds] = useState(
new Set(selectedId != null ? getInitialExpandedIds(selectedId, data) : []),
);
......@@ -43,7 +40,6 @@ export function Tree({
return (
<TreeNodeList
colorScheme={colorScheme}
TreeNodeComponent={TreeNodeComponent}
items={data}
onSelect={onSelect}
onToggleExpand={handleToggleExpand}
......@@ -53,5 +49,3 @@ export function Tree({
/>
);
}
Tree.propTypes = propTypes;
import styled from "@emotion/styled";
import { css } from "@emotion/react";
import colors, { lighten } from "metabase/lib/colors";
import Icon from "metabase/components/Icon";
import Icon, { IconProps } from "metabase/components/Icon";
import { ColorScheme } from "./types";
const COLOR_SCHEMES = {
admin: {
......@@ -14,7 +15,13 @@ const COLOR_SCHEMES = {
},
};
export const TreeNodeRoot = styled.li`
interface TreeNodeRootProps {
isSelected: boolean;
depth: number;
colorScheme: ColorScheme;
}
export const TreeNodeRoot = styled.li<TreeNodeRootProps>`
display: flex;
align-items: center;
color: ${props =>
......@@ -45,7 +52,11 @@ export const ExpandToggleButton = styled.button`
visibility: ${props => (props.hidden ? "hidden" : "visible")};
`;
export const ExpandToggleIcon = styled(Icon)`
interface ExpandToggleIconProps {
isExpanded: boolean;
}
export const ExpandToggleIcon = styled(Icon)<ExpandToggleIconProps & IconProps>`
transition: transform 200ms;
${props =>
......@@ -72,12 +83,3 @@ export const IconContainer = styled.div`
padding: 0.25rem;
opacity: 0.5;
`;
export const RightArrowContainer = styled.div`
display: flex;
align-items: center;
color: ${props =>
props.isSelected
? colors["white"]
: COLOR_SCHEMES[props.colorScheme].text()};
`;
/* eslint-disable react/prop-types */
import React from "react";
import PropTypes from "prop-types";
import _ from "underscore";
import Icon, { iconPropTypes } from "metabase/components/Icon";
import Icon from "metabase/components/Icon";
import {
TreeNodeRoot,
......@@ -10,31 +10,23 @@ import {
ExpandToggleIcon,
NameContainer,
IconContainer,
RightArrowContainer,
} from "./TreeNode.styled";
import { ITreeNodeItem } from "./types";
const propTypes = {
isExpanded: PropTypes.bool.isRequired,
isSelected: PropTypes.bool.isRequired,
hasChildren: PropTypes.bool.isRequired,
onToggleExpand: PropTypes.func,
onSelect: PropTypes.func.isRequired,
depth: PropTypes.number.isRequired,
item: PropTypes.shape({
name: PropTypes.string.isRequired,
icon: PropTypes.oneOfType([
PropTypes.string,
PropTypes.shape(iconPropTypes),
]),
hasRightArrow: PropTypes.string,
id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
}).isRequired,
colorScheme: PropTypes.oneOf(["default", "admin"]),
};
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;
}
// eslint-disable-next-line react/display-name
export const TreeNode = React.memo(
React.forwardRef(function TreeNode(
React.forwardRef<HTMLLIElement, TreeNodeProps>(function TreeNode(
{
isExpanded,
isSelected,
......@@ -47,7 +39,7 @@ export const TreeNode = React.memo(
},
ref,
) {
const { name, icon, hasRightArrow, id } = item;
const { name, icon, id } = item;
const iconProps = _.isObject(icon) ? icon : { name: icon };
......@@ -56,7 +48,7 @@ export const TreeNode = React.memo(
onToggleExpand(id);
};
const handleKeyDown = ({ key }) => {
const handleKeyDown: React.KeyboardEventHandler = ({ key }) => {
switch (key) {
case "Enter":
onSelect(item);
......@@ -82,24 +74,20 @@ export const TreeNode = React.memo(
onKeyDown={handleKeyDown}
>
<ExpandToggleButton hidden={!hasChildren}>
<ExpandToggleIcon isExpanded={isExpanded} />
<ExpandToggleIcon
isExpanded={isExpanded}
name="chevronright"
size={12}
/>
</ExpandToggleButton>
{icon && (
<IconContainer colorScheme={colorScheme}>
<IconContainer>
<Icon {...iconProps} />
</IconContainer>
)}
<NameContainer>{name}</NameContainer>
{hasRightArrow && (
<RightArrowContainer isSelected={isSelected}>
<Icon name="chevronright" size={14} />
</RightArrowContainer>
)}
</TreeNodeRoot>
);
}),
);
TreeNode.propTypes = propTypes;
import React from "react";
import PropTypes from "prop-types";
import { useScrollOnMount } from "metabase/hooks/use-scroll-on-mount";
import { ColorScheme, ITreeNodeItem } from "./types";
import { TreeNode } from "./TreeNode";
const propTypes = {
TreeNodeComponent: PropTypes.object.isRequired,
items: PropTypes.array.isRequired,
onToggleExpand: PropTypes.func,
onSelect: PropTypes.func.isRequired,
expandedIds: PropTypes.instanceOf(Set),
selectedId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
depth: PropTypes.number.isRequired,
colorScheme: PropTypes.oneOf(["default", "admin"]),
};
interface TreeNodeListProps {
items: ITreeNodeItem[];
onToggleExpand: (id: ITreeNodeItem["id"]) => void;
onSelect: (item: ITreeNodeItem) => void;
expandedIds: Set<ITreeNodeItem["id"]>;
selectedId?: ITreeNodeItem["id"];
depth: number;
colorScheme: ColorScheme;
}
export function TreeNodeList({
TreeNodeComponent,
items,
onToggleExpand,
onSelect,
......@@ -22,7 +21,7 @@ export function TreeNodeList({
selectedId,
depth,
colorScheme,
}) {
}: TreeNodeListProps) {
const selectedRef = useScrollOnMount();
return (
......@@ -35,7 +34,7 @@ export function TreeNodeList({
return (
<React.Fragment key={item.id}>
<TreeNodeComponent
<TreeNode
ref={isSelected ? selectedRef : null}
colorScheme={colorScheme}
item={item}
......@@ -49,8 +48,8 @@ export function TreeNodeList({
{isExpanded && (
<TreeNodeList
colorScheme={colorScheme}
TreeNodeComponent={TreeNodeComponent}
items={item.children}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
items={item.children!}
onToggleExpand={onToggleExpand}
onSelect={onSelect}
expandedIds={expandedIds}
......@@ -64,5 +63,3 @@ export function TreeNodeList({
</ul>
);
}
TreeNodeList.propTypes = propTypes;
import { IconProps } from "../Icon";
export interface ITreeNodeItem {
id: string | number;
name: string;
icon: string | IconProps;
children?: ITreeNodeItem[];
}
export type ColorScheme = "admin" | "default";
export const getInitialExpandedIds = (selectedId, nodes) =>
import { ITreeNodeItem } from "./types";
export const getInitialExpandedIds = (
selectedId: ITreeNodeItem["id"],
nodes: ITreeNodeItem[],
): ITreeNodeItem["id"][] =>
nodes
.map(node => {
if (node.id === selectedId) {
......@@ -9,5 +14,7 @@ export const getInitialExpandedIds = (selectedId, nodes) =>
const path = getInitialExpandedIds(selectedId, node.children);
return path.length > 0 ? [node.id, ...path] : [];
}
return [];
})
.flat();
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