diff --git a/packages/react-core/src/components/DualListSelector/DualListSelector.tsx b/packages/react-core/src/components/DualListSelector/DualListSelector.tsx index 0487e89e813..a8eed78a257 100644 --- a/packages/react-core/src/components/DualListSelector/DualListSelector.tsx +++ b/packages/react-core/src/components/DualListSelector/DualListSelector.tsx @@ -17,13 +17,19 @@ export interface DualListSelectorProps { isTree?: boolean; /** Content to be rendered in the dual list selector. */ children?: React.ReactNode; + /** Flag indicating whether a tree dual list selector has animations. This will always render + * nested dual list selector items rather than dynamically rendering them. This prop will be removed in + * the next breaking change release in favor of defaulting to always-rendered items. + */ + hasAnimations?: boolean; } class DualListSelector extends Component { static displayName = 'DualListSelector'; static defaultProps: PickOptional = { children: '', - isTree: false + isTree: false, + hasAnimations: false }; constructor(props: DualListSelectorProps) { @@ -31,13 +37,21 @@ class DualListSelector extends Component { } render() { - const { className, children, id, isTree, ...props } = this.props; + const { className, children, id, isTree, hasAnimations, ...props } = this.props; return ( - + {(randomId) => ( -
+
{children}
)} diff --git a/packages/react-core/src/components/DualListSelector/DualListSelectorContext.ts b/packages/react-core/src/components/DualListSelector/DualListSelectorContext.ts index ae05d7cdc01..9ace87d2817 100644 --- a/packages/react-core/src/components/DualListSelector/DualListSelectorContext.ts +++ b/packages/react-core/src/components/DualListSelector/DualListSelectorContext.ts @@ -1,7 +1,8 @@ import { createContext } from 'react'; export const DualListSelectorContext = createContext<{ isTree?: boolean; -}>({ isTree: false }); + hasAnimations?: boolean; +}>({ isTree: false, hasAnimations: false }); export const DualListSelectorListContext = createContext<{ setFocusedOption?: (id: string) => void; diff --git a/packages/react-core/src/components/DualListSelector/DualListSelectorListWrapper.tsx b/packages/react-core/src/components/DualListSelector/DualListSelectorListWrapper.tsx index be6e9f535dd..67cf4e1377f 100644 --- a/packages/react-core/src/components/DualListSelector/DualListSelectorListWrapper.tsx +++ b/packages/react-core/src/components/DualListSelector/DualListSelectorListWrapper.tsx @@ -56,14 +56,17 @@ export const DualListSelectorListWrapperBase: React.FunctionComponent input` - ) - ) as Element[]) + ? ( + Array.from( + menuRef.current.querySelectorAll( + `.${styles.dualListSelectorItemToggle}, .${styles.dualListSelectorItemCheck} > input` + ) + ) as Element[] + ).filter((item) => !item.closest(`.${styles.dualListSelectorList}[inert]`)) : (Array.from(menuRef.current.getElementsByTagName('LI')) as Element[]).filter( (el) => !el.classList.contains('pf-m-disabled') ); + const activeElement = document.activeElement; handleArrows( event, diff --git a/packages/react-core/src/components/DualListSelector/DualListSelectorTree.tsx b/packages/react-core/src/components/DualListSelector/DualListSelectorTree.tsx index 4555fb29b0b..e2a8bf57031 100644 --- a/packages/react-core/src/components/DualListSelector/DualListSelectorTree.tsx +++ b/packages/react-core/src/components/DualListSelector/DualListSelectorTree.tsx @@ -1,5 +1,7 @@ +import { useContext } from 'react'; import { css } from '@patternfly/react-styles'; import styles from '@patternfly/react-styles/css/components/DualListSelector/dual-list-selector'; +import { DualListSelectorContext } from './DualListSelectorContext'; import { DualListSelectorTreeItem } from './DualListSelectorTreeItem'; export interface DualListSelectorTreeItemData { @@ -68,11 +70,13 @@ export const DualListSelectorTree: React.FunctionComponent { + const { hasAnimations } = useContext(DualListSelectorContext); const dataToRender = typeof data === 'function' ? data() : data; const tree = dataToRender.map((item) => ( = ({ @@ -53,6 +58,7 @@ const DualListSelectorTreeItemBase: React.FunctionComponent + isValidElement(child) && + cloneElement(child as React.ReactElement, { + inert: isExpanded ? undefined : '' + }) + ); + return (
  • - {isExpanded && children} + {(isExpanded || hasAnimations) && clonedChildren} ); }; diff --git a/packages/react-core/src/components/DualListSelector/__tests__/DualListSelector.test.tsx b/packages/react-core/src/components/DualListSelector/__tests__/DualListSelector.test.tsx index 58ab92913eb..3d59432fac2 100644 --- a/packages/react-core/src/components/DualListSelector/__tests__/DualListSelector.test.tsx +++ b/packages/react-core/src/components/DualListSelector/__tests__/DualListSelector.test.tsx @@ -1,6 +1,37 @@ -import { render } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; +import styles from '@patternfly/react-styles/css/components/DualListSelector/dual-list-selector'; +import { DualListSelector } from '../DualListSelector'; import { DualListSelectorPane } from '../DualListSelectorPane'; import { SearchInput } from '../../SearchInput'; + +// The following tests can be removed as part of https://github.com/patternfly/patternfly-react/issues/11838 +describe('Opt-in animations', () => { + test(`Does not render with class ${styles.modifiers.animateExpand} by default`, () => { + render(); + + expect(screen.getByTestId('test-id')).not.toHaveClass(styles.modifiers.animateExpand); + }); + + test(`Does not render with class ${styles.modifiers.animateExpand} when hasAnimations is true and isTree is false`, () => { + render(); + + expect(screen.getByTestId('test-id')).not.toHaveClass(styles.modifiers.animateExpand); + }); + + test(`Does not render with class ${styles.modifiers.animateExpand} by default when isTree is true`, () => { + render(); + + expect(screen.getByTestId('test-id')).not.toHaveClass(styles.modifiers.animateExpand); + }); + + test(`Renders with class ${styles.modifiers.animateExpand} when both isTree and hasAnimations are true`, () => { + render(); + + expect(screen.getByTestId('test-id')).toHaveClass(styles.modifiers.animateExpand); + }); +}); + +// Following tests should be moved to a separate DualListSelectorPane test file describe('DualListSelector', () => { test('basic', () => { const { asFragment } = render(); diff --git a/packages/react-core/src/components/DualListSelector/__tests__/DualListSelectorTreeItem.test.tsx b/packages/react-core/src/components/DualListSelector/__tests__/DualListSelectorTreeItem.test.tsx new file mode 100644 index 00000000000..87ec2df53da --- /dev/null +++ b/packages/react-core/src/components/DualListSelector/__tests__/DualListSelectorTreeItem.test.tsx @@ -0,0 +1,55 @@ +import { render, screen } from '@testing-library/react'; +import styles from '@patternfly/react-styles/css/components/DualListSelector/dual-list-selector'; +import { DualListSelectorTreeItem } from '../DualListSelectorTreeItem'; + +// The following tests checking for children to not be/to be rendered will need to be refactored +// as part of https://github.com/patternfly/patternfly-react/issues/11838 +test('Does not render children by default', () => { + render( + +
    Children content
    +
    + ); + + expect(screen.queryByText('Children content')).not.toBeInTheDocument(); +}); + +test('Renders children when defaultExpanded is true', () => { + render( + +
    Children content
    +
    + ); + + expect(screen.getByText('Children content')).toBeVisible(); +}); + +test('Renders children when hasAnimations is true', () => { + render( + +
    Children content
    +
    + ); + + expect(screen.getByText('Children content')).toBeVisible(); +}); + +test('Renders children with inert attribute by default when hasAnimations is true', () => { + render( + +
    Children content
    +
    + ); + + expect(screen.getByText('Children content')).toHaveAttribute('inert', ''); +}); + +test('Does not render children with inert attribute when hasAnimations and defaultExpanded are true', () => { + render( + +
    Children content
    +
    + ); + + expect(screen.getByText('Children content')).not.toHaveAttribute('inert'); +}); diff --git a/packages/react-core/src/components/DualListSelector/examples/DualListSelectorTree.tsx b/packages/react-core/src/components/DualListSelector/examples/DualListSelectorTree.tsx index b7acf0dfeb2..a893541ed5e 100644 --- a/packages/react-core/src/components/DualListSelector/examples/DualListSelectorTree.tsx +++ b/packages/react-core/src/components/DualListSelector/examples/DualListSelectorTree.tsx @@ -283,7 +283,7 @@ export const DualListSelectorComposableTree: React.FunctionComponent + {buildPane(false)}