diff --git a/packages/block-editor/src/components/block-settings-menu-controls/index.js b/packages/block-editor/src/components/block-settings-menu-controls/index.js index 53b3835fad1a1..cb4be30d3b286 100644 --- a/packages/block-editor/src/components/block-settings-menu-controls/index.js +++ b/packages/block-editor/src/components/block-settings-menu-controls/index.js @@ -72,8 +72,8 @@ const BlockSettingsMenuControlsSlot = ( { } return ( - - { showConvertToGroupButton && ( + <> + { /* { showConvertToGroupButton && ( - ) } + ) } */ } { fills } - { fillProps?.canMove && ! fillProps?.onlyBlock && ( + { /* { fillProps?.canMove && ! fillProps?.onlyBlock && ( - ) } - + ) } */ } + ); } } diff --git a/packages/block-editor/src/components/block-settings-menu/block-settings-dropdown.js b/packages/block-editor/src/components/block-settings-menu/block-settings-dropdown.js index bb8e00d0c11db..bdc7d50efa8e4 100644 --- a/packages/block-editor/src/components/block-settings-menu/block-settings-dropdown.js +++ b/packages/block-editor/src/components/block-settings-menu/block-settings-dropdown.js @@ -6,7 +6,11 @@ import { serialize, store as blocksStore, } from '@wordpress/blocks'; -import { DropdownMenu, MenuGroup, MenuItem } from '@wordpress/components'; +import { + // DropdownMenu, MenuGroup, MenuItem + Icon, + privateApis as componentsPrivateApis, +} from '@wordpress/components'; import { useDispatch, useSelect } from '@wordpress/data'; import { moreVertical } from '@wordpress/icons'; import { @@ -34,6 +38,10 @@ import { store as blockEditorStore } from '../../store'; import { unlock } from '../../lock-unlock'; import { useShowHoveredOrFocusedGestures } from '../block-toolbar/utils'; +const { DropdownMenuV2Ariakit, DropdownMenuItemV2Ariakit } = unlock( + componentsPrivateApis +); + const POPOVER_PROPS = { className: 'block-editor-block-settings-menu__popover', placement: 'bottom-start', @@ -44,7 +52,11 @@ function CopyMenuItem( { blocks, onCopy, label } ) { const copyMenuItemBlocksLabel = blocks.length > 1 ? __( 'Copy blocks' ) : __( 'Copy' ); const copyMenuItemLabel = label ? label : copyMenuItemBlocksLabel; - return { copyMenuItemLabel }; + return ( + + { copyMenuItemLabel } + + ); } export function BlockSettingsDropdown( { @@ -234,153 +246,28 @@ export function BlockSettingsDropdown( { onMoveTo, blocks, } ) => ( - } > - { ( { onClose } ) => ( + + Test item + + { canCopyStyles && ( <> - - <__unstableBlockSettingsMenuFirstItem.Slot - fillProps={ { onClose } } - /> - { ! parentBlockIsSelected && - !! firstParentClientId && ( - - } - onClick={ () => - selectBlock( - firstParentClientId - ) - } - > - { sprintf( - /* translators: %s: Name of the block's parent. */ - __( - 'Select parent block (%s)' - ), - parentBlockType.title - ) } - - ) } - { count === 1 && ( - - ) } - - { canDuplicate && ( - - { __( 'Duplicate' ) } - - ) } - { canInsertDefaultBlock && ( - <> - - { __( 'Add before' ) } - - - { __( 'Add after' ) } - - - ) } - - { canCopyStyles && ( - - - - { __( 'Paste styles' ) } - - - ) } + + + { __( 'Paste styles' ) } + + - { typeof children === 'function' - ? children( { onClose } ) - : Children.map( ( child ) => - cloneElement( child, { onClose } ) - ) } - { canRemove && ( - - - { removeBlockLabel } - - - ) } + + + } + hideOnClick={ false } + > + { __( 'I am a link' ) } + ) } - + + // + // { ( { onClose } ) => ( + // <> + // + // <__unstableBlockSettingsMenuFirstItem.Slot + // fillProps={ { onClose } } + // /> + // { ! parentBlockIsSelected && + // !! firstParentClientId && ( + // + // } + // onClick={ () => + // selectBlock( + // firstParentClientId + // ) + // } + // > + // { sprintf( + // /* translators: %s: Name of the block's parent. */ + // __( + // 'Select parent block (%s)' + // ), + // parentBlockType.title + // ) } + // + // ) } + // { count === 1 && ( + // + // ) } + // + // { canDuplicate && ( + // + // { __( 'Duplicate' ) } + // + // ) } + // { canInsertDefaultBlock && ( + // <> + // + // { __( 'Add before' ) } + // + // + // { __( 'Add after' ) } + // + // + // ) } + // + // { canCopyStyles && ( + // + // + // + // { __( 'Paste styles' ) } + // + // + // ) } + // + // { typeof children === 'function' + // ? children( { onClose } ) + // : Children.map( ( child ) => + // cloneElement( child, { onClose } ) + // ) } + // { canRemove && ( + // + // + // { removeBlockLabel } + // + // + // ) } + // + // ) } + // ) } ); diff --git a/packages/components/src/dropdown-menu-v2-ariakit/index.tsx b/packages/components/src/dropdown-menu-v2-ariakit/index.tsx new file mode 100644 index 0000000000000..f5efa980fc3c5 --- /dev/null +++ b/packages/components/src/dropdown-menu-v2-ariakit/index.tsx @@ -0,0 +1,105 @@ +/** + * External dependencies + */ +// eslint-disable-next-line no-restricted-imports +import * as Ariakit from '@ariakit/react'; + +/** + * WordPress dependencies + */ +import { + forwardRef, + createContext, + useContext, + useMemo, +} from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { + StyledAriakitMenu, + StyledAriakitMenuItem, + toggleButton, +} from './styles'; +import { useCx } from '../utils'; + +export const DropdownMenuContext = createContext< + { store: Ariakit.MenuStore } | undefined +>( undefined ); + +export interface DropdownMenuItemProps + extends Omit< Ariakit.MenuItemProps, 'store' > {} + +export const DropdownMenuItem = forwardRef< + HTMLDivElement, + DropdownMenuItemProps +>( function DropdownMenuItem( props, ref ) { + const dropdownMenuContext = useContext( DropdownMenuContext ); + return ( + + ); +} ); + +export interface DropdownMenuProps extends Ariakit.MenuButtonProps { + trigger: React.ReactNode; + children?: React.ReactNode; +} + +export const DropdownMenu = forwardRef< HTMLDivElement, DropdownMenuProps >( + function DropdownMenu( { trigger, children, className, ...props }, ref ) { + const parentContext = useContext( DropdownMenuContext ); + + const dropdownMenuStore = Ariakit.useMenuStore( { + parent: parentContext?.store, + } ); + + const cx = useCx(); + const menuButtonClassName = useMemo( + () => cx( ! dropdownMenuStore.parent && toggleButton, className ), + [ cx, dropdownMenuStore.parent, className ] + ); + + const contextValue = useMemo( + () => ( { store: dropdownMenuStore } ), + [ dropdownMenuStore ] + ); + + return ( + <> + { /* Menu trigger */ } + + ) : undefined + } + > + { trigger } + + + + { /* Menu popover */ } + + + { children } + + + + ); + } +); diff --git a/packages/components/src/dropdown-menu-v2-ariakit/stories/index.story.tsx b/packages/components/src/dropdown-menu-v2-ariakit/stories/index.story.tsx new file mode 100644 index 0000000000000..c3e9d479384ee --- /dev/null +++ b/packages/components/src/dropdown-menu-v2-ariakit/stories/index.story.tsx @@ -0,0 +1,202 @@ +/** + * External dependencies + */ +import type { Meta, StoryFn } from '@storybook/react'; + +/** + * WordPress dependencies + */ +import { menu } from '@wordpress/icons'; +import { useState, useMemo, useContext } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { DropdownMenu, DropdownMenuItem, DropdownMenuContext } from '..'; +import Icon from '../../icon'; +import Modal from '../../modal'; +import { createSlotFill, Provider as SlotFillProvider } from '../../slot-fill'; + +const meta: Meta< typeof DropdownMenu > = { + title: 'Components (Experimental)/DropdownMenu v2 ariakit', + component: DropdownMenu, + subcomponents: { + // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170 + DropdownMenuItem, + }, + argTypes: { + children: { control: { type: null } }, + }, + parameters: { + actions: { argTypesRegex: '^on.*' }, + controls: { expanded: true }, + docs: { + canvas: { sourceState: 'shown' }, + source: { excludeDecorators: true }, + }, + }, + decorators: [ + // Layout wrapper + ( Story ) => ( +
+ +
+ ), + ], +}; +export default meta; + +const Template: StoryFn< typeof DropdownMenu > = ( props ) => ( + +); +export const Default = Template.bind( {} ); +Default.args = { + trigger: , + children: ( + <> + Undo + Redo + + Search the Web... + Find... + Find Next + Find Previous + + + Start Speaking + Stop Speaking + + + ), +}; + +export const WithModalAsSiblingOfMenu: StoryFn< typeof DropdownMenu > = ( + props +) => { + const [ isModalOpen, setModalOpen ] = useState( false ); + return ( + <> + + setModalOpen( true ) }> + Open modal + + + { isModalOpen && ( + setModalOpen( false ) }> + Modal's contents + + + ) } + + ); +}; +WithModalAsSiblingOfMenu.args = { + trigger: , +}; + +export const WithModalAsSiblingOfMenuItem: StoryFn< typeof DropdownMenu > = ( + props +) => { + const [ isModalOpen, setModalOpen ] = useState( false ); + return ( + + setModalOpen( true ) } + > + Open modal + + { isModalOpen && ( + setModalOpen( false ) }> + Yo! + + + ) } + + ); +}; +WithModalAsSiblingOfMenuItem.args = { + trigger: , +}; + +const ExampleSlotFill = createSlotFill( 'Example' ); + +const Slot = () => { + const dropdownMenuContext = useContext( DropdownMenuContext ); + + // Forwarding the content of the slot so that it can be used by the fill + const fillProps = useMemo( + () => ( { + forwardedContext: [ + [ + DropdownMenuContext.Provider, + { value: dropdownMenuContext }, + ], + ], + } ), + [ dropdownMenuContext ] + ); + + return ; +}; + +type ForwardedContextTuple< P = {} > = [ + React.ComponentType< React.PropsWithChildren< P > >, + P, +]; + +const Fill = ( { children }: { children: React.ReactNode } ) => { + const innerMarkup = <>{ children }; + + return ( + + { ( fillProps: { forwardedContext?: ForwardedContextTuple[] } ) => { + const { forwardedContext = [] } = fillProps; + + return forwardedContext.reduce( + ( inner: JSX.Element, [ Provider, props ] ) => ( + { inner } + ), + innerMarkup + ); + } } + + ); +}; + +export const AddItemsViaSlotFill: StoryFn< typeof DropdownMenu > = ( + props +) => { + return ( + + + Item + + + + + + + + Item from fill + + + + Test + + + + + ); +}; +AddItemsViaSlotFill.args = { + trigger: , +}; diff --git a/packages/components/src/dropdown-menu-v2-ariakit/styles.ts b/packages/components/src/dropdown-menu-v2-ariakit/styles.ts new file mode 100644 index 0000000000000..71dc6dc0fc563 --- /dev/null +++ b/packages/components/src/dropdown-menu-v2-ariakit/styles.ts @@ -0,0 +1,151 @@ +/** + * External dependencies + */ +// eslint-disable-next-line no-restricted-imports +import * as Ariakit from '@ariakit/react'; +import styled from '@emotion/styled'; +import { css } from '@emotion/react'; + +export const toggleButton = css` + display: flex; + height: 2.5rem; + touch-action: none; + user-select: none; + align-items: center; + justify-content: center; + gap: 0.25rem; + white-space: nowrap; + border-radius: 0.5rem; + border-style: none; + background-color: hsl( 204 20% 100% ); + padding-left: 1rem; + padding-right: 1rem; + font-size: 1rem; + line-height: 1.5rem; + color: hsl( 204 10% 10% ); + text-decoration-line: none; + outline-width: 2px; + outline-offset: 2px; + outline-color: hsl( 204 100% 40% ); + box-shadow: + inset 0 0 0 1px rgba( 0, 0, 0, 0.1 ), + inset 0 -1px 0 rgba( 0, 0, 0, 0.1 ), + 0 1px 1px rgba( 0, 0, 0, 0.1 ); + font-weight: 500; + + &:hover { + background-color: hsl( 204 20% 96% ); + } + + &[aria-disabled='true'] { + opacity: 0.5; + } + + &[aria-expanded='true'] { + background-color: hsl( 204 20% 96% ); + } + + &[data-focus-visible] { + outline-style: solid; + } + + &:active, + &[data-active] { + transform: scale( 0.98 ); + } + + &:active[aria-expanded='true'], + &[data-active][aria-expanded='true'] { + transform: scale( 1 ); + } + + @media ( min-width: 640px ) { + gap: 0.5rem; + } +`; + +// :is(.dark .button) { +// background-color: hsl(204 20% 100% / 0.05); +// color: hsl(204 20% 100%); +// box-shadow: +// inset 0 0 0 1px rgba(255, 255, 255, 0.1), +// inset 0 -1px 0 1px rgba(0, 0, 0, 0.2), +// inset 0 1px 0 rgba(255, 255, 255, 0.05); +// } + +// :is(.dark .button:hover) { +// background-color: hsl(204 20% 100% / 0.1); +// } + +// :is(.dark .button)[aria-expanded="true"] { +// background-color: hsl(204 20% 100% / 0.1); +// } + +export const StyledAriakitMenu = styled( Ariakit.Menu )` + position: relative; + z-index: 50; + display: flex; + max-height: var( --popover-available-height ); + min-width: 180px; + flex-direction: column; + overscroll-behavior: contain; + border-radius: 0.5rem; + border-width: 1px; + border-style: solid; + border-color: hsl( 204 20% 88% ); + background-color: hsl( 204 20% 100% ); + padding: 0.5rem; + color: hsl( 204 10% 10% ); + box-shadow: + 0 10px 15px -3px rgb( 0 0 0 / 0.1 ), + 0 4px 6px -4px rgb( 0 0 0 / 0.1 ); + outline: none !important; + overflow: visible; +`; + +// :is(.dark .menu) { +// border-color: hsl(204 3% 26%); +// background-color: hsl(204 3% 18%); +// color: hsl(204 20% 100%); +// box-shadow: +// 0 10px 15px -3px rgb(0 0 0 / 0.25), +// 0 4px 6px -4px rgb(0 0 0 / 0.1); +// } + +export const StyledAriakitMenuItem = styled( Ariakit.MenuItem )` + display: flex; + cursor: default; + scroll-margin: 0.5rem; + align-items: center; + gap: 0.5rem; + border-radius: 0.25rem; + padding: 0.5rem; + outline: none !important; + + &[aria-disabled='true'] { + opacity: 0.25; + } + + &[data-active-item] { + background-color: hsl( 204 100% 40% ); + color: hsl( 204 20% 100% ); + } + + &:active, + &[data-active] { + background-color: hsl( 204 100% 32% ); + } + + ${ StyledAriakitMenu }:not(:focus) &:not(:focus)[aria-expanded="true"] { + background-color: hsl( 204 10% 10% / 0.1 ); + color: currentColor; + } +`; + +// :is(.dark .menu:not(:focus) .menu-item:not(:focus)[aria-expanded="true"]) { +// background-color: hsl(204 20% 100% / 0.1); +// } + +export const StyledMenuButtonLabel = styled.span` + flex: 1 1 0%; +`; diff --git a/packages/components/src/dropdown-menu/index.tsx b/packages/components/src/dropdown-menu/index.tsx index 9105555927f47..b5ccd92d68b90 100644 --- a/packages/components/src/dropdown-menu/index.tsx +++ b/packages/components/src/dropdown-menu/index.tsx @@ -22,7 +22,7 @@ import type { } from './types'; function mergeProps< - T extends { className?: string; [ key: string ]: unknown }, + T extends { className?: string; [ key: string ]: unknown } >( defaultProps: Partial< T > = {}, props: T = {} as T ) { const mergedProps: T = { ...defaultProps, diff --git a/packages/components/src/private-apis.ts b/packages/components/src/private-apis.ts index 6e17abde0c627..966be9887a30a 100644 --- a/packages/components/src/private-apis.ts +++ b/packages/components/src/private-apis.ts @@ -22,6 +22,10 @@ import { DropdownSubMenu as DropdownSubMenuV2, DropdownSubMenuTrigger as DropdownSubMenuTriggerV2, } from './dropdown-menu-v2'; +import { + DropdownMenu as DropdownMenuV2Ariakit, + DropdownMenuItem as DropdownMenuItemV2Ariakit, +} from './dropdown-menu-v2-ariakit'; import { ComponentsContext } from './ui/context/context-system-provider'; import Theme from './theme'; @@ -49,4 +53,6 @@ lock( privateApis, { DropdownSubMenuTriggerV2, ProgressBar, Theme, + DropdownMenuV2Ariakit, + DropdownMenuItemV2Ariakit, } ); diff --git a/packages/patterns/src/components/pattern-convert-button.js b/packages/patterns/src/components/pattern-convert-button.js index 002dbbd8c0181..9356eddbfc7b6 100644 --- a/packages/patterns/src/components/pattern-convert-button.js +++ b/packages/patterns/src/components/pattern-convert-button.js @@ -9,8 +9,8 @@ import { } from '@wordpress/blocks'; import { store as blockEditorStore } from '@wordpress/block-editor'; import { useState, useCallback } from '@wordpress/element'; -import { MenuItem } from '@wordpress/components'; -import { symbol } from '@wordpress/icons'; +import { privateApis as componentsPrivateApis } from '@wordpress/components'; +// import { symbol } from '@wordpress/icons'; import { useSelect, useDispatch } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; import { __, sprintf } from '@wordpress/i18n'; @@ -23,6 +23,10 @@ import CreatePatternModal from './create-pattern-modal'; import { unlock } from '../lock-unlock'; import { PATTERN_SYNC_TYPES } from '../constants'; +const { DropdownMenuV2Ariakit, DropdownMenuItemV2Ariakit } = unlock( + componentsPrivateApis +); + /** * Menu control to convert block(s) to a pattern block. * @@ -126,15 +130,16 @@ export default function PatternConvertButton( { clientIds, rootClientId } ) { setIsModalOpen( false ); }; return ( - <> - + setIsModalOpen( true ) } + hideOnClick={ false } aria-expanded={ isModalOpen } aria-haspopup="dialog" > { __( 'Create pattern' ) } - + { isModalOpen && ( ) } - + ); }