diff --git a/packages/react-core/src/components/Backdrop/Backdrop.tsx b/packages/react-core/src/components/Backdrop/Backdrop.tsx index c564a8b7f53..3dea142179b 100644 --- a/packages/react-core/src/components/Backdrop/Backdrop.tsx +++ b/packages/react-core/src/components/Backdrop/Backdrop.tsx @@ -1,20 +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; + /** 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 = '', + 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 281c379eebc..bb916ef52ed 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; + /** 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 ed4d4b4b24b..17d8a0b3a8d 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'; + /** Flag indicating whether animations are enabled. */ + hasAnimations?: 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, + hasAnimations, ...props }: ModalBoxProps) => { if (positionOffset) { @@ -46,6 +53,9 @@ 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, 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..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,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; + /** Flag indicating whether animations are enabled. */ + hasAnimations?: boolean; } export const ModalContent: React.FunctionComponent = ({ @@ -72,9 +74,12 @@ export const ModalContent: React.FunctionComponent = ({ ouiaSafe = true, elementToFocus, focusTrapId, + hasAnimations: hasAnimationsProp, ...props }: ModalContentProps) => { - if (!isOpen) { + const hasAnimations = useHasAnimations(hasAnimationsProp); + + if (!isOpen && !hasAnimations) { return null; } @@ -91,6 +96,8 @@ export const ModalContent: React.FunctionComponent = ({ const modalBox = ( = ({ {children} ); + let focusTrapActive = !disableFocusTrap; + if (hasAnimations) { + 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-core/src/components/Modal/examples/ModalAnimatedProvider.tsx b/packages/react-core/src/components/Modal/examples/ModalAnimatedProvider.tsx new file mode 100644 index 00000000000..66a800e7acc --- /dev/null +++ b/packages/react-core/src/components/Modal/examples/ModalAnimatedProvider.tsx @@ -0,0 +1,58 @@ +import { Fragment, useState } from 'react'; +import { + AnimationsProvider, + Button, + Modal, + ModalHeader, + ModalBody, + ModalFooter, + ModalVariant +} from '@patternfly/react-core'; + +export const ModalAnimatedProvider: 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. + + + + + + + + + ); +}; 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..4ca23410911 --- /dev/null +++ b/packages/react-styles/src/css/components/BackdropAnimations/backdrop-animations.css @@ -0,0 +1,31 @@ +.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 new file mode 100644 index 00000000000..e03c3f0f98b --- /dev/null +++ b/packages/react-styles/src/css/components/ModalAnimations/modal-animations.css @@ -0,0 +1,37 @@ +.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; + } +} 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/');