Skip to content

Commit 1840aac

Browse files
authored
feat(TreeView): add treeview, demos, tests (#4701)
* feat(TreeView): add treeview, demos, tests * feat(Treeview): add check properties to checkbox example * feat(TreeView): pass check/badge props to items * feat(TreeView): update checkbox example behavior, update md title, update snaps * feat(TreeView): fix lint * feat(TreeView): allow item level properties to override tree level properties * feat(TreeView): prevent checkbox click from triggering item click, update snap
1 parent ea4d7ae commit 1840aac

File tree

13 files changed

+8894
-0
lines changed

13 files changed

+8894
-0
lines changed
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import React from 'react';
2+
import { TreeViewList } from './TreeViewList';
3+
import { TreeViewListItem } from './TreeViewListItem';
4+
5+
export interface TreeViewDataItem {
6+
/** Internal content of a tree view item */
7+
name: React.ReactNode;
8+
/** ID of a tree view item */
9+
id?: string;
10+
/** Child nodes of a tree view item */
11+
children?: TreeViewDataItem[];
12+
/** Flag indicating if node is expanded by default */
13+
defaultExpanded?: boolean;
14+
/** Default icon of a tree view item */
15+
icon?: React.ReactNode;
16+
/** Expanded icon of a tree view item */
17+
expandedIcon?: React.ReactNode;
18+
/** Flag indicating if a tree view item has a checkbox */
19+
hasCheck?: boolean;
20+
/** Additional properties of the tree view item checkbox */
21+
checkProps?: any;
22+
/** Flag indicating if a tree view item has a badge */
23+
hasBadge?: boolean;
24+
/** Additional properties of the tree view item badge */
25+
badgeProps?: any;
26+
/** Action of a tree view item, nested inside a button */
27+
action?: React.ReactNode;
28+
/** Additional properties of the tree view item action button */
29+
actionProps?: any;
30+
}
31+
32+
export interface TreeViewProps {
33+
/** Data of the tree view */
34+
data: TreeViewDataItem[];
35+
/** ID of the tree view */
36+
id?: string;
37+
/** Flag indicating if the tree view is nested */
38+
isNested?: boolean;
39+
/** Flag indicating if all nodes in the tree view should have checkboxes */
40+
hasChecks?: boolean;
41+
/** Flag indicating if all nodes in the tree view should have badges */
42+
hasBadges?: boolean;
43+
/** Icon for all leaf or unexpanded node items */
44+
icon?: React.ReactNode;
45+
/** Icon for all expanded node items */
46+
expandedIcon?: React.ReactNode;
47+
/** Sets the default expanded behavior */
48+
defaultAllExpanded?: boolean;
49+
/** Callback for item selection */
50+
onSelect?: (event: React.MouseEvent, item: TreeViewDataItem, parentItem: TreeViewDataItem) => void;
51+
/** Callback for item checkbox selection */
52+
onCheck?: (event: React.ChangeEvent, item: TreeViewDataItem, parentItem: TreeViewDataItem) => void;
53+
/** Callback for search input */
54+
onSearch?: (event: React.ChangeEvent<HTMLInputElement>) => void;
55+
/** Additional props for search input */
56+
searchProps?: any;
57+
/** Active items of tree view */
58+
activeItems?: TreeViewDataItem[];
59+
/** Internal. Parent item of a TreeViewListItem */
60+
parentItem?: TreeViewDataItem;
61+
/** Comparison function for determining active items */
62+
compareItems?: (item: TreeViewDataItem, itemToCheck: TreeViewDataItem) => boolean;
63+
}
64+
65+
export const TreeView: React.FunctionComponent<TreeViewProps> = ({
66+
data,
67+
isNested = false,
68+
hasChecks = false,
69+
hasBadges = false,
70+
defaultAllExpanded = false,
71+
icon,
72+
expandedIcon,
73+
parentItem,
74+
onSelect,
75+
onCheck,
76+
onSearch,
77+
searchProps,
78+
activeItems,
79+
compareItems = (item, itemToCheck) => item.id === itemToCheck.id,
80+
...props
81+
}: TreeViewProps) => (
82+
<TreeViewList isNested={isNested} onSearch={onSearch} searchProps={searchProps} {...props}>
83+
{data.map(item => (
84+
<TreeViewListItem
85+
key={item.name.toString()}
86+
name={item.name}
87+
id={item.id}
88+
defaultExpanded={item.defaultExpanded !== undefined ? item.defaultExpanded : defaultAllExpanded}
89+
onSelect={onSelect}
90+
onCheck={onCheck}
91+
hasCheck={item.hasCheck !== undefined ? item.hasCheck : hasChecks}
92+
checkProps={item.checkProps}
93+
hasBadge={item.hasBadge !== undefined ? item.hasBadge : hasBadges}
94+
badgeProps={item.checkProps}
95+
activeItems={activeItems}
96+
parentItem={parentItem}
97+
itemData={item}
98+
icon={item.icon !== undefined ? item.icon : icon}
99+
expandedIcon={item.expandedIcon !== undefined ? item.expandedIcon : expandedIcon}
100+
action={item.action}
101+
actionProps={item.actionProps}
102+
compareItems={compareItems}
103+
{...(item.children && {
104+
children: (
105+
<TreeView
106+
data={item.children}
107+
isNested
108+
parentItem={item}
109+
hasChecks={hasChecks}
110+
hasBadges={hasBadges}
111+
defaultAllExpanded={defaultAllExpanded}
112+
onSelect={onSelect}
113+
onCheck={onCheck}
114+
activeItems={activeItems}
115+
icon={icon}
116+
expandedIcon={expandedIcon}
117+
/>
118+
)
119+
})}
120+
/>
121+
))}
122+
</TreeViewList>
123+
);
124+
125+
TreeView.displayName = 'TreeView';
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import React from 'react';
2+
import { css } from '@patternfly/react-styles';
3+
import styles from '@patternfly/react-styles/css/components/TreeView/tree-view';
4+
import { TreeViewSearch } from './TreeViewSearch';
5+
import { Divider } from '../Divider';
6+
7+
export interface TreeViewListProps {
8+
/** Flag indicating if the tree view is nested under another tree view */
9+
isNested?: boolean;
10+
/** Callback for search input */
11+
onSearch?: (event: React.ChangeEvent<HTMLInputElement>) => void;
12+
/** Additional props for search input */
13+
searchProps?: any;
14+
/** Child nodes of the current tree view */
15+
children: React.ReactNode;
16+
}
17+
18+
export const TreeViewList: React.FunctionComponent<TreeViewListProps> = ({
19+
isNested = false,
20+
onSearch,
21+
searchProps,
22+
children,
23+
...props
24+
}: TreeViewListProps) => {
25+
const list = <ul role={isNested ? 'group' : 'tree'}>{children}</ul>;
26+
return isNested ? (
27+
list
28+
) : (
29+
<div className={css(styles.treeView)} {...props}>
30+
{onSearch && (
31+
<React.Fragment>
32+
<TreeViewSearch onChange={onSearch} {...searchProps} />
33+
<Divider />
34+
</React.Fragment>
35+
)}
36+
{list}
37+
</div>
38+
);
39+
};
40+
TreeViewList.displayName = 'TreeViewList';
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import React, { useState } from 'react';
2+
import { css } from '@patternfly/react-styles';
3+
import styles from '@patternfly/react-styles/css/components/TreeView/tree-view';
4+
import AngleRightIcon from '@patternfly/react-icons/dist/js/icons/angle-right-icon';
5+
import AngleDownIcon from '@patternfly/react-icons/dist/js/icons/angle-down-icon';
6+
import { TreeViewDataItem } from './TreeView';
7+
import { Badge } from '../Badge';
8+
9+
export interface TreeViewListItemProps {
10+
/** Internal content of a tree view item */
11+
name: React.ReactNode;
12+
/** ID of a tree view item */
13+
id?: string;
14+
/** Flag indicating if node is expanded by default */
15+
defaultExpanded?: boolean;
16+
/** Child nodes of a tree view item */
17+
children?: React.ReactNode;
18+
/** Callback for item selection */
19+
onSelect?: (event: React.MouseEvent, item: TreeViewDataItem, parent: TreeViewDataItem) => void;
20+
/** Callback for item checkbox selection */
21+
onCheck?: (event: React.ChangeEvent, item: TreeViewDataItem, parent: TreeViewDataItem) => void;
22+
/** Flag indicating if a tree view item has a checkbox */
23+
hasCheck?: boolean;
24+
/** Additional properties of the tree view item checkbox */
25+
checkProps?: any;
26+
/** Flag indicating if a tree view item has a badge */
27+
hasBadge?: boolean;
28+
/** Additional properties of the tree view item badge */
29+
badgeProps?: any;
30+
/** Active items of tree view */
31+
activeItems?: TreeViewDataItem[];
32+
/** Data structure of tree view item */
33+
itemData?: TreeViewDataItem;
34+
/** Parent item of tree view item */
35+
parentItem?: TreeViewDataItem;
36+
/** Default icon of a tree view item */
37+
icon?: React.ReactNode;
38+
/** Expanded icon of a tree view item */
39+
expandedIcon?: React.ReactNode;
40+
/** Action of a tree view item, nested inside a button */
41+
action?: React.ReactNode;
42+
/** Additional properties of the tree view item action button */
43+
actionProps?: any;
44+
/** Callback for item comparison function */
45+
compareItems?: (item: TreeViewDataItem, itemToCheck: TreeViewDataItem) => boolean;
46+
}
47+
48+
export const TreeViewListItem: React.FunctionComponent<TreeViewListItemProps> = ({
49+
name,
50+
id,
51+
defaultExpanded = false,
52+
children = null,
53+
onSelect,
54+
onCheck,
55+
hasCheck = false,
56+
checkProps = {
57+
checked: false
58+
},
59+
hasBadge = false,
60+
badgeProps = { isRead: true },
61+
activeItems = [],
62+
itemData,
63+
parentItem,
64+
icon,
65+
expandedIcon,
66+
action,
67+
actionProps = {
68+
onClick: (evt: React.MouseEvent) => {
69+
evt.stopPropagation();
70+
evt.preventDefault();
71+
onSelect && onSelect(evt, itemData, parentItem);
72+
}
73+
},
74+
compareItems
75+
}: TreeViewListItemProps) => {
76+
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
77+
return (
78+
<li
79+
id={id}
80+
className={css(
81+
styles.treeViewListItem,
82+
!!children && styles.modifiers.expandable,
83+
isExpanded && styles.modifiers.expanded
84+
)}
85+
{...(isExpanded && { 'aria-expanded': 'true' })}
86+
role="treeitem"
87+
tabIndex={0}
88+
>
89+
<div className={css(styles.treeViewContent)}>
90+
<button
91+
className={css(
92+
styles.treeViewNode,
93+
activeItems &&
94+
activeItems.length > 0 &&
95+
activeItems.some(item => compareItems && item && compareItems(item, itemData))
96+
? children
97+
? styles.modifiers.active
98+
: styles.modifiers.current
99+
: ''
100+
)}
101+
onClick={(evt: React.MouseEvent) => {
102+
if (children) {
103+
setIsExpanded(!isExpanded);
104+
}
105+
onSelect && onSelect(evt, itemData, parentItem);
106+
}}
107+
>
108+
{children && (
109+
<span className={css(styles.treeViewNodeToggleIcon)}>
110+
{!isExpanded && <AngleRightIcon aria-hidden="true" />}
111+
{isExpanded && <AngleDownIcon aria-hidden="true" />}
112+
</span>
113+
)}
114+
{hasCheck && (
115+
<span className={css(styles.treeViewNodeCheck)}>
116+
<input
117+
type="checkbox"
118+
onChange={(evt: React.ChangeEvent) => onCheck && onCheck(evt, itemData, parentItem)}
119+
onClick={(evt: React.MouseEvent) => evt.stopPropagation()}
120+
{...checkProps}
121+
/>
122+
</span>
123+
)}
124+
{icon && (
125+
<span className={css(styles.treeViewNodeIcon)}>
126+
{!isExpanded && icon}
127+
{isExpanded && (expandedIcon || icon)}
128+
</span>
129+
)}
130+
<span className={css(styles.treeViewNodeText)}>{name}</span>
131+
{hasBadge && children && (
132+
<span className={css(styles.treeViewNodeCount)}>
133+
<Badge {...badgeProps}>{(children as React.ReactElement).props.data.length}</Badge>
134+
</span>
135+
)}
136+
</button>
137+
{action && (
138+
<button className={css(styles.treeViewAction)} {...actionProps}>
139+
{action}
140+
</button>
141+
)}
142+
</div>
143+
{isExpanded && children}
144+
</li>
145+
);
146+
};
147+
TreeViewListItem.displayName = 'TreeViewListItem';
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import React from 'react';
2+
import { css } from '@patternfly/react-styles';
3+
import styles from '@patternfly/react-styles/css/components/TreeView/tree-view';
4+
import formStyles from '@patternfly/react-styles/css/components/FormControl/form-control';
5+
6+
export interface TreeViewSearchProps {}
7+
8+
export const TreeViewSearch: React.FunctionComponent<TreeViewSearchProps> = ({ ...props }: TreeViewSearchProps) => (
9+
<div className={css(styles.treeViewSearch)}>
10+
<input className={css(formStyles.formControl, formStyles.modifiers.search)} type="search" {...props} />
11+
</div>
12+
);
13+
TreeViewSearch.displayName = 'TreeViewSearch';

0 commit comments

Comments
 (0)