Skip to content

Commit bfc382c

Browse files
feat(TreeView): enabled opt-in animations (#11842)
* feat(TreeView): enabled opt-in animations * Fixed failing tests
1 parent c40f2f8 commit bfc382c

21 files changed

+116
-12
lines changed

packages/react-core/src/components/TreeView/TreeView.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,11 @@ export interface TreeViewProps {
103103
useMemo?: boolean;
104104
/** Variant presentation styles for the tree view. */
105105
variant?: 'default' | 'compact' | 'compactNoBackground';
106+
/** Flag indicating whether a tree view has animations. This will always render
107+
* nested tree view items rather than dynamically rendering them. This prop will be removed in
108+
* the next breaking change release in favor of defaulting to always-rendered items.
109+
*/
110+
hasAnimations?: boolean;
106111
}
107112

108113
export const TreeView: React.FunctionComponent<TreeViewProps> = ({
@@ -130,6 +135,7 @@ export const TreeView: React.FunctionComponent<TreeViewProps> = ({
130135
useMemo,
131136
'aria-label': ariaLabel,
132137
'aria-labelledby': ariaLabelledby,
138+
hasAnimations,
133139
...props
134140
}: TreeViewProps) => {
135141
const treeViewList = (
@@ -139,11 +145,13 @@ export const TreeView: React.FunctionComponent<TreeViewProps> = ({
139145
isMultiSelectable={isMultiSelectable}
140146
aria-label={ariaLabel}
141147
aria-labelledby={ariaLabelledby}
148+
{...props}
142149
>
143150
{data.map((item) => (
144151
<TreeViewListItem
145152
key={item.id?.toString() || item.name?.toString()}
146153
name={item.name}
154+
hasAnimations={hasAnimations}
147155
title={item.title}
148156
id={item.id}
149157
isExpanded={allExpanded}
@@ -172,6 +180,7 @@ export const TreeView: React.FunctionComponent<TreeViewProps> = ({
172180
<TreeView
173181
data={item.children}
174182
isNested
183+
hasAnimations={hasAnimations}
175184
parentItem={item}
176185
hasCheckboxes={hasCheckboxes}
177186
hasBadges={hasBadges}

packages/react-core/src/components/TreeView/TreeViewListItem.tsx

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { memo, useState, useEffect } from 'react';
1+
import { memo, useState, useEffect, Children, isValidElement, cloneElement } from 'react';
22
import { css } from '@patternfly/react-styles';
33
import styles from '@patternfly/react-styles/css/components/TreeView/tree-view';
44
import AngleRightIcon from '@patternfly/react-icons/dist/esm/icons/angle-right-icon';
@@ -68,6 +68,11 @@ export interface TreeViewListItemProps {
6868
* every node in the selected item's path.
6969
*/
7070
useMemo?: boolean;
71+
/** Flag indicating whether a tree view has animations. This will always render
72+
* nested tree view items rather than dynamically rendering them. This prop will be removed in
73+
* the next breaking change release in favor of defaulting to always-rendered items.
74+
*/
75+
hasAnimations?: boolean;
7176
}
7277

7378
const TreeViewListItemBase: React.FunctionComponent<TreeViewListItemProps> = ({
@@ -97,6 +102,7 @@ const TreeViewListItemBase: React.FunctionComponent<TreeViewListItemProps> = ({
97102
expandedIcon,
98103
action,
99104
compareItems,
105+
hasAnimations,
100106
// eslint-disable-next-line @typescript-eslint/no-unused-vars
101107
useMemo
102108
}: TreeViewListItemProps) => {
@@ -203,6 +209,15 @@ const TreeViewListItemBase: React.FunctionComponent<TreeViewListItemProps> = ({
203209
activeItems.length > 0 &&
204210
activeItems.some((item) => compareItems && item && compareItems(item, itemData));
205211

212+
const clonedChildren = Children.map(
213+
children,
214+
(child) =>
215+
isValidElement(child) &&
216+
cloneElement(child as React.ReactElement<any>, {
217+
inert: internalIsExpanded ? undefined : ''
218+
})
219+
);
220+
206221
return (
207222
<li
208223
id={id}
@@ -247,7 +262,7 @@ const TreeViewListItemBase: React.FunctionComponent<TreeViewListItemProps> = ({
247262
</GenerateId>
248263
{action && <div className={css(styles.treeViewAction)}>{action}</div>}
249264
</div>
250-
{internalIsExpanded && children}
265+
{(internalIsExpanded || hasAnimations) && clonedChildren}
251266
</li>
252267
);
253268
};

packages/react-core/src/components/TreeView/TreeViewRoot.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ class TreeViewRoot extends Component<TreeViewRootProps> {
7272
const activeElement = document.activeElement;
7373
const key = event.key;
7474
const treeItems = Array.from(this.treeRef.current?.getElementsByClassName(styles.treeViewNode)).filter(
75-
(el) => !el.classList.contains('pf-m-disabled')
75+
(el) => !el.classList.contains('pf-m-disabled') && !el.closest(`.${styles.treeViewList}[inert]`)
7676
);
7777

7878
if (key === KeyTypes.Space) {
@@ -138,7 +138,9 @@ class TreeViewRoot extends Component<TreeViewRootProps> {
138138
event.preventDefault();
139139
}
140140

141-
const treeNodes = Array.from(this.treeRef.current?.getElementsByClassName(styles.treeViewNode));
141+
const treeNodes = Array.from(this.treeRef.current?.getElementsByClassName(styles.treeViewNode)).filter(
142+
(el) => !el.closest(`.${styles.treeViewList}[inert]`)
143+
);
142144

143145
handleArrows(
144146
event,

packages/react-core/src/components/TreeView/__tests__/TreeView.test.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ jest.mock('../TreeViewListItem', () => ({
4747
onCollapse,
4848
parentItem,
4949
title,
50-
useMemo
50+
useMemo,
51+
hasAnimations
5152
}) => (
5253
<div data-testid="TreeViewListItem-mock">
5354
<p>{`TreeViewListItem action: ${action}`}</p>
@@ -69,6 +70,7 @@ jest.mock('../TreeViewListItem', () => ({
6970
<p>{`TreeViewListItem parentItem: ${parentItem?.name}`}</p>
7071
<p>{`TreeViewListItem title: ${title}`}</p>
7172
<p>{`TreeViewListItem useMemo: ${useMemo}`}</p>
73+
<p>{`TreeViewListItem hasAnimations: ${hasAnimations}`}</p>
7274
<button onClick={compareItems}>compareItems clicker</button>
7375
<button onClick={onCheck}>onCheck clicker</button>
7476
<button onClick={onSelect}>onSelect clicker</button>
@@ -286,6 +288,11 @@ test('Passes useMemo to TreeViewListItem', () => {
286288

287289
expect(screen.getByText('TreeViewListItem useMemo: true')).toBeVisible();
288290
});
291+
test('Passes hasAnimations to TreeViewListItem', () => {
292+
render(<TreeView data={[basicData]} hasAnimations={true} />);
293+
294+
expect(screen.getByText('TreeViewListItem hasAnimations: true')).toBeVisible();
295+
});
289296
test('Passes data.children to TreeViewListItem', () => {
290297
render(<TreeView data={[{ ...basicData, children: [{ name: 'Child 1' }] }]} />);
291298

packages/react-core/src/components/TreeView/__tests__/TreeViewList.test.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { render, screen } from '@testing-library/react';
22
import { TreeViewList } from '../TreeViewList';
3+
import { TreeViewListItem } from '../TreeViewListItem';
4+
import { TreeView } from '../TreeView';
35
import styles from '@patternfly/react-styles/css/components/TreeView/tree-view';
46

57
test(`Renders with class ${styles.treeView}__list by default`, () => {
@@ -84,6 +86,43 @@ test(`Does not render toolbar content when toolbar prop is not passed`, () => {
8486
expect(screen.queryByRole('separator')).not.toBeInTheDocument();
8587
});
8688

89+
test('Renders with inert attribute by default when TreeView is passed hasAnimations', () => {
90+
const options = [
91+
{
92+
name: 'Parent 1',
93+
id: 'parent-1',
94+
children: [
95+
{
96+
name: 'Child 1',
97+
id: 'child-1'
98+
}
99+
]
100+
}
101+
];
102+
render(<TreeView hasAnimations data={options} />);
103+
104+
expect(screen.getByRole('group')).toHaveAttribute('inert', '');
105+
});
106+
107+
test('Does not render with inert attribute when expanded and TreeView is passed hasAnimations', () => {
108+
const options = [
109+
{
110+
name: 'Parent 1',
111+
id: 'parent-1',
112+
defaultExpanded: true,
113+
children: [
114+
{
115+
name: 'Child 1',
116+
id: 'child-1'
117+
}
118+
]
119+
}
120+
];
121+
render(<TreeView hasAnimations data={options} />);
122+
123+
expect(screen.getByRole('group')).not.toHaveAttribute('inert');
124+
});
125+
87126
test('Matches snapshot by default', () => {
88127
const { asFragment } = render(<TreeViewList>Content</TreeViewList>);
89128

packages/react-core/src/components/TreeView/__tests__/TreeViewListItem.test.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ test(`Does not render children by default`, () => {
2828
test(`Renders children if defaultExpanded is true`, () => {
2929
render(
3030
<TreeViewListItem defaultExpanded={true} {...requiredProps}>
31-
Content
31+
<div>Content</div>
3232
</TreeViewListItem>
3333
);
3434

@@ -38,7 +38,7 @@ test(`Renders children if defaultExpanded is true`, () => {
3838
test(`Renders children if isExpanded is true`, () => {
3939
render(
4040
<TreeViewListItem isExpanded={true} {...requiredProps}>
41-
Content
41+
<div>Content</div>
4242
</TreeViewListItem>
4343
);
4444

@@ -47,7 +47,11 @@ test(`Renders children if isExpanded is true`, () => {
4747

4848
test(`Renders children when toggle is clicked`, async () => {
4949
const user = userEvent.setup();
50-
render(<TreeViewListItem {...requiredProps}>Content</TreeViewListItem>);
50+
render(
51+
<TreeViewListItem {...requiredProps}>
52+
<div>Content</div>
53+
</TreeViewListItem>
54+
);
5155

5256
await user.click(screen.getByRole('button', { name: requiredProps.name }));
5357

packages/react-core/src/components/TreeView/__tests__/__snapshots__/TreeView.test.tsx.snap

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,9 @@ exports[`Matches snapshot 1`] = `
104104
<p>
105105
TreeViewListItem useMemo: undefined
106106
</p>
107+
<p>
108+
TreeViewListItem hasAnimations: undefined
109+
</p>
107110
<button>
108111
compareItems clicker
109112
</button>

packages/react-core/src/components/TreeView/examples/TreeViewCompact.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,5 +54,5 @@ export const TreeViewCompact: React.FunctionComponent = () => {
5454
]
5555
}
5656
];
57-
return <TreeView aria-label="Tree View compact example" data={options} variant="compact" />;
57+
return <TreeView hasAnimations aria-label="Tree View compact example" data={options} variant="compact" />;
5858
};

packages/react-core/src/components/TreeView/examples/TreeViewCompactNoBackground.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,5 +54,12 @@ export const TreeViewCompactNoBackground: React.FunctionComponent = () => {
5454
]
5555
}
5656
];
57-
return <TreeView aria-label="Tree View compact no background example" data={options} variant="compactNoBackground" />;
57+
return (
58+
<TreeView
59+
hasAnimations
60+
aria-label="Tree View compact no background example"
61+
data={options}
62+
variant="compactNoBackground"
63+
/>
64+
);
5865
};

packages/react-core/src/components/TreeView/examples/TreeViewGuides.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,5 +60,5 @@ export const GuidesTreeView: React.FunctionComponent = () => {
6060
children: [{ name: 'Application 5', id: 'example8-App5' }]
6161
}
6262
];
63-
return <TreeView aria-label="Tree View guides example" data={options} hasGuides={true} />;
63+
return <TreeView hasAnimations aria-label="Tree View guides example" data={options} hasGuides={true} />;
6464
};

0 commit comments

Comments
 (0)