diff --git a/packages/base-styles/_animations.scss b/packages/base-styles/_animations.scss index 8706d185aa221..b17478ed24f04 100644 --- a/packages/base-styles/_animations.scss +++ b/packages/base-styles/_animations.scss @@ -4,7 +4,7 @@ } } -@mixin animation__fade-in($speed: 0.08s, $delay: 0s) { +@mixin animation__fade-in($speed: 0.08s, $delay: 0s, $easing: linear) { @include keyframes(__wp-base-styles-fade-in) { from { opacity: 0; @@ -15,12 +15,12 @@ } - animation: __wp-base-styles-fade-in $speed linear $delay; + animation: __wp-base-styles-fade-in $speed $easing $delay; animation-fill-mode: forwards; @include reduce-motion("animation"); } -@mixin animation__fade-out($speed: 0.08s, $delay: 0s) { +@mixin animation__fade-out($speed: 0.08s, $delay: 0s, $easing: linear) { @include keyframes(__wp-base-styles-fade-out) { from { opacity: 1; @@ -31,7 +31,7 @@ } - animation: __wp-base-styles-fade-out $speed linear $delay; + animation: __wp-base-styles-fade-out $speed $easing $delay; animation-fill-mode: forwards; @include reduce-motion("animation"); } diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index c705257a827c5..dc89bf6543f40 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -29,6 +29,7 @@ - `ResizeableBox`: Adopt elevation scale ([#65159](https://github.com/WordPress/gutenberg/pull/65159)). - `Snackbar`: Adopt elevation scale ([#65159](https://github.com/WordPress/gutenberg/pull/65159)). - `Tooltip`: Adopt elevation scale ([#65159](https://github.com/WordPress/gutenberg/pull/65159)). +- `Modal`: add exit animation for internally triggered events ([#65203](https://github.com/WordPress/gutenberg/pull/65203)). - `Card`: Adopt radius scale ([#65053](https://github.com/WordPress/gutenberg/pull/65053)). ### Bug Fixes diff --git a/packages/components/src/modal/index.tsx b/packages/components/src/modal/index.tsx index 7d988a0e7240b..7d55c9a4d5d1e 100644 --- a/packages/components/src/modal/index.tsx +++ b/packages/components/src/modal/index.tsx @@ -2,7 +2,6 @@ * External dependencies */ import clsx from 'clsx'; -import type { ForwardedRef, KeyboardEvent, RefObject, UIEvent } from 'react'; /** * WordPress dependencies @@ -38,10 +37,11 @@ import StyleProvider from '../style-provider'; import type { ModalProps } from './types'; import { withIgnoreIMEEvents } from '../utils/with-ignore-ime-events'; import { Spacer } from '../spacer'; +import { useModalExitAnimation } from './use-modal-exit-animation'; // Used to track and dismiss the prior modal when another opens unless nested. type Dismissers = Set< - RefObject< ModalProps[ 'onRequestClose' ] | undefined > + React.RefObject< ModalProps[ 'onRequestClose' ] | undefined > >; const ModalContext = createContext< Dismissers >( new Set() ); @@ -50,7 +50,7 @@ const bodyOpenClasses = new Map< string, number >(); function UnforwardedModal( props: ModalProps, - forwardedRef: ForwardedRef< HTMLDivElement > + forwardedRef: React.ForwardedRef< HTMLDivElement > ) { const { bodyOpenClassName = 'modal-open', @@ -70,7 +70,7 @@ function UnforwardedModal( closeButtonLabel, children, style, - overlayClassName, + overlayClassName: overlayClassnameProp, className, contentLabel, onKeyDown, @@ -184,6 +184,9 @@ function UnforwardedModal( }; }, [ bodyOpenClassName ] ); + const { closeModal, frameRef, frameStyle, overlayClassname } = + useModalExitAnimation(); + // Calls the isContentScrollable callback when the Modal children container resizes. useLayoutEffect( () => { if ( ! window.ResizeObserver || ! childrenContainerRef.current ) { @@ -200,21 +203,21 @@ function UnforwardedModal( }; }, [ isContentScrollable, childrenContainerRef ] ); - function handleEscapeKeyDown( event: KeyboardEvent< HTMLDivElement > ) { + function handleEscapeKeyDown( + event: React.KeyboardEvent< HTMLDivElement > + ) { if ( shouldCloseOnEsc && ( event.code === 'Escape' || event.key === 'Escape' ) && ! event.defaultPrevented ) { event.preventDefault(); - if ( onRequestClose ) { - onRequestClose( event ); - } + closeModal().then( () => onRequestClose( event ) ); } } const onContentContainerScroll = useCallback( - ( e: UIEvent< HTMLDivElement > ) => { + ( e: React.UIEvent< HTMLDivElement > ) => { const scrollY = e?.currentTarget?.scrollTop ?? -1; if ( ! hasScrolledContent && scrollY > 0 ) { @@ -248,7 +251,7 @@ function UnforwardedModal( const isSameTarget = target === pressTarget; pressTarget = null; if ( button === 0 && isSameTarget ) { - onRequestClose(); + closeModal().then( () => onRequestClose() ); } }, }; @@ -259,7 +262,8 @@ function UnforwardedModal( ref={ useMergeRefs( [ ref, forwardedRef ] ) } className={ clsx( 'components-modal__screen-overlay', - overlayClassName + overlayClassname, + overlayClassnameProp ) } onKeyDown={ withIgnoreIMEEvents( handleEscapeKeyDown ) } { ...( shouldCloseOnClickOutside ? overlayPressHandlers : {} ) } @@ -271,8 +275,12 @@ function UnforwardedModal( sizeClass, className ) } - style={ style } + style={ { + ...frameStyle, + ...style, + } } ref={ useMergeRefs( [ + frameRef, constrainedTabbingRef, focusReturnRef, focusOnMount !== 'firstContentElement' @@ -331,7 +339,13 @@ function UnforwardedModal( />