diff --git a/lib/block-supports/behaviors.php b/lib/block-supports/behaviors.php
index 6f442d7b0d2d7c..c1b3dceceee94d 100644
--- a/lib/block-supports/behaviors.php
+++ b/lib/block-supports/behaviors.php
@@ -84,17 +84,19 @@ function gutenberg_render_behaviors_support_lightbox( $block_content, $block ) {
$aria_label = __( 'Enlarge image', 'gutenberg' );
+ $processor->next_tag( 'img' );
$alt_attribute = $processor->get_attribute( 'alt' );
- if ( null !== $alt_attribute ) {
+ // An empty alt attribute `alt=""` is valid for decorative images.
+ if ( is_string( $alt_attribute ) ) {
$alt_attribute = trim( $alt_attribute );
}
+ // It only makes sense to append the alt text to the button aria-label when the alt text is non-empty.
if ( $alt_attribute ) {
/* translators: %s: Image alt text. */
$aria_label = sprintf( __( 'Enlarge image: %s', 'gutenberg' ), $alt_attribute );
}
- $content = $processor->get_updated_html();
// If we don't set a default, it won't work if Lightbox is set to enabled by default.
$lightbox_animation = 'zoom';
@@ -102,17 +104,15 @@ function gutenberg_render_behaviors_support_lightbox( $block_content, $block ) {
$lightbox_animation = $lightbox_settings['animation'];
}
- // We want to store the src in the context so we can set it dynamically when the lightbox is opened.
- $z = new WP_HTML_Tag_Processor( $content );
- $z->next_tag( 'img' );
-
+ // Note: We want to store the `src` in the context so we
+ // can set it dynamically when the lightbox is opened.
if ( isset( $block['attrs']['id'] ) ) {
$img_uploaded_src = wp_get_attachment_url( $block['attrs']['id'] );
$img_metadata = wp_get_attachment_metadata( $block['attrs']['id'] );
$img_width = $img_metadata['width'];
$img_height = $img_metadata['height'];
} else {
- $img_uploaded_src = $z->get_attribute( 'src' );
+ $img_uploaded_src = $processor->get_attribute( 'src' );
$img_width = 'none';
$img_height = 'none';
}
@@ -123,7 +123,7 @@ function gutenberg_render_behaviors_support_lightbox( $block_content, $block ) {
$scale_attr = false;
}
- $w = new WP_HTML_Tag_Processor( $content );
+ $w = new WP_HTML_Tag_Processor( $block_content );
$w->next_tag( 'figure' );
$w->add_class( 'wp-lightbox-container' );
$w->set_attribute( 'data-wp-interactive', true );
@@ -163,19 +163,20 @@ function gutenberg_render_behaviors_support_lightbox( $block_content, $block ) {
// Wrap the image in the body content with a button.
$img = null;
preg_match( '/ ]+>/', $body_content, $img );
- $button =
- '
- '
- . $img[0];
+
+ $button =
+ $img[0]
+ . ' ';
+
$body_content = preg_replace( '/ ]+>/', $button, $body_content );
// We need both a responsive image and an enlarged image to animate
@@ -183,7 +184,7 @@ function gutenberg_render_behaviors_support_lightbox( $block_content, $block ) {
// image is a copy of the one in the body, which animates immediately
// as the lightbox is opened, while the enlarged one is a full-sized
// version that will likely still be loading as the animation begins.
- $m = new WP_HTML_Tag_Processor( $content );
+ $m = new WP_HTML_Tag_Processor( $block_content );
$m->next_tag( 'figure' );
$m->add_class( 'responsive-image' );
$m->next_tag( 'img' );
@@ -199,7 +200,7 @@ function gutenberg_render_behaviors_support_lightbox( $block_content, $block ) {
$m->set_attribute( 'data-wp-style--object-fit', 'selectors.core.image.lightboxObjectFit' );
$initial_image_content = $m->get_updated_html();
- $q = new WP_HTML_Tag_Processor( $content );
+ $q = new WP_HTML_Tag_Processor( $block_content );
$q->next_tag( 'figure' );
$q->add_class( 'enlarged-image' );
$q->next_tag( 'img' );
@@ -219,7 +220,7 @@ function gutenberg_render_behaviors_support_lightbox( $block_content, $block ) {
$close_button_icon = ' ';
$close_button_color = esc_attr( wp_get_global_styles( array( 'color', 'text' ) ) );
- $dialog_label = $alt_attribute ? esc_attr( $alt_attribute ) : esc_attr__( 'Image', 'gutenberg' );
+ $dialog_label = esc_attr__( 'Enlarged image', 'gutenberg' );
$close_button_label = esc_attr__( 'Close', 'gutenberg' );
$lightbox_html = << array( $this, 'install_fonts' ),
'permission_callback' => array( $this, 'update_font_library_permissions_check' ),
'args' => array(
- 'fontFamilies' => array(
+ 'font_families' => array(
'required' => true,
'type' => 'string',
'validate_callback' => array( $this, 'validate_install_font_families' ),
@@ -147,13 +147,13 @@ private function get_validation_errors( $font_families, $files ) {
$error_messages = array();
if ( ! is_array( $font_families ) ) {
- $error_messages[] = __( 'fontFamilies should be an array of font families.', 'gutenberg' );
+ $error_messages[] = __( 'font_families should be an array of font families.', 'gutenberg' );
return $error_messages;
}
// Checks if there is at least one font family.
if ( count( $font_families ) < 1 ) {
- $error_messages[] = __( 'fontFamilies should have at least one font family definition.', 'gutenberg' );
+ $error_messages[] = __( 'font_families should have at least one font family definition.', 'gutenberg' );
return $error_messages;
}
@@ -260,7 +260,7 @@ public function validate_install_font_families( $param, $request ) {
*/
public function uninstall_schema() {
return array(
- 'fontFamilies' => array(
+ 'font_families' => array(
'type' => 'array',
'description' => __( 'The font families to install.', 'gutenberg' ),
'required' => true,
@@ -289,7 +289,7 @@ public function uninstall_schema() {
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function uninstall_fonts( $request ) {
- $fonts_to_uninstall = $request->get_param( 'fontFamilies' );
+ $fonts_to_uninstall = $request->get_param( 'font_families' );
$errors = array();
$successes = array();
@@ -397,7 +397,7 @@ private function needs_write_permission( $font_families ) {
*/
public function install_fonts( $request ) {
// Get new fonts to install.
- $fonts_param = $request->get_param( 'fontFamilies' );
+ $fonts_param = $request->get_param( 'font_families' );
/*
* As this is receiving form data, the font families are encoded as a string.
diff --git a/lib/load.php b/lib/load.php
index 77232efcfae1a9..72ec9e62a8d74e 100644
--- a/lib/load.php
+++ b/lib/load.php
@@ -244,3 +244,4 @@ function () {
require __DIR__ . '/block-supports/duotone.php';
require __DIR__ . '/block-supports/shadow.php';
require __DIR__ . '/block-supports/background.php';
+require __DIR__ . '/block-supports/behaviors.php';
diff --git a/packages/base-styles/_z-index.scss b/packages/base-styles/_z-index.scss
index 12443a30a96656..cbe495d3787cd9 100644
--- a/packages/base-styles/_z-index.scss
+++ b/packages/base-styles/_z-index.scss
@@ -128,6 +128,7 @@ $z-layers: (
".block-editor-block-rename-modal": 1000001,
".edit-site-list__rename-modal": 1000001,
".edit-site-swap-template-modal": 1000001,
+ ".edit-site-template-panel__replace-template-modal": 1000001,
// Note: The ConfirmDialog component's z-index is being set to 1000001 in packages/components/src/confirm-dialog/styles.ts
// because it uses emotion and not sass. We need it to render on top its parent popover.
diff --git a/packages/block-editor/src/components/block-styles/index.js b/packages/block-editor/src/components/block-styles/index.js
index b998614e4b0892..f598b35f890f15 100644
--- a/packages/block-editor/src/components/block-styles/index.js
+++ b/packages/block-editor/src/components/block-styles/index.js
@@ -14,7 +14,6 @@ import {
Popover,
} from '@wordpress/components';
import deprecated from '@wordpress/deprecated';
-import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
@@ -65,9 +64,7 @@ function BlockStyles( { clientId, onSwitch = noop, onHoverClassName = noop } ) {
{ stylesToRender.map( ( style ) => {
- const buttonText = style.isDefault
- ? __( 'Default' )
- : style.label || style.name;
+ const buttonText = style.label || style.name;
return (
{ () => (
<>
-
+
{
@@ -175,6 +171,22 @@ export function BlockPatternsSyncFilter( {
value={ patternSyncFilter }
/>
+
+ { createInterpolateElement(
+ __(
+ 'Patterns are available from the WordPress.org Pattern Directory, bundled in the active theme, or created by users on this site. Only patterns created on this site can be synced.'
+ ),
+ {
+ Link: (
+
+ ),
+ }
+ ) }
+
>
) }
diff --git a/packages/block-editor/src/components/inserter/block-patterns-tab.js b/packages/block-editor/src/components/inserter/block-patterns-tab.js
index 5d4b2684f1c70f..2dba8e08fa7476 100644
--- a/packages/block-editor/src/components/inserter/block-patterns-tab.js
+++ b/packages/block-editor/src/components/inserter/block-patterns-tab.js
@@ -321,9 +321,6 @@ export function BlockPatternsCategoryPanel( {
category={ category }
/>
- { category.description && (
- { category.description }
- ) }
{ ! currentCategoryPatterns.length && (
{
- const settings = select( blockEditorStore ).getSettings();
- return {
- inserterMediaCategories: settings.inserterMediaCategories,
- allowedMimeTypes: settings.allowedMimeTypes,
- enableOpenverseMediaCategory: settings.enableOpenverseMediaCategory,
- };
- }, [] );
- // The allowed `mime_types` can be altered by `upload_mimes` filter and restrict
- // some of them. In this case we shouldn't add the category to the available media
- // categories list in the inserter.
- const allowedCategories = useMemo( () => {
- if ( ! inserterMediaCategories || ! allowedMimeTypes ) {
- return;
- }
- return inserterMediaCategories.filter( ( category ) => {
- // Check if Openverse category is enabled.
- if (
- ! enableOpenverseMediaCategory &&
- category.name === 'openverse'
- ) {
- return false;
- }
- return Object.values( allowedMimeTypes ).some( ( mimeType ) =>
- mimeType.startsWith( `${ category.mediaType }/` )
- );
- } );
- }, [
- inserterMediaCategories,
- allowedMimeTypes,
- enableOpenverseMediaCategory,
- ] );
- return allowedCategories;
-}
-
export function useMediaCategories( rootClientId ) {
const [ categories, setCategories ] = useState( [] );
+
+ const inserterMediaCategories = useSelect(
+ ( select ) =>
+ unlock( select( blockEditorStore ) ).getInserterMediaCategories(),
+ []
+ );
const { canInsertImage, canInsertVideo, canInsertAudio } = useSelect(
( select ) => {
const { canInsertBlockType } = select( blockEditorStore );
@@ -112,7 +79,6 @@ export function useMediaCategories( rootClientId ) {
},
[ rootClientId ]
);
- const inserterMediaCategories = useInserterMediaCategories();
useEffect( () => {
( async () => {
const _categories = [];
diff --git a/packages/block-editor/src/components/inserter/style.scss b/packages/block-editor/src/components/inserter/style.scss
index 418990bfaf6710..d843fb81fac10c 100644
--- a/packages/block-editor/src/components/inserter/style.scss
+++ b/packages/block-editor/src/components/inserter/style.scss
@@ -314,7 +314,7 @@ $block-inserter-tabs-height: 44px;
overflow-y: auto;
flex-grow: 1;
height: 100%;
- padding: $grid-unit-40 $grid-unit-30;
+ padding: $grid-unit-20 $grid-unit-30;
}
}
diff --git a/packages/block-editor/src/components/list-view/block.js b/packages/block-editor/src/components/list-view/block.js
index 43fe4df4cb75aa..375f39a7cc3c81 100644
--- a/packages/block-editor/src/components/list-view/block.js
+++ b/packages/block-editor/src/components/list-view/block.js
@@ -13,16 +13,9 @@ import {
} from '@wordpress/components';
import { useInstanceId } from '@wordpress/compose';
import { moreVertical } from '@wordpress/icons';
-import {
- useState,
- useRef,
- useEffect,
- useCallback,
- memo,
-} from '@wordpress/element';
+import { useState, useRef, useCallback, memo } from '@wordpress/element';
import { useDispatch, useSelect } from '@wordpress/data';
import { sprintf, __ } from '@wordpress/i18n';
-import { focus } from '@wordpress/dom';
import { ESCAPE } from '@wordpress/keycodes';
/**
@@ -36,7 +29,7 @@ import {
} from '../block-mover/button';
import ListViewBlockContents from './block-contents';
import { useListViewContext } from './context';
-import { getBlockPositionDescription } from './utils';
+import { getBlockPositionDescription, focusListItem } from './utils';
import { store as blockEditorStore } from '../../store';
import useBlockDisplayInformation from '../use-block-display-information';
import { useBlockLock } from '../block-lock';
@@ -120,7 +113,6 @@ function ListViewBlock( {
);
const {
- isTreeGridMounted,
expand,
collapse,
BlockSettingsMenu,
@@ -142,15 +134,6 @@ function ListViewBlock( {
{ 'is-visible': isHovered || isFirstSelectedBlock }
);
- // If ListView has experimental features related to the Persistent List View,
- // only focus the selected list item on mount; otherwise the list would always
- // try to steal the focus from the editor canvas.
- useEffect( () => {
- if ( ! isTreeGridMounted && isSelected ) {
- cellRef.current.focus();
- }
- }, [] );
-
// If multiple blocks are selected, deselect all blocks when the user
// presses the escape key.
const onKeyDown = ( event ) => {
@@ -188,30 +171,7 @@ function ListViewBlock( {
selectBlock( undefined, focusClientId, null, null );
}
- const getFocusElement = () => {
- const row = treeGridElementRef.current?.querySelector(
- `[role=row][data-block="${ focusClientId }"]`
- );
- if ( ! row ) return null;
- // Focus the first focusable in the row, which is the ListViewBlockSelectButton.
- return focus.focusable.find( row )[ 0 ];
- };
-
- let focusElement = getFocusElement();
- if ( focusElement ) {
- focusElement.focus();
- } else {
- // The element hasn't been painted yet. Defer focusing on the next frame.
- // This could happen when all blocks have been deleted and the default block
- // hasn't been added to the editor yet.
- window.requestAnimationFrame( () => {
- focusElement = getFocusElement();
- // Ignore if the element still doesn't exist.
- if ( focusElement ) {
- focusElement.focus();
- }
- } );
- }
+ focusListItem( focusClientId, treeGridElementRef );
},
[ selectBlock, treeGridElementRef ]
);
diff --git a/packages/block-editor/src/components/list-view/branch.js b/packages/block-editor/src/components/list-view/branch.js
index d3b555c055afd1..e2e27f5f2cb5af 100644
--- a/packages/block-editor/src/components/list-view/branch.js
+++ b/packages/block-editor/src/components/list-view/branch.js
@@ -168,8 +168,18 @@ function ListViewBranch( props ) {
);
const isSelectedBranch =
isBranchSelected || ( isSelected && hasNestedBlocks );
+
+ // To avoid performance issues, we only render blocks that are in view,
+ // or blocks that are selected or dragged. If a block is selected,
+ // it is only counted if it is the first of the block selection.
+ // This prevents the entire tree from being rendered when a branch is
+ // selected, or a user selects all blocks, while still enabling scroll
+ // into view behavior when selecting a block or opening the list view.
const showBlock =
- isDragged || blockInView || isSelected || isBranchDragged;
+ isDragged ||
+ blockInView ||
+ isBranchDragged ||
+ ( isSelected && clientId === selectedClientIds[ 0 ] );
return (
{ showBlock && (
diff --git a/packages/block-editor/src/components/list-view/index.js b/packages/block-editor/src/components/list-view/index.js
index 917ebd883aa8d1..085864c4c88f45 100644
--- a/packages/block-editor/src/components/list-view/index.js
+++ b/packages/block-editor/src/components/list-view/index.js
@@ -32,6 +32,7 @@ import useListViewDropZone from './use-list-view-drop-zone';
import useListViewExpandSelectedItem from './use-list-view-expand-selected-item';
import { store as blockEditorStore } from '../../store';
import { BlockSettingsDropdown } from '../block-settings-menu/block-settings-dropdown';
+import { focusListItem } from './utils';
const expanded = ( state, action ) => {
if ( Array.isArray( action.clientIds ) ) {
@@ -132,8 +133,6 @@ function ListViewComponent(
const elementRef = useRef();
const treeGridRef = useMergeRefs( [ elementRef, dropZoneRef, ref ] );
- const isMounted = useRef( false );
-
const [ insertedBlock, setInsertedBlock ] = useState( null );
const { setSelectedTreeId } = useListViewExpandSelectedItem( {
@@ -156,7 +155,13 @@ function ListViewComponent(
[ setSelectedTreeId, updateBlockSelection, onSelect, getBlock ]
);
useEffect( () => {
- isMounted.current = true;
+ // If a blocks are already selected when the list view is initially
+ // mounted, shift focus to the first selected block.
+ if ( selectedClientIds?.length ) {
+ focusListItem( selectedClientIds[ 0 ], elementRef );
+ }
+ // Disable reason: Only focus on the selected item when the list view is mounted.
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [] );
const expand = useCallback(
@@ -204,7 +209,6 @@ function ListViewComponent(
const contextValue = useMemo(
() => ( {
- isTreeGridMounted: isMounted.current,
draggedClientIds,
expandedState,
expand,
diff --git a/packages/block-editor/src/components/list-view/utils.js b/packages/block-editor/src/components/list-view/utils.js
index f53f5a4cd4884a..632173e120691f 100644
--- a/packages/block-editor/src/components/list-view/utils.js
+++ b/packages/block-editor/src/components/list-view/utils.js
@@ -2,6 +2,7 @@
* WordPress dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
+import { focus } from '@wordpress/dom';
export const getBlockPositionDescription = ( position, siblingCount, level ) =>
sprintf(
@@ -56,3 +57,39 @@ export function getCommonDepthClientIds(
end,
};
}
+
+/**
+ * Shift focus to the list view item associated with a particular clientId.
+ *
+ * @typedef {import('@wordpress/element').RefObject} RefObject
+ *
+ * @param {string} focusClientId The client ID of the block to focus.
+ * @param {RefObject} treeGridElementRef The container element to search within.
+ */
+export function focusListItem( focusClientId, treeGridElementRef ) {
+ const getFocusElement = () => {
+ const row = treeGridElementRef.current?.querySelector(
+ `[role=row][data-block="${ focusClientId }"]`
+ );
+ if ( ! row ) return null;
+ // Focus the first focusable in the row, which is the ListViewBlockSelectButton.
+ return focus.focusable.find( row )[ 0 ];
+ };
+
+ let focusElement = getFocusElement();
+ if ( focusElement ) {
+ focusElement.focus();
+ } else {
+ // The element hasn't been painted yet. Defer focusing on the next frame.
+ // This could happen when all blocks have been deleted and the default block
+ // hasn't been added to the editor yet.
+ window.requestAnimationFrame( () => {
+ focusElement = getFocusElement();
+
+ // Ignore if the element still doesn't exist.
+ if ( focusElement ) {
+ focusElement.focus();
+ }
+ } );
+ }
+}
diff --git a/packages/block-editor/src/store/actions.js b/packages/block-editor/src/store/actions.js
index 1740543744aaa2..ae4b64a645d3ed 100644
--- a/packages/block-editor/src/store/actions.js
+++ b/packages/block-editor/src/store/actions.js
@@ -1893,9 +1893,10 @@ export const registerInserterMediaCategory =
);
return;
}
- const { inserterMediaCategories = [] } = select.getSettings();
+ const registeredInserterMediaCategories =
+ select.getRegisteredInserterMediaCategories();
if (
- inserterMediaCategories.some(
+ registeredInserterMediaCategories.some(
( { name } ) => name === category.name
)
) {
@@ -1905,8 +1906,8 @@ export const registerInserterMediaCategory =
return;
}
if (
- inserterMediaCategories.some(
- ( { labels: { name } } ) => name === category.labels?.name
+ registeredInserterMediaCategories.some(
+ ( { labels: { name } = {} } ) => name === category.labels?.name
)
) {
console.error(
@@ -1919,13 +1920,8 @@ export const registerInserterMediaCategory =
// private, so extenders can only add new inserter media categories and don't have any
// control over the core media categories.
dispatch( {
- type: 'UPDATE_SETTINGS',
- settings: {
- inserterMediaCategories: [
- ...inserterMediaCategories,
- { ...category, isExternalResource: true },
- ],
- },
+ type: 'REGISTER_INSERTER_MEDIA_CATEGORY',
+ category: { ...category, isExternalResource: true },
} );
};
diff --git a/packages/block-editor/src/store/private-selectors.js b/packages/block-editor/src/store/private-selectors.js
index 698da537728e01..c4220e6e7e516c 100644
--- a/packages/block-editor/src/store/private-selectors.js
+++ b/packages/block-editor/src/store/private-selectors.js
@@ -164,3 +164,75 @@ export function getOpenedBlockSettingsMenu( state ) {
export function getStyleOverrides( state ) {
return state.styleOverrides;
}
+
+/** @typedef {import('./actions').InserterMediaCategory} InserterMediaCategory */
+/**
+ * Returns the registered inserter media categories through the public API.
+ *
+ * @param {Object} state Editor state.
+ *
+ * @return {InserterMediaCategory[]} Inserter media categories.
+ */
+export function getRegisteredInserterMediaCategories( state ) {
+ return state.registeredInserterMediaCategories;
+}
+
+/**
+ * Returns an array containing the allowed inserter media categories.
+ * It merges the registered media categories from extenders with the
+ * core ones. It also takes into account the allowed `mime_types`, which
+ * can be altered by `upload_mimes` filter and restrict some of them.
+ *
+ * @param {Object} state Global application state.
+ *
+ * @return {InserterMediaCategory[]} Client IDs of descendants.
+ */
+export const getInserterMediaCategories = createSelector(
+ ( state ) => {
+ const {
+ settings: {
+ inserterMediaCategories,
+ allowedMimeTypes,
+ enableOpenverseMediaCategory,
+ },
+ registeredInserterMediaCategories,
+ } = state;
+ // The allowed `mime_types` can be altered by `upload_mimes` filter and restrict
+ // some of them. In this case we shouldn't add the category to the available media
+ // categories list in the inserter.
+ if (
+ ( ! inserterMediaCategories &&
+ ! registeredInserterMediaCategories.length ) ||
+ ! allowedMimeTypes
+ ) {
+ return;
+ }
+ const coreInserterMediaCategoriesNames =
+ inserterMediaCategories?.map( ( { name } ) => name ) || [];
+ const mergedCategories = [
+ ...( inserterMediaCategories || [] ),
+ ...( registeredInserterMediaCategories || [] ).filter(
+ ( { name } ) =>
+ ! coreInserterMediaCategoriesNames.includes( name )
+ ),
+ ];
+ return mergedCategories.filter( ( category ) => {
+ // Check if Openverse category is enabled.
+ if (
+ ! enableOpenverseMediaCategory &&
+ category.name === 'openverse'
+ ) {
+ return false;
+ }
+ return Object.values( allowedMimeTypes ).some( ( mimeType ) =>
+ mimeType.startsWith( `${ category.mediaType }/` )
+ );
+ } );
+ },
+ ( state ) => [
+ state.settings.inserterMediaCategories,
+ state.settings.allowedMimeTypes,
+ state.settings.enableOpenverseMediaCategory,
+ state.registeredInserterMediaCategories,
+ ]
+);
diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js
index 18048ce138eb23..4373182d986622 100644
--- a/packages/block-editor/src/store/reducer.js
+++ b/packages/block-editor/src/store/reducer.js
@@ -1949,6 +1949,22 @@ export function styleOverrides( state = new Map(), action ) {
return state;
}
+/**
+ * Reducer returning a map of the registered inserter media categories.
+ *
+ * @param {Array} state Current state.
+ * @param {Object} action Dispatched action.
+ *
+ * @return {Array} Updated state.
+ */
+export function registeredInserterMediaCategories( state = [], action ) {
+ switch ( action.type ) {
+ case 'REGISTER_INSERTER_MEDIA_CATEGORY':
+ return [ ...state, action.category ];
+ }
+ return state;
+}
+
const combinedReducers = combineReducers( {
blocks,
isTyping,
@@ -1976,6 +1992,7 @@ const combinedReducers = combineReducers( {
removalPromptData,
blockRemovalRules,
openedBlockSettingsMenu,
+ registeredInserterMediaCategories,
} );
function withAutomaticChangeReset( reducer ) {
diff --git a/packages/block-editor/src/store/test/actions.js b/packages/block-editor/src/store/test/actions.js
index f1f8cb29f1406e..e65921e30a6ce7 100644
--- a/packages/block-editor/src/store/test/actions.js
+++ b/packages/block-editor/src/store/test/actions.js
@@ -1279,9 +1279,9 @@ describe( 'actions', () => {
fetch: () => {},
} )( {
select: {
- getSettings: () => ( {
- inserterMediaCategories: [ { name: 'a' } ],
- } ),
+ getRegisteredInserterMediaCategories: () => [
+ { name: 'a' },
+ ],
},
} );
expect( console ).toHaveErroredWith(
@@ -1296,11 +1296,9 @@ describe( 'actions', () => {
fetch: () => {},
} )( {
select: {
- getSettings: () => ( {
- inserterMediaCategories: [
- { labels: { name: 'a' } },
- ],
- } ),
+ getRegisteredInserterMediaCategories: () => [
+ { labels: { name: 'a' } },
+ ],
},
} );
expect( console ).toHaveErroredWith(
@@ -1321,18 +1319,14 @@ describe( 'actions', () => {
const dispatch = jest.fn();
registerInserterMediaCategory( category )( {
select: {
- getSettings: () => ( { inserterMediaCategories } ),
+ getRegisteredInserterMediaCategories: () =>
+ inserterMediaCategories,
},
dispatch,
} );
expect( dispatch ).toHaveBeenLastCalledWith( {
- type: 'UPDATE_SETTINGS',
- settings: {
- inserterMediaCategories: [
- ...inserterMediaCategories,
- { ...category, isExternalResource: true },
- ],
- },
+ type: 'REGISTER_INSERTER_MEDIA_CATEGORY',
+ category: { ...category, isExternalResource: true },
} );
} );
} );
diff --git a/packages/block-editor/src/utils/transform-styles/transforms/test/__snapshots__/wrap.js.snap b/packages/block-editor/src/utils/transform-styles/transforms/test/__snapshots__/wrap.js.snap
index b55f74cfd7bb0b..b9815cdc700b38 100644
--- a/packages/block-editor/src/utils/transform-styles/transforms/test/__snapshots__/wrap.js.snap
+++ b/packages/block-editor/src/utils/transform-styles/transforms/test/__snapshots__/wrap.js.snap
@@ -22,6 +22,19 @@ color: red;
}"
`;
+exports[`CSS selector wrap should not double wrap selectors 1`] = `
+".my-namespace h1,
+.my-namespace .red {
+color: red;
+}"
+`;
+
+exports[`CSS selector wrap should replace :root selectors 1`] = `
+".my-namespace {
+--my-color: #ff0000;
+}"
+`;
+
exports[`CSS selector wrap should replace root tags 1`] = `
".my-namespace,
.my-namespace h1 {
@@ -49,9 +62,3 @@ color: red;
}
}"
`;
-
-exports[`CSS selector wrap should replace :root selectors 1`] = `
-".my-namespace {
---my-color: #ff0000;
-}"
-`;
diff --git a/packages/block-editor/src/utils/transform-styles/transforms/test/wrap.js b/packages/block-editor/src/utils/transform-styles/transforms/test/wrap.js
index c26bd3761212b1..a1f4f141d21c9b 100644
--- a/packages/block-editor/src/utils/transform-styles/transforms/test/wrap.js
+++ b/packages/block-editor/src/utils/transform-styles/transforms/test/wrap.js
@@ -83,4 +83,13 @@ describe( 'CSS selector wrap', () => {
expect( output ).toMatchSnapshot();
} );
+
+ it( 'should not double wrap selectors', () => {
+ const callback = wrap( '.my-namespace' );
+ const input = ` .my-namespace h1, .red { color: red; }`;
+
+ const output = traverse( input, callback );
+
+ expect( output ).toMatchSnapshot();
+ } );
} );
diff --git a/packages/block-editor/src/utils/transform-styles/transforms/wrap.js b/packages/block-editor/src/utils/transform-styles/transforms/wrap.js
index e61c78dc7e452f..74b940f80352b9 100644
--- a/packages/block-editor/src/utils/transform-styles/transforms/wrap.js
+++ b/packages/block-editor/src/utils/transform-styles/transforms/wrap.js
@@ -27,6 +27,11 @@ const wrap =
return selector;
}
+ // Skip the update when a selector already has a namespace + space (" ").
+ if ( selector.trim().startsWith( `${ namespace } ` ) ) {
+ return selector;
+ }
+
// Anything other than a root tag is always prefixed.
{
if ( ! selector.match( IS_ROOT_TAG ) ) {
diff --git a/packages/block-library/CHANGELOG.md b/packages/block-library/CHANGELOG.md
index 159cae031675bf..5598b49bcad76c 100644
--- a/packages/block-library/CHANGELOG.md
+++ b/packages/block-library/CHANGELOG.md
@@ -2,6 +2,13 @@
## Unreleased
+### Bug Fix
+
+- Fix Image block lightbox missing alt attribute and improve accessibility. ([#54608](https://github.com/WordPress/gutenberg/pull/55010))
+
+
+## 8.20.0 (2023-10-05)
+
## 8.19.0 (2023-09-20)
## 8.18.0 (2023-08-31)
diff --git a/packages/block-library/src/cover/edit/inspector-controls.js b/packages/block-library/src/cover/edit/inspector-controls.js
index 02058243f9f781..ed2501a0d0ec52 100644
--- a/packages/block-library/src/cover/edit/inspector-controls.js
+++ b/packages/block-library/src/cover/edit/inspector-controls.js
@@ -149,7 +149,7 @@ export default function CoverInspectorControls( {
'The element should represent introductory content, typically a group of introductory or navigational aids.'
),
main: __(
- 'The element should be used for the primary content of your document only. '
+ 'The element should be used for the primary content of your document only.'
),
section: __(
"The element should represent a standalone portion of the document that can't be better represented by another element."
diff --git a/packages/block-library/src/embed/edit.js b/packages/block-library/src/embed/edit.js
index 28902020c75e8e..2945fb0fbe888b 100644
--- a/packages/block-library/src/embed/edit.js
+++ b/packages/block-library/src/embed/edit.js
@@ -29,6 +29,7 @@ import { useDispatch, useSelect } from '@wordpress/data';
import { useBlockProps } from '@wordpress/block-editor';
import { store as coreStore } from '@wordpress/core-data';
import { View } from '@wordpress/primitives';
+import { getAuthority } from '@wordpress/url';
const EmbedEdit = ( props ) => {
const {
@@ -137,6 +138,20 @@ const EmbedEdit = ( props ) => {
setAttributes( { url: newURL } );
}, [ preview?.html, attributesUrl, cannotEmbed, fetching ] );
+ // Try a different provider in case the embed url is not supported.
+ useEffect( () => {
+ if ( ! cannotEmbed || fetching || ! url ) {
+ return;
+ }
+
+ // Until X provider is supported in WordPress, as a workaround we use Twitter provider.
+ if ( getAuthority( url ) === 'x.com' ) {
+ const newURL = new URL( url );
+ newURL.host = 'twitter.com';
+ setAttributes( { url: newURL.toString() } );
+ }
+ }, [ url, cannotEmbed, fetching, setAttributes ] );
+
// Handle incoming preview.
useEffect( () => {
if ( preview && ! isEditingURL ) {
diff --git a/packages/block-library/src/embed/edit.native.js b/packages/block-library/src/embed/edit.native.js
index eec991c7b2037b..a04e49fbd6d54e 100644
--- a/packages/block-library/src/embed/edit.native.js
+++ b/packages/block-library/src/embed/edit.native.js
@@ -33,6 +33,7 @@ import {
} from '@wordpress/block-editor';
import { store as coreStore } from '@wordpress/core-data';
import { View } from '@wordpress/primitives';
+import { getAuthority } from '@wordpress/url';
// The inline preview feature will be released progressible, for this reason
// the embed will only be considered previewable for the following providers list.
@@ -160,6 +161,20 @@ const EmbedEdit = ( props ) => {
setAttributes( { url: newURL } );
}, [ preview?.html, url, cannotEmbed, fetching ] );
+ // Try a different provider in case the embed url is not supported.
+ useEffect( () => {
+ if ( ! cannotEmbed || fetching || ! url ) {
+ return;
+ }
+
+ // Until X provider is supported in WordPress, as a workaround we use Twitter provider.
+ if ( getAuthority( url ) === 'x.com' ) {
+ const newURL = new URL( url );
+ newURL.host = 'twitter.com';
+ setAttributes( { url: newURL.toString() } );
+ }
+ }, [ url, cannotEmbed, fetching, setAttributes ] );
+
// Handle incoming preview.
useEffect( () => {
if ( preview && ! isEditingURL ) {
diff --git a/packages/block-library/src/image/image.js b/packages/block-library/src/image/image.js
index c207e0418aeaa1..d49a8f7cd05783 100644
--- a/packages/block-library/src/image/image.js
+++ b/packages/block-library/src/image/image.js
@@ -375,7 +375,7 @@ export default function Image( {
!! lightbox || lightboxSetting?.allowEditing === true;
const lightboxChecked =
- lightbox?.enabled || ( ! lightbox && lightboxSetting?.enabled );
+ !! lightbox?.enabled || ( ! lightbox && !! lightboxSetting?.enabled );
const dimensionsControl = (
next_tag( 'img' );
$alt_attribute = $processor->get_attribute( 'alt' );
- if ( null !== $alt_attribute ) {
+ // An empty alt attribute `alt=""` is valid for decorative images.
+ if ( is_string( $alt_attribute ) ) {
$alt_attribute = trim( $alt_attribute );
}
+ // It only makes sense to append the alt text to the button aria-label when the alt text is non-empty.
if ( $alt_attribute ) {
/* translators: %s: Image alt text. */
$aria_label = sprintf( __( 'Enlarge image: %s' ), $alt_attribute );
}
- $content = $processor->get_updated_html();
// Currently, we are only enabling the zoom animation.
$lightbox_animation = 'zoom';
- // We want to store the src in the context so we can set it dynamically when the lightbox is opened.
- $z = new WP_HTML_Tag_Processor( $content );
- $z->next_tag( 'img' );
-
+ // Note: We want to store the `src` in the context so we
+ // can set it dynamically when the lightbox is opened.
if ( isset( $block['attrs']['id'] ) ) {
$img_uploaded_src = wp_get_attachment_url( $block['attrs']['id'] );
$img_metadata = wp_get_attachment_metadata( $block['attrs']['id'] );
$img_width = $img_metadata['width'];
$img_height = $img_metadata['height'];
} else {
- $img_uploaded_src = $z->get_attribute( 'src' );
+ $img_uploaded_src = $processor->get_attribute( 'src' );
$img_width = 'none';
$img_height = 'none';
}
@@ -160,7 +163,7 @@ function block_core_image_render_lightbox( $block_content, $block ) {
$scale_attr = false;
}
- $w = new WP_HTML_Tag_Processor( $content );
+ $w = new WP_HTML_Tag_Processor( $block_content );
$w->next_tag( 'figure' );
$w->add_class( 'wp-lightbox-container' );
$w->set_attribute( 'data-wp-interactive', true );
@@ -180,7 +183,8 @@ function block_core_image_render_lightbox( $block_content, $block ) {
"imageCurrentSrc": "",
"targetWidth": "%s",
"targetHeight": "%s",
- "scaleAttr": "%s"
+ "scaleAttr": "%s",
+ "dialogLabel": "%s"
}
}
}',
@@ -188,7 +192,8 @@ function block_core_image_render_lightbox( $block_content, $block ) {
$img_uploaded_src,
$img_width,
$img_height,
- $scale_attr
+ $scale_attr,
+ __( 'Enlarged image' )
)
);
$w->next_tag( 'img' );
@@ -200,19 +205,20 @@ function block_core_image_render_lightbox( $block_content, $block ) {
// Wrap the image in the body content with a button.
$img = null;
preg_match( '/ ]+>/', $body_content, $img );
- $button =
- '
- '
- . $img[0];
+
+ $button =
+ $img[0]
+ . ' ';
+
$body_content = preg_replace( '/ ]+>/', $button, $body_content );
// We need both a responsive image and an enlarged image to animate
@@ -220,7 +226,7 @@ function block_core_image_render_lightbox( $block_content, $block ) {
// image is a copy of the one in the body, which animates immediately
// as the lightbox is opened, while the enlarged one is a full-sized
// version that will likely still be loading as the animation begins.
- $m = new WP_HTML_Tag_Processor( $content );
+ $m = new WP_HTML_Tag_Processor( $block_content );
$m->next_tag( 'figure' );
$m->add_class( 'responsive-image' );
$m->next_tag( 'img' );
@@ -236,7 +242,7 @@ function block_core_image_render_lightbox( $block_content, $block ) {
$m->set_attribute( 'data-wp-style--object-fit', 'selectors.core.image.lightboxObjectFit' );
$initial_image_content = $m->get_updated_html();
- $q = new WP_HTML_Tag_Processor( $content );
+ $q = new WP_HTML_Tag_Processor( $block_content );
$q->next_tag( 'figure' );
$q->add_class( 'enlarged-image' );
$q->next_tag( 'img' );
@@ -252,24 +258,32 @@ function block_core_image_render_lightbox( $block_content, $block ) {
$q->set_attribute( 'data-wp-style--object-fit', 'selectors.core.image.lightboxObjectFit' );
$enlarged_image_content = $q->get_updated_html();
- $background_color = esc_attr( wp_get_global_styles( array( 'color', 'background' ) ) );
+ // If the current theme does NOT have a `theme.json`, or the colors are not defined,
+ // we need to set the background color & close button color to some default values
+ // because we can't get them from the Global Styles.
+ $background_color = '#fff';
+ $close_button_color = '#000';
+ if ( wp_theme_has_theme_json() ) {
+ $global_styles_color = wp_get_global_styles( array( 'color' ) );
+ if ( ! empty( $global_styles_color['background'] ) ) {
+ $background_color = esc_attr( $global_styles_color['background'] );
+ }
+ if ( ! empty( $global_styles_color['text'] ) ) {
+ $close_button_color = esc_attr( $global_styles_color['text'] );
+ }
+ }
$close_button_icon = ' ';
- $close_button_color = esc_attr( wp_get_global_styles( array( 'color', 'text' ) ) );
- $dialog_label = $alt_attribute ? esc_attr( $alt_attribute ) : esc_attr__( 'Image' );
$close_button_label = esc_attr__( 'Close' );
$lightbox_html = <<
$initial_image_content
$enlarged_image_content
-
+
HTML;
@@ -290,11 +304,13 @@ function block_core_image_render_lightbox( $block_content, $block ) {
}
/**
- * Ensure that the view script has the `wp-interactivity` dependency.
+ * Ensures that the view script has the `wp-interactivity` dependency.
*
* @since 6.4.0
*
* @global WP_Scripts $wp_scripts
+ *
+ * @return void
*/
function block_core_image_ensure_interactivity_dependency() {
global $wp_scripts;
@@ -310,6 +326,8 @@ function block_core_image_ensure_interactivity_dependency() {
/**
* Registers the `core/image` block on server.
+ *
+ * @return void
*/
function register_block_core_image() {
register_block_type_from_metadata(
diff --git a/packages/block-library/src/image/style.scss b/packages/block-library/src/image/style.scss
index 5c3552fd80c2ee..2ef602982e57b5 100644
--- a/packages/block-library/src/image/style.scss
+++ b/packages/block-library/src/image/style.scss
@@ -154,6 +154,8 @@
.wp-lightbox-container {
position: relative;
+ display: flex;
+ flex-direction: column;
button {
border: none;
@@ -169,6 +171,13 @@
outline: 5px auto -webkit-focus-ring-color;
outline-offset: 5px;
}
+
+ &:hover,
+ &:focus,
+ &:not(:hover):not(:active):not(.has-background) {
+ background: none;
+ border: none;
+ }
}
}
@@ -186,11 +195,23 @@
.close-button {
position: absolute;
- top: calc(env(safe-area-inset-top) + 20px);
- right: calc(env(safe-area-inset-right) + 20px);
+ top: calc(env(safe-area-inset-top) + 16px); // equivalent to $grid-unit-20
+ right: calc(env(safe-area-inset-right) + 16px); // equivalent to $grid-unit-20
padding: 0;
cursor: pointer;
z-index: 5000000;
+ min-width: 40px; // equivalent to $button-size-next-default-40px
+ min-height: 40px; // equivalent to $button-size-next-default-40px
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ &:hover,
+ &:focus,
+ &:not(:hover):not(:active):not(.has-background) {
+ background: none;
+ border: none;
+ }
}
.lightbox-image-container {
diff --git a/packages/block-library/src/image/view.js b/packages/block-library/src/image/view.js
index 13f20c9cd7cb68..3eb47dcc7cab4b 100644
--- a/packages/block-library/src/image/view.js
+++ b/packages/block-library/src/image/view.js
@@ -227,7 +227,17 @@ store(
roleAttribute: ( { context } ) => {
return context.core.image.lightboxEnabled
? 'dialog'
- : '';
+ : null;
+ },
+ ariaModal: ( { context } ) => {
+ return context.core.image.lightboxEnabled
+ ? 'true'
+ : null;
+ },
+ dialogLabel: ( { context } ) => {
+ return context.core.image.lightboxEnabled
+ ? context.core.image.dialogLabel
+ : null;
},
lightboxObjectFit: ( { context } ) => {
if ( context.core.image.initialized ) {
@@ -237,7 +247,7 @@ store(
enlargedImgSrc: ( { context } ) => {
return context.core.image.initialized
? context.core.image.imageUploadedSrc
- : '';
+ : 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=';
},
},
},
@@ -360,9 +370,9 @@ function setStyles( context, event ) {
naturalHeight,
offsetWidth: originalWidth,
offsetHeight: originalHeight,
- } = event.target.nextElementSibling;
+ } = event.target.previousElementSibling;
let { x: screenPosX, y: screenPosY } =
- event.target.nextElementSibling.getBoundingClientRect();
+ event.target.previousElementSibling.getBoundingClientRect();
// Natural ratio of the image clicked to open the lightbox.
const naturalRatio = naturalWidth / naturalHeight;
diff --git a/packages/block-library/src/latest-posts/edit.js b/packages/block-library/src/latest-posts/edit.js
index 7aaf1b3ecf0eda..586ecc59432730 100644
--- a/packages/block-library/src/latest-posts/edit.js
+++ b/packages/block-library/src/latest-posts/edit.js
@@ -483,12 +483,17 @@ export default function LatestPostsEdit( { attributes, setAttributes } ) {
.split( ' ', excerptLength )
.join( ' ' ) }
{ createInterpolateElement(
- /* translators: excerpt truncation character, default … */
- __( ' …
Read more ' ),
+ sprintf(
+ /* translators: 1: The static string "Read more", 2: The post title only visible to screen readers. */
+ __( '…
%1$s: %2$s ' ),
+ __( 'Read more' ),
+ titleTrimmed || __( '(no title)' )
+ ),
{
a: (
// eslint-disable-next-line jsx-a11y/anchor-has-content
),
+ span: (
+
+ ),
}
) }
>
diff --git a/packages/block-library/src/latest-posts/index.php b/packages/block-library/src/latest-posts/index.php
index 356ba5032689f7..d5f759c0c0e259 100644
--- a/packages/block-library/src/latest-posts/index.php
+++ b/packages/block-library/src/latest-posts/index.php
@@ -48,14 +48,6 @@ function render_block_core_latest_posts( $attributes ) {
$block_core_latest_posts_excerpt_length = $attributes['excerptLength'];
add_filter( 'excerpt_length', 'block_core_latest_posts_get_excerpt_length', 20 );
- $filter_latest_posts_excerpt_more = static function ( $more ) use ( $attributes ) {
- $use_excerpt = 'excerpt' === $attributes['displayPostContentRadio'];
- /* translators: %1$s is a URL to a post, excerpt truncation character, default … */
- return $use_excerpt ? sprintf( __( ' …
Read more ' ), esc_url( get_permalink() ) ) : $more;
- };
-
- add_filter( 'excerpt_more', $filter_latest_posts_excerpt_more );
-
if ( ! empty( $attributes['categories'] ) ) {
$args['category__in'] = array_column( $attributes['categories'], 'id' );
}
@@ -151,6 +143,24 @@ function render_block_core_latest_posts( $attributes ) {
$trimmed_excerpt = get_the_excerpt( $post );
+ /*
+ * Adds a "Read more" link with screen reader text.
+ * […] is the default excerpt ending from wp_trim_excerpt() in Core.
+ */
+ if ( str_ends_with( $trimmed_excerpt, ' […]' ) ) {
+ $excerpt_length = (int) apply_filters( 'excerpt_length', $block_core_latest_posts_excerpt_length );
+ if ( $excerpt_length <= $block_core_latest_posts_excerpt_length ) {
+ $trimmed_excerpt = substr( $trimmed_excerpt, 0, -11 );
+ $trimmed_excerpt .= sprintf(
+ /* translators: 1: A URL to a post, 2: The static string "Read more", 3: The post title only visible to screen readers. */
+ __( '…
%2$s: %3$s ' ),
+ esc_url( $post_link ),
+ __( 'Read more' ),
+ esc_html( $title )
+ );
+ }
+ }
+
if ( post_password_required( $post ) ) {
$trimmed_excerpt = __( 'This content is password protected.' );
}
diff --git a/packages/block-library/src/media-text/edit.js b/packages/block-library/src/media-text/edit.js
index 30181c9044c34d..f3baeb8c1756ba 100644
--- a/packages/block-library/src/media-text/edit.js
+++ b/packages/block-library/src/media-text/edit.js
@@ -244,7 +244,7 @@ function MediaTextEdit( { attributes, isSelected, setAttributes } ) {
setAttributes( {
imageFill: ! imageFill,
diff --git a/packages/block-library/src/navigation/edit/deleted-navigation-warning.js b/packages/block-library/src/navigation/edit/deleted-navigation-warning.js
index c787b90b76682f..6386cee71431e0 100644
--- a/packages/block-library/src/navigation/edit/deleted-navigation-warning.js
+++ b/packages/block-library/src/navigation/edit/deleted-navigation-warning.js
@@ -4,14 +4,19 @@
import { Warning } from '@wordpress/block-editor';
import { Button } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
+import { createInterpolateElement } from '@wordpress/element';
function DeletedNavigationWarning( { onCreateNew } ) {
return (
- { __( 'Navigation menu has been deleted or is unavailable. ' ) }
-
- { __( 'Create a new menu?' ) }
-
+ { createInterpolateElement(
+ __(
+ 'Navigation menu has been deleted or is unavailable. Create a new menu? '
+ ),
+ {
+ button: ,
+ }
+ ) }
);
}
diff --git a/packages/block-library/src/navigation/edit/index.js b/packages/block-library/src/navigation/edit/index.js
index 483e8abaab24f1..7c29f18d4940d4 100644
--- a/packages/block-library/src/navigation/edit/index.js
+++ b/packages/block-library/src/navigation/edit/index.js
@@ -93,6 +93,7 @@ function Navigation( {
// navigation block settings.
hasSubmenuIndicatorSetting = true,
customPlaceholder: CustomPlaceholder = null,
+ __unstableLayoutClassNames: layoutClassNames,
} ) {
const {
openSubmenusOnClick,
@@ -293,23 +294,31 @@ function Navigation( {
const isResponsive = 'never' !== overlayMenu;
const blockProps = useBlockProps( {
ref: navRef,
- className: classnames( className, {
- 'items-justified-right': justifyContent === 'right',
- 'items-justified-space-between': justifyContent === 'space-between',
- 'items-justified-left': justifyContent === 'left',
- 'items-justified-center': justifyContent === 'center',
- 'is-vertical': orientation === 'vertical',
- 'no-wrap': flexWrap === 'nowrap',
- 'is-responsive': isResponsive,
- 'has-text-color': !! textColor.color || !! textColor?.class,
- [ getColorClassName( 'color', textColor?.slug ) ]:
- !! textColor?.slug,
- 'has-background': !! backgroundColor.color || backgroundColor.class,
- [ getColorClassName( 'background-color', backgroundColor?.slug ) ]:
- !! backgroundColor?.slug,
- [ `has-text-decoration-${ textDecoration }` ]: textDecoration,
- 'block-editor-block-content-overlay': hasBlockOverlay,
- } ),
+ className: classnames(
+ className,
+ {
+ 'items-justified-right': justifyContent === 'right',
+ 'items-justified-space-between':
+ justifyContent === 'space-between',
+ 'items-justified-left': justifyContent === 'left',
+ 'items-justified-center': justifyContent === 'center',
+ 'is-vertical': orientation === 'vertical',
+ 'no-wrap': flexWrap === 'nowrap',
+ 'is-responsive': isResponsive,
+ 'has-text-color': !! textColor.color || !! textColor?.class,
+ [ getColorClassName( 'color', textColor?.slug ) ]:
+ !! textColor?.slug,
+ 'has-background':
+ !! backgroundColor.color || backgroundColor.class,
+ [ getColorClassName(
+ 'background-color',
+ backgroundColor?.slug
+ ) ]: !! backgroundColor?.slug,
+ [ `has-text-decoration-${ textDecoration }` ]: textDecoration,
+ 'block-editor-block-content-overlay': hasBlockOverlay,
+ },
+ layoutClassNames
+ ),
style: {
color: ! textColor?.slug && textColor?.color,
backgroundColor: ! backgroundColor?.slug && backgroundColor?.color,
diff --git a/packages/block-library/src/navigation/edit/inner-blocks.js b/packages/block-library/src/navigation/edit/inner-blocks.js
index 812b37ea71a641..19258213f26e5f 100644
--- a/packages/block-library/src/navigation/edit/inner-blocks.js
+++ b/packages/block-library/src/navigation/edit/inner-blocks.js
@@ -118,6 +118,7 @@ export default function NavigationInnerBlocks( {
: false,
placeholder: showPlaceholder ? placeholder : undefined,
__experimentalCaptureToolbars: true,
+ __unstableDisableLayoutClassNames: true,
}
);
diff --git a/packages/block-library/src/post-featured-image/style.scss b/packages/block-library/src/post-featured-image/style.scss
index 4821e634b60327..e740b8c56e608c 100644
--- a/packages/block-library/src/post-featured-image/style.scss
+++ b/packages/block-library/src/post-featured-image/style.scss
@@ -39,4 +39,8 @@
}
}
}
+
+ &:where(.alignleft, .alignright) {
+ width: 100%;
+ }
}
diff --git a/packages/block-library/src/post-template/style.scss b/packages/block-library/src/post-template/style.scss
index 00305a17123369..4af30e30b23098 100644
--- a/packages/block-library/src/post-template/style.scss
+++ b/packages/block-library/src/post-template/style.scss
@@ -37,3 +37,23 @@
grid-template-columns: 1fr;
}
}
+
+.wp-block-post-template-is-layout-constrained > li > .alignright,
+.wp-block-post-template-is-layout-flow > li > .alignright {
+ float: right;
+ margin-inline-start: 2em;
+ margin-inline-end: 0;
+}
+
+.wp-block-post-template-is-layout-constrained > li > .alignleft,
+.wp-block-post-template-is-layout-flow > li > .alignleft {
+ float: left;
+ margin-inline-start: 0;
+ margin-inline-end: 2em;
+}
+
+.wp-block-post-template-is-layout-constrained > li > .aligncenter,
+.wp-block-post-template-is-layout-flow > li > .aligncenter {
+ margin-inline-start: auto;
+ margin-inline-end: auto;
+}
diff --git a/packages/edit-site/src/components/add-new-pattern/index.js b/packages/edit-site/src/components/add-new-pattern/index.js
index 1ee32615435b49..014ac6165aaef2 100644
--- a/packages/edit-site/src/components/add-new-pattern/index.js
+++ b/packages/edit-site/src/components/add-new-pattern/index.js
@@ -4,7 +4,7 @@
import { DropdownMenu } from '@wordpress/components';
import { useState, useRef } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
-import { plus, symbol, symbolFilled } from '@wordpress/icons';
+import { plus, symbol, symbolFilled, upload } from '@wordpress/icons';
import { useSelect, useDispatch } from '@wordpress/data';
import { privateApis as routerPrivateApis } from '@wordpress/router';
import {
@@ -90,7 +90,7 @@ export default function AddNewPattern() {
}
controls.push( {
- icon: symbol,
+ icon: upload,
onClick: () => {
patternUploadInputRef.current.click();
},
diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/resolvers.js b/packages/edit-site/src/components/global-styles/font-library-modal/resolvers.js
index 8f4fd2f7c22bda..425b3afb0e7c3c 100644
--- a/packages/edit-site/src/components/global-styles/font-library-modal/resolvers.js
+++ b/packages/edit-site/src/components/global-styles/font-library-modal/resolvers.js
@@ -18,7 +18,7 @@ export async function fetchInstallFonts( data ) {
export async function fetchUninstallFonts( fonts ) {
const data = {
- fontFamilies: fonts,
+ font_families: fonts,
};
const config = {
path: '/wp/v2/fonts',
diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js b/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js
index 7afa0f7aee17a5..d0a57978bcce94 100644
--- a/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js
+++ b/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js
@@ -152,6 +152,6 @@ export function makeFormDataFromFontFamilies( fontFamilies ) {
}
return family;
} );
- formData.append( 'fontFamilies', JSON.stringify( newFontFamilies ) );
+ formData.append( 'font_families', JSON.stringify( newFontFamilies ) );
return formData;
}
diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/makeFormDataFromFontFamilies.spec.js b/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/makeFormDataFromFontFamilies.spec.js
index 9db0195f30072e..4adae7889cc5e5 100644
--- a/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/makeFormDataFromFontFamilies.spec.js
+++ b/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/makeFormDataFromFontFamilies.spec.js
@@ -55,7 +55,7 @@ describe( 'makeFormDataFromFontFamilies', () => {
fontFamily: 'Bebas',
},
];
- expect( JSON.parse( formData.get( 'fontFamilies' ) ) ).toEqual(
+ expect( JSON.parse( formData.get( 'font_families' ) ) ).toEqual(
expectedFontFamilies
);
} );
diff --git a/packages/edit-site/src/components/sidebar-edit-mode/template-panel/hooks.js b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/hooks.js
new file mode 100644
index 00000000000000..b5e5988491396a
--- /dev/null
+++ b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/hooks.js
@@ -0,0 +1,97 @@
+/**
+ * WordPress dependencies
+ */
+import { useSelect } from '@wordpress/data';
+import { useMemo } from '@wordpress/element';
+import { store as coreStore } from '@wordpress/core-data';
+import { parse } from '@wordpress/blocks';
+
+/**
+ * Internal dependencies
+ */
+import { store as editSiteStore } from '../../../store';
+import { PATTERN_CORE_SOURCES, PATTERN_TYPES } from '../../../utils/constants';
+import { unlock } from '../../../lock-unlock';
+
+function injectThemeAttributeInBlockTemplateContent(
+ block,
+ currentThemeStylesheet
+) {
+ block.innerBlocks = block.innerBlocks.map( ( innerBlock ) => {
+ return injectThemeAttributeInBlockTemplateContent(
+ innerBlock,
+ currentThemeStylesheet
+ );
+ } );
+
+ if (
+ block.name === 'core/template-part' &&
+ block.attributes.theme === undefined
+ ) {
+ block.attributes.theme = currentThemeStylesheet;
+ }
+ return block;
+}
+
+function preparePatterns( patterns, template, currentThemeStylesheet ) {
+ // Filter out duplicates.
+ const filterOutDuplicatesByName = ( currentItem, index, items ) =>
+ index === items.findIndex( ( item ) => currentItem.name === item.name );
+
+ // Filter out core patterns.
+ const filterOutCorePatterns = ( pattern ) =>
+ ! PATTERN_CORE_SOURCES.includes( pattern.source );
+
+ // Filter only the patterns that are compatible with the current template.
+ const filterCompatiblePatterns = ( pattern ) =>
+ pattern.templateTypes?.includes( template.slug );
+
+ return patterns
+ .filter(
+ filterOutCorePatterns &&
+ filterOutDuplicatesByName &&
+ filterCompatiblePatterns
+ )
+ .map( ( pattern ) => ( {
+ ...pattern,
+ keywords: pattern.keywords || [],
+ type: PATTERN_TYPES.theme,
+ blocks: parse( pattern.content, {
+ __unstableSkipMigrationLogs: true,
+ } ).map( ( block ) =>
+ injectThemeAttributeInBlockTemplateContent(
+ block,
+ currentThemeStylesheet
+ )
+ ),
+ } ) );
+}
+
+export function useAvailablePatterns( template ) {
+ const { blockPatterns, restBlockPatterns, currentThemeStylesheet } =
+ useSelect( ( select ) => {
+ const { getSettings } = unlock( select( editSiteStore ) );
+ const settings = getSettings();
+
+ return {
+ blockPatterns:
+ settings.__experimentalAdditionalBlockPatterns ??
+ settings.__experimentalBlockPatterns,
+ restBlockPatterns: select( coreStore ).getBlockPatterns(),
+ currentThemeStylesheet:
+ select( coreStore ).getCurrentTheme().stylesheet,
+ };
+ }, [] );
+
+ return useMemo( () => {
+ const mergedPatterns = [
+ ...( blockPatterns || [] ),
+ ...( restBlockPatterns || [] ),
+ ];
+ return preparePatterns(
+ mergedPatterns,
+ template,
+ currentThemeStylesheet
+ );
+ }, [ blockPatterns, restBlockPatterns, template, currentThemeStylesheet ] );
+}
diff --git a/packages/edit-site/src/components/sidebar-edit-mode/template-panel/replace-template-button.js b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/replace-template-button.js
new file mode 100644
index 00000000000000..658aacd331debc
--- /dev/null
+++ b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/replace-template-button.js
@@ -0,0 +1,89 @@
+/**
+ * WordPress dependencies
+ */
+import { useSelect, useDispatch } from '@wordpress/data';
+import { useState } from '@wordpress/element';
+import { __experimentalBlockPatternsList as BlockPatternsList } from '@wordpress/block-editor';
+import { MenuItem, Modal } from '@wordpress/components';
+import { __ } from '@wordpress/i18n';
+import { store as coreStore } from '@wordpress/core-data';
+import { useAsyncList } from '@wordpress/compose';
+import { serialize } from '@wordpress/blocks';
+
+/**
+ * Internal dependencies
+ */
+import { store as editSiteStore } from '../../../store';
+
+export default function ReplaceTemplateButton( {
+ onClick,
+ availableTemplates,
+} ) {
+ const { editEntityRecord } = useDispatch( coreStore );
+ const [ showModal, setShowModal ] = useState( false );
+ const onClose = () => {
+ setShowModal( false );
+ };
+
+ const { postId, postType } = useSelect( ( select ) => {
+ return {
+ postId: select( editSiteStore ).getEditedPostId(),
+ postType: select( editSiteStore ).getEditedPostType(),
+ };
+ }, [] );
+
+ const onTemplateSelect = async ( selectedTemplate ) => {
+ onClose(); // Close the template suggestions modal first.
+ onClick();
+ await editEntityRecord( 'postType', postType, postId, {
+ blocks: selectedTemplate.blocks,
+ content: serialize( selectedTemplate.blocks ),
+ } );
+ };
+
+ if ( ! availableTemplates.length || availableTemplates.length < 1 ) {
+ return null;
+ }
+
+ return (
+ <>
+ setShowModal( true ) }
+ >
+ { __( 'Replace template' ) }
+
+
+ { showModal && (
+
+
+
+
+
+ ) }
+ >
+ );
+}
+
+function TemplatesList( { availableTemplates, onSelect } ) {
+ const shownTemplates = useAsyncList( availableTemplates );
+
+ return (
+
+ );
+}
diff --git a/packages/edit-site/src/components/sidebar-edit-mode/template-panel/style.scss b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/style.scss
index 4c8ef94855dcb1..6eab753e8ad285 100644
--- a/packages/edit-site/src/components/sidebar-edit-mode/template-panel/style.scss
+++ b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/style.scss
@@ -37,3 +37,21 @@ h3.edit-site-template-card__template-areas-title {
font-weight: 500;
margin: 0 0 $grid-unit-10;
}
+
+
+.edit-site-template-panel__replace-template-modal {
+ z-index: z-index(".edit-site-template-panel__replace-template-modal");
+}
+
+.edit-site-template-panel__replace-template-modal__content {
+ column-count: 2;
+ column-gap: $grid-unit-30;
+
+ @include break-medium() {
+ column-count: 3;
+ }
+
+ @include break-wide() {
+ column-count: 4;
+ }
+}
diff --git a/packages/edit-site/src/components/sidebar-edit-mode/template-panel/template-actions.js b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/template-actions.js
index b68cf1ff617579..81acb244a11863 100644
--- a/packages/edit-site/src/components/sidebar-edit-mode/template-panel/template-actions.js
+++ b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/template-actions.js
@@ -11,13 +11,21 @@ import { moreVertical } from '@wordpress/icons';
*/
import { store as editSiteStore } from '../../../store';
import isTemplateRevertable from '../../../utils/is-template-revertable';
+import ReplaceTemplateButton from './replace-template-button';
+import { useAvailablePatterns } from './hooks';
export default function Actions( { template } ) {
+ const availablePatterns = useAvailablePatterns( template );
const { revertTemplate } = useDispatch( editSiteStore );
const isRevertable = isTemplateRevertable( template );
- if ( ! isRevertable ) {
+
+ if (
+ ! isRevertable &&
+ ( ! availablePatterns.length || availablePatterns.length < 1 )
+ ) {
return null;
}
+
return (
{ ( { onClose } ) => (
- {
- revertTemplate( template );
- onClose();
- } }
- >
- { __( 'Clear customizations' ) }
-
+ { isRevertable && (
+ {
+ revertTemplate( template );
+ onClose();
+ } }
+ >
+ { __( 'Clear customizations' ) }
+
+ ) }
+
) }
diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-pattern/template-part-navigation-menu-list-item.js b/packages/edit-site/src/components/sidebar-navigation-screen-pattern/template-part-navigation-menu-list-item.js
index 92c2f2450769f2..22d9d841dc6fd5 100644
--- a/packages/edit-site/src/components/sidebar-navigation-screen-pattern/template-part-navigation-menu-list-item.js
+++ b/packages/edit-site/src/components/sidebar-navigation-screen-pattern/template-part-navigation-menu-list-item.js
@@ -1,30 +1,27 @@
/**
* WordPress dependencies
*/
-import { useEntityProp } from '@wordpress/core-data';
import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import SidebarNavigationItem from '../sidebar-navigation-item';
+import useNavigationMenuTitle from './use-navigation-menu-title';
import { useLink } from '../routes/link';
import { NAVIGATION_POST_TYPE } from '../../utils/constants';
export default function TemplatePartNavigationMenuListItem( { id } ) {
- const [ title ] = useEntityProp(
- 'postType',
- NAVIGATION_POST_TYPE,
- 'title',
- id
- );
+ const title = useNavigationMenuTitle( id );
const linkInfo = useLink( {
postId: id,
postType: NAVIGATION_POST_TYPE,
} );
- if ( ! id ) return null;
+ if ( ! id || title === undefined ) {
+ return null;
+ }
return (
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 6512bcbc6345aa..b7f9b5295fd0f5 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
@@ -3,23 +3,19 @@
*/
import { __ } from '@wordpress/i18n';
import { __experimentalHeading as Heading } from '@wordpress/components';
-import { useEntityProp } from '@wordpress/core-data';
/**
* Internal dependencies
*/
import NavigationMenuEditor from '../sidebar-navigation-screen-navigation-menu/navigation-menu-editor';
-import { NAVIGATION_POST_TYPE } from '../../utils/constants';
+import useNavigationMenuTitle from './use-navigation-menu-title';
export default function TemplatePartNavigationMenu( { id } ) {
- const [ title ] = useEntityProp(
- 'postType',
- NAVIGATION_POST_TYPE,
- 'title',
- id
- );
+ const title = useNavigationMenuTitle( id );
- if ( ! id ) return null;
+ if ( ! id || title === undefined ) {
+ return null;
+ }
return (
<>
diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-pattern/use-navigation-menu-content.js b/packages/edit-site/src/components/sidebar-navigation-screen-pattern/use-navigation-menu-content.js
index 6c44ea5fbc9bbe..249124b1054cec 100644
--- a/packages/edit-site/src/components/sidebar-navigation-screen-pattern/use-navigation-menu-content.js
+++ b/packages/edit-site/src/components/sidebar-navigation-screen-pattern/use-navigation-menu-content.js
@@ -10,6 +10,16 @@ import TemplatePartNavigationMenus from './template-part-navigation-menus';
import useEditedEntityRecord from '../use-edited-entity-record';
import { TEMPLATE_PART_POST_TYPE } from '../../utils/constants';
+function getBlocksFromRecord( record ) {
+ if ( record?.blocks ) {
+ return record?.blocks;
+ }
+
+ return record?.content && typeof record.content !== 'function'
+ ? parse( record.content )
+ : [];
+}
+
/**
* Retrieves a list of specific blocks from a given tree of blocks.
*
@@ -60,11 +70,7 @@ export default function useNavigationMenuContent( postType, postId ) {
return;
}
- const blocks =
- record?.content && typeof record.content !== 'function'
- ? parse( record.content )
- : [];
-
+ const blocks = getBlocksFromRecord( record );
const navigationBlocks = getBlocksOfTypeFromBlocks(
'core/navigation',
blocks
diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-pattern/use-navigation-menu-title.js b/packages/edit-site/src/components/sidebar-navigation-screen-pattern/use-navigation-menu-title.js
new file mode 100644
index 00000000000000..4585c98ce3e1f5
--- /dev/null
+++ b/packages/edit-site/src/components/sidebar-navigation-screen-pattern/use-navigation-menu-title.js
@@ -0,0 +1,32 @@
+/**
+ * WordPress dependencies
+ */
+import { useSelect } from '@wordpress/data';
+import { store as coreStore } from '@wordpress/core-data';
+
+/**
+ * Internal dependencies
+ */
+import { NAVIGATION_POST_TYPE } from '../../utils/constants';
+
+export default function useNavigationMenuTitle( id ) {
+ return useSelect(
+ ( select ) => {
+ if ( ! id ) {
+ return undefined;
+ }
+
+ const editedRecord = select( coreStore ).getEditedEntityRecord(
+ 'postType',
+ NAVIGATION_POST_TYPE,
+ id
+ );
+
+ // Do not display a 'trashed' navigation menu.
+ return editedRecord.status === 'trash'
+ ? undefined
+ : editedRecord.title;
+ },
+ [ id ]
+ );
+}
diff --git a/packages/edit-site/src/store/selectors.js b/packages/edit-site/src/store/selectors.js
index 3f44ab57ba8072..9e861c7567e4ac 100644
--- a/packages/edit-site/src/store/selectors.js
+++ b/packages/edit-site/src/store/selectors.js
@@ -292,22 +292,21 @@ export function isSaveViewOpened( state ) {
* @return {Array} Template parts and their blocks in an array.
*/
export const getCurrentTemplateTemplateParts = createRegistrySelector(
- ( select ) => ( state ) => {
- const templateType = getEditedPostType( state );
- const templateId = getEditedPostId( state );
- const template = select( coreDataStore ).getEditedEntityRecord(
- 'postType',
- templateType,
- templateId
- );
-
+ ( select ) => () => {
const templateParts = select( coreDataStore ).getEntityRecords(
'postType',
TEMPLATE_PART_POST_TYPE,
{ per_page: -1 }
);
- return getFilteredTemplatePartBlocks( template.blocks, templateParts );
+ const clientIds =
+ select( blockEditorStore ).__experimentalGetGlobalBlocksByName(
+ 'core/template-part'
+ );
+ const blocks =
+ select( blockEditorStore ).getBlocksByClientId( clientIds );
+
+ return getFilteredTemplatePartBlocks( blocks, templateParts );
}
);
diff --git a/packages/edit-site/src/utils/constants.js b/packages/edit-site/src/utils/constants.js
index 6acf09182dba99..7d123818043801 100644
--- a/packages/edit-site/src/utils/constants.js
+++ b/packages/edit-site/src/utils/constants.js
@@ -50,7 +50,7 @@ export const PAGE_CONTENT_BLOCK_TYPES = {
export const POST_TYPE_LABELS = {
[ TEMPLATE_POST_TYPE ]: __( 'Template' ),
- [ TEMPLATE_PART_POST_TYPE ]: __( 'Template Part' ),
+ [ TEMPLATE_PART_POST_TYPE ]: __( 'Template part' ),
[ PATTERN_TYPES.user ]: __( 'Pattern' ),
[ NAVIGATION_POST_TYPE ]: __( 'Navigation' ),
};
diff --git a/packages/patterns/src/components/category-selector.js b/packages/patterns/src/components/category-selector.js
index 397d851d3886b9..7f00350e278ecf 100644
--- a/packages/patterns/src/components/category-selector.js
+++ b/packages/patterns/src/components/category-selector.js
@@ -4,8 +4,6 @@
import { __ } from '@wordpress/i18n';
import { useMemo, useState } from '@wordpress/element';
import { FormTokenField } from '@wordpress/components';
-import { useSelect } from '@wordpress/data';
-import { store as coreStore } from '@wordpress/core-data';
import { useDebounce } from '@wordpress/compose';
import { decodeEntities } from '@wordpress/html-entities';
@@ -13,40 +11,29 @@ const unescapeString = ( arg ) => {
return decodeEntities( arg );
};
-const EMPTY_ARRAY = [];
-const MAX_TERMS_SUGGESTIONS = 20;
-const DEFAULT_QUERY = {
- per_page: MAX_TERMS_SUGGESTIONS,
- _fields: 'id,name',
- context: 'view',
-};
export const CATEGORY_SLUG = 'wp_pattern_category';
-export default function CategorySelector( { values, onChange } ) {
+export default function CategorySelector( {
+ categoryTerms,
+ onChange,
+ categoryMap,
+} ) {
const [ search, setSearch ] = useState( '' );
const debouncedSearch = useDebounce( setSearch, 500 );
- const { searchResults } = useSelect(
- ( select ) => {
- const { getEntityRecords } = select( coreStore );
-
- return {
- searchResults: !! search
- ? getEntityRecords( 'taxonomy', CATEGORY_SLUG, {
- ...DEFAULT_QUERY,
- search,
- } )
- : EMPTY_ARRAY,
- };
- },
- [ search ]
- );
-
const suggestions = useMemo( () => {
- return ( searchResults ?? [] ).map( ( term ) =>
- unescapeString( term.name )
- );
- }, [ searchResults ] );
+ return Array.from( categoryMap.values() )
+ .map( ( category ) => unescapeString( category.label ) )
+ .filter( ( category ) => {
+ if ( search !== '' ) {
+ return category
+ .toLowerCase()
+ .includes( search.toLowerCase() );
+ }
+ return true;
+ } )
+ .sort( ( a, b ) => a.localeCompare( b ) );
+ }, [ search, categoryMap ] );
function handleChange( termNames ) {
const uniqueTerms = termNames.reduce( ( terms, newTerm ) => {
@@ -64,17 +51,16 @@ export default function CategorySelector( { values, onChange } ) {
}
return (
- <>
-
- >
+
);
}
diff --git a/packages/patterns/src/components/create-pattern-modal.js b/packages/patterns/src/components/create-pattern-modal.js
index 531936da5e5c28..37dd725ef9226a 100644
--- a/packages/patterns/src/components/create-pattern-modal.js
+++ b/packages/patterns/src/components/create-pattern-modal.js
@@ -10,8 +10,8 @@ import {
ToggleControl,
} from '@wordpress/components';
import { __ } from '@wordpress/i18n';
-import { useState } from '@wordpress/element';
-import { useDispatch } from '@wordpress/data';
+import { useState, useMemo } from '@wordpress/element';
+import { useDispatch, useSelect } from '@wordpress/data';
import { store as noticesStore } from '@wordpress/notices';
import { store as coreStore } from '@wordpress/core-data';
@@ -42,6 +42,42 @@ export default function CreatePatternModal( {
const { saveEntityRecord, invalidateResolution } = useDispatch( coreStore );
const { createErrorNotice } = useDispatch( noticesStore );
+ const { corePatternCategories, userPatternCategories } = useSelect(
+ ( select ) => {
+ const { getUserPatternCategories, getBlockPatternCategories } =
+ select( coreStore );
+
+ return {
+ corePatternCategories: getBlockPatternCategories(),
+ userPatternCategories: getUserPatternCategories(),
+ };
+ }
+ );
+
+ const categoryMap = useMemo( () => {
+ // Merge the user and core pattern categories and remove any duplicates.
+ const uniqueCategories = new Map();
+ [ ...userPatternCategories, ...corePatternCategories ].forEach(
+ ( category ) => {
+ if (
+ ! uniqueCategories.has( category.label ) &&
+ // There are two core categories with `Post` label so explicitly remove the one with
+ // the `query` slug to avoid any confusion.
+ category.name !== 'query'
+ ) {
+ // We need to store the name separately as this is used as the slug in the
+ // taxonomy and may vary from the label.
+ uniqueCategories.set( category.label, {
+ label: category.label,
+ value: category.label,
+ name: category.name,
+ } );
+ }
+ }
+ );
+ return uniqueCategories;
+ }, [ userPatternCategories, corePatternCategories ] );
+
async function onCreate( patternTitle, sync ) {
if ( ! title || isSaving ) {
return;
@@ -84,10 +120,16 @@ export default function CreatePatternModal( {
*/
async function findOrCreateTerm( term ) {
try {
+ // We need to match any existing term to the correct slug to prevent duplicates, eg.
+ // the core `Headers` category uses the singular `header` as the slug.
+ const existingTerm = categoryMap.get( term );
+ const termData = existingTerm
+ ? { name: existingTerm.label, slug: existingTerm.name }
+ : { name: term };
const newTerm = await saveEntityRecord(
'taxonomy',
CATEGORY_SLUG,
- { name: term },
+ termData,
{ throwOnError: true }
);
invalidateResolution( 'getUserPatternCategories' );
@@ -126,8 +168,9 @@ export default function CreatePatternModal( {
className="patterns-create-modal__name-input"
/>
[role="document"] {
+ width: 350px;
+ }
+
.patterns-menu-items__convert-modal-categories {
- max-width: 300px;
+ width: 100%;
+ position: relative;
+ min-height: 40px;
+ }
+ .components-form-token-field__suggestions-list {
+ position: absolute;
+ box-sizing: border-box;
+ z-index: 1;
+ background-color: $white;
+ // Account for the border width of the token field.
+ width: calc(100% + 2px);
+ left: -1px;
+ min-width: initial;
+ border: 1px solid var(--wp-admin-theme-color);
+ border-top: none;
+ box-shadow: 0 0 0 0.5px var(--wp-admin-theme-color);
+ border-bottom-left-radius: 2px;
+ border-bottom-right-radius: 2px;
}
}
.patterns-create-modal__name-input input[type="text"] {
- min-height: 34px;
+ // Match the minimal height of the category selector.
+ min-height: 40px;
+ // Override the default 1px margin-x.
+ margin: 0;
}
diff --git a/packages/react-native-editor/CHANGELOG.md b/packages/react-native-editor/CHANGELOG.md
index 1debe8fbaad8e8..df489757dd6bdd 100644
--- a/packages/react-native-editor/CHANGELOG.md
+++ b/packages/react-native-editor/CHANGELOG.md
@@ -10,6 +10,9 @@ For each user feature we should also add a importance categorization label to i
-->
## Unreleased
+- [*] Limit inner blocks nesting depth to avoid call stack size exceeded crash [#54382]
+- [*] Prevent crashes when setting an invalid media URL for Video or Audio blocks [#54834]
+- [**] Fallback to Twitter provider when embedding X URLs [#54876]
## 1.104.0
- [*] Fix the obscurred "Insert from URL" input for media blocks when using a device in landscape orientation. [#54096]
diff --git a/packages/scripts/config/playwright.config.js b/packages/scripts/config/playwright.config.js
index 16ee210bdfb86e..6f380017ccdc4f 100644
--- a/packages/scripts/config/playwright.config.js
+++ b/packages/scripts/config/playwright.config.js
@@ -25,7 +25,7 @@ const config = defineConfig( {
snapshotPathTemplate:
'{testDir}/{testFileDir}/__snapshots__/{arg}-{projectName}{ext}',
globalSetup: fileURLToPath(
- new URL( './playwright/global-setup.ts', 'file:' + __filename ).href
+ new URL( './playwright/global-setup.js', 'file:' + __filename ).href
),
use: {
baseURL: process.env.WP_BASE_URL || 'http://localhost:8889',
@@ -60,4 +60,4 @@ const config = defineConfig( {
],
} );
-export default config;
+module.exports = config;
diff --git a/packages/scripts/config/playwright/global-setup.js b/packages/scripts/config/playwright/global-setup.js
index eace349c6309dd..9a4cbde9b7d241 100644
--- a/packages/scripts/config/playwright/global-setup.js
+++ b/packages/scripts/config/playwright/global-setup.js
@@ -32,4 +32,4 @@ async function globalSetup( config ) {
await requestContext.dispose();
}
-export default globalSetup;
+module.exports = globalSetup;
diff --git a/phpunit/tests/fonts/font-library/wpRestFontLibraryController/installFonts.php b/phpunit/tests/fonts/font-library/wpRestFontLibraryController/installFonts.php
index 6ec15c71ade596..01ac1ff8436ed7 100644
--- a/phpunit/tests/fonts/font-library/wpRestFontLibraryController/installFonts.php
+++ b/phpunit/tests/fonts/font-library/wpRestFontLibraryController/installFonts.php
@@ -24,7 +24,7 @@ class Tests_Fonts_WPRESTFontLibraryController_InstallFonts extends WP_REST_Font_
public function test_install_fonts( $font_families, $files, $expected_response ) {
$install_request = new WP_REST_Request( 'POST', '/wp/v2/fonts' );
$font_families_json = json_encode( $font_families );
- $install_request->set_param( 'fontFamilies', $font_families_json );
+ $install_request->set_param( 'font_families', $font_families_json );
$install_request->set_file_params( $files );
$response = rest_get_server()->dispatch( $install_request );
$data = $response->get_data();
@@ -309,7 +309,7 @@ public function data_install_fonts() {
public function test_install_with_improper_inputs( $font_families, $files = array() ) {
$install_request = new WP_REST_Request( 'POST', '/wp/v2/fonts' );
$font_families_json = json_encode( $font_families );
- $install_request->set_param( 'fontFamilies', $font_families_json );
+ $install_request->set_param( 'font_families', $font_families_json );
$install_request->set_file_params( $files );
$response = rest_get_server()->dispatch( $install_request );
diff --git a/phpunit/tests/fonts/font-library/wpRestFontLibraryController/uninstallFonts.php b/phpunit/tests/fonts/font-library/wpRestFontLibraryController/uninstallFonts.php
index 860f47d8764810..a3b613e6f983e0 100644
--- a/phpunit/tests/fonts/font-library/wpRestFontLibraryController/uninstallFonts.php
+++ b/phpunit/tests/fonts/font-library/wpRestFontLibraryController/uninstallFonts.php
@@ -53,7 +53,7 @@ public function set_up() {
$install_request = new WP_REST_Request( 'POST', '/wp/v2/fonts' );
$font_families_json = json_encode( $mock_families );
- $install_request->set_param( 'fontFamilies', $font_families_json );
+ $install_request->set_param( 'font_families', $font_families_json );
rest_get_server()->dispatch( $install_request );
}
@@ -68,7 +68,7 @@ public function test_uninstall() {
);
$uninstall_request = new WP_REST_Request( 'DELETE', '/wp/v2/fonts' );
- $uninstall_request->set_param( 'fontFamilies', $font_families_to_uninstall );
+ $uninstall_request->set_param( 'font_families', $font_families_to_uninstall );
$response = rest_get_server()->dispatch( $uninstall_request );
$this->assertSame( 200, $response->get_status(), 'The response status is not 200.' );
}
@@ -88,7 +88,7 @@ public function test_uninstall_non_existing_fonts() {
),
);
- $uninstall_request->set_param( 'fontFamilies', $non_existing_font_data );
+ $uninstall_request->set_param( 'font_families', $non_existing_font_data );
$response = rest_get_server()->dispatch( $uninstall_request );
$data = $response->get_data();
$this->assertCount( 2, $data['errors'], 'The response should have 2 errors, one for each font family uninstall failure.' );
diff --git a/test/e2e/playwright.config.ts b/test/e2e/playwright.config.ts
index 742ca54f4f2ace..cbbfbac3be4159 100644
--- a/test/e2e/playwright.config.ts
+++ b/test/e2e/playwright.config.ts
@@ -11,7 +11,7 @@ import { defineConfig, devices } from '@playwright/test';
const baseConfig = require( '@wordpress/scripts/config/playwright.config' );
const config = defineConfig( {
- ...baseConfig.default,
+ ...baseConfig,
reporter: process.env.CI
? [ [ 'github' ], [ './config/flaky-tests-reporter.ts' ] ]
: 'list',
diff --git a/test/e2e/specs/editor/various/toolbar-roving-tabindex.spec.js b/test/e2e/specs/editor/various/toolbar-roving-tabindex.spec.js
index 17746bf95fe23d..23186dfff2de96 100644
--- a/test/e2e/specs/editor/various/toolbar-roving-tabindex.spec.js
+++ b/test/e2e/specs/editor/various/toolbar-roving-tabindex.spec.js
@@ -14,6 +14,10 @@ test.describe( 'Toolbar roving tabindex', () => {
await admin.createNewPost();
await editor.insertBlock( { name: 'core/paragraph' } );
await page.keyboard.type( 'First block' );
+
+ // Ensure the fixed toolbar option is off.
+ // See: https://github.com/WordPress/gutenberg/pull/54785.
+ await editor.setIsFixedToolbar( false );
} );
test( 'ensures base block toolbars use roving tabindex', async ( {
diff --git a/test/e2e/specs/site-editor/writing-flow.spec.js b/test/e2e/specs/site-editor/writing-flow.spec.js
index a2e05f0f0a463a..f9681a5ea2d46f 100644
--- a/test/e2e/specs/site-editor/writing-flow.spec.js
+++ b/test/e2e/specs/site-editor/writing-flow.spec.js
@@ -66,7 +66,7 @@ test.describe( 'Site editor writing flow', () => {
// Tab to the inspector, tabbing three times to go past the two resize handles.
await pageUtils.pressKeys( 'Tab', { times: 3 } );
const inspectorTemplateTab = page.locator(
- 'role=region[name="Editor settings"i] >> role=button[name="Template Part"i]'
+ 'role=region[name="Editor settings"i] >> role=button[name="Template part"i]'
);
await expect( inspectorTemplateTab ).toBeFocused();
} );
diff --git a/test/performance/playwright.config.ts b/test/performance/playwright.config.ts
index 0918b79d3144d2..a8208342ac2d81 100644
--- a/test/performance/playwright.config.ts
+++ b/test/performance/playwright.config.ts
@@ -13,7 +13,7 @@ const baseConfig = require( '@wordpress/scripts/config/playwright.config' );
process.env.ASSETS_PATH = path.join( __dirname, 'assets' );
const config = defineConfig( {
- ...baseConfig.default,
+ ...baseConfig,
reporter: process.env.CI
? './config/performance-reporter.ts'
: [ [ 'list' ], [ './config/performance-reporter.ts' ] ],
@@ -26,7 +26,7 @@ const config = defineConfig( {
new URL( './config/global-setup.ts', 'file:' + __filename ).href
),
use: {
- ...baseConfig.default.use,
+ ...baseConfig.use,
video: 'off',
},
} );