diff --git a/packages/block-editor/src/components/block-lock/modal.js b/packages/block-editor/src/components/block-lock/modal.js index 7d09f7b63f8cd..3be23f6adde14 100644 --- a/packages/block-editor/src/components/block-lock/modal.js +++ b/packages/block-editor/src/components/block-lock/modal.js @@ -24,7 +24,7 @@ import useBlockDisplayInformation from '../use-block-display-information'; import { store as blockEditorStore } from '../../store'; // Entity based blocks which allow edit locking -const ALLOWS_EDIT_LOCKING = [ 'core/block', 'core/navigation' ]; +const ALLOWS_EDIT_LOCKING = [ 'core/navigation' ]; function getTemplateLockValue( lock ) { // Prevents all operations. diff --git a/packages/block-editor/src/components/block-mover/README.md b/packages/block-editor/src/components/block-mover/README.md index 38520072b4ac8..b781de773ef9f 100644 --- a/packages/block-editor/src/components/block-mover/README.md +++ b/packages/block-editor/src/components/block-mover/README.md @@ -1,12 +1,10 @@ -# Block mover +# BlockMover -Block movers allow moving blocks inside the editor using up and down buttons. +BlockMover component allows moving blocks inside the editor using up and down buttons. ![Block mover screenshot](https://make.wordpress.org/core/files/2020/08/block-mover-screenshot.png) -## Development guidelines - -### Usage +## Usage Shows the block mover buttons in the block toolbar. @@ -15,13 +13,22 @@ import { BlockMover } from '@wordpress/block-editor'; const MyMover = () => ; ``` -### Props +## Props -#### clientIds +### clientIds -Blocks IDs +The IDs of the blocks to move. - Type: `Array` +- Required: Yes + +### hideDragHandle + +If this property is true, the drag handle is hidden. + +- Type: `boolean` +- Required: No +- Default: `false` ## Related components diff --git a/packages/block-editor/src/components/block-mover/stories/index.story.js b/packages/block-editor/src/components/block-mover/stories/index.story.js index de30260563f91..de6d13c797b4d 100644 --- a/packages/block-editor/src/components/block-mover/stories/index.story.js +++ b/packages/block-editor/src/components/block-mover/stories/index.story.js @@ -14,6 +14,7 @@ import BlockMover from '../'; import { ExperimentalBlockEditorProvider } from '../../provider'; import { store as blockEditorStore } from '../../../store'; +// For the purpose of this story, we need to register the core blocks samples. registerCoreBlocks(); const blocks = [ // vertical @@ -30,81 +31,82 @@ const blocks = [ ] ), ]; -function Provider( { children } ) { - const wrapperStyle = { margin: '24px', position: 'relative' }; - - return ( -
+/** + * BlockMover component allows moving blocks inside the editor using up and down buttons. + */ +const meta = { + title: 'BlockEditor/BlockMover', + component: BlockMover, + parameters: { + docs: { canvas: { sourceState: 'shown' } }, + }, + decorators: [ + ( Story ) => ( - { children } + + + -
- ); -} - -function BlockMoverStory() { - const { updateBlockListSettings } = useDispatch( blockEditorStore ); - - useEffect( () => { - /** - * This shouldn't be needed but unfortunatley - * the layout orientation is not declarative, we need - * to render the blocks to update the block settings in the state. - */ - updateBlockListSettings( blocks[ 1 ].clientId, { - orientation: 'horizontal', - } ); - }, [] ); - - return ( -
-

The mover by default is vertical

- - - - -

- But it can also accommodate horizontal blocks. -

- - - + ), + ], + argTypes: { + clientIds: { + control: { + type: 'none', + }, + description: 'The client IDs of the blocks to move.', + }, + hideDragHandle: { + control: { + type: 'boolean', + }, + description: 'If this property is true, the drag handle is hidden.', + }, + }, +}; +export default meta; -

We can also hide the drag handle.

- - - -
- ); -} +export const Default = { + args: { + clientIds: [ blocks[ 0 ].innerBlocks[ 1 ].clientId ], + }, +}; -export default { - title: 'BlockEditor/BlockMover', +/** + * This story shows the block mover with horizontal orientation. + * It is necessary to render the blocks to update the block settings in the state. + */ +export const Horizontal = { + decorators: [ + ( Story ) => { + const { updateBlockListSettings } = useDispatch( blockEditorStore ); + useEffect( () => { + /** + * This shouldn't be needed but unfortunately + * the layout orientation is not declarative, we need + * to render the blocks to update the block settings in the state. + */ + updateBlockListSettings( blocks[ 1 ].clientId, { + orientation: 'horizontal', + } ); + }, [] ); + return ; + }, + ], + args: { + clientIds: [ blocks[ 1 ].innerBlocks[ 1 ].clientId ], + }, + parameters: { + docs: { canvas: { sourceState: 'hidden' } }, + }, }; -export const _default = () => { - return ( - - - - ); +/** + * You can hide the drag handle by `hideDragHandle` attribute. + */ +export const HideDragHandle = { + args: { + ...Default.args, + hideDragHandle: true, + }, }; diff --git a/packages/block-editor/src/components/inserter-draggable-blocks/index.js b/packages/block-editor/src/components/inserter-draggable-blocks/index.js index 5a63535be3d3c..0e1aaadc72e67 100644 --- a/packages/block-editor/src/components/inserter-draggable-blocks/index.js +++ b/packages/block-editor/src/components/inserter-draggable-blocks/index.js @@ -29,6 +29,15 @@ const InserterDraggableBlocks = ( { blocks, }; + const blocksContainMedia = + blocks.filter( + ( block ) => + ( block.name === 'core/image' || + block.name === 'core/audio' || + block.name === 'core/video' ) && + ( block.attributes.url || block.attributes.src ) + ).length > 0; + const blockTypeIcon = useSelect( ( select ) => { const { getBlockType } = select( blocksStore ); @@ -63,7 +72,7 @@ const InserterDraggableBlocks = ( { ? [ createBlock( 'core/block', { ref: pattern.id } ) ] : blocks; event.dataTransfer.setData( - 'text/html', + blocksContainMedia ? 'default' : 'text/html', serialize( parsedBlocks ) ); } } diff --git a/packages/block-editor/src/components/media-placeholder/index.js b/packages/block-editor/src/components/media-placeholder/index.js index 07a3f8829e71d..2f1a47be2f5a6 100644 --- a/packages/block-editor/src/components/media-placeholder/index.js +++ b/packages/block-editor/src/components/media-placeholder/index.js @@ -288,8 +288,10 @@ export function MediaPlaceholder( { } } - async function onHTMLDrop( HTML ) { - const blocks = pasteHandler( { HTML } ); + async function onDrop( event ) { + const blocks = pasteHandler( { + HTML: event.dataTransfer?.getData( 'default' ), + } ); return await handleBlocksDrop( blocks ); } @@ -379,9 +381,7 @@ export function MediaPlaceholder( { return null; } - return ( - - ); + return ; }; const renderCancelLink = () => { diff --git a/packages/block-library/src/block-keyboard-shortcuts/index.js b/packages/block-library/src/block-keyboard-shortcuts/index.js index 6d9cde8364001..2ce106b8ed99d 100644 --- a/packages/block-library/src/block-keyboard-shortcuts/index.js +++ b/packages/block-library/src/block-keyboard-shortcuts/index.js @@ -90,22 +90,36 @@ function BlockKeyboardShortcuts() { }, } ); } ); - }, [] ); + }, [ registerShortcut ] ); useShortcut( 'core/block-editor/transform-heading-to-paragraph', ( event ) => handleTransformHeadingAndParagraph( event, 0 ) ); - - [ 1, 2, 3, 4, 5, 6 ].forEach( ( level ) => { - //the loop is based off on a constant therefore - //the hook will execute the same way every time - //eslint-disable-next-line react-hooks/rules-of-hooks - useShortcut( - `core/block-editor/transform-paragraph-to-heading-${ level }`, - ( event ) => handleTransformHeadingAndParagraph( event, level ) - ); - } ); + useShortcut( + 'core/block-editor/transform-paragraph-to-heading-1', + ( event ) => handleTransformHeadingAndParagraph( event, 1 ) + ); + useShortcut( + 'core/block-editor/transform-paragraph-to-heading-2', + ( event ) => handleTransformHeadingAndParagraph( event, 2 ) + ); + useShortcut( + 'core/block-editor/transform-paragraph-to-heading-3', + ( event ) => handleTransformHeadingAndParagraph( event, 3 ) + ); + useShortcut( + 'core/block-editor/transform-paragraph-to-heading-4', + ( event ) => handleTransformHeadingAndParagraph( event, 4 ) + ); + useShortcut( + 'core/block-editor/transform-paragraph-to-heading-5', + ( event ) => handleTransformHeadingAndParagraph( event, 5 ) + ); + useShortcut( + 'core/block-editor/transform-paragraph-to-heading-6', + ( event ) => handleTransformHeadingAndParagraph( event, 6 ) + ); return null; } diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 2fddf51173e83..dc51518eb32f1 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -10,6 +10,7 @@ - `Popover`: Fix missing label of the headerTitle Close button ([#66813](https://github.com/WordPress/gutenberg/pull/66813)). - `ToggleGroupControl`: Fix active background for `0` value ([#66855](https://github.com/WordPress/gutenberg/pull/66855)). +- `SlotFill`: Fix a bug where a stale value of `fillProps` could be used ([#67000](https://github.com/WordPress/gutenberg/pull/67000)). ### Enhancements diff --git a/packages/components/src/slot-fill/bubbles-virtually/slot-fill-provider.tsx b/packages/components/src/slot-fill/bubbles-virtually/slot-fill-provider.tsx index 4d68db6fd175e..16a19c6569fda 100644 --- a/packages/components/src/slot-fill/bubbles-virtually/slot-fill-provider.tsx +++ b/packages/components/src/slot-fill/bubbles-virtually/slot-fill-provider.tsx @@ -34,15 +34,23 @@ function createSlotRegistry(): SlotFillBubblesVirtuallyContext { const unregisterSlot: SlotFillBubblesVirtuallyContext[ 'unregisterSlot' ] = ( name, ref ) => { + const slot = slots.get( name ); + if ( ! slot ) { + return; + } + // Make sure we're not unregistering a slot registered by another element // See https://github.com/WordPress/gutenberg/pull/19242#issuecomment-590295412 - if ( slots.get( name )?.ref === ref ) { - slots.delete( name ); + if ( slot.ref !== ref ) { + return; } + + slots.delete( name ); }; const updateSlot: SlotFillBubblesVirtuallyContext[ 'updateSlot' ] = ( name, + ref, fillProps ) => { const slot = slots.get( name ); @@ -50,6 +58,10 @@ function createSlotRegistry(): SlotFillBubblesVirtuallyContext { return; } + if ( slot.ref !== ref ) { + return; + } + if ( isShallowEqual( slot.fillProps, fillProps ) ) { return; } @@ -69,20 +81,18 @@ function createSlotRegistry(): SlotFillBubblesVirtuallyContext { fills.set( name, [ ...( fills.get( name ) || [] ), ref ] ); }; - const unregisterFill: SlotFillBubblesVirtuallyContext[ 'registerFill' ] = ( - name, - ref - ) => { - const fillsForName = fills.get( name ); - if ( ! fillsForName ) { - return; - } + const unregisterFill: SlotFillBubblesVirtuallyContext[ 'unregisterFill' ] = + ( name, ref ) => { + const fillsForName = fills.get( name ); + if ( ! fillsForName ) { + return; + } - fills.set( - name, - fillsForName.filter( ( fillRef ) => fillRef !== ref ) - ); - }; + fills.set( + name, + fillsForName.filter( ( fillRef ) => fillRef !== ref ) + ); + }; return { slots, diff --git a/packages/components/src/slot-fill/bubbles-virtually/slot.tsx b/packages/components/src/slot-fill/bubbles-virtually/slot.tsx index 6ac2d51e1f857..b8ead7fc7ea8b 100644 --- a/packages/components/src/slot-fill/bubbles-virtually/slot.tsx +++ b/packages/components/src/slot-fill/bubbles-virtually/slot.tsx @@ -35,29 +35,33 @@ function Slot( as, // `children` is not allowed. However, if it is passed, // it will be displayed as is, so remove `children`. - // @ts-ignore children, ...restProps } = props; const { registerSlot, unregisterSlot, ...registry } = useContext( SlotFillContext ); + const ref = useRef< HTMLElement >( null ); + // We don't want to unregister and register the slot whenever + // `fillProps` change, which would cause the fill to be re-mounted. Instead, + // we can just update the slot (see hook below). + // For more context, see https://github.com/WordPress/gutenberg/pull/44403#discussion_r994415973 + const fillPropsRef = useRef( fillProps ); + useLayoutEffect( () => { + fillPropsRef.current = fillProps; + }, [ fillProps ] ); + useLayoutEffect( () => { - registerSlot( name, ref, fillProps ); + registerSlot( name, ref, fillPropsRef.current ); return () => { unregisterSlot( name, ref ); }; - // We don't want to unregister and register the slot whenever - // `fillProps` change, which would cause the fill to be re-mounted. Instead, - // we can just update the slot (see hook below). - // For more context, see https://github.com/WordPress/gutenberg/pull/44403#discussion_r994415973 }, [ registerSlot, unregisterSlot, name ] ); - // fillProps may be an update that interacts with the layout, so we - // useLayoutEffect. + useLayoutEffect( () => { - registry.updateSlot( name, fillProps ); + registry.updateSlot( name, ref, fillPropsRef.current ); } ); return ( diff --git a/packages/components/src/slot-fill/bubbles-virtually/use-slot.ts b/packages/components/src/slot-fill/bubbles-virtually/use-slot.ts index d1d37e1d8e541..ec78771bfa92a 100644 --- a/packages/components/src/slot-fill/bubbles-virtually/use-slot.ts +++ b/packages/components/src/slot-fill/bubbles-virtually/use-slot.ts @@ -21,8 +21,10 @@ export default function useSlot( name: SlotKey ) { const api = useMemo( () => ( { - updateSlot: ( fillProps: FillProps ) => - registry.updateSlot( name, fillProps ), + updateSlot: ( + ref: SlotFillBubblesVirtuallySlotRef, + fillProps: FillProps + ) => registry.updateSlot( name, ref, fillProps ), unregisterSlot: ( ref: SlotFillBubblesVirtuallySlotRef ) => registry.unregisterSlot( name, ref ), registerFill: ( ref: SlotFillBubblesVirtuallyFillRef ) => diff --git a/packages/components/src/slot-fill/types.ts b/packages/components/src/slot-fill/types.ts index 1711e04cbb1f4..7e1b8b7e1f3f9 100644 --- a/packages/components/src/slot-fill/types.ts +++ b/packages/components/src/slot-fill/types.ts @@ -131,7 +131,11 @@ export type SlotFillBubblesVirtuallyContext = { name: SlotKey, ref: SlotFillBubblesVirtuallySlotRef ) => void; - updateSlot: ( name: SlotKey, fillProps: FillProps ) => void; + updateSlot: ( + name: SlotKey, + ref: SlotFillBubblesVirtuallySlotRef, + fillProps: FillProps + ) => void; registerFill: ( name: SlotKey, ref: SlotFillBubblesVirtuallyFillRef diff --git a/packages/dataviews/src/components/dataviews-item-actions/index.tsx b/packages/dataviews/src/components/dataviews-item-actions/index.tsx index 20e58a2c6bb1f..c1a1f84b99e62 100644 --- a/packages/dataviews/src/components/dataviews-item-actions/index.tsx +++ b/packages/dataviews/src/components/dataviews-item-actions/index.tsx @@ -59,6 +59,12 @@ interface CompactItemActionsProps< Item > { actions: Action< Item >[]; } +interface PrimaryActionsProps< Item > { + item: Item; + actions: Action< Item >[]; + registry: ReturnType< typeof useRegistry >; +} + function ButtonTrigger< Item >( { action, onClick, @@ -179,6 +185,13 @@ export function ActionsMenuGroup< Item >( { ); } +function hasOnlyOneActionAndIsPrimary< Item >( + primaryActions: Action< Item >[], + actions: Action< Item >[] +) { + return primaryActions.length === 1 && actions.length; +} + export default function ItemActions< Item >( { item, actions, @@ -199,9 +212,21 @@ export default function ItemActions< Item >( { eligibleActions: _eligibleActions, }; }, [ actions, item ] ); + if ( isCompact ) { return ; } + + if ( hasOnlyOneActionAndIsPrimary( primaryActions, actions ) ) { + return ( + + ); + } + return ( ( { width: 'auto', } } > - { !! primaryActions.length && - primaryActions.map( ( action ) => { - if ( 'RenderModal' in action ) { - return ( - - ); - } - return ( - { - action.callback( [ item ], { registry } ); - } } - items={ [ item ] } - /> - ); - } ) } + ); @@ -262,3 +269,36 @@ function CompactItemActions< Item >( { ); } + +function PrimaryActions< Item >( { + item, + actions, + registry, +}: PrimaryActionsProps< Item > ) { + if ( ! Array.isArray( actions ) || actions.length === 0 ) { + return null; + } + + return actions.map( ( action ) => { + if ( 'RenderModal' in action ) { + return ( + + ); + } + return ( + { + action.callback( [ item ], { registry } ); + } } + items={ [ item ] } + /> + ); + } ); +} diff --git a/packages/dataviews/src/dataforms-layouts/panel/style.scss b/packages/dataviews/src/dataforms-layouts/panel/style.scss index ae69c4ff45243..c7058f6366b3b 100644 --- a/packages/dataviews/src/dataforms-layouts/panel/style.scss +++ b/packages/dataviews/src/dataforms-layouts/panel/style.scss @@ -44,3 +44,7 @@ .dataforms-layouts-panel__dropdown-header { margin-bottom: $grid-unit-20; } + +.components-popover.components-dropdown__content.dataforms-layouts-panel__field-dropdown { + z-index: z-index(".components-popover.components-dropdown__content.dataforms-layouts-panel__field-dropdown"); +} diff --git a/packages/dataviews/src/dataviews-layouts/grid/index.tsx b/packages/dataviews/src/dataviews-layouts/grid/index.tsx index 91cc87ec7b35b..e218172b7900a 100644 --- a/packages/dataviews/src/dataviews-layouts/grid/index.tsx +++ b/packages/dataviews/src/dataviews-layouts/grid/index.tsx @@ -114,11 +114,9 @@ function GridItem< Item >( { justify="space-between" className="dataviews-view-grid__title-actions" > - -
- { renderedPrimaryField } -
-
+
+ { renderedPrimaryField } +
{ !! badgeFields?.length && ( diff --git a/packages/dataviews/src/dataviews-layouts/grid/style.scss b/packages/dataviews/src/dataviews-layouts/grid/style.scss index 55768240a1871..081adf51b5dc1 100644 --- a/packages/dataviews/src/dataviews-layouts/grid/style.scss +++ b/packages/dataviews/src/dataviews-layouts/grid/style.scss @@ -17,6 +17,8 @@ .dataviews-view-grid__primary-field { min-height: $grid-unit-40; // Preserve layout when there is no ellipsis button + display: flex; + align-items: center; &--clickable { width: fit-content; diff --git a/packages/dataviews/src/dataviews-layouts/list/index.tsx b/packages/dataviews/src/dataviews-layouts/list/index.tsx index a4f94e482c69b..47b9961d97df1 100644 --- a/packages/dataviews/src/dataviews-layouts/list/index.tsx +++ b/packages/dataviews/src/dataviews-layouts/list/index.tsx @@ -170,11 +170,13 @@ function ListItem< Item >( { ( action ) => action.isPrimary && !! action.icon ); return { - primaryAction: _primaryActions?.[ 0 ], + primaryAction: _primaryActions[ 0 ], eligibleActions: _eligibleActions, }; }, [ actions, item ] ); + const hasOnlyOnePrimaryAction = primaryAction && actions.length === 1; + const renderedMediaField = mediaField?.render ? (
@@ -194,33 +196,35 @@ function ListItem< Item >( { item={ item } /> ) } -
- - } + { ! hasOnlyOnePrimaryAction && ( +
+ + } + /> + } + placement="bottom-end" + > + - } - placement="bottom-end" - > - - -
+
+
+ ) } ); diff --git a/packages/edit-post/src/components/preferences-modal/enable-custom-fields.js b/packages/edit-post/src/components/preferences-modal/enable-custom-fields.js index b8125e96c7c2c..f3db9d123f9d7 100644 --- a/packages/edit-post/src/components/preferences-modal/enable-custom-fields.js +++ b/packages/edit-post/src/components/preferences-modal/enable-custom-fields.js @@ -4,7 +4,7 @@ import { useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { Button } from '@wordpress/components'; -import { withSelect } from '@wordpress/data'; +import { useSelect } from '@wordpress/data'; import { store as editorStore } from '@wordpress/editor'; import { privateApis as preferencesPrivateApis } from '@wordpress/preferences'; import { getPathAndQueryString } from '@wordpress/url'; @@ -57,7 +57,10 @@ export function CustomFieldsConfirmation( { willEnable } ) { ); } -export function EnableCustomFieldsOption( { label, areCustomFieldsEnabled } ) { +export default function EnableCustomFieldsOption( { label } ) { + const areCustomFieldsEnabled = useSelect( ( select ) => { + return !! select( editorStore ).getEditorSettings().enableCustomFields; + }, [] ); const [ isChecked, setIsChecked ] = useState( areCustomFieldsEnabled ); return ( @@ -72,8 +75,3 @@ export function EnableCustomFieldsOption( { label, areCustomFieldsEnabled } ) { ); } - -export default withSelect( ( select ) => ( { - areCustomFieldsEnabled: - !! select( editorStore ).getEditorSettings().enableCustomFields, -} ) )( EnableCustomFieldsOption ); diff --git a/packages/edit-post/src/components/preferences-modal/test/enable-custom-fields.js b/packages/edit-post/src/components/preferences-modal/test/enable-custom-fields.js index 2dbeadec8350a..adfa4a3df391d 100644 --- a/packages/edit-post/src/components/preferences-modal/test/enable-custom-fields.js +++ b/packages/edit-post/src/components/preferences-modal/test/enable-custom-fields.js @@ -4,27 +4,38 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +/** + * WordPress dependencies + */ +import { useSelect } from '@wordpress/data'; + /** * Internal dependencies */ import { - EnableCustomFieldsOption, + default as EnableCustomFieldsOption, CustomFieldsConfirmation, } from '../enable-custom-fields'; +jest.mock( '@wordpress/data/src/components/use-select', () => jest.fn() ); + +function setupUseSelectMock( areCustomFieldsEnabled ) { + useSelect.mockImplementation( () => { + return areCustomFieldsEnabled; + } ); +} + describe( 'EnableCustomFieldsOption', () => { it( 'renders a checked checkbox when custom fields are enabled', () => { - const { container } = render( - - ); + setupUseSelectMock( true ); + const { container } = render( ); expect( container ).toMatchSnapshot(); } ); it( 'renders an unchecked checkbox when custom fields are disabled', () => { - const { container } = render( - - ); + setupUseSelectMock( false ); + const { container } = render( ); expect( container ).toMatchSnapshot(); } ); @@ -32,9 +43,8 @@ describe( 'EnableCustomFieldsOption', () => { it( 'renders an unchecked checkbox and a confirmation message when toggled off', async () => { const user = userEvent.setup(); - const { container } = render( - - ); + setupUseSelectMock( true ); + const { container } = render( ); await user.click( screen.getByRole( 'checkbox' ) ); @@ -44,9 +54,8 @@ describe( 'EnableCustomFieldsOption', () => { it( 'renders a checked checkbox and a confirmation message when toggled on', async () => { const user = userEvent.setup(); - const { container } = render( - - ); + setupUseSelectMock( false ); + const { container } = render( ); await user.click( screen.getByRole( 'checkbox' ) ); diff --git a/packages/edit-site/src/components/post-edit/index.js b/packages/edit-site/src/components/post-edit/index.js index fbff29ed67afa..a535eef4ce787 100644 --- a/packages/edit-site/src/components/post-edit/index.js +++ b/packages/edit-site/src/components/post-edit/index.js @@ -18,10 +18,9 @@ import { privateApis as editorPrivateApis } from '@wordpress/editor'; * Internal dependencies */ import Page from '../page'; -import usePostFields from '../post-fields'; import { unlock } from '../../lock-unlock'; -const { PostCardPanel } = unlock( editorPrivateApis ); +const { PostCardPanel, usePostFields } = unlock( editorPrivateApis ); const fieldsWithBulkEditSupport = [ 'title', diff --git a/packages/edit-site/src/components/post-fields/index.js b/packages/edit-site/src/components/post-fields/index.js deleted file mode 100644 index 6ba9453709f0d..0000000000000 --- a/packages/edit-site/src/components/post-fields/index.js +++ /dev/null @@ -1,50 +0,0 @@ -/** - * WordPress dependencies - */ -import { - featuredImageField, - slugField, - parentField, - passwordField, - statusField, - commentStatusField, - titleField, - dateField, - authorField, -} from '@wordpress/fields'; -import { useMemo } from '@wordpress/element'; -import { useEntityRecords } from '@wordpress/core-data'; - -function usePostFields() { - const { records: authors, isResolving: isLoadingAuthors } = - useEntityRecords( 'root', 'user', { per_page: -1 } ); - - const fields = useMemo( - () => [ - featuredImageField, - titleField, - { - ...authorField, - elements: - authors?.map( ( { id, name } ) => ( { - value: id, - label: name, - } ) ) || [], - }, - statusField, - dateField, - slugField, - parentField, - commentStatusField, - passwordField, - ], - [ authors ] - ); - - return { - isLoading: isLoadingAuthors, - fields, - }; -} - -export default usePostFields; diff --git a/packages/edit-site/src/components/post-fields/style.scss b/packages/edit-site/src/components/post-fields/style.scss deleted file mode 100644 index adeaf9a267825..0000000000000 --- a/packages/edit-site/src/components/post-fields/style.scss +++ /dev/null @@ -1,3 +0,0 @@ -.components-popover.components-dropdown__content.dataforms-layouts-panel__field-dropdown { - z-index: z-index(".components-popover.components-dropdown__content.dataforms-layouts-panel__field-dropdown"); -} diff --git a/packages/edit-site/src/components/post-list/index.js b/packages/edit-site/src/components/post-list/index.js index 4639cb3c950b7..975809b2ad610 100644 --- a/packages/edit-site/src/components/post-list/index.js +++ b/packages/edit-site/src/components/post-list/index.js @@ -32,9 +32,8 @@ import AddNewPostModal from '../add-new-post'; import { unlock } from '../../lock-unlock'; import { useEditPostAction } from '../dataviews-actions'; import { usePrevious } from '@wordpress/compose'; -import usePostFields from '../post-fields'; -const { usePostActions } = unlock( editorPrivateApis ); +const { usePostActions, usePostFields } = unlock( editorPrivateApis ); const { useLocation, useHistory } = unlock( routerPrivateApis ); const { useEntityRecordsWithPermissions } = unlock( coreDataPrivateApis ); const EMPTY_ARRAY = []; diff --git a/packages/edit-site/src/style.scss b/packages/edit-site/src/style.scss index 0e5744fe362e3..63ad8244a7c95 100644 --- a/packages/edit-site/src/style.scss +++ b/packages/edit-site/src/style.scss @@ -30,7 +30,6 @@ @import "./components/editor-canvas-container/style.scss"; @import "./components/post-edit/style.scss"; @import "./components/post-list/style.scss"; -@import "./components/post-fields/style.scss"; @import "./components/resizable-frame/style.scss"; @import "./hooks/push-changes-to-global-styles/style.scss"; @import "./components/global-styles/font-library-modal/style.scss"; diff --git a/packages/editor/src/components/post-featured-image/index.js b/packages/editor/src/components/post-featured-image/index.js index cabd791e938bf..46a194f311a5e 100644 --- a/packages/editor/src/components/post-featured-image/index.js +++ b/packages/editor/src/components/post-featured-image/index.js @@ -1,3 +1,8 @@ +/** + * External dependencies + */ +import clsx from 'clsx'; + /** * WordPress dependencies */ @@ -10,9 +15,10 @@ import { withNotices, withFilters, __experimentalHStack as HStack, + Notice, } from '@wordpress/components'; import { isBlobURL } from '@wordpress/blob'; -import { useState, useRef } from '@wordpress/element'; +import { useState, useRef, useEffect } from '@wordpress/element'; import { compose } from '@wordpress/compose'; import { useSelect, withDispatch, withSelect } from '@wordpress/data'; import { @@ -94,11 +100,19 @@ function PostFeaturedImage( { postType, noticeUI, noticeOperations, + isRequestingFeaturedImageMedia, } ) { const toggleRef = useRef(); const [ isLoading, setIsLoading ] = useState( false ); const { getSettings } = useSelect( blockEditorStore ); const { mediaSourceUrl } = getMediaDetails( media, currentPostId ); + const toggleFocusTimerRef = useRef(); + + useEffect( () => { + return () => { + clearTimeout( toggleFocusTimerRef.current ); + }; + }, [] ); function onDropFiles( filesList ) { getSettings().mediaUpload( { @@ -150,6 +164,9 @@ function PostFeaturedImage( { ); } + const isMissingMedia = + ! isRequestingFeaturedImageMedia && !! featuredImageId && ! media; + return ( { noticeUI } @@ -174,52 +191,83 @@ function PostFeaturedImage( { modalClass="editor-post-featured-image__media-modal" render={ ( { open } ) => (
- + { isMissingMedia ? ( + + { __( + 'Could not retrieve the featured image data.' + ) } + + ) : ( + + ) } { !! featuredImageId && ( - + @@ -228,8 +276,19 @@ function PostFeaturedImage( { className="editor-post-featured-image__action" onClick={ () => { onRemoveImage(); - toggleRef.current.focus(); + // The toggle button is rendered conditionally, we need + // to wait it is rendered before moving focus to it. + toggleFocusTimerRef.current = + setTimeout( () => { + toggleRef.current?.focus(); + } ); } } + variant={ + isMissingMedia + ? 'secondary' + : undefined + } + isDestructive={ isMissingMedia } > { __( 'Remove' ) } @@ -247,7 +306,8 @@ function PostFeaturedImage( { } const applyWithSelect = withSelect( ( select ) => { - const { getMedia, getPostType } = select( coreStore ); + const { getMedia, getPostType, hasFinishedResolution } = + select( coreStore ); const { getCurrentPostId, getEditedPostAttribute } = select( editorStore ); const featuredImageId = getEditedPostAttribute( 'featured_media' ); @@ -258,6 +318,12 @@ const applyWithSelect = withSelect( ( select ) => { currentPostId: getCurrentPostId(), postType: getPostType( getEditedPostAttribute( 'type' ) ), featuredImageId, + isRequestingFeaturedImageMedia: + !! featuredImageId && + ! hasFinishedResolution( 'getMedia', [ + featuredImageId, + { context: 'view' }, + ] ), }; } ); diff --git a/packages/editor/src/components/post-featured-image/style.scss b/packages/editor/src/components/post-featured-image/style.scss index 30d5cb43403cd..bf9433faa662d 100644 --- a/packages/editor/src/components/post-featured-image/style.scss +++ b/packages/editor/src/components/post-featured-image/style.scss @@ -16,11 +16,16 @@ &:hover, &:focus, &:focus-within { - .editor-post-featured-image__actions { + .editor-post-featured-image__actions:not(.editor-post-featured-image__actions-is-requesting-image) { opacity: 1; } } + .editor-post-featured-image__actions.editor-post-featured-image__actions-missing-image { + opacity: 1; + margin-top: $grid-unit-20; + } + .components-drop-zone__content { border-radius: $radius-small; } @@ -72,17 +77,22 @@ } .editor-post-featured-image__actions { - @include reduce-motion("transition"); - bottom: 0; - opacity: 0; // Use opacity instead of visibility so that the buttons remain in the tab order. - padding: $grid-unit-10; - position: absolute; - transition: opacity 50ms ease-out; -} + &:not(.editor-post-featured-image__actions-missing-image) { + @include reduce-motion("transition"); + bottom: 0; + opacity: 0; // Use opacity instead of visibility so that the buttons remain in the tab order. + padding: $grid-unit-10; + position: absolute; + transition: opacity 50ms ease-out; -.editor-post-featured-image__action { - backdrop-filter: blur(16px) saturate(180%); - background: rgba(255, 255, 255, 0.75); - flex-grow: 1; - justify-content: center; + .editor-post-featured-image__action { + backdrop-filter: blur(16px) saturate(180%); + background: rgba(255, 255, 255, 0.75); + } + } + + .editor-post-featured-image__action { + flex-grow: 1; + justify-content: center; + } } diff --git a/packages/editor/src/components/post-fields/index.ts b/packages/editor/src/components/post-fields/index.ts new file mode 100644 index 0000000000000..3d675ab763d64 --- /dev/null +++ b/packages/editor/src/components/post-fields/index.ts @@ -0,0 +1,65 @@ +/** + * WordPress dependencies + */ +import { useMemo } from '@wordpress/element'; +import { useEntityRecords } from '@wordpress/core-data'; +import type { Field } from '@wordpress/dataviews'; +import { + featuredImageField, + slugField, + parentField, + passwordField, + statusField, + commentStatusField, + titleField, + dateField, + authorField, +} from '@wordpress/fields'; +import type { BasePostWithEmbeddedAuthor } from '@wordpress/fields'; + +interface UsePostFieldsReturn { + isLoading: boolean; + fields: Field< BasePostWithEmbeddedAuthor >[]; +} + +interface Author { + id: number; + name: string; +} + +function usePostFields(): UsePostFieldsReturn { + const { records: authors, isResolving: isLoadingAuthors } = + useEntityRecords< Author >( 'root', 'user', { per_page: -1 } ); + + const fields = useMemo( + () => + [ + featuredImageField, + titleField, + { + ...authorField, + elements: authors?.map( ( { id, name } ) => ( { + value: id, + label: name, + } ) ), + }, + statusField, + dateField, + slugField, + parentField, + commentStatusField, + passwordField, + ] as Field< BasePostWithEmbeddedAuthor >[], + [ authors ] + ); + + return { + isLoading: isLoadingAuthors, + fields, + }; +} + +/** + * Hook to get the fields for a post (BasePost or BasePostWithEmbeddedAuthor). + */ +export default usePostFields; diff --git a/packages/editor/src/private-apis.js b/packages/editor/src/private-apis.js index f9a6d4d17904e..b49b2a69a3bf2 100644 --- a/packages/editor/src/private-apis.js +++ b/packages/editor/src/private-apis.js @@ -16,6 +16,7 @@ import PluginPostExcerpt from './components/post-excerpt/plugin'; import PostCardPanel from './components/post-card-panel'; import PreferencesModal from './components/preferences-modal'; import { usePostActions } from './components/post-actions/actions'; +import usePostFields from './components/post-fields'; import ToolsMoreMenuGroup from './components/more-menu/tools-more-menu-group'; import ViewMoreMenuGroup from './components/more-menu/view-more-menu-group'; import ResizableEditor from './components/resizable-editor'; @@ -40,6 +41,7 @@ lock( privateApis, { PostCardPanel, PreferencesModal, usePostActions, + usePostFields, ToolsMoreMenuGroup, ViewMoreMenuGroup, ResizableEditor, diff --git a/packages/fields/src/index.ts b/packages/fields/src/index.ts index 4c721b85b61a4..41879a86e76be 100644 --- a/packages/fields/src/index.ts +++ b/packages/fields/src/index.ts @@ -1,2 +1,3 @@ export * from './fields'; export * from './actions'; +export type * from './types'; diff --git a/test/e2e/specs/editor/blocks/image.spec.js b/test/e2e/specs/editor/blocks/image.spec.js index 6110a125ff6f7..b2195f2c67688 100644 --- a/test/e2e/specs/editor/blocks/image.spec.js +++ b/test/e2e/specs/editor/blocks/image.spec.js @@ -545,7 +545,7 @@ test.describe( 'Image', () => { dummy.style.left = 0; dummy.draggable = 'true'; dummy.addEventListener( 'dragstart', ( event ) => { - event.dataTransfer.setData( 'text/html', _html ); + event.dataTransfer.setData( 'default', _html ); setTimeout( () => { dummy.remove(); }, 0 );