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

DropdownMenu v2: tweak styles, add toolbar-specific styles #51097

Merged
merged 12 commits into from
May 31, 2023
2 changes: 1 addition & 1 deletion packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

### Experimental

- `DropdownMenu` v2: Tweak styles ([#50967](https://github.com/WordPress/gutenberg/pull/50967)).
- `DropdownMenu` v2: Tweak styles ([#50967](https://github.com/WordPress/gutenberg/pull/50967), [#51097](https://github.com/WordPress/gutenberg/pull/51097)).
- `DropdownMenu` v2: Render in the default `Popover.Slot` ([#51046](https://github.com/WordPress/gutenberg/pull/51046)).

## 25.0.0 (2023-05-24)
Expand Down
96 changes: 65 additions & 31 deletions packages/components/src/dropdown-menu-v2/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,20 @@ import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
/**
* WordPress dependencies
*/
import { forwardRef, createContext, useContext } from '@wordpress/element';
import {
forwardRef,
createContext,
useContext,
useMemo,
} from '@wordpress/element';
import { isRTL } from '@wordpress/i18n';
import { check, chevronRightSmall, lineSolid } from '@wordpress/icons';
import { SVG, Circle } from '@wordpress/primitives';

/**
* Internal dependencies
*/
import { useContextSystem, contextConnectWithoutRef } from '../ui/context';
import { useSlot } from '../slot-fill';
import Icon from '../icon';
import { SLOT_NAME as POPOVER_DEFAULT_SLOT_NAME } from '../popover';
Expand All @@ -29,42 +35,56 @@ import type {
DropdownMenuRadioItemProps,
DropdownMenuSeparatorProps,
DropdownSubMenuTriggerProps,
DropdownMenuContext,
DropdownMenuPrivateContext as DropdownMenuPrivateContextType,
} from './types';

// Menu content's side padding + 4px
const SUB_MENU_OFFSET_SIDE = 12;
const SUB_MENU_OFFSET_SIDE = 16;
// Opposite amount of the top padding of the menu item
const SUB_MENU_OFFSET_ALIGN = -8;

const DropdownMenuPrivateContext = createContext< {
portalContainer: HTMLElement | null;
} >( {
portalContainer: null,
} );
const DropdownMenuPrivateContext =
createContext< DropdownMenuPrivateContextType >( {
variant: undefined,
portalContainer: null,
} );

const UnconnectedDropdownMenu = ( props: DropdownMenuProps ) => {
const {
// Root props
defaultOpen,
open,
onOpenChange,
modal = true,
// Content positioning props
side = 'bottom',
sideOffset = 0,
align = 'center',
alignOffset = 0,
// Render props
children,
trigger,

// From internal components context
variant,
} = useContextSystem<
// Adding `className` to the context type to avoid a TS error
DropdownMenuProps & DropdownMenuContext & { className?: string }
ciampo marked this conversation as resolved.
Show resolved Hide resolved
>( props, 'DropdownMenu' );

/**
* `DropdownMenu` displays a menu to the user (such as a set of actions
* or functions) triggered by a button.
*/
export const DropdownMenu = ( {
// Root props
defaultOpen,
open,
onOpenChange,
modal = true,
// Content positioning props
side = 'bottom',
sideOffset = 0,
align = 'center',
alignOffset = 0,
// Render props
children,
trigger,
}: DropdownMenuProps ) => {
// Render the portal in the default slot used by the legacy Popover component.
const slot = useSlot( POPOVER_DEFAULT_SLOT_NAME );
const portalContainer = slot.ref?.current;

const privateContextValue = useMemo(
() => ( {
variant,
portalContainer,
} ),
[ variant, portalContainer ]
);

return (
<DropdownMenuPrimitive.Root
defaultOpen={ defaultOpen }
Expand All @@ -83,9 +103,10 @@ export const DropdownMenu = ( {
sideOffset={ sideOffset }
alignOffset={ alignOffset }
loop={ true }
variant={ variant }
>
<DropdownMenuPrivateContext.Provider
value={ { portalContainer } }
value={ privateContextValue }
>
{ children }
</DropdownMenuPrivateContext.Provider>
Expand All @@ -95,6 +116,15 @@ export const DropdownMenu = ( {
);
};

/**
* `DropdownMenu` displays a menu to the user (such as a set of actions
* or functions) triggered by a button.
*/
export const DropdownMenu = contextConnectWithoutRef(
UnconnectedDropdownMenu,
'DropdownMenu'
);

export const DropdownSubMenuTrigger = ( {
prefix,
suffix = (
Expand Down Expand Up @@ -134,7 +164,9 @@ export const DropdownSubMenu = ( {
children,
trigger,
}: DropdownSubMenuProps ) => {
const { portalContainer } = useContext( DropdownMenuPrivateContext );
const { variant, portalContainer } = useContext(
DropdownMenuPrivateContext
);

return (
<DropdownMenuPrimitive.Sub
Expand All @@ -153,6 +185,7 @@ export const DropdownSubMenu = ( {
loop
sideOffset={ SUB_MENU_OFFSET_SIDE }
alignOffset={ SUB_MENU_OFFSET_ALIGN }
variant={ variant }
>
{ children }
</DropdownMenuStyled.SubContent>
Expand Down Expand Up @@ -254,6 +287,7 @@ export const DropdownMenuRadioItem = ( {
);
};

export const DropdownMenuSeparator = ( props: DropdownMenuSeparatorProps ) => (
<DropdownMenuStyled.Separator { ...props } />
);
export const DropdownMenuSeparator = ( props: DropdownMenuSeparatorProps ) => {
const { variant } = useContext( DropdownMenuPrivateContext );
return <DropdownMenuStyled.Separator { ...props } variant={ variant } />;
};
24 changes: 21 additions & 3 deletions packages/components/src/dropdown-menu-v2/stories/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import styled from '@emotion/styled';
/**
* Internal dependencies
*/
import { COLORS } from '../../utils';
import {
DropdownMenu,
DropdownMenuItem,
Expand All @@ -33,6 +34,7 @@ import { menu, wordpress } from '@wordpress/icons';
* Internal dependencies
*/
import Icon from '../../icon';
import { ContextSystemProvider } from '../../ui/context';

const meta: ComponentMeta< typeof DropdownMenu > = {
title: 'Components (Experimental)/DropdownMenu v2',
Expand Down Expand Up @@ -73,8 +75,8 @@ const meta: ComponentMeta< typeof DropdownMenu > = {
export default meta;

const ItemHelpText = styled.span`
font-size: 10px;
color: #777;
font-size: 12px;
color: ${ COLORS.gray[ '700' ] };

/* "> * > &" syntax is to target only immediate parent menu item */
[data-highlighted] > * > &,
Expand Down Expand Up @@ -144,7 +146,7 @@ Default.args = {
<DropdownMenuGroup>
<DropdownMenuItem>Menu item</DropdownMenuItem>
<DropdownMenuItem
prefix={ <Icon icon={ wordpress } size={ 18 } /> }
prefix={ <Icon icon={ wordpress } size={ 24 } /> }
>
Menu item with prefix
</DropdownMenuItem>
Expand Down Expand Up @@ -197,3 +199,19 @@ Default.args = {
</>
),
};

const toolbarVariantContextValue = {
DropdownMenu: {
variant: 'toolbar',
},
};
export const ToolbarVariant: ComponentStory< typeof DropdownMenu > = (
props
) => (
<ContextSystemProvider value={ toolbarVariantContextValue }>
<DropdownMenu { ...props } />
</ContextSystemProvider>
);
ToolbarVariant.args = {
...Default.args,
};
45 changes: 30 additions & 15 deletions packages/components/src/dropdown-menu-v2/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
/**
* Internal dependencies
*/
import { COLORS, font, rtl } from '../utils';
import { COLORS, font, rtl, CONFIG } from '../utils';
import { space } from '../ui/utils/space';
import Icon from '../icon';
import type { DropdownMenuContext } from './types';

const ANIMATION_PARAMS = {
SLIDE_AMOUNT: '2px',
Expand All @@ -23,6 +24,12 @@ const ITEM_PREFIX_WIDTH = space( 7 );
const ITEM_PADDING_INLINE_START = space( 2 );
const ITEM_PADDING_INLINE_END = space( 2.5 );

// TODO: should bring this into the config, and make themeable
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

const DEFAULT_BORDER_COLOR = COLORS.ui.borderDisabled;
const TOOLBAR_VARIANT_BORDER_COLOR = COLORS.gray[ '900' ];
const DEFAULT_BOX_SHADOW = `0 0 0 ${ CONFIG.borderWidth } ${ DEFAULT_BORDER_COLOR }, ${ CONFIG.popoverShadow }`;
const TOOLBAR_VARIANT_BOX_SHADOW = `0 0 0 ${ CONFIG.borderWidth } ${ TOOLBAR_VARIANT_BORDER_COLOR }`;

const slideUpAndFade = keyframes( {
'0%': {
opacity: 0,
Expand Down Expand Up @@ -55,15 +62,14 @@ const slideLeftAndFade = keyframes( {
'100%': { opacity: 1, transform: 'translateX(0)' },
} );

const baseContent = css`
const baseContent = ( variant: DropdownMenuContext[ 'variant' ] ) => css`
min-width: 220px;
background-color: ${ COLORS.ui.background };
border-radius: 6px;
border-radius: ${ CONFIG.radiusBlockUi };
padding: ${ CONTENT_WRAPPER_PADDING };
box-shadow: 0.1px 4px 16.4px -0.5px rgba( 0, 0, 0, 0.1 ),
0px 5.5px 7.8px -0.3px rgba( 0, 0, 0, 0.1 ),
0px 2.7px 3.8px -0.2px rgba( 0, 0, 0, 0.1 ),
0px 0.7px 1px rgba( 0, 0, 0, 0.1 );
box-shadow: ${ variant === 'toolbar'
? TOOLBAR_VARIANT_BOX_SHADOW
: DEFAULT_BOX_SHADOW };
animation-duration: ${ ANIMATION_PARAMS.DURATION };
animation-timing-function: ${ ANIMATION_PARAMS.EASING };
will-change: transform, opacity;
Expand Down Expand Up @@ -156,7 +162,7 @@ const baseItem = css`
font-weight: normal;
line-height: 20px;
color: ${ COLORS.gray[ 900 ] };
border-radius: 3px;
border-radius: ${ CONFIG.radiusBlockUi };
display: flex;
align-items: center;
padding: ${ space( 2 ) } ${ ITEM_PADDING_INLINE_END } ${ space( 2 ) }
Expand Down Expand Up @@ -193,11 +199,15 @@ const baseItem = css`
}
`;

export const Content = styled( DropdownMenu.Content )`
${ baseContent }
export const Content = styled( DropdownMenu.Content )<
Pick< DropdownMenuContext, 'variant' >
>`
${ ( props ) => baseContent( props.variant ) }
`;
export const SubContent = styled( DropdownMenu.SubContent )`
${ baseContent }
export const SubContent = styled( DropdownMenu.SubContent )<
Pick< DropdownMenuContext, 'variant' >
>`
${ ( props ) => baseContent( props.variant ) }
`;

export const Item = styled( DropdownMenu.Item )`
Expand Down Expand Up @@ -235,10 +245,15 @@ export const Label = styled( DropdownMenu.Label )`
text-transform: uppercase;
`;

export const Separator = styled( DropdownMenu.Separator )`
height: 1px;
export const Separator = styled( DropdownMenu.Separator )<
Pick< DropdownMenuContext, 'variant' >
>`
height: ${ CONFIG.borderWidth };
/* TODO: doesn't match border color from variables */
background-color: ${ COLORS.ui.borderDisabled };
background-color: ${ ( props ) =>
props.variant === 'toolbar'
? TOOLBAR_VARIANT_BORDER_COLOR
: DEFAULT_BORDER_COLOR };
/* Negative horizontal margin to make separator go from side to side */
margin: ${ space( 2 ) } calc( -1 * ${ CONTENT_WRAPPER_PADDING } );
`;
Expand Down
15 changes: 15 additions & 0 deletions packages/components/src/dropdown-menu-v2/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,3 +248,18 @@ export type DropdownMenuGroupProps = {
};

export type DropdownMenuSeparatorProps = {};

export type DropdownMenuContext = {
/**
* This variant can be used to change the appearance of the component in
* specific contexts, ie. when rendered inside the `Toolbar` component.
*/
variant?: 'toolbar';
};

export type DropdownMenuPrivateContext = Pick<
DropdownMenuContext,
'variant'
> & {
portalContainer: HTMLElement | null;
};
31 changes: 24 additions & 7 deletions packages/components/src/toolbar/toolbar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,22 @@ import deprecated from '@wordpress/deprecated';
import ToolbarGroup from '../toolbar-group';
import ToolbarContainer from './toolbar-container';
import type { ToolbarProps } from './types';
import type { WordPressComponentProps } from '../../ui/context';
import {
WordPressComponentProps,
ContextSystemProvider,
} from '../../ui/context';

// TODO:
// - (optional) make the legacy `DropdownMenu` read the context variable
// - swap the legacy `DropdownMenu` with the new version of the component
// once it's stable
const CONTEXT_SYSTEM_VALUE = {
ciampo marked this conversation as resolved.
Show resolved Hide resolved
DropdownMenu: {
// Note: the legacy `DropdownMenu` component is not yet reactive to this
// context variant. See https://github.com/WordPress/gutenberg/pull/51097.
variant: 'toolbar',
},
};

function UnforwardedToolbar(
{
Expand All @@ -40,12 +55,14 @@ function UnforwardedToolbar(
className
);
return (
<ToolbarContainer
className={ finalClassName }
label={ label }
ref={ ref }
{ ...props }
/>
<ContextSystemProvider value={ CONTEXT_SYSTEM_VALUE }>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This won't have any effect on dropdowns inside Toolbar just yet, since it's still using the legacy DropdownMenu component. But it's setting the ground.

<ToolbarContainer
className={ finalClassName }
label={ label }
ref={ ref }
{ ...props }
/>
</ContextSystemProvider>
);
}

Expand Down
1 change: 1 addition & 0 deletions packages/components/src/ui/context/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export {
} from './context-system-provider';
export {
contextConnect,
contextConnectWithoutRef,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function was added in #43611 but was never added to the list of exports

hasConnectNamespace,
getConnectNamespace,
} from './context-connect';
Expand Down
Loading