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(