diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 0021df7a36110..966599107dca2 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -67,6 +67,7 @@ - `DropdownMenu` v2: fix flashing menu item styles when using keyboard ([#64873](https://github.com/WordPress/gutenberg/pull/64873)). - `DropdownMenu` v2: refactor to overloaded naming convention ([#64654](https://github.com/WordPress/gutenberg/pull/64654)). - `DropdownMenu` v2: add `GroupLabel` subcomponent ([#64854](https://github.com/WordPress/gutenberg/pull/64854)). +- `DropdownMenuV2`: update animation ([#64868](https://github.com/WordPress/gutenberg/pull/64868)). - `Composite` V2: fix Storybook docgen ([#64682](https://github.com/WordPress/gutenberg/pull/64682)). - `Composite` V2: accept store props on top-level component ([#64832](https://github.com/WordPress/gutenberg/pull/64832)). diff --git a/packages/components/src/dropdown-menu-v2/index.tsx b/packages/components/src/dropdown-menu-v2/index.tsx index 34805d2d3e268..50c4f3069d51b 100644 --- a/packages/components/src/dropdown-menu-v2/index.tsx +++ b/packages/components/src/dropdown-menu-v2/index.tsx @@ -109,9 +109,11 @@ const UnconnectedDropdownMenu = ( ); // Extract the side from the applied placement — useful for animations. + // Using `currentPlacement` instead of `placement` to make sure that we + // use the final computed placement (including "flips" etc). const appliedPlacementSide = useStoreState( dropdownMenuStore, - 'placement' + 'currentPlacement' ).split( '-' )[ 0 ]; if ( @@ -173,7 +175,7 @@ const UnconnectedDropdownMenu = ( /> { /* Menu popover */ } - ( + // Two wrappers are needed for the entry animation, where the menu + // container scales with a different factor than its contents. + // The {...renderProps} are passed to the inner wrapper, so that the + // menu element is the direct parent of the menu item elements. + + + + ) } > { children } - + ); }; diff --git a/packages/components/src/dropdown-menu-v2/styles.ts b/packages/components/src/dropdown-menu-v2/styles.ts index 9bb2f07b28262..1fe2c220526b5 100644 --- a/packages/components/src/dropdown-menu-v2/styles.ts +++ b/packages/components/src/dropdown-menu-v2/styles.ts @@ -2,7 +2,7 @@ * External dependencies */ import * as Ariakit from '@ariakit/react'; -import { css, keyframes } from '@emotion/react'; +import { css } from '@emotion/react'; import styled from '@emotion/styled'; /** @@ -15,9 +15,13 @@ import { Truncate } from '../truncate'; import type { DropdownMenuContext } from './types'; const ANIMATION_PARAMS = { - SLIDE_AMOUNT: '2px', - DURATION: '400ms', - EASING: 'cubic-bezier( 0.16, 1, 0.3, 1 )', + SCALE_AMOUNT_OUTER: 0.82, + SCALE_AMOUNT_CONTENT: 0.9, + DURATION: { + IN: '400ms', + OUT: '200ms', + }, + EASING: 'cubic-bezier(0.33, 0, 0, 1)', }; const CONTENT_WRAPPER_PADDING = space( 1 ); @@ -38,41 +42,60 @@ const TOOLBAR_VARIANT_BOX_SHADOW = `0 0 0 ${ CONFIG.borderWidth } ${ TOOLBAR_VAR const GRID_TEMPLATE_COLS = 'minmax( 0, max-content ) 1fr'; -const slideUpAndFade = keyframes( { - '0%': { - opacity: 0, - transform: `translateY(${ ANIMATION_PARAMS.SLIDE_AMOUNT })`, - }, - '100%': { opacity: 1, transform: 'translateY(0)' }, -} ); +export const MenuPopoverOuterWrapper = styled.div< + Pick< DropdownMenuContext, 'variant' > +>` + position: relative; -const slideRightAndFade = keyframes( { - '0%': { - opacity: 0, - transform: `translateX(-${ ANIMATION_PARAMS.SLIDE_AMOUNT })`, - }, - '100%': { opacity: 1, transform: 'translateX(0)' }, -} ); + background-color: ${ COLORS.ui.background }; + border-radius: ${ CONFIG.radiusMedium }; + ${ ( props ) => css` + box-shadow: ${ props.variant === 'toolbar' + ? TOOLBAR_VARIANT_BOX_SHADOW + : DEFAULT_BOX_SHADOW }; + ` } -const slideDownAndFade = keyframes( { - '0%': { - opacity: 0, - transform: `translateY(-${ ANIMATION_PARAMS.SLIDE_AMOUNT })`, - }, - '100%': { opacity: 1, transform: 'translateY(0)' }, -} ); + overflow: hidden; -const slideLeftAndFade = keyframes( { - '0%': { - opacity: 0, - transform: `translateX(${ ANIMATION_PARAMS.SLIDE_AMOUNT })`, - }, - '100%': { opacity: 1, transform: 'translateX(0)' }, -} ); + /* Open/close animation (outer wrapper) */ + @media not ( prefers-reduced-motion ) { + transition-property: transform, opacity; + transition-timing-function: ${ ANIMATION_PARAMS.EASING }; + transition-duration: ${ ANIMATION_PARAMS.DURATION.IN }; + will-change: transform, opacity; -export const DropdownMenu = styled( Ariakit.Menu )< - Pick< DropdownMenuContext, 'variant' > ->` + /* Regardless of the side, fade in and out. */ + opacity: 0; + &:has( [data-enter] ) { + opacity: 1; + } + + &:has( [data-leave] ) { + transition-duration: ${ ANIMATION_PARAMS.DURATION.OUT }; + } + + /* For menus opening on top and bottom side, animate the scale Y too. */ + &:has( [data-side='bottom'] ), + &:has( [data-side='top'] ) { + transform: scaleY( ${ ANIMATION_PARAMS.SCALE_AMOUNT_OUTER } ); + } + &:has( [data-side='bottom'] ) { + transform-origin: top; + } + &:has( [data-side='top'] ) { + transform-origin: bottom; + } + &:has( [data-enter][data-side='bottom'] ), + &:has( [data-enter][data-side='top'] ), + /* Do not animate the scaleY when closing the menu */ + &:has( [data-leave][data-side='bottom'] ), + &:has( [data-leave][data-side='top'] ) { + transform: scaleY( 1 ); + } + } +`; + +export const MenuPopoverInnerWrapper = styled.div` position: relative; /* Same as popover component */ /* TODO: is there a way to read the sass variable? */ @@ -86,15 +109,8 @@ export const DropdownMenu = styled( Ariakit.Menu )< min-width: 160px; max-width: 320px; max-height: var( --popover-available-height ); - padding: ${ CONTENT_WRAPPER_PADDING }; - background-color: ${ COLORS.ui.background }; - border-radius: ${ CONFIG.radiusMedium }; - ${ ( props ) => css` - box-shadow: ${ props.variant === 'toolbar' - ? TOOLBAR_VARIANT_BOX_SHADOW - : DEFAULT_BOX_SHADOW }; - ` } + padding: ${ CONTENT_WRAPPER_PADDING }; overscroll-behavior: contain; overflow: auto; @@ -102,23 +118,32 @@ export const DropdownMenu = styled( Ariakit.Menu )< /* Only visible in Windows High Contrast mode */ outline: 2px solid transparent !important; - /* Animation */ - &[data-open] { - @media not ( prefers-reduced-motion ) { - animation-duration: ${ ANIMATION_PARAMS.DURATION }; - animation-timing-function: ${ ANIMATION_PARAMS.EASING }; - will-change: transform, opacity; - /* Default animation.*/ - animation-name: ${ slideDownAndFade }; - &[data-side='left'] { - animation-name: ${ slideLeftAndFade }; - } - &[data-side='up'] { - animation-name: ${ slideUpAndFade }; - } - &[data-side='right'] { - animation-name: ${ slideRightAndFade }; - } + /* Open/close animation (inner content wrapper) */ + @media not ( prefers-reduced-motion ) { + transition: inherit; + transform-origin: inherit; + + /* + * For menus opening on top and bottom side, animate the scale Y too. + * The content scales at a different rate than the outer container: + * - first, counter the outer scale factor by doing "1 / scaleAmountOuter" + * - then, apply the content scale factor. + */ + &[data-side='bottom'], + &[data-side='top'] { + transform: scaleY( + calc( + 1 / ${ ANIMATION_PARAMS.SCALE_AMOUNT_OUTER } * + ${ ANIMATION_PARAMS.SCALE_AMOUNT_CONTENT } + ) + ); + } + &[data-enter][data-side='bottom'], + &[data-enter][data-side='top'], + /* Do not animate the scaleY when closing the menu */ + &[data-leave][data-side='bottom'], + &[data-leave][data-side='top'] { + transform: scaleY( 1 ); } } `; @@ -201,7 +226,7 @@ const baseItem = css` } /* When the item is the trigger of an open submenu */ - ${ DropdownMenu }:not(:focus) &:not(:focus)[aria-expanded="true"] { + ${ MenuPopoverInnerWrapper }:not(:focus) &:not(:focus)[aria-expanded="true"] { background-color: ${ LIGHT_BACKGROUND_COLOR }; color: ${ COLORS.theme.foreground }; } @@ -299,9 +324,9 @@ export const ItemSuffixWrapper = styled.span` * When the parent menu item is active, except when it's a non-focused/hovered * submenu trigger (in that case, color should not be inherited) */ - [data-active-item]:not( [data-focus-visible] ) *:not(${ DropdownMenu }) &, + [data-active-item]:not( [data-focus-visible] ) *:not(${ MenuPopoverInnerWrapper }) &, /* When the parent menu item is disabled */ - [aria-disabled='true'] *:not(${ DropdownMenu }) & { + [aria-disabled='true'] *:not(${ MenuPopoverInnerWrapper }) & { color: inherit; } `; @@ -364,8 +389,10 @@ export const DropdownMenuItemHelpText = styled( Truncate )` color: ${ LIGHTER_TEXT_COLOR }; word-break: break-all; - [data-active-item]:not( [data-focus-visible] ) *:not( ${ DropdownMenu } ) &, - [aria-disabled='true'] *:not( ${ DropdownMenu } ) & { + [data-active-item]:not( [data-focus-visible] ) + *:not( ${ MenuPopoverInnerWrapper } ) + &, + [aria-disabled='true'] *:not( ${ MenuPopoverInnerWrapper } ) & { color: inherit; } `;