Skip to content

Conversation

jenny-s51
Copy link
Contributor

What: Closes #11962

This PR enables support for AnimationsProvider.

PatternFly consumers will now be able to set animations for opt-in components once at the app level (instead of on every component via hasAnimations) while maintaining full backward compatibility.

Testing Instructions

To test the new context provider functionality:

  1. Run yarn start and navigate to http://localhost:8002/components/alert#toast-alert-group

  2. Copy and paste the code below into the demo TSX code:

    Click to expand
     // eslint-disable-next-line no-restricted-imports
     import React, { Fragment, useState } from 'react';
     import {
       Alert,
       AlertProps,
       AlertGroup,
       AlertActionCloseButton,
       AlertVariant,
       InputGroup,
       InputGroupItem
     } from '@patternfly/react-core';
     import buttonStyles from '@patternfly/react-styles/css/components/Button/button';
     import { AnimationsProvider } from '@patternfly/react-core/dist/esm/helpers';
     export const AlertGroupToast: React.FunctionComponent = () => {
       const [alerts, setAlerts] = useState<Partial<AlertProps>[]>([]);
     
       const addAlert = (title: string, variant: AlertProps['variant'], key: React.Key) => {
         setAlerts((prevAlerts) => [{ title, variant, key }, ...prevAlerts]);
       };
     
       const removeAlert = (key: React.Key) => {
         setAlerts((prevAlerts) => [...prevAlerts.filter((alert) => alert.key !== key)]);
       };
     
       const btnClasses = [buttonStyles.button, buttonStyles.modifiers.secondary].join(' ');
     
       const getUniqueId = () => new Date().getTime();
     
       const addSuccessAlert = () => {
         addAlert('Toast success alert', 'success', getUniqueId());
       };
     
       const addDangerAlert = () => {
         addAlert('Toast danger alert', 'danger', getUniqueId());
       };
     
       const addInfoAlert = () => {
         addAlert('Toast info alert', 'info', getUniqueId());
       };
     
       return (
         <Fragment>
           <InputGroup style={{ marginBottom: '16px' }}>
             <InputGroupItem>
               <button onClick={addSuccessAlert} type="button" className={btnClasses}>
                 Add toast success alert
               </button>
             </InputGroupItem>
             <InputGroupItem>
               <button onClick={addDangerAlert} type="button" className={btnClasses}>
                 Add toast danger alert
               </button>
             </InputGroupItem>
             <InputGroupItem>
               <button onClick={addInfoAlert} type="button" className={btnClasses}>
                 Add toast info alert
               </button>
             </InputGroupItem>
           </InputGroup>
           <AnimationsProvider config={{ hasAnimations: true }}>
             <AlertGroup isToast isLiveRegion>
               {alerts.map(({ key, variant, title }) => (
                 <Alert
                   variant={AlertVariant[variant]}
                   title={title}
                   actionClose={
                     <AlertActionCloseButton
                       title={title as string}
                       variantLabel={`${variant} alert`}
                       onClose={() => removeAlert(key)}
                     />
                   }
                   key={key}
                 />
               ))}
             </AlertGroup>
           </AnimationsProvider>
         </Fragment>
       );
     };
    
  1. Verify the animations are applied to the component via the context provider rather than with the hasAnimations prop.

Additional Changes & Fixes

Converted class components to functional components to enable React Hooks usage useHasAnimations hook integration:

  • AlertGroup.tsx - Required for useHasAnimations hook integration
  • DualListSelector.tsx - Required for useHasAnimations hook integration

Quick Verification

  1. Go to any example for a component that uses the hasAnimations prop.
  2. Wrap with <AnimationsProvider config={{ hasAnimations: true }}>
  3. Remove the hasAnimations prop from the child component.
  4. Verify animations work as expected.

Testing Note: Since PatternFly example files don't typically import from /helpers, the ESLint configs require a React import with the // eslint-disable-next-line no-restricted-imports comment at the top of the file for local testing. Real consumers won't have this issue.

Success Criteria

  • ✅ Component hasAnimations prop overrides provider when set
  • ✅ Existing examples work unchanged without provider
  • ✅ No console errors or TypeScript issues

@jenny-s51 jenny-s51 marked this pull request as draft September 4, 2025 14:49
@patternfly-build
Copy link
Contributor

patternfly-build commented Sep 4, 2025

@jenny-s51 jenny-s51 marked this pull request as ready for review September 4, 2025 16:17
…ons, 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
Copy link
Contributor

@wise-king-sullyman wise-king-sullyman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work on this! I have a handful of comments/change requests but they're mostly fairly small things.

This review didn't include looking at changing AlertGroup and DualListSelector from class based to function components that was needed as unfortunately I ran out of time. I wouldn't say to redo anything, but just for the sake of the future if a similar situation arises I would advocate for splitting that work into a distinct PR just to make review/testing easier, but that's just my $0.02.

);

const closeButton = screen.getByLabelText('Close');
fireEvent.click(closeButton);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless there's a particular reason to need fireEvent we should default to userEvent per the suggestions of the RTL team.

If there is a particular reason fireEvent is needed please add a comment explaining why.


// Should call callback on transition end when animations are enabled via context
fireEvent.transitionEnd(screen.getByText('Test Alert').closest('.pf-v6-c-alert-group__item') as HTMLElement);
expect(mockCallback).toHaveBeenCalled();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of checking for the onClose callback to be called/not called would it make more sense to test for animation related classes on the element?

Copy link
Contributor Author

@jenny-s51 jenny-s51 Sep 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, really good call here - updated

children,
id,
isTree = false,
hasAnimations: localHasAnimations,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I feel like hasAnimationsProp or something would maybe be a little clearer of a deconstructed name? I don't have strong feelings on that though and it might just be a matter of preference.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may not be consistent with it, but we do have instances where in this context we've used somethingProp, while for variables inside the component we use localSomething. Again we may not be consistent on this, but I'd probably lean towards using hasAnimationsProp instead here, and reserving localHasAnimations for a variable/state name inside the component code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wise-king-sullyman @thatblindgeye thank you, great points, I like hasAnimationsProp here too, it's clearer and follows our existing conventions better - updated it.

Comment on lines 4 to 12
export interface AnimationsConfig {
/** Whether animations are enabled globally */
hasAnimations?: boolean;
}

/** Props for the AnimationsProvider component */
export interface AnimationsProviderProps {
/** Animation configuration settings */
config?: AnimationsConfig;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these be required? I'm not sure what use case there would be for someone to use the provider without setting a config with a hasAnimations value.


// Local prop takes precedence when explicitly set (including false)
// If local prop is undefined, fall back to context
return localHasAnimations !== undefined ? localHasAnimations : (contextHasAnimations ?? false);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be simplified to just localHasAnimations ?? contextHasAnimations ?? false instead of a ternary with a nullish coalescing operator nested inside it?

return <div data-testid="has-animations">{hasAnimations ? 'animations-enabled' : 'animations-disabled'}</div>;
};

describe('AnimationsProvider', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: can you remove this nesting describe block? We default to not using them around whole suites.

Also if you're not aware of our testing styles/guidelines you might want to give that article in our wiki a look 🙂

@@ -0,0 +1,161 @@
---
id: Animations provider
section: helpers
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we going to be adding a helpers section to the site?

Copy link
Contributor

@thatblindgeye thatblindgeye Sep 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a topic to bring up in a platform topics meeting @jenny-s51 . I don't believe we really document helpers anywhere on Org currently, and I would have thought communication about this new context would be housed in something like release highlights/notes. A decision on this could mean having to remove the new examples that you added here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wise-king-sullyman @thatblindgeye yep great callout, I think it makes sense to delete it here and include this md file as part of patternfly/patternfly-org#4783 - removed it from this PR and will add it there as a follow-up.

@@ -0,0 +1,69 @@
import { TreeView } from '@patternfly/react-core';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this file being used anywhere?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch, deleted it

});

// Animation context tests
test('respects AnimationsProvider context when no local hasAnimations prop', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kind of the opposite of Austin's comment about regarding a describe block, I personally might wrap these tests in a describe block specifically for the context tests. Not a blocker and depending how quickly we want to get this merged in, would like to hear @wise-king-sullyman opinion here.

Copy link
Contributor Author

@jenny-s51 jenny-s51 Sep 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think so too @thatblindgeye - based on the RTL guide this seems like a good use case for a grouped describe() block, I've updated the tests to include it, but will also defer to @wise-king-sullyman here for the final word on whether or not we'd like to keep it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not against grouping associated things like the animation tests together in a describe block in an otherwise broader test suite, just wrapping full test suites in a describe block 🙂

target.removeChild(this.state.container);
}
}
appendTo, // do not pass down to ul
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: it's pre-existing but I think we could remove this comment now that it's a functional component.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch, removed

children,
id,
isTree = false,
hasAnimations: localHasAnimations,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may not be consistent with it, but we do have instances where in this context we've used somethingProp, while for variables inside the component we use localSomething. Again we may not be consistent on this, but I'd probably lean towards using hasAnimationsProp instead here, and reserving localHasAnimations for a variable/state name inside the component code.

const [focusAfterExpandChange, setFocusAfterExpandChange] = useState(false);

const { isExpanded, onToggleExpand, toggleAriaLabel, hasAnimations } = expandableInput || {};
const { isExpanded, onToggleExpand, toggleAriaLabel, hasAnimations: localHasAnimations } = expandableInput || {};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just as a followup to my above reply to Austin about the prop name, I think this makes sense to call it localHasAnimations.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just following up here to confirm based on the discussion in the thread here, I renamed it to hasAnimations for both the destructed and internal variable for consistency with our existing conventions for destructured props, for example JumpLinks uses activeIndex: activeIndexProp and isExpanded: isExpandedProp. What are your thoughts on using hasAnimationsProp here?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Context Provider - Implement AnimationContextProvider
4 participants