Skip to content

Commit a545f05

Browse files
feat(DualListSelector): enabled opt-in animations (#11837)
1 parent e62933d commit a545f05

File tree

9 files changed

+139
-16
lines changed

9 files changed

+139
-16
lines changed

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

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,27 +17,41 @@ export interface DualListSelectorProps {
1717
isTree?: boolean;
1818
/** Content to be rendered in the dual list selector. */
1919
children?: React.ReactNode;
20+
/** Flag indicating whether a tree dual list selector has animations. This will always render
21+
* nested dual list selector items rather than dynamically rendering them. This prop will be removed in
22+
* the next breaking change release in favor of defaulting to always-rendered items.
23+
*/
24+
hasAnimations?: boolean;
2025
}
2126

2227
class DualListSelector extends Component<DualListSelectorProps> {
2328
static displayName = 'DualListSelector';
2429
static defaultProps: PickOptional<DualListSelectorProps> = {
2530
children: '',
26-
isTree: false
31+
isTree: false,
32+
hasAnimations: false
2733
};
2834

2935
constructor(props: DualListSelectorProps) {
3036
super(props);
3137
}
3238

3339
render() {
34-
const { className, children, id, isTree, ...props } = this.props;
40+
const { className, children, id, isTree, hasAnimations, ...props } = this.props;
3541

3642
return (
37-
<DualListSelectorContext.Provider value={{ isTree }}>
43+
<DualListSelectorContext.Provider value={{ isTree, hasAnimations }}>
3844
<GenerateId>
3945
{(randomId) => (
40-
<div className={css(styles.dualListSelector, className)} id={id || randomId} {...props}>
46+
<div
47+
className={css(
48+
styles.dualListSelector,
49+
hasAnimations && isTree && styles.modifiers.animateExpand,
50+
className
51+
)}
52+
id={id || randomId}
53+
{...props}
54+
>
4155
{children}
4256
</div>
4357
)}

packages/react-core/src/components/DualListSelector/DualListSelectorContext.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { createContext } from 'react';
22
export const DualListSelectorContext = createContext<{
33
isTree?: boolean;
4-
}>({ isTree: false });
4+
hasAnimations?: boolean;
5+
}>({ isTree: false, hasAnimations: false });
56

67
export const DualListSelectorListContext = createContext<{
78
setFocusedOption?: (id: string) => void;

packages/react-core/src/components/DualListSelector/DualListSelectorListWrapper.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,17 @@ export const DualListSelectorListWrapperBase: React.FunctionComponent<DualListSe
5656
}
5757
event.stopImmediatePropagation();
5858
const validOptions = isTree
59-
? (Array.from(
60-
menuRef.current.querySelectorAll(
61-
`.${styles.dualListSelectorItemToggle}, .${styles.dualListSelectorItemCheck} > input`
62-
)
63-
) as Element[])
59+
? (
60+
Array.from(
61+
menuRef.current.querySelectorAll(
62+
`.${styles.dualListSelectorItemToggle}, .${styles.dualListSelectorItemCheck} > input`
63+
)
64+
) as Element[]
65+
).filter((item) => !item.closest(`.${styles.dualListSelectorList}[inert]`))
6466
: (Array.from(menuRef.current.getElementsByTagName('LI')) as Element[]).filter(
6567
(el) => !el.classList.contains('pf-m-disabled')
6668
);
69+
6770
const activeElement = document.activeElement;
6871
handleArrows(
6972
event,

packages/react-core/src/components/DualListSelector/DualListSelectorTree.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import { useContext } from 'react';
12
import { css } from '@patternfly/react-styles';
23
import styles from '@patternfly/react-styles/css/components/DualListSelector/dual-list-selector';
4+
import { DualListSelectorContext } from './DualListSelectorContext';
35
import { DualListSelectorTreeItem } from './DualListSelectorTreeItem';
46

57
export interface DualListSelectorTreeItemData {
@@ -68,11 +70,13 @@ export const DualListSelectorTree: React.FunctionComponent<DualListSelectorTreeP
6870
isDisabled = false,
6971
...props
7072
}: DualListSelectorTreeProps) => {
73+
const { hasAnimations } = useContext(DualListSelectorContext);
7174
const dataToRender = typeof data === 'function' ? data() : data;
7275
const tree = dataToRender.map((item) => (
7376
<DualListSelectorTreeItem
7477
key={item.id}
7578
text={item.text}
79+
hasAnimations={hasAnimations}
7680
id={item.id}
7781
defaultExpanded={item.defaultExpanded !== undefined ? item.defaultExpanded : defaultAllExpanded}
7882
onOptionCheck={onOptionCheck}

packages/react-core/src/components/DualListSelector/DualListSelectorTreeItem.tsx

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { memo, useContext, useEffect, useRef, useState } from 'react';
1+
import { memo, useContext, useEffect, useRef, useState, cloneElement, Children, isValidElement } from 'react';
22
import styles from '@patternfly/react-styles/css/components/DualListSelector/dual-list-selector';
33
import { css } from '@patternfly/react-styles';
44
import { DualListSelectorTreeItemData } from './DualListSelectorTree';
@@ -38,6 +38,11 @@ export interface DualListSelectorTreeItemProps extends React.HTMLProps<HTMLLIEle
3838
isDisabled?: boolean;
3939
/** Flag indicating the DualListSelector tree should utilize memoization to help render large data sets. */
4040
useMemo?: boolean;
41+
/** Flag indicating whether a tree dual list selector has animations. This will always render
42+
* nested dual list selector items rather than dynamically rendering them. This prop will be removed in
43+
* the next breaking change release in favor of defaulting to always-rendered items.
44+
*/
45+
hasAnimations?: boolean;
4146
}
4247

4348
const DualListSelectorTreeItemBase: React.FunctionComponent<DualListSelectorTreeItemProps> = ({
@@ -53,6 +58,7 @@ const DualListSelectorTreeItemBase: React.FunctionComponent<DualListSelectorTree
5358
badgeProps,
5459
itemData,
5560
isDisabled = false,
61+
hasAnimations,
5662
// eslint-disable-next-line @typescript-eslint/no-unused-vars
5763
useMemo,
5864
...props
@@ -65,6 +71,15 @@ const DualListSelectorTreeItemBase: React.FunctionComponent<DualListSelectorTree
6571
setIsExpanded(defaultExpanded);
6672
}, [defaultExpanded]);
6773

74+
const clonedChildren = Children.map(
75+
children,
76+
(child) =>
77+
isValidElement(child) &&
78+
cloneElement(child as React.ReactElement<any>, {
79+
inert: isExpanded ? undefined : ''
80+
})
81+
);
82+
6883
return (
6984
<li
7085
className={css(
@@ -156,7 +171,7 @@ const DualListSelectorTreeItemBase: React.FunctionComponent<DualListSelectorTree
156171
</span>
157172
</div>
158173
</div>
159-
{isExpanded && children}
174+
{(isExpanded || hasAnimations) && clonedChildren}
160175
</li>
161176
);
162177
};

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

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,37 @@
1-
import { render } from '@testing-library/react';
1+
import { render, screen } from '@testing-library/react';
2+
import styles from '@patternfly/react-styles/css/components/DualListSelector/dual-list-selector';
3+
import { DualListSelector } from '../DualListSelector';
24
import { DualListSelectorPane } from '../DualListSelectorPane';
35
import { SearchInput } from '../../SearchInput';
6+
7+
// The following tests can be removed as part of https://github.com/patternfly/patternfly-react/issues/11838
8+
describe('Opt-in animations', () => {
9+
test(`Does not render with class ${styles.modifiers.animateExpand} by default`, () => {
10+
render(<DualListSelector data-testid="test-id" />);
11+
12+
expect(screen.getByTestId('test-id')).not.toHaveClass(styles.modifiers.animateExpand);
13+
});
14+
15+
test(`Does not render with class ${styles.modifiers.animateExpand} when hasAnimations is true and isTree is false`, () => {
16+
render(<DualListSelector hasAnimations data-testid="test-id" />);
17+
18+
expect(screen.getByTestId('test-id')).not.toHaveClass(styles.modifiers.animateExpand);
19+
});
20+
21+
test(`Does not render with class ${styles.modifiers.animateExpand} by default when isTree is true`, () => {
22+
render(<DualListSelector isTree data-testid="test-id" />);
23+
24+
expect(screen.getByTestId('test-id')).not.toHaveClass(styles.modifiers.animateExpand);
25+
});
26+
27+
test(`Renders with class ${styles.modifiers.animateExpand} when both isTree and hasAnimations are true`, () => {
28+
render(<DualListSelector isTree hasAnimations data-testid="test-id" />);
29+
30+
expect(screen.getByTestId('test-id')).toHaveClass(styles.modifiers.animateExpand);
31+
});
32+
});
33+
34+
// Following tests should be moved to a separate DualListSelectorPane test file
435
describe('DualListSelector', () => {
536
test('basic', () => {
637
const { asFragment } = render(<DualListSelectorPane id="basicTest" />);
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { render, screen } from '@testing-library/react';
2+
import styles from '@patternfly/react-styles/css/components/DualListSelector/dual-list-selector';
3+
import { DualListSelectorTreeItem } from '../DualListSelectorTreeItem';
4+
5+
// The following tests checking for children to not be/to be rendered will need to be refactored
6+
// as part of https://github.com/patternfly/patternfly-react/issues/11838
7+
test('Does not render children by default', () => {
8+
render(
9+
<DualListSelectorTreeItem id="item-id" text="Test text">
10+
<div>Children content</div>
11+
</DualListSelectorTreeItem>
12+
);
13+
14+
expect(screen.queryByText('Children content')).not.toBeInTheDocument();
15+
});
16+
17+
test('Renders children when defaultExpanded is true', () => {
18+
render(
19+
<DualListSelectorTreeItem defaultExpanded id="item-id" text="Test text">
20+
<div>Children content</div>
21+
</DualListSelectorTreeItem>
22+
);
23+
24+
expect(screen.getByText('Children content')).toBeVisible();
25+
});
26+
27+
test('Renders children when hasAnimations is true', () => {
28+
render(
29+
<DualListSelectorTreeItem hasAnimations id="item-id" text="Test text">
30+
<div>Children content</div>
31+
</DualListSelectorTreeItem>
32+
);
33+
34+
expect(screen.getByText('Children content')).toBeVisible();
35+
});
36+
37+
test('Renders children with inert attribute by default when hasAnimations is true', () => {
38+
render(
39+
<DualListSelectorTreeItem hasAnimations id="item-id" text="Test text">
40+
<div>Children content</div>
41+
</DualListSelectorTreeItem>
42+
);
43+
44+
expect(screen.getByText('Children content')).toHaveAttribute('inert', '');
45+
});
46+
47+
test('Does not render children with inert attribute when hasAnimations and defaultExpanded are true', () => {
48+
render(
49+
<DualListSelectorTreeItem hasAnimations defaultExpanded id="item-id" text="Test text">
50+
<div>Children content</div>
51+
</DualListSelectorTreeItem>
52+
);
53+
54+
expect(screen.getByText('Children content')).not.toHaveAttribute('inert');
55+
});

packages/react-core/src/components/DualListSelector/examples/DualListSelectorTree.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ export const DualListSelectorComposableTree: React.FunctionComponent<ExampleProp
283283
};
284284

285285
return (
286-
<DualListSelector isTree>
286+
<DualListSelector hasAnimations isTree>
287287
{buildPane(false)}
288288
<DualListSelectorControlsWrapper>
289289
<DualListSelectorControl

packages/react-core/src/helpers/KeyboardHandler.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,12 +116,12 @@ export const handleArrows = (
116116
: nextSiblingMainElement.nextElementSibling;
117117

118118
if (nextSibling) {
119-
if (validSiblingTags.includes(nextSibling.tagName)) {
119+
if (validSiblingTags.includes(nextSibling?.tagName)) {
120120
moveTarget = nextSibling;
121121
break;
122122
}
123123
// For cases where the validSiblingTag is inside a div wrapper
124-
if (validSiblingTags.includes(nextSibling.children[0].tagName)) {
124+
if (validSiblingTags.includes(nextSibling.children[0]?.tagName)) {
125125
moveTarget = nextSibling.children[0];
126126
break;
127127
}

0 commit comments

Comments
 (0)