diff --git a/bin/packages/check-build-type-declaration-files.js b/bin/packages/check-build-type-declaration-files.js new file mode 100644 index 0000000000000..cd8bad2a70b53 --- /dev/null +++ b/bin/packages/check-build-type-declaration-files.js @@ -0,0 +1,121 @@ +/** + * This script verifies the published index.d.ts file for every package which both + * builds types and also sets checkJs to false in its tsconfig.json. (This scenario + * can cause unchecked errors in JS files to be included in the compiled types.) + * + * We do so by running `tsc --noEmit` on the $package/build-types/index.d.ts file. + * This also verifies everything index.d.ts references, so it checks the entire + * public api of the type declarations for that package. + * + * @see https://github.com/WordPress/gutenberg/pull/49650 for more discussion. + */ + +/** + * External dependencies + */ +const fs = require( 'fs' ).promises; +const path = require( 'path' ); +const { exec } = require( 'child_process' ); +const chalk = require( 'chalk' ); + +/** + * Returns whether a package needs its compiled types to be double-checked. This + * needs to happen when both of these are true: + * 1. The package compiles types. (It has a tsconfig file.) + * 2. The tsconfig sets checkJs to false. + * + * NOTE: In the future, if we run into issues parsing JSON, we should migrate to + * a proper json5 parser, such as the json5 npm package. The current regex just + * handles comments, which at the time is the only thing we use from JSON5. + * + * @param {string} packagePath Path to the package. + * @return {boolean} whether or not the package checksJs. + */ +async function packageNeedsExtraCheck( packagePath ) { + const configPath = path.join( packagePath, 'tsconfig.json' ); + + try { + const tsconfigRaw = await fs.readFile( configPath, 'utf-8' ); + // Removes comments from the JSON5 string to convert it to plain JSON. + const jsonString = tsconfigRaw.replace( /\s+\/\/.*$/gm, '' ); + const config = JSON.parse( jsonString ); + + // If checkJs both exists and is false, then we need the extra check. + return config.compilerOptions?.checkJs === false; + } catch ( e ) { + if ( e.code !== 'ENOENT' ) { + throw e; + } + + // No tsconfig means no checkJs + return false; + } +} + +// Returns the path to the build-types declaration file for a package if it exists. +// Throws an error and exits the script otherwise. +async function getDecFile( packagePath ) { + const decFile = path.join( packagePath, 'build-types', 'index.d.ts' ); + try { + await fs.access( decFile ); + return decFile; + } catch ( err ) { + console.error( + `Cannot access this declaration file. You may need to run tsc again: ${ decFile }` + ); + process.exit( 1 ); + } +} + +async function typecheckDeclarations( file ) { + return new Promise( ( resolve, reject ) => { + exec( `npx tsc --noEmit ${ file }`, ( error, stdout, stderr ) => { + if ( error ) { + reject( { file, error, stderr, stdout } ); + } else { + resolve( { file, stdout } ); + } + } ); + } ); +} + +async function checkUnverifiedDeclarationFiles() { + const packageDir = path.resolve( 'packages' ); + const packageDirs = ( + await fs.readdir( packageDir, { withFileTypes: true } ) + ) + .filter( ( dirent ) => dirent.isDirectory() ) + .map( ( dirent ) => path.join( packageDir, dirent.name ) ); + + // Finds the compiled type declarations for each package which both checks + // types and has checkJs disabled. + const declarations = ( + await Promise.all( + packageDirs.map( async ( pkg ) => + ( await packageNeedsExtraCheck( pkg ) ) + ? getDecFile( pkg ) + : null + ) + ) + ).filter( Boolean ); + + const tscResults = await Promise.allSettled( + declarations.map( typecheckDeclarations ) + ); + + tscResults.forEach( ( { status, reason } ) => { + if ( status !== 'fulfilled' ) { + console.error( + chalk.red( + `Incorrect published types for ${ reason.file }:\n` + ), + reason.stdout + ); + } + } ); + + if ( tscResults.some( ( { status } ) => status !== 'fulfilled' ) ) { + process.exit( 1 ); + } +} +checkUnverifiedDeclarationFiles(); diff --git a/changelog.txt b/changelog.txt index 24ef23da801f4..4925a76895653 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,30 @@ == Changelog == += 15.6.0-rc.3 = + + + +## Changelog + +### Bug Fixes + +#### Site Editor +- Fix the site editor loading in multi-site installs. ([49861](https://github.com/WordPress/gutenberg/pull/49861)) + + +## First time contributors + +The following PRs were merged by first time contributors: + + + +## Contributors + +The following contributors merged PRs in this release: + +@youknowriad + + = 15.6.0-rc.2 = diff --git a/docs/contributors/release-screenshot.png b/docs/contributors/release-screenshot.png deleted file mode 100644 index 63334f489c0b2..0000000000000 Binary files a/docs/contributors/release-screenshot.png and /dev/null differ diff --git a/gutenberg.php b/gutenberg.php index b968de94f30d8..bf0b5e01528ae 100644 --- a/gutenberg.php +++ b/gutenberg.php @@ -5,7 +5,7 @@ * Description: Printing since 1440. This is the development plugin for the block editor, site editor, and other future WordPress core functionality. * Requires at least: 6.0 * Requires PHP: 5.6 - * Version: 15.6.0-rc.2 + * Version: 15.6.0-rc.3 * Author: Gutenberg Team * Text Domain: gutenberg * diff --git a/lib/block-supports/duotone.php b/lib/block-supports/duotone.php index 28bb579efa8fb..153a96cf128a8 100644 --- a/lib/block-supports/duotone.php +++ b/lib/block-supports/duotone.php @@ -5,6 +5,41 @@ * @package gutenberg */ +// Register duotone block supports. +WP_Block_Supports::get_instance()->register( + 'duotone', + array( + 'register_attribute' => array( 'WP_Duotone_Gutenberg', 'register_duotone_support' ), + ) +); + +// Set up metadata prior to rendering any blocks. +add_action( 'wp_loaded', array( 'WP_Duotone_Gutenberg', 'set_global_styles_presets' ), 10 ); +add_action( 'wp_loaded', array( 'WP_Duotone_Gutenberg', 'set_global_style_block_names' ), 10 ); + +// Remove WordPress core filter to avoid rendering duplicate support elements. +remove_filter( 'render_block', 'wp_render_duotone_support', 10, 2 ); +add_filter( 'render_block', array( 'WP_Duotone_Gutenberg', 'render_duotone_support' ), 10, 2 ); + +// Enqueue styles. +// Block styles (core-block-supports-inline-css) before the style engine (gutenberg_enqueue_stored_styles). +// Global styles (global-styles-inline-css) after the other global styles (gutenberg_enqueue_global_styles). +add_action( 'wp_enqueue_scripts', array( 'WP_Duotone_Gutenberg', 'output_block_styles' ), 9 ); +add_action( 'wp_enqueue_scripts', array( 'WP_Duotone_Gutenberg', 'output_global_styles' ), 11 ); + +// Add SVG filters to the footer. Also, for classic themes, output block styles (core-block-supports-inline-css). +add_action( 'wp_footer', array( 'WP_Duotone_Gutenberg', 'output_footer_assets' ), 10 ); + +// Add styles and SVGs for use in the editor via the EditorStyles component. +add_filter( 'block_editor_settings_all', array( 'WP_Duotone_Gutenberg', 'add_editor_settings' ), 10 ); + +// Migrate the old experimental duotone support flag. +add_filter( 'block_type_metadata_settings', array( 'WP_Duotone_Gutenberg', 'migrate_experimental_duotone_support_flag' ), 10, 2 ); + +/* + * Deprecated functions below. All new functions should be added in class-wp-duotone-gutenberg.php. + */ + /** * Direct port of tinycolor's bound01 function, lightly simplified to maintain * consistency with tinycolor. @@ -317,29 +352,27 @@ function gutenberg_tinycolor_string_to_rgb( $color_str ) { /** * Returns the prefixed id for the duotone filter for use as a CSS id. * + * @deprecated 6.3.0 + * * @param array $preset Duotone preset value as seen in theme.json. * @return string Duotone filter CSS id. */ function gutenberg_get_duotone_filter_id( $preset ) { - if ( ! isset( $preset['slug'] ) ) { - return ''; - } - - return 'wp-duotone-' . $preset['slug']; + _deprecated_function( __FUNCTION__, '6.3.0' ); + return WP_Duotone_Gutenberg::get_filter_id_from_preset( $preset ); } /** * Returns the CSS filter property url to reference the rendered SVG. * + * @deprecated 6.3.0 + * * @param array $preset Duotone preset value as seen in theme.json. * @return string Duotone CSS filter property url value. */ function gutenberg_get_duotone_filter_property( $preset ) { - if ( isset( $preset['colors'] ) && is_string( $preset['colors'] ) ) { - return $preset['colors']; - } - $filter_id = gutenberg_get_duotone_filter_id( $preset ); - return "url('#" . $filter_id . "')"; + _deprecated_function( __FUNCTION__, '6.3.0' ); + return WP_Duotone_Gutenberg::get_filter_css_property_value_from_preset( $preset ); } /** @@ -358,56 +391,25 @@ function gutenberg_get_duotone_filter_svg( $preset ) { /** * Registers the style and colors block attributes for block types that support it. * + * @deprecated 6.3.0 Use WP_Duotone_Gutenberg::register_duotone_support() instead. + * * @param WP_Block_Type $block_type Block Type. */ function gutenberg_register_duotone_support( $block_type ) { - $has_duotone_support = false; - if ( property_exists( $block_type, 'supports' ) ) { - // Previous `color.__experimentalDuotone` support flag is migrated - // to `filter.duotone` via `block_type_metadata_settings` filter. - $has_duotone_support = _wp_array_get( $block_type->supports, array( 'filter', 'duotone' ), null ); - } - - if ( $has_duotone_support ) { - if ( ! $block_type->attributes ) { - $block_type->attributes = array(); - } - - if ( ! array_key_exists( 'style', $block_type->attributes ) ) { - $block_type->attributes['style'] = array( - 'type' => 'object', - ); - } - } + _deprecated_function( __FUNCTION__, '6.3.0', 'WP_Duotone_Gutenberg::register_duotone_support' ); + return WP_Duotone_Gutenberg::register_duotone_support( $block_type ); } /** * Render out the duotone stylesheet and SVG. * + * @deprecated 6.3.0 Use WP_Duotone_Gutenberg::render_duotone_support() instead. + * * @param string $block_content Rendered block content. * @param array $block Block object. - * @deprecated 6.3.0 Use WP_Duotone_Gutenberg::render_duotone_support() instead. * @return string Filtered block content. */ function gutenberg_render_duotone_support( $block_content, $block ) { _deprecated_function( __FUNCTION__, '6.3.0', 'WP_Duotone_Gutenberg::render_duotone_support' ); return WP_Duotone_Gutenberg::render_duotone_support( $block_content, $block ); } - -// Register the block support. -WP_Block_Supports::get_instance()->register( - 'duotone', - array( - 'register_attribute' => 'gutenberg_register_duotone_support', - ) -); - -add_action( 'wp_loaded', array( 'WP_Duotone_Gutenberg', 'set_global_styles_presets' ), 10 ); -add_action( 'wp_loaded', array( 'WP_Duotone_Gutenberg', 'set_global_style_block_names' ), 10 ); -// Remove WordPress core filter to avoid rendering duplicate support elements. -remove_filter( 'render_block', 'wp_render_duotone_support', 10, 2 ); -add_filter( 'render_block', array( 'WP_Duotone_Gutenberg', 'render_duotone_support' ), 10, 2 ); -add_action( 'wp_enqueue_scripts', array( 'WP_Duotone_Gutenberg', 'output_global_styles' ), 11 ); -add_action( 'wp_footer', array( 'WP_Duotone_Gutenberg', 'output_footer_assets' ), 10 ); -add_filter( 'block_editor_settings_all', array( 'WP_Duotone_Gutenberg', 'add_editor_settings' ), 10 ); -add_filter( 'block_type_metadata_settings', array( 'WP_Duotone_Gutenberg', 'migrate_experimental_duotone_support_flag' ), 10, 2 ); diff --git a/lib/class-wp-duotone-gutenberg.php b/lib/class-wp-duotone-gutenberg.php index 62532ac9a6d03..970840b683d27 100644 --- a/lib/class-wp-duotone-gutenberg.php +++ b/lib/class-wp-duotone-gutenberg.php @@ -74,17 +74,17 @@ class WP_Duotone_Gutenberg { /** * An array of Duotone SVG and CSS output needed for the frontend duotone rendering based on what is - * being output on the page. Organized by a slug of the preset/color group and the information needed + * being output on the page. Organized by an id of the preset/color group and the information needed * to generate the SVG and CSS at render. * * Example: * [ - * 'blue-orange' => [ + * 'wp-duotone-blue-orange' => [ * 'slug' => 'blue-orange', * 'colors' => [ '#0000ff', '#ffcc00' ], * ], * 'wp-duotone-000000-ffffff-2' => [ - * 'slug' => 'wp-duotone-000000-ffffff-2', + * 'slug' => '000000-ffffff-2', * 'colors' => [ '#000000', '#ffffff' ], * ], * ] @@ -92,13 +92,53 @@ class WP_Duotone_Gutenberg { * @since 6.3.0 * @var array */ - private static $output = array(); + private static $used_svg_filter_data = array(); + + /** + * All of the duotone filter data from presets for CSS custom properties on + * the page. + * + * Example: + * [ + * 'wp-duotone-blue-orange' => [ + * 'slug' => 'blue-orange', + * 'colors' => [ '#0000ff', '#ffcc00' ], + * ], + * … + * ] + * + * @var array + */ + private static $used_global_styles_presets = array(); + + /** + * All of the block CSS declarations for styles on the page. + * + * Example: + * [ + * [ + * 'selector' => '.wp-duotone-000000-ffffff-2.wp-block-image img', + * 'declarations' => [ + * 'filter' => 'url(#wp-duotone-000000-ffffff-2)', + * ], + * ], + * … + * ] + * + * @var array + */ + private static $block_css_declarations = array(); /** * Prefix used for generating and referencing duotone CSS custom properties. */ const CSS_VAR_PREFIX = '--wp--preset--duotone--'; + /** + * Prefix used for generating and referencing duotone filter IDs. + */ + const FILTER_ID_PREFIX = 'wp-duotone-'; + /** * Direct port of colord's clamp function. Using min/max instead of * nested ternaries. @@ -425,10 +465,9 @@ public static function set_global_styles_presets() { foreach ( $presets_by_origin as $presets ) { foreach ( $presets as $preset ) { - self::$global_styles_presets[ _wp_to_kebab_case( $preset['slug'] ) ] = array( - 'slug' => $preset['slug'], - 'colors' => $preset['colors'], - ); + $filter_id = self::get_filter_id( _wp_to_kebab_case( $preset['slug'] ) ); + + self::$global_styles_presets[ $filter_id ] = $preset; } } } @@ -486,9 +525,10 @@ private static function gutenberg_get_slug_from_attr( $duotone_attr ) { * @return bool True if the duotone preset present and valid. */ private static function is_preset( $duotone_attr ) { - $slug = self::gutenberg_get_slug_from_attr( $duotone_attr ); + $slug = self::gutenberg_get_slug_from_attr( $duotone_attr ); + $filter_id = self::get_filter_id( $slug ); - return array_key_exists( $slug, self::$global_styles_presets ); + return array_key_exists( $filter_id, self::$global_styles_presets ); } /** @@ -501,6 +541,26 @@ private static function get_css_custom_property_name( $slug ) { return self::CSS_VAR_PREFIX . $slug; } + /** + * Get the ID of the duotone filter. + * + * @param string $slug The slug of the duotone preset. + * @return string The ID of the duotone filter. + */ + private static function get_filter_id( $slug ) { + return self::FILTER_ID_PREFIX . $slug; + } + + /** + * Get the URL for a duotone filter. + * + * @param string $filter_id The ID of the filter. + * @return string The URL for the duotone filter. + */ + private static function get_filter_url( $filter_id ) { + return 'url(#' . $filter_id . ')'; + } + /** * Gets the SVG for the duotone filter definition. * @@ -585,34 +645,17 @@ private static function get_css_var( $slug ) { return 'var(' . self::get_css_custom_property_name( $slug ) . ')'; } - /** - * Get the CSS declaration for a duotone preset. - * Example: --wp--preset--duotone--blue-orange: url('#wp-duotone-blue-orange'); - * - * @param array $filter_data The duotone data for presets and custom filters. - * @return string The CSS declaration. - */ - private static function get_css_custom_property_declaration( $filter_data ) { - $declaration_value = gutenberg_get_duotone_filter_property( $filter_data ); - $duotone_preset_css_property_name = self::get_css_custom_property_name( $filter_data['slug'] ); - return $duotone_preset_css_property_name . ': ' . $declaration_value . ';'; - } - /** * Outputs all necessary SVG for duotone filters, CSS for classic themes. */ public static function output_footer_assets() { - foreach ( self::$output as $filter_data ) { - - // SVG will be output on the page later. - $filter_svg = self::get_filter_svg_from_preset( $filter_data ); - - echo $filter_svg; + if ( ! empty( self::$used_svg_filter_data ) ) { + echo self::get_svg_definitions( self::$used_svg_filter_data ); + } - // This is for classic themes - in block themes, the CSS is added in the head via wp_add_inline_style in the wp_enqueue_scripts action. - if ( ! wp_is_block_theme() ) { - wp_add_inline_style( 'core-block-supports', 'body{' . self::get_css_custom_property_declaration( $filter_data ) . '}' ); - } + // This is for classic themes - in block themes, the CSS is added in the head via wp_add_inline_style in the wp_enqueue_scripts action. + if ( ! wp_is_block_theme() && ! empty( self::$used_global_styles_presets ) ) { + wp_add_inline_style( 'core-block-supports', self::get_global_styles_presets( self::$used_global_styles_presets ) ); } } @@ -625,32 +668,29 @@ public static function output_footer_assets() { * @return array The editor settings with duotone SVGs and CSS custom properties. */ public static function add_editor_settings( $settings ) { - $duotone_svgs = ''; - $duotone_css = 'body{'; - foreach ( self::$global_styles_presets as $filter_data ) { - $duotone_svgs .= self::get_filter_svg_from_preset( $filter_data ); - $duotone_css .= self::get_css_custom_property_declaration( $filter_data ); - } - $duotone_css .= '}'; - - if ( ! isset( $settings['styles'] ) ) { - $settings['styles'] = array(); - } + if ( ! empty( self::$global_styles_presets ) ) { + if ( ! isset( $settings['styles'] ) ) { + $settings['styles'] = array(); + } - $settings['styles'][] = array( - 'assets' => $duotone_svgs, - // The 'svgs' type is new in 6.3 and requires the corresponding JS changes in the EditorStyles component to work. - '__unstableType' => 'svgs', - 'isGlobalStyles' => false, - ); + $settings['styles'][] = array( + // For the editor we can add all of the presets by default. + 'assets' => self::get_svg_definitions( self::$global_styles_presets ), + // The 'svgs' type is new in 6.3 and requires the corresponding JS changes in the EditorStyles component to work. + '__unstableType' => 'svgs', + // These styles not generated by global styles, so this must be false or they will be stripped out in gutenberg_get_block_editor_settings. + 'isGlobalStyles' => false, + ); - $settings['styles'][] = array( - 'css' => $duotone_css, - // This must be set and must be something other than 'theme' or they will be stripped out in the post editor component. - '__unstableType' => 'presets', - // These styles are no longer generated by global styles, so this must be false or they will be stripped out in gutenberg_get_block_editor_settings. - 'isGlobalStyles' => false, - ); + $settings['styles'][] = array( + // For the editor we can add all of the presets by default. + 'css' => self::get_global_styles_presets( self::$global_styles_presets ), + // This must be set and must be something other than 'theme' or they will be stripped out in the post editor component. + '__unstableType' => 'presets', + // These styles are no longer generated by global styles, so this must be false or they will be stripped out in gutenberg_get_block_editor_settings. + 'isGlobalStyles' => false, + ); + } return $settings; } @@ -659,24 +699,63 @@ public static function add_editor_settings( $settings ) { * Appends the used global style duotone filter CSS Vars to the inline global styles CSS */ public static function output_global_styles() { - - if ( empty( self::$output ) ) { - return; + if ( ! empty( self::$used_global_styles_presets ) ) { + wp_add_inline_style( 'global-styles', self::get_global_styles_presets( self::$used_global_styles_presets ) ); } + } - $duotone_css_vars = ''; - - foreach ( self::$output as $filter_data ) { - if ( ! array_key_exists( $filter_data['slug'], self::$global_styles_presets ) ) { - continue; - } + /** + * Appends the used block duotone filter declarations to the inline block supports CSS. + */ + public static function output_block_styles() { + if ( ! empty( self::$block_css_declarations ) ) { + gutenberg_style_engine_get_stylesheet_from_css_rules( + self::$block_css_declarations, + array( + 'context' => 'block-supports', + ) + ); + } + } - $duotone_css_vars .= self::get_css_custom_property_declaration( $filter_data ); + /** + * Get the SVGs for the duotone filters. + * + * Example output: + * + * + * @param array $sources The duotone presets. + * @return string The SVGs for the duotone filters. + */ + private static function get_svg_definitions( $sources ) { + $svgs = ''; + foreach ( $sources as $filter_id => $filter_data ) { + $colors = $filter_data['colors']; + $svgs .= self::get_filter_svg( $filter_id, $colors ); } + return $svgs; + } - if ( ! empty( $duotone_css_vars ) ) { - wp_add_inline_style( 'global-styles', 'body{' . $duotone_css_vars . '}' ); + /** + * Get the CSS for global styles. + * + * Example output: + * body{--wp--preset--duotone--blue-orange:url('#wp-duotone-blue-orange');} + * + * @param array $sources The duotone presets. + * @return string The CSS for global styles. + */ + private static function get_global_styles_presets( $sources ) { + $css = 'body{'; + foreach ( $sources as $filter_id => $filter_data ) { + $slug = $filter_data['slug']; + $colors = $filter_data['colors']; + $css_property_name = self::get_css_custom_property_name( $slug ); + $declaration_value = is_string( $colors ) ? $colors : self::get_filter_url( $filter_id ); + $css .= $css_property_name . ':' . $declaration_value . ';'; } + $css .= '}'; + return $css; } /** @@ -715,6 +794,86 @@ private static function get_selector( $block_name ) { } } + /** + * Enqueue a block CSS declaration for the page. + * + * @param string $filter_id The filter ID. e.g. 'wp-duotone-000000-ffffff-2'. + * @param string $duotone_selector The block's duotone selector. e.g. '.wp-block-image img'. + * @param string $filter_value The filter CSS value. e.g. 'url(#wp-duotone-000000-ffffff-2)' or 'unset'. + */ + private static function enqueue_block_css( $filter_id, $duotone_selector, $filter_value ) { + // Build the CSS selectors to which the filter will be applied. + $selectors = explode( ',', $duotone_selector ); + + $selectors_scoped = array(); + foreach ( $selectors as $selector_part ) { + // Assuming the selector part is a subclass selector (not a tag name) + // so we can prepend the filter id class. If we want to support elements + // such as `img` or namespaces, we'll need to add a case for that here. + $selectors_scoped[] = '.' . $filter_id . trim( $selector_part ); + } + + $selector = implode( ', ', $selectors_scoped ); + + self::$block_css_declarations[] = array( + 'selector' => $selector, + 'declarations' => array( + 'filter' => $filter_value, + ), + ); + } + + /** + * Enqueue custom filter assets for the page. Includes an SVG filter and block CSS declaration. + * + * @param string $filter_id The filter ID. e.g. 'wp-duotone-000000-ffffff-2'. + * @param string $duotone_selector The block's duotone selector. e.g. '.wp-block-image img'. + * @param string $filter_value The filter CSS value. e.g. 'url(#wp-duotone-000000-ffffff-2)' or 'unset'. + * @param array $filter_data Duotone filter data with 'slug' and 'colors' keys. + */ + private static function enqueue_custom_filter( $filter_id, $duotone_selector, $filter_value, $filter_data ) { + self::$used_svg_filter_data[ $filter_id ] = $filter_data; + self::enqueue_block_css( $filter_id, $duotone_selector, $filter_value ); + } + + /** + * Enqueue preset assets for the page. Includes a CSS custom property, SVG filter, and block CSS declaration. + * + * @param string $filter_id The filter ID. e.g. 'wp-duotone-blue-orange'. + * @param string $duotone_selector The block's duotone selector. e.g. '.wp-block-image img'. + * @param string $filter_value The filter CSS value. e.g. 'url(#wp-duotone-blue-orange)' or 'unset'. + */ + private static function enqueue_global_styles_preset( $filter_id, $duotone_selector, $filter_value ) { + self::$used_global_styles_presets[ $filter_id ] = self::$global_styles_presets[ $filter_id ]; + self::enqueue_custom_filter( $filter_id, $duotone_selector, $filter_value, self::$global_styles_presets[ $filter_id ] ); + } + + /** + * Registers the style and colors block attributes for block types that support it. + * + * @param WP_Block_Type $block_type Block Type. + */ + public static function register_duotone_support( $block_type ) { + $has_duotone_support = false; + if ( property_exists( $block_type, 'supports' ) ) { + // Previous `color.__experimentalDuotone` support flag is migrated + // to `filter.duotone` via `block_type_metadata_settings` filter. + $has_duotone_support = _wp_array_get( $block_type->supports, array( 'filter', 'duotone' ), null ); + } + + if ( $has_duotone_support ) { + if ( ! $block_type->attributes ) { + $block_type->attributes = array(); + } + + if ( ! array_key_exists( 'style', $block_type->attributes ) ) { + $block_type->attributes['style'] = array( + 'type' => 'object', + ); + } + } + } + /** * Render out the duotone CSS styles and SVG. * @@ -752,86 +911,41 @@ public static function render_duotone_support( $block_content, $block ) { if ( $is_preset ) { - // Extract the slug from the preset variable string. - $slug = self::gutenberg_get_slug_from_attr( $duotone_attr ); - - // Utilize existing preset CSS custom property. - $declaration_value = self::get_css_var( $slug ); + $slug = self::gutenberg_get_slug_from_attr( $duotone_attr ); // e.g. 'green-blue'. + $filter_id = self::get_filter_id( $slug ); // e.g. 'wp-duotone-filter-green-blue'. + $filter_value = self::get_css_var( $slug ); // e.g. 'var(--wp--preset--duotone--green-blue)'. - self::$output[ $slug ] = self::$global_styles_presets[ $slug ]; + // CSS custom property, SVG filter, and block CSS. + self::enqueue_global_styles_preset( $filter_id, $duotone_selector, $filter_value ); } elseif ( $is_css ) { - // Build a unique slug for the filter based on the CSS value. - $slug = wp_unique_id( sanitize_key( $duotone_attr . '-' ) ); + $slug = wp_unique_id( sanitize_key( $duotone_attr . '-' ) ); // e.g. 'unset-1'. + $filter_id = self::get_filter_id( $slug ); // e.g. 'wp-duotone-filter-unset-1'. + $filter_value = $duotone_attr; // e.g. 'unset'. - // Pass through the CSS value. - $declaration_value = $duotone_attr; + // Just block CSS. + self::enqueue_block_css( $filter_id, $duotone_selector, $filter_value ); } elseif ( $is_custom ) { - // Build a unique slug for the filter based on the array of colors. - $slug = wp_unique_id( sanitize_key( implode( '-', $duotone_attr ) . '-' ) ); - - $filter_data = array( + $slug = wp_unique_id( sanitize_key( implode( '-', $duotone_attr ) . '-' ) ); // e.g. '000000-ffffff-2'. + $filter_id = self::get_filter_id( $slug ); // e.g. 'wp-duotone-filter-000000-ffffff-2'. + $filter_value = self::get_filter_url( $filter_id ); // e.g. 'url(#wp-duotone-filter-000000-ffffff-2)'. + $filter_data = array( 'slug' => $slug, 'colors' => $duotone_attr, ); - // Build a customized CSS filter property for unique slug. - $declaration_value = gutenberg_get_duotone_filter_property( $filter_data ); - self::$output[ $slug ] = $filter_data; + // SVG filter and block CSS. + self::enqueue_custom_filter( $filter_id, $duotone_selector, $filter_value, $filter_data ); } } elseif ( $has_global_styles_duotone ) { - $slug = self::$global_styles_block_names[ $block['blockName'] ]; - - // Utilize existing preset CSS custom property. - $declaration_value = self::get_css_var( $slug ); - - self::$output[ $slug ] = self::$global_styles_presets[ $slug ]; - } - - // - Applied as a class attribute to the block wrapper. - // - Used as a selector to apply the filter to the block. - $filter_id = gutenberg_get_duotone_filter_id( array( 'slug' => $slug ) ); - - // Build the CSS selectors to which the filter will be applied. - $selectors = explode( ',', $duotone_selector ); + $slug = self::$global_styles_block_names[ $block['blockName'] ]; // e.g. 'green-blue'. + $filter_id = self::get_filter_id( $slug ); // e.g. 'wp-duotone-filter-green-blue'. + $filter_value = self::get_css_var( $slug ); // e.g. 'var(--wp--preset--duotone--green-blue)'. - $selectors_scoped = array(); - foreach ( $selectors as $selector_part ) { - // Assuming the selector part is a subclass selector (not a tag name) - // so we can prepend the filter id class. If we want to support elements - // such as `img` or namespaces, we'll need to add a case for that here. - $selectors_scoped[] = '.' . $filter_id . trim( $selector_part ); + // CSS custom property, SVG filter, and block CSS. + self::enqueue_global_styles_preset( $filter_id, $duotone_selector, $filter_value ); } - $selector = implode( ', ', $selectors_scoped ); - - // We only want to add the selector if we have it in the output already, essentially skipping 'unset'. - if ( array_key_exists( $slug, self::$output ) ) { - self::$output[ $slug ]['selector'] = $selector; - } - - // Pass styles to the block-supports stylesheet via the style engine. - // This ensures that Duotone styles are included in a single stylesheet, - // avoiding multiple style tags or multiple stylesheets being output to - // the site frontend. - gutenberg_style_engine_get_stylesheet_from_css_rules( - array( - array( - 'selector' => $selector, - 'declarations' => array( - // !important is needed because these styles - // render before global styles, - // and they should be overriding the duotone - // filters set by global styles. - 'filter' => $declaration_value . ' !important', - ), - ), - ), - array( - 'context' => 'block-supports', - ) - ); - // Like the layout hook, this assumes the hook only applies to blocks with a single wrapper. $tags = new WP_HTML_Tag_Processor( $block_content ); if ( $tags->next_tag() ) { @@ -860,15 +974,64 @@ public static function migrate_experimental_duotone_support_flag( $settings, $me return $settings; } + /** + * Returns the prefixed id for the duotone filter for use as a CSS id. + * + * Exported for the deprecated function gutenberg_get_duotone_filter_id(). + * + * @since 6.3.0 + * @deprecated 6.3.0 + * + * @param array $preset Duotone preset value as seen in theme.json. + * @return string Duotone filter CSS id. + */ + public static function get_filter_id_from_preset( $preset ) { + _deprecated_function( __FUNCTION__, '6.3.0' ); + + $filter_id = ''; + if ( isset( $preset['slug'] ) ) { + $filter_id = self::get_filter_id( $preset['slug'] ); + } + return $filter_id; + } + /** * Gets the SVG for the duotone filter definition from a preset. * + * Exported for the deprecated function gutenberg_get_duotone_filter_property(). + * + * @since 6.3.0 + * @deprecated 6.3.0 + * * @param array $preset The duotone preset. * @return string The SVG for the filter definition. */ public static function get_filter_svg_from_preset( $preset ) { - // TODO: This function will be refactored out in a follow-up PR where it will be deprecated. - $filter_id = gutenberg_get_duotone_filter_id( $preset ); + _deprecated_function( __FUNCTION__, '6.3.0' ); + + $filter_id = self::get_filter_id_from_preset( $preset ); return self::get_filter_svg( $filter_id, $preset['colors'] ); } + + /** + * Gets the CSS filter property value from a preset. + * + * Exported for the deprecated function gutenberg_get_duotone_filter_id(). + * + * @since 6.3.0 + * @deprecated 6.3.0 + * + * @param array $preset The duotone preset. + * @return string The CSS filter property value. + */ + public static function get_filter_css_property_value_from_preset( $preset ) { + _deprecated_function( __FUNCTION__, '6.3.0' ); + + if ( isset( $preset['colors'] ) && is_string( $preset['colors'] ) ) { + return $preset['colors']; + } + + $filter_id = self::get_filter_id_from_preset( $preset ); + return 'url(#' . $filter_id . ')'; + } } diff --git a/package-lock.json b/package-lock.json index 6d804ff79a5d1..362e158552728 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "15.6.0-rc.2", + "version": "15.6.0-rc.3", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -16810,7 +16810,7 @@ "@wordpress/hooks": "file:packages/hooks", "@wordpress/i18n": "file:packages/i18n", "@wordpress/rich-text": "file:packages/rich-text", - "rememo": "^4.0.0", + "rememo": "^4.0.2", "uuid": "^8.3.0" } }, @@ -16946,7 +16946,7 @@ "lodash": "^4.17.21", "react-autosize-textarea": "^7.1.0", "react-easy-crop": "^4.5.1", - "rememo": "^4.0.0", + "rememo": "^4.0.2", "remove-accents": "^0.4.2", "traverse": "^0.6.6" } @@ -17034,7 +17034,7 @@ "is-plain-object": "^5.0.0", "lodash": "^4.17.21", "memize": "^1.1.0", - "rememo": "^4.0.0", + "rememo": "^4.0.2", "remove-accents": "^0.4.2", "showdown": "^1.9.1", "simple-html-tokenizer": "^0.5.7", @@ -17057,7 +17057,7 @@ "@wordpress/keyboard-shortcuts": "file:packages/keyboard-shortcuts", "@wordpress/private-apis": "file:packages/private-apis", "cmdk": "^0.2.0", - "rememo": "^4.0.0" + "rememo": "^4.0.2" } }, "@wordpress/components": { @@ -17165,7 +17165,7 @@ "equivalent-key-map": "^0.2.2", "fast-deep-equal": "^3.1.3", "memize": "^1.1.0", - "rememo": "^4.0.0", + "rememo": "^4.0.2", "uuid": "^8.3.0" } }, @@ -17383,7 +17383,7 @@ "@wordpress/widgets": "file:packages/widgets", "classnames": "^2.3.1", "memize": "^1.1.0", - "rememo": "^4.0.0" + "rememo": "^4.0.2" }, "dependencies": { "@wordpress/dom": { @@ -17459,7 +17459,7 @@ "lodash": "^4.17.21", "memize": "^1.1.0", "react-autosize-textarea": "^7.1.0", - "rememo": "^4.0.0" + "rememo": "^4.0.2" }, "dependencies": { "colord": { @@ -17539,7 +17539,7 @@ "lodash": "^4.17.21", "memize": "^1.1.0", "react-autosize-textarea": "^7.1.0", - "rememo": "^4.0.0", + "rememo": "^4.0.2", "remove-accents": "^0.4.2" }, "dependencies": { @@ -17839,7 +17839,7 @@ "@wordpress/data": "file:packages/data", "@wordpress/element": "file:packages/element", "@wordpress/keycodes": "file:packages/keycodes", - "rememo": "^4.0.0" + "rememo": "^4.0.2" } }, "@wordpress/keycodes": { @@ -18222,7 +18222,7 @@ "@wordpress/i18n": "file:packages/i18n", "@wordpress/keycodes": "file:packages/keycodes", "memize": "^1.1.0", - "rememo": "^4.0.0" + "rememo": "^4.0.2" } }, "@wordpress/scripts": { @@ -50858,9 +50858,9 @@ } }, "rememo": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/rememo/-/rememo-4.0.0.tgz", - "integrity": "sha512-6BAfg1Dqg6UteZBEH9k6EHHersM86/EcBOMtJV+h+xEn1GC3H+gAgJWpexWYAamAxD0qXNmIt50iS/zuZKnQag==" + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/rememo/-/rememo-4.0.2.tgz", + "integrity": "sha512-NVfSP9NstE3QPNs/TnegQY0vnJnstKQSpcrsI2kBTB3dB2PkdfKdTa+abbjMIDqpc63fE5LfjLgfMst0ULMFxQ==" }, "remove-accents": { "version": "0.4.2", diff --git a/package.json b/package.json index fb6d0a3710ab8..125beeb7a82b1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "15.6.0-rc.2", + "version": "15.6.0-rc.3", "private": true, "description": "A new WordPress editor experience.", "author": "The WordPress Contributors", @@ -251,7 +251,7 @@ "scripts": { "build": "npm run build:packages && wp-scripts build", "build:analyze-bundles": "npm run build -- --webpack-bundle-analyzer", - "build:package-types": "node ./bin/packages/validate-typescript-version.js && tsc --build", + "build:package-types": "node ./bin/packages/validate-typescript-version.js && tsc --build && node ./bin/packages/check-build-type-declaration-files.js", "prebuild:packages": "npm run clean:packages && lerna run build", "build:packages": "npm run build:package-types && node ./bin/packages/build.js", "build:plugin-zip": "bash ./bin/build-plugin-zip.sh", diff --git a/packages/annotations/package.json b/packages/annotations/package.json index dcdeaab3180bb..231a66fcaaa5d 100644 --- a/packages/annotations/package.json +++ b/packages/annotations/package.json @@ -30,7 +30,7 @@ "@wordpress/hooks": "file:../hooks", "@wordpress/i18n": "file:../i18n", "@wordpress/rich-text": "file:../rich-text", - "rememo": "^4.0.0", + "rememo": "^4.0.2", "uuid": "^8.3.0" }, "publishConfig": { diff --git a/packages/base-styles/_mixins.scss b/packages/base-styles/_mixins.scss index 81e8424c65f0e..ff68bbb36b1c7 100644 --- a/packages/base-styles/_mixins.scss +++ b/packages/base-styles/_mixins.scss @@ -514,12 +514,9 @@ /* stylelint-enable function-comma-space-after */ } -@mixin custom-scrollbars-on-hover() { +@mixin custom-scrollbars-on-hover($handle-color, $track-color) { visibility: hidden; - $handle-color: #757575; - $track-color: #1e1e1e; - // WebKit &::-webkit-scrollbar { width: 12px; diff --git a/packages/block-editor/README.md b/packages/block-editor/README.md index fd0b0b375bb9b..703dbd337567d 100644 --- a/packages/block-editor/README.md +++ b/packages/block-editor/README.md @@ -430,6 +430,19 @@ _Returns_ - `string|null`: A font-size value using clamp(). +### getCustomValueFromPreset + +Converts a spacing preset into a custom value. + +_Parameters_ + +- _value_ `string`: Value to convert +- _spacingSizes_ `Array`: Array of the current spacing preset objects + +_Returns_ + +- `string`: Mapping of the spacing preset to its equivalent custom value. + ### getFontSize Returns the font size object based on an array of named font sizes and the namedFontSize and customFontSize values. If namedFontSize is undefined or not found in fontSizes an object with just the size value based on customFontSize is returned. @@ -735,7 +748,7 @@ Applies a series of CSS rule transforms to wrap selectors inside a given class a _Parameters_ -- _styles_ `Array`: CSS rules. +- _styles_ `Object|Array`: CSS rules. - _wrapperClassName_ `string`: Wrapper Class Name. _Returns_ diff --git a/packages/block-editor/package.json b/packages/block-editor/package.json index bb7e99ca21038..7db45af56aa2b 100644 --- a/packages/block-editor/package.json +++ b/packages/block-editor/package.json @@ -72,7 +72,7 @@ "lodash": "^4.17.21", "react-autosize-textarea": "^7.1.0", "react-easy-crop": "^4.5.1", - "rememo": "^4.0.0", + "rememo": "^4.0.2", "remove-accents": "^0.4.2", "traverse": "^0.6.6" }, diff --git a/packages/block-editor/src/components/block-info-slot-fill/index.js b/packages/block-editor/src/components/block-info-slot-fill/index.js new file mode 100644 index 0000000000000..bba49bc7cc637 --- /dev/null +++ b/packages/block-editor/src/components/block-info-slot-fill/index.js @@ -0,0 +1,17 @@ +/** + * WordPress dependencies + */ +import { privateApis as componentsPrivateApis } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { unlock } from '../../lock-unlock'; + +const { createPrivateSlotFill } = unlock( componentsPrivateApis ); +const { Fill, Slot } = createPrivateSlotFill( 'BlockInformation' ); + +const BlockInfo = ( props ) => ; +BlockInfo.Slot = ( props ) => ; + +export default BlockInfo; diff --git a/packages/block-editor/src/components/block-inspector/index.js b/packages/block-editor/src/components/block-inspector/index.js index b2fd086a2a29c..199f4f4628b4b 100644 --- a/packages/block-editor/src/components/block-inspector/index.js +++ b/packages/block-editor/src/components/block-inspector/index.js @@ -37,6 +37,7 @@ import useInspectorControlsTabs from '../inspector-controls-tabs/use-inspector-c import AdvancedControls from '../inspector-controls-tabs/advanced-controls-panel'; import PositionControls from '../inspector-controls-tabs/position-controls-panel'; import useBlockInspectorAnimationSettings from './useBlockInspectorAnimationSettings'; +import BlockInfo from '../block-info-slot-fill'; function useContentBlocks( blockTypes, block ) { const contentBlocksObjectAux = useMemo( () => { @@ -115,6 +116,7 @@ function BlockInspectorLockedBlocks( { topLevelLockedBlock } ) { className={ blockInformation.isSynced && 'is-synced' } /> + { className={ blockInformation.isSynced && 'is-synced' } /> + { showTabs && ( { within( paragraphBlock ).getByPlaceholderText( 'Start writing…' ); - changeTextOfRichText( paragraphField, 'Hello!' ); + typeInRichText( paragraphField, 'Hello!' ); // Add Spacer block await addBlock( screen, 'Spacer' ); @@ -132,7 +132,7 @@ describe( 'Block Actions Menu', () => { within( paragraphBlock ).getByPlaceholderText( 'Start writing…' ); - changeTextOfRichText( paragraphField, 'Hello!' ); + typeInRichText( paragraphField, 'Hello!' ); // Add Spacer block await addBlock( screen, 'Spacer' ); @@ -179,7 +179,7 @@ describe( 'Block Actions Menu', () => { within( paragraphBlock ).getByPlaceholderText( 'Start writing…' ); - changeTextOfRichText( paragraphField, 'Hello!' ); + typeInRichText( paragraphField, 'Hello!' ); // Add Spacer block await addBlock( screen, 'Spacer' ); @@ -228,7 +228,7 @@ describe( 'Block Actions Menu', () => { within( paragraphBlock ).getByPlaceholderText( 'Start writing…' ); - changeTextOfRichText( paragraphField, 'Hello!' ); + typeInRichText( paragraphField, 'Hello!' ); // Add Spacer block await addBlock( screen, 'Spacer' ); @@ -275,7 +275,7 @@ describe( 'Block Actions Menu', () => { within( paragraphBlock ).getByPlaceholderText( 'Start writing…' ); - changeTextOfRichText( paragraphField, 'Hello!' ); + typeInRichText( paragraphField, 'Hello!' ); // Add Spacer block await addBlock( screen, 'Spacer' ); @@ -322,7 +322,7 @@ describe( 'Block Actions Menu', () => { within( paragraphBlock ).getByPlaceholderText( 'Start writing…' ); - changeTextOfRichText( paragraphField, 'Hello!' ); + typeInRichText( paragraphField, 'Hello!' ); // Add Spacer block await addBlock( screen, 'Spacer' ); @@ -368,7 +368,7 @@ describe( 'Block Actions Menu', () => { within( paragraphBlock ).getByPlaceholderText( 'Start writing…' ); - changeTextOfRichText( paragraphField, 'Hello!' ); + typeInRichText( paragraphField, 'Hello!' ); // Add Spacer block await addBlock( screen, 'Spacer' ); @@ -405,7 +405,7 @@ describe( 'Block Actions Menu', () => { within( paragraphBlock ).getByPlaceholderText( 'Start writing…' ); - changeTextOfRichText( paragraphField, 'Hello!' ); + typeInRichText( paragraphField, 'Hello!' ); // Add Spacer block await addBlock( screen, 'Spacer' ); diff --git a/packages/block-editor/src/components/block-mover/style.scss b/packages/block-editor/src/components/block-mover/style.scss index cf4061b3789df..9a20f7d8e3e3d 100644 --- a/packages/block-editor/src/components/block-mover/style.scss +++ b/packages/block-editor/src/components/block-mover/style.scss @@ -35,11 +35,14 @@ > * { width: $block-toolbar-height * 0.5; min-width: 0 !important; // overrides default button width. - padding-left: 0 !important; - padding-right: 0 !important; overflow: hidden; } + .block-editor-block-mover-button { + padding-left: 0; + padding-right: 0; + } + .block-editor-block-mover-button.is-up-button svg { left: 5px; } @@ -55,9 +58,12 @@ @include break-small() { width: $block-toolbar-height * 0.5; min-width: 0 !important; // overrides default button width. - padding-left: 0 !important; - padding-right: 0 !important; overflow: hidden; + + .block-editor-block-mover &.has-icon.has-icon { + padding-left: 0; + padding-right: 0; + } } } diff --git a/packages/block-editor/src/components/block-mover/test/index.native.js b/packages/block-editor/src/components/block-mover/test/index.native.js index 903289c6d5a31..dc1a8f974c3c6 100644 --- a/packages/block-editor/src/components/block-mover/test/index.native.js +++ b/packages/block-editor/src/components/block-mover/test/index.native.js @@ -9,7 +9,7 @@ import { within, getEditorHtml, render, - changeTextOfRichText, + typeInRichText, } from 'test/helpers'; /** @@ -89,7 +89,7 @@ describe( 'Block Mover Picker', () => { within( paragraphBlock ).getByPlaceholderText( 'Start writing…' ); - changeTextOfRichText( paragraphField, 'Hello!' ); + typeInRichText( paragraphField, 'Hello!' ); // Add Spacer block await addBlock( screen, 'Spacer' ); @@ -138,7 +138,7 @@ describe( 'Block Mover Picker', () => { within( paragraphBlock ).getByPlaceholderText( 'Start writing…' ); - changeTextOfRichText( paragraphField, 'Hello!' ); + typeInRichText( paragraphField, 'Hello!' ); // Add Spacer block await addBlock( screen, 'Spacer' ); @@ -176,7 +176,7 @@ describe( 'Block Mover Picker', () => { within( paragraphBlock ).getByPlaceholderText( 'Start writing…' ); - changeTextOfRichText( paragraphField, 'Hello!' ); + typeInRichText( paragraphField, 'Hello!' ); // Add Spacer block await addBlock( screen, 'Spacer' ); diff --git a/packages/block-editor/src/components/block-toolbar/index.js b/packages/block-editor/src/components/block-toolbar/index.js index 6727851e1647b..d9799849b1399 100644 --- a/packages/block-editor/src/components/block-toolbar/index.js +++ b/packages/block-editor/src/components/block-toolbar/index.js @@ -99,6 +99,7 @@ const BlockToolbar = ( { hideDragHandle } ) => { // header area and not contextually to the block. const displayHeaderToolbar = useViewportMatch( 'medium', '<' ) || hasFixedToolbar; + const isLargeViewport = ! useViewportMatch( 'medium', '<' ); if ( blockType ) { if ( ! hasBlockSupport( blockType, '__experimentalToolbar', true ) ) { @@ -124,9 +125,9 @@ const BlockToolbar = ( { hideDragHandle } ) => { return (
- { ! isMultiToolbar && - ! displayHeaderToolbar && - ! isContentLocked && } + { ! isMultiToolbar && isLargeViewport && ! isContentLocked && ( + + ) }
{ ( shouldShowVisualToolbar || isMultiToolbar ) && ! isContentLocked && ( diff --git a/packages/block-editor/src/components/block-toolbar/style.scss b/packages/block-editor/src/components/block-toolbar/style.scss index 554c5d96d65db..583c2453ed9d7 100644 --- a/packages/block-editor/src/components/block-toolbar/style.scss +++ b/packages/block-editor/src/components/block-toolbar/style.scss @@ -56,26 +56,46 @@ } } -.block-editor-block-contextual-toolbar.has-parent:not(.is-fixed) { - margin-left: calc(#{$grid-unit-60} + #{$grid-unit-10}); +// on desktop browsers the fixed toolbar has tweaked borders +@include break-medium() { + .block-editor-block-contextual-toolbar.is-fixed { + .block-editor-block-toolbar { + .components-toolbar-group, + .components-toolbar { + border-right: none; + + &::after { + content: ""; + width: $border-width; + margin-top: $grid-unit + $grid-unit-05; + margin-bottom: $grid-unit + $grid-unit-05; + background-color: $gray-300; + margin-left: $grid-unit; + } + + & .components-toolbar-group.components-toolbar-group { + &::after { + display: none; + } + } + } - .show-icon-labels & { - margin-left: 0; + > :last-child, + > :last-child .components-toolbar-group, + > :last-child .components-toolbar { + &::after { + display: none; + } + } + } } } -.block-editor-block-parent-selector { - position: absolute; - top: -$border-width; - left: calc(-#{$grid-unit-60} - #{$grid-unit-10} - #{$border-width}); +.block-editor-block-contextual-toolbar.has-parent:not(.is-fixed) { + margin-left: calc(#{$grid-unit-60} + #{$grid-unit-10}); .show-icon-labels & { - position: relative; - left: auto; - top: auto; - margin-top: -$border-width; - margin-left: -$border-width; - margin-bottom: -$border-width; + margin-left: 0; } } @@ -167,10 +187,23 @@ } } - .block-editor-block-mover:not(.is-horizontal) .block-editor-block-mover__move-button-container { + .block-editor-block-mover .block-editor-block-mover__move-button-container { width: auto; } + .block-editor-block-mover.is-horizontal { + .block-editor-block-mover__move-button-container, + .block-editor-block-mover-button { + padding-left: 6px; + padding-right: 6px; + } + } + + .block-editor-block-mover:not(.is-horizontal) .block-editor-block-mover-button { + padding-left: $grid-unit; + padding-right: $grid-unit; + } + // Mover overrides. .block-editor-block-toolbar__block-controls .block-editor-block-mover { border-left: 1px solid $gray-900; @@ -183,15 +216,9 @@ border-left-color: $gray-200; } - .block-editor-block-mover-button { - // The specificity can be reduced once https://github.com/WordPress/gutenberg/blob/try/block-toolbar-labels/packages/block-editor/src/components/block-mover/style.scss#L34 is also dealt with. - padding-left: $grid-unit !important; - padding-right: $grid-unit !important; - } - - .block-editor-block-mover__drag-handle.has-icon { - padding-left: 12px !important; - padding-right: 12px !important; + .block-editor-block-mover .block-editor-block-mover__drag-handle.has-icon { + padding-left: $grid-unit-15; + padding-right: $grid-unit-15; } .block-editor-block-contextual-toolbar.is-fixed .block-editor-block-mover__move-button-container { diff --git a/packages/block-editor/src/components/block-tools/block-contextual-toolbar.js b/packages/block-editor/src/components/block-tools/block-contextual-toolbar.js index ba31043ada2dd..c8eee6b48cbd4 100644 --- a/packages/block-editor/src/components/block-tools/block-contextual-toolbar.js +++ b/packages/block-editor/src/components/block-tools/block-contextual-toolbar.js @@ -7,8 +7,16 @@ import classnames from 'classnames'; * WordPress dependencies */ import { __ } from '@wordpress/i18n'; +import { forwardRef, useEffect, useRef, useState } from '@wordpress/element'; import { hasBlockSupport, store as blocksStore } from '@wordpress/blocks'; import { useSelect } from '@wordpress/data'; +import { + ToolbarItem, + ToolbarButton, + ToolbarGroup, +} from '@wordpress/components'; +import { levelUp } from '@wordpress/icons'; +import { useViewportMatch } from '@wordpress/compose'; /** * Internal dependencies @@ -16,10 +24,57 @@ import { useSelect } from '@wordpress/data'; import NavigableToolbar from '../navigable-toolbar'; import BlockToolbar from '../block-toolbar'; import { store as blockEditorStore } from '../../store'; +import BlockIcon from '../block-icon'; + +const CollapseFixedToolbarButton = forwardRef( ( { onClick }, ref ) => { + return ( + + ); +} ); + +const ExpandFixedToolbarButton = forwardRef( ( { onClick, icon }, ref ) => { + return ( + } + onClick={ onClick } + ref={ ref } + label={ __( 'Show block tools' ) } + /> + ); +} ); function BlockContextualToolbar( { focusOnMount, isFixed, ...props } ) { - const { blockType, hasParents, showParentSelector } = useSelect( - ( select ) => { + // When the toolbar is fixed it can be collapsed + const [ isCollapsed, setIsCollapsed ] = useState( false ); + const expandFixedToolbarButtonRef = useRef(); + const collapseFixedToolbarButtonRef = useRef(); + + // Don't focus the block toolbar just because it mounts + const initialRender = useRef( true ); + useEffect( () => { + if ( initialRender.current ) { + initialRender.current = false; + return; + } + if ( isCollapsed && expandFixedToolbarButtonRef.current ) { + expandFixedToolbarButtonRef.current.focus(); + } + if ( ! isCollapsed && collapseFixedToolbarButtonRef.current ) { + collapseFixedToolbarButtonRef.current.focus(); + } + }, [ isCollapsed ] ); + + const { blockType, hasParents, showParentSelector, selectedBlockClientId } = + useSelect( ( select ) => { const { getBlockName, getBlockParents, @@ -28,16 +83,17 @@ function BlockContextualToolbar( { focusOnMount, isFixed, ...props } ) { } = select( blockEditorStore ); const { getBlockType } = select( blocksStore ); const selectedBlockClientIds = getSelectedBlockClientIds(); - const selectedBlockClientId = selectedBlockClientIds[ 0 ]; - const parents = getBlockParents( selectedBlockClientId ); + const _selectedBlockClientId = selectedBlockClientIds[ 0 ]; + const parents = getBlockParents( _selectedBlockClientId ); const firstParentClientId = parents[ parents.length - 1 ]; const parentBlockName = getBlockName( firstParentClientId ); const parentBlockType = getBlockType( parentBlockName ); return { + selectedBlockClientId: _selectedBlockClientId, blockType: - selectedBlockClientId && - getBlockType( getBlockName( selectedBlockClientId ) ), + _selectedBlockClientId && + getBlockType( getBlockName( _selectedBlockClientId ) ), hasParents: parents.length, showParentSelector: parentBlockType && @@ -48,12 +104,16 @@ function BlockContextualToolbar( { focusOnMount, isFixed, ...props } ) { ) && selectedBlockClientIds.length <= 1 && ! __unstableGetContentLockingParent( - selectedBlockClientId + _selectedBlockClientId ), }; - }, - [] - ); + }, [] ); + + useEffect( () => { + setIsCollapsed( false ); + }, [ selectedBlockClientId ] ); + + const isLargeViewport = useViewportMatch( 'medium' ); if ( blockType ) { if ( ! hasBlockSupport( blockType, '__experimentalToolbar', true ) ) { @@ -65,6 +125,7 @@ function BlockContextualToolbar( { focusOnMount, isFixed, ...props } ) { const classes = classnames( 'block-editor-block-contextual-toolbar', { 'has-parent': hasParents && showParentSelector, 'is-fixed': isFixed, + 'is-collapsed': isCollapsed, } ); return ( @@ -75,7 +136,29 @@ function BlockContextualToolbar( { focusOnMount, isFixed, ...props } ) { aria-label={ __( 'Block tools' ) } { ...props } > - + { isFixed && isLargeViewport && blockType && ( + + { isCollapsed ? ( + setIsCollapsed( false ) } + icon={ blockType.icon } + ref={ expandFixedToolbarButtonRef } + /> + ) : ( + setIsCollapsed( true ) } + ref={ collapseFixedToolbarButtonRef } + /> + ) } + + ) } + { ! isCollapsed && } ); } diff --git a/packages/block-editor/src/components/block-tools/style.scss b/packages/block-editor/src/components/block-tools/style.scss index 099bf14987dcb..9194935c616b2 100644 --- a/packages/block-editor/src/components/block-tools/style.scss +++ b/packages/block-editor/src/components/block-tools/style.scss @@ -104,11 +104,10 @@ &.is-fixed { position: sticky; top: 0; - width: 100%; + left: 0; z-index: z-index(".block-editor-block-popover"); - // Fill up when empty - min-height: $block-toolbar-height; display: block; + width: 100%; border: none; border-bottom: $border-width solid $gray-200; @@ -119,6 +118,226 @@ border-right-color: $gray-200; } } + + // on desktop and tablet viewports the toolbar is fixed + // on top of interface header + @include break-medium() { + &.is-fixed { + + // position on top of interface header + position: fixed; + top: $grid-unit-50 - 2; + // leave room for block inserter + left: $grid-unit-80 + $grid-unit-40; + // Don't fill up when empty + min-height: initial; + // remove the border + border-bottom: none; + // has to be flex for collapse button to fit + display: flex; + + &.is-collapsed { + left: $grid-unit-10 * 32; + } + + .is-fullscreen-mode & { + top: $grid-unit - 2; + // leave room for block inserter + left: $grid-unit-80 + $grid-unit-70; + &.is-collapsed { + left: $grid-unit-10 * 35; + } + } + + & > .block-editor-block-toolbar__group-collapse-fixed-toolbar { + border: none; + + // Add a border as separator in the block toolbar. + &::after { + content: ""; + width: $border-width; + height: 24px; + margin-top: $grid-unit + $grid-unit-05; + margin-bottom: $grid-unit + $grid-unit-05; + background-color: $gray-300; + position: absolute; + left: 44px; + top: -1px; + } + } + + & > .block-editor-block-toolbar__group-expand-fixed-toolbar { + border: none; + + // Add a border as separator in the block toolbar. + &::before { + content: ""; + width: $border-width; + margin-top: $grid-unit + $grid-unit-05; + margin-bottom: $grid-unit + $grid-unit-05; + background-color: $gray-300; + position: relative; + left: -12px; //the padding of buttons + height: 24px; + } + } + + .show-icon-labels & { + left: $grid-unit-80 + $grid-unit-50; + + &.is-collapsed { + left: $grid-unit-10 * 56; + } + + .is-fullscreen-mode & { + left: $grid-unit-80 + $grid-unit-80; + } + + .block-editor-block-parent-selector .block-editor-block-parent-selector__button::after { + left: 0; + } + + .block-editor-block-toolbar__block-controls .block-editor-block-mover { + border-left: none; + &::before { + content: ""; + width: $border-width; + margin-top: $grid-unit + $grid-unit-05; + margin-bottom: $grid-unit + $grid-unit-05; + background-color: $gray-300; + position: relative; + } + } + } + } + + &.is-fixed .block-editor-block-parent-selector { + .block-editor-block-parent-selector__button { + position: relative; + top: -1px; + border: 0; + padding-right: 6px; + padding-left: 6px; + &::after { + content: "\00B7"; + font-size: 16px; + line-height: $grid-unit-40 + $grid-unit-10; + position: absolute; + left: $grid-unit-40 + $grid-unit-15 + 2px; + bottom: $grid-unit-05; + } + } + } + + &:not(.is-fixed) .block-editor-block-parent-selector { + position: absolute; + top: -$border-width; + left: calc(-#{$grid-unit-60} - #{$grid-unit-10} - #{$border-width}); + + .show-icon-labels & { + position: relative; + left: auto; + top: auto; + margin-top: -$border-width; + margin-left: -$border-width; + margin-bottom: -$border-width; + } + } + } + + // on tablet vewports the toolbar is fixed + // on top of interface header and covers the whole header + // except for the inserter on the left + @include break-medium() { + &.is-fixed { + + left: 28 * $grid-unit; + width: calc(100% - #{28 * $grid-unit}); + + &.is-collapsed { + // when collapsed minimize area + width: initial; + left: $grid-unit * 48; + } + + // collapsed wp admin sidebar when not in full screen mode + .auto-fold & { + left: $grid-unit-80 + $grid-unit-40; + width: calc(100% - #{$grid-unit-80 + $grid-unit-40}); + + &.is-collapsed { + left: $grid-unit * 32; + } + } + + .is-fullscreen-mode & { + width: calc(100% - #{$grid-unit-80 + $grid-unit-70}); + left: $grid-unit-80 + $grid-unit-70; + &.is-collapsed { + left: $grid-unit * 36; + // when collapsed minimize area + width: initial; + } + } + } + } + + // on desktop viewports the toolbar is fixed + // on top of interface header and leaves room + // for the block inserter the publish button + @include break-large() { + &.is-fixed { + + .auto-fold & { + // Don't fill the whole header, minimize area + width: initial; + + // leave room for block inserter and the dashboard navigation + left: $grid-unit-80 + $grid-unit-40 + ( $grid-unit-80 * 2 ); + + &.is-collapsed { + // when collapsed minimize area + width: initial; + left: $grid-unit * 48; + } + + } + + // collapsed wp admin sidebar when not in full screen mode + .auto-fold.folded & { + width: initial; + left: $grid-unit-80 + $grid-unit-40; + + &.is-collapsed { + // when collapsed minimize area + width: initial; + left: $grid-unit * 32; + } + + } + + .auto-fold.is-fullscreen-mode & { + // Don't fill the whole header, minimize area + width: initial; + left: $grid-unit-80 + $grid-unit-70; + + &.is-collapsed { + // when collapsed minimize area + width: initial; + left: $grid-unit * 36; + } + } + + .auto-fold.is-fullscreen-mode .show-icon-labels & { + left: $grid-unit-80 * 2; + &.is-collapsed { + left: $grid-unit * 48; + } + } + + } + } + } /** diff --git a/packages/block-editor/src/components/editor-styles/index.js b/packages/block-editor/src/components/editor-styles/index.js index 819efe5721127..66f2a08f115a4 100644 --- a/packages/block-editor/src/components/editor-styles/index.js +++ b/packages/block-editor/src/components/editor-styles/index.js @@ -68,29 +68,33 @@ function useDarkThemeBodyClassName( styles ) { } export default function EditorStyles( { styles } ) { + const stylesArray = useMemo( + () => Object.values( styles ?? [] ), + [ styles ] + ); const transformedStyles = useMemo( () => transformStyles( - styles.filter( ( style ) => style?.css ), + stylesArray.filter( ( style ) => style?.css ), EDITOR_STYLES_SELECTOR ), - [ styles ] + [ stylesArray ] ); const transformedSvgs = useMemo( () => - styles + stylesArray .filter( ( style ) => style.__unstableType === 'svgs' ) .map( ( style ) => style.assets ) .join( '' ), - [ styles ] + [ stylesArray ] ); return ( <> { /* Use an empty style element to have a document reference, but this could be any element. */ } - ) ) } diff --git a/packages/block-editor/src/components/global-styles/border-panel.js b/packages/block-editor/src/components/global-styles/border-panel.js index b4d47c03efe21..63ff223f78272 100644 --- a/packages/block-editor/src/components/global-styles/border-panel.js +++ b/packages/block-editor/src/components/global-styles/border-panel.js @@ -45,35 +45,6 @@ function useHasBorderWidthControl( settings ) { return settings?.border?.width; } -function applyFallbackStyle( border ) { - if ( ! border ) { - return border; - } - - if ( ! border.style && ( border.color || border.width ) ) { - return { ...border, style: 'solid' }; - } - - return border; -} - -function applyAllFallbackStyles( border ) { - if ( ! border ) { - return border; - } - - if ( hasSplitBorders( border ) ) { - return { - top: applyFallbackStyle( border.top ), - right: applyFallbackStyle( border.right ), - bottom: applyFallbackStyle( border.bottom ), - left: applyFallbackStyle( border.left ), - }; - } - - return applyFallbackStyle( border ); -} - function BorderToolsPanel( { resetAllFilter, onChange, @@ -186,7 +157,7 @@ export default function BorderPanel( { const onBorderChange = ( newBorder ) => { // Ensure we have a visible border style when a border width or // color is being selected. - const updatedBorder = applyAllFallbackStyles( newBorder ); + const updatedBorder = { ...newBorder }; if ( hasSplitBorders( updatedBorder ) ) { [ 'top', 'right', 'bottom', 'left' ].forEach( ( side ) => { diff --git a/packages/block-editor/src/components/global-styles/test/use-global-styles-output.js b/packages/block-editor/src/components/global-styles/test/use-global-styles-output.js index 2df709ebbfca6..611e436743308 100644 --- a/packages/block-editor/src/components/global-styles/test/use-global-styles-output.js +++ b/packages/block-editor/src/components/global-styles/test/use-global-styles-output.js @@ -480,10 +480,128 @@ describe( 'global styles renderer', () => { expect( toStyles( tree, blockSelectors ) ).toEqual( 'body {margin: 0;}' + 'body{background-color: red;margin: 10px;padding: 10px;}a{color: blue;}a:hover{color: orange;}a:focus{color: orange;}h1{font-size: 42px;}.wp-block-group{margin-top: 10px;margin-right: 20px;margin-bottom: 30px;margin-left: 40px;padding-top: 11px;padding-right: 22px;padding-bottom: 33px;padding-left: 44px;}h1,h2,h3,h4,h5,h6{color: orange;}h1 a,h2 a,h3 a,h4 a,h5 a,h6 a{color: hotpink;}h1 a:hover,h2 a:hover,h3 a:hover,h4 a:hover,h5 a:hover,h6 a:hover{color: red;}h1 a:focus,h2 a:focus,h3 a:focus,h4 a:focus,h5 a:focus,h6 a:focus{color: red;}' + - '.wp-block-image img, .wp-block-image .wp-crop-area{border-radius: 9999px}.wp-block-image{color: red;}.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }' + + '.wp-block-image img, .wp-block-image .wp-crop-area{border-radius: 9999px;}.wp-block-image{color: red;}.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }' + '.has-white-color{color: var(--wp--preset--color--white) !important;}.has-white-background-color{background-color: var(--wp--preset--color--white) !important;}.has-white-border-color{border-color: var(--wp--preset--color--white) !important;}.has-black-color{color: var(--wp--preset--color--black) !important;}.has-black-background-color{background-color: var(--wp--preset--color--black) !important;}.has-black-border-color{border-color: var(--wp--preset--color--black) !important;}h1.has-blue-color,h2.has-blue-color,h3.has-blue-color,h4.has-blue-color,h5.has-blue-color,h6.has-blue-color{color: var(--wp--preset--color--blue) !important;}h1.has-blue-background-color,h2.has-blue-background-color,h3.has-blue-background-color,h4.has-blue-background-color,h5.has-blue-background-color,h6.has-blue-background-color{background-color: var(--wp--preset--color--blue) !important;}h1.has-blue-border-color,h2.has-blue-border-color,h3.has-blue-border-color,h4.has-blue-border-color,h5.has-blue-border-color,h6.has-blue-border-color{border-color: var(--wp--preset--color--blue) !important;}' ); } ); + + it( 'should handle feature selectors', () => { + const tree = { + styles: { + blocks: { + 'core/image': { + color: { + text: 'red', + }, + spacing: { + padding: { + top: '1px', + }, + }, + border: { + color: 'red', + radius: '9999px', + }, + }, + }, + }, + }; + + const blockSelectors = { + 'core/image': { + selector: '.wp-image', + featureSelectors: { + spacing: '.wp-image-spacing', + border: { + root: '.wp-image-border', + color: '.wp-image-border-color', + }, + }, + }, + }; + + expect( toStyles( Object.freeze( tree ), blockSelectors ) ).toEqual( + 'body {margin: 0;}' + + '.wp-image-spacing{padding-top: 1px;}.wp-image-border-color{border-color: red;}.wp-image-border{border-radius: 9999px;}.wp-image{color: red;}' + + '.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }' + ); + } ); + + it( 'should handle block variations', () => { + const tree = { + styles: { + blocks: { + 'core/image': { + variations: { + foo: { + color: { + text: 'blue', + }, + spacing: { + padding: { + top: '2px', + }, + }, + border: { + color: 'blue', + }, + }, + }, + }, + }, + }, + }; + + const blockSelectors = { + 'core/image': { + selector: '.wp-image', + featureSelectors: { + spacing: '.wp-image-spacing', + border: { + root: '.wp-image-border', + color: '.wp-image-border-color', + }, + }, + styleVariationSelectors: { + foo: '.is-style-foo.wp-image', + }, + }, + }; + + expect( toStyles( Object.freeze( tree ), blockSelectors ) ).toEqual( + 'body {margin: 0;}' + + '.is-style-foo.wp-image.wp-image-spacing{padding-top: 2px;}.is-style-foo.wp-image.wp-image-border-color{border-color: blue;}.is-style-foo.wp-image{color: blue;}' + + '.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }' + ); + } ); + + it( 'should handle duotone filter', () => { + const tree = { + styles: { + blocks: { + 'core/image': { + filter: { + duotone: 'blur(5px)', + }, + }, + }, + }, + }; + + const blockSelectors = { + 'core/image': { + selector: '.wp-image', + duotoneSelector: '.wp-image img', + }, + }; + + expect( toStyles( Object.freeze( tree ), blockSelectors ) ).toEqual( + 'body {margin: 0;}' + + '.wp-image img{filter: blur(5px);}' + + '.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }' + ); + } ); + it( 'should output content and wide size variables if values are specified', () => { const tree = { settings: { @@ -493,7 +611,7 @@ describe( 'global styles renderer', () => { }, }, }; - expect( toStyles( tree, 'body' ) ).toEqual( + expect( toStyles( Object.freeze( tree ), 'body' ) ).toEqual( 'body {margin: 0; --wp--style--global--content-size: 840px; --wp--style--global--wide-size: 1100px;}.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }' ); } ); @@ -678,8 +796,9 @@ describe( 'global styles renderer', () => { selectors: imageSelectors, }; const blockTypes = [ imageBlock ]; + const getBlockStyles = () => [ { name: 'foo' } ]; - expect( getBlockSelectors( blockTypes, () => {} ) ).toEqual( { + expect( getBlockSelectors( blockTypes, getBlockStyles ) ).toEqual( { 'core/image': { name: imageBlock.name, selector: imageSelectors.root, @@ -690,6 +809,9 @@ describe( 'global styles renderer', () => { border: '.my-image img, .my-image .crop-area', filter: { duotone: 'img' }, }, + styleVariationSelectors: { + foo: '.is-style-foo.my-image', + }, hasLayoutSupport: false, }, } ); diff --git a/packages/block-editor/src/components/global-styles/use-global-styles-output.js b/packages/block-editor/src/components/global-styles/use-global-styles-output.js index 72e15dc923c21..8777869d43621 100644 --- a/packages/block-editor/src/components/global-styles/use-global-styles-output.js +++ b/packages/block-editor/src/components/global-styles/use-global-styles-output.js @@ -325,7 +325,7 @@ export function getStylesDeclarations( return declarations; } - if ( !! properties && typeof styleValue !== 'string' ) { + if ( properties && typeof styleValue !== 'string' ) { Object.entries( properties ).forEach( ( entry ) => { const [ name, prop ] = entry; @@ -382,7 +382,7 @@ export function getStylesDeclarations( ruleValue = get( tree, refPath ); // Presence of another ref indicates a reference to another dynamic value. // Pointing to another dynamic value is not supported. - if ( ! ruleValue || !! ruleValue?.ref ) { + if ( ! ruleValue || ruleValue?.ref ) { return; } } @@ -551,6 +551,33 @@ export function getLayoutStyles( { return ruleset; } +const STYLE_KEYS = [ + 'border', + 'color', + 'dimensions', + 'spacing', + 'typography', + 'filter', + 'outline', + 'shadow', +]; + +function pickStyleKeys( treeToPickFrom ) { + if ( ! treeToPickFrom ) { + return {}; + } + const entries = Object.entries( treeToPickFrom ); + const pickedEntries = entries.filter( ( [ key ] ) => + STYLE_KEYS.includes( key ) + ); + // clone the style objects so that `getFeatureDeclarations` can remove consumed keys from it + const clonedEntries = pickedEntries.map( ( [ key, style ] ) => [ + key, + JSON.parse( JSON.stringify( style ) ), + ] ); + return Object.fromEntries( clonedEntries ); +} + export const getNodesWithStyles = ( tree, blockSelectors ) => { const nodes = []; @@ -558,25 +585,9 @@ export const getNodesWithStyles = ( tree, blockSelectors ) => { return nodes; } - const pickStyleKeys = ( treeToPickFrom ) => - Object.fromEntries( - Object.entries( treeToPickFrom ?? {} ).filter( ( [ key ] ) => - [ - 'border', - 'color', - 'dimensions', - 'spacing', - 'typography', - 'filter', - 'outline', - 'shadow', - ].includes( key ) - ) - ); - // Top-level. const styles = pickStyleKeys( tree.styles ); - if ( !! styles ) { + if ( styles ) { nodes.push( { styles, selector: ROOT_BLOCK_SELECTOR, @@ -584,7 +595,7 @@ export const getNodesWithStyles = ( tree, blockSelectors ) => { } Object.entries( ELEMENTS ).forEach( ( [ name, selector ] ) => { - if ( !! tree.styles?.elements?.[ name ] ) { + if ( tree.styles?.elements?.[ name ] ) { nodes.push( { styles: tree.styles?.elements?.[ name ], selector, @@ -606,10 +617,7 @@ export const getNodesWithStyles = ( tree, blockSelectors ) => { } ); blockStyles.variations = variations; } - if ( - !! blockStyles && - !! blockSelectors?.[ blockName ]?.selector - ) { + if ( blockStyles && blockSelectors?.[ blockName ]?.selector ) { nodes.push( { duotoneSelector: blockSelectors[ blockName ].duotoneSelector, @@ -617,7 +625,7 @@ export const getNodesWithStyles = ( tree, blockSelectors ) => { blockSelectors[ blockName ].fallbackGapValue, hasLayoutSupport: blockSelectors[ blockName ].hasLayoutSupport, - selector: blockSelectors[ blockName ]?.selector, + selector: blockSelectors[ blockName ].selector, styles: blockStyles, featureSelectors: blockSelectors[ blockName ].featureSelectors, @@ -629,9 +637,9 @@ export const getNodesWithStyles = ( tree, blockSelectors ) => { Object.entries( node?.elements ?? {} ).forEach( ( [ elementName, value ] ) => { if ( - !! value && - !! blockSelectors?.[ blockName ] && - !! ELEMENTS?.[ elementName ] + value && + blockSelectors?.[ blockName ] && + ELEMENTS[ elementName ] ) { nodes.push( { styles: value, @@ -677,7 +685,7 @@ export const getNodesWithSettings = ( tree, blockSelectors ) => { // Top-level. const presets = pickPresets( tree.settings ); const custom = tree.settings?.custom; - if ( ! isEmpty( presets ) || !! custom ) { + if ( ! isEmpty( presets ) || custom ) { nodes.push( { presets, custom, @@ -690,7 +698,7 @@ export const getNodesWithSettings = ( tree, blockSelectors ) => { ( [ blockName, node ] ) => { const blockPresets = pickPresets( node ); const blockCustom = node.custom; - if ( ! isEmpty( blockPresets ) || !! blockCustom ) { + if ( ! isEmpty( blockPresets ) || blockCustom ) { nodes.push( { presets: blockPresets, custom: blockCustom, @@ -714,7 +722,7 @@ export const toCustomProperties = ( tree, blockSelectors ) => { } if ( declarations.length > 0 ) { - ruleset = ruleset + `${ selector }{${ declarations.join( ';' ) };}`; + ruleset += `${ selector }{${ declarations.join( ';' ) };}`; } } ); @@ -787,9 +795,9 @@ export const toStyles = ( Object.entries( featureDeclarations ).forEach( ( [ cssSelector, declarations ] ) => { - if ( !! declarations.length ) { + if ( declarations.length ) { const rules = declarations.join( ';' ); - ruleset = ruleset + `${ cssSelector }{${ rules }}`; + ruleset += `${ cssSelector }{${ rules };}`; } } ); @@ -798,20 +806,20 @@ export const toStyles = ( if ( styleVariationSelectors ) { Object.entries( styleVariationSelectors ).forEach( ( [ styleVariationName, styleVariationSelector ] ) => { - if ( styles?.variations?.[ styleVariationName ] ) { + const styleVariations = + styles?.variations?.[ styleVariationName ]; + if ( styleVariations ) { // If the block uses any custom selectors for block support, add those first. if ( featureSelectors ) { const featureDeclarations = getFeatureDeclarations( featureSelectors, - styles?.variations?.[ - styleVariationName - ] + styleVariations ); Object.entries( featureDeclarations ).forEach( ( [ baseSelector, declarations ] ) => { - if ( !! declarations.length ) { + if ( declarations.length ) { const cssSelector = concatFeatureVariationSelectorString( baseSelector, @@ -819,9 +827,7 @@ export const toStyles = ( ); const rules = declarations.join( ';' ); - ruleset = - ruleset + - `${ cssSelector }{${ rules }}`; + ruleset += `${ cssSelector }{${ rules };}`; } } ); @@ -830,39 +836,34 @@ export const toStyles = ( // Otherwise add regular selectors. const styleVariationDeclarations = getStylesDeclarations( - styles?.variations?.[ styleVariationName ], + styleVariations, styleVariationSelector, useRootPaddingAlign, tree ); - if ( !! styleVariationDeclarations.length ) { - ruleset = - ruleset + - `${ styleVariationSelector }{${ styleVariationDeclarations.join( - ';' - ) }}`; + if ( styleVariationDeclarations.length ) { + ruleset += `${ styleVariationSelector }{${ styleVariationDeclarations.join( + ';' + ) };}`; } } } ); } - const duotoneStyles = {}; - if ( styles?.filter ) { - duotoneStyles.filter = styles.filter; - delete styles.filter; - } - // Process duotone styles. if ( duotoneSelector ) { + const duotoneStyles = {}; + if ( styles?.filter ) { + duotoneStyles.filter = styles.filter; + delete styles.filter; + } const duotoneDeclarations = getStylesDeclarations( duotoneStyles ); - if ( duotoneDeclarations.length > 0 ) { - ruleset = - ruleset + - `${ duotoneSelector }{${ duotoneDeclarations.join( - ';' - ) };}`; + if ( duotoneDeclarations.length ) { + ruleset += `${ duotoneSelector }{${ duotoneDeclarations.join( + ';' + ) };}`; } } @@ -889,8 +890,7 @@ export const toStyles = ( tree ); if ( declarations?.length ) { - ruleset = - ruleset + `${ selector }{${ declarations.join( ';' ) };}`; + ruleset += `${ selector }{${ declarations.join( ';' ) };}`; } // Check for pseudo selector in `styles` and handle separately. @@ -924,7 +924,7 @@ export const toStyles = ( ';' ) };}`; - ruleset = ruleset + pseudoRule; + ruleset += pseudoRule; } ); } @@ -965,7 +965,7 @@ export const toStyles = ( const classes = getPresetsClasses( selector, presets ); if ( ! isEmpty( classes ) ) { - ruleset = ruleset + classes; + ruleset += classes; } } ); diff --git a/packages/block-editor/src/components/index.js b/packages/block-editor/src/components/index.js index 66830c4e57aaf..f113ad3b05f63 100644 --- a/packages/block-editor/src/components/index.js +++ b/packages/block-editor/src/components/index.js @@ -95,6 +95,7 @@ export { default as __experimentalSpacingSizesControl } from './spacing-sizes-co export { getSpacingPresetCssVar, isValueSpacingPreset, + getCustomValueFromPreset, } from './spacing-sizes-control/utils'; /* * Content Related Components diff --git a/packages/block-editor/src/components/list-view/index.js b/packages/block-editor/src/components/list-view/index.js index 6e627a5b8b1d6..5b028c52bb937 100644 --- a/packages/block-editor/src/components/list-view/index.js +++ b/packages/block-editor/src/components/list-view/index.js @@ -63,6 +63,7 @@ export const BLOCK_LIST_ITEM_HEIGHT = 36; * @param {?boolean} props.showAppender Flag to show or hide the block appender. Defaults to `false`. * @param {?ComponentType} props.blockSettingsMenu Optional more menu substitution. Defaults to the standard `BlockSettingsDropdown` component. * @param {string} props.rootClientId The client id of the root block from which we determine the blocks to show in the list. + * @param {string} props.description Optional accessible description for the tree grid component. * @param {Ref} ref Forwarded ref */ function ListViewComponent( @@ -74,6 +75,7 @@ function ListViewComponent( showAppender = false, blockSettingsMenu: BlockSettingsMenu = BlockSettingsDropdown, rootClientId, + description, }, ref ) { @@ -229,6 +231,8 @@ function ListViewComponent( onExpandRow={ expandRow } onFocusRow={ focusRow } applicationAriaLabel={ __( 'Block navigation structure' ) } + // eslint-disable-next-line jsx-a11y/aria-props + aria-description={ description } > { diff --git a/packages/block-library/src/buttons/test/edit.native.js b/packages/block-library/src/buttons/test/edit.native.js index 9d23413fd79be..788c1b1e40e4b 100644 --- a/packages/block-library/src/buttons/test/edit.native.js +++ b/packages/block-library/src/buttons/test/edit.native.js @@ -7,7 +7,7 @@ import { within, getBlock, initializeEditor, - changeTextOfRichText, + typeInRichText, } from 'test/helpers'; /** @@ -196,7 +196,7 @@ describe( 'Buttons block', () => { within( secondButtonBlock ).getByLabelText( 'Text input. Empty' ); - changeTextOfRichText( secondButtonInput, 'Hello!' ); + typeInRichText( secondButtonInput, 'Hello!' ); expect( getEditorHtml() ).toMatchSnapshot(); } ); diff --git a/packages/block-library/src/gallery/test/index.native.js b/packages/block-library/src/gallery/test/index.native.js index 53bedb03190f4..fc0efcdbdcb6c 100644 --- a/packages/block-library/src/gallery/test/index.native.js +++ b/packages/block-library/src/gallery/test/index.native.js @@ -3,7 +3,7 @@ */ import { act, - changeTextOfRichText, + typeInRichText, fireEvent, getBlock, getEditorHtml, @@ -173,7 +173,7 @@ describe( 'Gallery block', () => { const captionField = within( getByLabelText( /Gallery caption. Empty/ ) ).getByPlaceholderText( 'Add caption' ); - changeTextOfRichText( + typeInRichText( captionField, 'Bold italic strikethrough gallery caption' ); @@ -197,7 +197,7 @@ describe( 'Gallery block', () => { // Set gallery item caption const captionField = within( galleryItem ).getByPlaceholderText( 'Add caption' ); - changeTextOfRichText( + typeInRichText( captionField, 'Bold italic strikethrough image caption' ); @@ -537,7 +537,7 @@ describe( 'Gallery block', () => { diff --git a/packages/block-library/src/image/block.json b/packages/block-library/src/image/block.json index 9e0e749187aab..92931455c1144 100644 --- a/packages/block-library/src/image/block.json +++ b/packages/block-library/src/image/block.json @@ -104,7 +104,7 @@ } }, "selectors": { - "border": ".wp-block-image img, .wp-block-image .wp-block-image__crop-area", + "border": ".wp-block-image img, .wp-block-image .wp-block-image__crop-area, .wp-block-image .components-placeholder", "filter": { "duotone": ".wp-block-image img, .wp-block-image .components-placeholder" } diff --git a/packages/block-library/src/image/edit.js b/packages/block-library/src/image/edit.js index 175c0b8bcf21a..19e8196dfc7a5 100644 --- a/packages/block-library/src/image/edit.js +++ b/packages/block-library/src/image/edit.js @@ -29,23 +29,6 @@ import { store as noticesStore } from '@wordpress/notices'; */ import Image from './image'; -// Much of this description is duplicated from MediaPlaceholder. -const placeholder = ( content ) => { - return ( - - { content } - - ); -}; - /** * Module constants */ @@ -345,6 +328,27 @@ export function ImageEdit( { className: classes, } ); + // Much of this description is duplicated from MediaPlaceholder. + const placeholder = ( content ) => { + return ( + + { content } + + ); + }; + return (
{ ( temporaryURL || url ) && ( diff --git a/packages/block-library/src/index.native.js b/packages/block-library/src/index.native.js index 1a0f0c017024e..f613a44e72799 100644 --- a/packages/block-library/src/index.native.js +++ b/packages/block-library/src/index.native.js @@ -261,10 +261,14 @@ export const registerCoreBlocks = () => { * than 0, a "new" badge is displayed on the block type within the block * inserter. * + * With the below example, the Audio block will be displayed as "new" until its + * impression count reaches 0, which occurs by various actions decrementing + * the impression count. + * + * { + * [ audio.name ]: 40 + * } + * * @constant {{ string, number }} */ -export const NEW_BLOCK_TYPES = { - [ embed.name ]: 40, - [ search.name ]: 40, - [ audio.name ]: 40, -}; +export const NEW_BLOCK_TYPES = {}; diff --git a/packages/block-library/src/list/test/edit.native.js b/packages/block-library/src/list/test/edit.native.js index 2c9cbcd43ec3e..67470228afc63 100644 --- a/packages/block-library/src/list/test/edit.native.js +++ b/packages/block-library/src/list/test/edit.native.js @@ -2,8 +2,8 @@ * External dependencies */ import { - changeTextOfRichText, - changeAndSelectTextOfRichText, + selectRangeInRichText, + typeInRichText, fireEvent, getEditorHtml, initializeEditor, @@ -79,7 +79,7 @@ describe( 'List block', () => { const listItemField = within( listBlock ).getByPlaceholderText( 'List' ); - changeTextOfRichText( listItemField, 'First list item' ); + typeInRichText( listItemField, 'First list item' ); expect( getEditorHtml() ).toMatchSnapshot(); } ); @@ -347,10 +347,7 @@ describe( 'List block', () => { // backward delete const listItemField = within( listItemBlock ).getByLabelText( /Text input. .*Two.*/ ); - changeAndSelectTextOfRichText( listItemField, 'Two', { - initialSelectionStart: 0, - initialSelectionEnd: 3, - } ); + selectRangeInRichText( listItemField, 0 ); fireEvent( listItemField, 'onKeyDown', { nativeEvent: {}, preventDefault() {}, @@ -398,10 +395,7 @@ describe( 'List block', () => { // backward delete const listItemField = within( listItemBlock ).getByLabelText( /Text input. .*One.*/ ); - changeAndSelectTextOfRichText( listItemField, 'One', { - initialSelectionStart: 0, - initialSelectionEnd: 3, - } ); + selectRangeInRichText( listItemField, 0 ); fireEvent( listItemField, 'onKeyDown', { nativeEvent: {}, preventDefault() {}, diff --git a/packages/block-library/src/paragraph/test/edit.native.js b/packages/block-library/src/paragraph/test/edit.native.js index 02501a3cd49eb..a4dcfdac92041 100644 --- a/packages/block-library/src/paragraph/test/edit.native.js +++ b/packages/block-library/src/paragraph/test/edit.native.js @@ -5,8 +5,7 @@ import { act, addBlock, getBlock, - changeTextOfRichText, - changeAndSelectTextOfRichText, + typeInRichText, fireEvent, getEditorHtml, initializeEditor, @@ -55,10 +54,10 @@ describe( 'Paragraph block', () => { fireEvent.press( paragraphBlock ); const paragraphTextInput = within( paragraphBlock ).getByPlaceholderText( 'Start writing…' ); - changeAndSelectTextOfRichText( + typeInRichText( paragraphTextInput, 'A quick brown fox jumps over the lazy dog.', - { selectionStart: 2, selectionEnd: 7 } + { finalSelectionStart: 2, finalSelectionEnd: 7 } ); fireEvent.press( screen.getByLabelText( 'Bold' ) ); @@ -80,10 +79,10 @@ describe( 'Paragraph block', () => { fireEvent.press( paragraphBlock ); const paragraphTextInput = within( paragraphBlock ).getByPlaceholderText( 'Start writing…' ); - changeAndSelectTextOfRichText( + typeInRichText( paragraphTextInput, 'A quick brown fox jumps over the lazy dog.', - { selectionStart: 2, selectionEnd: 7 } + { finalSelectionStart: 2, finalSelectionEnd: 7 } ); fireEvent.press( screen.getByLabelText( 'Italic' ) ); @@ -105,10 +104,10 @@ describe( 'Paragraph block', () => { fireEvent.press( paragraphBlock ); const paragraphTextInput = within( paragraphBlock ).getByPlaceholderText( 'Start writing…' ); - changeAndSelectTextOfRichText( + typeInRichText( paragraphTextInput, 'A quick brown fox jumps over the lazy dog.', - { selectionStart: 2, selectionEnd: 7 } + { finalSelectionStart: 2, finalSelectionEnd: 7 } ); fireEvent.press( screen.getByLabelText( 'Strikethrough' ) ); @@ -130,7 +129,7 @@ describe( 'Paragraph block', () => { fireEvent.press( paragraphBlock ); const paragraphTextInput = within( paragraphBlock ).getByPlaceholderText( 'Start writing…' ); - changeTextOfRichText( + typeInRichText( paragraphTextInput, 'A quick brown fox jumps over the lazy dog.' ); @@ -155,7 +154,7 @@ describe( 'Paragraph block', () => { fireEvent.press( paragraphBlock ); const paragraphTextInput = within( paragraphBlock ).getByPlaceholderText( 'Start writing…' ); - changeTextOfRichText( + typeInRichText( paragraphTextInput, 'A quick brown fox jumps over the lazy dog.' ); @@ -180,7 +179,7 @@ describe( 'Paragraph block', () => { fireEvent.press( paragraphBlock ); const paragraphTextInput = within( paragraphBlock ).getByPlaceholderText( 'Start writing…' ); - changeTextOfRichText( + typeInRichText( paragraphTextInput, 'A quick brown fox jumps over the lazy dog.' ); @@ -208,9 +207,9 @@ describe( 'Paragraph block', () => { const paragraphTextInput = within( paragraphBlock ).getByPlaceholderText( 'Start writing…' ); const string = 'A quick brown fox jumps over the lazy dog.'; - changeAndSelectTextOfRichText( paragraphTextInput, string, { - selectionStart: string.length / 2, - selectionEnd: string.length / 2, + typeInRichText( paragraphTextInput, string, { + finalSelectionStart: string.length / 2, + finalSelectionEnd: string.length / 2, } ); fireEvent( paragraphTextInput, 'onKeyDown', { nativeEvent: {}, @@ -279,12 +278,12 @@ describe( 'Paragraph block', () => { fireEvent.press( paragraphBlock ); const paragraphTextInput = within( paragraphBlock ).getByPlaceholderText( 'Start writing…' ); - changeAndSelectTextOfRichText( + typeInRichText( paragraphTextInput, 'A quick brown fox jumps over the lazy dog.', { - selectionStart: 2, - selectionEnd: 7, + finalSelectionStart: 2, + finalSelectionEnd: 7, } ); // Await React Navigation: https://github.com/WordPress/gutenberg/issues/35685#issuecomment-961919931 @@ -325,12 +324,12 @@ describe( 'Paragraph block', () => { fireEvent.press( paragraphBlock ); const paragraphTextInput = within( paragraphBlock ).getByPlaceholderText( 'Start writing…' ); - changeAndSelectTextOfRichText( + typeInRichText( paragraphTextInput, 'A quick brown fox jumps over the lazy dog.', { - selectionStart: 2, - selectionEnd: 7, + finalSelectionStart: 2, + finalSelectionEnd: 7, } ); // Await React Navigation: https://github.com/WordPress/gutenberg/issues/35685#issuecomment-961919931 @@ -362,14 +361,10 @@ describe( 'Paragraph block', () => { fireEvent.press( paragraphBlock ); const paragraphTextInput = within( paragraphBlock ).getByPlaceholderText( 'Start writing…' ); - changeAndSelectTextOfRichText( - paragraphTextInput, - ' some text ', - { - selectionStart: 5, - selectionEnd: 14, - } - ); + typeInRichText( paragraphTextInput, ' some text ', { + finalSelectionStart: 5, + finalSelectionEnd: 14, + } ); fireEvent.press( screen.getByLabelText( 'Italic' ) ); // Assert diff --git a/packages/block-library/src/post-excerpt/edit.js b/packages/block-library/src/post-excerpt/edit.js index c88a4ab936882..b84abaa4e98d8 100644 --- a/packages/block-library/src/post-excerpt/edit.js +++ b/packages/block-library/src/post-excerpt/edit.js @@ -56,7 +56,7 @@ export default function PostExcerptEditor( { return true; } return !! select( coreStore ).getPostType( postType )?.supports - .excerpt; + ?.excerpt; }, [ postType ] ); diff --git a/packages/block-library/src/preformatted/test/edit.native.js b/packages/block-library/src/preformatted/test/edit.native.js index 81a7caa29255f..21286a7cdd67d 100644 --- a/packages/block-library/src/preformatted/test/edit.native.js +++ b/packages/block-library/src/preformatted/test/edit.native.js @@ -3,7 +3,7 @@ */ import { addBlock, - changeAndSelectTextOfRichText, + typeInRichText, fireEvent, getEditorHtml, initializeEditor, @@ -67,17 +67,13 @@ describe( 'Preformatted', () => { const preformattedTextInput = await screen.findByPlaceholderText( 'Write preformatted text…' ); - const string = 'A great statement.'; - changeAndSelectTextOfRichText( preformattedTextInput, string, { - selectionStart: string.length, - selectionEnd: string.length, - } ); + typeInRichText( preformattedTextInput, 'A great statement.' ); fireEvent( preformattedTextInput, 'onKeyDown', { nativeEvent: {}, preventDefault() {}, keyCode: ENTER, } ); - changeAndSelectTextOfRichText( preformattedTextInput, 'Again' ); + typeInRichText( preformattedTextInput, 'Again' ); // Assert expect( getEditorHtml() ).toMatchInlineSnapshot( ` diff --git a/packages/block-library/src/pullquote/test/edit.native.js b/packages/block-library/src/pullquote/test/edit.native.js index dd9508f2ec054..9346750627edb 100644 --- a/packages/block-library/src/pullquote/test/edit.native.js +++ b/packages/block-library/src/pullquote/test/edit.native.js @@ -10,7 +10,7 @@ import { fireEvent, within, waitFor, - changeAndSelectTextOfRichText, + typeInRichText, } from 'test/helpers'; /** @@ -35,23 +35,19 @@ describe( 'Pullquote', () => { fireEvent.press( pullquoteBlock ); const pullquoteTextInput = within( pullquoteBlock ).getByPlaceholderText( 'Add quote' ); - const string = 'A great statement.'; - changeAndSelectTextOfRichText( pullquoteTextInput, string, { - selectionStart: string.length, - selectionEnd: string.length, - } ); + typeInRichText( pullquoteTextInput, 'A great statement.' ); fireEvent( pullquoteTextInput, 'onKeyDown', { nativeEvent: {}, preventDefault() {}, keyCode: ENTER, } ); - changeAndSelectTextOfRichText( pullquoteTextInput, 'Again' ); + typeInRichText( pullquoteTextInput, 'Again' ); const citationTextInput = within( citationBlock ).getByPlaceholderText( 'Add citation' ); - changeAndSelectTextOfRichText( citationTextInput, 'A person', { - selectionStart: 2, - selectionEnd: 2, + typeInRichText( citationTextInput, 'A person', { + finalSelectionStart: 2, + finalSelectionEnd: 2, } ); fireEvent( citationTextInput, 'onKeyDown', { nativeEvent: {}, diff --git a/packages/block-library/src/query/edit/inspector-controls/create-new-post-link.js b/packages/block-library/src/query/edit/inspector-controls/create-new-post-link.js new file mode 100644 index 0000000000000..ac9904f0f2d51 --- /dev/null +++ b/packages/block-library/src/query/edit/inspector-controls/create-new-post-link.js @@ -0,0 +1,26 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { createInterpolateElement } from '@wordpress/element'; +import { addQueryArgs } from '@wordpress/url'; + +const CreateNewPostLink = ( { + attributes: { query: { postType } = {} } = {}, +} ) => { + if ( ! postType ) return null; + const newPostUrl = addQueryArgs( 'post-new.php', { + post_type: postType, + } ); + return ( +
+ { createInterpolateElement( + __( 'Add new post' ), + // eslint-disable-next-line jsx-a11y/anchor-has-content + { a: } + ) } +
+ ); +}; + +export default CreateNewPostLink; diff --git a/packages/block-library/src/query/edit/inspector-controls/index.js b/packages/block-library/src/query/edit/inspector-controls/index.js index 95ccb43e58452..2222f7d2d1eff 100644 --- a/packages/block-library/src/query/edit/inspector-controls/index.js +++ b/packages/block-library/src/query/edit/inspector-controls/index.js @@ -12,7 +12,10 @@ import { __experimentalToolsPanelItem as ToolsPanelItem, } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; -import { InspectorControls } from '@wordpress/block-editor'; +import { + InspectorControls, + privateApis as blockEditorPrivateApis, +} from '@wordpress/block-editor'; import { debounce } from '@wordpress/compose'; import { useEffect, useState, useCallback } from '@wordpress/element'; @@ -24,6 +27,8 @@ import AuthorControl from './author-control'; import ParentControl from './parent-control'; import { TaxonomyControls } from './taxonomy-controls'; import StickyControl from './sticky-control'; +import CreateNewPostLink from './create-new-post-link'; +import { unlock } from '../../../private-apis'; import { usePostTypes, useIsPostTypeHierarchical, @@ -32,11 +37,10 @@ import { useTaxonomies, } from '../../utils'; -export default function QueryInspectorControls( { - attributes, - setQuery, - setDisplayLayout, -} ) { +const { BlockInfo } = unlock( blockEditorPrivateApis ); + +export default function QueryInspectorControls( props ) { + const { attributes, setQuery, setDisplayLayout } = props; const { query, displayLayout } = attributes; const { order, @@ -127,6 +131,9 @@ export default function QueryInspectorControls( { return ( <> + + + { showSettingsPanel && ( diff --git a/packages/block-library/src/query/hooks.js b/packages/block-library/src/query/hooks.js index b2bb32a7be7b4..1f367e13d019f 100644 --- a/packages/block-library/src/query/hooks.js +++ b/packages/block-library/src/query/hooks.js @@ -5,8 +5,14 @@ import { __ } from '@wordpress/i18n'; import { createInterpolateElement } from '@wordpress/element'; import { addQueryArgs } from '@wordpress/url'; import { createHigherOrderComponent } from '@wordpress/compose'; -import { InspectorControls } from '@wordpress/block-editor'; +import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; +/** + * Internal dependencies + */ +import { unlock } from '../private-apis'; + +const { BlockInfo } = unlock( blockEditorPrivateApis ); const CreateNewPostLink = ( { attributes: { query: { postType } = {} } = {}, } ) => { @@ -17,7 +23,7 @@ const CreateNewPostLink = ( { return (
{ createInterpolateElement( - __( 'Create a new post for this feed.' ), + __( 'Add new post' ), // eslint-disable-next-line jsx-a11y/anchor-has-content { a: } ) } @@ -31,7 +37,7 @@ const CreateNewPostLink = ( { * @param {Function} BlockEdit Original component * @return {Function} Wrapped component */ -const queryTopInspectorControls = createHigherOrderComponent( +const queryTopBlockInfo = createHigherOrderComponent( ( BlockEdit ) => ( props ) => { const { name, isSelected } = props; if ( name !== 'core/query' || ! isSelected ) { @@ -40,14 +46,14 @@ const queryTopInspectorControls = createHigherOrderComponent( return ( <> - + - + ); }, - 'withInspectorControls' + 'withBlockInfo' ); -export default queryTopInspectorControls; +export default queryTopBlockInfo; diff --git a/packages/block-library/src/query/index.js b/packages/block-library/src/query/index.js index baf58470b76ac..8d82391923603 100644 --- a/packages/block-library/src/query/index.js +++ b/packages/block-library/src/query/index.js @@ -2,7 +2,6 @@ * WordPress dependencies */ import { loop as icon } from '@wordpress/icons'; -import { addFilter } from '@wordpress/hooks'; /** * Internal dependencies @@ -13,7 +12,6 @@ import edit from './edit'; import save from './save'; import variations from './variations'; import deprecated from './deprecated'; -import queryInspectorControls from './hooks'; const { name } = metadata; export { metadata, name }; @@ -26,8 +24,4 @@ export const settings = { deprecated, }; -export const init = () => { - addFilter( 'editor.BlockEdit', 'core/query', queryInspectorControls ); - - return initBlock( { name, metadata, settings } ); -}; +export const init = () => initBlock( { name, metadata, settings } ); diff --git a/packages/block-library/src/quote/test/edit.native.js b/packages/block-library/src/quote/test/edit.native.js index f0279ee03334a..388ef1b8d15f7 100644 --- a/packages/block-library/src/quote/test/edit.native.js +++ b/packages/block-library/src/quote/test/edit.native.js @@ -10,7 +10,7 @@ import { fireEvent, within, waitFor, - changeAndSelectTextOfRichText, + typeInRichText, } from 'test/helpers'; /** @@ -49,11 +49,7 @@ describe( 'Quote', () => { // screen.debug(); let quoteTextInput = within( quoteBlock ).getByPlaceholderText( 'Start writing…' ); - const string = 'A great statement.'; - changeAndSelectTextOfRichText( quoteTextInput, string, { - selectionStart: string.length, - selectionEnd: string.length, - } ); + typeInRichText( quoteTextInput, 'A great statement.' ); fireEvent( quoteTextInput, 'onKeyDown', { nativeEvent: {}, preventDefault() {}, @@ -63,12 +59,12 @@ describe( 'Quote', () => { within( quoteBlock ).getAllByPlaceholderText( 'Start writing…' )[ 1 ]; - changeAndSelectTextOfRichText( quoteTextInput, 'Again.' ); + typeInRichText( quoteTextInput, 'Again.' ); const citationTextInput = within( citationBlock ).getByPlaceholderText( 'Add citation' ); - changeAndSelectTextOfRichText( citationTextInput, 'A person', { - selectionStart: 2, - selectionEnd: 2, + typeInRichText( citationTextInput, 'A person', { + finalSelectionStart: 2, + finalSelectionEnd: 2, } ); fireEvent( citationTextInput, 'onKeyDown', { nativeEvent: {}, diff --git a/packages/block-library/src/spacer/edit.js b/packages/block-library/src/spacer/edit.js index dd788ce8c2a4f..1786c435ebbf2 100644 --- a/packages/block-library/src/spacer/edit.js +++ b/packages/block-library/src/spacer/edit.js @@ -8,6 +8,8 @@ import classnames from 'classnames'; */ import { useBlockProps, + useSetting, + getCustomValueFromPreset, getSpacingPresetCssVar, store as blockEditorStore, } from '@wordpress/block-editor'; @@ -92,13 +94,20 @@ const SpacerEdit = ( { } ); const { orientation } = context; const { orientation: parentOrientation, type } = parentLayout || {}; + // Check if the spacer is inside a flex container. + const isFlexLayout = type === 'flex'; // If the spacer is inside a flex container, it should either inherit the orientation // of the parent or use the flex default orientation. const inheritedOrientation = - ! parentOrientation && type === 'flex' + ! parentOrientation && isFlexLayout ? 'horizontal' : parentOrientation || orientation; - const { height, width } = attributes; + const { height, width, style: blockStyle = {} } = attributes; + + const { layout = {} } = blockStyle; + const { selfStretch, flexSize } = layout; + + const spacingSizes = useSetting( 'spacing.spacingSizes' ); const [ isResizing, setIsResizing ] = useState( false ); const [ temporaryHeight, setTemporaryHeight ] = useState( null ); @@ -110,32 +119,80 @@ const SpacerEdit = ( { const handleOnVerticalResizeStop = ( newHeight ) => { onResizeStop(); + if ( isFlexLayout ) { + setAttributes( { + style: { + ...blockStyle, + layout: { + ...layout, + flexSize: newHeight, + selfStretch: 'fixed', + }, + }, + } ); + } + setAttributes( { height: newHeight } ); setTemporaryHeight( null ); }; const handleOnHorizontalResizeStop = ( newWidth ) => { onResizeStop(); + + if ( isFlexLayout ) { + setAttributes( { + style: { + ...blockStyle, + layout: { + ...layout, + flexSize: newWidth, + selfStretch: 'fixed', + }, + }, + } ); + } + setAttributes( { width: newWidth } ); setTemporaryWidth( null ); }; + const getHeightForVerticalBlocks = () => { + if ( isFlexLayout ) { + return undefined; + } + return temporaryHeight || getSpacingPresetCssVar( height ) || undefined; + }; + + const getWidthForHorizontalBlocks = () => { + if ( isFlexLayout ) { + return undefined; + } + return temporaryWidth || getSpacingPresetCssVar( width ) || undefined; + }; + + const sizeConditionalOnOrientation = + inheritedOrientation === 'horizontal' + ? temporaryWidth || flexSize + : temporaryHeight || flexSize; + const style = { height: inheritedOrientation === 'horizontal' ? 24 - : temporaryHeight || - getSpacingPresetCssVar( height ) || - undefined, + : getHeightForVerticalBlocks(), width: inheritedOrientation === 'horizontal' - ? temporaryWidth || getSpacingPresetCssVar( width ) || undefined + ? getWidthForHorizontalBlocks() : undefined, // In vertical flex containers, the spacer shrinks to nothing without a minimum width. minWidth: - inheritedOrientation === 'vertical' && type === 'flex' + inheritedOrientation === 'vertical' && isFlexLayout ? 48 : undefined, + // Add flex-basis so temporary sizes are respected. + flexBasis: isFlexLayout ? sizeConditionalOnOrientation : undefined, + // Remove flex-grow when resizing. + flexGrow: isFlexLayout && isResizing ? 0 : undefined, }; const resizableBoxWithOrientation = ( blockOrientation ) => { @@ -191,13 +248,93 @@ const SpacerEdit = ( { }; useEffect( () => { - if ( inheritedOrientation === 'horizontal' && ! width ) { + if ( + isFlexLayout && + selfStretch !== 'fill' && + selfStretch !== 'fit' && + ! flexSize + ) { + if ( inheritedOrientation === 'horizontal' ) { + // If spacer is moving from a vertical container to a horizontal container, + // it might not have width but have height instead. + const newSize = + getCustomValueFromPreset( width, spacingSizes ) || + getCustomValueFromPreset( height, spacingSizes ) || + '100px'; + setAttributes( { + width: '0px', + style: { + ...blockStyle, + layout: { + ...layout, + flexSize: newSize, + selfStretch: 'fixed', + }, + }, + } ); + } else { + const newSize = + getCustomValueFromPreset( height, spacingSizes ) || + getCustomValueFromPreset( width, spacingSizes ) || + '100px'; + setAttributes( { + height: '0px', + style: { + ...blockStyle, + layout: { + ...layout, + flexSize: newSize, + selfStretch: 'fixed', + }, + }, + } ); + } + } else if ( + isFlexLayout && + ( selfStretch === 'fill' || selfStretch === 'fit' ) + ) { + if ( inheritedOrientation === 'horizontal' ) { + setAttributes( { + width: undefined, + } ); + } else { + setAttributes( { + height: undefined, + } ); + } + } else if ( ! isFlexLayout && ( selfStretch || flexSize ) ) { + if ( inheritedOrientation === 'horizontal' ) { + setAttributes( { + width: flexSize, + } ); + } else { + setAttributes( { + height: flexSize, + } ); + } setAttributes( { - height: '0px', - width: '72px', + style: { + ...blockStyle, + layout: { + ...layout, + flexSize: undefined, + selfStretch: undefined, + }, + }, } ); } - }, [] ); + }, [ + blockStyle, + flexSize, + height, + inheritedOrientation, + isFlexLayout, + layout, + selfStretch, + setAttributes, + spacingSizes, + width, + ] ); return ( <> @@ -211,13 +348,15 @@ const SpacerEdit = ( { > { resizableBoxWithOrientation( inheritedOrientation ) } - + { ! isFlexLayout && ( + + ) } ); }; diff --git a/packages/block-library/src/verse/test/edit.native.js b/packages/block-library/src/verse/test/edit.native.js index f2dec9d7c0874..bbdacbb90366a 100644 --- a/packages/block-library/src/verse/test/edit.native.js +++ b/packages/block-library/src/verse/test/edit.native.js @@ -6,7 +6,7 @@ import { getEditorHtml, initializeEditor, getBlock, - changeAndSelectTextOfRichText, + typeInRichText, fireEvent, } from 'test/helpers'; @@ -64,17 +64,13 @@ describe( 'Verse block', () => { const verseTextInput = await screen.findByPlaceholderText( 'Write verse…' ); - const string = 'A great statement.'; - changeAndSelectTextOfRichText( verseTextInput, string, { - selectionStart: string.length, - selectionEnd: string.length, - } ); + typeInRichText( verseTextInput, 'A great statement.' ); fireEvent( verseTextInput, 'onKeyDown', { nativeEvent: {}, preventDefault() {}, keyCode: ENTER, } ); - changeAndSelectTextOfRichText( verseTextInput, 'Again' ); + typeInRichText( verseTextInput, 'Again' ); // Assert expect( getEditorHtml() ).toMatchInlineSnapshot( ` diff --git a/packages/blocks/package.json b/packages/blocks/package.json index e0dae87182dc1..ad0e188502362 100644 --- a/packages/blocks/package.json +++ b/packages/blocks/package.json @@ -50,7 +50,7 @@ "is-plain-object": "^5.0.0", "lodash": "^4.17.21", "memize": "^1.1.0", - "rememo": "^4.0.0", + "rememo": "^4.0.2", "remove-accents": "^0.4.2", "showdown": "^1.9.1", "simple-html-tokenizer": "^0.5.7", diff --git a/packages/commands/package.json b/packages/commands/package.json index 0ba9afd4807c8..0d5cc0d81fa4d 100644 --- a/packages/commands/package.json +++ b/packages/commands/package.json @@ -35,7 +35,7 @@ "@wordpress/keyboard-shortcuts": "file:../keyboard-shortcuts", "@wordpress/private-apis": "file:../private-apis", "cmdk": "^0.2.0", - "rememo": "^4.0.0" + "rememo": "^4.0.2" }, "peerDependencies": { "react": "^18.0.0" diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 31156181cf756..60216ef8ae050 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -5,8 +5,11 @@ ### Enhancements - `TreeGrid`: Modify keyboard navigation code to use a data-expanded attribute if aria-expanded is to be controlled outside of the TreeGrid component ([#48461](https://github.com/WordPress/gutenberg/pull/48461)). +- `Modal`: Equalize internal spacing ([#49890](https://github.com/WordPress/gutenberg/pull/49890)). - `Modal`: Increased border radius ([#49870](https://github.com/WordPress/gutenberg/pull/49870)). - `Modal`: Updated spacing / dimensions of `isFullScreen` ([#49894](https://github.com/WordPress/gutenberg/pull/49894)). +- `SlotFill`: Added util for creating private SlotFills and supporting Symbol keys ([#49819](https://github.com/WordPress/gutenberg/pull/49819)). + ### Documentation diff --git a/packages/components/src/color-picker/test/index.tsx b/packages/components/src/color-picker/test/index.tsx index f531455f734e7..8d584d626487a 100644 --- a/packages/components/src/color-picker/test/index.tsx +++ b/packages/components/src/color-picker/test/index.tsx @@ -1,49 +1,14 @@ /** * External dependencies */ -import { render, fireEvent, waitFor } from '@testing-library/react'; +import { screen, render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; /** * Internal dependencies */ import { ColorPicker } from '..'; -/** - * Ordinarily we'd try to select the component by role but the slider role appears - * on several elements and we'd end up encoding assumptions about order when - * trying to select the appropriate element. We might as well just use the class name - * on the container which will be more durable if, for example, the order changes. - */ -function getSaturation( container: HTMLElement ) { - return container.querySelector( - '.react-colorful__saturation .react-colorful__interactive' - ); -} - -type PageXPageY = { pageX: number; pageY: number }; - -// Fix to pass `pageX` and `pageY` -// See https://github.com/testing-library/react-testing-library/issues/268 -class FakeMouseEvent extends MouseEvent { - constructor( type: MouseEvent[ 'type' ], values?: PageXPageY ) { - super( type, { buttons: 1, bubbles: true, ...values } ); - - Object.assign( this, { - pageX: values?.pageX ?? 0, - pageY: values?.pageY ?? 0, - } ); - } -} - -function moveReactColorfulSlider( - sliderElement: Element, - from: PageXPageY, - to: PageXPageY -) { - fireEvent( sliderElement, new FakeMouseEvent( 'mousedown', from ) ); - fireEvent( sliderElement, new FakeMouseEvent( 'mousemove', to ) ); -} - const hslaMatcher = expect.objectContaining( { h: expect.any( Number ), s: expect.any( Number ), @@ -73,99 +38,134 @@ const legacyColorMatcher = { describe( 'ColorPicker', () => { describe( 'legacy props', () => { it( 'should fire onChangeComplete with the legacy color format', async () => { + const user = userEvent.setup(); const onChangeComplete = jest.fn(); - const color = '#fff'; + const color = '#000'; - const { container } = render( + render( ); - const saturation = getSaturation( container ); - - if ( saturation === null ) { - throw new Error( 'The saturation slider could not be found' ); - } + const formatSelector = screen.getByRole( 'combobox' ); + expect( formatSelector ).toBeVisible(); - expect( saturation ).toBeInTheDocument(); + await user.selectOptions( formatSelector, 'hex' ); - moveReactColorfulSlider( - saturation, - { pageX: 0, pageY: 0 }, - { pageX: 10, pageY: 10 } - ); + const hexInput = screen.getByRole( 'textbox' ); + expect( hexInput ).toBeVisible(); - await waitFor( () => - expect( onChangeComplete ).toHaveBeenCalled() - ); + await user.clear( hexInput ); + await user.type( hexInput, '1ab' ); - expect( onChangeComplete ).toHaveBeenCalledWith( + expect( onChangeComplete ).toHaveBeenCalledTimes( 3 ); + expect( onChangeComplete ).toHaveBeenLastCalledWith( legacyColorMatcher ); } ); } ); + describe( 'Hex input', () => { + it( 'should fire onChange with the correct value from the hex input', async () => { + const user = userEvent.setup(); + const onChange = jest.fn(); + const color = '#000'; + + render( + + ); + + const formatSelector = screen.getByRole( 'combobox' ); + expect( formatSelector ).toBeVisible(); + + await user.selectOptions( formatSelector, 'hex' ); - it( 'should fire onChange with the string value', async () => { - const onChange = jest.fn(); - const color = 'rgba(1, 1, 1, 0.5)'; + const hexInput = screen.getByRole( 'textbox' ); + expect( hexInput ).toBeVisible(); - const { container } = render( - - ); + await user.clear( hexInput ); + await user.type( hexInput, '1ab' ); - const saturation = getSaturation( container ); + expect( onChange ).toHaveBeenCalledTimes( 3 ); + expect( onChange ).toHaveBeenLastCalledWith( '#11aabb' ); + } ); + } ); - if ( saturation === null ) { - throw new Error( 'The saturation slider could not be found' ); - } + describe.each( [ + [ 'red', 'Red', '#7dffff' ], + [ 'green', 'Green', '#ff7dff' ], + [ 'blue', 'Blue', '#ffff7d' ], + ] )( 'RGB inputs', ( colorInput, inputLabel, expected ) => { + it( `should fire onChange with the correct value when the ${ colorInput } value is updated`, async () => { + const user = userEvent.setup(); + const onChange = jest.fn(); + const color = '#fff'; - expect( saturation ).toBeInTheDocument(); + render( + + ); - moveReactColorfulSlider( - saturation, - { pageX: 0, pageY: 0 }, - { pageX: 10, pageY: 10 } - ); + const formatSelector = screen.getByRole( 'combobox' ); + expect( formatSelector ).toBeVisible(); - await waitFor( () => expect( onChange ).toHaveBeenCalled() ); + await user.selectOptions( formatSelector, 'rgb' ); - expect( onChange ).toHaveBeenCalledWith( - expect.stringMatching( /^#([a-fA-F0-9]{8})$/ ) - ); - } ); + const inputElement = screen.getByRole( 'spinbutton', { + name: inputLabel, + } ); + expect( inputElement ).toBeVisible(); - it( 'should fire onChange with the HSL value', async () => { - const onChange = jest.fn(); - const color = 'hsla(125, 20%, 50%, 0.5)'; + await user.clear( inputElement ); + await user.type( inputElement, '125' ); - const { container } = render( - - ); + expect( onChange ).toHaveBeenCalledTimes( 4 ); + expect( onChange ).toHaveBeenLastCalledWith( expected ); + } ); + } ); - const saturation = getSaturation( container ); + describe.each( [ + [ 'hue', 'Hue', '#aad52a' ], + [ 'saturation', 'Saturation', '#20dfdf' ], + [ 'lightness', 'Lightness', '#95eaea' ], + ] )( 'HSL inputs', ( colorInput, inputLabel, expected ) => { + it( `should fire onChange with the correct value when the ${ colorInput } value is updated`, async () => { + const user = userEvent.setup(); + const onChange = jest.fn(); + const color = '#2ad5d5'; + + render( + + ); - if ( saturation === null ) { - throw new Error( 'The saturation slider could not be found' ); - } + const formatSelector = screen.getByRole( 'combobox' ); + expect( formatSelector ).toBeVisible(); - expect( saturation ).toBeInTheDocument(); + await user.selectOptions( formatSelector, 'hsl' ); - moveReactColorfulSlider( - saturation, - { pageX: 0, pageY: 0 }, - { pageX: 10, pageY: 10 } - ); + const inputElement = screen.getByRole( 'spinbutton', { + name: inputLabel, + } ); + expect( inputElement ).toBeVisible(); - await waitFor( () => expect( onChange ).toHaveBeenCalled() ); + await user.clear( inputElement ); + await user.type( inputElement, '75' ); - expect( onChange ).toHaveBeenCalledWith( - expect.stringMatching( /^#([a-fA-F0-9]{6})$/ ) - ); + expect( onChange ).toHaveBeenCalledTimes( 3 ); + expect( onChange ).toHaveBeenLastCalledWith( expected ); + } ); } ); } ); diff --git a/packages/components/src/modal/style.scss b/packages/components/src/modal/style.scss index c262259524160..ae643e87034eb 100644 --- a/packages/components/src/modal/style.scss +++ b/packages/components/src/modal/style.scss @@ -72,12 +72,12 @@ .components-modal__header { box-sizing: border-box; border-bottom: $border-width solid transparent; - padding: 0 $grid-unit-40; + padding: $grid-unit-30 $grid-unit-40 $grid-unit-15; display: flex; flex-direction: row; justify-content: space-between; align-items: center; - height: $header-height + $grid-unit-20; + height: $header-height + $grid-unit-15; width: 100%; z-index: z-index(".components-modal__header"); position: absolute; @@ -129,7 +129,7 @@ // Modal contents. .components-modal__content { flex: 1; - margin-top: $header-height + $grid-unit-20; + margin-top: $header-height + $grid-unit-15; padding: 0 $grid-unit-40 $grid-unit-40; overflow: auto; diff --git a/packages/components/src/private-apis.ts b/packages/components/src/private-apis.ts index 07efc6f6a039b..e114559e5088c 100644 --- a/packages/components/src/private-apis.ts +++ b/packages/components/src/private-apis.ts @@ -8,6 +8,7 @@ import { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '@wordpress/pri */ import { default as CustomSelectControl } from './custom-select-control'; import { positionToPlacement as __experimentalPopoverLegacyPositionToPlacement } from './popover/utils'; +import { createPrivateSlotFill } from './slot-fill'; export const { lock, unlock } = __dangerousOptInToUnstableAPIsOnlyForCoreModules( @@ -19,4 +20,5 @@ export const privateApis = {}; lock( privateApis, { CustomSelectControl, __experimentalPopoverLegacyPositionToPlacement, + createPrivateSlotFill, } ); diff --git a/packages/components/src/slot-fill/index.js b/packages/components/src/slot-fill/index.js index 18aea10467701..24e68aa887704 100644 --- a/packages/components/src/slot-fill/index.js +++ b/packages/components/src/slot-fill/index.js @@ -44,13 +44,14 @@ export function Provider( { children, ...props } ) { ); } -export function createSlotFill( name ) { - const FillComponent = ( props ) => ; - FillComponent.displayName = name + 'Fill'; +export function createSlotFill( key ) { + const baseName = typeof key === 'symbol' ? key.description : key; + const FillComponent = ( props ) => ; + FillComponent.displayName = `${ baseName }Fill`; - const SlotComponent = ( props ) => ; - SlotComponent.displayName = name + 'Slot'; - SlotComponent.__unstableName = name; + const SlotComponent = ( props ) => ; + SlotComponent.displayName = `${ baseName }Slot`; + SlotComponent.__unstableName = key; return { Fill: FillComponent, @@ -58,4 +59,11 @@ export function createSlotFill( name ) { }; } +export const createPrivateSlotFill = ( name ) => { + const privateKey = Symbol( name ); + const privateSlotFill = createSlotFill( privateKey ); + + return { privateKey, ...privateSlotFill }; +}; + export { useSlot }; diff --git a/packages/core-data/package.json b/packages/core-data/package.json index 8ed5b7790c96b..34f0a19ee81f9 100644 --- a/packages/core-data/package.json +++ b/packages/core-data/package.json @@ -45,7 +45,7 @@ "equivalent-key-map": "^0.2.2", "fast-deep-equal": "^3.1.3", "memize": "^1.1.0", - "rememo": "^4.0.0", + "rememo": "^4.0.2", "uuid": "^8.3.0" }, "peerDependencies": { diff --git a/packages/e2e-tests/specs/editor/various/navigable-toolbar.test.js b/packages/e2e-tests/specs/editor/various/navigable-toolbar.test.js index ec93851707849..8fb2edf4f6768 100644 --- a/packages/e2e-tests/specs/editor/various/navigable-toolbar.test.js +++ b/packages/e2e-tests/specs/editor/various/navigable-toolbar.test.js @@ -11,38 +11,12 @@ async function isInBlockToolbar() { } ); } -describe.each( [ - [ 'unified', true ], - [ 'contextual', false ], -] )( 'block toolbar (%s: %p)', ( label, isUnifiedToolbar ) => { +describe( 'Block Toolbar', () => { beforeEach( async () => { await createNewPost(); - - await page.evaluate( ( _isUnifiedToolbar ) => { - const { select, dispatch } = wp.data; - const isCurrentlyUnified = - select( 'core/edit-post' ).isFeatureActive( 'fixedToolbar' ); - if ( isCurrentlyUnified !== _isUnifiedToolbar ) { - dispatch( 'core/edit-post' ).toggleFeature( 'fixedToolbar' ); - } - }, isUnifiedToolbar ); } ); - it( 'navigates in and out of toolbar by keyboard (Alt+F10, Escape)', async () => { - // Assumes new post focus starts in title. Create first new - // block by Enter. - await page.keyboard.press( 'Enter' ); - - // [TEMPORARY]: A new paragraph is not technically a block yet - // until starting to type within it. - await page.keyboard.type( 'Example' ); - - // Upward. - await pressKeyWithModifier( 'alt', 'F10' ); - expect( await isInBlockToolbar() ).toBe( true ); - } ); - - if ( ! isUnifiedToolbar ) { + describe( 'Contextual Toolbar', () => { it( 'should not scroll page', async () => { while ( await page.evaluate( () => { @@ -74,5 +48,57 @@ describe.each( [ expect( scrollTopBefore ).toBe( scrollTopAfter ); } ); - } + + it( 'navigates into the toolbar by keyboard (Alt+F10)', async () => { + // Assumes new post focus starts in title. Create first new + // block by Enter. + await page.keyboard.press( 'Enter' ); + + // [TEMPORARY]: A new paragraph is not technically a block yet + // until starting to type within it. + await page.keyboard.type( 'Example' ); + + // Upward. + await pressKeyWithModifier( 'alt', 'F10' ); + + expect( await isInBlockToolbar() ).toBe( true ); + } ); + } ); + + describe( 'Unified Toolbar', () => { + beforeEach( async () => { + // Enable unified toolbar + await page.evaluate( () => { + const { select, dispatch } = wp.data; + const isCurrentlyUnified = + select( 'core/edit-post' ).isFeatureActive( + 'fixedToolbar' + ); + if ( ! isCurrentlyUnified ) { + dispatch( 'core/edit-post' ).toggleFeature( + 'fixedToolbar' + ); + } + } ); + } ); + + it( 'navigates into the toolbar by keyboard (Alt+F10)', async () => { + // Assumes new post focus starts in title. Create first new + // block by Enter. + await page.keyboard.press( 'Enter' ); + + // [TEMPORARY]: A new paragraph is not technically a block yet + // until starting to type within it. + await page.keyboard.type( 'Example' ); + + // Upward. + await pressKeyWithModifier( 'alt', 'F10' ); + + expect( + await page.evaluate( () => { + return document.activeElement.getAttribute( 'aria-label' ); + } ) + ).toBe( 'Show document tools' ); + } ); + } ); } ); diff --git a/packages/edit-post/package.json b/packages/edit-post/package.json index d774dd50b0b9b..ed2e70633451b 100644 --- a/packages/edit-post/package.json +++ b/packages/edit-post/package.json @@ -57,7 +57,7 @@ "@wordpress/widgets": "file:../widgets", "classnames": "^2.3.1", "memize": "^1.1.0", - "rememo": "^4.0.0" + "rememo": "^4.0.2" }, "peerDependencies": { "react": "^18.0.0", diff --git a/packages/edit-post/src/components/layout/style.scss b/packages/edit-post/src/components/layout/style.scss index 229ab58a4e14b..3fdb90dea8555 100644 --- a/packages/edit-post/src/components/layout/style.scss +++ b/packages/edit-post/src/components/layout/style.scss @@ -99,3 +99,15 @@ .edit-post-layout .entities-saved-states__panel-header { height: $header-height + $border-width; } + +.edit-post-layout.has-fixed-toolbar { + // making the header be lower than the content + // so the fixed toolbar can be positioned on top of it + // but only on desktop + @include break-medium() { + .interface-interface-skeleton__header:not(:focus-within) { + z-index: 19; + } + } + +} diff --git a/packages/edit-site/package.json b/packages/edit-site/package.json index 463eceff350ff..db4d51782f93f 100644 --- a/packages/edit-site/package.json +++ b/packages/edit-site/package.json @@ -66,7 +66,7 @@ "lodash": "^4.17.21", "memize": "^1.1.0", "react-autosize-textarea": "^7.1.0", - "rememo": "^4.0.0" + "rememo": "^4.0.2" }, "peerDependencies": { "react": "^18.0.0", diff --git a/packages/edit-site/src/components/global-styles/border-panel.js b/packages/edit-site/src/components/global-styles/border-panel.js index 4d95757e2e9dc..674f91692e4a0 100644 --- a/packages/edit-site/src/components/global-styles/border-panel.js +++ b/packages/edit-site/src/components/global-styles/border-panel.js @@ -16,6 +16,41 @@ const { BorderPanel: StylesBorderPanel, } = unlock( blockEditorPrivateApis ); +function applyFallbackStyle( border ) { + if ( ! border ) { + return border; + } + + const hasColorOrWidth = border.color || border.width; + + if ( ! border.style && hasColorOrWidth ) { + return { ...border, style: 'solid' }; + } + + if ( border.style && ! hasColorOrWidth ) { + return undefined; + } + + return border; +} + +function applyAllFallbackStyles( border ) { + if ( ! border ) { + return border; + } + + if ( hasSplitBorders( border ) ) { + return { + top: applyFallbackStyle( border.top ), + right: applyFallbackStyle( border.right ), + bottom: applyFallbackStyle( border.bottom ), + left: applyFallbackStyle( border.left ), + }; + } + + return applyFallbackStyle( border ); +} + export default function BorderPanel( { name, variation = '' } ) { let prefixParts = []; if ( variation ) { @@ -42,7 +77,7 @@ export default function BorderPanel( { name, variation = '' } ) { // the `border` style property. This means if the theme.json defined // split borders and the user condenses them into a flat border or // vice-versa we'd get both sets of styles which would conflict. - const { border } = newStyle; + const border = applyAllFallbackStyles( newStyle?.border ); const updatedBorder = ! hasSplitBorders( border ) ? { top: border, diff --git a/packages/edit-site/src/components/layout/index.js b/packages/edit-site/src/components/layout/index.js index 7ea454da4f150..609d677c07b3d 100644 --- a/packages/edit-site/src/components/layout/index.js +++ b/packages/edit-site/src/components/layout/index.js @@ -23,6 +23,7 @@ import { useState, useRef } from '@wordpress/element'; import { NavigableRegion } from '@wordpress/interface'; import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts'; import { CommandMenu } from '@wordpress/commands'; +import { store as preferencesStore } from '@wordpress/preferences'; /** * Internal dependencies @@ -68,8 +69,8 @@ export default function Layout() { const { params } = useLocation(); const isListPage = getIsListPage( params ); const isEditorPage = ! isListPage; - const { canvasMode, previousShortcut, nextShortcut } = useSelect( - ( select ) => { + const { hasFixedToolbar, canvasMode, previousShortcut, nextShortcut } = + useSelect( ( select ) => { const { getAllShortcutKeyCombinations } = select( keyboardShortcutsStore ); @@ -82,10 +83,10 @@ export default function Layout() { nextShortcut: getAllShortcutKeyCombinations( 'core/edit-site/next-region' ), + hasFixedToolbar: + select( preferencesStore ).get( 'fixedToolbar' ), }; - }, - [] - ); + }, [] ); const navigateRegionsProps = useNavigateRegions( { previous: previousShortcut, next: nextShortcut, @@ -139,6 +140,7 @@ export default function Layout() { { 'is-full-canvas': isFullCanvas, 'is-edit-mode': canvasMode === 'edit', + 'has-fixed-toolbar': hasFixedToolbar, } ) } > diff --git a/packages/edit-site/src/components/layout/style.scss b/packages/edit-site/src/components/layout/style.scss index 03c206eaf7f5e..c069c471a361e 100644 --- a/packages/edit-site/src/components/layout/style.scss +++ b/packages/edit-site/src/components/layout/style.scss @@ -221,3 +221,18 @@ border-left: $border-width solid $gray-300; } } + +.edit-site-layout.has-fixed-toolbar { + // making the header be lower than the content + // so the fixed toolbar can be positioned on top of it + // but only on desktop + @include break-medium() { + .edit-site-site-hub { + z-index: 4; + } + .edit-site-layout__header:focus-within { + z-index: 3; + } + } + +} diff --git a/packages/edit-site/src/components/sidebar/style.scss b/packages/edit-site/src/components/sidebar/style.scss index fb8bc46d41878..4db6f772beea7 100644 --- a/packages/edit-site/src/components/sidebar/style.scss +++ b/packages/edit-site/src/components/sidebar/style.scss @@ -3,7 +3,7 @@ overflow-y: auto; .components-navigator-screen { - @include custom-scrollbars-on-hover; + @include custom-scrollbars-on-hover($gray-700, $gray-900); } } diff --git a/packages/edit-site/src/components/sync-state-with-url/use-init-edited-entity-from-url.js b/packages/edit-site/src/components/sync-state-with-url/use-init-edited-entity-from-url.js index 0ba70b52d5fda..256280859c044 100644 --- a/packages/edit-site/src/components/sync-state-with-url/use-init-edited-entity-from-url.js +++ b/packages/edit-site/src/components/sync-state-with-url/use-init-edited-entity-from-url.js @@ -14,16 +14,17 @@ import { store as editSiteStore } from '../../store'; export default function useInitEditedEntityFromURL() { const { params: { postId, postType } = {} } = useLocation(); const { isRequestingSite, homepageId, url } = useSelect( ( select ) => { - const { getSite } = select( coreDataStore ); + const { getSite, getUnstableBase } = select( coreDataStore ); const siteData = getSite(); + const base = getUnstableBase(); return { - isRequestingSite: ! siteData, + isRequestingSite: ! base, homepageId: siteData?.show_on_front === 'page' ? siteData.page_on_front : null, - url: siteData?.url, + url: base?.home, }; }, [] ); diff --git a/packages/edit-site/src/hooks/commands/use-wp-admin-commands.js b/packages/edit-site/src/hooks/commands/use-wp-admin-commands.js index 881ce31fc4142..81337b85684ea 100644 --- a/packages/edit-site/src/hooks/commands/use-wp-admin-commands.js +++ b/packages/edit-site/src/hooks/commands/use-wp-admin-commands.js @@ -44,7 +44,7 @@ const getWPAdminAddCommandLoader = ( postType ) => const commands = useMemo( () => [ { - name: 'core/wp-admin/add-' + postType, + name: 'core/wp-admin/add-' + postType + '-' + search, label, icon: plus, callback: () => { diff --git a/packages/editor/package.json b/packages/editor/package.json index c5769be6d1720..68adcde0398c9 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -65,7 +65,7 @@ "lodash": "^4.17.21", "memize": "^1.1.0", "react-autosize-textarea": "^7.1.0", - "rememo": "^4.0.0", + "rememo": "^4.0.2", "remove-accents": "^0.4.2" }, "peerDependencies": { diff --git a/packages/icons/src/index.js b/packages/icons/src/index.js index ba9d28ee266f1..c3f0380728e55 100644 --- a/packages/icons/src/index.js +++ b/packages/icons/src/index.js @@ -120,6 +120,7 @@ export { default as key } from './library/key'; export { default as keyboardClose } from './library/keyboard-close'; export { default as keyboardReturn } from './library/keyboard-return'; export { default as layout } from './library/layout'; +export { default as levelUp } from './library/level-up'; export { default as lifesaver } from './library/lifesaver'; export { default as lineDashed } from './library/line-dashed'; export { default as lineDotted } from './library/line-dotted'; diff --git a/packages/icons/src/library/level-up.js b/packages/icons/src/library/level-up.js new file mode 100644 index 0000000000000..fc992c8dbada5 --- /dev/null +++ b/packages/icons/src/library/level-up.js @@ -0,0 +1,12 @@ +/** + * WordPress dependencies + */ +import { SVG, Path } from '@wordpress/primitives'; + +const levelUp = ( + + + +); + +export default levelUp; diff --git a/packages/keyboard-shortcuts/package.json b/packages/keyboard-shortcuts/package.json index df206934ffaaf..e06c0e637e932 100644 --- a/packages/keyboard-shortcuts/package.json +++ b/packages/keyboard-shortcuts/package.json @@ -29,7 +29,7 @@ "@wordpress/data": "file:../data", "@wordpress/element": "file:../element", "@wordpress/keycodes": "file:../keycodes", - "rememo": "^4.0.0" + "rememo": "^4.0.2" }, "peerDependencies": { "react": "^18.0.0" diff --git a/packages/react-native-editor/jest_ui.config.js b/packages/react-native-editor/jest_ui.config.js index 6998b3b98dc00..43dc5519ee869 100644 --- a/packages/react-native-editor/jest_ui.config.js +++ b/packages/react-native-editor/jest_ui.config.js @@ -16,7 +16,7 @@ module.exports = { platforms: [ 'android', 'ios', 'native' ], }, transform: { - '^.+\\.(js|ts|tsx)$': 'babel-jest', + '^.+\\.(js|ts|tsx|cjs)$': 'babel-jest', }, setupFilesAfterEnv: [ './jest_ui_setup_after_env.js' ], testEnvironment: './jest_ui_test_environment.js', diff --git a/packages/react-native-editor/metro.config.js b/packages/react-native-editor/metro.config.js index 73bc29c15d1b1..05e57e3cfbcab 100644 --- a/packages/react-native-editor/metro.config.js +++ b/packages/react-native-editor/metro.config.js @@ -13,7 +13,7 @@ const packageNames = fs.readdirSync( PACKAGES_DIR ).filter( ( file ) => { module.exports = { watchFolders: [ path.resolve( __dirname, '../..' ) ], resolver: { - sourceExts: [ 'js', 'json', 'scss', 'sass', 'ts', 'tsx' ], + sourceExts: [ 'js', 'cjs', 'json', 'scss', 'sass', 'ts', 'tsx' ], platforms: [ 'native', 'android', 'ios' ], }, transformer: { diff --git a/packages/rich-text/package.json b/packages/rich-text/package.json index 5e7867baeafce..e986aaf7365ed 100644 --- a/packages/rich-text/package.json +++ b/packages/rich-text/package.json @@ -39,7 +39,7 @@ "@wordpress/i18n": "file:../i18n", "@wordpress/keycodes": "file:../keycodes", "memize": "^1.1.0", - "rememo": "^4.0.0" + "rememo": "^4.0.2" }, "peerDependencies": { "react": "^18.0.0" diff --git a/packages/rich-text/src/test/performance/rich-text.native.js b/packages/rich-text/src/test/performance/rich-text.native.js index faa76e9d5bff9..aaaf42c90137f 100644 --- a/packages/rich-text/src/test/performance/rich-text.native.js +++ b/packages/rich-text/src/test/performance/rich-text.native.js @@ -2,7 +2,7 @@ * External dependencies */ import { - changeTextOfRichText, + typeInRichText, fireEvent, measurePerformance, screen, @@ -24,7 +24,7 @@ describe( 'RichText Performance', () => { fireEvent( richTextInput, 'focus' ); - changeTextOfRichText( + typeInRichText( richTextInput, 'Bold italic strikethrough text' ); diff --git a/phpunit/class-wp-theme-json-test.php b/phpunit/class-wp-theme-json-test.php index 5fc566f4e5400..d7a28e2d6e238 100644 --- a/phpunit/class-wp-theme-json-test.php +++ b/phpunit/class-wp-theme-json-test.php @@ -460,7 +460,7 @@ public function test_get_stylesheet() { ); $variables = 'body{--wp--preset--color--grey: grey;--wp--preset--font-family--small: 14px;--wp--preset--font-family--big: 41px;}.wp-block-group{--wp--custom--base-font: 16;--wp--custom--line-height--small: 1.2;--wp--custom--line-height--medium: 1.4;--wp--custom--line-height--large: 1.8;}'; - $styles = 'body { margin: 0;}.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }body{color: var(--wp--preset--color--grey);}a:where(:not(.wp-element-button)){background-color: #333;color: #111;}.wp-block-group{border-radius: 10px;min-height: 50vh;padding: 24px;}.wp-block-group a:where(:not(.wp-element-button)){color: #111;}.wp-block-heading{color: #123456;}.wp-block-heading a:where(:not(.wp-element-button)){background-color: #333;color: #111;font-size: 60px;}.wp-block-post-date{color: #123456;}.wp-block-post-date a:where(:not(.wp-element-button)){background-color: #777;color: #555;}.wp-block-post-excerpt{column-count: 2;}.wp-block-image{margin-bottom: 30px;}.wp-block-image img, .wp-block-image .wp-block-image__crop-area{border-top-left-radius: 10px;border-bottom-right-radius: 1em;}'; + $styles = 'body { margin: 0;}.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }body{color: var(--wp--preset--color--grey);}a:where(:not(.wp-element-button)){background-color: #333;color: #111;}.wp-block-group{border-radius: 10px;min-height: 50vh;padding: 24px;}.wp-block-group a:where(:not(.wp-element-button)){color: #111;}.wp-block-heading{color: #123456;}.wp-block-heading a:where(:not(.wp-element-button)){background-color: #333;color: #111;font-size: 60px;}.wp-block-post-date{color: #123456;}.wp-block-post-date a:where(:not(.wp-element-button)){background-color: #777;color: #555;}.wp-block-post-excerpt{column-count: 2;}.wp-block-image{margin-bottom: 30px;}.wp-block-image img, .wp-block-image .wp-block-image__crop-area, .wp-block-image .components-placeholder{border-top-left-radius: 10px;border-bottom-right-radius: 1em;}'; $presets = '.has-grey-color{color: var(--wp--preset--color--grey) !important;}.has-grey-background-color{background-color: var(--wp--preset--color--grey) !important;}.has-grey-border-color{border-color: var(--wp--preset--color--grey) !important;}.has-small-font-family{font-family: var(--wp--preset--font-family--small) !important;}.has-big-font-family{font-family: var(--wp--preset--font-family--big) !important;}'; $all = $variables . $styles . $presets; diff --git a/post-content.php b/post-content.php index 2d65eeff8b87c..0bdeb30733f6c 100644 --- a/post-content.php +++ b/post-content.php @@ -6,7 +6,7 @@ */ ?> - +

diff --git a/test/native/integration-test-helpers/index.js b/test/native/integration-test-helpers/index.js index 1d1c2666f309b..39f441f4fae93 100644 --- a/test/native/integration-test-helpers/index.js +++ b/test/native/integration-test-helpers/index.js @@ -11,8 +11,8 @@ export { getInnerBlock } from './get-inner-block'; export { initializeEditor } from './initialize-editor'; export { openBlockActionsMenu } from './open-block-actions-menu'; export { openBlockSettings } from './open-block-settings'; -export { changeAndSelectTextOfRichText } from './rich-text-change-and-select-text'; -export { changeTextOfRichText } from './rich-text-change-text'; +export { selectRangeInRichText } from './rich-text-select-range'; +export { typeInRichText } from './rich-text-type'; export { pasteIntoRichText } from './rich-text-paste'; export { setupCoreBlocks } from './setup-core-blocks'; export { setupMediaPicker } from './setup-media-picker'; diff --git a/test/native/integration-test-helpers/rich-text-change-text.js b/test/native/integration-test-helpers/rich-text-change-text.js deleted file mode 100644 index 1eb758855f91c..0000000000000 --- a/test/native/integration-test-helpers/rich-text-change-text.js +++ /dev/null @@ -1,47 +0,0 @@ -/** - * External dependencies - */ -import { fireEvent } from '@testing-library/react-native'; - -let eventCount = 0; - -function stripOuterHtmlTags( string ) { - return string.replace( /^<[^>]*>|<[^>]*>$/g, '' ); -} - -function insertTextAtPosition( text, newText, start, end ) { - return text.slice( 0, start ) + newText + text.slice( end ); -} - -/** - * Changes the text of a RichText component. - * - * @param {import('react-test-renderer').ReactTestInstance} richText RichText test instance. - * @param {string} text Text to be set. - * @param {Object} options - * @param {number} [options.initialSelectionStart] - * @param {number} [options.initialSelectionEnd] - */ -export const changeTextOfRichText = ( richText, text, options = {} ) => { - const currentValueSansOuterHtmlTags = stripOuterHtmlTags( - richText.props.value - ); - const { - initialSelectionStart = currentValueSansOuterHtmlTags.length, - initialSelectionEnd = initialSelectionStart, - } = options; - - fireEvent( richText, 'focus' ); - fireEvent( richText, 'onChange', { - nativeEvent: { - eventCount: ( eventCount += 101 ), - target: undefined, - text: insertTextAtPosition( - currentValueSansOuterHtmlTags, - text, - initialSelectionStart, - initialSelectionEnd - ), - }, - } ); -}; diff --git a/test/native/integration-test-helpers/rich-text-select-range.js b/test/native/integration-test-helpers/rich-text-select-range.js new file mode 100644 index 0000000000000..1ff5025d3df14 --- /dev/null +++ b/test/native/integration-test-helpers/rich-text-select-range.js @@ -0,0 +1,23 @@ +/** + * Internal dependencies + */ +import { typeInRichText } from './rich-text-type'; + +/** + * Select a range within a RichText component. + * + * @param {import('react-test-renderer').ReactTestInstance} richText RichText test instance. + * @param {number} start Selection start position. + * @param {number} end Selection end position. + * + */ +export const selectRangeInRichText = ( richText, start, end = start ) => { + if ( typeof start !== 'number' ) { + throw new Error( 'A numerical range start value must be provided.' ); + } + + typeInRichText( richText, '', { + finalSelectionStart: start, + finalSelectionEnd: end, + } ); +}; diff --git a/test/native/integration-test-helpers/rich-text-change-and-select-text.js b/test/native/integration-test-helpers/rich-text-type.js similarity index 66% rename from test/native/integration-test-helpers/rich-text-change-and-select-text.js rename to test/native/integration-test-helpers/rich-text-type.js index 9decdd78c38d0..3be020325c2ed 100644 --- a/test/native/integration-test-helpers/rich-text-change-and-select-text.js +++ b/test/native/integration-test-helpers/rich-text-type.js @@ -17,38 +17,36 @@ function insertTextAtPosition( text, newText, start, end ) { * Changes the text and selection of a RichText component. * * @param {import('react-test-renderer').ReactTestInstance} richText RichText test instance. - * @param {string} text Text to be set. + * @param {string} text Text to set. * @param {Object} options Configuration options for selection. - * @param {number} [options.initialSelectionStart] - * @param {number} [options.initialSelectionEnd] - * @param {number} [options.selectionStart] Selection start position. - * @param {number} [options.selectionEnd] Selection end position. + * @param {number} [options.initialSelectionStart] Selection start position before the text is inserted. + * @param {number} [options.initialSelectionEnd] Selection end position before the text is inserted. + * @param {number} [options.finalSelectionStart] Selection start position after the text is inserted. + * @param {number} [options.finalSelectionEnd] Selection end position after the text is inserted. */ -export const changeAndSelectTextOfRichText = ( - richText, - text, - options = {} -) => { +export const typeInRichText = ( richText, text, options = {} ) => { const currentValueSansOuterHtmlTags = stripOuterHtmlTags( richText.props.value ); const { initialSelectionStart = currentValueSansOuterHtmlTags.length, initialSelectionEnd = initialSelectionStart, - selectionStart = 0, - selectionEnd = selectionStart, + finalSelectionStart = currentValueSansOuterHtmlTags.length + + text.length, + finalSelectionEnd = finalSelectionStart, } = options; fireEvent( richText, 'focus' ); + // `onSelectionChange` invokes `onChange`; we only need to trigger the former. fireEvent( richText, 'onSelectionChange', - selectionStart, - selectionEnd, + finalSelectionStart, + finalSelectionEnd, text, { nativeEvent: { - eventCount: ( eventCount += 101 ), + eventCount: ( eventCount += 101 ), // Avoid Aztec dropping the event. target: undefined, text: insertTextAtPosition( currentValueSansOuterHtmlTags, diff --git a/test/native/integration/editor-history.native.js b/test/native/integration/editor-history.native.js index 4afdcc5bbefe2..acb3eb23192af 100644 --- a/test/native/integration/editor-history.native.js +++ b/test/native/integration/editor-history.native.js @@ -4,8 +4,7 @@ import { addBlock, getBlock, - changeTextOfRichText, - changeAndSelectTextOfRichText, + typeInRichText, fireEvent, getEditorHtml, initializeEditor, @@ -79,7 +78,7 @@ describe( 'Editor History', () => { fireEvent.press( paragraphBlock ); const paragraphTextInput = within( paragraphBlock ).getByPlaceholderText( 'Start writing…' ); - changeTextOfRichText( + typeInRichText( paragraphTextInput, 'A quick brown fox jumps over the lazy dog.' ); @@ -124,10 +123,10 @@ describe( 'Editor History', () => { fireEvent.press( paragraphBlock ); const paragraphTextInput = within( paragraphBlock ).getByPlaceholderText( 'Start writing…' ); - changeAndSelectTextOfRichText( + typeInRichText( paragraphTextInput, 'A quick brown fox jumps over the lazy dog.', - { selectionStart: 2, selectionEnd: 7 } + { finalSelectionStart: 2, finalSelectionEnd: 7 } ); // Artifical delay to create two history entries for typing and bolding. await new Promise( ( resolve ) => setTimeout( resolve, 1000 ) );