UNPKG

7.43 kBTypeScriptView Raw
1/*
2 * Copyright 2015 Palantir Technologies, Inc. All rights reserved.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17import classNames from "classnames";
18import * as React from "react";
19
20import * as Classes from "../../common/classes";
21import { DISPLAYNAME_PREFIX, Props, MaybeElement } from "../../common/props";
22import { Collapse } from "../collapse/collapse";
23import { Icon, IconName } from "../icon/icon";
24
25// eslint-disable-next-line @typescript-eslint/ban-types, deprecation/deprecation
26export type TreeNodeInfo<T = {}> = ITreeNode<T>;
27/** @deprecated use TreeNodeInfo */
28// eslint-disable-next-line @typescript-eslint/ban-types
29export interface ITreeNode<T = {}> extends Props {
30 /**
31 * Child tree nodes of this node.
32 */
33 childNodes?: Array<TreeNodeInfo<T>>;
34
35 /**
36 * Whether this tree node is non-interactive. Enabling this prop will ignore
37 * mouse event handlers (in particular click, down, enter, leave).
38 */
39 disabled?: boolean;
40
41 /**
42 * Whether the caret to expand/collapse a node should be shown.
43 * If not specified, this will be true if the node has children and false otherwise.
44 */
45 hasCaret?: boolean;
46
47 /**
48 * The name of a Blueprint icon (or an icon element) to render next to the node's label.
49 */
50 icon?: IconName | MaybeElement;
51
52 /**
53 * A unique identifier for the node.
54 */
55 id: string | number;
56
57 /**
58 */
59 isExpanded?: boolean;
60
61 /**
62 * Whether this node is selected.
63 *
64 * @default false
65 */
66 isSelected?: boolean;
67
68 /**
69 * The main label for the node.
70 */
71 label: string | JSX.Element;
72
73 /**
74 * A secondary label/component that is displayed at the right side of the node.
75 */
76 secondaryLabel?: string | MaybeElement;
77
78 /**
79 * An optional custom user object to associate with the node.
80 * This property can then be used in the `onClick`, `onContextMenu` and `onDoubleClick`
81 * event handlers for doing custom logic per node.
82 */
83 nodeData?: T;
84}
85
86// eslint-disable-next-line @typescript-eslint/ban-types
87export type TreeNodeProps<T = {}> = ITreeNodeProps<T>;
88// eslint-disable-next-line @typescript-eslint/ban-types
89export interface ITreeNodeProps<T = {}> extends TreeNodeInfo<T> {
90 children?: React.ReactNode;
91 contentRef?: (node: TreeNode<T>, element: HTMLDivElement | null) => void;
92 depth: number;
93 key?: string | number;
94 onClick?: (node: TreeNode<T>, e: React.MouseEvent<HTMLDivElement>) => void;
95 onCollapse?: (node: TreeNode<T>, e: React.MouseEvent<HTMLSpanElement>) => void;
96 onContextMenu?: (node: TreeNode<T>, e: React.MouseEvent<HTMLDivElement>) => void;
97 onDoubleClick?: (node: TreeNode<T>, e: React.MouseEvent<HTMLDivElement>) => void;
98 onExpand?: (node: TreeNode<T>, e: React.MouseEvent<HTMLSpanElement>) => void;
99 onMouseEnter?: (node: TreeNode<T>, e: React.MouseEvent<HTMLDivElement>) => void;
100 onMouseLeave?: (node: TreeNode<T>, e: React.MouseEvent<HTMLDivElement>) => void;
101 path: number[];
102}
103
104// eslint-disable-next-line @typescript-eslint/ban-types
105export class TreeNode<T = {}> extends React.Component<ITreeNodeProps<T>> {
106 public static displayName = `${DISPLAYNAME_PREFIX}.TreeNode`;
107
108 public static ofType<U>() {
109 return TreeNode as new (props: ITreeNodeProps<U>) => TreeNode<U>;
110 }
111
112 public render() {
113 const { children, className, disabled, icon, isExpanded, isSelected, label } = this.props;
114 const classes = classNames(
115 Classes.TREE_NODE,
116 {
117 [Classes.DISABLED]: disabled,
118 [Classes.TREE_NODE_SELECTED]: isSelected,
119 [Classes.TREE_NODE_EXPANDED]: isExpanded,
120 },
121 className,
122 );
123
124 const contentClasses = classNames(
125 Classes.TREE_NODE_CONTENT,
126 `${Classes.TREE_NODE_CONTENT}-${this.props.depth}`,
127 );
128
129 const eventHandlers =
130 disabled === true
131 ? {}
132 : {
133 onClick: this.handleClick,
134 onContextMenu: this.handleContextMenu,
135 onDoubleClick: this.handleDoubleClick,
136 onMouseEnter: this.handleMouseEnter,
137 onMouseLeave: this.handleMouseLeave,
138 };
139
140 return (
141 <li className={classes}>
142 <div className={contentClasses} ref={this.handleContentRef} {...eventHandlers}>
143 {this.maybeRenderCaret()}
144 <Icon className={Classes.TREE_NODE_ICON} icon={icon} />
145 <span className={Classes.TREE_NODE_LABEL}>{label}</span>
146 {this.maybeRenderSecondaryLabel()}
147 </div>
148 <Collapse isOpen={isExpanded}>{children}</Collapse>
149 </li>
150 );
151 }
152
153 private maybeRenderCaret() {
154 const { children, isExpanded, disabled, hasCaret = React.Children.count(children) > 0 } = this.props;
155 if (hasCaret) {
156 const caretClasses = classNames(
157 Classes.TREE_NODE_CARET,
158 isExpanded ? Classes.TREE_NODE_CARET_OPEN : Classes.TREE_NODE_CARET_CLOSED,
159 );
160 const onClick = disabled === true ? undefined : this.handleCaretClick;
161 return (
162 <Icon
163 title={isExpanded ? "Collapse group" : "Expand group"}
164 className={caretClasses}
165 onClick={onClick}
166 icon={"chevron-right"}
167 />
168 );
169 }
170 return <span className={Classes.TREE_NODE_CARET_NONE} />;
171 }
172
173 private maybeRenderSecondaryLabel() {
174 if (this.props.secondaryLabel != null) {
175 return <span className={Classes.TREE_NODE_SECONDARY_LABEL}>{this.props.secondaryLabel}</span>;
176 } else {
177 return undefined;
178 }
179 }
180
181 private handleCaretClick = (e: React.MouseEvent<HTMLElement>) => {
182 e.stopPropagation();
183 const { isExpanded, onCollapse, onExpand } = this.props;
184 (isExpanded ? onCollapse : onExpand)?.(this, e);
185 };
186
187 private handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
188 this.props.onClick?.(this, e);
189 };
190
191 private handleContentRef = (element: HTMLDivElement | null) => {
192 this.props.contentRef?.(this, element);
193 };
194
195 private handleContextMenu = (e: React.MouseEvent<HTMLDivElement>) => {
196 this.props.onContextMenu?.(this, e);
197 };
198
199 private handleDoubleClick = (e: React.MouseEvent<HTMLDivElement>) => {
200 this.props.onDoubleClick?.(this, e);
201 };
202
203 private handleMouseEnter = (e: React.MouseEvent<HTMLDivElement>) => {
204 this.props.onMouseEnter?.(this, e);
205 };
206
207 private handleMouseLeave = (e: React.MouseEvent<HTMLDivElement>) => {
208 this.props.onMouseLeave?.(this, e);
209 };
210}