From 2d4c4d869cc3e0f3868b0d6a5050468e71adaa7c Mon Sep 17 00:00:00 2001 From: Gustavo Murcia Date: Thu, 9 Jul 2026 17:10:39 -0400 Subject: [PATCH 01/16] feat(Modal,Backdrop): Add animation support [WIP] This is an initial and in-progress implementation of Modal and Backdrop animation support. These changes were cherry-picked (file-level) from branch gmurcia/tearsheet, originating from the following commits: - c027e56bb chore: Initial modal/backdrop animation impl - 1927470f0 chore: Animation tweaks. - a7f41028e chore: Implement TearsheetGroup for unbounded tearsheet levels Commit-generated-by: Claude Opus 4.6 Co-authored-by: Claude Opus 4.6 --- .../src/components/Backdrop/Backdrop.tsx | 18 +++++++- .../react-core/src/components/Modal/Modal.tsx | 2 + .../src/components/Modal/ModalBox.tsx | 10 +++++ .../src/components/Modal/ModalContent.tsx | 16 +++++-- .../src/components/Modal/examples/Modal.md | 14 +++++- .../Modal/examples/ModalAnimated.tsx | 43 +++++++++++++++++++ .../backdrop-animations.css | 36 ++++++++++++++++ .../ModalAnimations/modal-animations.css | 42 ++++++++++++++++++ .../react-tokens/scripts/generateTokens.mjs | 6 +-- scripts/build-single-packages.mjs | 1 + 10 files changed, 179 insertions(+), 9 deletions(-) create mode 100644 packages/react-core/src/components/Modal/examples/ModalAnimated.tsx create mode 100644 packages/react-styles/src/css/components/BackdropAnimations/backdrop-animations.css create mode 100644 packages/react-styles/src/css/components/ModalAnimations/modal-animations.css diff --git a/packages/react-core/src/components/Backdrop/Backdrop.tsx b/packages/react-core/src/components/Backdrop/Backdrop.tsx index c564a8b7f53..d17bbfb3817 100644 --- a/packages/react-core/src/components/Backdrop/Backdrop.tsx +++ b/packages/react-core/src/components/Backdrop/Backdrop.tsx @@ -1,19 +1,35 @@ import { css } from '@patternfly/react-styles'; import styles from '@patternfly/react-styles/css/components/Backdrop/backdrop'; +import stylesAnimated from '@patternfly/react-styles/css/components/BackdropAnimations/backdrop-animations'; export interface BackdropProps extends React.HTMLProps { /** Content rendered inside the backdrop */ children?: React.ReactNode; /** Additional classes added to the backdrop */ className?: string; + /** Whether the Backdrop should open/close with animations. (BETA) */ + animated?: boolean; + /** Flag to show the backdrop. Used in conjunction with `animated` (BETA) */ + isVisible?: boolean; } export const Backdrop: React.FunctionComponent = ({ children = null, className = '', + animated, + isVisible, ...props }: BackdropProps) => ( -
+
{children}
); diff --git a/packages/react-core/src/components/Modal/Modal.tsx b/packages/react-core/src/components/Modal/Modal.tsx index 281c379eebc..aebfd22146e 100644 --- a/packages/react-core/src/components/Modal/Modal.tsx +++ b/packages/react-core/src/components/Modal/Modal.tsx @@ -53,6 +53,8 @@ export interface ModalProps extends React.HTMLProps, OUIAProps { ouiaId?: number | string; /** Set the value of data-ouia-safe. Only set to true when the component is in a static state, i.e. no animations are occurring. At all other times, this value must be false. */ ouiaSafe?: boolean; + /** Whether the Modal should open/close with animations. (BETA) */ + animated?: boolean; } export enum ModalVariant { diff --git a/packages/react-core/src/components/Modal/ModalBox.tsx b/packages/react-core/src/components/Modal/ModalBox.tsx index ed4d4b4b24b..30eaac0518f 100644 --- a/packages/react-core/src/components/Modal/ModalBox.tsx +++ b/packages/react-core/src/components/Modal/ModalBox.tsx @@ -1,5 +1,6 @@ import { css } from '@patternfly/react-styles'; import styles from '@patternfly/react-styles/css/components/ModalBox/modal-box'; +import stylesAnimated from '@patternfly/react-styles/css/components/ModalAnimations/modal-animations'; import topSpacer from '@patternfly/react-tokens/dist/esm/c_modal_box_m_align_top_spacer'; export interface ModalBoxProps extends React.HTMLProps { @@ -19,6 +20,10 @@ export interface ModalBoxProps extends React.HTMLProps { positionOffset?: string; /** Variant of the modal. */ variant?: 'small' | 'medium' | 'large' | 'default'; + /** Whether the Modal should open/close with animations. (BETA) */ + animated?: boolean; + /** Flag to show the modal. */ + isOpen?: boolean; } export const ModalBox: React.FunctionComponent = ({ @@ -31,6 +36,8 @@ export const ModalBox: React.FunctionComponent = ({ 'aria-label': ariaLabel, 'aria-describedby': ariaDescribedby, style, + isOpen, + animated, ...props }: ModalBoxProps) => { if (positionOffset) { @@ -46,6 +53,9 @@ export const ModalBox: React.FunctionComponent = ({ aria-modal="true" className={css( styles.modalBox, + animated && stylesAnimated.modalAnimated, + animated && isOpen === true && stylesAnimated.modalAnimatedOpen, + animated && isOpen !== true && stylesAnimated.modalAnimatedClosed, className, position === 'top' && styles.modifiers.alignTop, variant === 'large' && styles.modifiers.lg, diff --git a/packages/react-core/src/components/Modal/ModalContent.tsx b/packages/react-core/src/components/Modal/ModalContent.tsx index 38e311c8cfa..3d6698086cb 100644 --- a/packages/react-core/src/components/Modal/ModalContent.tsx +++ b/packages/react-core/src/components/Modal/ModalContent.tsx @@ -49,6 +49,8 @@ export interface ModalContentProps extends OUIAProps { ouiaId?: number | string; /** Set the value of data-ouia-safe. Only set to true when the component is in a static state, i.e. no animations are occurring. At all other times, this value must be false. */ ouiaSafe?: boolean; + /** Whether the Modal should open/close with animations. (BETA) */ + animated?: boolean; } export const ModalContent: React.FunctionComponent = ({ @@ -72,9 +74,10 @@ export const ModalContent: React.FunctionComponent = ({ ouiaSafe = true, elementToFocus, focusTrapId, + animated, ...props }: ModalContentProps) => { - if (!isOpen) { + if (!isOpen && !animated) { return null; } @@ -91,6 +94,8 @@ export const ModalContent: React.FunctionComponent = ({ const modalBox = ( = ({ {children} ); + let focusTrapActive = !disableFocusTrap; + if (animated) { + focusTrapActive = !disableFocusTrap && isOpen; + } + return ( - + { + const [isModalOpen, setIsModalOpen] = useState(false); + + const handleModalToggle = (_event: KeyboardEvent | React.MouseEvent) => { + setIsModalOpen((prevIsModalOpen) => !prevIsModalOpen); + }; + + return ( + + + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore + magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo + consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla + pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. + + + + + + + + ); +}; diff --git a/packages/react-styles/src/css/components/BackdropAnimations/backdrop-animations.css b/packages/react-styles/src/css/components/BackdropAnimations/backdrop-animations.css new file mode 100644 index 00000000000..c76f0cc4015 --- /dev/null +++ b/packages/react-styles/src/css/components/BackdropAnimations/backdrop-animations.css @@ -0,0 +1,36 @@ +.pf-v6-c-backdrop-animated { + background-color: transparent; + visibility: hidden; + pointer-events: none; + transition: + background-color 240ms cubic-bezier(0.4, 0.14, 1, 1), /* Carbon equivalent: duration-moderate-02 + motion(exit, expressive) */ + visibility 0ms linear 240ms; /* Carbon equivalent: duration-moderate-02 */ +} + +.pf-v6-c-backdrop-animated-visible { + background-color: var(--pf-v6-c-backdrop--BackgroundColor); + visibility: visible; + pointer-events: unset; + transition: + background-color 240ms cubic-bezier(0, 0, 0.2, 1), + visibility 0ms linear 0ms; +} + +.pf-v6-c-backdrop-animated-hidden { + background-color: transparent; + visibility: hidden; + pointer-events: none; +} + +/* TODO [Gustavo]: My local setup has reduced animations at the OS level which kicks this media query on. + Chrome doesn't have an easy way to FORCE animations on in this case so commenting this out for now. + It should be enabled if PR is merged in. */ +/* * +@media screen and (prefers-reduced-motion: reduce) { + .pf-v6-c-backdrop-animated, + .pf-v6-c-backdrop-animated-visible, + .pf-v6-c-backdrop-animated-hidden { + transition: none; + } +} +/* */ diff --git a/packages/react-styles/src/css/components/ModalAnimations/modal-animations.css b/packages/react-styles/src/css/components/ModalAnimations/modal-animations.css new file mode 100644 index 00000000000..b22cb28b5e6 --- /dev/null +++ b/packages/react-styles/src/css/components/ModalAnimations/modal-animations.css @@ -0,0 +1,42 @@ +.pf-v6-c-modal-animated { + --pf-v6-c-modal-animated--Transition: + opacity 240ms cubic-bezier(0.4, 0.14, 1, 1), + transform 240ms cubic-bezier(0.4, 0.14, 1, 1), + visibility 0ms linear 240ms; + opacity: 0; + visibility: hidden; + pointer-events: none; + transform: translate3d(0, -24px, 0); + transform-origin: top center; + transition: var(--pf-v6-c-modal-animated--Transition); +} + +.pf-v6-c-modal-animated-open { + --pf-v6-c-modal-animated--Transition: + transform 240ms cubic-bezier(0, 0, 0.2, 1), + visibility 0ms linear 0ms; + opacity: 1; + visibility: visible; + pointer-events: unset; + transform: translate3d(0, 0, 0); +} + +.pf-v6-c-modal-animated-closed { + opacity: 0; + visibility: hidden; + pointer-events: none; + transform: translate3d(0, -24px, 0); +} + +/* TODO [Gustavo]: My local setup has reduced animations at the OS level which kicks this media query on. + Chrome doesn't have an easy way to FORCE animations on in this case so commenting this out for now. + It should be enabled if PR is merged in. */ +/* * +@media screen and (prefers-reduced-motion: reduce) { + .pf-v6-c-modal-animated, + .pf-v6-c-modal-animated-open, + .pf-v6-c-modal-animated-closed { + transition: none; + } +} +/* */ diff --git a/packages/react-tokens/scripts/generateTokens.mjs b/packages/react-tokens/scripts/generateTokens.mjs index 8da03dcdfd7..a5943f5f5c3 100644 --- a/packages/react-tokens/scripts/generateTokens.mjs +++ b/packages/react-tokens/scripts/generateTokens.mjs @@ -33,11 +33,11 @@ const getDeclarations = (cssAst) => .reduce((acc, val) => acc.concat(val), []); // flatten const formatFilePathToName = (filePath) => { - // const filePathArr = filePath.split('/'); + const normalizedPath = filePath.replace(/\\/g, '/'); let prefix = ''; - if (filePath.includes('components/')) { + if (normalizedPath.includes('components/')) { prefix = 'c_'; - } else if (filePath.includes('layouts/')) { + } else if (normalizedPath.includes('layouts/')) { prefix = 'l_'; } return `${prefix}${basename(filePath, '.css').replace(/-+/g, '_')}`; diff --git a/scripts/build-single-packages.mjs b/scripts/build-single-packages.mjs index afa1bfd3c13..54e65142217 100644 --- a/scripts/build-single-packages.mjs +++ b/scripts/build-single-packages.mjs @@ -43,6 +43,7 @@ const components = { }; async function createPackage(component) { + component = component.replaceAll('\\', '/'); const cmds = []; let destFile = component.replace(/[^/]+\.js$/g, 'package.json').replace('/dist/esm/', '/dist/dynamic/'); From bcfd6b4995422e0cbd01f06b0d1e2a05ea9d4333 Mon Sep 17 00:00:00 2001 From: Gustavo Murcia Date: Thu, 9 Jul 2026 19:36:21 -0400 Subject: [PATCH 02/16] refactor: Previous ad-hoc animation impl now aligns with standard PF AnimationsProvider + hooks Replace custom `animated` prop with `hasAnimations` across Modal, ModalContent, ModalBox, and Backdrop. Wire up `useHasAnimations` hook in ModalContent and Backdrop so both participate in AnimationsProvider context. Add second example demonstrating the AnimationsProvider path. Generated-by: Claude Co-authored-by: Claude --- .../src/components/Backdrop/Backdrop.tsx | 41 +++++++------ .../react-core/src/components/Modal/Modal.tsx | 4 +- .../src/components/Modal/ModalBox.tsx | 12 ++-- .../src/components/Modal/ModalContent.tsx | 18 +++--- .../src/components/Modal/examples/Modal.md | 13 ++++- .../Modal/examples/ModalAnimated.tsx | 6 +- .../Modal/examples/ModalAnimatedProvider.tsx | 57 +++++++++++++++++++ 7 files changed, 111 insertions(+), 40 deletions(-) create mode 100644 packages/react-core/src/components/Modal/examples/ModalAnimatedProvider.tsx diff --git a/packages/react-core/src/components/Backdrop/Backdrop.tsx b/packages/react-core/src/components/Backdrop/Backdrop.tsx index d17bbfb3817..3dea142179b 100644 --- a/packages/react-core/src/components/Backdrop/Backdrop.tsx +++ b/packages/react-core/src/components/Backdrop/Backdrop.tsx @@ -1,36 +1,41 @@ import { css } from '@patternfly/react-styles'; import styles from '@patternfly/react-styles/css/components/Backdrop/backdrop'; import stylesAnimated from '@patternfly/react-styles/css/components/BackdropAnimations/backdrop-animations'; +import { useHasAnimations } from '../../helpers'; export interface BackdropProps extends React.HTMLProps { /** Content rendered inside the backdrop */ children?: React.ReactNode; /** Additional classes added to the backdrop */ className?: string; - /** Whether the Backdrop should open/close with animations. (BETA) */ - animated?: boolean; - /** Flag to show the backdrop. Used in conjunction with `animated` (BETA) */ + /** Flag indicating whether animations are enabled. */ + hasAnimations?: boolean; + /** Flag to show the backdrop. Used in conjunction with `hasAnimations`. */ isVisible?: boolean; } export const Backdrop: React.FunctionComponent = ({ children = null, className = '', - animated, + hasAnimations: hasAnimationsProp, isVisible, ...props -}: BackdropProps) => ( -
- {children} -
-); +}: BackdropProps) => { + const hasAnimations = useHasAnimations(hasAnimationsProp); + + return ( +
+ {children} +
+ ); +}; Backdrop.displayName = 'Backdrop'; diff --git a/packages/react-core/src/components/Modal/Modal.tsx b/packages/react-core/src/components/Modal/Modal.tsx index aebfd22146e..bb916ef52ed 100644 --- a/packages/react-core/src/components/Modal/Modal.tsx +++ b/packages/react-core/src/components/Modal/Modal.tsx @@ -53,8 +53,8 @@ export interface ModalProps extends React.HTMLProps, OUIAProps { ouiaId?: number | string; /** Set the value of data-ouia-safe. Only set to true when the component is in a static state, i.e. no animations are occurring. At all other times, this value must be false. */ ouiaSafe?: boolean; - /** Whether the Modal should open/close with animations. (BETA) */ - animated?: boolean; + /** Flag indicating whether animations are enabled. */ + hasAnimations?: boolean; } export enum ModalVariant { diff --git a/packages/react-core/src/components/Modal/ModalBox.tsx b/packages/react-core/src/components/Modal/ModalBox.tsx index 30eaac0518f..17d8a0b3a8d 100644 --- a/packages/react-core/src/components/Modal/ModalBox.tsx +++ b/packages/react-core/src/components/Modal/ModalBox.tsx @@ -20,8 +20,8 @@ export interface ModalBoxProps extends React.HTMLProps { positionOffset?: string; /** Variant of the modal. */ variant?: 'small' | 'medium' | 'large' | 'default'; - /** Whether the Modal should open/close with animations. (BETA) */ - animated?: boolean; + /** Flag indicating whether animations are enabled. */ + hasAnimations?: boolean; /** Flag to show the modal. */ isOpen?: boolean; } @@ -37,7 +37,7 @@ export const ModalBox: React.FunctionComponent = ({ 'aria-describedby': ariaDescribedby, style, isOpen, - animated, + hasAnimations, ...props }: ModalBoxProps) => { if (positionOffset) { @@ -53,9 +53,9 @@ export const ModalBox: React.FunctionComponent = ({ aria-modal="true" className={css( styles.modalBox, - animated && stylesAnimated.modalAnimated, - animated && isOpen === true && stylesAnimated.modalAnimatedOpen, - animated && isOpen !== true && stylesAnimated.modalAnimatedClosed, + hasAnimations && stylesAnimated.modalAnimated, + hasAnimations && isOpen === true && stylesAnimated.modalAnimatedOpen, + hasAnimations && isOpen !== true && stylesAnimated.modalAnimatedClosed, className, position === 'top' && styles.modifiers.alignTop, variant === 'large' && styles.modifiers.lg, diff --git a/packages/react-core/src/components/Modal/ModalContent.tsx b/packages/react-core/src/components/Modal/ModalContent.tsx index 3d6698086cb..499959b4ab9 100644 --- a/packages/react-core/src/components/Modal/ModalContent.tsx +++ b/packages/react-core/src/components/Modal/ModalContent.tsx @@ -1,7 +1,7 @@ import { FocusTrap } from '../../helpers'; import bullsEyeStyles from '@patternfly/react-styles/css/layouts/Bullseye/bullseye'; import { css } from '@patternfly/react-styles'; -import { getOUIAProps, OUIAProps } from '../../helpers'; +import { getOUIAProps, OUIAProps, useHasAnimations } from '../../helpers'; import { Backdrop } from '../Backdrop'; import { ModalBoxCloseButton } from './ModalBoxCloseButton'; import { ModalBox } from './ModalBox'; @@ -49,8 +49,8 @@ export interface ModalContentProps extends OUIAProps { ouiaId?: number | string; /** Set the value of data-ouia-safe. Only set to true when the component is in a static state, i.e. no animations are occurring. At all other times, this value must be false. */ ouiaSafe?: boolean; - /** Whether the Modal should open/close with animations. (BETA) */ - animated?: boolean; + /** Flag indicating whether animations are enabled. */ + hasAnimations?: boolean; } export const ModalContent: React.FunctionComponent = ({ @@ -74,10 +74,12 @@ export const ModalContent: React.FunctionComponent = ({ ouiaSafe = true, elementToFocus, focusTrapId, - animated, + hasAnimations: hasAnimationsProp, ...props }: ModalContentProps) => { - if (!isOpen && !animated) { + const hasAnimations = useHasAnimations(hasAnimationsProp); + + if (!isOpen && !hasAnimations) { return null; } @@ -95,7 +97,7 @@ export const ModalContent: React.FunctionComponent = ({ = ({ ); let focusTrapActive = !disableFocusTrap; - if (animated) { + if (hasAnimations) { focusTrapActive = !disableFocusTrap && isOpen; } return ( - + { Show animated modal @@ -30,7 +30,7 @@ export const ModalAnimated: React.FunctionComponent = () => { est laborum. - + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et + dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex + ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat + nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit + anim id est laborum. + + + + + + + + + ); +}; From df1305e68e808bc3915918b3aeebf794bf471466 Mon Sep 17 00:00:00 2001 From: Gustavo Murcia Date: Thu, 9 Jul 2026 19:50:42 -0400 Subject: [PATCH 03/16] chore: Cleanup --- packages/react-core/src/components/Modal/examples/Modal.md | 4 +--- .../components/BackdropAnimations/backdrop-animations.css | 5 ----- .../src/css/components/ModalAnimations/modal-animations.css | 5 ----- 3 files changed, 1 insertion(+), 13 deletions(-) diff --git a/packages/react-core/src/components/Modal/examples/Modal.md b/packages/react-core/src/components/Modal/examples/Modal.md index 75e2c8ab8e5..f2727587cf5 100644 --- a/packages/react-core/src/components/Modal/examples/Modal.md +++ b/packages/react-core/src/components/Modal/examples/Modal.md @@ -7,10 +7,8 @@ ouia: true --- import { Fragment, useRef, useState } from 'react'; -import WarningTriangleIcon from '@patternfly/react-icons/dist/esm/icons/warning-triangle-icon'; -import RhMicronsCaretDownIcon from '@patternfly/react-icons/dist/esm/icons/rh-microns-caret-down-icon'; +import RhUiWarningIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-warning-icon'; import RhUiAttentionBellFillIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-attention-bell-fill-icon'; -import HelpIcon from '@patternfly/react-icons/dist/esm/icons/help-icon'; import formStyles from '@patternfly/react-styles/css/components/Form/form'; ## Examples diff --git a/packages/react-styles/src/css/components/BackdropAnimations/backdrop-animations.css b/packages/react-styles/src/css/components/BackdropAnimations/backdrop-animations.css index c76f0cc4015..4ca23410911 100644 --- a/packages/react-styles/src/css/components/BackdropAnimations/backdrop-animations.css +++ b/packages/react-styles/src/css/components/BackdropAnimations/backdrop-animations.css @@ -22,10 +22,6 @@ pointer-events: none; } -/* TODO [Gustavo]: My local setup has reduced animations at the OS level which kicks this media query on. - Chrome doesn't have an easy way to FORCE animations on in this case so commenting this out for now. - It should be enabled if PR is merged in. */ -/* * @media screen and (prefers-reduced-motion: reduce) { .pf-v6-c-backdrop-animated, .pf-v6-c-backdrop-animated-visible, @@ -33,4 +29,3 @@ transition: none; } } -/* */ diff --git a/packages/react-styles/src/css/components/ModalAnimations/modal-animations.css b/packages/react-styles/src/css/components/ModalAnimations/modal-animations.css index b22cb28b5e6..e03c3f0f98b 100644 --- a/packages/react-styles/src/css/components/ModalAnimations/modal-animations.css +++ b/packages/react-styles/src/css/components/ModalAnimations/modal-animations.css @@ -28,10 +28,6 @@ transform: translate3d(0, -24px, 0); } -/* TODO [Gustavo]: My local setup has reduced animations at the OS level which kicks this media query on. - Chrome doesn't have an easy way to FORCE animations on in this case so commenting this out for now. - It should be enabled if PR is merged in. */ -/* * @media screen and (prefers-reduced-motion: reduce) { .pf-v6-c-modal-animated, .pf-v6-c-modal-animated-open, @@ -39,4 +35,3 @@ transition: none; } } -/* */ From fa54f31d3aedb5c130a663fad75240cc79369a01 Mon Sep 17 00:00:00 2001 From: Gustavo Murcia Date: Thu, 9 Jul 2026 20:37:21 -0400 Subject: [PATCH 04/16] chore: PR feedback - Add missing onClose handler to ModalAnimated example - Add missing onClose handler to ModalAnimatedProvider example Generated-by: Claude Opus 4.6 Co-authored-by: Claude Opus 4.6 --- .../react-core/src/components/Modal/examples/ModalAnimated.tsx | 1 + .../src/components/Modal/examples/ModalAnimatedProvider.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/react-core/src/components/Modal/examples/ModalAnimated.tsx b/packages/react-core/src/components/Modal/examples/ModalAnimated.tsx index 79bd114b943..96a289e6c6f 100644 --- a/packages/react-core/src/components/Modal/examples/ModalAnimated.tsx +++ b/packages/react-core/src/components/Modal/examples/ModalAnimated.tsx @@ -17,6 +17,7 @@ export const ModalAnimated: React.FunctionComponent = () => { hasAnimations variant={ModalVariant.large} isOpen={isModalOpen} + onClose={handleModalToggle} aria-labelledby="modal-animated-label" aria-describedby="modal-animated-description" elementToFocus="#modal-animated-confirm-button" diff --git a/packages/react-core/src/components/Modal/examples/ModalAnimatedProvider.tsx b/packages/react-core/src/components/Modal/examples/ModalAnimatedProvider.tsx index eda7ef31c08..66a800e7acc 100644 --- a/packages/react-core/src/components/Modal/examples/ModalAnimatedProvider.tsx +++ b/packages/react-core/src/components/Modal/examples/ModalAnimatedProvider.tsx @@ -25,6 +25,7 @@ export const ModalAnimatedProvider: React.FunctionComponent = () => { Date: Mon, 13 Jul 2026 11:59:23 -0400 Subject: [PATCH 05/16] chore: PR feedback --- packages/react-core/src/components/Modal/examples/Modal.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/react-core/src/components/Modal/examples/Modal.md b/packages/react-core/src/components/Modal/examples/Modal.md index f2727587cf5..804d5428d0e 100644 --- a/packages/react-core/src/components/Modal/examples/Modal.md +++ b/packages/react-core/src/components/Modal/examples/Modal.md @@ -9,6 +9,7 @@ ouia: true import { Fragment, useRef, useState } from 'react'; import RhUiWarningIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-warning-icon'; import RhUiAttentionBellFillIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-attention-bell-fill-icon'; +import RhUiQuestionMarkCircleIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-question-mark-circle-icon'; import formStyles from '@patternfly/react-styles/css/components/Form/form'; ## Examples From ca4d8f3c684d82bc35898c6478d32d4f8683d572 Mon Sep 17 00:00:00 2001 From: Gustavo Murcia Date: Mon, 13 Jul 2026 13:04:27 -0400 Subject: [PATCH 06/16] chore: PR feedback --- .../src/components/Modal/examples/Modal.css | 81 +++++++++++++++++++ .../src/components/Modal/examples/Modal.md | 1 + 2 files changed, 82 insertions(+) create mode 100644 packages/react-core/src/components/Modal/examples/Modal.css diff --git a/packages/react-core/src/components/Modal/examples/Modal.css b/packages/react-core/src/components/Modal/examples/Modal.css new file mode 100644 index 00000000000..48d000ae169 --- /dev/null +++ b/packages/react-core/src/components/Modal/examples/Modal.css @@ -0,0 +1,81 @@ +/** + * Modal.css + * + * To fix an issue with the PR preview deployment, + * this file contains the contents of the new modal-animations.css + backdrop-animations.css + */ + + +/* --- modal-animations.css ------ */ + +.pf-v6-c-modal-animated { + --pf-v6-c-modal-animated--Transition: + opacity 240ms cubic-bezier(0.4, 0.14, 1, 1), + transform 240ms cubic-bezier(0.4, 0.14, 1, 1), + visibility 0ms linear 240ms; + opacity: 0; + visibility: hidden; + pointer-events: none; + transform: translate3d(0, -24px, 0); + transform-origin: top center; + transition: var(--pf-v6-c-modal-animated--Transition); +} + +.pf-v6-c-modal-animated-open { + --pf-v6-c-modal-animated--Transition: + transform 240ms cubic-bezier(0, 0, 0.2, 1), + visibility 0ms linear 0ms; + opacity: 1; + visibility: visible; + pointer-events: unset; + transform: translate3d(0, 0, 0); +} + +.pf-v6-c-modal-animated-closed { + opacity: 0; + visibility: hidden; + pointer-events: none; + transform: translate3d(0, -24px, 0); +} + +@media screen and (prefers-reduced-motion: reduce) { + .pf-v6-c-modal-animated, + .pf-v6-c-modal-animated-open, + .pf-v6-c-modal-animated-closed { + transition: none; + } +} + +/* --- backdrop-animations.css --- */ + +.pf-v6-c-backdrop-animated { + background-color: transparent; + visibility: hidden; + pointer-events: none; + transition: + background-color 240ms cubic-bezier(0.4, 0.14, 1, 1), /* Carbon equivalent: duration-moderate-02 + motion(exit, expressive) */ + visibility 0ms linear 240ms; /* Carbon equivalent: duration-moderate-02 */ +} + +.pf-v6-c-backdrop-animated-visible { + background-color: var(--pf-v6-c-backdrop--BackgroundColor); + visibility: visible; + pointer-events: unset; + transition: + background-color 240ms cubic-bezier(0, 0, 0.2, 1), + visibility 0ms linear 0ms; +} + +.pf-v6-c-backdrop-animated-hidden { + background-color: transparent; + visibility: hidden; + pointer-events: none; +} + +@media screen and (prefers-reduced-motion: reduce) { + .pf-v6-c-backdrop-animated, + .pf-v6-c-backdrop-animated-visible, + .pf-v6-c-backdrop-animated-hidden { + transition: none; + } +} diff --git a/packages/react-core/src/components/Modal/examples/Modal.md b/packages/react-core/src/components/Modal/examples/Modal.md index 804d5428d0e..e8ffe61e9e9 100644 --- a/packages/react-core/src/components/Modal/examples/Modal.md +++ b/packages/react-core/src/components/Modal/examples/Modal.md @@ -11,6 +11,7 @@ import RhUiWarningIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-warnin import RhUiAttentionBellFillIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-attention-bell-fill-icon'; import RhUiQuestionMarkCircleIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-question-mark-circle-icon'; import formStyles from '@patternfly/react-styles/css/components/Form/form'; +import './Modal.css'; ## Examples From cf29347221688b2e1bf94f0a696a024b2559b79e Mon Sep 17 00:00:00 2001 From: Michael Coker <35148959+mcoker@users.noreply.github.com> Date: Mon, 21 Sep 2026 14:21:34 -0500 Subject: [PATCH 07/16] chore: updates to support new CSS (#1) --- .../src/components/Backdrop/Backdrop.tsx | 6 +- .../src/components/Modal/ModalBox.tsx | 7 +- .../src/components/Modal/ModalContent.tsx | 33 +++++++- .../src/components/Modal/examples/Modal.css | 81 ------------------- .../src/components/Modal/examples/Modal.md | 1 - 5 files changed, 36 insertions(+), 92 deletions(-) delete mode 100644 packages/react-core/src/components/Modal/examples/Modal.css diff --git a/packages/react-core/src/components/Backdrop/Backdrop.tsx b/packages/react-core/src/components/Backdrop/Backdrop.tsx index 3dea142179b..8d38edea35e 100644 --- a/packages/react-core/src/components/Backdrop/Backdrop.tsx +++ b/packages/react-core/src/components/Backdrop/Backdrop.tsx @@ -1,6 +1,5 @@ import { css } from '@patternfly/react-styles'; import styles from '@patternfly/react-styles/css/components/Backdrop/backdrop'; -import stylesAnimated from '@patternfly/react-styles/css/components/BackdropAnimations/backdrop-animations'; import { useHasAnimations } from '../../helpers'; export interface BackdropProps extends React.HTMLProps { @@ -28,9 +27,8 @@ export const Backdrop: React.FunctionComponent = ({ {...props} className={css( styles.backdrop, - hasAnimations && stylesAnimated.backdropAnimated, - hasAnimations && isVisible === true && stylesAnimated.backdropAnimatedVisible, - hasAnimations && isVisible !== true && stylesAnimated.backdropAnimatedHidden, + hasAnimations && styles.modifiers.animate, + hasAnimations && isVisible === true && styles.modifiers.show, className )} > diff --git a/packages/react-core/src/components/Modal/ModalBox.tsx b/packages/react-core/src/components/Modal/ModalBox.tsx index 17d8a0b3a8d..93c65932af1 100644 --- a/packages/react-core/src/components/Modal/ModalBox.tsx +++ b/packages/react-core/src/components/Modal/ModalBox.tsx @@ -1,6 +1,6 @@ import { css } from '@patternfly/react-styles'; import styles from '@patternfly/react-styles/css/components/ModalBox/modal-box'; -import stylesAnimated from '@patternfly/react-styles/css/components/ModalAnimations/modal-animations'; +// import stylesAnimated from '@patternfly/react-styles/css/components/ModalAnimations/modal-animations'; import topSpacer from '@patternfly/react-tokens/dist/esm/c_modal_box_m_align_top_spacer'; export interface ModalBoxProps extends React.HTMLProps { @@ -53,9 +53,8 @@ export const ModalBox: React.FunctionComponent = ({ aria-modal="true" className={css( styles.modalBox, - hasAnimations && stylesAnimated.modalAnimated, - hasAnimations && isOpen === true && stylesAnimated.modalAnimatedOpen, - hasAnimations && isOpen !== true && stylesAnimated.modalAnimatedClosed, + hasAnimations && styles.modifiers.animate, + hasAnimations && isOpen === true && styles.modifiers.open, className, position === 'top' && styles.modifiers.alignTop, variant === 'large' && styles.modifiers.lg, diff --git a/packages/react-core/src/components/Modal/ModalContent.tsx b/packages/react-core/src/components/Modal/ModalContent.tsx index 499959b4ab9..bf38aa0dc0f 100644 --- a/packages/react-core/src/components/Modal/ModalContent.tsx +++ b/packages/react-core/src/components/Modal/ModalContent.tsx @@ -1,3 +1,4 @@ +import { useEffect, useState } from 'react'; import { FocusTrap } from '../../helpers'; import bullsEyeStyles from '@patternfly/react-styles/css/layouts/Bullseye/bullseye'; import { css } from '@patternfly/react-styles'; @@ -78,8 +79,20 @@ export const ModalContent: React.FunctionComponent = ({ ...props }: ModalContentProps) => { const hasAnimations = useHasAnimations(hasAnimationsProp); + // Keeps the modal in the DOM while the close animation runs. When animations are enabled we defer + // unmounting until the backdrop's transition ends (see onTransitionEnd below) instead of removing + // it immediately when isOpen becomes false. + const [isRendered, setIsRendered] = useState(isOpen); - if (!isOpen && !hasAnimations) { + useEffect(() => { + if (isOpen) { + setIsRendered(true); + } else if (!hasAnimations) { + setIsRendered(false); + } + }, [isOpen, hasAnimations]); + + if (!isRendered) { return null; } @@ -126,7 +139,23 @@ export const ModalContent: React.FunctionComponent = ({ } return ( - + { + // Only unmount once the backdrop's own closing transition finishes. Guarding on the + // target prevents bubbled transitions from child elements from triggering this early. + if (!isOpen && event.target === event.currentTarget) { + setIsRendered(false); + } + } + : undefined + } + > Date: Mon, 21 Sep 2026 15:37:16 -0400 Subject: [PATCH 08/16] chore: Update to @patternfly/patternfly@npm:6.6.0-prerelease.44 --- packages/react-core/package.json | 2 +- packages/react-docs/package.json | 2 +- packages/react-icons/package.json | 2 +- packages/react-styles/package.json | 2 +- packages/react-tokens/package.json | 2 +- yarn.lock | 18 +++++++++--------- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/react-core/package.json b/packages/react-core/package.json index b2f88c56783..354216e5f31 100644 --- a/packages/react-core/package.json +++ b/packages/react-core/package.json @@ -54,7 +54,7 @@ "tslib": "^2.8.1" }, "devDependencies": { - "@patternfly/patternfly": "6.6.0-prerelease.41", + "@patternfly/patternfly": "6.6.0-prerelease.44", "case-anything": "^3.1.2", "css": "^3.0.0", "fs-extra": "^11.3.3" diff --git a/packages/react-docs/package.json b/packages/react-docs/package.json index f8beabfe2d4..9cffefb6924 100644 --- a/packages/react-docs/package.json +++ b/packages/react-docs/package.json @@ -23,7 +23,7 @@ "test:a11y": "patternfly-a11y --config patternfly-a11y.config" }, "dependencies": { - "@patternfly/patternfly": "6.6.0-prerelease.41", + "@patternfly/patternfly": "6.6.0-prerelease.44", "@patternfly/react-charts": "workspace:^", "@patternfly/react-code-editor": "workspace:^", "@patternfly/react-core": "workspace:^", diff --git a/packages/react-icons/package.json b/packages/react-icons/package.json index 52ada9259eb..57662b9db9b 100644 --- a/packages/react-icons/package.json +++ b/packages/react-icons/package.json @@ -38,7 +38,7 @@ "@fortawesome/free-brands-svg-icons": "^5.15.4", "@fortawesome/free-regular-svg-icons": "^5.15.4", "@fortawesome/free-solid-svg-icons": "^5.15.4", - "@patternfly/patternfly": "6.6.0-prerelease.41", + "@patternfly/patternfly": "6.6.0-prerelease.44", "@rhds/icons": "^2.3.1", "fs-extra": "^11.3.3" }, diff --git a/packages/react-styles/package.json b/packages/react-styles/package.json index 93bb7b055ff..1ea83b7f569 100644 --- a/packages/react-styles/package.json +++ b/packages/react-styles/package.json @@ -19,7 +19,7 @@ "clean": "rimraf dist css" }, "devDependencies": { - "@patternfly/patternfly": "6.6.0-prerelease.41", + "@patternfly/patternfly": "6.6.0-prerelease.44", "change-case": "^5.4.4", "fs-extra": "^11.3.3" }, diff --git a/packages/react-tokens/package.json b/packages/react-tokens/package.json index 1394764ec77..abd004ad907 100644 --- a/packages/react-tokens/package.json +++ b/packages/react-tokens/package.json @@ -30,7 +30,7 @@ }, "devDependencies": { "@adobe/css-tools": "^4.4.4", - "@patternfly/patternfly": "6.6.0-prerelease.41", + "@patternfly/patternfly": "6.6.0-prerelease.44", "fs-extra": "^11.3.3" } } diff --git a/yarn.lock b/yarn.lock index bd7e19b785b..edaac195630 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5070,10 +5070,10 @@ __metadata: languageName: node linkType: hard -"@patternfly/patternfly@npm:6.6.0-prerelease.41": - version: 6.6.0-prerelease.41 - resolution: "@patternfly/patternfly@npm:6.6.0-prerelease.41" - checksum: 10c0/1b9fa30c2a8b5713d5bcbfc8194dcfff85c3cdc1028eb0b44945b8bbdd8e98d9cb348e2cd4be85de6784c595f79e6a9f0a839e4f1b58ac6004405cf956052b42 +"@patternfly/patternfly@npm:6.6.0-prerelease.44": + version: 6.6.0-prerelease.44 + resolution: "@patternfly/patternfly@npm:6.6.0-prerelease.44" + checksum: 10c0/4a8ba21c3c9c2979a7fed14d9d9a1f7c99e89c9ff81feef3c061e47521de4f846464761b1117838f87a447ddc71e966f08e13a129324c6ea8f169178a6738736 languageName: node linkType: hard @@ -5171,7 +5171,7 @@ __metadata: version: 0.0.0-use.local resolution: "@patternfly/react-core@workspace:packages/react-core" dependencies: - "@patternfly/patternfly": "npm:6.6.0-prerelease.41" + "@patternfly/patternfly": "npm:6.6.0-prerelease.44" "@patternfly/react-icons": "workspace:^" "@patternfly/react-styles": "workspace:^" "@patternfly/react-tokens": "workspace:^" @@ -5192,7 +5192,7 @@ __metadata: resolution: "@patternfly/react-docs@workspace:packages/react-docs" dependencies: "@patternfly/documentation-framework": "npm:^6.40.0" - "@patternfly/patternfly": "npm:6.6.0-prerelease.41" + "@patternfly/patternfly": "npm:6.6.0-prerelease.44" "@patternfly/patternfly-a11y": "npm:5.2.1" "@patternfly/react-charts": "workspace:^" "@patternfly/react-code-editor": "workspace:^" @@ -5232,7 +5232,7 @@ __metadata: "@fortawesome/free-brands-svg-icons": "npm:^5.15.4" "@fortawesome/free-regular-svg-icons": "npm:^5.15.4" "@fortawesome/free-solid-svg-icons": "npm:^5.15.4" - "@patternfly/patternfly": "npm:6.6.0-prerelease.41" + "@patternfly/patternfly": "npm:6.6.0-prerelease.44" "@rhds/icons": "npm:^2.3.1" fs-extra: "npm:^11.3.3" tslib: "npm:^2.8.1" @@ -5319,7 +5319,7 @@ __metadata: version: 0.0.0-use.local resolution: "@patternfly/react-styles@workspace:packages/react-styles" dependencies: - "@patternfly/patternfly": "npm:6.6.0-prerelease.41" + "@patternfly/patternfly": "npm:6.6.0-prerelease.44" change-case: "npm:^5.4.4" fs-extra: "npm:^11.3.3" languageName: unknown @@ -5361,7 +5361,7 @@ __metadata: resolution: "@patternfly/react-tokens@workspace:packages/react-tokens" dependencies: "@adobe/css-tools": "npm:^4.4.4" - "@patternfly/patternfly": "npm:6.6.0-prerelease.41" + "@patternfly/patternfly": "npm:6.6.0-prerelease.44" fs-extra: "npm:^11.3.3" languageName: unknown linkType: soft From 58ab3a71e9b4ae57f0e81d492271ccf4893aece7 Mon Sep 17 00:00:00 2001 From: Gustavo Murcia Date: Mon, 21 Sep 2026 16:15:43 -0400 Subject: [PATCH 09/16] fix(Backdrop): Default isVisible to true for standalone backdrops. Generated-by: GPT-5.6 Sol Co-authored-by: GPT-5.6 Sol --- .../src/components/Backdrop/Backdrop.tsx | 6 ++--- .../Backdrop/__tests__/Backdrop.test.tsx | 27 +++++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/packages/react-core/src/components/Backdrop/Backdrop.tsx b/packages/react-core/src/components/Backdrop/Backdrop.tsx index 8d38edea35e..c2bc7277d0a 100644 --- a/packages/react-core/src/components/Backdrop/Backdrop.tsx +++ b/packages/react-core/src/components/Backdrop/Backdrop.tsx @@ -9,7 +9,7 @@ export interface BackdropProps extends React.HTMLProps { className?: string; /** Flag indicating whether animations are enabled. */ hasAnimations?: boolean; - /** Flag to show the backdrop. Used in conjunction with `hasAnimations`. */ + /** Flag to show the backdrop when animations are enabled. Set to false while the backdrop remains mounted to play its exit transition. */ isVisible?: boolean; } @@ -17,7 +17,7 @@ export const Backdrop: React.FunctionComponent = ({ children = null, className = '', hasAnimations: hasAnimationsProp, - isVisible, + isVisible = true, ...props }: BackdropProps) => { const hasAnimations = useHasAnimations(hasAnimationsProp); @@ -28,7 +28,7 @@ export const Backdrop: React.FunctionComponent = ({ className={css( styles.backdrop, hasAnimations && styles.modifiers.animate, - hasAnimations && isVisible === true && styles.modifiers.show, + hasAnimations && isVisible && styles.modifiers.show, className )} > diff --git a/packages/react-core/src/components/Backdrop/__tests__/Backdrop.test.tsx b/packages/react-core/src/components/Backdrop/__tests__/Backdrop.test.tsx index 927bf1a4c44..9e50cd9a42a 100644 --- a/packages/react-core/src/components/Backdrop/__tests__/Backdrop.test.tsx +++ b/packages/react-core/src/components/Backdrop/__tests__/Backdrop.test.tsx @@ -26,6 +26,33 @@ test(`Renders with only the class ${styles.backdrop} by default`, () => { expect(screen.getByText('Test')).toHaveClass(styles.backdrop, { exact: true }); }); +test('Renders as visible by default when animations are enabled', () => { + render(Test); + expect(screen.getByText('Test')).toHaveClass(styles.backdrop, styles.modifiers.animate, styles.modifiers.show, { + exact: true + }); +}); + +test('Renders as hidden when animations are enabled and isVisible is false', () => { + render( + + Test + + ); + expect(screen.getByText('Test')).toHaveClass(styles.backdrop, styles.modifiers.animate, { exact: true }); +}); + +test('Renders as visible when animations are enabled and isVisible is true', () => { + render( + + Test + + ); + expect(screen.getByText('Test')).toHaveClass(styles.backdrop, styles.modifiers.animate, styles.modifiers.show, { + exact: true + }); +}); + test('Renders with custom class name when className prop is passed', () => { render(Test); expect(screen.getByText('Test')).toHaveClass('test-class'); From 811e1cc6b5e6f39675d111bf03e583654b634b71 Mon Sep 17 00:00:00 2001 From: Gustavo Murcia Date: Tue, 22 Sep 2026 12:54:40 -0400 Subject: [PATCH 10/16] fix(ModalContent): Hide the dialog during its closing transition. Generated-by: GPT-5.6 Sol Co-authored-by: GPT-5.6 Sol --- .../src/components/Modal/ModalContent.tsx | 1 + .../Modal/__tests__/ModalContent.test.tsx | 25 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/packages/react-core/src/components/Modal/ModalContent.tsx b/packages/react-core/src/components/Modal/ModalContent.tsx index bf38aa0dc0f..7fde36e0e2c 100644 --- a/packages/react-core/src/components/Modal/ModalContent.tsx +++ b/packages/react-core/src/components/Modal/ModalContent.tsx @@ -144,6 +144,7 @@ export const ModalContent: React.FunctionComponent = ({ id={backdropId} hasAnimations={hasAnimations} isVisible={isOpen} + aria-hidden={hasAnimations && !isOpen ? true : undefined} onTransitionEnd={ hasAnimations ? (event) => { diff --git a/packages/react-core/src/components/Modal/__tests__/ModalContent.test.tsx b/packages/react-core/src/components/Modal/__tests__/ModalContent.test.tsx index c75a3eadecb..0f60ee3a187 100644 --- a/packages/react-core/src/components/Modal/__tests__/ModalContent.test.tsx +++ b/packages/react-core/src/components/Modal/__tests__/ModalContent.test.tsx @@ -59,3 +59,28 @@ test('Modal content can add id to focus trap correctly for use with dropdowns', 'pf-v6-l-bullseye' ); }); + +test('Modal content is hidden from assistive technologies during its closing animation', () => { + const { rerender } = render( + + This is a ModalBox header + + ); + const backdrop = document.getElementById('backdropId'); + + expect(backdrop).not.toHaveAttribute('aria-hidden'); + + rerender( + + This is a ModalBox header + + ); + expect(backdrop).toHaveAttribute('aria-hidden', 'true'); + + rerender( + + This is a ModalBox header + + ); + expect(backdrop).not.toHaveAttribute('aria-hidden'); +}); From 4ca62ea3a185be888a472eaa1d94a9cb819d97da Mon Sep 17 00:00:00 2001 From: Gustavo Murcia Date: Tue, 22 Sep 2026 13:03:07 -0400 Subject: [PATCH 11/16] fix(ModalContent): Complete the close without transitionend when reduced motion is active Generated-by: GPT-5.6 Sol Co-authored-by: GPT-5.6 Sol --- .../src/components/Modal/ModalContent.tsx | 12 ++++- .../Modal/__tests__/ModalContent.test.tsx | 46 ++++++++++++++++++- 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/packages/react-core/src/components/Modal/ModalContent.tsx b/packages/react-core/src/components/Modal/ModalContent.tsx index 7fde36e0e2c..4671210fe97 100644 --- a/packages/react-core/src/components/Modal/ModalContent.tsx +++ b/packages/react-core/src/components/Modal/ModalContent.tsx @@ -7,6 +7,8 @@ import { Backdrop } from '../Backdrop'; import { ModalBoxCloseButton } from './ModalBoxCloseButton'; import { ModalBox } from './ModalBox'; +const transitionEndFallbackDelay = 300; + export interface ModalContentProps extends OUIAProps { /** Id to use for the modal box description. This should match the ModalHeader labelId or descriptorId. */ 'aria-describedby'?: string; @@ -87,10 +89,16 @@ export const ModalContent: React.FunctionComponent = ({ useEffect(() => { if (isOpen) { setIsRendered(true); - } else if (!hasAnimations) { + } else if (!isRendered) { + return; + } else if (!hasAnimations || window.matchMedia?.('(prefers-reduced-motion: reduce)').matches) { setIsRendered(false); + } else { + // Ensure the modal is removed if CSS transitions are disabled or transitionend does not fire. + const transitionEndFallback = window.setTimeout(() => setIsRendered(false), transitionEndFallbackDelay); + return () => window.clearTimeout(transitionEndFallback); } - }, [isOpen, hasAnimations]); + }, [isOpen, hasAnimations, isRendered]); if (!isRendered) { return null; diff --git a/packages/react-core/src/components/Modal/__tests__/ModalContent.test.tsx b/packages/react-core/src/components/Modal/__tests__/ModalContent.test.tsx index 0f60ee3a187..a7bada1d474 100644 --- a/packages/react-core/src/components/Modal/__tests__/ModalContent.test.tsx +++ b/packages/react-core/src/components/Modal/__tests__/ModalContent.test.tsx @@ -1,4 +1,4 @@ -import { render, screen } from '@testing-library/react'; +import { act, render, screen } from '@testing-library/react'; import { ModalContent } from '../ModalContent'; @@ -84,3 +84,47 @@ test('Modal content is hidden from assistive technologies during its closing ani ); expect(backdrop).not.toHaveAttribute('aria-hidden'); }); + +test('Modal content unmounts if its closing transition does not end', () => { + jest.useFakeTimers(); + try { + const { rerender } = render( + + This is a ModalBox header + + ); + + rerender( + + This is a ModalBox header + + ); + expect(document.getElementById('backdropId')).toBeInTheDocument(); + + act(() => jest.runOnlyPendingTimers()); + expect(document.getElementById('backdropId')).not.toBeInTheDocument(); + } finally { + jest.useRealTimers(); + } +}); + +test('Modal content unmounts immediately with reduced motion', () => { + const matchMedia = window.matchMedia; + window.matchMedia = jest.fn().mockReturnValue({ matches: true } as MediaQueryList); + try { + const { rerender } = render( + + This is a ModalBox header + + ); + + rerender( + + This is a ModalBox header + + ); + expect(document.getElementById('backdropId')).not.toBeInTheDocument(); + } finally { + window.matchMedia = matchMedia; + } +}); From 278653c1d9b84896dbece2a9da0e3b7d89aa1550 Mon Sep 17 00:00:00 2001 From: Gustavo Murcia Date: Tue, 22 Sep 2026 13:18:12 -0400 Subject: [PATCH 12/16] refactor(Modal): Enable animations on by default Generated-by: GPT-5.6 Sol Co-authored-by: GPT-5.6 Sol --- .../src/components/Backdrop/Backdrop.tsx | 4 ++-- .../Backdrop/__tests__/Backdrop.test.tsx | 9 ++++++++- .../__snapshots__/Backdrop.test.tsx.snap | 2 +- .../react-core/src/components/Modal/Modal.tsx | 5 +++-- .../components/Modal/__tests__/Modal.test.tsx | 14 ++++++++++++++ .../__snapshots__/ModalContent.test.tsx.snap | 16 ++++++++-------- 6 files changed, 36 insertions(+), 14 deletions(-) diff --git a/packages/react-core/src/components/Backdrop/Backdrop.tsx b/packages/react-core/src/components/Backdrop/Backdrop.tsx index c2bc7277d0a..a3860cfa5c9 100644 --- a/packages/react-core/src/components/Backdrop/Backdrop.tsx +++ b/packages/react-core/src/components/Backdrop/Backdrop.tsx @@ -7,7 +7,7 @@ export interface BackdropProps extends React.HTMLProps { children?: React.ReactNode; /** Additional classes added to the backdrop */ className?: string; - /** Flag indicating whether animations are enabled. */ + /** Flag indicating whether animations are enabled. Animations are enabled by default. */ hasAnimations?: boolean; /** Flag to show the backdrop when animations are enabled. Set to false while the backdrop remains mounted to play its exit transition. */ isVisible?: boolean; @@ -16,7 +16,7 @@ export interface BackdropProps extends React.HTMLProps { export const Backdrop: React.FunctionComponent = ({ children = null, className = '', - hasAnimations: hasAnimationsProp, + hasAnimations: hasAnimationsProp = true, isVisible = true, ...props }: BackdropProps) => { diff --git a/packages/react-core/src/components/Backdrop/__tests__/Backdrop.test.tsx b/packages/react-core/src/components/Backdrop/__tests__/Backdrop.test.tsx index 9e50cd9a42a..522d7555843 100644 --- a/packages/react-core/src/components/Backdrop/__tests__/Backdrop.test.tsx +++ b/packages/react-core/src/components/Backdrop/__tests__/Backdrop.test.tsx @@ -21,8 +21,15 @@ test(`Renders with the ${styles.backdrop}`, () => { expect(screen.getByText('Test')).toHaveClass(styles.backdrop); }); -test(`Renders with only the class ${styles.backdrop} by default`, () => { +test('Renders with animations enabled by default', () => { render(Test); + expect(screen.getByText('Test')).toHaveClass(styles.backdrop, styles.modifiers.animate, styles.modifiers.show, { + exact: true + }); +}); + +test('Renders without animation classes when animations are explicitly disabled', () => { + render(Test); expect(screen.getByText('Test')).toHaveClass(styles.backdrop, { exact: true }); }); diff --git a/packages/react-core/src/components/Backdrop/__tests__/__snapshots__/Backdrop.test.tsx.snap b/packages/react-core/src/components/Backdrop/__tests__/__snapshots__/Backdrop.test.tsx.snap index 9c7c1fe815e..352fd75e515 100644 --- a/packages/react-core/src/components/Backdrop/__tests__/__snapshots__/Backdrop.test.tsx.snap +++ b/packages/react-core/src/components/Backdrop/__tests__/__snapshots__/Backdrop.test.tsx.snap @@ -3,7 +3,7 @@ exports[`Matches the snapshot 1`] = `
Backdrop
diff --git a/packages/react-core/src/components/Modal/Modal.tsx b/packages/react-core/src/components/Modal/Modal.tsx index a5786bef957..ff8bae6a45a 100644 --- a/packages/react-core/src/components/Modal/Modal.tsx +++ b/packages/react-core/src/components/Modal/Modal.tsx @@ -53,7 +53,7 @@ export interface ModalProps extends React.HTMLProps, OUIAProps { ouiaId?: number | string; /** Set the value of data-ouia-safe. Only set to true when the component is in a static state, i.e. no animations are occurring. At all other times, this value must be false. */ ouiaSafe?: boolean; - /** Flag indicating whether animations are enabled. */ + /** Flag indicating whether animations are enabled. Animations are enabled by default. */ hasAnimations?: boolean; } @@ -80,7 +80,8 @@ class Modal extends Component { variant: 'default', appendTo: () => document.body, ouiaSafe: true, - position: 'default' + position: 'default', + hasAnimations: true }; constructor(props: ModalProps) { diff --git a/packages/react-core/src/components/Modal/__tests__/Modal.test.tsx b/packages/react-core/src/components/Modal/__tests__/Modal.test.tsx index f4c4ea87feb..a0d1ec0a7f2 100644 --- a/packages/react-core/src/components/Modal/__tests__/Modal.test.tsx +++ b/packages/react-core/src/components/Modal/__tests__/Modal.test.tsx @@ -111,6 +111,20 @@ describe('Modal', () => { expect(document.body).toHaveClass(css(styles.backdropOpen)); }); + test('modal has animations enabled by default', () => { + render(); + const backdrop = screen.getByText('modal content').closest(`.${styles.backdrop}`); + + expect(backdrop).toHaveClass(styles.modifiers.animate, styles.modifiers.show); + }); + + test('modal animations can be explicitly disabled', () => { + render(); + const backdrop = screen.getByText('modal content').closest(`.${styles.backdrop}`); + + expect(backdrop).not.toHaveClass(styles.modifiers.animate, styles.modifiers.show); + }); + test('modal has no body backdropOpen class when not open', () => { render(); expect(document.body).not.toHaveClass(css(styles.backdropOpen)); diff --git a/packages/react-core/src/deprecated/components/Modal/__tests__/__snapshots__/ModalContent.test.tsx.snap b/packages/react-core/src/deprecated/components/Modal/__tests__/__snapshots__/ModalContent.test.tsx.snap index 96ccbb5cac3..c660267b743 100644 --- a/packages/react-core/src/deprecated/components/Modal/__tests__/__snapshots__/ModalContent.test.tsx.snap +++ b/packages/react-core/src/deprecated/components/Modal/__tests__/__snapshots__/ModalContent.test.tsx.snap @@ -3,7 +3,7 @@ exports[`Modal Content Test description 1`] = `
Date: Tue, 22 Sep 2026 14:27:50 -0400 Subject: [PATCH 13/16] removed the reduced-motion shortcut that immediately unmounted closing modals. Modals now remain mounted until the backdrop transition ends, with a 300ms fallback Generated-by: GPT-5.6 Sol Co-authored-by: GPT-5.6 Sol --- packages/react-core/src/components/Modal/ModalContent.tsx | 2 +- .../src/components/Modal/__tests__/ModalContent.test.tsx | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/react-core/src/components/Modal/ModalContent.tsx b/packages/react-core/src/components/Modal/ModalContent.tsx index 4671210fe97..a76b9a9838a 100644 --- a/packages/react-core/src/components/Modal/ModalContent.tsx +++ b/packages/react-core/src/components/Modal/ModalContent.tsx @@ -91,7 +91,7 @@ export const ModalContent: React.FunctionComponent = ({ setIsRendered(true); } else if (!isRendered) { return; - } else if (!hasAnimations || window.matchMedia?.('(prefers-reduced-motion: reduce)').matches) { + } else if (!hasAnimations) { setIsRendered(false); } else { // Ensure the modal is removed if CSS transitions are disabled or transitionend does not fire. diff --git a/packages/react-core/src/components/Modal/__tests__/ModalContent.test.tsx b/packages/react-core/src/components/Modal/__tests__/ModalContent.test.tsx index a7bada1d474..e13e81da9b6 100644 --- a/packages/react-core/src/components/Modal/__tests__/ModalContent.test.tsx +++ b/packages/react-core/src/components/Modal/__tests__/ModalContent.test.tsx @@ -108,9 +108,10 @@ test('Modal content unmounts if its closing transition does not end', () => { } }); -test('Modal content unmounts immediately with reduced motion', () => { +test('Modal content remains mounted during close when reduced motion is preferred', () => { const matchMedia = window.matchMedia; window.matchMedia = jest.fn().mockReturnValue({ matches: true } as MediaQueryList); + jest.useFakeTimers(); try { const { rerender } = render( @@ -123,8 +124,12 @@ test('Modal content unmounts immediately with reduced motion', () => { This is a ModalBox header ); + expect(document.getElementById('backdropId')).toBeInTheDocument(); + + act(() => jest.runOnlyPendingTimers()); expect(document.getElementById('backdropId')).not.toBeInTheDocument(); } finally { + jest.useRealTimers(); window.matchMedia = matchMedia; } }); From 526bde099f5b41b8f13dae01a68c511bfb138b42 Mon Sep 17 00:00:00 2001 From: Gustavo Murcia Date: Tue, 22 Sep 2026 14:46:52 -0400 Subject: [PATCH 14/16] chore: Add disabled animations modal example --- .../src/components/Modal/examples/Modal.md | 8 ++++ .../Modal/examples/ModalNotAnimated.tsx | 44 +++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 packages/react-core/src/components/Modal/examples/ModalNotAnimated.tsx diff --git a/packages/react-core/src/components/Modal/examples/Modal.md b/packages/react-core/src/components/Modal/examples/Modal.md index 804d5428d0e..2fa9cea6052 100644 --- a/packages/react-core/src/components/Modal/examples/Modal.md +++ b/packages/react-core/src/components/Modal/examples/Modal.md @@ -170,3 +170,11 @@ To enable animations globally, wrap your application with `AnimationsProvider`. ```ts file="./ModalAnimatedProvider.tsx" ``` + +### Not animated modal + +To explicitly disable animations, set the `hasAnimations` property to `false` on the modal. + +```ts file="./ModalNotAnimated.tsx" + +``` diff --git a/packages/react-core/src/components/Modal/examples/ModalNotAnimated.tsx b/packages/react-core/src/components/Modal/examples/ModalNotAnimated.tsx new file mode 100644 index 00000000000..85060793a9e --- /dev/null +++ b/packages/react-core/src/components/Modal/examples/ModalNotAnimated.tsx @@ -0,0 +1,44 @@ +import { Fragment, useState } from 'react'; +import { Button, Modal, ModalHeader, ModalBody, ModalFooter, ModalVariant } from '@patternfly/react-core'; + +export const ModalNotAnimated: React.FunctionComponent = () => { + const [isModalOpen, setIsModalOpen] = useState(false); + + const handleModalToggle = (_event: KeyboardEvent | React.MouseEvent) => { + setIsModalOpen((prevIsModalOpen) => !prevIsModalOpen); + }; + + return ( + + + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore + magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo + consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla + pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. + + + + + + + + ); +}; From b2c33203cd34b7c8dcb6422d80627d4967a09123 Mon Sep 17 00:00:00 2001 From: Gustavo Andres Murcia Date: Wed, 23 Sep 2026 17:03:12 -0400 Subject: [PATCH 15/16] chore: Remove unnecessary styles (PR feedback) Update packages/react-core/src/components/Modal/ModalBox.tsx From: @mcoker From: @ Co-authored-by: Michael Coker <35148959+mcoker@users.noreply.github.com> --- packages/react-core/src/components/Modal/ModalBox.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/react-core/src/components/Modal/ModalBox.tsx b/packages/react-core/src/components/Modal/ModalBox.tsx index 93c65932af1..c015460d18b 100644 --- a/packages/react-core/src/components/Modal/ModalBox.tsx +++ b/packages/react-core/src/components/Modal/ModalBox.tsx @@ -1,6 +1,5 @@ import { css } from '@patternfly/react-styles'; import styles from '@patternfly/react-styles/css/components/ModalBox/modal-box'; -// import stylesAnimated from '@patternfly/react-styles/css/components/ModalAnimations/modal-animations'; import topSpacer from '@patternfly/react-tokens/dist/esm/c_modal_box_m_align_top_spacer'; export interface ModalBoxProps extends React.HTMLProps { From 6207f26f6289c349fc90eff90012a788b6fe8e0c Mon Sep 17 00:00:00 2001 From: Gustavo Murcia Date: Wed, 23 Sep 2026 17:05:17 -0400 Subject: [PATCH 16/16] chore: Remove unnecessary styles --- .../backdrop-animations.css | 31 ---------------- .../ModalAnimations/modal-animations.css | 37 ------------------- 2 files changed, 68 deletions(-) delete mode 100644 packages/react-styles/src/css/components/BackdropAnimations/backdrop-animations.css delete mode 100644 packages/react-styles/src/css/components/ModalAnimations/modal-animations.css diff --git a/packages/react-styles/src/css/components/BackdropAnimations/backdrop-animations.css b/packages/react-styles/src/css/components/BackdropAnimations/backdrop-animations.css deleted file mode 100644 index 4ca23410911..00000000000 --- a/packages/react-styles/src/css/components/BackdropAnimations/backdrop-animations.css +++ /dev/null @@ -1,31 +0,0 @@ -.pf-v6-c-backdrop-animated { - background-color: transparent; - visibility: hidden; - pointer-events: none; - transition: - background-color 240ms cubic-bezier(0.4, 0.14, 1, 1), /* Carbon equivalent: duration-moderate-02 + motion(exit, expressive) */ - visibility 0ms linear 240ms; /* Carbon equivalent: duration-moderate-02 */ -} - -.pf-v6-c-backdrop-animated-visible { - background-color: var(--pf-v6-c-backdrop--BackgroundColor); - visibility: visible; - pointer-events: unset; - transition: - background-color 240ms cubic-bezier(0, 0, 0.2, 1), - visibility 0ms linear 0ms; -} - -.pf-v6-c-backdrop-animated-hidden { - background-color: transparent; - visibility: hidden; - pointer-events: none; -} - -@media screen and (prefers-reduced-motion: reduce) { - .pf-v6-c-backdrop-animated, - .pf-v6-c-backdrop-animated-visible, - .pf-v6-c-backdrop-animated-hidden { - transition: none; - } -} diff --git a/packages/react-styles/src/css/components/ModalAnimations/modal-animations.css b/packages/react-styles/src/css/components/ModalAnimations/modal-animations.css deleted file mode 100644 index e03c3f0f98b..00000000000 --- a/packages/react-styles/src/css/components/ModalAnimations/modal-animations.css +++ /dev/null @@ -1,37 +0,0 @@ -.pf-v6-c-modal-animated { - --pf-v6-c-modal-animated--Transition: - opacity 240ms cubic-bezier(0.4, 0.14, 1, 1), - transform 240ms cubic-bezier(0.4, 0.14, 1, 1), - visibility 0ms linear 240ms; - opacity: 0; - visibility: hidden; - pointer-events: none; - transform: translate3d(0, -24px, 0); - transform-origin: top center; - transition: var(--pf-v6-c-modal-animated--Transition); -} - -.pf-v6-c-modal-animated-open { - --pf-v6-c-modal-animated--Transition: - transform 240ms cubic-bezier(0, 0, 0.2, 1), - visibility 0ms linear 0ms; - opacity: 1; - visibility: visible; - pointer-events: unset; - transform: translate3d(0, 0, 0); -} - -.pf-v6-c-modal-animated-closed { - opacity: 0; - visibility: hidden; - pointer-events: none; - transform: translate3d(0, -24px, 0); -} - -@media screen and (prefers-reduced-motion: reduce) { - .pf-v6-c-modal-animated, - .pf-v6-c-modal-animated-open, - .pf-v6-c-modal-animated-closed { - transition: none; - } -}