From 49bcb04e0d573bafeec0dcdf15e9af734700bb64 Mon Sep 17 00:00:00 2001
From: Jenny <32821331+jenny-s51@users.noreply.github.com>
Date: Wed, 3 Sep 2025 11:13:45 -0400
Subject: [PATCH 1/5] feat(AnimationsProvider): update opt-in components with
useHasAnimations, add AnimationsProvider to helpers, add md files for
documentation
fix(components): update hasAnimations to fall back to context
fix(md): remove cssPrefix
fix(alertgroup): fix tests
---
.../src/components/Alert/AlertGroup.tsx | 122 +++++++------
.../src/components/Alert/AlertGroupInline.tsx | 4 +-
.../Alert/__tests__/AlertGroup.test.tsx | 12 +-
.../DualListSelector/DualListSelector.tsx | 71 ++++----
.../DualListSelectorTreeItem.tsx | 4 +-
.../Form/FormFieldGroupExpandable.tsx | 4 +-
.../Form/InternalFormFieldGroup.tsx | 4 +-
.../components/SearchInput/SearchInput.tsx | 4 +-
.../src/components/TreeView/TreeView.tsx | 4 +-
.../components/TreeView/TreeViewListItem.tsx | 4 +-
.../__snapshots__/TreeView.test.tsx.snap | 2 +-
.../AnimationsProvider/AnimationsProvider.tsx | 105 ++++++++++++
.../__tests__/AnimationsProvider.test.tsx | 134 +++++++++++++++
.../examples/AnimationsProvider.md | 161 ++++++++++++++++++
.../examples/AnimationsProviderBasic.tsx | 69 ++++++++
.../src/helpers/AnimationsProvider/index.ts | 1 +
packages/react-core/src/helpers/index.ts | 1 +
.../src/components/Table/Table.tsx | 4 +-
18 files changed, 595 insertions(+), 115 deletions(-)
create mode 100644 packages/react-core/src/helpers/AnimationsProvider/AnimationsProvider.tsx
create mode 100644 packages/react-core/src/helpers/AnimationsProvider/__tests__/AnimationsProvider.test.tsx
create mode 100644 packages/react-core/src/helpers/AnimationsProvider/examples/AnimationsProvider.md
create mode 100644 packages/react-core/src/helpers/AnimationsProvider/examples/AnimationsProviderBasic.tsx
create mode 100644 packages/react-core/src/helpers/AnimationsProvider/index.ts
diff --git a/packages/react-core/src/components/Alert/AlertGroup.tsx b/packages/react-core/src/components/Alert/AlertGroup.tsx
index 26028c1e16b..ed845ac32dc 100644
--- a/packages/react-core/src/components/Alert/AlertGroup.tsx
+++ b/packages/react-core/src/components/Alert/AlertGroup.tsx
@@ -1,7 +1,8 @@
-import { Component } from 'react';
+import { useEffect, useRef, useState } from 'react';
import * as ReactDOM from 'react-dom';
import { canUseDOM } from '../../helpers';
import { AlertGroupInline } from './AlertGroupInline';
+import { useHasAnimations } from '../../helpers';
export interface AlertGroupProps extends Omit, 'className'> {
/** Additional classes added to the AlertGroup */
@@ -26,78 +27,73 @@ export interface AlertGroupProps extends Omit,
'aria-label'?: string;
}
-interface AlertGroupState {
- container: HTMLElement;
-}
-
-class AlertGroup extends Component {
- static displayName = 'AlertGroup';
- state = {
- container: undefined
- } as AlertGroupState;
-
- componentDidMount() {
- const container = document.createElement('div');
- const target: HTMLElement = this.getTargetElement();
- this.setState({ container });
- target.appendChild(container);
- }
+export const AlertGroup: React.FunctionComponent = ({
+ className,
+ children,
+ hasAnimations: localHasAnimations,
+ isToast,
+ isLiveRegion,
+ onOverflowClick,
+ overflowMessage,
+ 'aria-label': ariaLabel,
- componentWillUnmount() {
- const target: HTMLElement = this.getTargetElement();
- if (this.state.container) {
- target.removeChild(this.state.container);
- }
- }
+ appendTo, // do not pass down to ul
+ ...props
+}: AlertGroupProps) => {
+ const containerRef = useRef(null);
+ const [isContainerReady, setIsContainerReady] = useState(false);
+ const hasAnimations = useHasAnimations(localHasAnimations);
- getTargetElement() {
- const appendTo = this.props.appendTo;
+ const getTargetElement = () => {
if (typeof appendTo === 'function') {
return appendTo();
}
return appendTo || document.body;
- }
+ };
+
+ useEffect(() => {
+ if (isToast && canUseDOM) {
+ const container = document.createElement('div');
+ const target = getTargetElement();
+ containerRef.current = container;
+ target.appendChild(container);
+ setIsContainerReady(true);
- render() {
- const {
- className,
- children,
- hasAnimations = false,
- isToast,
- isLiveRegion,
- onOverflowClick,
- overflowMessage,
- 'aria-label': ariaLabel,
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- appendTo, // do not pass down to ul
- ...props
- } = this.props;
- const alertGroup = (
-
- {children}
-
- );
- if (!this.props.isToast) {
- return alertGroup;
+ return () => {
+ if (containerRef.current) {
+ target.removeChild(containerRef.current);
+ containerRef.current = null;
+ }
+ setIsContainerReady(false);
+ };
}
+ }, [isToast, appendTo]);
- const container = this.state.container;
+ const alertGroup = (
+
+ {children}
+
+ );
- if (!canUseDOM || !container) {
- return null;
- }
+ if (!isToast) {
+ return alertGroup;
+ }
+
+ const container = containerRef.current;
- return ReactDOM.createPortal(alertGroup, container);
+ if (!canUseDOM || !container || !isContainerReady) {
+ return null;
}
-}
-export { AlertGroup };
+ return ReactDOM.createPortal(alertGroup, container);
+};
+AlertGroup.displayName = 'AlertGroup';
diff --git a/packages/react-core/src/components/Alert/AlertGroupInline.tsx b/packages/react-core/src/components/Alert/AlertGroupInline.tsx
index 95622162b11..391647de280 100644
--- a/packages/react-core/src/components/Alert/AlertGroupInline.tsx
+++ b/packages/react-core/src/components/Alert/AlertGroupInline.tsx
@@ -4,17 +4,19 @@ import styles from '@patternfly/react-styles/css/components/Alert/alert-group';
import { AlertGroupProps } from './AlertGroup';
import { AlertProps } from '../Alert';
import { AlertGroupContext } from './AlertGroupContext';
+import { useHasAnimations } from '../../helpers';
export const AlertGroupInline: React.FunctionComponent = ({
className,
children,
- hasAnimations,
+ hasAnimations: localHasAnimations,
isToast,
isLiveRegion,
onOverflowClick,
overflowMessage,
...props
}: AlertGroupProps) => {
+ const hasAnimations = useHasAnimations(localHasAnimations);
const [handleTransitionEnd, setHandleTransitionEnd] = useState<() => void>(() => () => {});
const updateTransitionEnd = (onTransitionEnd: () => void) => {
diff --git a/packages/react-core/src/components/Alert/__tests__/AlertGroup.test.tsx b/packages/react-core/src/components/Alert/__tests__/AlertGroup.test.tsx
index 4b5bade997a..aca2c9a8fcd 100644
--- a/packages/react-core/src/components/Alert/__tests__/AlertGroup.test.tsx
+++ b/packages/react-core/src/components/Alert/__tests__/AlertGroup.test.tsx
@@ -56,14 +56,16 @@ test('Standard Alert Group is not a toast alert group', () => {
expect(screen.getByText('alert title').parentElement).not.toHaveClass('pf-m-toast');
});
-test('Toast Alert Group contains expected modifier class', () => {
+test('Toast Alert Group contains expected modifier class', async () => {
render(
);
- expect(screen.getByLabelText('group label')).toHaveClass('pf-m-toast');
+ // Wait for the portal to be created and rendered
+ const alertGroup = await screen.findByLabelText('group label');
+ expect(alertGroup).toHaveClass('pf-m-toast');
});
test('Calls the callback set by updateTransitionEnd when transition ends and animations are enabled', async () => {
@@ -90,7 +92,8 @@ test('Calls the callback set by updateTransitionEnd when transition ends and ani
);
- await user.click(screen.getByLabelText('Close'));
+ const closeButton = await screen.findByLabelText('Close');
+ await user.click(closeButton);
expect(mockCallback).not.toHaveBeenCalled();
fireEvent.transitionEnd(screen.getByText('Test Alert').closest('.pf-v6-c-alert-group__item') as HTMLElement);
expect(mockCallback).toHaveBeenCalled();
@@ -120,7 +123,8 @@ test('Does not call the callback set by updateTransitionEnd when transition ends
);
- await user.click(screen.getByLabelText('Close'));
+ const closeButton = await screen.findByLabelText('Close');
+ await user.click(closeButton);
expect(mockCallback).toHaveBeenCalledTimes(1);
// The transitionend event firing should not cause the callback to be called again
fireEvent.transitionEnd(screen.getByText('Test Alert').closest('.pf-v6-c-alert-group__item') as HTMLElement);
diff --git a/packages/react-core/src/components/DualListSelector/DualListSelector.tsx b/packages/react-core/src/components/DualListSelector/DualListSelector.tsx
index a8eed78a257..6e4aa6fb69b 100644
--- a/packages/react-core/src/components/DualListSelector/DualListSelector.tsx
+++ b/packages/react-core/src/components/DualListSelector/DualListSelector.tsx
@@ -1,8 +1,8 @@
-import { Component } from 'react';
import styles from '@patternfly/react-styles/css/components/DualListSelector/dual-list-selector';
import { css } from '@patternfly/react-styles';
-import { GenerateId, PickOptional } from '../../helpers';
+import { GenerateId } from '../../helpers';
import { DualListSelectorContext } from './DualListSelectorContext';
+import { useHasAnimations } from '../../helpers';
/** Acts as a container for all other DualListSelector sub-components when using a
* composable dual list selector.
@@ -24,41 +24,34 @@ export interface DualListSelectorProps {
hasAnimations?: boolean;
}
-class DualListSelector extends Component {
- static displayName = 'DualListSelector';
- static defaultProps: PickOptional = {
- children: '',
- isTree: false,
- hasAnimations: false
- };
+export const DualListSelector: React.FunctionComponent = ({
+ className,
+ children,
+ id,
+ isTree = false,
+ hasAnimations: localHasAnimations,
+ ...props
+}: DualListSelectorProps) => {
+ const hasAnimations = useHasAnimations(localHasAnimations);
- constructor(props: DualListSelectorProps) {
- super(props);
- }
-
- render() {
- const { className, children, id, isTree, hasAnimations, ...props } = this.props;
-
- return (
-
-
- {(randomId) => (
-
- {children}
-
- )}
-
-
- );
- }
-}
-
-export { DualListSelector };
+ return (
+
+
+ {(randomId) => (
+
+ {children}
+
+ )}
+
+
+ );
+};
+DualListSelector.displayName = 'DualListSelector';
diff --git a/packages/react-core/src/components/DualListSelector/DualListSelectorTreeItem.tsx b/packages/react-core/src/components/DualListSelector/DualListSelectorTreeItem.tsx
index 94141f9f38e..d9fd32082da 100644
--- a/packages/react-core/src/components/DualListSelector/DualListSelectorTreeItem.tsx
+++ b/packages/react-core/src/components/DualListSelector/DualListSelectorTreeItem.tsx
@@ -6,6 +6,7 @@ import { Badge } from '../Badge';
import AngleRightIcon from '@patternfly/react-icons/dist/esm/icons/angle-right-icon';
import { flattenTree } from './treeUtils';
import { DualListSelectorListContext } from './DualListSelectorContext';
+import { useHasAnimations } from '../../helpers';
export interface DualListSelectorTreeItemProps extends React.HTMLProps {
/** Content rendered inside the dual list selector. */
@@ -58,7 +59,7 @@ const DualListSelectorTreeItemBase: React.FunctionComponent {
setIsExpanded(defaultExpanded);
diff --git a/packages/react-core/src/components/Form/FormFieldGroupExpandable.tsx b/packages/react-core/src/components/Form/FormFieldGroupExpandable.tsx
index 369a5adf3fc..bd0e7f9e21d 100644
--- a/packages/react-core/src/components/Form/FormFieldGroupExpandable.tsx
+++ b/packages/react-core/src/components/Form/FormFieldGroupExpandable.tsx
@@ -1,5 +1,6 @@
import { useState } from 'react';
import { InternalFormFieldGroup } from './InternalFormFieldGroup';
+import { useHasAnimations } from '../../helpers';
export interface FormFieldGroupExpandableProps extends Omit, 'onToggle'> {
/** Anything that can be rendered as form field group content. */
@@ -25,10 +26,11 @@ export const FormFieldGroupExpandable: React.FunctionComponent {
const [localIsExpanded, setIsExpanded] = useState(isExpanded);
+ const hasAnimations = useHasAnimations(localHasAnimations);
return (
, 'label' | 'onToggle'> {
/** Anything that can be rendered as form field group content. */
@@ -33,9 +34,10 @@ export const InternalFormFieldGroup: React.FunctionComponent {
+ const hasAnimations = useHasAnimations(localHasAnimations);
const headerTitleText = header ? header.props.titleText : null;
if (isExpandable && !toggleAriaLabel && !headerTitleText) {
// eslint-disable-next-line no-console
diff --git a/packages/react-core/src/components/SearchInput/SearchInput.tsx b/packages/react-core/src/components/SearchInput/SearchInput.tsx
index ddb9f96a1dd..c85d49b0f60 100644
--- a/packages/react-core/src/components/SearchInput/SearchInput.tsx
+++ b/packages/react-core/src/components/SearchInput/SearchInput.tsx
@@ -13,6 +13,7 @@ import { AdvancedSearchMenu } from './AdvancedSearchMenu';
import { TextInputGroup, TextInputGroupMain, TextInputGroupUtilities } from '../TextInputGroup';
import { InputGroup, InputGroupItem } from '../InputGroup';
import { Popper } from '../../helpers';
+import { useHasAnimations } from '../../helpers';
import textInputGroupStyles from '@patternfly/react-styles/css/components/TextInputGroup/text-input-group';
import inputGroupStyles from '@patternfly/react-styles/css/components/InputGroup/input-group';
@@ -180,7 +181,8 @@ const SearchInputBase: React.FunctionComponent = ({
const popperRef = useRef(null);
const [focusAfterExpandChange, setFocusAfterExpandChange] = useState(false);
- const { isExpanded, onToggleExpand, toggleAriaLabel, hasAnimations } = expandableInput || {};
+ const { isExpanded, onToggleExpand, toggleAriaLabel, hasAnimations: localHasAnimations } = expandableInput || {};
+ const hasAnimations = useHasAnimations(localHasAnimations);
useEffect(() => {
// this effect and the focusAfterExpandChange variable are needed to focus the input/toggle as needed when the
diff --git a/packages/react-core/src/components/TreeView/TreeView.tsx b/packages/react-core/src/components/TreeView/TreeView.tsx
index 890a86782ba..ebd57522277 100644
--- a/packages/react-core/src/components/TreeView/TreeView.tsx
+++ b/packages/react-core/src/components/TreeView/TreeView.tsx
@@ -1,6 +1,7 @@
import { TreeViewList } from './TreeViewList';
import { TreeViewCheckProps, TreeViewListItem } from './TreeViewListItem';
import { TreeViewRoot } from './TreeViewRoot';
+import { useHasAnimations } from '../../helpers';
/** Properties that make up a tree view data item. These properties should be passed in as an
* object to one of the various tree view component properties which accept TreeViewDataItem as
@@ -135,9 +136,10 @@ export const TreeView: React.FunctionComponent = ({
useMemo,
'aria-label': ariaLabel,
'aria-labelledby': ariaLabelledby,
- hasAnimations,
+ hasAnimations: localHasAnimations,
...props
}: TreeViewProps) => {
+ const hasAnimations = useHasAnimations(localHasAnimations);
const treeViewList = (
>, 'checked'> {
checked?: boolean | null;
@@ -102,10 +103,11 @@ const TreeViewListItemBase: React.FunctionComponent = ({
expandedIcon,
action,
compareItems,
- hasAnimations,
+ hasAnimations: localHasAnimations,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
useMemo
}: TreeViewListItemProps) => {
+ const hasAnimations = useHasAnimations(localHasAnimations);
const [internalIsExpanded, setIsExpanded] = useState(defaultExpanded);
useEffect(() => {
if (isExpanded !== undefined && isExpanded !== null) {
diff --git a/packages/react-core/src/components/TreeView/__tests__/__snapshots__/TreeView.test.tsx.snap b/packages/react-core/src/components/TreeView/__tests__/__snapshots__/TreeView.test.tsx.snap
index 8b523ae077c..6f0bf3de7c5 100644
--- a/packages/react-core/src/components/TreeView/__tests__/__snapshots__/TreeView.test.tsx.snap
+++ b/packages/react-core/src/components/TreeView/__tests__/__snapshots__/TreeView.test.tsx.snap
@@ -105,7 +105,7 @@ exports[`Matches snapshot 1`] = `
TreeViewListItem useMemo: undefined
- TreeViewListItem hasAnimations: undefined
+ TreeViewListItem hasAnimations: false