Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DropdownMenuV2: update animation #64868

Merged
merged 12 commits into from
Sep 2, 2024
1 change: 1 addition & 0 deletions packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)).

Expand Down
18 changes: 14 additions & 4 deletions packages/components/src/dropdown-menu-v2/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
ciampo marked this conversation as resolved.
Show resolved Hide resolved
).split( '-' )[ 0 ];

if (
Expand Down Expand Up @@ -173,7 +175,7 @@ const UnconnectedDropdownMenu = (
/>

{ /* Menu popover */ }
<Styled.DropdownMenu
<Ariakit.Menu
{ ...otherProps }
modal={ modal }
store={ dropdownMenuStore }
Expand All @@ -185,15 +187,23 @@ const UnconnectedDropdownMenu = (
shift={ shift ?? ( dropdownMenuStore.parent ? -4 : 0 ) }
hideOnHoverOutside={ false }
data-side={ appliedPlacementSide }
variant={ variant }
wrapperProps={ wrapperProps }
hideOnEscape={ hideOnEscape }
unmountOnHide
render={ ( renderProps ) => (
// 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.
<Styled.MenuPopoverOuterWrapper variant={ variant }>
<Styled.MenuPopoverInnerWrapper { ...renderProps } />
</Styled.MenuPopoverOuterWrapper>
) }
>
<DropdownMenuContext.Provider value={ contextValue }>
{ children }
</DropdownMenuContext.Provider>
</Styled.DropdownMenu>
</Ariakit.Menu>
</>
);
};
Expand Down
157 changes: 92 additions & 65 deletions packages/components/src/dropdown-menu-v2/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand All @@ -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 );
Expand All @@ -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? */
Expand All @@ -86,39 +109,41 @@ 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;

/* 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 );
}
}
`;
Expand Down Expand Up @@ -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 };
}
Expand Down Expand Up @@ -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;
}
`;
Expand Down Expand Up @@ -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;
}
`;
Loading