diff --git a/package-lock.json b/package-lock.json index 180c1aa08814a1..297507f7291cca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17050,6 +17050,7 @@ "@wordpress/editor": "file:packages/editor", "@wordpress/element": "file:packages/element", "@wordpress/hooks": "file:packages/hooks", + "@wordpress/html-entities": "file:packages/html-entities", "@wordpress/i18n": "file:packages/i18n", "@wordpress/icons": "file:packages/icons", "@wordpress/interface": "file:packages/interface", @@ -30764,7 +30765,7 @@ "css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", - "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "integrity": "sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s=", "dev": true }, "cssesc": { @@ -43688,7 +43689,7 @@ "lz-string": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz", - "integrity": "sha512-0ckx7ZHRPqb0oUm8zNr+90mtf9DQB60H1wMCjBtfi62Kl3a7JbHob6gA2bC+xRvZoOL+1hzUK8jeuEIQE8svEQ==", + "integrity": "sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY=", "dev": true }, "macos-release": { diff --git a/packages/block-editor/src/components/inspector-popover-header/index.js b/packages/block-editor/src/components/inspector-popover-header/index.js index e5f492eddd2b77..f66f2fca4c640d 100644 --- a/packages/block-editor/src/components/inspector-popover-header/index.js +++ b/packages/block-editor/src/components/inspector-popover-header/index.js @@ -1,3 +1,8 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + /** * WordPress dependencies */ @@ -13,13 +18,20 @@ import { closeSmall } from '@wordpress/icons'; import { __ } from '@wordpress/i18n'; export default function InspectorPopoverHeader( { + className, title, help, actions = [], onClose, } ) { return ( - + { await page.keyboard.type( 'Excerpt from content.' ); await page.type( '.editor-post-title__input', 'A published post' ); - // Open the excerpt panel. + // Add an excerpt. await openDocumentSettingsSidebar(); - const excerptButton = await findSidebarPanelToggleButtonWithTitle( - 'Excerpt' + await page.waitForSelector( + '*[aria-label="Editor settings"] *[aria-label="Add excerpt"]' ); - if ( excerptButton ) { - await excerptButton.click( 'button' ); - } - - await page.waitForSelector( '.editor-post-excerpt textarea' ); - await page.type( - '.editor-post-excerpt textarea', + '*[aria-label="Editor settings"] *[aria-label="Add excerpt"]', 'Explicitly set excerpt.' ); diff --git a/packages/e2e-tests/specs/editor/various/new-post-default-content.test.js b/packages/e2e-tests/specs/editor/various/new-post-default-content.test.js index e61927e10ff17f..9e3b057bf53770 100644 --- a/packages/e2e-tests/specs/editor/various/new-post-default-content.test.js +++ b/packages/e2e-tests/specs/editor/various/new-post-default-content.test.js @@ -5,7 +5,6 @@ import { activatePlugin, createNewPost, deactivatePlugin, - findSidebarPanelToggleButtonWithTitle, getEditedPostContent, openDocumentSettingsSidebar, } from '@wordpress/e2e-test-utils'; @@ -33,14 +32,8 @@ describe( 'new editor filtered state', () => { // open the sidebar, we want to see the excerpt. await openDocumentSettingsSidebar(); - const excerptButton = await findSidebarPanelToggleButtonWithTitle( - 'Excerpt' - ); - if ( excerptButton ) { - await excerptButton.click( 'button' ); - } const excerpt = await page.$eval( - '.editor-post-excerpt textarea', + '*[aria-label="Editor settings"] *[aria-label="Add excerpt"]', ( element ) => element.innerHTML ); diff --git a/packages/e2e-tests/specs/editor/various/sidebar.test.js b/packages/e2e-tests/specs/editor/various/sidebar.test.js index 39409b0fbeb7bc..c6a5498056e62d 100644 --- a/packages/e2e-tests/specs/editor/various/sidebar.test.js +++ b/packages/e2e-tests/specs/editor/various/sidebar.test.js @@ -126,10 +126,6 @@ describe( 'Sidebar', () => { expect( await findSidebarPanelWithTitle( 'Categories' ) ).toBeDefined(); expect( await findSidebarPanelWithTitle( 'Tags' ) ).toBeDefined(); - expect( - await findSidebarPanelWithTitle( 'Featured image' ) - ).toBeDefined(); - expect( await findSidebarPanelWithTitle( 'Excerpt' ) ).toBeDefined(); expect( await findSidebarPanelWithTitle( 'Discussion' ) ).toBeDefined(); expect( await findSidebarPanelWithTitle( 'Summary' ) ).toBeDefined(); @@ -138,8 +134,6 @@ describe( 'Sidebar', () => { removeEditorPanel( 'taxonomy-panel-category' ); removeEditorPanel( 'taxonomy-panel-post_tag' ); - removeEditorPanel( 'featured-image' ); - removeEditorPanel( 'post-excerpt' ); removeEditorPanel( 'discussion-panel' ); removeEditorPanel( 'post-status' ); } ); @@ -154,12 +148,6 @@ describe( 'Sidebar', () => { expect( await page.$x( getPanelToggleSelector( 'Tags' ) ) ).toEqual( [] ); - expect( - await page.$x( getPanelToggleSelector( 'Featured image' ) ) - ).toEqual( [] ); - expect( await page.$x( getPanelToggleSelector( 'Excerpt' ) ) ).toEqual( - [] - ); expect( await page.$x( getPanelToggleSelector( 'Discussion' ) ) ).toEqual( [] ); diff --git a/packages/edit-post/package.json b/packages/edit-post/package.json index 5aa7395126f8f5..992dd8c38b8ab7 100644 --- a/packages/edit-post/package.json +++ b/packages/edit-post/package.json @@ -40,6 +40,7 @@ "@wordpress/editor": "file:../editor", "@wordpress/element": "file:../element", "@wordpress/hooks": "file:../hooks", + "@wordpress/html-entities": "file:../html-entities", "@wordpress/i18n": "file:../i18n", "@wordpress/icons": "file:../icons", "@wordpress/interface": "file:../interface", diff --git a/packages/edit-post/src/components/preferences-modal/index.js b/packages/edit-post/src/components/preferences-modal/index.js index 9e3e1a53f4a870..997ced517b897a 100644 --- a/packages/edit-post/src/components/preferences-modal/index.js +++ b/packages/edit-post/src/components/preferences-modal/index.js @@ -13,9 +13,7 @@ import { useSelect, useDispatch } from '@wordpress/data'; import { useMemo } from '@wordpress/element'; import { PostTaxonomies, - PostExcerptCheck, PageAttributesCheck, - PostFeaturedImageCheck, PostTypeSupportCheck, store as editorStore, } from '@wordpress/editor'; @@ -205,18 +203,6 @@ export default function EditPostPreferencesModal() { /> ) } /> - - - - - - diff --git a/packages/edit-post/src/components/preferences-modal/test/__snapshots__/index.js.snap b/packages/edit-post/src/components/preferences-modal/test/__snapshots__/index.js.snap index 8a1de0ddfb75b8..690be2028c7c2a 100644 --- a/packages/edit-post/src/components/preferences-modal/test/__snapshots__/index.js.snap +++ b/packages/edit-post/src/components/preferences-modal/test/__snapshots__/index.js.snap @@ -94,19 +94,7 @@ exports[`EditPostPreferencesModal should match snapshot when the modal is active - - - - - - - - + - - - - - - - - + - - - - - ); -} - -const applyWithSelect = withSelect( ( select ) => { - const { getEditedPostAttribute } = select( editorStore ); - const { getPostType } = select( coreStore ); - const { isEditorPanelEnabled, isEditorPanelOpened } = - select( editPostStore ); - - return { - postType: getPostType( getEditedPostAttribute( 'type' ) ), - isEnabled: isEditorPanelEnabled( PANEL_NAME ), - isOpened: isEditorPanelOpened( PANEL_NAME ), - }; -} ); - -const applyWithDispatch = withDispatch( ( dispatch ) => { - const { toggleEditorPanelOpened } = dispatch( editPostStore ); - - return { - onTogglePanel: partial( toggleEditorPanelOpened, PANEL_NAME ), - }; -} ); - -export default compose( applyWithSelect, applyWithDispatch )( FeaturedImage ); diff --git a/packages/edit-post/src/components/sidebar/post-excerpt/index.js b/packages/edit-post/src/components/sidebar/post-excerpt/index.js deleted file mode 100644 index b2d56808d64c79..00000000000000 --- a/packages/edit-post/src/components/sidebar/post-excerpt/index.js +++ /dev/null @@ -1,56 +0,0 @@ -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; -import { PanelBody } from '@wordpress/components'; -import { - PostExcerpt as PostExcerptForm, - PostExcerptCheck, -} from '@wordpress/editor'; -import { compose } from '@wordpress/compose'; -import { withSelect, withDispatch } from '@wordpress/data'; - -/** - * Internal dependencies - */ -import { store as editPostStore } from '../../../store'; - -/** - * Module Constants - */ -const PANEL_NAME = 'post-excerpt'; - -function PostExcerpt( { isEnabled, isOpened, onTogglePanel } ) { - if ( ! isEnabled ) { - return null; - } - - return ( - - - - - - ); -} - -export default compose( [ - withSelect( ( select ) => { - return { - isEnabled: - select( editPostStore ).isEditorPanelEnabled( PANEL_NAME ), - isOpened: select( editPostStore ).isEditorPanelOpened( PANEL_NAME ), - }; - } ), - withDispatch( ( dispatch ) => ( { - onTogglePanel() { - return dispatch( editPostStore ).toggleEditorPanelOpened( - PANEL_NAME - ); - }, - } ) ), -] )( PostExcerpt ); diff --git a/packages/edit-post/src/components/sidebar/post-featured-image/index.js b/packages/edit-post/src/components/sidebar/post-featured-image/index.js new file mode 100644 index 00000000000000..2444fe21a832dc --- /dev/null +++ b/packages/edit-post/src/components/sidebar/post-featured-image/index.js @@ -0,0 +1,18 @@ +/** + * WordPress dependencies + */ +import { + PostFeaturedImageCheck, + PostFeaturedImage as PostFeaturedImageForm, +} from '@wordpress/editor'; +import { MediaUploadCheck } from '@wordpress/block-editor'; + +export default function PostFeaturedImage() { + return ( + + + + + + ); +} diff --git a/packages/edit-post/src/components/sidebar/post-featured-image/style.scss b/packages/edit-post/src/components/sidebar/post-featured-image/style.scss new file mode 100644 index 00000000000000..6c35d935663a87 --- /dev/null +++ b/packages/edit-post/src/components/sidebar/post-featured-image/style.scss @@ -0,0 +1,11 @@ +.edit-post-post-featured-image { + .editor-post-featured-image__container { + margin-left: -$grid-unit-20; + margin-right: -$grid-unit-20; + } + + .editor-post-featured-image__toggle { + border-bottom: $border-width solid $gray-200; + border-top: $border-width solid $gray-200; + } +} diff --git a/packages/edit-post/src/components/sidebar/post-status/index.js b/packages/edit-post/src/components/sidebar/post-status/index.js index 7470cf76ce7ddf..7ba67358149eb2 100644 --- a/packages/edit-post/src/components/sidebar/post-status/index.js +++ b/packages/edit-post/src/components/sidebar/post-status/index.js @@ -9,6 +9,8 @@ import { compose, ifCondition } from '@wordpress/compose'; /** * Internal dependencies */ +import PostFeaturedImage from '../post-featured-image'; +import PostSummary from '../post-summary'; import PostVisibility from '../post-visibility'; import PostTrash from '../post-trash'; import PostSchedule from '../post-schedule'; @@ -38,6 +40,8 @@ function PostStatus( { isOpened, onTogglePanel } ) { { ( fills ) => ( <> + + diff --git a/packages/edit-post/src/components/sidebar/post-status/style.scss b/packages/edit-post/src/components/sidebar/post-status/style.scss index 381c0744967962..a0d64858083f1a 100644 --- a/packages/edit-post/src/components/sidebar/post-status/style.scss +++ b/packages/edit-post/src/components/sidebar/post-status/style.scss @@ -1,5 +1,7 @@ -.edit-post-post-status .edit-post-post-publish-dropdown__switch-to-draft { - margin-top: 15px; - width: 100%; - text-align: center; +.edit-post-post-status { + // Counteract the 5px of bottom margin on .components-panel__body-title. + .edit-post-post-featured-image:first-of-type, + .edit-post-post-summary:first-of-type { + margin-top: -5px; + } } diff --git a/packages/edit-post/src/components/sidebar/post-summary/excerpt.js b/packages/edit-post/src/components/sidebar/post-summary/excerpt.js new file mode 100644 index 00000000000000..6ce844b12f3d80 --- /dev/null +++ b/packages/edit-post/src/components/sidebar/post-summary/excerpt.js @@ -0,0 +1,26 @@ +/** + * WordPress dependencies + */ +import { useSelect, useDispatch } from '@wordpress/data'; +import { store as editorStore } from '@wordpress/editor'; +import { PlainText } from '@wordpress/block-editor'; +import { __ } from '@wordpress/i18n'; + +export default function PostSummaryExcerpt() { + const excerpt = useSelect( + ( select ) => select( editorStore ).getEditedPostAttribute( 'excerpt' ), + [] + ); + + const { editPost } = useDispatch( editorStore ); + + return ( + editPost( { excerpt: value } ) } + /> + ); +} diff --git a/packages/edit-post/src/components/sidebar/post-summary/index.js b/packages/edit-post/src/components/sidebar/post-summary/index.js new file mode 100644 index 00000000000000..3d48169cae331c --- /dev/null +++ b/packages/edit-post/src/components/sidebar/post-summary/index.js @@ -0,0 +1,23 @@ +/** + * WordPress dependencies + */ +import { usePostTypeSupportCheck } from '@wordpress/editor'; + +/** + * Internal dependencies + */ +import PostSummaryTitle from './title'; +import PostSummaryExcerpt from './excerpt'; + +export default function PostSummary() { + const hasPostTitle = usePostTypeSupportCheck( 'title' ); + const hasPostExcerpt = usePostTypeSupportCheck( 'excerpt' ); + return ( + ( hasPostTitle || hasPostExcerpt ) && ( + <div className="edit-post-post-summary"> + { hasPostTitle && <PostSummaryTitle /> } + { hasPostExcerpt && <PostSummaryExcerpt /> } + </div> + ) + ); +} diff --git a/packages/edit-post/src/components/sidebar/post-summary/style.scss b/packages/edit-post/src/components/sidebar/post-summary/style.scss new file mode 100644 index 00000000000000..8e806a4c3a978d --- /dev/null +++ b/packages/edit-post/src/components/sidebar/post-summary/style.scss @@ -0,0 +1,21 @@ +.edit-post-post-summary { + border-bottom: $border-width solid $gray-200; + border-top: $border-width solid $gray-200; + margin-left: -$grid-unit-20; + margin-right: -$grid-unit-20; + padding: $grid-unit-10 $grid-unit-20 $grid-unit-20; + + .edit-post-post-featured-image + & { + margin-top: -1px; + } +} + +.edit-post-post-summary__title, +.edit-post-post-summary__excerpt { + margin-top: $grid-unit-10; +} + +.edit-post-post-summary__title { + font-size: 16px; + font-weight: 600; +} diff --git a/packages/edit-post/src/components/sidebar/post-summary/title.js b/packages/edit-post/src/components/sidebar/post-summary/title.js new file mode 100644 index 00000000000000..6efc25811187d5 --- /dev/null +++ b/packages/edit-post/src/components/sidebar/post-summary/title.js @@ -0,0 +1,34 @@ +/** + * WordPress dependencies + */ +import { useSelect, useDispatch } from '@wordpress/data'; +import { store as editorStore } from '@wordpress/editor'; +import { PlainText } from '@wordpress/block-editor'; +import { decodeEntities } from '@wordpress/html-entities'; +import { __ } from '@wordpress/i18n'; + +export default function PostSummaryTitle() { + const { title, titlePlaceholder } = useSelect( + ( select ) => ( { + title: select( editorStore ).getEditedPostAttribute( 'title' ), + titlePlaceholder: + select( editorStore ).getEditorSettings().titlePlaceholder, + } ), + [] + ); + + const { editPost } = useDispatch( editorStore ); + + return ( + <PlainText + __experimentalVersion={ 2 } + className="edit-post-post-summary__title" + placeholder={ + decodeEntities( titlePlaceholder ) || __( 'Add title' ) + } + disableLineBreaks + value={ title } + onChange={ ( value ) => editPost( { title: value } ) } + /> + ); +} diff --git a/packages/edit-post/src/components/sidebar/settings-sidebar/index.js b/packages/edit-post/src/components/sidebar/settings-sidebar/index.js index c044d0b49714cd..6e354149ff2af7 100644 --- a/packages/edit-post/src/components/sidebar/settings-sidebar/index.js +++ b/packages/edit-post/src/components/sidebar/settings-sidebar/index.js @@ -17,8 +17,6 @@ import SettingsHeader from '../settings-header'; import PostStatus from '../post-status'; import LastRevision from '../last-revision'; import PostTaxonomies from '../post-taxonomies'; -import FeaturedImage from '../featured-image'; -import PostExcerpt from '../post-excerpt'; import DiscussionPanel from '../discussion-panel'; import PageAttributes from '../page-attributes'; import MetaBoxes from '../../meta-boxes'; @@ -87,8 +85,6 @@ const SettingsSidebar = () => { <PluginDocumentSettingPanel.Slot /> <LastRevision /> <PostTaxonomies /> - <FeaturedImage /> - <PostExcerpt /> <DiscussionPanel /> <PageAttributes /> <MetaBoxes location="side" /> diff --git a/packages/edit-post/src/style.scss b/packages/edit-post/src/style.scss index 1c9e1d182eef21..6e84ff997210c2 100644 --- a/packages/edit-post/src/style.scss +++ b/packages/edit-post/src/style.scss @@ -11,10 +11,12 @@ @import "./components/sidebar/style.scss"; @import "./components/sidebar/last-revision/style.scss"; @import "./components/sidebar/post-author/style.scss"; +@import "./components/sidebar/post-featured-image/style.scss"; @import "./components/sidebar/post-format/style.scss"; @import "./components/sidebar/post-schedule/style.scss"; @import "./components/sidebar/post-slug/style.scss"; @import "./components/sidebar/post-status/style.scss"; +@import "./components/sidebar/post-summary/style.scss"; @import "./components/sidebar/post-template/style.scss"; @import "./components/sidebar/post-url/style.scss"; @import "./components/sidebar/post-visibility/style.scss"; diff --git a/packages/editor/src/components/index.js b/packages/editor/src/components/index.js index 3ea8aa0042ab73..e27c84ef85f96f 100644 --- a/packages/editor/src/components/index.js +++ b/packages/editor/src/components/index.js @@ -58,7 +58,10 @@ export { default as PostTextEditor } from './post-text-editor'; export { default as PostTitle } from './post-title'; export { default as PostTrash } from './post-trash'; export { default as PostTrashCheck } from './post-trash/check'; -export { default as PostTypeSupportCheck } from './post-type-support-check'; +export { + default as PostTypeSupportCheck, + usePostTypeSupportCheck, +} from './post-type-support-check'; export { default as PostURL } from './post-url'; export { default as PostURLCheck } from './post-url/check'; export { default as PostURLLabel, usePostURLLabel } from './post-url/label'; diff --git a/packages/editor/src/components/post-excerpt/check.js b/packages/editor/src/components/post-excerpt/check.js index a94a5badec180c..441be668ee256e 100644 --- a/packages/editor/src/components/post-excerpt/check.js +++ b/packages/editor/src/components/post-excerpt/check.js @@ -1,10 +1,17 @@ +/** + * WordPress dependencies + */ +import deprecated from '@wordpress/deprecated'; + /** * Internal dependencies */ import PostTypeSupportCheck from '../post-type-support-check'; -function PostExcerptCheck( props ) { +export default function PostExcerptCheck( props ) { + deprecated( 'PostExcerptCheck', { + since: '6.1', + alternative: '<PostTypeSupportCheck supportKeys="excerpt">', + } ); return <PostTypeSupportCheck { ...props } supportKeys="excerpt" />; } - -export default PostExcerptCheck; diff --git a/packages/editor/src/components/post-featured-image/index.js b/packages/editor/src/components/post-featured-image/index.js index 800177adfccd04..d87ae7b718169e 100644 --- a/packages/editor/src/components/post-featured-image/index.js +++ b/packages/editor/src/components/post-featured-image/index.js @@ -2,6 +2,7 @@ * External dependencies */ import { has, get } from 'lodash'; +import classnames from 'classnames'; /** * WordPress dependencies @@ -10,30 +11,29 @@ import { __, sprintf } from '@wordpress/i18n'; import { applyFilters } from '@wordpress/hooks'; import { DropZone, - Button, - Spinner, - ResponsiveWrapper, withNotices, withFilters, + Dropdown, } from '@wordpress/components'; import { isBlobURL } from '@wordpress/blob'; -import { useState } from '@wordpress/element'; +import { useRef, useState } from '@wordpress/element'; import { compose } from '@wordpress/compose'; import { useSelect, withDispatch, withSelect } from '@wordpress/data'; -import { - MediaUpload, - MediaUploadCheck, - store as blockEditorStore, -} from '@wordpress/block-editor'; +import { store as blockEditorStore } from '@wordpress/block-editor'; import { store as coreStore } from '@wordpress/core-data'; /** * Internal dependencies */ import PostFeaturedImageCheck from './check'; +import PostFeaturedImageUploadProvider from './upload-provider'; +import PostFeaturedImageToggle from './toggle'; +import PostFeaturedImagePreview from './preview'; +import PostFeaturedImageMenu from './menu'; import { store as editorStore } from '../../store'; const ALLOWED_MEDIA_TYPES = [ 'image' ]; +const ALLOWED_UPLOAD_TYPES = 'image/*'; // Used when labels from post type were not yet loaded or when they are not present. const DEFAULT_FEATURE_IMAGE_LABEL = __( 'Featured image' ); @@ -50,7 +50,7 @@ const instructions = ( function getMediaDetails( media, postId ) { if ( ! media ) { - return {}; + return null; } const defaultSize = applyFilters( @@ -61,9 +61,9 @@ function getMediaDetails( media, postId ) { ); if ( has( media, [ 'media_details', 'sizes', defaultSize ] ) ) { return { - mediaWidth: media.media_details.sizes[ defaultSize ].width, - mediaHeight: media.media_details.sizes[ defaultSize ].height, - mediaSourceUrl: media.media_details.sizes[ defaultSize ].source_url, + width: media.media_details.sizes[ defaultSize ].width, + height: media.media_details.sizes[ defaultSize ].height, + sourceUrl: media.media_details.sizes[ defaultSize ].source_url, }; } @@ -76,22 +76,22 @@ function getMediaDetails( media, postId ) { ); if ( has( media, [ 'media_details', 'sizes', fallbackSize ] ) ) { return { - mediaWidth: media.media_details.sizes[ fallbackSize ].width, - mediaHeight: media.media_details.sizes[ fallbackSize ].height, - mediaSourceUrl: - media.media_details.sizes[ fallbackSize ].source_url, + width: media.media_details.sizes[ fallbackSize ].width, + height: media.media_details.sizes[ fallbackSize ].height, + sourceUrl: media.media_details.sizes[ fallbackSize ].source_url, }; } // Use full image size when fallbackSize and defaultSize are not available. return { - mediaWidth: media.media_details.width, - mediaHeight: media.media_details.height, - mediaSourceUrl: media.source_url, + width: media.media_details.width, + height: media.media_details.height, + sourceUrl: media.source_url, }; } function PostFeaturedImage( { + className, currentPostId, featuredImageId, onUpdateImage, @@ -101,19 +101,20 @@ function PostFeaturedImage( { noticeUI, noticeOperations, } ) { + const menuAnchorRef = useRef(); + const [ isLoading, setIsLoading ] = useState( false ); + const mediaUpload = useSelect( ( select ) => { return select( blockEditorStore ).getSettings().mediaUpload; }, [] ); + const postLabel = get( postType, [ 'labels' ], {} ); - const { mediaWidth, mediaHeight, mediaSourceUrl } = getMediaDetails( - media, - currentPostId - ); + const mediaDetails = getMediaDetails( media, currentPostId ); - function onDropFiles( filesList ) { + function processUpload( filesList ) { mediaUpload( { - allowedTypes: [ 'image' ], + allowedTypes: ALLOWED_MEDIA_TYPES, filesList, onFileChange( [ image ] ) { if ( isBlobURL( image?.url ) ) { @@ -133,7 +134,12 @@ function PostFeaturedImage( { return ( <PostFeaturedImageCheck> { noticeUI } - <div className="editor-post-featured-image"> + <div + className={ classnames( + 'editor-post-featured-image', + className + ) } + > { media && ( <div id={ `editor-post-featured-image-${ featuredImageId }-describedby` } @@ -156,92 +162,75 @@ function PostFeaturedImage( { ) } </div> ) } - <MediaUploadCheck fallback={ instructions }> - <MediaUpload - title={ - postLabel.featured_image || - DEFAULT_FEATURE_IMAGE_LABEL - } - onSelect={ onUpdateImage } - unstableFeaturedImageFlow - allowedTypes={ ALLOWED_MEDIA_TYPES } - modalClass="editor-post-featured-image__media-modal" - render={ ( { open } ) => ( - <div className="editor-post-featured-image__container"> - <Button - className={ - ! featuredImageId - ? 'editor-post-featured-image__toggle' - : 'editor-post-featured-image__preview' - } - onClick={ open } - aria-label={ - ! featuredImageId - ? null - : __( 'Edit or update the image' ) - } - aria-describedby={ - ! featuredImageId - ? null - : `editor-post-featured-image-${ featuredImageId }-describedby` - } - > - { !! featuredImageId && media && ( - <ResponsiveWrapper - naturalWidth={ mediaWidth } - naturalHeight={ mediaHeight } - isInline + <PostFeaturedImageUploadProvider + fallback={ instructions } + title={ + postLabel.featured_image || DEFAULT_FEATURE_IMAGE_LABEL + } + selectedId={ featuredImageId } + allowedMediaTypes={ ALLOWED_MEDIA_TYPES } + allowedUploadTypes={ ALLOWED_UPLOAD_TYPES } + onSelect={ onUpdateImage } + onUpload={ processUpload } + > + { ( { openMediaLibrary, openFileDialog } ) => ( + <div className="editor-post-featured-image__container"> + <Dropdown + className="editor-post-featured-image__dropdown" + position={ + featuredImageId + ? 'bottom left' + : 'bottom center' + } + popoverProps={ { anchorRef: menuAnchorRef } } + focusOnMount + renderToggle={ ( { isOpen, onToggle } ) => + featuredImageId ? ( + <PostFeaturedImagePreview + isMenuOpen={ isOpen } + menuAnchorRef={ menuAnchorRef } + mediaDetails={ mediaDetails } + isLoading={ isLoading } + aria-expanded={ isOpen } + aria-describedby={ `editor-post-featured-image-${ featuredImageId }-describedby` } + onClick={ onToggle } + /> + ) : ( + <PostFeaturedImageToggle + menuAnchorRef={ menuAnchorRef } + aria-expanded={ isOpen } + onClick={ onToggle } > - <img - src={ mediaSourceUrl } - alt="" - /> - </ResponsiveWrapper> - ) } - { isLoading && <Spinner /> } - { ! featuredImageId && - ! isLoading && - ( postLabel.set_featured_image || - DEFAULT_SET_FEATURE_IMAGE_LABEL ) } - </Button> - <DropZone onFilesDrop={ onDropFiles } /> - </div> - ) } - value={ featuredImageId } - /> - </MediaUploadCheck> - { !! featuredImageId && ( - <MediaUploadCheck> - { media && ( - <MediaUpload - title={ - postLabel.featured_image || - DEFAULT_FEATURE_IMAGE_LABEL + { postLabel.set_featured_image || + DEFAULT_SET_FEATURE_IMAGE_LABEL } + </PostFeaturedImageToggle> + ) } - onSelect={ onUpdateImage } - unstableFeaturedImageFlow - allowedTypes={ ALLOWED_MEDIA_TYPES } - modalClass="editor-post-featured-image__media-modal" - render={ ( { open } ) => ( - <Button - onClick={ open } - variant="secondary" - > - { __( 'Replace Image' ) } - </Button> + renderContent={ ( { onClose } ) => ( + <PostFeaturedImageMenu + title={ + postLabel.featured_image || + DEFAULT_FEATURE_IMAGE_LABEL + } + removeImageLabel={ + postLabel.remove_featured_image || + DEFAULT_REMOVE_FEATURE_IMAGE_LABEL + } + onClose={ onClose } + onOpenMediaLibrary={ openMediaLibrary } + onOpenFileDialog={ openFileDialog } + onRemoveImage={ + featuredImageId + ? onRemoveImage + : null + } + /> ) } /> - ) } - <Button - onClick={ onRemoveImage } - variant="link" - isDestructive - > - { postLabel.remove_featured_image || - DEFAULT_REMOVE_FEATURE_IMAGE_LABEL } - </Button> - </MediaUploadCheck> - ) } + <DropZone onFilesDrop={ processUpload } /> + </div> + ) } + </PostFeaturedImageUploadProvider> </div> </PostFeaturedImageCheck> ); @@ -269,11 +258,13 @@ const applyWithDispatch = withDispatch( onUpdateImage( image ) { editPost( { featured_media: image.id } ); }, + // This is dead code, but can't be removed, as third parties using + // the 'editor.PostFeaturedImage' filter might rely on it. onDropImage( filesList ) { select( blockEditorStore ) .getSettings() .mediaUpload( { - allowedTypes: [ 'image' ], + allowedTypes: ALLOWED_MEDIA_TYPES, filesList, onFileChange( [ image ] ) { editPost( { featured_media: image.id } ); diff --git a/packages/editor/src/components/post-featured-image/menu.js b/packages/editor/src/components/post-featured-image/menu.js new file mode 100644 index 00000000000000..b9b5aef6a19dd6 --- /dev/null +++ b/packages/editor/src/components/post-featured-image/menu.js @@ -0,0 +1,67 @@ +/** + * WordPress dependencies + */ +import { __experimentalInspectorPopoverHeader as InspectorPopoverHeader } from '@wordpress/block-editor'; +import { NavigableMenu, MenuGroup, MenuItem } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { + media as mediaIcon, + upload as uploadIcon, + trash as trashIcon, +} from '@wordpress/icons'; + +export default function PostFeaturedImageMenu( { + title, + removeImageLabel, + onClose, + onOpenMediaLibrary, + onOpenFileDialog, + onRemoveImage, +} ) { + return ( + <div className="editor-post-featured-image__menu"> + <InspectorPopoverHeader + className="editor-post-featured-image__menu-header" + title={ title } + onClose={ onClose } + /> + <NavigableMenu> + <MenuGroup> + <MenuItem + icon={ mediaIcon } + iconPosition="left" + onClick={ () => { + onOpenMediaLibrary(); + onClose(); + } } + > + { __( 'Open Media Library' ) } + </MenuItem> + <MenuItem + icon={ uploadIcon } + iconPosition="left" + onClick={ () => { + onOpenFileDialog(); + onClose(); + } } + > + { __( 'Upload file' ) } + </MenuItem> + { onRemoveImage && ( + <MenuItem + icon={ trashIcon } + iconPosition="left" + isDestructive + onClick={ () => { + onRemoveImage(); + onClose(); + } } + > + { removeImageLabel } + </MenuItem> + ) } + </MenuGroup> + </NavigableMenu> + </div> + ); +} diff --git a/packages/editor/src/components/post-featured-image/preview.js b/packages/editor/src/components/post-featured-image/preview.js new file mode 100644 index 00000000000000..4b857645f15621 --- /dev/null +++ b/packages/editor/src/components/post-featured-image/preview.js @@ -0,0 +1,52 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { + Button, + ResponsiveWrapper, + Spinner, + Icon, +} from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { pencil } from '@wordpress/icons'; + +export default function PostFeaturedImagePreview( { + isMenuOpen, + menuAnchorRef, + mediaDetails, + isLoading, + ...props +} ) { + return ( + <Button + className={ classnames( 'editor-post-featured-image__preview', { + 'is-menu-open': isMenuOpen, + } ) } + aria-label={ __( 'Edit or update the image' ) } + { ...props } + > + { mediaDetails && ( + <ResponsiveWrapper + naturalWidth={ mediaDetails.width } + naturalHeight={ mediaDetails.height } + isInline + > + <img src={ mediaDetails.sourceUrl } alt="" /> + </ResponsiveWrapper> + ) } + { isLoading && <Spinner /> } + { /* TODO: If Icon supported ref then could eliminate this wrapper. */ } + <span + ref={ menuAnchorRef } + className="editor-post-featured-image__preview-icon" + > + <Icon icon={ pencil } /> + </span> + </Button> + ); +} diff --git a/packages/editor/src/components/post-featured-image/style.scss b/packages/editor/src/components/post-featured-image/style.scss index 965780179e6ef9..75f51d396d130e 100644 --- a/packages/editor/src/components/post-featured-image/style.scss +++ b/packages/editor/src/components/post-featured-image/style.scss @@ -1,30 +1,12 @@ .editor-post-featured-image { padding: 0; - &__container { - margin-bottom: 1em; + .editor-post-featured-image__container { position: relative; } - .components-spinner { - position: absolute; - top: 50%; - left: 50%; - margin-top: -9px; - margin-left: -9px; - } - - // Stack consecutive buttons. - .components-button + .components-button { + .editor-post-featured-image__dropdown { display: block; - margin-top: 1em; - } - - // This keeps images at their intrinsic size (eg. a 50px - // image will never be wider than 50px). - .components-responsive-wrapper__content { - max-width: 100%; - width: auto; } } @@ -33,29 +15,62 @@ display: block; width: 100%; padding: 0; - transition: all 0.1s ease-out; - @include reduce-motion("transition"); - box-shadow: 0 0 0 0 var(--wp-admin-theme-color); + border-radius: 0; +} + +.editor-post-featured-image__toggle { + min-height: $grid-unit-80; } .editor-post-featured-image__preview { height: auto; + + .components-responsive-wrapper { + background: $gray-100; + } + + // This keeps images at their intrinsic size (eg. a 50px + // image will never be wider than 50px). + .components-responsive-wrapper__content { + max-width: 100%; + width: auto; + } + + .components-spinner { + position: absolute; + top: 50%; + left: 50%; + margin-top: -9px; + margin-left: -9px; + } + + .editor-post-featured-image__preview-icon { + background: $white; + border-radius: $radius-block-ui; + display: block; + height: $grid-unit-40; + padding: $grid-unit-05; + position: absolute; + right: $grid-unit-10; + top: $grid-unit-10; + visibility: hidden; + width: $grid-unit-40; + } + + &:hover, + &:focus, + &.is-menu-open { + .editor-post-featured-image__preview-icon { + visibility: visible; + } + } } -.editor-post-featured-image__preview:not(:disabled):not([aria-disabled="true"]):focus { - box-shadow: 0 0 0 4px var(--wp-admin-theme-color); +.editor-post-featured-image__menu { + margin: $grid-unit-10 0; } -.editor-post-featured-image__toggle { - border-radius: $radius-block-ui; - background-color: $gray-100; - min-height: 90px; - line-height: 20px; - padding: $grid-unit-10 0; - text-align: center; - - &:hover { - background: $gray-300; - color: $gray-900; - } +.editor-post-featured-image__menu-header { + margin-left: $grid-unit-10; + margin-right: $grid-unit-10; } diff --git a/packages/editor/src/components/post-featured-image/toggle.js b/packages/editor/src/components/post-featured-image/toggle.js new file mode 100644 index 00000000000000..f977a102ea330d --- /dev/null +++ b/packages/editor/src/components/post-featured-image/toggle.js @@ -0,0 +1,21 @@ +/** + * WordPress dependencies + */ +import { Button } from '@wordpress/components'; + +export default function PostFeaturedImageToggle( { + menuAnchorRef, + children, + ...props +} ) { + return ( + <Button + ref={ menuAnchorRef } + className="editor-post-featured-image__toggle" + variant="tertiary" + { ...props } + > + { children } + </Button> + ); +} diff --git a/packages/editor/src/components/post-featured-image/upload-provider.js b/packages/editor/src/components/post-featured-image/upload-provider.js new file mode 100644 index 00000000000000..d391c24a8b5672 --- /dev/null +++ b/packages/editor/src/components/post-featured-image/upload-provider.js @@ -0,0 +1,38 @@ +/** + * WordPress dependencies + */ +import { FormFileUpload } from '@wordpress/components'; +import { MediaUpload, MediaUploadCheck } from '@wordpress/block-editor'; + +export default function PostFeaturedImageUploadProvider( { + fallback, + title, + selectedId, + allowedMediaTypes, + allowedUploadTypes, + onSelect, + onUpload, + children, +} ) { + return ( + <MediaUploadCheck fallback={ fallback }> + <MediaUpload + title={ title } + onSelect={ onSelect } + unstableFeaturedImageFlow + allowedTypes={ allowedMediaTypes } + modalClass="editor-post-featured-image__media-modal" + value={ selectedId } + render={ ( { open: openMediaLibrary } ) => ( + <FormFileUpload + onChange={ ( event ) => onUpload( event.target.files ) } + accept={ allowedUploadTypes } + render={ ( { openFileDialog } ) => + children( { openMediaLibrary, openFileDialog } ) + } + /> + ) } + /> + </MediaUploadCheck> + ); +} diff --git a/packages/editor/src/components/post-type-support-check/index.js b/packages/editor/src/components/post-type-support-check/index.js index 5c4af91c4530ad..014b25c4119ca3 100644 --- a/packages/editor/src/components/post-type-support-check/index.js +++ b/packages/editor/src/components/post-type-support-check/index.js @@ -1,12 +1,12 @@ /** * External dependencies */ -import { some, castArray } from 'lodash'; +import { castArray } from 'lodash'; /** * WordPress dependencies */ -import { withSelect } from '@wordpress/data'; +import { useSelect } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; /** @@ -19,34 +19,27 @@ import { store as editorStore } from '../../store'; * type supports one of the given `supportKeys` prop. * * @param {Object} props Props. - * @param {string} [props.postType] Current post type. - * @param {WPElement} props.children Children to be rendered if post - * type supports. * @param {(string|string[])} props.supportKeys String or string array of keys * to test. + * @param {WPElement} props.children Children to be rendered if post + * type supports. * * @return {WPComponent} The component to be rendered. */ -export function PostTypeSupportCheck( { postType, children, supportKeys } ) { - let isSupported = true; - if ( postType ) { - isSupported = some( - castArray( supportKeys ), - ( key ) => !! postType.supports[ key ] - ); - } - - if ( ! isSupported ) { - return null; - } - - return children; +export default function PostTypeSupportCheck( { supportKeys, children } ) { + return usePostTypeSupportCheck( supportKeys ) ? children : null; } -export default withSelect( ( select ) => { - const { getEditedPostAttribute } = select( editorStore ); - const { getPostType } = select( coreStore ); - return { - postType: getPostType( getEditedPostAttribute( 'type' ) ), - }; -} )( PostTypeSupportCheck ); +export function usePostTypeSupportCheck( supportKeys ) { + return useSelect( ( select ) => { + const postTypeSlug = + select( editorStore ).getEditedPostAttribute( 'type' ); + const postType = select( coreStore ).getPostType( postTypeSlug ); + if ( postType ) { + return castArray( supportKeys ).some( + ( key ) => !! postType.supports[ key ] + ); + } + return true; + }, [] ); +} diff --git a/packages/editor/src/components/post-type-support-check/test/index.js b/packages/editor/src/components/post-type-support-check/test/index.js index 251cf1a0052225..bbd1718ecbf6c9 100644 --- a/packages/editor/src/components/post-type-support-check/test/index.js +++ b/packages/editor/src/components/post-type-support-check/test/index.js @@ -3,80 +3,111 @@ */ import { create } from 'react-test-renderer'; +/** + * WordPress dependencies + */ +import { createRegistry, RegistryProvider } from '@wordpress/data'; + /** * Internal dependencies */ -import { PostTypeSupportCheck } from '../'; +import PostTypeSupportCheck from '../'; + +function createMockRegistry( postType ) { + return createRegistry( { + 'core/editor': { + selectors: { + getEditedPostAttribute: () => 'post', + }, + reducer: ( state = {} ) => state, + }, + core: { + selectors: { + getPostType: () => postType, + }, + reducer: ( state = {} ) => state, + }, + } ); +} describe( 'PostTypeSupportCheck', () => { it( 'renders its children when post type is not known', () => { - let postType; + const registry = createMockRegistry( null ); + const tree = create( - <PostTypeSupportCheck postType={ postType } supportKeys="title"> - Supported - </PostTypeSupportCheck> + <RegistryProvider value={ registry }> + <PostTypeSupportCheck supportKeys="title"> + Supported + </PostTypeSupportCheck> + </RegistryProvider> ); expect( tree.toJSON() ).toBe( 'Supported' ); } ); it( 'does not render its children when post type is known and not supports', () => { - const postType = { + const registry = createMockRegistry( { supports: {}, - }; + } ); + const tree = create( - <PostTypeSupportCheck postType={ postType } supportKeys="title"> - Supported - </PostTypeSupportCheck> + <RegistryProvider value={ registry }> + <PostTypeSupportCheck supportKeys="title"> + Supported + </PostTypeSupportCheck> + </RegistryProvider> ); expect( tree.toJSON() ).toBe( null ); } ); it( 'renders its children when post type is known and supports', () => { - const postType = { + const registry = createMockRegistry( { supports: { title: true, }, - }; + } ); + const tree = create( - <PostTypeSupportCheck postType={ postType } supportKeys="title"> - Supported - </PostTypeSupportCheck> + <RegistryProvider value={ registry }> + <PostTypeSupportCheck supportKeys="title"> + Supported + </PostTypeSupportCheck> + </RegistryProvider> ); expect( tree.toJSON() ).toBe( 'Supported' ); } ); it( 'renders its children if some of keys supported', () => { - const postType = { + const registry = createMockRegistry( { supports: { title: true, }, - }; + } ); + const tree = create( - <PostTypeSupportCheck - postType={ postType } - supportKeys={ [ 'title', 'thumbnail' ] } - > - Supported - </PostTypeSupportCheck> + <RegistryProvider value={ registry }> + <PostTypeSupportCheck supportKeys={ [ 'title', 'thumbnail' ] }> + Supported + </PostTypeSupportCheck> + </RegistryProvider> ); expect( tree.toJSON() ).toBe( 'Supported' ); } ); it( 'does not render its children if none of keys supported', () => { - const postType = { + const registry = createMockRegistry( { supports: {}, - }; + } ); + const tree = create( - <PostTypeSupportCheck - postType={ postType } - supportKeys={ [ 'title', 'thumbnail' ] } - > - Supported - </PostTypeSupportCheck> + <RegistryProvider value={ registry }> + <PostTypeSupportCheck supportKeys={ [ 'title', 'thumbnail' ] }> + Supported + </PostTypeSupportCheck> + </RegistryProvider> ); expect( tree.toJSON() ).toBe( null ); diff --git a/test/e2e/specs/editor/various/new-post.spec.js b/test/e2e/specs/editor/various/new-post.spec.js index 3d31ff27ebae4e..9e6ec8ffd6a512 100644 --- a/test/e2e/specs/editor/various/new-post.spec.js +++ b/test/e2e/specs/editor/various/new-post.spec.js @@ -25,7 +25,9 @@ test.describe( 'new editor state', () => { await expect( page ).toHaveURL( /post-new.php/ ); // Should display the blank title. - const title = page.locator( 'role=textbox[name="Add title"i]' ); + const title = page.locator( + 'role=region[name="Editor content"i] >> role=textbox[name="Add title"i]' + ); await expect( title ).toBeEditable(); await expect( title ).toHaveText( '' ); @@ -58,7 +60,9 @@ test.describe( 'new editor state', () => { await admin.createNewPost(); await expect( - page.locator( 'role=textbox[name="Add title"i]' ) + page.locator( + 'role=region[name="Editor content"i] >> role=textbox[name="Add title"i]' + ) ).toBeFocused(); } ); @@ -70,7 +74,7 @@ test.describe( 'new editor state', () => { // Enter a title for this post. await page.type( - 'role=textbox[name="Add title"i]', + 'role=region[name="Editor content"i] >> role=textbox[name="Add title"i]', 'Here is the title' ); // Save the post as a draft.