diff --git a/packages/edit-post/src/components/header/document-title/index.js b/packages/edit-post/src/components/header/document-title/index.js new file mode 100644 index 00000000000000..1b27a0bacf014b --- /dev/null +++ b/packages/edit-post/src/components/header/document-title/index.js @@ -0,0 +1,89 @@ +/** + * WordPress dependencies + */ +import { __, isRTL } from '@wordpress/i18n'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { BlockIcon, store as blockEditorStore } from '@wordpress/block-editor'; +import { + Button, + VisuallyHidden, + __experimentalHStack as HStack, + __experimentalText as Text, +} from '@wordpress/components'; +import { layout, chevronLeftSmall, chevronRightSmall } from '@wordpress/icons'; +import { privateApis as commandsPrivateApis } from '@wordpress/commands'; +import { displayShortcut } from '@wordpress/keycodes'; + +/** + * Internal dependencies + */ +import { unlock } from '../../../private-apis'; +import { store as editPostStore } from '../../../store'; + +const { store: commandsStore } = unlock( commandsPrivateApis ); + +function DocumentTitle() { + const { template, isEditing } = useSelect( ( select ) => { + const { isEditingTemplate, getEditedPostTemplate } = + select( editPostStore ); + const _isEditing = isEditingTemplate(); + + return { + template: _isEditing ? getEditedPostTemplate() : null, + isEditing: _isEditing, + }; + }, [] ); + const { clearSelectedBlock } = useDispatch( blockEditorStore ); + const { setIsEditingTemplate } = useDispatch( editPostStore ); + const { open: openCommandCenter } = useDispatch( commandsStore ); + + if ( ! isEditing || ! template ) { + return null; + } + + let templateTitle = __( 'Default' ); + if ( template?.title ) { + templateTitle = template.title; + } else if ( !! template ) { + templateTitle = template.slug; + } + + return ( +
+ + + + + + +
+ ); +} + +export default DocumentTitle; diff --git a/packages/edit-post/src/components/header/document-title/style.scss b/packages/edit-post/src/components/header/document-title/style.scss new file mode 100644 index 00000000000000..e39ecf607e4306 --- /dev/null +++ b/packages/edit-post/src/components/header/document-title/style.scss @@ -0,0 +1,61 @@ +.edit-post-document-title { + display: flex; + align-items: center; + gap: $grid-unit; + height: $button-size; + justify-content: space-between; + // Flex items will, by default, refuse to shrink below a minimum + // intrinsic width. In order to shrink this flexbox item, and + // subsequently truncate child text, we set an explicit min-width. + // See https://dev.w3.org/csswg/css-flexbox/#min-size-auto + min-width: 0; + background: $gray-100; + border-radius: 4px; + width: min(100%, 450px); + + &:hover { + color: currentColor; + background: $gray-200; + } +} + +.edit-post-document-title__title.components-button { + flex-grow: 1; + color: var(--wp-block-synced-color); + overflow: hidden; + + &:hover { + color: var(--wp-block-synced-color); + } + + h1 { + color: var(--wp-block-synced-color); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } +} + +.edit-post-document-title__shortcut { + flex-shrink: 0; + color: $gray-700; + padding: 0 $grid-unit-15; + + &:hover { + color: $gray-700; + } +} + +.edit-post-document-title__left { + min-width: $button-size; + flex-shrink: 0; + + .components-button.has-icon.has-text { + color: $gray-700; + gap: 0; + + &:hover { + color: currentColor; + } + } +} diff --git a/packages/edit-post/src/components/header/header-toolbar/style.scss b/packages/edit-post/src/components/header/header-toolbar/style.scss index 87aec00004c02b..694dcb5a2d678a 100644 --- a/packages/edit-post/src/components/header/header-toolbar/style.scss +++ b/packages/edit-post/src/components/header/header-toolbar/style.scss @@ -1,6 +1,5 @@ .edit-post-header-toolbar { display: inline-flex; - flex-grow: 1; align-items: center; border: none; diff --git a/packages/edit-post/src/components/header/index.js b/packages/edit-post/src/components/header/index.js index 09a93424f6903d..3306a0fdf1606a 100644 --- a/packages/edit-post/src/components/header/index.js +++ b/packages/edit-post/src/components/header/index.js @@ -18,7 +18,7 @@ import { default as DevicePreview } from '../device-preview'; import ViewLink from '../view-link'; import MainDashboardButton from './main-dashboard-button'; import { store as editPostStore } from '../../store'; -import TemplateTitle from './template-title'; +import DocumentTitle from './document-title'; function Header( { setEntitiesSavedStatesCallback } ) { const isLargeViewport = useViewportMatch( 'large' ); @@ -70,7 +70,9 @@ function Header( { setEntitiesSavedStatesCallback } ) { className="edit-post-header__toolbar" > - +
+ +
{ - const { isEditingTemplate, getEditedPostTemplate } = - select( editPostStore ); - const _isEditing = isEditingTemplate(); - return { - template: _isEditing ? getEditedPostTemplate() : null, - }; - }, [] ); - const [ showConfirmDialog, setShowConfirmDialog ] = useState( false ); - - if ( ! template || ! template.wp_id ) { - return null; - } - let templateTitle = template.slug; - if ( template?.title ) { - templateTitle = template.title; - } - - const isRevertable = template?.has_theme_file; - - const onDelete = () => { - clearSelectedBlock(); - setIsEditingTemplate( false ); - setShowConfirmDialog( false ); - - editPost( { - template: '', - } ); - const settings = getEditorSettings(); - const newAvailableTemplates = Object.fromEntries( - Object.entries( settings.availableTemplates ?? {} ).filter( - ( [ id ] ) => id !== template.slug - ) - ); - updateEditorSettings( { - availableTemplates: newAvailableTemplates, - } ); - deleteEntityRecord( 'postType', 'wp_template', template.id, { - throwOnError: true, - } ); - }; - - return ( - - <> - { - setShowConfirmDialog( true ); - } } - info={ - isRevertable - ? __( 'Use the template as supplied by the theme.' ) - : undefined - } - > - { isRevertable - ? __( 'Clear customizations' ) - : __( 'Delete template' ) } - - { - setShowConfirmDialog( false ); - } } - > - { sprintf( - /* translators: %s: template name */ - __( - 'Are you sure you want to delete the %s template? It may be used by other pages or posts.' - ), - templateTitle - ) } - - - - ); -} diff --git a/packages/edit-post/src/components/header/template-title/edit-template-title.js b/packages/edit-post/src/components/header/template-title/edit-template-title.js deleted file mode 100644 index 447ea5e4e02d72..00000000000000 --- a/packages/edit-post/src/components/header/template-title/edit-template-title.js +++ /dev/null @@ -1,78 +0,0 @@ -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; -import { TextControl } from '@wordpress/components'; -import { useDispatch, useSelect } from '@wordpress/data'; -import { useState } from '@wordpress/element'; -import { store as editorStore } from '@wordpress/editor'; -import { store as coreStore } from '@wordpress/core-data'; - -/** - * Internal dependencies - */ -import { store as editPostStore } from '../../../store'; - -export default function EditTemplateTitle() { - const [ forceEmpty, setForceEmpty ] = useState( false ); - const { template } = useSelect( ( select ) => { - const { getEditedPostTemplate } = select( editPostStore ); - return { - template: getEditedPostTemplate(), - }; - }, [] ); - - const { editEntityRecord } = useDispatch( coreStore ); - const { getEditorSettings } = useSelect( editorStore ); - const { updateEditorSettings } = useDispatch( editorStore ); - - // Only user-created and non-default templates can change the name. - if ( ! template.is_custom || template.has_theme_file ) { - return null; - } - - let templateTitle = __( 'Default' ); - if ( template?.title ) { - templateTitle = template.title; - } else if ( !! template ) { - templateTitle = template.slug; - } - - return ( -
- { - // Allow having the field temporarily empty while typing. - if ( ! newTitle && ! forceEmpty ) { - setForceEmpty( true ); - return; - } - setForceEmpty( false ); - - const settings = getEditorSettings(); - const newAvailableTemplates = Object.fromEntries( - Object.entries( settings.availableTemplates ?? {} ).map( - ( [ id, existingTitle ] ) => [ - id, - id !== template.slug ? existingTitle : newTitle, - ] - ) - ); - updateEditorSettings( { - availableTemplates: newAvailableTemplates, - } ); - editEntityRecord( 'postType', 'wp_template', template.id, { - title: newTitle, - } ); - } } - onBlur={ () => setForceEmpty( false ) } - /> -
- ); -} diff --git a/packages/edit-post/src/components/header/template-title/index.js b/packages/edit-post/src/components/header/template-title/index.js deleted file mode 100644 index c0745dc0451b74..00000000000000 --- a/packages/edit-post/src/components/header/template-title/index.js +++ /dev/null @@ -1,115 +0,0 @@ -/** - * WordPress dependencies - */ -import { __, sprintf } from '@wordpress/i18n'; -import { useSelect, useDispatch } from '@wordpress/data'; -import { - Dropdown, - Button, - __experimentalText as Text, -} from '@wordpress/components'; -import { chevronDown } from '@wordpress/icons'; - -/** - * Internal dependencies - */ -import { store as editPostStore } from '../../../store'; -import { store as blockEditorStore } from '@wordpress/block-editor'; -import { store as editorStore } from '@wordpress/editor'; -import DeleteTemplate from './delete-template'; -import EditTemplateTitle from './edit-template-title'; -import TemplateDescription from './template-description'; - -function TemplateTitle() { - const { template, isEditing, title } = useSelect( ( select ) => { - const { isEditingTemplate, getEditedPostTemplate } = - select( editPostStore ); - const { getEditedPostAttribute } = select( editorStore ); - - const _isEditing = isEditingTemplate(); - - return { - template: _isEditing ? getEditedPostTemplate() : null, - isEditing: _isEditing, - title: getEditedPostAttribute( 'title' ) - ? getEditedPostAttribute( 'title' ) - : __( 'Untitled' ), - }; - }, [] ); - - const { clearSelectedBlock } = useDispatch( blockEditorStore ); - const { setIsEditingTemplate } = useDispatch( editPostStore ); - - if ( ! isEditing || ! template ) { - return null; - } - - let templateTitle = __( 'Default' ); - if ( template?.title ) { - templateTitle = template.title; - } else if ( !! template ) { - templateTitle = template.slug; - } - - const hasOptions = !! ( - template.custom || - template.wp_id || - template.description - ); - - return ( -
- - { hasOptions ? ( - ( - - ) } - renderContent={ () => ( - <> - - - - - ) } - /> - ) : ( - - { templateTitle } - - ) } -
- ); -} - -export default TemplateTitle; diff --git a/packages/edit-post/src/components/header/template-title/style.scss b/packages/edit-post/src/components/header/template-title/style.scss deleted file mode 100644 index b5fe5120bfb64c..00000000000000 --- a/packages/edit-post/src/components/header/template-title/style.scss +++ /dev/null @@ -1,94 +0,0 @@ -.edit-post-template-top-area { - display: flex; - flex-direction: column; - align-content: space-between; - width: 100%; - align-items: center; - - .edit-post-template-title, - .edit-post-template-post-title { - padding: 0; - text-decoration: none; - height: auto; - - &::before { - height: 100%; - } - - &.has-icon { - svg { - order: 1; - margin-right: 0; - } - } - } - - .edit-post-template-title { - color: $gray-900; - } - - .edit-post-template-post-title { - margin-top: $grid-unit-05; - max-width: 160px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - display: block; - - &::before { - left: 0; - right: 0; - } - - @include break-xlarge() { - max-width: 400px; - } - } -} - -.edit-post-template-top-area__popover { - .components-popover__content { - min-width: 280px; - padding: 0; - } - - .edit-site-template-details__group { - padding: $grid-unit-20; - - .components-base-control__help { - margin-bottom: 0; - } - } - - .edit-post-template-details__description { - color: $gray-700; - } -} - -.edit-post-template-top-area__second-menu-group { - border-top: $border-width solid $gray-300; - padding: $grid-unit-20 $grid-unit-10; - - .edit-post-template-top-area__delete-template-button { - display: flex; - justify-content: center; - padding: $grid-unit-05 $grid-unit; - - &.is-destructive { - padding: inherit; - margin-left: $grid-unit-10; - margin-right: $grid-unit-10; - width: calc(100% - #{($grid-unit * 2)}); - - .components-menu-item__item { - width: auto; - } - } - - .components-menu-item__item { - margin-right: 0; - min-width: 0; - width: 100%; - } - } -} diff --git a/packages/edit-post/src/components/header/template-title/template-description.js b/packages/edit-post/src/components/header/template-title/template-description.js deleted file mode 100644 index 3513496852c339..00000000000000 --- a/packages/edit-post/src/components/header/template-title/template-description.js +++ /dev/null @@ -1,42 +0,0 @@ -/** - * WordPress dependencies - */ -import { useSelect } from '@wordpress/data'; -import { - __experimentalHeading as Heading, - __experimentalText as Text, -} from '@wordpress/components'; - -/** - * Internal dependencies - */ -import { store as editPostStore } from '../../../store'; - -export default function TemplateDescription() { - const { description, title } = useSelect( ( select ) => { - const { getEditedPostTemplate } = select( editPostStore ); - return { - title: getEditedPostTemplate().title, - description: getEditedPostTemplate().description, - }; - }, [] ); - if ( ! description ) { - return null; - } - - return ( -
- - { title } - - - { description } - -
- ); -} diff --git a/packages/edit-post/src/components/visual-editor/index.js b/packages/edit-post/src/components/visual-editor/index.js index ac8902f6a5f7a1..638a869aa8350c 100644 --- a/packages/edit-post/src/components/visual-editor/index.js +++ b/packages/edit-post/src/components/visual-editor/index.js @@ -31,11 +31,9 @@ import { __experimentaluseLayoutStyles as useLayoutStyles, } from '@wordpress/block-editor'; import { useEffect, useRef, useMemo } from '@wordpress/element'; -import { Button, __unstableMotion as motion } from '@wordpress/components'; -import { useSelect, useDispatch } from '@wordpress/data'; +import { __unstableMotion as motion } from '@wordpress/components'; +import { useSelect } from '@wordpress/data'; import { useMergeRefs } from '@wordpress/compose'; -import { arrowLeft } from '@wordpress/icons'; -import { __ } from '@wordpress/i18n'; import { parse } from '@wordpress/blocks'; import { store as coreStore } from '@wordpress/core-data'; @@ -175,8 +173,6 @@ export default function VisualEditor( { styles } ) { _settings.__experimentalFeatures?.useRootPaddingAwareAlignments, }; }, [] ); - const { clearSelectedBlock } = useDispatch( blockEditorStore ); - const { setIsEditingTemplate } = useDispatch( editPostStore ); const desktopCanvasStyles = { height: '100%', width: '100%', @@ -349,18 +345,6 @@ export default function VisualEditor( { styles } ) { } } ref={ blockSelectionClearerRef } > - { isTemplateMode && ( - - ) } { ) ).toBeVisible(); } ); - - test( 'Allow editing the title of a new custom template', async ( { - page, - postEditorTemplateMode, - } ) => { - async function editTemplateTitle( newTitle ) { - await page - .getByRole( 'button', { name: 'Template Options' } ) - .click(); - - await page - .getByRole( 'textbox', { name: 'Title' } ) - .fill( newTitle ); - - const editorContent = page.getByLabel( 'Editor Content' ); - await editorContent.click(); - } - - await postEditorTemplateMode.createPostAndSaveDraft(); - await postEditorTemplateMode.createNewTemplate( 'Foobar' ); - await editTemplateTitle( 'Barfoo' ); - - await expect( - page.getByRole( 'button', { name: 'Template Options' } ) - ).toHaveText( 'Barfoo' ); - } ); - - test.describe( 'Delete Post Template Confirmation Dialog', () => { - test.beforeAll( async ( { requestUtils } ) => { - await requestUtils.activateTheme( 'twentytwentyone' ); - } ); - - test.beforeEach( async ( { postEditorTemplateMode } ) => { - await postEditorTemplateMode.createPostAndSaveDraft(); - } ); - - [ 'large', 'small' ].forEach( ( viewport ) => { - test( `should retain template if deletion is canceled when the viewport is ${ viewport }`, async ( { - editor, - page, - pageUtils, - postEditorTemplateMode, - } ) => { - await pageUtils.setBrowserViewport( viewport ); - - await postEditorTemplateMode.disableTemplateWelcomeGuide(); - - const templateTitle = `${ viewport } Viewport Deletion Test`; - - await postEditorTemplateMode.createNewTemplate( templateTitle ); - - // Close the settings in small viewport. - if ( viewport === 'small' ) { - await page.click( 'role=button[name="Close settings"i]' ); - } - - // Edit the template. - await editor.insertBlock( { name: 'core/paragraph' } ); - await page.keyboard.type( - 'Just a random paragraph added to the template' - ); - - await postEditorTemplateMode.saveTemplateWithoutPublishing(); - - // Test deletion dialog. - { - const templateDropdown = - postEditorTemplateMode.editorTopBar.locator( - 'role=button[name="Template Options"i]' - ); - await templateDropdown.click(); - await page.click( - 'role=menuitem[name="Delete template"i]' - ); - - const confirmDeletionDialog = page.locator( 'role=dialog' ); - await expect( confirmDeletionDialog ).toBeFocused(); - await expect( - confirmDeletionDialog.locator( - `text=Are you sure you want to delete the ${ templateTitle } template? It may be used by other pages or posts.` - ) - ).toBeVisible(); - - await confirmDeletionDialog - .locator( 'role=button[name="Cancel"i]' ) - .click(); - } - - // Exit template mode. - await page.click( 'role=button[name="Back"i]' ); - - await editor.openDocumentSettingsSidebar(); - - // Move focus to the "Post" panel in the editor sidebar. - const postPanel = - postEditorTemplateMode.editorSettingsSidebar.locator( - 'role=button[name="Post"i]' - ); - await postPanel.click(); - - await postEditorTemplateMode.openTemplatePopover(); - - const templateSelect = page.locator( - 'role=combobox[name="Template"i]' - ); - await expect( templateSelect ).toHaveValue( - `${ viewport }-viewport-deletion-test` - ); - } ); - - test( `should delete template if deletion is confirmed when the viewport is ${ viewport }`, async ( { - editor, - page, - pageUtils, - postEditorTemplateMode, - } ) => { - const templateTitle = `${ viewport } Viewport Deletion Test`; - - await pageUtils.setBrowserViewport( viewport ); - - await postEditorTemplateMode.createNewTemplate( templateTitle ); - - // Close the settings in small viewport. - if ( viewport === 'small' ) { - await page.click( 'role=button[name="Close settings"i]' ); - } - - // Edit the template. - await editor.insertBlock( { name: 'core/paragraph' } ); - await page.keyboard.type( - 'Just a random paragraph added to the template' - ); - - await postEditorTemplateMode.saveTemplateWithoutPublishing(); - - { - const templateDropdown = - postEditorTemplateMode.editorTopBar.locator( - 'role=button[name="Template Options"i]' - ); - await templateDropdown.click(); - await page.click( - 'role=menuitem[name="Delete template"i]' - ); - - const confirmDeletionDialog = page.locator( 'role=dialog' ); - await expect( confirmDeletionDialog ).toBeFocused(); - await expect( - confirmDeletionDialog.locator( - `text=Are you sure you want to delete the ${ templateTitle } template? It may be used by other pages or posts.` - ) - ).toBeVisible(); - - await confirmDeletionDialog - .locator( 'role=button[name="OK"i]' ) - .click(); - } - - // Saving isn't technically necessary, but for themes without any specified templates, - // the removal of the Templates dropdown is delayed. A save and reload allows for this - // delay and prevents flakiness - { - await page.click( 'role=button[name="Save draft"i]' ); - await page.waitForSelector( - 'role=button[name="Dismiss this notice"] >> text=Draft saved' - ); - await page.reload(); - } - - const templateOptions = - postEditorTemplateMode.editorSettingsSidebar.locator( - 'role=combobox[name="Template:"i] >> role=menuitem' - ); - const availableTemplates = - await templateOptions.allTextContents(); - - expect( availableTemplates ).not.toContain( - `${ viewport } Viewport Deletion Test` - ); - } ); - } ); - } ); } ); class PostEditorTemplateMode { @@ -331,7 +149,9 @@ class PostEditorTemplateMode { 'role=button[name="Dismiss this notice"] >> text=Editing template. Changes made here affect all posts and pages that use the template.' ); - await expect( this.editorTopBar ).toHaveText( /Just an FSE Post/ ); + await expect( + this.editorTopBar.getByRole( 'heading[level=1]' ) + ).toHaveText( 'Editing template: Singular' ); } async createPostAndSaveDraft() {