diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index 628225f536e45..2bf81ebf2cc96 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -38,7 +38,7 @@ Add a user’s avatar. ([Source](https://github.com/WordPress/gutenberg/tree/tru ## Pattern -Create and save content to reuse across your site. Update the block, and the changes apply everywhere it’s used. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/block)) +Create and save content to reuse across your site. Update the pattern, and the changes apply everywhere it’s used. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/block)) - **Name:** core/block - **Category:** reusable diff --git a/lib/compat/wordpress-6.3/footnotes.php b/lib/compat/wordpress-6.3/footnotes.php new file mode 100644 index 0000000000000..45f4c74b05e37 --- /dev/null +++ b/lib/compat/wordpress-6.3/footnotes.php @@ -0,0 +1,25 @@ +\s*\d+\s*_'; + return preg_replace( $footnote_pattern, '', $content ); +} + +add_filter( 'the_content', 'gutenberg_trim_footnotes' ); diff --git a/lib/load.php b/lib/load.php index 8b76586030c35..cc3f46cb7837f 100644 --- a/lib/load.php +++ b/lib/load.php @@ -57,6 +57,7 @@ function gutenberg_is_experiment_enabled( $name ) { require_once __DIR__ . '/compat/wordpress-6.3/link-template.php'; require_once __DIR__ . '/compat/wordpress-6.3/block-patterns.php'; require_once __DIR__ . '/compat/wordpress-6.3/class-gutenberg-rest-blocks-controller.php'; + require_once __DIR__ . '/compat/wordpress-6.3/footnotes.php'; // Experimental. if ( ! class_exists( 'WP_Rest_Customizer_Nonces' ) ) { diff --git a/packages/block-editor/src/components/block-draggable/index.js b/packages/block-editor/src/components/block-draggable/index.js index 1e4048c90ab61..0b8f3c2d87f52 100644 --- a/packages/block-editor/src/components/block-draggable/index.js +++ b/packages/block-editor/src/components/block-draggable/index.js @@ -22,16 +22,25 @@ const BlockDraggable = ( { } ) => { const { srcRootClientId, isDraggable, icon } = useSelect( ( select ) => { - const { canMoveBlocks, getBlockRootClientId, getBlockName } = - select( blockEditorStore ); - const { getBlockType } = select( blocksStore ); + const { + canMoveBlocks, + getBlockRootClientId, + getBlockName, + getBlockAttributes, + } = select( blockEditorStore ); + const { getBlockType, getActiveBlockVariation } = + select( blocksStore ); const rootClientId = getBlockRootClientId( clientIds[ 0 ] ); const blockName = getBlockName( clientIds[ 0 ] ); + const variation = getActiveBlockVariation( + blockName, + getBlockAttributes( clientIds[ 0 ] ) + ); return { srcRootClientId: rootClientId, isDraggable: canMoveBlocks( clientIds, rootClientId ), - icon: getBlockType( blockName )?.icon, + icon: variation?.icon || getBlockType( blockName )?.icon, }; }, [ clientIds ] diff --git a/packages/block-editor/src/components/inserter/reusable-block-rename-hint.js b/packages/block-editor/src/components/inserter/reusable-block-rename-hint.js index 09861d9b97f1c..d4702eb137283 100644 --- a/packages/block-editor/src/components/inserter/reusable-block-rename-hint.js +++ b/packages/block-editor/src/components/inserter/reusable-block-rename-hint.js @@ -29,7 +29,7 @@ export default function ReusableBlocksRenameHint() {
{ __( - 'Reusable blocks are now called patterns. A synced pattern will behave in exactly the same way as a reusable block.' + 'Reusable blocks are now synced patterns. A synced pattern will behave in exactly the same way as a reusable block.' ) }
+ { text } + ); }; diff --git a/packages/block-editor/src/components/link-control/search-item.js b/packages/block-editor/src/components/link-control/search-item.js index 250b28596f4f3..976bb4420cb0c 100644 --- a/packages/block-editor/src/components/link-control/search-item.js +++ b/packages/block-editor/src/components/link-control/search-item.js @@ -1,14 +1,8 @@ -/** - * External dependencies - */ -import classnames from 'classnames'; - /** * WordPress dependencies */ -import { safeDecodeURI, filterURLForDisplay } from '@wordpress/url'; import { __ } from '@wordpress/i18n'; -import { Button, TextHighlight } from '@wordpress/components'; +import { MenuItem, TextHighlight } from '@wordpress/components'; import { Icon, globe, @@ -19,6 +13,7 @@ import { file, } from '@wordpress/icons'; import { __unstableStripHTML as stripHTML } from '@wordpress/dom'; +import { safeDecodeURI, filterURLForDisplay } from '@wordpress/url'; const ICONS_MAP = { post: postList, @@ -52,50 +47,33 @@ function SearchItemIcon( { isURL, suggestion } ) { export const LinkControlSearchItem = ( { itemProps, suggestion, - isSelected = false, + searchTerm, onClick, isURL = false, - searchTerm = '', shouldShowType = false, } ) => { + const info = isURL + ? __( 'Press ENTER to add this link' ) + : filterURLForDisplay( safeDecodeURI( suggestion?.url ) ); + return ( - + + ); }; diff --git a/packages/block-editor/src/components/link-control/search-results.js b/packages/block-editor/src/components/link-control/search-results.js index 9d7ee7ca41abd..71e258c769bf1 100644 --- a/packages/block-editor/src/components/link-control/search-results.js +++ b/packages/block-editor/src/components/link-control/search-results.js @@ -2,7 +2,7 @@ * WordPress dependencies */ import { __, sprintf } from '@wordpress/i18n'; -import { VisuallyHidden } from '@wordpress/components'; +import { VisuallyHidden, MenuGroup } from '@wordpress/components'; /** * External dependencies @@ -72,59 +72,61 @@ export default function LinkControlSearchResults( { className={ resultsListClasses } aria-labelledby={ searchResultsLabelId } > - { suggestions.map( ( suggestion, index ) => { - if ( - shouldShowCreateSuggestion && - CREATE_TYPE === suggestion.type - ) { + + { suggestions.map( ( suggestion, index ) => { + if ( + shouldShowCreateSuggestion && + CREATE_TYPE === suggestion.type + ) { + return ( + + handleSuggestionClick( suggestion ) + } + // Intentionally only using `type` here as + // the constant is enough to uniquely + // identify the single "CREATE" suggestion. + key={ suggestion.type } + itemProps={ buildSuggestionItemProps( + suggestion, + index + ) } + isSelected={ index === selectedSuggestion } + /> + ); + } + + // If we're not handling "Create" suggestions above then + // we don't want them in the main results so exit early. + if ( CREATE_TYPE === suggestion.type ) { + return null; + } + return ( - - handleSuggestionClick( suggestion ) - } - // Intentionally only using `type` here as - // the constant is enough to uniquely - // identify the single "CREATE" suggestion. - key={ suggestion.type } + { + handleSuggestionClick( suggestion ); + } } isSelected={ index === selectedSuggestion } + isURL={ LINK_ENTRY_TYPES.includes( + suggestion.type + ) } + searchTerm={ currentInputValue } + shouldShowType={ shouldShowSuggestionsTypes } + isFrontPage={ suggestion?.isFrontPage } /> ); - } - - // If we're not handling "Create" suggestions above then - // we don't want them in the main results so exit early. - if ( CREATE_TYPE === suggestion.type ) { - return null; - } - - return ( - { - handleSuggestionClick( suggestion ); - } } - isSelected={ index === selectedSuggestion } - isURL={ LINK_ENTRY_TYPES.includes( - suggestion.type - ) } - searchTerm={ currentInputValue } - shouldShowType={ shouldShowSuggestionsTypes } - isFrontPage={ suggestion?.isFrontPage } - /> - ); - } ) } + } ) } +
); diff --git a/packages/block-editor/src/components/link-control/style.scss b/packages/block-editor/src/components/link-control/style.scss index eb9769c2d299b..0d93ff01bcc66 100644 --- a/packages/block-editor/src/components/link-control/style.scss +++ b/packages/block-editor/src/components/link-control/style.scss @@ -41,6 +41,7 @@ $preview-image-height: 140px; // Provides positioning context for reset button. Without this then when an // error notice is displayed the input's reset button is incorrectly positioned. .block-editor-link-control__search-input-wrapper { + margin-bottom: $grid-unit-10; position: relative; } @@ -87,36 +88,9 @@ $preview-image-height: 140px; order: 20; } -.block-editor-link-control__search-results-wrapper { - position: relative; - - &::before, - &::after { - content: ""; - position: absolute; - left: -1px; - right: $grid-unit-20; // avoid overlaying scrollbars - display: block; - pointer-events: none; - z-index: 100; - } - - &::before { - height: $grid-unit-20 * 0.5; - top: 0; - bottom: auto; - } - - &::after { - height: $grid-unit-20; - bottom: 0; - top: auto; - } -} - .block-editor-link-control__search-results { - margin: 0; - padding: $grid-unit-20 * 0.5 $grid-unit-20 $grid-unit-20 * 0.5; + margin-top: -$grid-unit-20; + padding: $grid-unit-10; max-height: 200px; overflow-y: auto; // allow results list to scroll @@ -126,39 +100,35 @@ $preview-image-height: 140px; } .block-editor-link-control__search-item { - position: relative; - display: flex; - align-items: flex-start; // when link text is very long it is important this indicator remains visible and thus should be aligned top. - font-size: $default-font-size; - cursor: pointer; - background: $white; - width: 100%; - border: none; - text-align: left; - padding: $grid-unit-15 $grid-unit-20; - border-radius: 2px; - height: auto; - &:hover, - &:focus { - background-color: $gray-100; + &.components-button.components-menu-item__button { + height: auto; + text-align: left; + } - .block-editor-link-control__search-item-type { - background: $white; + .components-menu-item__item { + overflow: hidden; + text-overflow: ellipsis; + // Inline block required to preserve white space + // between `` elements and text nodes. + display: inline-block; + width: 100%; + + mark { + font-weight: 600; + color: inherit; + background-color: transparent; } } - // The added specificity is needed to override. - &:focus:not(:disabled) { - box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color) inset; + .components-menu-item__shortcut { + color: $gray-700; + text-transform: capitalize; + white-space: nowrap; // tags shouldn't go over two lines. } - &.is-selected { + &[aria-selected] { background: $gray-100; - - .block-editor-link-control__search-item-type { - background: $white; - } } &.is-current { @@ -198,7 +168,6 @@ $preview-image-height: 140px; .block-editor-link-control__search-item-icon { position: relative; - top: 0.2em; margin-right: $grid-unit-10; max-height: 24px; flex-shrink: 0; @@ -217,18 +186,6 @@ $preview-image-height: 140px; max-height: 32px; } - .block-editor-link-control__search-item-info, - .block-editor-link-control__search-item-title { - overflow: hidden; - text-overflow: ellipsis; - - .components-external-link__icon { - position: absolute; - right: 0; - margin-top: 0; - } - } - .block-editor-link-control__search-item-title { display: block; margin-bottom: 0.2em; @@ -236,7 +193,7 @@ $preview-image-height: 140px; position: relative; mark { - font-weight: 700; + font-weight: 600; color: inherit; background-color: transparent; } @@ -250,28 +207,6 @@ $preview-image-height: 140px; } } - .block-editor-link-control__search-item-info { - display: block; - color: $gray-700; - font-size: 0.9em; - line-height: 1.3; - } - - .block-editor-link-control__search-item-error-notice { - font-style: italic; - font-size: 1.1em; - } - - .block-editor-link-control__search-item-type { - display: block; - padding: 3px 6px; - margin-left: auto; - font-size: 0.9em; - background-color: $gray-100; - border-radius: 2px; - white-space: nowrap; // tags shouldn't go over two lines. - } - .block-editor-link-control__search-item-description { padding-top: 12px; margin: 0; @@ -411,11 +346,6 @@ $preview-image-height: 140px; } } -// Specificity override -.block-editor-link-control__search-results div[role="menu"] > .block-editor-link-control__search-item.block-editor-link-control__search-item { - padding: 10px; -} - .block-editor-link-control__drawer { display: flex; // allow for ordering. order: 30; diff --git a/packages/block-editor/src/components/link-control/test/index.js b/packages/block-editor/src/components/link-control/test/index.js index 20b72363391ee..82bb82a0cfc51 100644 --- a/packages/block-editor/src/components/link-control/test/index.js +++ b/packages/block-editor/src/components/link-control/test/index.js @@ -478,16 +478,16 @@ describe( 'Searching for a link', () => { // The fallback URL suggestion should not be shown when input is not URL-like. expect( searchResultElements[ searchResultElements.length - 1 ] - ).not.toHaveTextContent( 'URL' ); + ).not.toHaveTextContent( 'Press ENTER to add this link' ); } ); it.each( [ - [ 'https://wordpress.org', 'URL' ], - [ 'http://wordpress.org', 'URL' ], - [ 'www.wordpress.org', 'URL' ], - [ 'wordpress.org', 'URL' ], - [ 'ftp://wordpress.org', 'URL' ], + [ 'https://wordpress.org', 'link' ], + [ 'http://wordpress.org', 'link' ], + [ 'www.wordpress.org', 'link' ], + [ 'wordpress.org', 'link' ], + [ 'ftp://wordpress.org', 'link' ], [ 'mailto:hello@wordpress.org', 'mailto' ], [ 'tel:123456789', 'tel' ], [ '#internal', 'internal' ], @@ -667,7 +667,6 @@ describe( 'Manual link entry', () => { expect( searchResultElements ).toBeVisible(); expect( searchResultElements ).toHaveTextContent( searchTerm ); - expect( searchResultElements ).toHaveTextContent( 'URL' ); expect( searchResultElements ).toHaveTextContent( 'Press ENTER to add this link' ); diff --git a/packages/block-editor/src/components/use-block-display-information/index.js b/packages/block-editor/src/components/use-block-display-information/index.js index 87909cea45f63..1cff9da4bc04a 100644 --- a/packages/block-editor/src/components/use-block-display-information/index.js +++ b/packages/block-editor/src/components/use-block-display-information/index.js @@ -67,8 +67,11 @@ export default function useBlockDisplayInformation( clientId ) { return useSelect( ( select ) => { if ( ! clientId ) return null; - const { getBlockName, getBlockAttributes } = - select( blockEditorStore ); + const { + getBlockName, + getBlockAttributes, + __experimentalGetReusableBlockTitle, + } = select( blockEditorStore ); const { getBlockType, getActiveBlockVariation } = select( blocksStore ); const blockName = getBlockName( clientId ); @@ -76,12 +79,16 @@ export default function useBlockDisplayInformation( clientId ) { if ( ! blockType ) return null; const attributes = getBlockAttributes( clientId ); const match = getActiveBlockVariation( blockName, attributes ); - const isSynced = - isReusableBlock( blockType ) || isTemplatePart( blockType ); + const isReusable = isReusableBlock( blockType ); + const resusableTitle = isReusable + ? __experimentalGetReusableBlockTitle( attributes.ref ) + : undefined; + const title = resusableTitle || blockType.title; + const isSynced = isReusable || isTemplatePart( blockType ); const positionLabel = getPositionTypeLabel( attributes ); const blockTypeInfo = { isSynced, - title: blockType.title, + title, icon: blockType.icon, description: blockType.description, anchor: attributes?.anchor, diff --git a/packages/block-editor/src/hooks/margin.js b/packages/block-editor/src/hooks/margin.js index bdf5cabea6a9d..8f723b3f8c97d 100644 --- a/packages/block-editor/src/hooks/margin.js +++ b/packages/block-editor/src/hooks/margin.js @@ -23,7 +23,10 @@ export function MarginVisualizer( { clientId, attributes, forceShow } ) { const margin = attributes?.style?.spacing?.margin; useEffect( () => { - if ( ! blockElement ) { + if ( + ! blockElement || + null === blockElement.ownerDocument.defaultView + ) { return; } diff --git a/packages/block-editor/src/hooks/padding.js b/packages/block-editor/src/hooks/padding.js index f451f2cb4262e..b6e4e50e30f9c 100644 --- a/packages/block-editor/src/hooks/padding.js +++ b/packages/block-editor/src/hooks/padding.js @@ -23,7 +23,10 @@ export function PaddingVisualizer( { clientId, attributes, forceShow } ) { const padding = attributes?.style?.spacing?.padding; useEffect( () => { - if ( ! blockElement ) { + if ( + ! blockElement || + null === blockElement.ownerDocument.defaultView + ) { return; } diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 4cca99535a8e5..b6d030293c815 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -2018,7 +2018,7 @@ export const getInserterItems = createSelector( title: reusableBlock.title.raw, icon, category: 'reusable', - keywords: [], + keywords: [ 'reusable' ], isDisabled: false, utility: 1, // Deprecated. frecency, diff --git a/packages/block-editor/src/store/test/selectors.js b/packages/block-editor/src/store/test/selectors.js index 270bceaad447f..fc4db2c41f8a3 100644 --- a/packages/block-editor/src/store/test/selectors.js +++ b/packages/block-editor/src/store/test/selectors.js @@ -3351,7 +3351,7 @@ describe( 'selectors', () => { id: 'core/block/1', initialAttributes: { ref: 1 }, isDisabled: false, - keywords: [], + keywords: [ 'reusable' ], name: 'core/block', syncStatus: undefined, title: 'Reusable Block 1', diff --git a/packages/block-library/src/block/block.json b/packages/block-library/src/block/block.json index 5846e7ead0c9b..d6630177fc685 100644 --- a/packages/block-library/src/block/block.json +++ b/packages/block-library/src/block/block.json @@ -4,7 +4,8 @@ "name": "core/block", "title": "Pattern", "category": "reusable", - "description": "Create and save content to reuse across your site. Update the block, and the changes apply everywhere it’s used.", + "description": "Create and save content to reuse across your site. Update the pattern, and the changes apply everywhere it’s used.", + "keywords": [ "reusable" ], "textdomain": "default", "attributes": { "ref": { diff --git a/packages/block-library/src/cover/deprecated.js b/packages/block-library/src/cover/deprecated.js index 7212af5c5cdc9..d1801a11ade9d 100644 --- a/packages/block-library/src/cover/deprecated.js +++ b/packages/block-library/src/cover/deprecated.js @@ -329,6 +329,7 @@ const v11 = { ); }, + migrate: migrateTag, }; // Deprecation for blocks that renders fixed background as backgroud from the main block container. @@ -465,6 +466,7 @@ const v10 = { ); }, + migrate: migrateTag, }; // Deprecation for blocks with `minHeightUnit` set but no `minHeight`. diff --git a/packages/block-library/src/footnotes/format.js b/packages/block-library/src/footnotes/format.js index 40de6a132ea99..eb700787d02ee 100644 --- a/packages/block-library/src/footnotes/format.js +++ b/packages/block-library/src/footnotes/format.js @@ -40,24 +40,30 @@ export const format = { } = useSelect( blockEditorStore ); const { selectionChange, insertBlock } = useDispatch( blockEditorStore ); + function onClick() { registry.batch( () => { - const id = createId(); - const newValue = insertObject( - value, - { - type: formatName, - attributes: { - 'data-fn': id, + let id; + if ( isObjectActive ) { + const object = value.replacements[ value.start ]; + id = object?.attributes?.[ 'data-fn' ]; + } else { + id = createId(); + const newValue = insertObject( + value, + { + type: formatName, + attributes: { + 'data-fn': id, + }, + innerHTML: `*`, }, - innerHTML: `*`, - }, - value.end, - value.end - ); - newValue.start = newValue.end - 1; - - onChange( newValue ); + value.end, + value.end + ); + newValue.start = newValue.end - 1; + onChange( newValue ); + } // BFS search to find the first footnote block. let fnBlock = null; diff --git a/packages/block-library/src/navigation/edit/index.js b/packages/block-library/src/navigation/edit/index.js index 7ec990d5b7389..87078999b0c9b 100644 --- a/packages/block-library/src/navigation/edit/index.js +++ b/packages/block-library/src/navigation/edit/index.js @@ -196,7 +196,7 @@ function Navigation( { convert: convertClassicMenu, status: classicMenuConversionStatus, error: classicMenuConversionError, - } = useConvertClassicToBlockMenu( clientId ); + } = useConvertClassicToBlockMenu( createNavigationMenu ); const isConvertingClassicMenu = classicMenuConversionStatus === CLASSIC_MENU_CONVERSION_PENDING; diff --git a/packages/block-library/src/navigation/edit/use-convert-classic-menu-to-block-menu.js b/packages/block-library/src/navigation/edit/use-convert-classic-menu-to-block-menu.js index 70f9a6ff4bfea..405663726cee8 100644 --- a/packages/block-library/src/navigation/edit/use-convert-classic-menu-to-block-menu.js +++ b/packages/block-library/src/navigation/edit/use-convert-classic-menu-to-block-menu.js @@ -9,7 +9,6 @@ import { __, sprintf } from '@wordpress/i18n'; /** * Internal dependencies */ -import useCreateNavigationMenu from './use-create-navigation-menu'; import menuItemsToBlocks from '../menu-items-to-blocks'; export const CLASSIC_MENU_CONVERSION_SUCCESS = 'success'; @@ -21,15 +20,7 @@ export const CLASSIC_MENU_CONVERSION_IDLE = 'idle'; // do not import the same classic menu twice. let classicMenuBeingConvertedId = null; -function useConvertClassicToBlockMenu( clientId ) { - /* - * The wp_navigation post is created as a draft so the changes on the frontend and - * the site editor are not permanent without a save interaction done by the user. - */ - const { create: createNavigationMenu } = useCreateNavigationMenu( - clientId, - 'draft' - ); +function useConvertClassicToBlockMenu( createNavigationMenu ) { const registry = useRegistry(); const { editEntityRecord } = useDispatch( coreStore ); diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index bdf9ef752d98d..c6e48d108ae50 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Enhancements + +- `Navigator`: Add `replace` option to `navigator.goTo()` and `navigator.goToParent()` ([#52456](https://github.com/WordPress/gutenberg/pull/52456)). + ### Bug Fix - `UnitControl`: Fix crash when certain units are used ([#52211](https://github.com/WordPress/gutenberg/pull/52211)). diff --git a/packages/components/src/navigator/navigator-provider/component.tsx b/packages/components/src/navigator/navigator-provider/component.tsx index 4c98db5b1a4d1..9bcffad6bc54b 100644 --- a/packages/components/src/navigator/navigator-provider/component.tsx +++ b/packages/components/src/navigator/navigator-provider/component.tsx @@ -148,6 +148,7 @@ function UnconnectedNavigatorProvider( focusTargetSelector, isBack = false, skipFocus = false, + replace = false, ...restOptions } = options; @@ -172,34 +173,38 @@ function UnconnectedNavigatorProvider( skipFocus, }; - if ( prevLocationHistory.length < 1 ) { - return [ newLocation ]; + if ( prevLocationHistory.length === 0 ) { + return replace ? [] : [ newLocation ]; } - return [ - ...prevLocationHistory.slice( - prevLocationHistory.length > MAX_HISTORY_LENGTH - 1 - ? 1 - : 0, - -1 - ), - // Assign `focusTargetSelector` to the previous location in history - // (the one we just navigated from). - { - ...prevLocationHistory[ - prevLocationHistory.length - 1 - ], - focusTargetSelector, - }, - newLocation, - ]; + const newLocationHistory = prevLocationHistory.slice( + prevLocationHistory.length > MAX_HISTORY_LENGTH - 1 ? 1 : 0, + -1 + ); + + if ( ! replace ) { + newLocationHistory.push( + // Assign `focusTargetSelector` to the previous location in history + // (the one we just navigated from). + { + ...prevLocationHistory[ + prevLocationHistory.length - 1 + ], + focusTargetSelector, + } + ); + } + + newLocationHistory.push( newLocation ); + + return newLocationHistory; } ); }, [ goBack ] ); - const goToParent: NavigatorContextType[ 'goToParent' ] = - useCallback( () => { + const goToParent: NavigatorContextType[ 'goToParent' ] = useCallback( + ( options = {} ) => { const currentPath = currentLocationHistory.current[ currentLocationHistory.current.length - 1 @@ -214,8 +219,10 @@ function UnconnectedNavigatorProvider( if ( parentPath === undefined ) { return; } - goTo( parentPath, { isBack: true } ); - }, [ goTo ] ); + goTo( parentPath, { ...options, isBack: true } ); + }, + [ goTo ] + ); const navigatorContextValue: NavigatorContextType = useMemo( () => ( { diff --git a/packages/components/src/navigator/types.ts b/packages/components/src/navigator/types.ts index e638084e8376d..557f8074fd42e 100644 --- a/packages/components/src/navigator/types.ts +++ b/packages/components/src/navigator/types.ts @@ -14,8 +14,11 @@ export type NavigateOptions = { focusTargetSelector?: string; isBack?: boolean; skipFocus?: boolean; + replace?: boolean; }; +export type NavigateToParentOptions = Omit< NavigateOptions, 'isBack' >; + export type NavigatorLocation = NavigateOptions & { isInitial?: boolean; path?: string; @@ -28,7 +31,7 @@ export type Navigator = { params: MatchParams; goTo: ( path: string, options?: NavigateOptions ) => void; goBack: () => void; - goToParent: () => void; + goToParent: ( options?: NavigateToParentOptions ) => void; }; export type NavigatorContext = Navigator & { diff --git a/packages/e2e-tests/specs/editor/various/reusable-blocks.test.js b/packages/e2e-tests/specs/editor/various/reusable-blocks.test.js index 0a18c75528930..2f237822b1ccc 100644 --- a/packages/e2e-tests/specs/editor/various/reusable-blocks.test.js +++ b/packages/e2e-tests/specs/editor/various/reusable-blocks.test.js @@ -352,7 +352,7 @@ describe( 'Reusable blocks', () => { expect( reusableBlockWithParagraph ).toBeTruthy(); // Convert back to regular blocks. - await clickBlockToolbarButton( 'Select Pattern' ); + await clickBlockToolbarButton( 'Select Edited block' ); await clickBlockToolbarButton( 'Detach pattern' ); await page.waitForXPath( selector, { hidden: true, diff --git a/packages/edit-post/src/components/layout/index.js b/packages/edit-post/src/components/layout/index.js index 15bc017900daa..b7aa8bcd025af 100644 --- a/packages/edit-post/src/components/layout/index.js +++ b/packages/edit-post/src/components/layout/index.js @@ -13,6 +13,7 @@ import { EditorNotices, EditorKeyboardShortcutsRegister, EditorSnackbars, + PostSyncStatusModal, store as editorStore, } from '@wordpress/editor'; import { useSelect, useDispatch } from '@wordpress/data'; @@ -291,6 +292,7 @@ function Layout( { styles } ) { + diff --git a/packages/edit-site/src/components/block-editor/index.js b/packages/edit-site/src/components/block-editor/index.js index fcd28948ccbb3..e2d9e2680b263 100644 --- a/packages/edit-site/src/components/block-editor/index.js +++ b/packages/edit-site/src/components/block-editor/index.js @@ -1,7 +1,3 @@ -/** - * External dependencies - */ - /** * WordPress dependencies */ diff --git a/packages/edit-site/src/components/block-editor/use-site-editor-settings.js b/packages/edit-site/src/components/block-editor/use-site-editor-settings.js index 4241c7f55cb67..ab94f8f86000b 100644 --- a/packages/edit-site/src/components/block-editor/use-site-editor-settings.js +++ b/packages/edit-site/src/components/block-editor/use-site-editor-settings.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { useSelect } from '@wordpress/data'; +import { useDispatch, useSelect } from '@wordpress/data'; import { useMemo } from '@wordpress/element'; import { store as coreStore } from '@wordpress/core-data'; /** @@ -11,16 +11,21 @@ import { store as editSiteStore } from '../../store'; import { unlock } from '../../lock-unlock'; import inserterMediaCategories from './inserter-media-categories'; -export default function useSiteEditorSettings( templateType ) { - const { storedSettings, canvasMode } = useSelect( ( select ) => { - const { getSettings, getCanvasMode } = unlock( - select( editSiteStore ) - ); - return { - storedSettings: getSettings(), - canvasMode: getCanvasMode(), - }; - }, [] ); +export default function useSiteEditorSettings() { + const { setIsInserterOpened } = useDispatch( editSiteStore ); + const { storedSettings, canvasMode, templateType } = useSelect( + ( select ) => { + const { getSettings, getCanvasMode, getEditedPostType } = unlock( + select( editSiteStore ) + ); + return { + storedSettings: getSettings( setIsInserterOpened ), + canvasMode: getCanvasMode(), + templateType: getEditedPostType(), + }; + }, + [ setIsInserterOpened ] + ); const settingsBlockPatterns = storedSettings.__experimentalAdditionalBlockPatterns ?? // WP 6.0 diff --git a/packages/edit-site/src/components/layout/style.scss b/packages/edit-site/src/components/layout/style.scss index 2fe210327661d..e5661c61eb789 100644 --- a/packages/edit-site/src/components/layout/style.scss +++ b/packages/edit-site/src/components/layout/style.scss @@ -61,6 +61,7 @@ .edit-site-layout__sidebar { z-index: z-index(".edit-site-layout__sidebar"); width: 100vw; + flex-shrink: 0; @include break-medium { width: $nav-sidebar-width; diff --git a/packages/edit-site/src/components/page-patterns/grid.js b/packages/edit-site/src/components/page-patterns/grid.js index 1902b36982c14..47bcdc8a1f768 100644 --- a/packages/edit-site/src/components/page-patterns/grid.js +++ b/packages/edit-site/src/components/page-patterns/grid.js @@ -1,26 +1,115 @@ /** * WordPress dependencies */ -import { __experimentalText as Text } from '@wordpress/components'; -import { useRef } from '@wordpress/element'; -import { __, sprintf } from '@wordpress/i18n'; +import { + __experimentalHStack as HStack, + __experimentalText as Text, + Button, +} from '@wordpress/components'; +import { useRef, useState, useMemo } from '@wordpress/element'; +import { __, _x, _n, sprintf } from '@wordpress/i18n'; +import { useAsyncList } from '@wordpress/compose'; /** * Internal dependencies */ import GridItem from './grid-item'; -const PAGE_SIZE = 100; +const PAGE_SIZE = 20; + +function Pagination( { currentPage, numPages, changePage, totalItems } ) { + return ( + + + { + // translators: %s: Total number of patterns. + sprintf( + // translators: %s: Total number of patterns. + _n( '%s item', '%s items', totalItems ), + totalItems + ) + } + + + + + + + { sprintf( + // translators: %1$s: Current page number, %2$s: Total number of pages. + _x( '%1$s of %2$s', 'paging' ), + currentPage, + numPages + ) } + + + + + + + ); +} export default function Grid( { categoryId, items, ...props } ) { + const [ currentPage, setCurrentPage ] = useState( 1 ); const gridRef = useRef(); + const totalItems = items.length; + const pageIndex = currentPage - 1; - if ( ! items?.length ) { + const list = useMemo( + () => + items.slice( + pageIndex * PAGE_SIZE, + pageIndex * PAGE_SIZE + PAGE_SIZE + ), + [ pageIndex, items ] + ); + + const asyncList = useAsyncList( list, { step: 10 } ); + + if ( ! list?.length ) { return null; } - const list = items.slice( 0, PAGE_SIZE ); - const restLength = items.length - PAGE_SIZE; + const numPages = Math.ceil( items.length / PAGE_SIZE ); + const changePage = ( page ) => { + const scrollContainer = document.querySelector( '.edit-site-patterns' ); + scrollContainer?.scrollTo( 0, 0 ); + + setCurrentPage( page ); + }; return ( <> @@ -30,7 +119,7 @@ export default function Grid( { categoryId, items, ...props } ) { { ...props } ref={ gridRef } > - { list.map( ( item ) => ( + { asyncList.map( ( item ) => ( ) ) } - { restLength > 0 && ( - - { sprintf( - /* translators: %d: number of patterns */ - __( '+ %d more patterns discoverable by searching' ), - restLength - ) } - + { numPages > 1 && ( + ) } ); diff --git a/packages/edit-site/src/components/page-patterns/patterns-list.js b/packages/edit-site/src/components/page-patterns/patterns-list.js index 7bf2a9d506584..f4fefed4a2d3e 100644 --- a/packages/edit-site/src/components/page-patterns/patterns-list.js +++ b/packages/edit-site/src/components/page-patterns/patterns-list.js @@ -15,7 +15,7 @@ import { import { __, isRTL } from '@wordpress/i18n'; import { chevronLeft, chevronRight } from '@wordpress/icons'; import { privateApis as routerPrivateApis } from '@wordpress/router'; -import { useViewportMatch, useAsyncList } from '@wordpress/compose'; +import { useViewportMatch } from '@wordpress/compose'; /** * Internal dependencies @@ -70,7 +70,6 @@ export default function PatternsList( { categoryId, type } ) { const hasPatterns = patterns.length; const title = SYNC_FILTERS[ syncFilter ]; const description = SYNC_DESCRIPTIONS[ syncFilter ]; - const shownPatterns = useAsyncList( patterns ); return ( @@ -132,7 +131,7 @@ export default function PatternsList( { categoryId, type } ) { { syncFilter !== 'all' && ( - + { title } { description ? ( @@ -145,7 +144,7 @@ export default function PatternsList( { categoryId, type } ) { { hasPatterns && ( diff --git a/packages/edit-site/src/components/page-patterns/style.scss b/packages/edit-site/src/components/page-patterns/style.scss index 79731999f46ef..d1fbedb141f70 100644 --- a/packages/edit-site/src/components/page-patterns/style.scss +++ b/packages/edit-site/src/components/page-patterns/style.scss @@ -1,6 +1,8 @@ .edit-site-patterns { - background: rgba(0, 0, 0, 0.15); + border: 1px solid $gray-800; + background: none; margin: $header-height 0 0; + border-radius: 0; .components-base-control { width: 100%; @include break-medium { @@ -59,6 +61,23 @@ background: $gray-700; color: $gray-100; } + + .edit-site-patterns__grid-pagination { + width: fit-content; + .components-button.is-tertiary { + width: $button-size-compact; + height: $button-size-compact; + color: $gray-100; + background-color: $gray-800; + &:disabled { + color: $gray-600; + background: none; + } + &:hover:not(:disabled) { + background-color: $gray-700; + } + } + } } .edit-site-patterns__section-header { @@ -74,6 +93,7 @@ // Small top padding required to avoid cutting off the visible outline // when hovering items. padding-top: $border-width-focus-fallback; + margin-top: 0; margin-bottom: $grid-unit-40; @include break-large { grid-template-columns: 1fr 1fr; diff --git a/packages/edit-site/src/components/page-template-parts/add-new-template-part.js b/packages/edit-site/src/components/page-template-parts/add-new-template-part.js new file mode 100644 index 0000000000000..d2b52a88701fd --- /dev/null +++ b/packages/edit-site/src/components/page-template-parts/add-new-template-part.js @@ -0,0 +1,57 @@ +/** + * WordPress dependencies + */ +import { privateApis as routerPrivateApis } from '@wordpress/router'; +import { useSelect } from '@wordpress/data'; +import { store as coreStore } from '@wordpress/core-data'; +import { useState } from '@wordpress/element'; +import { Button } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { unlock } from '../../lock-unlock'; +import { store as editSiteStore } from '../../store'; +import CreateTemplatePartModal from '../create-template-part-modal'; + +const { useHistory } = unlock( routerPrivateApis ); + +export default function AddNewTemplatePart() { + const { canCreate, postType } = useSelect( ( select ) => { + const { supportsTemplatePartsMode } = + select( editSiteStore ).getSettings(); + return { + canCreate: ! supportsTemplatePartsMode, + postType: select( coreStore ).getPostType( 'wp_template_part' ), + }; + }, [] ); + const [ isModalOpen, setIsModalOpen ] = useState( false ); + const history = useHistory(); + + if ( ! canCreate || ! postType ) { + return null; + } + + return ( + <> + + { isModalOpen && ( + setIsModalOpen( false ) } + blocks={ [] } + onCreate={ ( templatePart ) => { + setIsModalOpen( false ); + history.push( { + postId: templatePart.id, + postType: 'wp_template_part', + canvas: 'edit', + } ); + } } + onError={ () => setIsModalOpen( false ) } + /> + ) } + + ); +} diff --git a/packages/edit-site/src/components/page-template-parts/index.js b/packages/edit-site/src/components/page-template-parts/index.js index 7e9c8cb6dd6e1..24b042ed92001 100644 --- a/packages/edit-site/src/components/page-template-parts/index.js +++ b/packages/edit-site/src/components/page-template-parts/index.js @@ -7,8 +7,7 @@ import { __experimentalVStack as VStack, } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; -import { useSelect } from '@wordpress/data'; -import { store as coreStore, useEntityRecords } from '@wordpress/core-data'; +import { useEntityRecords } from '@wordpress/core-data'; import { decodeEntities } from '@wordpress/html-entities'; /** @@ -19,8 +18,7 @@ import Table from '../table'; import Link from '../routes/link'; import AddedBy from '../list/added-by'; import TemplateActions from '../template-actions'; -import AddNewTemplate from '../add-new-template'; -import { store as editSiteStore } from '../../store'; +import AddNewTemplatePart from './add-new-template-part'; export default function PageTemplateParts() { const { records: templateParts } = useEntityRecords( @@ -31,15 +29,6 @@ export default function PageTemplateParts() { } ); - const { canCreate } = useSelect( ( select ) => { - const { supportsTemplatePartsMode } = - select( editSiteStore ).getSettings(); - return { - postType: select( coreStore ).getPostType( 'wp_template_part' ), - canCreate: ! supportsTemplatePartsMode, - }; - } ); - const columns = [ { header: __( 'Template Part' ), @@ -87,15 +76,7 @@ export default function PageTemplateParts() { return ( - ) - } + actions={ } > { templateParts && ( diff --git a/packages/edit-site/src/components/resizable-frame/index.js b/packages/edit-site/src/components/resizable-frame/index.js index a13881e7905ff..1bb9315a07e38 100644 --- a/packages/edit-site/src/components/resizable-frame/index.js +++ b/packages/edit-site/src/components/resizable-frame/index.js @@ -9,9 +9,12 @@ import classnames from 'classnames'; import { useState, useRef, useEffect } from '@wordpress/element'; import { ResizableBox, + Tooltip, __unstableMotion as motion, } from '@wordpress/components'; -import { useDispatch } from '@wordpress/data'; +import { useInstanceId } from '@wordpress/compose'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { __ } from '@wordpress/i18n'; /** * Internal dependencies @@ -33,7 +36,7 @@ const HANDLE_STYLES_OVERRIDE = { }; // The minimum width of the frame (in px) while resizing. -const FRAME_MIN_WIDTH = 340; +const FRAME_MIN_WIDTH = 320; // The reference width of the frame (in px) used to calculate the aspect ratio. const FRAME_REFERENCE_WIDTH = 1300; // 9 : 19.5 is the target aspect ratio enforced (when possible) while resizing. @@ -42,6 +45,8 @@ const FRAME_TARGET_ASPECT_RATIO = 9 / 19.5; // viewport's edge. If the frame is resized to be closer to the viewport's edge // than this distance, then "canvas mode" will be enabled. const SNAP_TO_EDIT_CANVAS_MODE_THRESHOLD = 200; +// Default size for the `frameSize` state. +const INITIAL_FRAME_SIZE = { width: '100%', height: '100%' }; function calculateNewHeight( width, initialAspectRatio ) { const lerp = ( a, b, amount ) => { @@ -78,22 +83,27 @@ function ResizableFrame( { oversizedClassName, innerContentStyle, } ) { - const [ frameSize, setFrameSize ] = useState( { - width: '100%', - height: '100%', - } ); + const [ frameSize, setFrameSize ] = useState( INITIAL_FRAME_SIZE ); // The width of the resizable frame when a new resize gesture starts. const [ startingWidth, setStartingWidth ] = useState(); const [ isResizing, setIsResizing ] = useState( false ); - const [ isHovering, setIsHovering ] = useState( false ); + const [ shouldShowHandle, setShouldShowHandle ] = useState( false ); const [ isOversized, setIsOversized ] = useState( false ); const [ resizeRatio, setResizeRatio ] = useState( 1 ); + const canvasMode = useSelect( + ( select ) => unlock( select( editSiteStore ) ).getCanvasMode(), + [] + ); const { setCanvasMode } = unlock( useDispatch( editSiteStore ) ); const initialAspectRatioRef = useRef( null ); // The width of the resizable frame on initial render. const initialComputedWidthRef = useRef( null ); const FRAME_TRANSITION = { type: 'tween', duration: isResizing ? 0 : 0.5 }; const frameRef = useRef( null ); + const resizableHandleHelpId = useInstanceId( + ResizableFrame, + 'edit-site-resizable-frame-handle-help' + ); // Remember frame dimensions on initial render. useEffect( () => { @@ -154,13 +164,40 @@ function ResizableFrame( { if ( remainingWidth > SNAP_TO_EDIT_CANVAS_MODE_THRESHOLD ) { // Reset the initial aspect ratio if the frame is resized slightly // above the sidebar but not far enough to trigger full screen. - setFrameSize( { width: '100%', height: '100%' } ); + setFrameSize( INITIAL_FRAME_SIZE ); } else { // Trigger full screen if the frame is resized far enough to the left. setCanvasMode( 'edit' ); } }; + // Handle resize by arrow keys + const handleResizableHandleKeyDown = ( event ) => { + if ( ! [ 'ArrowLeft', 'ArrowRight' ].includes( event.key ) ) { + return; + } + + event.preventDefault(); + + const step = 20 * ( event.shiftKey ? 5 : 1 ); + const delta = step * ( event.key === 'ArrowLeft' ? 1 : -1 ); + const newWidth = Math.min( + Math.max( + FRAME_MIN_WIDTH, + frameRef.current.resizable.offsetWidth + delta + ), + initialComputedWidthRef.current + ); + + setFrameSize( { + width: newWidth, + height: calculateNewHeight( + newWidth, + initialAspectRatioRef.current + ), + } ); + }; + const frameAnimationVariants = { default: { flexGrow: 0, @@ -173,16 +210,26 @@ function ResizableFrame( { }; const resizeHandleVariants = { - default: { + hidden: { + opacity: 0, + left: 0, + }, + visible: { opacity: 1, left: -16, }, - resizing: { + active: { opacity: 1, left: -16, scaleY: 1.3, }, }; + const currentResizeHandleVariant = ( () => { + if ( isResizing ) { + return 'active'; + } + return shouldShowHandle ? 'visible' : 'hidden'; + } )(); return ( setIsHovering( true ) } - onMouseOut={ () => setIsHovering( false ) } + onFocus={ () => setShouldShowHandle( true ) } + onBlur={ () => setShouldShowHandle( false ) } + onMouseOver={ () => setShouldShowHandle( true ) } + onMouseOut={ () => setShouldShowHandle( false ) } handleComponent={ { - left: - isHovering || isResizing ? ( - - ) : null, + left: canvasMode === 'view' && ( + <> + + { /* Disable reason: role="separator" does in fact support aria-valuenow */ } + { /* eslint-disable-next-line jsx-a11y/role-supports-aria-props */ } + + + + + ), } } onResizeStart={ handleResizeStart } onResize={ handleResize } diff --git a/packages/edit-site/src/components/resizable-frame/style.scss b/packages/edit-site/src/components/resizable-frame/style.scss index 9a959afe845e4..596304be8d6b9 100644 --- a/packages/edit-site/src/components/resizable-frame/style.scss +++ b/packages/edit-site/src/components/resizable-frame/style.scss @@ -1,6 +1,23 @@ .edit-site-resizable-frame__inner { position: relative; + &.edit-site-layout__resizable-frame-oversized { + @at-root { + body:has(&) { + .edit-site-site-hub { + .edit-site-site-hub__site-title, + .edit-site-site-hub_toggle-command-center { + opacity: 0 !important; + } + + .edit-site-site-hub__view-mode-toggle-container { + background-color: transparent; + } + } + } + } + } + &.is-resizing { @at-root { body:has(&) { @@ -30,11 +47,13 @@ .edit-site-resizable-frame__handle { align-items: center; background-color: rgba($gray-700, 0.4); + border: 0; border-radius: $grid-unit-05; cursor: col-resize; display: flex; height: $grid-unit-80; justify-content: flex-end; + padding: 0; position: absolute; top: calc(50% - #{$grid-unit-40}); width: $grid-unit-05; @@ -56,16 +75,14 @@ width: $grid-unit-40; } - &:hover, - .is-resizing & { - background-color: var(--wp-admin-theme-color); + &:focus-visible { + // Works with Windows high contrast mode while also hiding weird outline in Safari. + outline: 2px solid transparent; } - .edit-site-resizable-frame__handle-label { - background: var(--wp-admin-theme-color); - border-radius: 2px; - color: #fff; - margin-right: $grid-unit-10; - padding: 4px 8px; + &:hover, + &:focus, + &.is-resizing { + background-color: var(--wp-admin-theme-color); } } diff --git a/packages/edit-site/src/components/sidebar-navigation-item/style.scss b/packages/edit-site/src/components/sidebar-navigation-item/style.scss index d81b6764f7b72..14dbc54efe523 100644 --- a/packages/edit-site/src/components/sidebar-navigation-item/style.scss +++ b/packages/edit-site/src/components/sidebar-navigation-item/style.scss @@ -11,6 +11,10 @@ &[aria-current] { color: $gray-200; background: $gray-800; + + .edit-site-sidebar-navigation-item__drilldown-indicator { + fill: $gray-200; + } } &[aria-current] { @@ -19,7 +23,7 @@ } .edit-site-sidebar-navigation-item__drilldown-indicator { - fill: $gray-700; + fill: $gray-600; } &:is(a) { @@ -31,6 +35,11 @@ box-shadow: none; outline: none; } + + &:focus-visible { + box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); + outline: 2px solid transparent; + } } &.with-suffix { diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-main/template-part-hint.js b/packages/edit-site/src/components/sidebar-navigation-screen-main/template-part-hint.js index 8fbe74f81bb4d..c6f270465b86f 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-main/template-part-hint.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-main/template-part-hint.js @@ -28,9 +28,7 @@ export default function TemplatePartHint() { setPreference( 'core', PREFERENCE_NAME, false ); } } > - { __( - 'Looking for template parts? You can now find them in the new "Patterns" page.' - ) } + { __( 'Looking for template parts? Find them in "Patterns".' ) } ); } diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menu/more-menu.js b/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menu/more-menu.js index 08c17304ca38b..4de10b8377da9 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menu/more-menu.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menu/more-menu.js @@ -33,6 +33,7 @@ export default function ScreenNavigationMoreMenu( props ) { <> diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menu/single-navigation-menu.js b/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menu/single-navigation-menu.js index 6b6fc8587228f..f52204f3a1843 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menu/single-navigation-menu.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menu/single-navigation-menu.js @@ -23,13 +23,13 @@ export default function SingleNavigationMenu( { - + } title={ decodeEntities( menuTitle ) } diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-pages/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-pages/index.js index 567bf91aca69f..331221dde7985 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-pages/index.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-pages/index.js @@ -58,13 +58,16 @@ export default function SidebarNavigationScreenPages() { templates?.find( ( template ) => template.slug === 'home' ) || templates?.find( ( template ) => template.slug === 'index' ); + const getPostsPageTemplate = () => + templates?.find( ( template ) => template.slug === 'home' ) || + templates?.find( ( template ) => template.slug === 'index' ); + const pagesAndTemplates = pages?.concat( dynamicPageTemplates, [ homeTemplate, ] ); const { frontPage, postsPage } = useSelect( ( select ) => { const { getEntityRecord } = select( coreStore ); - const siteSettings = getEntityRecord( 'root', 'site' ); return { frontPage: siteSettings?.page_on_front, @@ -106,6 +109,27 @@ export default function SidebarNavigationScreenPages() { setShowAddPage( false ); }; + const getPageProps = ( id ) => { + let itemIcon = page; + const postsPageTemplateId = + postsPage && postsPage === id ? getPostsPageTemplate()?.id : null; + + switch ( id ) { + case frontPage: + itemIcon = home; + break; + case postsPage: + itemIcon = verse; + break; + } + + return { + icon: itemIcon, + postType: postsPageTemplateId ? 'wp_template' : 'page', + postId: postsPageTemplateId || id, + }; + }; + return ( <> { showAddPage && ( @@ -152,34 +176,20 @@ export default function SidebarNavigationScreenPages() { ) } - { reorderedPages?.map( ( item ) => { - let itemIcon; - switch ( item.id ) { - case frontPage: - itemIcon = home; - break; - case postsPage: - itemIcon = verse; - break; - default: - itemIcon = page; - } - return ( - - - { decodeEntities( - item?.title?.rendered || - __( '(no title)' ) - ) } - - - ); - } ) } + { reorderedPages?.map( ( { id, title } ) => ( + + + { decodeEntities( + title?.rendered || + __( '(no title)' ) + ) } + + + ) ) } ) } diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-pattern/template-part-navigation-menu.js b/packages/edit-site/src/components/sidebar-navigation-screen-pattern/template-part-navigation-menu.js index f451c17e00adb..a57f5e025907c 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-pattern/template-part-navigation-menu.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-pattern/template-part-navigation-menu.js @@ -19,8 +19,9 @@ export default function TemplatePartNavigationMenu( { id } ) { <> { title?.rendered || __( 'Navigation' ) } diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-pattern/template-part-navigation-menus.js b/packages/edit-site/src/components/sidebar-navigation-screen-pattern/template-part-navigation-menus.js index 418b1d4423b20..d04eba2e435b4 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-pattern/template-part-navigation-menus.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-pattern/template-part-navigation-menus.js @@ -22,8 +22,9 @@ export default function TemplatePartNavigationMenus( { menus } ) { <> { __( 'Navigation' ) } diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-patterns/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-patterns/index.js index d3fc15358027b..c7d3a3b77d151 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-patterns/index.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-patterns/index.js @@ -107,7 +107,7 @@ export default function SidebarNavigationScreenPatterns() { const { templatePartAreas, hasTemplateParts, isLoading } = useTemplatePartAreas(); const { patternCategories, hasPatterns } = usePatternCategories(); - const { myPatterns, hasPatterns: hasMyPatterns } = useMyPatterns(); + const { myPatterns } = useMyPatterns(); const isTemplatePartsMode = useSelect( ( select ) => { const settings = select( editSiteStore ).getSettings(); @@ -153,23 +153,25 @@ export default function SidebarNavigationScreenPatterns() { ) } - { hasMyPatterns && ( - - - - ) } + + + { hasTemplateParts && ( { ! isRoot && ! backPath && ( - { + if ( navigator.location.isInitial ) { + navigator.goToParent( { replace: true } ); + } else { + navigator.goBack(); + } + } } + icon={ icon } label={ __( 'Back' ) } showTooltip={ false } /> ) } { ! isRoot && backPath && ( goTo( backPath, { isBack: true } ) } + onClick={ () => + navigator.goTo( backPath, { isBack: true } ) + } icon={ icon } label={ __( 'Back' ) } showTooltip={ false } diff --git a/packages/edit-site/src/components/site-hub/index.js b/packages/edit-site/src/components/site-hub/index.js index 897924a92a12b..1e6eddabe90a2 100644 --- a/packages/edit-site/src/components/site-hub/index.js +++ b/packages/edit-site/src/components/site-hub/index.js @@ -51,7 +51,10 @@ const SiteHub = forwardRef( ( props, ref ) => { const { open: openCommandCenter } = useDispatch( commandsStore ); const disableMotion = useReducedMotion(); - const { setCanvasMode } = unlock( useDispatch( editSiteStore ) ); + const { + setCanvasMode, + __experimentalSetPreviewDeviceType: setPreviewDeviceType, + } = unlock( useDispatch( editSiteStore ) ); const { clearSelectedBlock } = useDispatch( blockEditorStore ); const isBackToDashboardButton = canvasMode === 'view'; const siteIconButtonProps = isBackToDashboardButton @@ -67,6 +70,7 @@ const SiteHub = forwardRef( ( props, ref ) => { event.preventDefault(); if ( canvasMode === 'edit' ) { clearSelectedBlock(); + setPreviewDeviceType( 'desktop' ); setCanvasMode( 'view' ); } }, diff --git a/packages/edit-site/src/components/site-hub/style.scss b/packages/edit-site/src/components/site-hub/style.scss index d0689fec4efa9..70d256e2c4a56 100644 --- a/packages/edit-site/src/components/site-hub/style.scss +++ b/packages/edit-site/src/components/site-hub/style.scss @@ -8,6 +8,11 @@ gap: 0; } + .edit-site-site-hub__site-title, + .edit-site-site-hub_toggle-command-center { + transition: opacity ease 0.1s; + } + .edit-site-site-hub__site-view-link { flex-grow: 0; @include break-mobile() { diff --git a/packages/edit-site/src/components/sync-state-with-url/use-sync-path-with-url.js b/packages/edit-site/src/components/sync-state-with-url/use-sync-path-with-url.js index 45f7eb1b044c1..86928c1920a94 100644 --- a/packages/edit-site/src/components/sync-state-with-url/use-sync-path-with-url.js +++ b/packages/edit-site/src/components/sync-state-with-url/use-sync-path-with-url.js @@ -36,6 +36,12 @@ export function getPathFromURL( urlParams ) { return path; } +function isSubset( subset, superset ) { + return Object.entries( subset ).every( ( [ key, value ] ) => { + return superset[ key ] === value; + } ); +} + export default function useSyncPathWithURL() { const history = useHistory(); const { params: urlParams } = useLocation(); @@ -44,76 +50,77 @@ export default function useSyncPathWithURL() { params: navigatorParams, goTo, } = useNavigator(); - const currentUrlParams = useRef( urlParams ); - const currentPath = useRef( navigatorLocation.path ); const isMounting = useRef( true ); - useEffect( () => { - // The navigatorParams are only initially filled properly when the - // navigator screens mount. so we ignore the first synchronisation. - if ( isMounting.current ) { - isMounting.current = false; - return; - } - - function updateUrlParams( newUrlParams ) { - if ( - Object.entries( newUrlParams ).every( ( [ key, value ] ) => { - return currentUrlParams.current[ key ] === value; - } ) - ) { + useEffect( + () => { + // The navigatorParams are only initially filled properly when the + // navigator screens mount. so we ignore the first synchronisation. + if ( isMounting.current ) { + isMounting.current = false; return; } - const updatedParams = { - ...currentUrlParams.current, - ...newUrlParams, - }; - currentUrlParams.current = updatedParams; - history.push( updatedParams ); - } - if ( navigatorParams?.postType && navigatorParams?.postId ) { - updateUrlParams( { - postType: navigatorParams?.postType, - postId: navigatorParams?.postId, - path: undefined, - } ); - } else if ( - navigatorLocation.path.startsWith( '/page/' ) && - navigatorParams?.postId - ) { - updateUrlParams( { - postType: 'page', - postId: navigatorParams?.postId, - path: undefined, - } ); - } else if ( navigatorLocation.path === '/patterns' ) { - updateUrlParams( { - postType: undefined, - postId: undefined, - canvas: undefined, - path: navigatorLocation.path, - } ); - } else { - updateUrlParams( { - postType: undefined, - postId: undefined, - categoryType: undefined, - categoryId: undefined, - path: - navigatorLocation.path === '/' - ? undefined - : navigatorLocation.path, - } ); - } - }, [ navigatorLocation?.path, navigatorParams, history ] ); + function updateUrlParams( newUrlParams ) { + if ( isSubset( newUrlParams, urlParams ) ) { + return; + } + const updatedParams = { + ...urlParams, + ...newUrlParams, + }; + history.push( updatedParams ); + } - useEffect( () => { - currentUrlParams.current = urlParams; - const path = getPathFromURL( urlParams ); - if ( currentPath.current !== path ) { - currentPath.current = path; - goTo( path ); - } - }, [ urlParams, goTo ] ); + if ( navigatorParams?.postType && navigatorParams?.postId ) { + updateUrlParams( { + postType: navigatorParams?.postType, + postId: navigatorParams?.postId, + path: undefined, + } ); + } else if ( + navigatorLocation.path.startsWith( '/page/' ) && + navigatorParams?.postId + ) { + updateUrlParams( { + postType: 'page', + postId: navigatorParams?.postId, + path: undefined, + } ); + } else if ( navigatorLocation.path === '/patterns' ) { + updateUrlParams( { + postType: undefined, + postId: undefined, + canvas: undefined, + path: navigatorLocation.path, + } ); + } else { + updateUrlParams( { + postType: undefined, + postId: undefined, + categoryType: undefined, + categoryId: undefined, + path: + navigatorLocation.path === '/' + ? undefined + : navigatorLocation.path, + } ); + } + }, + // Trigger only when navigator changes to prevent infinite loops. + // eslint-disable-next-line react-hooks/exhaustive-deps + [ navigatorLocation?.path, navigatorParams ] + ); + + useEffect( + () => { + const path = getPathFromURL( urlParams ); + if ( navigatorLocation.path !== path ) { + goTo( path ); + } + }, + // Trigger only when URL changes to prevent infinite loops. + // eslint-disable-next-line react-hooks/exhaustive-deps + [ urlParams ] + ); } diff --git a/packages/editor/src/components/index.js b/packages/editor/src/components/index.js index b037397cfdd4c..aa20f9d6ae9fa 100644 --- a/packages/editor/src/components/index.js +++ b/packages/editor/src/components/index.js @@ -51,7 +51,10 @@ export { default as PostSlugCheck } from './post-slug/check'; export { default as PostSticky } from './post-sticky'; export { default as PostStickyCheck } from './post-sticky/check'; export { default as PostSwitchToDraftButton } from './post-switch-to-draft-button'; -export { default as PostSyncStatus } from './post-sync-status'; +export { + default as PostSyncStatus, + PostSyncStatusModal, +} from './post-sync-status'; export { default as PostTaxonomies } from './post-taxonomies'; export { FlatTermSelector as PostTaxonomiesFlatTermSelector } from './post-taxonomies/flat-term-selector'; export { HierarchicalTermSelector as PostTaxonomiesHierarchicalTermSelector } from './post-taxonomies/hierarchical-term-selector'; diff --git a/packages/editor/src/components/post-sync-status/index.js b/packages/editor/src/components/post-sync-status/index.js index 0600ece953173..8219ef7c0e4f5 100644 --- a/packages/editor/src/components/post-sync-status/index.js +++ b/packages/editor/src/components/post-sync-status/index.js @@ -1,9 +1,18 @@ /** * WordPress dependencies */ -import { useSelect } from '@wordpress/data'; +import { useSelect, useDispatch } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; -import { PanelRow } from '@wordpress/components'; +import { + PanelRow, + Modal, + Button, + __experimentalHStack as HStack, + __experimentalVStack as VStack, + ToggleControl, +} from '@wordpress/components'; +import { useEffect, useState } from '@wordpress/element'; +import { ReusableBlocksRenameHint } from '@wordpress/block-editor'; /** * Internal dependencies @@ -11,24 +20,108 @@ import { PanelRow } from '@wordpress/components'; import { store as editorStore } from '../../store'; export default function PostSyncStatus() { - const { syncStatus, postType } = useSelect( ( select ) => { + const { syncStatus, postType, meta } = useSelect( ( select ) => { const { getEditedPostAttribute } = select( editorStore ); return { syncStatus: getEditedPostAttribute( 'wp_pattern_sync_status' ), + meta: getEditedPostAttribute( 'meta' ), postType: getEditedPostAttribute( 'type' ), }; - }, [] ); + } ); + if ( postType !== 'wp_block' ) { return null; } - const isFullySynced = ! syncStatus; + // When the post is first created, the top level wp_pattern_sync_status is not set so get meta value instead. + const currentSyncStatus = + meta?.wp_pattern_sync_status === 'unsynced' ? 'unsynced' : syncStatus; return ( { __( 'Sync status' ) }
- { isFullySynced ? __( 'Fully synced' ) : __( 'Not synced' ) } + { currentSyncStatus === 'unsynced' + ? __( 'Not synced' ) + : __( 'Fully synced' ) }
); } + +export function PostSyncStatusModal() { + const { editPost } = useDispatch( editorStore ); + const [ isModalOpen, setIsModalOpen ] = useState( false ); + const [ syncType, setSyncType ] = useState( undefined ); + + const { postType, isNewPost } = useSelect( ( select ) => { + const { getEditedPostAttribute, isCleanNewPost } = + select( editorStore ); + return { + postType: getEditedPostAttribute( 'type' ), + isNewPost: isCleanNewPost(), + }; + }, [] ); + + useEffect( () => { + if ( isNewPost && postType === 'wp_block' ) { + setIsModalOpen( true ); + } + // We only want the modal to open when the page is first loaded. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [] ); + + const setSyncStatus = () => { + editPost( { + meta: { + wp_pattern_sync_status: syncType, + }, + } ); + }; + + if ( postType !== 'wp_block' || ! isNewPost ) { + return null; + } + + return ( + <> + { isModalOpen && ( + { + setIsModalOpen( false ); + } } + overlayClassName="reusable-blocks-menu-items__convert-modal" + > +
{ + event.preventDefault(); + setIsModalOpen( false ); + setSyncStatus(); + } } + > + + + { + setSyncType( + ! syncType ? 'unsynced' : undefined + ); + } } + /> + + + + + +
+ ) } + + ); +} diff --git a/packages/format-library/src/link/inline.js b/packages/format-library/src/link/inline.js index 064ed81943de1..384fa4c653e53 100644 --- a/packages/format-library/src/link/inline.js +++ b/packages/format-library/src/link/inline.js @@ -243,7 +243,7 @@ function InlineLinkUI( { return createInterpolateElement( sprintf( /* translators: %s: search term. */ - __( 'Create Page: %s' ), + __( 'Create page: %s' ), searchTerm ), { mark: } diff --git a/test/e2e/specs/editor/blocks/navigation.spec.js b/test/e2e/specs/editor/blocks/navigation.spec.js index 8f45e8ddcf7e3..b7632530ab150 100644 --- a/test/e2e/specs/editor/blocks/navigation.spec.js +++ b/test/e2e/specs/editor/blocks/navigation.spec.js @@ -1300,7 +1300,9 @@ class LinkControl { await expect( result ).toBeVisible(); return result - .locator( '.block-editor-link-control__search-item-title' ) // this is the only way to get the label text without the URL. + .locator( + '.components-menu-item__info-wrapper .components-menu-item__item' + ) // this is the only way to get the label text without the URL. .innerText(); } }