diff --git a/lib/block-supports/elements.php b/lib/block-supports/elements.php
index 85dbd7d39797c1..89275830f64b25 100644
--- a/lib/block-supports/elements.php
+++ b/lib/block-supports/elements.php
@@ -142,8 +142,9 @@ function gutenberg_render_elements_support_styles( $pre_render, $block ) {
'skip' => $skip_button_color_serialization,
),
'link' => array(
- 'selector' => ".$class_name a",
- 'hover_selector' => ".$class_name a:hover",
+ // :where(:not) matches theme.json selector.
+ 'selector' => ".$class_name a:where(:not(.wp-element-button))",
+ 'hover_selector' => ".$class_name a:where(:not(.wp-element-button)):hover",
'skip' => $skip_link_color_serialization,
),
'heading' => array(
diff --git a/lib/compat/wordpress-6.5/block-bindings/block-bindings.php b/lib/compat/wordpress-6.5/block-bindings/block-bindings.php
index 7ea0f1bab4683c..1bcb23ccd6549c 100644
--- a/lib/compat/wordpress-6.5/block-bindings/block-bindings.php
+++ b/lib/compat/wordpress-6.5/block-bindings/block-bindings.php
@@ -25,18 +25,19 @@
* @param array $source_properties {
* The array of arguments that are used to register a source.
*
- * @type string $label The label of the source.
- * @type callback $get_value_callback A callback executed when the source is processed during block rendering.
- * The callback should have the following signature:
+ * @type string $label The label of the source.
+ * @type callback $get_value_callback A callback executed when the source is processed during block rendering.
+ * The callback should have the following signature:
*
- * `function ($source_args, $block_instance,$attribute_name): mixed`
- * - @param array $source_args Array containing source arguments
- * used to look up the override value,
- * i.e. {"key": "foo"}.
- * - @param WP_Block $block_instance The block instance.
- * - @param string $attribute_name The name of an attribute .
- * The callback has a mixed return type; it may return a string to override
- * the block's original value, null, false to remove an attribute, etc.
+ * `function ($source_args, $block_instance,$attribute_name): mixed`
+ * - @param array $source_args Array containing source arguments
+ * used to look up the override value,
+ * i.e. {"key": "foo"}.
+ * - @param WP_Block $block_instance The block instance.
+ * - @param string $attribute_name The name of an attribute .
+ * The callback has a mixed return type; it may return a string to override
+ * the block's original value, null, false to remove an attribute, etc.
+ * @type array $uses_context (optional) Array of values to add to block `uses_context` needed by the source.
* }
* @return WP_Block_Bindings_Source|false Source when the registration was successful, or `false` on failure.
*/
diff --git a/lib/compat/wordpress-6.5/block-bindings/class-wp-block-bindings-registry.php b/lib/compat/wordpress-6.5/block-bindings/class-wp-block-bindings-registry.php
index e60b9ac8fbd082..7f04820050a913 100644
--- a/lib/compat/wordpress-6.5/block-bindings/class-wp-block-bindings-registry.php
+++ b/lib/compat/wordpress-6.5/block-bindings/class-wp-block-bindings-registry.php
@@ -32,6 +32,31 @@ final class WP_Block_Bindings_Registry {
*/
private static $instance = null;
+ /**
+ * Supported source properties that can be passed to the registered source.
+ *
+ * @since 6.5.0
+ * @var array
+ */
+ private $allowed_source_properties = array(
+ 'label',
+ 'get_value_callback',
+ 'uses_context',
+ );
+
+ /**
+ * Supported blocks that can use the block bindings API.
+ *
+ * @since 6.5.0
+ * @var array
+ */
+ private $supported_blocks = array(
+ 'core/paragraph',
+ 'core/heading',
+ 'core/image',
+ 'core/button',
+ );
+
/**
* Registers a new block bindings source.
*
@@ -48,18 +73,19 @@ final class WP_Block_Bindings_Registry {
* @param array $source_properties {
* The array of arguments that are used to register a source.
*
- * @type string $label The label of the source.
- * @type callback $get_value_callback A callback executed when the source is processed during block rendering.
- * The callback should have the following signature:
- *
- * `function ($source_args, $block_instance,$attribute_name): mixed`
- * - @param array $source_args Array containing source arguments
- * used to look up the override value,
- * i.e. {"key": "foo"}.
- * - @param WP_Block $block_instance The block instance.
- * - @param string $attribute_name The name of the target attribute.
- * The callback has a mixed return type; it may return a string to override
- * the block's original value, null, false to remove an attribute, etc.
+ * @type string $label The label of the source.
+ * @type callback $get_value_callback A callback executed when the source is processed during block rendering.
+ * The callback should have the following signature:
+ *
+ * `function ($source_args, $block_instance,$attribute_name): mixed`
+ * - @param array $source_args Array containing source arguments
+ * used to look up the override value,
+ * i.e. {"key": "foo"}.
+ * - @param WP_Block $block_instance The block instance.
+ * - @param string $attribute_name The name of the target attribute.
+ * The callback has a mixed return type; it may return a string to override
+ * the block's original value, null, false to remove an attribute, etc.
+ * @type array $uses_context (optional) Array of values to add to block `uses_context` needed by the source.
* }
* @return WP_Block_Bindings_Source|false Source when the registration was successful, or `false` on failure.
*/
@@ -102,7 +128,7 @@ public function register( string $source_name, array $source_properties ) {
return false;
}
- /* Validate that the source properties contain the label */
+ // Validate that the source properties contain the label.
if ( ! isset( $source_properties['label'] ) ) {
_doing_it_wrong(
__METHOD__,
@@ -112,7 +138,7 @@ public function register( string $source_name, array $source_properties ) {
return false;
}
- /* Validate that the source properties contain the get_value_callback */
+ // Validate that the source properties contain the get_value_callback.
if ( ! isset( $source_properties['get_value_callback'] ) ) {
_doing_it_wrong(
__METHOD__,
@@ -122,7 +148,7 @@ public function register( string $source_name, array $source_properties ) {
return false;
}
- /* Validate that the get_value_callback is a valid callback */
+ // Validate that the get_value_callback is a valid callback.
if ( ! is_callable( $source_properties['get_value_callback'] ) ) {
_doing_it_wrong(
__METHOD__,
@@ -132,6 +158,26 @@ public function register( string $source_name, array $source_properties ) {
return false;
}
+ // Validate that the uses_context parameter is an array.
+ if ( isset( $source_properties['uses_context'] ) && ! is_array( $source_properties['uses_context'] ) ) {
+ _doing_it_wrong(
+ __METHOD__,
+ __( 'The "uses_context" parameter must be an array.' ),
+ '6.5.0'
+ );
+ return false;
+ }
+
+ // Validate that the source properties contain only allowed properties.
+ if ( ! empty( array_diff( array_keys( $source_properties ), $this->allowed_source_properties ) ) ) {
+ _doing_it_wrong(
+ __METHOD__,
+ __( 'The $source_properties array contains invalid properties.' ),
+ '6.5.0'
+ );
+ return false;
+ }
+
$source = new WP_Block_Bindings_Source(
$source_name,
$source_properties
@@ -139,6 +185,22 @@ public function register( string $source_name, array $source_properties ) {
$this->sources[ $source_name ] = $source;
+ // Add `uses_context` defined by block bindings sources.
+ add_filter(
+ 'register_block_type_args',
+ function ( $args, $block_name ) use ( $source ) {
+ if ( ! in_array( $block_name, $this->supported_blocks, true ) || empty( $source->uses_context ) ) {
+ return $args;
+ }
+ $original_use_context = isset( $args['uses_context'] ) ? $args['uses_context'] : array();
+ // Use array_values to reset the array keys.
+ $args['uses_context'] = array_values( array_unique( array_merge( $original_use_context, $source->uses_context ) ) );
+
+ return $args;
+ },
+ 10,
+ 2
+ );
return $source;
}
diff --git a/lib/compat/wordpress-6.5/block-bindings/class-wp-block-bindings-source.php b/lib/compat/wordpress-6.5/block-bindings/class-wp-block-bindings-source.php
index fa2bc53705fdc4..1862f2af8bb11c 100644
--- a/lib/compat/wordpress-6.5/block-bindings/class-wp-block-bindings-source.php
+++ b/lib/compat/wordpress-6.5/block-bindings/class-wp-block-bindings-source.php
@@ -46,6 +46,14 @@ final class WP_Block_Bindings_Source {
*/
private $get_value_callback;
+ /**
+ * The context added to the blocks needed by the source.
+ *
+ * @since 6.5.0
+ * @var array|null
+ */
+ public $uses_context = null;
+
/**
* Constructor.
*
@@ -58,9 +66,10 @@ final class WP_Block_Bindings_Source {
* @param array $source_properties The properties of the source.
*/
public function __construct( string $name, array $source_properties ) {
- $this->name = $name;
- $this->label = $source_properties['label'];
- $this->get_value_callback = $source_properties['get_value_callback'];
+ $this->name = $name;
+ foreach ( $source_properties as $property_name => $property_value ) {
+ $this->$property_name = $property_value;
+ }
}
/**
diff --git a/lib/compat/wordpress-6.5/block-bindings/pattern-overrides.php b/lib/compat/wordpress-6.5/block-bindings/pattern-overrides.php
index 0e6d3fc94eeaa7..76c3d49ca8085f 100644
--- a/lib/compat/wordpress-6.5/block-bindings/pattern-overrides.php
+++ b/lib/compat/wordpress-6.5/block-bindings/pattern-overrides.php
@@ -35,6 +35,7 @@ function gutenberg_register_block_bindings_pattern_overrides_source() {
array(
'label' => _x( 'Pattern Overrides', 'block bindings source' ),
'get_value_callback' => 'gutenberg_block_bindings_pattern_overrides_callback',
+ 'uses_context' => array( 'pattern/overrides' ),
)
);
}
diff --git a/lib/compat/wordpress-6.5/block-bindings/post-meta.php b/lib/compat/wordpress-6.5/block-bindings/post-meta.php
index 655abd982663b8..5ce8eb7ac56ee7 100644
--- a/lib/compat/wordpress-6.5/block-bindings/post-meta.php
+++ b/lib/compat/wordpress-6.5/block-bindings/post-meta.php
@@ -8,23 +8,20 @@
/**
* Gets value for Post Meta source.
*
- * @param array $source_args Array containing source arguments used to look up the override value.
- * Example: array( "key" => "foo" ).
+ * @param array $source_args Array containing source arguments used to look up the override value.
+ * Example: array( "key" => "foo" ).
+ * @param WP_Block $block_instance The block instance.
* @return mixed The value computed for the source.
*/
-function gutenberg_block_bindings_post_meta_callback( $source_attrs ) {
- if ( ! isset( $source_attrs['key'] ) ) {
+function gutenberg_block_bindings_post_meta_callback( $source_attrs, $block_instance ) {
+ if ( empty( $source_attrs['key'] ) ) {
return null;
}
- // Use the postId attribute if available
- if ( isset( $source_attrs['postId'] ) ) {
- $post_id = $source_attrs['postId'];
- } else {
- // I tried using $block_instance->context['postId'] but it wasn't available in the image block.
- $post_id = get_the_ID();
+ if ( empty( $block_instance->context['postId'] ) ) {
+ return null;
}
-
+ $post_id = $block_instance->context['postId'];
// If a post isn't public, we need to prevent unauthorized users from accessing the post meta.
$post = get_post( $post_id );
if ( ( ! is_post_publicly_viewable( $post ) && ! current_user_can( 'read_post', $post_id ) ) || post_password_required( $post ) ) {
@@ -47,6 +44,7 @@ function gutenberg_register_block_bindings_post_meta_source() {
array(
'label' => _x( 'Post Meta', 'block bindings source' ),
'get_value_callback' => 'gutenberg_block_bindings_post_meta_callback',
+ 'uses_context' => array( 'postId', 'postType' ),
)
);
}
diff --git a/lib/compat/wordpress-6.5/blocks.php b/lib/compat/wordpress-6.5/blocks.php
index c670e1363a7f81..dbcdc433788ab1 100644
--- a/lib/compat/wordpress-6.5/blocks.php
+++ b/lib/compat/wordpress-6.5/blocks.php
@@ -57,7 +57,7 @@ function gutenberg_register_metadata_attribute( $args ) {
*/
function gutenberg_block_bindings_replace_html( $block_content, $block_name, string $attribute_name, $source_value ) {
$block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block_name );
- if ( ! isset( $block_type->attributes[ $attribute_name ] ) ) {
+ if ( ! isset( $block_type->attributes[ $attribute_name ]['source'] ) ) {
return $block_content;
}
@@ -157,18 +157,16 @@ function gutenberg_block_bindings_replace_html( $block_content, $block_name, str
* @param WP_Block $block_instance The block instance.
*/
function gutenberg_process_block_bindings( $block_content, $parsed_block, $block_instance ) {
- // Allowed blocks that support block bindings.
- // TODO: Look for a mechanism to opt-in for this. Maybe adding a property to block attributes?
- $allowed_blocks = array(
+ $supported_block_attrs = array(
'core/paragraph' => array( 'content' ),
'core/heading' => array( 'content' ),
- 'core/image' => array( 'url', 'title', 'alt' ),
+ 'core/image' => array( 'id', 'url', 'title', 'alt' ),
'core/button' => array( 'url', 'text', 'linkTarget', 'rel' ),
);
- // If the block doesn't have the bindings property or isn't one of the allowed block types, return.
+ // If the block doesn't have the bindings property or isn't one of the supported block types, return.
if (
- ! isset( $allowed_blocks[ $block_instance->name ] ) ||
+ ! isset( $supported_block_attrs[ $block_instance->name ] ) ||
empty( $parsed_block['attrs']['metadata']['bindings'] ) ||
! is_array( $parsed_block['attrs']['metadata']['bindings'] )
) {
@@ -192,8 +190,8 @@ function gutenberg_process_block_bindings( $block_content, $parsed_block, $block
$modified_block_content = $block_content;
foreach ( $parsed_block['attrs']['metadata']['bindings'] as $attribute_name => $block_binding ) {
- // If the attribute is not in the allowed list, process next attribute.
- if ( ! in_array( $attribute_name, $allowed_blocks[ $block_instance->name ], true ) ) {
+ // If the attribute is not in the supported list, process next attribute.
+ if ( ! in_array( $attribute_name, $supported_block_attrs[ $block_instance->name ], true ) ) {
continue;
}
// If no source is provided, or that source is not registered, process next attribute.
diff --git a/lib/compat/wordpress-6.5/class-wp-script-modules.php b/lib/compat/wordpress-6.5/class-wp-script-modules.php
index 205b50cd532596..4d3280f1db0b97 100644
--- a/lib/compat/wordpress-6.5/class-wp-script-modules.php
+++ b/lib/compat/wordpress-6.5/class-wp-script-modules.php
@@ -152,6 +152,18 @@ public function dequeue( string $id ) {
unset( $this->enqueued_before_registered[ $id ] );
}
+ /**
+ * Removes a registered script module.
+ *
+ * @since 6.5.0
+ *
+ * @param string $id The identifier of the script module.
+ */
+ public function deregister( string $id ) {
+ unset( $this->registered[ $id ] );
+ unset( $this->enqueued_before_registered[ $id ] );
+ }
+
/**
* Adds the hooks to print the import map, enqueued script modules and script
* module preloads.
diff --git a/lib/compat/wordpress-6.5/fonts/class-wp-font-collection.php b/lib/compat/wordpress-6.5/fonts/class-wp-font-collection.php
index 4a01f748d16e42..cd402a8e545003 100644
--- a/lib/compat/wordpress-6.5/fonts/class-wp-font-collection.php
+++ b/lib/compat/wordpress-6.5/fonts/class-wp-font-collection.php
@@ -221,8 +221,10 @@ private static function get_sanitization_schema() {
array(
'font_family_settings' => array(
'name' => 'sanitize_text_field',
- 'slug' => 'sanitize_title',
- 'fontFamily' => 'sanitize_text_field',
+ 'slug' => static function ( $value ) {
+ return _wp_to_kebab_case( sanitize_title( $value ) );
+ },
+ 'fontFamily' => 'WP_Font_Utils::sanitize_font_family',
'preview' => 'sanitize_url',
'fontFace' => array(
array(
diff --git a/lib/compat/wordpress-6.5/fonts/class-wp-font-utils.php b/lib/compat/wordpress-6.5/fonts/class-wp-font-utils.php
index 23c95d633fb47b..59a85132f72f11 100644
--- a/lib/compat/wordpress-6.5/fonts/class-wp-font-utils.php
+++ b/lib/compat/wordpress-6.5/fonts/class-wp-font-utils.php
@@ -20,11 +20,37 @@
* @access private
*/
class WP_Font_Utils {
+
+ /**
+ * Adds surrounding quotes to font family names that contain special characters.
+ *
+ * It follows the recommendations from the CSS Fonts Module Level 4.
+ * @link https://www.w3.org/TR/css-fonts-4/#font-family-prop
+ *
+ * @since 6.5.0
+ *
+ * @param string $item A font family name.
+ * @return string The font family name with surrounding quotes, if necessary.
+ */
+ private static function maybe_add_quotes( $item ) {
+ // Matches strings that are not exclusively alphabetic characters or hyphens, and do not exactly follow the pattern generic(alphabetic characters or hyphens).
+ $regex = '/^(?!generic\([a-zA-Z\-]+\)$)(?!^[a-zA-Z\-]+$).+/';
+ $item = trim( $item );
+ if ( preg_match( $regex, $item ) ) {
+ $item = trim( $item, "\"'" );
+ return '"' . $item . '"';
+ }
+ return $item;
+ }
+
/**
* Sanitizes and formats font family names.
*
- * - Applies `sanitize_text_field`
- * - Adds surrounding quotes to names that contain spaces and are not already quoted
+ * - Applies `sanitize_text_field`.
+ * - Adds surrounding quotes to names containing any characters that are not alphabetic or dashes.
+ *
+ * It follows the recommendations from the CSS Fonts Module Level 4.
+ * @link https://www.w3.org/TR/css-fonts-4/#font-family-prop
*
* @since 6.5.0
* @access private
@@ -39,26 +65,19 @@ public static function sanitize_font_family( $font_family ) {
return '';
}
- $font_family = sanitize_text_field( $font_family );
- $font_families = explode( ',', $font_family );
- $wrapped_font_families = array_map(
- function ( $family ) {
- $trimmed = trim( $family );
- if ( ! empty( $trimmed ) && str_contains( $trimmed, ' ' ) && ! str_contains( $trimmed, "'" ) && ! str_contains( $trimmed, '"' ) ) {
- return '"' . $trimmed . '"';
+ $output = sanitize_text_field( $font_family );
+ $formatted_items = array();
+ if ( str_contains( $output, ',' ) ) {
+ $items = explode( ',', $output );
+ foreach ( $items as $item ) {
+ $formatted_item = self::maybe_add_quotes( $item );
+ if ( ! empty( $formatted_item ) ) {
+ $formatted_items[] = $formatted_item;
}
- return $trimmed;
- },
- $font_families
- );
-
- if ( count( $wrapped_font_families ) === 1 ) {
- $font_family = $wrapped_font_families[0];
- } else {
- $font_family = implode( ', ', $wrapped_font_families );
+ }
+ return implode( ', ', $formatted_items );
}
-
- return $font_family;
+ return self::maybe_add_quotes( $output );
}
/**
diff --git a/lib/compat/wordpress-6.5/navigation-block-variations.php b/lib/compat/wordpress-6.5/navigation-block-variations.php
index 0bf789d42901e9..4009eb39386aeb 100644
--- a/lib/compat/wordpress-6.5/navigation-block-variations.php
+++ b/lib/compat/wordpress-6.5/navigation-block-variations.php
@@ -11,7 +11,7 @@ function gutenberg_navigation_link_variations_compat( $args ) {
if ( 'core/navigation-link' !== $args['name'] || ! empty( $args['variation_callback'] ) ) {
return $args;
}
- $args['variation_callback'] = 'build_navigation_link_block_variations';
+ $args['variation_callback'] = 'gutenberg_block_core_navigation_link_build_variations';
return $args;
}
diff --git a/lib/compat/wordpress-6.5/scripts-modules.php b/lib/compat/wordpress-6.5/scripts-modules.php
index 8fd8978d9e8f5d..c4a1690b6986f0 100644
--- a/lib/compat/wordpress-6.5/scripts-modules.php
+++ b/lib/compat/wordpress-6.5/scripts-modules.php
@@ -207,3 +207,16 @@ function wp_dequeue_script_module( string $id ) {
wp_script_modules()->dequeue( $id );
}
}
+
+if ( ! function_exists( 'wp_deregister_script_module' ) ) {
+ /**
+ * Deregisters the script module.
+ *
+ * @since 6.5.0
+ *
+ * @param string $id The identifier of the script module.
+ */
+ function wp_deregister_script_module( string $id ) {
+ wp_script_modules()->deregister( $id );
+ }
+}
diff --git a/lib/experimental/interactivity-api.php b/lib/experimental/interactivity-api.php
deleted file mode 100644
index 5d0f694dca3692..00000000000000
--- a/lib/experimental/interactivity-api.php
+++ /dev/null
@@ -1,22 +0,0 @@
- {
- const slug = mergedShadowPresets?.find(
+ const slug = overriddenShadowPresets?.find(
( { shadow: shadowName } ) => shadowName === newValue
)?.slug;
diff --git a/packages/block-editor/src/components/global-styles/get-global-styles-changes.js b/packages/block-editor/src/components/global-styles/get-global-styles-changes.js
index 9bbd11fb7d7977..05ac3429e4b65e 100644
--- a/packages/block-editor/src/components/global-styles/get-global-styles-changes.js
+++ b/packages/block-editor/src/components/global-styles/get-global-styles-changes.js
@@ -22,8 +22,8 @@ const translationMap = {
h4: __( 'H4' ),
h5: __( 'H5' ),
h6: __( 'H6' ),
- 'settings.color': __( 'Color settings' ),
- 'settings.typography': __( 'Typography settings' ),
+ 'settings.color': __( 'Color' ),
+ 'settings.typography': __( 'Typography' ),
'styles.color': __( 'Colors' ),
'styles.spacing': __( 'Spacing' ),
'styles.typography': __( 'Typography' ),
@@ -54,12 +54,7 @@ function getTranslation( key ) {
}
if ( keyArray?.[ 0 ] === 'elements' ) {
- const elementName = translationMap[ keyArray[ 1 ] ] || keyArray[ 1 ];
- return sprintf(
- // translators: %s: element name, e.g., heading button, link, caption.
- __( '%s element' ),
- elementName
- );
+ return translationMap[ keyArray[ 1 ] ] || keyArray[ 1 ];
}
return undefined;
@@ -114,9 +109,9 @@ function deepCompare( changedObject, originalObject, parentPath = '' ) {
*
* @param {Object} next The changed object to compare.
* @param {Object} previous The original object to compare against.
- * @return {string[]} An array of translated changes.
+ * @return {Array[]} A 2-dimensional array of tuples: [ "group", "translated change" ].
*/
-function getGlobalStylesChangelist( next, previous ) {
+export function getGlobalStylesChangelist( next, previous ) {
const cacheKey = JSON.stringify( { next, previous } );
if ( globalStylesChangesCache.has( cacheKey ) ) {
@@ -160,12 +155,12 @@ function getGlobalStylesChangelist( next, previous ) {
const result = [ ...new Set( changedValueTree ) ]
/*
* Translate the keys.
- * Remove duplicate or empty translations.
+ * Remove empty translations.
*/
.reduce( ( acc, curr ) => {
const translation = getTranslation( curr );
- if ( translation && ! acc.includes( translation ) ) {
- acc.push( translation );
+ if ( translation ) {
+ acc.push( [ curr.split( '.' )[ 0 ], translation ] );
}
return acc;
}, [] );
@@ -176,29 +171,74 @@ function getGlobalStylesChangelist( next, previous ) {
}
/**
- * From a getGlobalStylesChangelist() result, returns a truncated array of translated changes.
- * Appends a translated string indicating the number of changes that were truncated.
+ * From a getGlobalStylesChangelist() result, returns an array of translated global styles changes, grouped by type.
+ * The types are 'blocks', 'elements', 'settings', and 'styles'.
*
* @param {Object} next The changed object to compare.
* @param {Object} previous The original object to compare against.
* @param {{maxResults:number}} options Options. maxResults: results to return before truncating.
- * @return {string[]} An array of translated changes.
+ * @return {string[]} An array of translated changes.
*/
export default function getGlobalStylesChanges( next, previous, options = {} ) {
- const changes = getGlobalStylesChangelist( next, previous );
- const changesLength = changes.length;
+ let changeList = getGlobalStylesChangelist( next, previous );
+ const changesLength = changeList.length;
const { maxResults } = options;
- // Truncate to `n` results if necessary.
- if ( !! maxResults && changesLength && changesLength > maxResults ) {
- const deleteCount = changesLength - maxResults;
- const andMoreText = sprintf(
- // translators: %d: number of global styles changes that are not displayed in the UI.
- _n( '…and %d more change', '…and %d more changes', deleteCount ),
- deleteCount
- );
- changes.splice( maxResults, deleteCount, andMoreText );
+ if ( changesLength ) {
+ // Truncate to `n` results if necessary.
+ if ( !! maxResults && changesLength > maxResults ) {
+ changeList = changeList.slice( 0, maxResults );
+ }
+ return Object.entries(
+ changeList.reduce( ( acc, curr ) => {
+ const group = acc[ curr[ 0 ] ] || [];
+ if ( ! group.includes( curr[ 1 ] ) ) {
+ acc[ curr[ 0 ] ] = [ ...group, curr[ 1 ] ];
+ }
+ return acc;
+ }, {} )
+ ).map( ( [ key, changeValues ] ) => {
+ const changeValuesLength = changeValues.length;
+ const joinedChangesValue = changeValues.join( __( ', ' ) );
+ switch ( key ) {
+ case 'blocks': {
+ return sprintf(
+ // translators: %s: a list of block names separated by a comma.
+ _n( '%s block.', '%s blocks.', changeValuesLength ),
+ joinedChangesValue
+ );
+ }
+ case 'elements': {
+ return sprintf(
+ // translators: %s: a list of element names separated by a comma.
+ _n( '%s element.', '%s elements.', changeValuesLength ),
+ joinedChangesValue
+ );
+ }
+ case 'settings': {
+ return sprintf(
+ // translators: %s: a list of theme.json setting labels separated by a comma.
+ __( '%s settings.' ),
+ joinedChangesValue
+ );
+ }
+ case 'styles': {
+ return sprintf(
+ // translators: %s: a list of theme.json top-level styles labels separated by a comma.
+ __( '%s styles.' ),
+ joinedChangesValue
+ );
+ }
+ default: {
+ return sprintf(
+ // translators: %s: a list of global styles changes separated by a comma.
+ __( '%s.' ),
+ joinedChangesValue
+ );
+ }
+ }
+ } );
}
- return changes;
+ return EMPTY_ARRAY;
}
diff --git a/packages/block-editor/src/components/global-styles/test/get-global-styles-changes.js b/packages/block-editor/src/components/global-styles/test/get-global-styles-changes.js
index 2e7a68dab1f7bb..9ff840dc76730a 100644
--- a/packages/block-editor/src/components/global-styles/test/get-global-styles-changes.js
+++ b/packages/block-editor/src/components/global-styles/test/get-global-styles-changes.js
@@ -1,7 +1,9 @@
/**
* Internal dependencies
*/
-import getGlobalStylesChanges from '../get-global-styles-changes';
+import getGlobalStylesChanges, {
+ getGlobalStylesChangelist,
+} from '../get-global-styles-changes';
/**
* WordPress dependencies
@@ -12,24 +14,8 @@ import {
getBlockTypes,
} from '@wordpress/blocks';
-describe( 'getGlobalStylesChanges', () => {
- beforeEach( () => {
- registerBlockType( 'core/test-fiori-di-zucca', {
- save: () => {},
- category: 'text',
- title: 'Test pumpkin flowers',
- edit: () => {},
- } );
- } );
-
- afterEach( () => {
- getBlockTypes().forEach( ( block ) => {
- unregisterBlockType( block.name );
- } );
- } );
-
- const revision = {
- id: 10,
+describe( 'getGlobalStylesChanges and utils', () => {
+ const next = {
styles: {
typography: {
fontSize: 'var(--wp--preset--font-size--potato)',
@@ -85,11 +71,18 @@ describe( 'getGlobalStylesChanges', () => {
},
],
},
+ gradients: [
+ {
+ name: 'Something something',
+ gradient:
+ 'linear-gradient(105deg,rgba(6,147,100,1) 0%,rgb(155,81,100) 100%)',
+ slug: 'something-something',
+ },
+ ],
},
},
};
- const previousRevision = {
- id: 9,
+ const previous = {
styles: {
typography: {
fontSize: 'var(--wp--preset--font-size--fungus)',
@@ -161,74 +154,120 @@ describe( 'getGlobalStylesChanges', () => {
color: 'blue',
},
],
+ custom: [
+ {
+ slug: 'one',
+ color: 'tomato',
+ },
+ ],
},
+ gradients: [
+ {
+ name: 'Vivid cyan blue to vivid purple',
+ gradient:
+ 'linear-gradient(135deg,rgba(6,147,227,1) 0%,rgb(155,81,224) 100%)',
+ slug: 'vivid-cyan-blue-to-vivid-purple',
+ },
+ ],
+ },
+ typography: {
+ fluid: true,
},
},
};
- it( 'returns a list of changes and caches them', () => {
- const resultA = getGlobalStylesChanges( revision, previousRevision );
- expect( resultA ).toEqual( [
- 'Colors',
- 'Typography',
- 'Test pumpkin flowers',
- 'H3 element',
- 'Caption element',
- 'H6 element',
- 'Link element',
- 'Color settings',
- ] );
-
- const resultB = getGlobalStylesChanges( revision, previousRevision );
-
- expect( resultA ).toBe( resultB );
+ beforeEach( () => {
+ registerBlockType( 'core/test-fiori-di-zucca', {
+ save: () => {},
+ category: 'text',
+ title: 'Test pumpkin flowers',
+ edit: () => {},
+ } );
} );
- it( 'returns a list of truncated changes', () => {
- const resultA = getGlobalStylesChanges( revision, previousRevision, {
- maxResults: 3,
+ afterEach( () => {
+ getBlockTypes().forEach( ( block ) => {
+ unregisterBlockType( block.name );
} );
- expect( resultA ).toEqual( [
- 'Colors',
- 'Typography',
- 'Test pumpkin flowers',
- '…and 5 more changes',
- ] );
} );
- it( 'skips unknown and unchanged keys', () => {
- const result = getGlobalStylesChanges(
- {
- styles: {
- frogs: {
- legs: 'green',
- },
- typography: {
- fontSize: '1rem',
- },
- settings: {
- '': {
- '': 'foo',
+ describe( 'getGlobalStylesChanges()', () => {
+ it( 'returns a list of changes', () => {
+ const result = getGlobalStylesChanges( next, previous );
+ expect( result ).toEqual( [
+ 'Colors, Typography styles.',
+ 'Test pumpkin flowers block.',
+ 'H3, Caption, H6, Link elements.',
+ 'Color, Typography settings.',
+ ] );
+ } );
+
+ it( 'returns a list of truncated changes', () => {
+ const resultA = getGlobalStylesChanges( next, previous, {
+ maxResults: 3,
+ } );
+ expect( resultA ).toEqual( [
+ 'Colors, Typography styles.',
+ 'Test pumpkin flowers block.',
+ ] );
+ } );
+
+ it( 'skips unknown and unchanged keys', () => {
+ const result = getGlobalStylesChanges(
+ {
+ styles: {
+ frogs: {
+ legs: 'green',
+ },
+ typography: {
+ fontSize: '1rem',
+ },
+ settings: {
+ '': {
+ '': 'foo',
+ },
},
},
},
- },
- {
- styles: {
- frogs: {
- legs: 'yellow',
- },
- typography: {
- fontSize: '1rem',
- },
- settings: {
- '': {
- '': 'bar',
+ {
+ styles: {
+ frogs: {
+ legs: 'yellow',
+ },
+ typography: {
+ fontSize: '1rem',
+ },
+ settings: {
+ '': {
+ '': 'bar',
+ },
},
},
- },
- }
- );
- expect( result ).toEqual( [] );
+ }
+ );
+ expect( result ).toEqual( [] );
+ } );
+ } );
+
+ describe( 'getGlobalStylesChangelist()', () => {
+ it( 'compares two objects and returns a cached list of changed keys', () => {
+ const resultA = getGlobalStylesChangelist( next, previous );
+
+ expect( resultA ).toEqual( [
+ [ 'styles', 'Colors' ],
+ [ 'styles', 'Typography' ],
+ [ 'blocks', 'Test pumpkin flowers' ],
+ [ 'elements', 'H3' ],
+ [ 'elements', 'Caption' ],
+ [ 'elements', 'H6' ],
+ [ 'elements', 'Link' ],
+ [ 'settings', 'Color' ],
+ [ 'settings', 'Typography' ],
+ ] );
+
+ const resultB = getGlobalStylesChangelist( next, previous );
+
+ expect( resultB ).toEqual( resultA );
+ } );
} );
} );
diff --git a/packages/block-editor/src/components/global-styles/typography-panel.js b/packages/block-editor/src/components/global-styles/typography-panel.js
index e689d84c83c981..7fab783c4d0e7b 100644
--- a/packages/block-editor/src/components/global-styles/typography-panel.js
+++ b/packages/block-editor/src/components/global-styles/typography-panel.js
@@ -13,7 +13,11 @@ import { useCallback } from '@wordpress/element';
/**
* Internal dependencies
*/
-import { mergeOrigins, hasMergedOrigins } from '../../store/get-block-settings';
+import {
+ mergeOrigins,
+ overrideOrigins,
+ hasOriginValue,
+} from '../../store/get-block-settings';
import FontFamilyControl from '../font-family';
import FontAppearanceControl from '../font-appearance-control';
import LineHeightControl from '../line-height-control';
@@ -53,13 +57,13 @@ export function useHasTypographyPanel( settings ) {
function useHasFontSizeControl( settings ) {
return (
- hasMergedOrigins( settings?.typography?.fontSizes ) ||
+ hasOriginValue( settings?.typography?.fontSizes ) ||
settings?.typography?.customFontSize
);
}
function useHasFontFamilyControl( settings ) {
- return hasMergedOrigins( settings?.typography?.fontFamilies );
+ return hasOriginValue( settings?.typography?.fontFamilies );
}
function useHasLineHeightControl( settings ) {
@@ -101,10 +105,10 @@ function useHasTextColumnsControl( settings ) {
}
function getUniqueFontSizesBySlug( settings ) {
- const fontSizes = settings?.typography?.fontSizes;
- const mergedFontSizes = fontSizes ? mergeOrigins( fontSizes ) : [];
+ const fontSizes = settings?.typography?.fontSizes ?? {};
+ const overriddenFontSizes = overrideOrigins( fontSizes ) ?? [];
const uniqueSizes = [];
- for ( const currentSize of mergedFontSizes ) {
+ for ( const currentSize of overriddenFontSizes ) {
if ( ! uniqueSizes.some( ( { slug } ) => slug === currentSize.slug ) ) {
uniqueSizes.push( currentSize );
}
@@ -162,7 +166,7 @@ export default function TypographyPanel( {
// Font Family
const hasFontFamilyEnabled = useHasFontFamilyControl( settings );
- const fontFamilies = settings?.typography?.fontFamilies;
+ const fontFamilies = settings?.typography?.fontFamilies ?? {};
const mergedFontFamilies = fontFamilies ? mergeOrigins( fontFamilies ) : [];
const fontFamily = decodeValue( inheritedValue?.typography?.fontFamily );
const setFontFamily = ( newValue ) => {
diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js
index de482c5f059dc8..673562fde34f58 100644
--- a/packages/block-editor/src/components/iframe/index.js
+++ b/packages/block-editor/src/components/iframe/index.js
@@ -273,13 +273,16 @@ function Iframe( {
src={ src }
title={ __( 'Editor canvas' ) }
onKeyDown={ ( event ) => {
+ if ( props.onKeyDown ) {
+ props.onKeyDown( event );
+ }
// If the event originates from inside the iframe, it means
// it bubbled through the portal, but only with React
// events. We need to to bubble native events as well,
// though by doing so we also trigger another React event,
// so we need to stop the propagation of this event to avoid
// duplication.
- if (
+ else if (
event.currentTarget.ownerDocument !==
event.target.ownerDocument
) {
diff --git a/packages/block-editor/src/components/rich-text/index.native.js b/packages/block-editor/src/components/rich-text/index.native.js
index acadfb24a72217..7739fcf357c900 100644
--- a/packages/block-editor/src/components/rich-text/index.native.js
+++ b/packages/block-editor/src/components/rich-text/index.native.js
@@ -316,6 +316,7 @@ export function RichTextWrapper(
transformation.transform( { content: value.text } ),
] );
__unstableMarkAutomaticChange();
+ return;
}
}
diff --git a/packages/block-editor/src/components/rich-text/use-enter.js b/packages/block-editor/src/components/rich-text/use-enter.js
index 40d4f9be4759fa..4daf70e7fa3c74 100644
--- a/packages/block-editor/src/components/rich-text/use-enter.js
+++ b/packages/block-editor/src/components/rich-text/use-enter.js
@@ -61,6 +61,7 @@ export function useEnter( props ) {
} ),
] );
__unstableMarkAutomaticChange();
+ return;
}
}
diff --git a/packages/block-editor/src/hooks/use-bindings-attributes.js b/packages/block-editor/src/hooks/use-bindings-attributes.js
index 75f337cff97957..899faf0a8cbd5d 100644
--- a/packages/block-editor/src/hooks/use-bindings-attributes.js
+++ b/packages/block-editor/src/hooks/use-bindings-attributes.js
@@ -112,24 +112,3 @@ addFilter(
'core/editor/custom-sources-backwards-compatibility/shim-attribute-source',
shimAttributeSource
);
-
-// Add the context to all blocks.
-addFilter(
- 'blocks.registerBlockType',
- 'core/block-bindings-ui',
- ( settings, name ) => {
- if ( ! ( name in BLOCK_BINDINGS_ALLOWED_BLOCKS ) ) {
- return settings;
- }
- const contextItems = [ 'postId', 'postType', 'queryId' ];
- const usesContextArray = settings.usesContext;
- const oldUsesContextArray = new Set( usesContextArray );
- contextItems.forEach( ( item ) => {
- if ( ! oldUsesContextArray.has( item ) ) {
- usesContextArray.push( item );
- }
- } );
- settings.usesContext = usesContextArray;
- return settings;
- }
-);
diff --git a/packages/block-editor/src/hooks/utils.js b/packages/block-editor/src/hooks/utils.js
index 2f7a8f3a81f19d..fbe84514c3e53c 100644
--- a/packages/block-editor/src/hooks/utils.js
+++ b/packages/block-editor/src/hooks/utils.js
@@ -176,8 +176,12 @@ export function useBlockSettings( name, parentLayout ) {
const [
backgroundImage,
backgroundSize,
- fontFamilies,
- fontSizes,
+ customFontFamilies,
+ defaultFontFamilies,
+ themeFontFamilies,
+ customFontSizes,
+ defaultFontSizes,
+ themeFontSizes,
customFontSize,
fontStyle,
fontWeight,
@@ -223,8 +227,12 @@ export function useBlockSettings( name, parentLayout ) {
] = useSettings(
'background.backgroundImage',
'background.backgroundSize',
- 'typography.fontFamilies',
- 'typography.fontSizes',
+ 'typography.fontFamilies.custom',
+ 'typography.fontFamilies.default',
+ 'typography.fontFamilies.theme',
+ 'typography.fontSizes.custom',
+ 'typography.fontSizes.default',
+ 'typography.fontSizes.theme',
'typography.customFontSize',
'typography.fontStyle',
'typography.fontWeight',
@@ -305,10 +313,14 @@ export function useBlockSettings( name, parentLayout ) {
},
typography: {
fontFamilies: {
- custom: fontFamilies,
+ custom: customFontFamilies,
+ default: defaultFontFamilies,
+ theme: themeFontFamilies,
},
fontSizes: {
- custom: fontSizes,
+ custom: customFontSizes,
+ default: defaultFontSizes,
+ theme: themeFontSizes,
},
customFontSize,
fontStyle,
@@ -346,8 +358,12 @@ export function useBlockSettings( name, parentLayout ) {
}, [
backgroundImage,
backgroundSize,
- fontFamilies,
- fontSizes,
+ customFontFamilies,
+ defaultFontFamilies,
+ themeFontFamilies,
+ customFontSizes,
+ defaultFontSizes,
+ themeFontSizes,
customFontSize,
fontStyle,
fontWeight,
diff --git a/packages/block-editor/src/store/get-block-settings.js b/packages/block-editor/src/store/get-block-settings.js
index 598754a3497184..1bffebf931e818 100644
--- a/packages/block-editor/src/store/get-block-settings.js
+++ b/packages/block-editor/src/store/get-block-settings.js
@@ -2,7 +2,7 @@
* WordPress dependencies
*/
import {
- __EXPERIMENTAL_PATHS_WITH_MERGE as PATHS_WITH_MERGE,
+ __EXPERIMENTAL_PATHS_WITH_OVERRIDE as PATHS_WITH_OVERRIDE,
hasBlockSupport,
} from '@wordpress/blocks';
import { applyFilters } from '@wordpress/hooks';
@@ -111,6 +111,17 @@ export function mergeOrigins( value ) {
}
const mergeCache = new WeakMap();
+/**
+ * For settings like `color.palette`, which have a value that is an object
+ * with `default`, `theme`, `custom`, with field values that are arrays of
+ * items, returns the one with the highest priority among these three arrays.
+ * @param {Object} value Object to extract from
+ * @return {Array} Array of items extracted from the three origins
+ */
+export function overrideOrigins( value ) {
+ return value.custom ?? value.theme ?? value.default;
+}
+
/**
* For settings like `color.palette`, which have a value that is an object
* with `default`, `theme`, `custom`, with field values that are arrays of
@@ -119,7 +130,7 @@ const mergeCache = new WeakMap();
* @param {Object} value Object to check
* @return {boolean} Whether the object has values in any of the three origins
*/
-export function hasMergedOrigins( value ) {
+export function hasOriginValue( value ) {
return [ 'default', 'theme', 'custom' ].some(
( key ) => value?.[ key ]?.length
);
@@ -203,8 +214,8 @@ export function getBlockSettings( state, clientId, ...paths ) {
// Return if the setting was found in either the block instance or the store.
if ( result !== undefined ) {
- if ( PATHS_WITH_MERGE[ normalizedPath ] ) {
- return mergeOrigins( result );
+ if ( PATHS_WITH_OVERRIDE[ normalizedPath ] ) {
+ return overrideOrigins( result );
}
return result;
}
diff --git a/packages/block-editor/src/store/private-actions.js b/packages/block-editor/src/store/private-actions.js
index d1db598c8c8e65..ae0a06152fb933 100644
--- a/packages/block-editor/src/store/private-actions.js
+++ b/packages/block-editor/src/store/private-actions.js
@@ -140,6 +140,15 @@ export const privateRemoveBlocks =
}
if ( rules[ 'bindings/core/pattern-overrides' ] ) {
+ const parentPatternBlocks =
+ select.getBlockParentsByBlockName(
+ clientId,
+ 'core/block'
+ );
+ // We only need to run this check when editing the original pattern, not pattern instances.
+ if ( parentPatternBlocks?.length > 0 ) {
+ continue;
+ }
const blockAttributes =
select.getBlockAttributes( clientId );
if (
diff --git a/packages/block-library/src/button/block.json b/packages/block-library/src/button/block.json
index c3d51c61a0999a..ec9f042cf5bcf7 100644
--- a/packages/block-library/src/button/block.json
+++ b/packages/block-library/src/button/block.json
@@ -8,7 +8,6 @@
"description": "Prompt visitors to take action with a button-style link.",
"keywords": [ "link" ],
"textdomain": "default",
- "usesContext": [ "pattern/overrides" ],
"attributes": {
"tagName": {
"type": "string",
diff --git a/packages/block-library/src/footnotes/index.php b/packages/block-library/src/footnotes/index.php
index 0cd2ad73ef3d42..86eaf694add4ca 100644
--- a/packages/block-library/src/footnotes/index.php
+++ b/packages/block-library/src/footnotes/index.php
@@ -68,15 +68,30 @@ function render_block_core_footnotes( $attributes, $content, $block ) {
* @since 6.3.0
*/
function register_block_core_footnotes() {
- $post_types = get_post_types(
+ register_block_type_from_metadata(
+ __DIR__ . '/footnotes',
array(
- 'show_in_rest' => true,
- 'public' => true,
+ 'render_callback' => 'render_block_core_footnotes',
)
);
+}
+add_action( 'init', 'register_block_core_footnotes' );
+
+
+/**
+ * Registers the footnotes meta field required for footnotes to work.
+ *
+ * @since 6.5.0
+ */
+function register_block_core_footnotes_post_meta() {
+ $post_types = get_post_types( array( 'show_in_rest' => true ) );
foreach ( $post_types as $post_type ) {
// Only register the meta field if the post type supports the editor, custom fields, and revisions.
- if ( post_type_supports( $post_type, 'editor' ) && post_type_supports( $post_type, 'custom-fields' ) && post_type_supports( $post_type, 'revisions' ) ) {
+ if (
+ post_type_supports( $post_type, 'editor' ) &&
+ post_type_supports( $post_type, 'custom-fields' ) &&
+ post_type_supports( $post_type, 'revisions' )
+ ) {
register_post_meta(
$post_type,
'footnotes',
@@ -89,14 +104,12 @@ function register_block_core_footnotes() {
);
}
}
- register_block_type_from_metadata(
- __DIR__ . '/footnotes',
- array(
- 'render_callback' => 'render_block_core_footnotes',
- )
- );
}
-add_action( 'init', 'register_block_core_footnotes' );
+/**
+ * Most post types are registered at priority 10, so use priority 20 here in
+ * order to catch them.
+*/
+add_action( 'init', 'register_block_core_footnotes_post_meta', 20 );
/**
* Adds the footnotes field to the revisions display.
diff --git a/packages/block-library/src/heading/block.json b/packages/block-library/src/heading/block.json
index 90ef0d383af2c5..9990ef582e2f43 100644
--- a/packages/block-library/src/heading/block.json
+++ b/packages/block-library/src/heading/block.json
@@ -7,7 +7,6 @@
"description": "Introduce new sections and organize content to help visitors (and search engines) understand the structure of your content.",
"keywords": [ "title", "subtitle" ],
"textdomain": "default",
- "usesContext": [ "pattern/overrides" ],
"attributes": {
"textAlign": {
"type": "string"
diff --git a/packages/block-library/src/image/block.json b/packages/block-library/src/image/block.json
index 7a46a34db591f0..1076aad0f17982 100644
--- a/packages/block-library/src/image/block.json
+++ b/packages/block-library/src/image/block.json
@@ -4,12 +4,7 @@
"name": "core/image",
"title": "Image",
"category": "media",
- "usesContext": [
- "allowResize",
- "imageCrop",
- "fixedHeight",
- "pattern/overrides"
- ],
+ "usesContext": [ "allowResize", "imageCrop", "fixedHeight" ],
"description": "Insert an image to make a visual statement.",
"keywords": [ "img", "photo", "picture" ],
"textdomain": "default",
diff --git a/packages/block-library/src/image/editor.scss b/packages/block-library/src/image/editor.scss
index ded3768dfa7d38..d3fafcfcab4eca 100644
--- a/packages/block-library/src/image/editor.scss
+++ b/packages/block-library/src/image/editor.scss
@@ -181,3 +181,8 @@ figure.wp-block-image:not(.wp-block) {
padding-right: 0;
}
}
+
+.wp-block-image__toolbar_content_textarea {
+ // Corresponds to the size of the textarea in the block inspector.
+ width: 250px;
+}
diff --git a/packages/block-library/src/image/image.js b/packages/block-library/src/image/image.js
index f551d8df007a8e..62184e6522adb9 100644
--- a/packages/block-library/src/image/image.js
+++ b/packages/block-library/src/image/image.js
@@ -10,6 +10,7 @@ import {
TextControl,
ToolbarButton,
ToolbarGroup,
+ Dropdown,
__experimentalToolsPanel as ToolsPanel,
__experimentalToolsPanelItem as ToolsPanelItem,
__experimentalUseCustomUnits as useCustomUnits,
@@ -30,6 +31,7 @@ import {
} from '@wordpress/block-editor';
import { useEffect, useMemo, useState, useRef } from '@wordpress/element';
import { __, _x, sprintf, isRTL } from '@wordpress/i18n';
+import { DOWN } from '@wordpress/keycodes';
import { getFilename } from '@wordpress/url';
import { switchToBlockType } from '@wordpress/blocks';
import { crop, overlayText, upload } from '@wordpress/icons';
@@ -177,6 +179,7 @@ export default function Image( {
const [ externalBlob, setExternalBlob ] = useState();
const clientWidth = useClientWidth( containerRef, [ align ] );
const hasNonContentControls = blockEditingMode === 'default';
+ const isContentOnlyMode = blockEditingMode === 'contentOnly';
const isResizable =
allowResize &&
hasNonContentControls &&
@@ -505,6 +508,113 @@ export default function Image( {
) }
+ { isContentOnlyMode && (
+ // Add some extra controls for content attributes when content only mode is active.
+ // With content only mode active, the inspector is hidden, so users need another way
+ // to edit these attributes.
+
+ (
+ {
+ if ( ! isOpen && event.keyCode === DOWN ) {
+ event.preventDefault();
+ onToggle();
+ }
+ } }
+ >
+ { _x(
+ 'Alt',
+ 'Alternative text for an image. Block toolbar label, a low character count is preferred.'
+ ) }
+
+ ) }
+ renderContent={ () => (
+
+ { __(
+ 'Connected to a custom field'
+ ) }
+ >
+ ) : (
+ <>
+
+ { __(
+ 'Describe the purpose of the image.'
+ ) }
+
+
+ { __(
+ 'Leave empty if decorative.'
+ ) }
+ >
+ )
+ }
+ __nextHasNoMarginBottom
+ />
+ ) }
+ />
+ (
+ {
+ if ( ! isOpen && event.keyCode === DOWN ) {
+ event.preventDefault();
+ onToggle();
+ }
+ } }
+ >
+ { __( 'Title' ) }
+
+ ) }
+ renderContent={ () => (
+
+ { __(
+ 'Connected to a custom field'
+ ) }
+ >
+ ) : (
+ <>
+ { __(
+ 'Describe the role of this image on the page.'
+ ) }
+
+ { __(
+ '(Note: many devices and browsers do not display this text.)'
+ ) }
+
+ >
+ )
+ }
+ />
+ ) }
+ />
+
+ ) }
@@ -566,7 +676,7 @@ export default function Image( {
label={ __( 'Title attribute' ) }
value={ title || '' }
onChange={ onSetTitle }
- disabled={ lockTitleControls }
+ readOnly={ lockTitleControls }
help={
lockTitleControls ? (
<>{ __( 'Connected to a custom field' ) }>
diff --git a/packages/block-library/src/navigation-link/index.php b/packages/block-library/src/navigation-link/index.php
index afd34dcae779ba..4ed54fcc09bf40 100644
--- a/packages/block-library/src/navigation-link/index.php
+++ b/packages/block-library/src/navigation-link/index.php
@@ -392,7 +392,6 @@ function block_core_navigation_link_build_variations() {
* Registers the navigation link block.
*
* @uses render_block_core_navigation_link()
- * @uses build_navigation_link_block_variations()
* @throws WP_Error An WP_Error exception parsing the block definition.
*/
function register_block_core_navigation_link() {
diff --git a/packages/block-library/src/navigation/constants.js b/packages/block-library/src/navigation/constants.js
index ff13309d1e4e78..154c490e83839b 100644
--- a/packages/block-library/src/navigation/constants.js
+++ b/packages/block-library/src/navigation/constants.js
@@ -23,5 +23,3 @@ export const SELECT_NAVIGATION_MENUS_ARGS = [
'wp_navigation',
PRELOADED_NAVIGATION_MENUS_QUERY,
];
-
-export const NAVIGATION_MOBILE_COLLAPSE = '600px';
diff --git a/packages/block-library/src/navigation/edit/index.js b/packages/block-library/src/navigation/edit/index.js
index 14ef6fc73d48f0..3cacd814119e6f 100644
--- a/packages/block-library/src/navigation/edit/index.js
+++ b/packages/block-library/src/navigation/edit/index.js
@@ -42,7 +42,7 @@ import {
import { __, sprintf } from '@wordpress/i18n';
import { speak } from '@wordpress/a11y';
import { close, Icon } from '@wordpress/icons';
-import { useInstanceId, useMediaQuery } from '@wordpress/compose';
+import { useInstanceId } from '@wordpress/compose';
/**
* Internal dependencies
@@ -71,7 +71,6 @@ import MenuInspectorControls from './menu-inspector-controls';
import DeletedNavigationWarning from './deleted-navigation-warning';
import AccessibleDescription from './accessible-description';
import AccessibleMenuDescription from './accessible-menu-description';
-import { NAVIGATION_MOBILE_COLLAPSE } from '../constants';
import { unlock } from '../../lock-unlock';
function Navigation( {
@@ -298,14 +297,6 @@ function Navigation( {
[ clientId ]
);
const isResponsive = 'never' !== overlayMenu;
- const isMobileBreakPoint = useMediaQuery(
- `(max-width: ${ NAVIGATION_MOBILE_COLLAPSE })`
- );
-
- const isCollapsed =
- ( 'mobile' === overlayMenu && isMobileBreakPoint ) ||
- 'always' === overlayMenu;
-
const blockProps = useBlockProps( {
ref: navRef,
className: classnames(
@@ -319,7 +310,6 @@ function Navigation( {
'is-vertical': orientation === 'vertical',
'no-wrap': flexWrap === 'nowrap',
'is-responsive': isResponsive,
- 'is-collapsed': isCollapsed,
'has-text-color': !! textColor.color || !! textColor?.class,
[ getColorClassName( 'color', textColor?.slug ) ]:
!! textColor?.slug,
diff --git a/packages/block-library/src/navigation/editor.scss b/packages/block-library/src/navigation/editor.scss
index eb796ae6965412..107fb6e6de5fd5 100644
--- a/packages/block-library/src/navigation/editor.scss
+++ b/packages/block-library/src/navigation/editor.scss
@@ -429,7 +429,7 @@ $color-control-label-height: 20px;
// These needs extra specificity in the editor.
.wp-block-navigation__responsive-container:not(.is-menu-open) {
.components-button.wp-block-navigation__responsive-container-close {
- .is-collapsed & {
+ @include break-small {
display: none;
}
}
diff --git a/packages/block-library/src/navigation/index.php b/packages/block-library/src/navigation/index.php
index f1292e5d2e723a..5dcada62f6feb5 100644
--- a/packages/block-library/src/navigation/index.php
+++ b/packages/block-library/src/navigation/index.php
@@ -218,7 +218,7 @@ private static function get_inner_blocks_from_navigation_post( $attributes ) {
// it encounters whitespace. This code strips it.
$blocks = block_core_navigation_filter_out_empty_blocks( $parsed_blocks );
- if ( function_exists( 'get_hooked_block_markup' ) ) {
+ if ( function_exists( 'set_ignored_hooked_blocks_metadata' ) ) {
// Run Block Hooks algorithm to inject hooked blocks.
$markup = block_core_navigation_insert_hooked_blocks( $blocks, $navigation_post );
$root_nav_block = parse_blocks( $markup )[0];
@@ -390,25 +390,16 @@ private static function get_classes( $attributes ) {
$text_decoration = $attributes['style']['typography']['textDecoration'] ?? null;
$text_decoration_class = sprintf( 'has-text-decoration-%s', $text_decoration );
- // Sets the is-collapsed class when the navigation is set to always use the overlay.
- // This saves us from needing to do this check in the view.js file (see the collapseNav function).
- $is_collapsed_class = static::is_always_overlay( $attributes ) ? array( 'is-collapsed' ) : array();
-
$classes = array_merge(
$colors['css_classes'],
$font_sizes['css_classes'],
$is_responsive_menu ? array( 'is-responsive' ) : array(),
$layout_class ? array( $layout_class ) : array(),
- $text_decoration ? array( $text_decoration_class ) : array(),
- $is_collapsed_class
+ $text_decoration ? array( $text_decoration_class ) : array()
);
return implode( ' ', $classes );
}
- private static function is_always_overlay( $attributes ) {
- return isset( $attributes['overlayMenu'] ) && 'always' === $attributes['overlayMenu'];
- }
-
/**
* Get styles for the navigation block.
*
@@ -435,12 +426,16 @@ private static function get_responsive_container_markup( $attributes, $inner_blo
$colors = block_core_navigation_build_css_colors( $attributes );
$modal_unique_id = wp_unique_id( 'modal-' );
+ $is_hidden_by_default = isset( $attributes['overlayMenu'] ) && 'always' === $attributes['overlayMenu'];
+
$responsive_container_classes = array(
'wp-block-navigation__responsive-container',
+ $is_hidden_by_default ? 'hidden-by-default' : '',
implode( ' ', $colors['overlay_css_classes'] ),
);
$open_button_classes = array(
'wp-block-navigation__responsive-container-open',
+ $is_hidden_by_default ? 'always-shown' : '',
);
$should_display_icon_label = isset( $attributes['hasIcon'] ) && true === $attributes['hasIcon'];
@@ -538,7 +533,7 @@ private static function get_nav_wrapper_attributes( $attributes, $inner_blocks )
);
if ( $is_responsive_menu ) {
- $nav_element_directives = static::get_nav_element_directives( $is_interactive, $attributes );
+ $nav_element_directives = static::get_nav_element_directives( $is_interactive );
$wrapper_attributes .= ' ' . $nav_element_directives;
}
@@ -552,7 +547,7 @@ private static function get_nav_wrapper_attributes( $attributes, $inner_blocks )
* @param array $attributes The block attributes.
* @return string the directives for the navigation element.
*/
- private static function get_nav_element_directives( $is_interactive, $attributes ) {
+ private static function get_nav_element_directives( $is_interactive ) {
if ( ! $is_interactive ) {
return '';
}
@@ -569,16 +564,6 @@ private static function get_nav_element_directives( $is_interactive, $attributes
data-wp-interactive="core/navigation"'
. $nav_element_context;
- /*
- * When the navigation's 'overlayMenu' attribute is set to 'always', JavaScript
- * is not needed for collapsing the menu because the class is set manually.
- */
- if ( ! static::is_always_overlay( $attributes ) ) {
- $nav_element_directives .= 'data-wp-init="callbacks.initNav"';
- $nav_element_directives .= ' '; // space separator
- $nav_element_directives .= 'data-wp-class--is-collapsed="context.isCollapsed"';
- }
-
return $nav_element_directives;
}
@@ -1024,7 +1009,7 @@ function block_core_navigation_get_fallback_blocks() {
// In this case default to the (Page List) fallback.
$fallback_blocks = ! empty( $maybe_fallback ) ? $maybe_fallback : $fallback_blocks;
- if ( function_exists( 'get_hooked_block_markup' ) ) {
+ if ( function_exists( 'set_ignored_hooked_blocks_metadata' ) ) {
// Run Block Hooks algorithm to inject hooked blocks.
// We have to run it here because we need the post ID of the Navigation block to track ignored hooked blocks.
$markup = block_core_navigation_insert_hooked_blocks( $fallback_blocks, $navigation_post );
@@ -1369,25 +1354,28 @@ function block_core_navigation_get_most_recently_published_navigation() {
}
/**
- * Insert hooked blocks into a Navigation block.
- *
- * Given a Navigation block's inner blocks and its corresponding `wp_navigation` post object,
- * this function inserts hooked blocks into it, and returns the serialized inner blocks in a
- * mock Navigation block wrapper.
+ * Accepts the serialized markup of a block and its inner blocks, and returns serialized markup of the inner blocks.
*
- * If there are any hooked blocks that need to be inserted as the Navigation block's first or last
- * children, the `wp_navigation` post's `_wp_ignored_hooked_blocks` meta is checked to see if any
- * of those hooked blocks should be exempted from insertion.
+ * @param string $serialized_block The serialized markup of a block and its inner blocks.
+ * @return string
+ */
+function block_core_navigation_remove_serialized_parent_block( $serialized_block ) {
+ $start = strpos( $serialized_block, '-->' ) + strlen( '-->' );
+ $end = strrpos( $serialized_block, '' ) + strlen( '-->' );
- $end = strrpos( $content, '
+
+`,
+ status: 'publish',
+ } );
+
+ await admin.createNewPost();
+
+ await editor.insertBlock( {
+ name: 'core/block',
+ attributes: { ref: id },
+ } );
+
+ const imageBlock = editor.canvas.getByRole( 'document', {
+ name: 'Block: Image',
+ } );
+ await editor.selectBlocks( imageBlock );
+ await imageBlock
+ .getByTestId( 'form-file-upload-input' )
+ .setInputFiles( TEST_IMAGE_FILE_PATH );
+ await expect( imageBlock.getByRole( 'img' ) ).toHaveCount( 1 );
+ await expect( imageBlock.getByRole( 'img' ) ).toHaveAttribute(
+ 'src',
+ /\/wp-content\/uploads\//
+ );
+
+ await editor.publishPost();
+ await page.reload();
+
+ await editor.selectBlocks( imageBlock );
+ await editor.showBlockToolbar();
+ const blockToolbar = page.getByRole( 'toolbar', {
+ name: 'Block tools',
+ } );
+ await expect( imageBlock.getByRole( 'img' ) ).toHaveAttribute(
+ 'src',
+ /\/wp-content\/uploads\//
+ );
+ await expect(
+ blockToolbar.getByRole( 'button', { name: 'Replace' } )
+ ).toBeEnabled();
+ await expect(
+ blockToolbar.getByRole( 'button', {
+ name: 'Upload to Media Library',
+ } )
+ ).toBeHidden();
+ } );
} );
diff --git a/test/e2e/specs/site-editor/navigation.spec.js b/test/e2e/specs/site-editor/navigation.spec.js
new file mode 100644
index 00000000000000..25a5b5dee59ffb
--- /dev/null
+++ b/test/e2e/specs/site-editor/navigation.spec.js
@@ -0,0 +1,119 @@
+/**
+ * WordPress dependencies
+ */
+const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' );
+
+test.use( {
+ editorNavigationUtils: async ( { page, pageUtils }, use ) => {
+ await use( new EditorNavigationUtils( { page, pageUtils } ) );
+ },
+} );
+
+test.describe( 'Site editor navigation', () => {
+ test.beforeAll( async ( { requestUtils } ) => {
+ await requestUtils.activateTheme( 'emptytheme' );
+ } );
+
+ test.afterAll( async ( { requestUtils } ) => {
+ await requestUtils.activateTheme( 'twentytwentyone' );
+ } );
+
+ test( 'Can use keyboard to navigate the site editor', async ( {
+ admin,
+ editorNavigationUtils,
+ page,
+ pageUtils,
+ } ) => {
+ await admin.visitSiteEditor();
+
+ // Test: Can navigate to a sidebar item and into its subnavigation frame without losing focus
+ // Go to the Pages button
+
+ await editorNavigationUtils.tabToLabel( 'Pages' );
+
+ await expect(
+ page.getByRole( 'button', { name: 'Pages' } )
+ ).toBeFocused();
+ await pageUtils.pressKeys( 'Enter' );
+ // We should be in the Pages sidebar
+ await expect(
+ page.getByRole( 'button', { name: 'Back', exact: true } )
+ ).toBeFocused();
+ await pageUtils.pressKeys( 'Enter' );
+ // Go back to the main navigation
+ await expect(
+ page.getByRole( 'button', { name: 'Pages' } )
+ ).toBeFocused();
+
+ // Test: Can navigate into the iframe using the keyboard
+ await editorNavigationUtils.tabToLabel( 'Editor Canvas' );
+ const editorCanvasButton = page.getByRole( 'button', {
+ name: 'Editor Canvas',
+ } );
+ await expect( editorCanvasButton ).toBeFocused();
+ // Enter into the site editor frame
+ await pageUtils.pressKeys( 'Enter' );
+ // Focus should be on the iframe without the button role
+ await expect(
+ page.locator( 'iframe[name="editor-canvas"]' )
+ ).toBeFocused();
+ // The button role should have been removed from the iframe.
+ await expect( editorCanvasButton ).toBeHidden();
+
+ // Test to make sure a Tab keypress works as expected.
+ // As of this writing, we are in select mode and a tab
+ // keypress will reveal the header template select mode
+ // button. This test is not documenting that we _want_
+ // that action, but checking that we are within the site
+ // editor and keypresses work as intened.
+ await pageUtils.pressKeys( 'Tab' );
+ await expect(
+ page.getByRole( 'button', {
+ name: 'Template Part Block. Row 1. header',
+ } )
+ ).toBeFocused();
+
+ // Test: We can go back to the main navigation from the editor frame
+ // Move to the document toolbar
+ await pageUtils.pressKeys( 'alt+F10' );
+ // Go to the open navigation button
+ await pageUtils.pressKeys( 'shift+Tab' );
+
+ // Open the sidebar again
+ await expect(
+ page.getByRole( 'button', {
+ name: 'Open Navigation',
+ exact: true,
+ } )
+ ).toBeFocused();
+ await pageUtils.pressKeys( 'Enter' );
+
+ await expect(
+ page.getByLabel( 'Go to the Dashboard' ).first()
+ ).toBeFocused();
+ // We should have our editor canvas button back
+ await expect( editorCanvasButton ).toBeVisible();
+ } );
+} );
+
+class EditorNavigationUtils {
+ constructor( { page, pageUtils } ) {
+ this.page = page;
+ this.pageUtils = pageUtils;
+ }
+
+ async tabToLabel( label, times = 10 ) {
+ for ( let i = 0; i < times; i++ ) {
+ await this.pageUtils.pressKeys( 'Tab' );
+ const activeLabel = await this.page.evaluate( () => {
+ return (
+ document.activeElement.getAttribute( 'aria-label' ) ||
+ document.activeElement.textContent
+ );
+ } );
+ if ( activeLabel === label ) {
+ return;
+ }
+ }
+ }
+}
diff --git a/test/e2e/specs/site-editor/user-global-styles-revisions.spec.js b/test/e2e/specs/site-editor/user-global-styles-revisions.spec.js
index c8a87ecbeb69e8..9f5c9c8e36b221 100644
--- a/test/e2e/specs/site-editor/user-global-styles-revisions.spec.js
+++ b/test/e2e/specs/site-editor/user-global-styles-revisions.spec.js
@@ -56,8 +56,11 @@ test.describe( 'Style Revisions', () => {
// Shows changes made in the revision.
await expect(
- page.getByTestId( 'global-styles-revision-changes' )
- ).toHaveText( 'Colors.' );
+ page
+ .getByTestId( 'global-styles-revision-changes' )
+ .getByRole( 'listitem' )
+ .first()
+ ).toHaveText( 'Colors styles.' );
// There should be 2 revisions not including the reset to theme defaults button.
await expect( revisionButtons ).toHaveCount(