diff --git a/packages/base-styles/_z-index.scss b/packages/base-styles/_z-index.scss index 6f7e3f2f13c309..527f8141d2ec4e 100644 --- a/packages/base-styles/_z-index.scss +++ b/packages/base-styles/_z-index.scss @@ -199,6 +199,7 @@ $z-layers: ( // Ensure modal footer actions appear above modal contents ".editor-start-template-options__modal__actions": 1, + ".editor-start-page-options__modal__actions": 1, // Ensure checkbox + actions don't overlap table header ".dataviews-view-table thead": 1, diff --git a/packages/block-editor/src/components/inserter/block-patterns-explorer/index.js b/packages/block-editor/src/components/inserter/block-patterns-explorer/index.js index 2bc41a7176954c..93a03ee200497e 100644 --- a/packages/block-editor/src/components/inserter/block-patterns-explorer/index.js +++ b/packages/block-editor/src/components/inserter/block-patterns-explorer/index.js @@ -14,8 +14,9 @@ import { usePatternCategories } from '../block-patterns-tab/use-pattern-categori function PatternsExplorer( { initialCategory, rootClientId } ) { const [ searchValue, setSearchValue ] = useState( '' ); - const [ selectedCategory, setSelectedCategory ] = - useState( initialCategory ); + const [ selectedCategory, setSelectedCategory ] = useState( + initialCategory?.name + ); const patternCategories = usePatternCategories( rootClientId ); diff --git a/packages/block-editor/src/components/inserter/block-patterns-tab/index.js b/packages/block-editor/src/components/inserter/block-patterns-tab/index.js index f250ed6f12ebad..45db4732aa9c6a 100644 --- a/packages/block-editor/src/components/inserter/block-patterns-tab/index.js +++ b/packages/block-editor/src/components/inserter/block-patterns-tab/index.js @@ -70,9 +70,7 @@ function BlockPatternsTab( { ) } { showPatternsExplorer && ( setShowPatternsExplorer( false ) } rootClientId={ rootClientId } diff --git a/packages/block-editor/src/components/inserter/block-patterns-tab/pattern-category-previews.js b/packages/block-editor/src/components/inserter/block-patterns-tab/pattern-category-previews.js index f9af2b6f8c42d2..c6ce9ba97d2501 100644 --- a/packages/block-editor/src/components/inserter/block-patterns-tab/pattern-category-previews.js +++ b/packages/block-editor/src/components/inserter/block-patterns-tab/pattern-category-previews.js @@ -69,19 +69,19 @@ export function PatternCategoryPreviews( { return false; } - if ( category.name === allPatternsCategory?.name ) { + if ( category.name === allPatternsCategory.name ) { return true; } if ( - category.name === myPatternsCategory?.name && + category.name === myPatternsCategory.name && pattern.type === INSERTER_PATTERN_TYPES.user ) { return true; } if ( - category.name === starterPatternsCategory?.name && + category.name === starterPatternsCategory.name && pattern.blockTypes?.includes( 'core/post-content' ) ) { return true; @@ -149,7 +149,7 @@ export function PatternCategoryPreviews( { level={ 4 } as="div" > - { category?.label } + { category.label } sourceFilter !== 'all' && sourceFilter !== 'user'; const getShouldHideSourcesFilter = ( category ) => { - return category?.name === myPatternsCategory.name; + return category.name === myPatternsCategory.name; }; const PATTERN_SOURCE_MENU_OPTIONS = [ @@ -60,7 +60,7 @@ export function PatternsFilter( { // the user may be confused when switching to another category if the haven't explicitly set // this filter themselves. const currentPatternSourceFilter = - category?.name === myPatternsCategory.name + category.name === myPatternsCategory.name ? INSERTER_PATTERN_TYPES.user : patternSourceFilter; diff --git a/packages/block-editor/src/components/inserter/category-tabs/index.js b/packages/block-editor/src/components/inserter/category-tabs/index.js index 7f5f9ba3f65ad6..2f70ea58f2532a 100644 --- a/packages/block-editor/src/components/inserter/category-tabs/index.js +++ b/packages/block-editor/src/components/inserter/category-tabs/index.js @@ -65,9 +65,7 @@ function CategoryTabs( { key={ category.name } tabId={ category.name } aria-current={ - category.name === selectedCategory?.name - ? 'true' - : undefined + category === selectedCategory ? 'true' : undefined } > { category.label } diff --git a/packages/block-editor/src/hooks/use-zoom-out.js b/packages/block-editor/src/hooks/use-zoom-out.js index 5c37822eba4b38..adcea8b605aeb7 100644 --- a/packages/block-editor/src/hooks/use-zoom-out.js +++ b/packages/block-editor/src/hooks/use-zoom-out.js @@ -2,14 +2,13 @@ * WordPress dependencies */ import { useSelect, useDispatch } from '@wordpress/data'; -import { useEffect, useRef, useContext } from '@wordpress/element'; +import { useEffect, useRef } from '@wordpress/element'; /** * Internal dependencies */ import { store as blockEditorStore } from '../store'; import { unlock } from '../lock-unlock'; -import BlockContext from '../components/block-context'; /** * A hook used to set the editor mode to zoomed out mode, invoking the hook sets the mode. @@ -20,7 +19,6 @@ import BlockContext from '../components/block-context'; * @param {boolean} enabled If we should enter into zoomOut mode or not */ export function useZoomOut( enabled = true ) { - const { postId } = useContext( BlockContext ); const { setZoomLevel, resetZoomLevel } = unlock( useDispatch( blockEditorStore ) ); @@ -39,7 +37,6 @@ export function useZoomOut( enabled = true ) { const controlZoomLevelRef = useRef( false ); const isEnabledRef = useRef( enabled ); - const postIdRef = useRef( postId ); /** * This hook tracks if the zoom state was changed manually by the user via clicking @@ -58,11 +55,6 @@ export function useZoomOut( enabled = true ) { useEffect( () => { isEnabledRef.current = enabled; - // If the user created a new post/page, we should take control of the zoom level. - if ( postIdRef.current !== postId ) { - controlZoomLevelRef.current = true; - } - if ( enabled !== isZoomOut() ) { controlZoomLevelRef.current = true; @@ -79,5 +71,5 @@ export function useZoomOut( enabled = true ) { resetZoomLevel(); } }; - }, [ enabled, isZoomOut, postId, resetZoomLevel, setZoomLevel ] ); + }, [ enabled, isZoomOut, resetZoomLevel, setZoomLevel ] ); } diff --git a/packages/editor/src/components/start-page-options/index.js b/packages/editor/src/components/start-page-options/index.js index 54d956ff06e7c3..1910a39af1b3c2 100644 --- a/packages/editor/src/components/start-page-options/index.js +++ b/packages/editor/src/components/start-page-options/index.js @@ -1,8 +1,16 @@ /** * WordPress dependencies */ -import { useEffect } from '@wordpress/element'; +import { Flex, FlexItem, Modal, ToggleControl } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { useState, useMemo, useEffect } from '@wordpress/element'; +import { + store as blockEditorStore, + __experimentalBlockPatternsList as BlockPatternsList, +} from '@wordpress/block-editor'; import { useSelect, useDispatch } from '@wordpress/data'; +import { store as coreStore } from '@wordpress/core-data'; +import { __unstableSerializeAndClean } from '@wordpress/blocks'; import { store as preferencesStore } from '@wordpress/preferences'; import { store as interfaceStore } from '@wordpress/interface'; @@ -11,8 +19,130 @@ import { store as interfaceStore } from '@wordpress/interface'; */ import { store as editorStore } from '../../store'; +export function useStartPatterns() { + // A pattern is a start pattern if it includes 'core/post-content' in its blockTypes, + // and it has no postTypes declared and the current post type is page or if + // the current post type is part of the postTypes declared. + const { blockPatternsWithPostContentBlockType, postType } = useSelect( + ( select ) => { + const { getPatternsByBlockTypes, getBlocksByName } = + select( blockEditorStore ); + const { getCurrentPostType, getRenderingMode } = + select( editorStore ); + const rootClientId = + getRenderingMode() === 'post-only' + ? '' + : getBlocksByName( 'core/post-content' )?.[ 0 ]; + return { + blockPatternsWithPostContentBlockType: getPatternsByBlockTypes( + 'core/post-content', + rootClientId + ), + postType: getCurrentPostType(), + }; + }, + [] + ); + + return useMemo( () => { + if ( ! blockPatternsWithPostContentBlockType?.length ) { + return []; + } + + /* + * Filter patterns without postTypes declared if the current postType is page + * or patterns that declare the current postType in its post type array. + */ + return blockPatternsWithPostContentBlockType.filter( ( pattern ) => { + return ( + ( postType === 'page' && ! pattern.postTypes ) || + ( Array.isArray( pattern.postTypes ) && + pattern.postTypes.includes( postType ) ) + ); + } ); + }, [ postType, blockPatternsWithPostContentBlockType ] ); +} + +function PatternSelection( { blockPatterns, onChoosePattern } ) { + const { editEntityRecord } = useDispatch( coreStore ); + const { postType, postId } = useSelect( ( select ) => { + const { getCurrentPostType, getCurrentPostId } = select( editorStore ); + + return { + postType: getCurrentPostType(), + postId: getCurrentPostId(), + }; + }, [] ); + return ( + { + editEntityRecord( 'postType', postType, postId, { + blocks, + content: ( { blocks: blocksForSerialization = [] } ) => + __unstableSerializeAndClean( blocksForSerialization ), + } ); + onChoosePattern(); + } } + /> + ); +} + +function StartPageOptionsModal( { onClose } ) { + const [ showStartPatterns, setShowStartPatterns ] = useState( true ); + const { set: setPreference } = useDispatch( preferencesStore ); + const startPatterns = useStartPatterns(); + const hasStartPattern = startPatterns.length > 0; + + if ( ! hasStartPattern ) { + return null; + } + + function handleClose() { + onClose(); + setPreference( 'core', 'enableChoosePatternModal', showStartPatterns ); + } + + return ( + +
+ +
+ + + { + setShowStartPatterns( newValue ); + } } + /> + + +
+ ); +} + export default function StartPageOptions() { - const { postId, enabled } = useSelect( ( select ) => { + const [ isOpen, setIsOpen ] = useState( false ); + const { isEditedPostDirty, isEditedPostEmpty } = useSelect( editorStore ); + const { enabled, postId } = useSelect( ( select ) => { const { getCurrentPostId, getCurrentPostType } = select( editorStore ); const preferencesModalActive = select( interfaceStore ).isModalActive( 'editor/preferences' ); @@ -28,31 +158,22 @@ export default function StartPageOptions() { 'page' === getCurrentPostType(), }; }, [] ); - const { isEditedPostDirty, isEditedPostEmpty } = useSelect( editorStore ); - const { setIsInserterOpened } = useDispatch( editorStore ); + // Note: The `postId` ensures the effect re-runs when pages are switched without remounting the component. + // Examples: changing pages in the List View, creating a new page via Command Palette. useEffect( () => { - if ( ! enabled ) { + const isFreshPage = ! isEditedPostDirty() && isEditedPostEmpty(); + if ( ! enabled || ! isFreshPage ) { return; } - const isFreshPage = ! isEditedPostDirty() && isEditedPostEmpty(); - if ( isFreshPage ) { - setIsInserterOpened( { - tab: 'patterns', - category: 'core/starter-content', - } ); - } + // Open the modal after the initial render for a new page. + setIsOpen( true ); + }, [ enabled, postId, isEditedPostDirty, isEditedPostEmpty ] ); + + if ( ! isOpen ) { + return null; + } - // Note: The `postId` ensures the effect re-runs when pages are switched without remounting the component. - // Examples: changing pages in the List View, creating a new page via Command Palette. - }, [ - postId, - enabled, - setIsInserterOpened, - isEditedPostDirty, - isEditedPostEmpty, - ] ); - - return null; + return setIsOpen( false ) } />; } diff --git a/packages/editor/src/components/start-page-options/style.scss b/packages/editor/src/components/start-page-options/style.scss index 129d670526c709..e97fc4a9b2cd9a 100644 --- a/packages/editor/src/components/start-page-options/style.scss +++ b/packages/editor/src/components/start-page-options/style.scss @@ -1,3 +1,30 @@ +$actions-height: 92px; + +.editor-start-page-options__modal { + .editor-start-page-options__modal__actions { + position: absolute; + bottom: 0; + width: 100%; + height: $actions-height; + background-color: $white; + margin-left: - $grid-unit-40; + margin-right: - $grid-unit-40; + padding-left: $grid-unit-40; + padding-right: $grid-unit-40; + border-top: 1px solid $gray-300; + z-index: z-index(".editor-start-page-options__modal__actions"); + } + + .block-editor-block-patterns-list { + // Since the actions container is positioned absolutely, + // this padding bottom ensures that the content wrapper will properly + // detect overflowing content and start showing scrollbars at the right + // moment. Without this padding, the content would render under the actions + // bar without causing the wrapper to show a scrollbar. + padding-bottom: $actions-height; + } +} + // 2 column masonry layout. .editor-start-page-options__modal-content .block-editor-block-patterns-list { column-count: 2; diff --git a/test/e2e/specs/editor/various/template-resolution.spec.js b/test/e2e/specs/editor/various/template-resolution.spec.js index 82e336feff7334..13503ddaf23d5b 100644 --- a/test/e2e/specs/editor/various/template-resolution.spec.js +++ b/test/e2e/specs/editor/various/template-resolution.spec.js @@ -55,15 +55,12 @@ test.describe( 'Template resolution', () => { status: 'publish', } ); await admin.editPost( newPage.id ); - await page.locator( 'role=button[name="Block Inserter"i]' ).click(); await editor.openDocumentSettingsSidebar(); await expect( page.getByRole( 'button', { name: 'Template options' } ) ).toHaveText( 'Single Entries' ); await updateSiteSettings( { requestUtils, pageId: newPage.id } ); await page.reload(); - await page.locator( 'role=button[name="Block Inserter"i]' ).click(); - await editor.openDocumentSettingsSidebar(); await expect( page.getByRole( 'button', { name: 'Template options' } ) ).toHaveText( 'Index' ); @@ -84,7 +81,6 @@ test.describe( 'Template resolution', () => { postType: 'page', canvas: 'edit', } ); - await page.locator( 'role=button[name="Block Inserter"i]' ).click(); await editor.openDocumentSettingsSidebar(); await expect( page.getByRole( 'button', { name: 'Template options' } ) diff --git a/test/e2e/specs/site-editor/block-style-variations.spec.js b/test/e2e/specs/site-editor/block-style-variations.spec.js index 1fa8972d34d6c8..03fc5398f4a0a5 100644 --- a/test/e2e/specs/site-editor/block-style-variations.spec.js +++ b/test/e2e/specs/site-editor/block-style-variations.spec.js @@ -317,7 +317,9 @@ async function draftNewPage( page ) { // Create a Group block with 2 nested Group blocks. async function addPageContent( editor, page ) { - const inserterButton = page.locator( 'role=tab[name="Blocks"i]' ); + const inserterButton = page.locator( + 'role=button[name="Block Inserter"i]' + ); await inserterButton.click(); await page.type( 'role=searchbox[name="Search"i]', 'Group' ); await page.click( diff --git a/test/e2e/specs/site-editor/pages.spec.js b/test/e2e/specs/site-editor/pages.spec.js index 54f8a64e067cbf..1adb33e5531f36 100644 --- a/test/e2e/specs/site-editor/pages.spec.js +++ b/test/e2e/specs/site-editor/pages.spec.js @@ -272,7 +272,6 @@ test.describe( 'Pages', () => { // Create new page that has the default template so as to swap it. await draftNewPage( page ); - await page.locator( 'role=button[name="Block Inserter"i]' ).click(); await editor.openDocumentSettingsSidebar(); const templateOptionsButton = page .getByRole( 'region', { name: 'Editor settings' } ) @@ -295,7 +294,6 @@ test.describe( 'Pages', () => { } ); // Now reset, and apply the default template back. - await editor.openDocumentSettingsSidebar(); await templateOptionsButton.click(); const resetButton = page .getByRole( 'menu', { name: 'Template options' } ) @@ -310,7 +308,6 @@ test.describe( 'Pages', () => { editor, } ) => { await draftNewPage( page ); - await page.locator( 'role=button[name="Block Inserter"i]' ).click(); await editor.openDocumentSettingsSidebar(); const templateOptionsButton = page .getByRole( 'region', { name: 'Editor settings' } )