From 714e88cfa2c5896ea964aad8c706e2aebf6dcd09 Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+c4rl0sbr4v0@users.noreply.github.com> Date: Mon, 5 Feb 2024 13:06:02 +0100 Subject: [PATCH 01/41] Fix flaky test (#58668) --- .../directive-on-document.spec.ts | 39 ++++++++++++------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/test/e2e/specs/interactivity/directive-on-document.spec.ts b/test/e2e/specs/interactivity/directive-on-document.spec.ts index 02a6ac99d45c4..2b4f36f51efcc 100644 --- a/test/e2e/specs/interactivity/directive-on-document.spec.ts +++ b/test/e2e/specs/interactivity/directive-on-document.spec.ts @@ -18,39 +18,50 @@ test.describe( 'data-wp-on-document', () => { await utils.deleteAllPosts(); } ); - test( 'callbacks should run whenever the specified event is dispatched', async ( { + test( 'the event listener is attached when the element is added', async ( { page, } ) => { const counter = page.getByTestId( 'counter' ); - await expect( counter ).toHaveText( '0' ); - await page.keyboard.press( 'ArrowDown' ); - await expect( counter ).toHaveText( '1' ); - } ); - test( 'the event listener is removed when the element is removed', async ( { - page, - } ) => { - const counter = page.getByTestId( 'counter' ); - const isEventAttached = page.getByTestId( 'isEventAttached' ); const visibilityButton = page.getByTestId( 'visibility' ); + // Initial value. await expect( counter ).toHaveText( '0' ); - await expect( isEventAttached ).toHaveText( 'yes' ); + + // Make sure the event listener is attached. + await page + .getByTestId( 'isEventAttached' ) + .filter( { hasText: 'yes' } ) + .waitFor(); + + // This keyboard press should increase the counter. await page.keyboard.press( 'ArrowDown' ); await expect( counter ).toHaveText( '1' ); // Remove the element. await visibilityButton.click(); + + // Make sure the event listener is not attached. + await page + .getByTestId( 'isEventAttached' ) + .filter( { hasText: 'no' } ) + .waitFor(); + // This keyboard press should not increase the counter. await page.keyboard.press( 'ArrowDown' ); // Add the element back. await visibilityButton.click(); + + // The counter should have the same value as before. await expect( counter ).toHaveText( '1' ); - // Wait until the effects run again. - await expect( isEventAttached ).toHaveText( 'yes' ); + // Make sure the event listener is re-attached. + await page + .getByTestId( 'isEventAttached' ) + .filter( { hasText: 'yes' } ) + .waitFor(); - // Check that the event listener is attached again. + // This keyboard press should increase the counter. await page.keyboard.press( 'ArrowDown' ); await expect( counter ).toHaveText( '2' ); } ); From 528d2b7d9805e7191884e46ea0abe092e6a7c9c2 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Mon, 5 Feb 2024 13:11:27 +0100 Subject: [PATCH 02/41] Move getAllowedMimeTypes to FontUtils (#58667) Co-authored-by: youknowriad Co-authored-by: getdave --- .../fonts/class-wp-font-library.php | 35 -------- .../fonts/class-wp-font-utils.php | 20 +++++ .../class-wp-rest-font-faces-controller.php | 6 +- .../fonts/font-library/fontLibraryHooks.php | 4 +- .../wpFontLibrary/getMimeTypes.php | 89 ------------------- 5 files changed, 25 insertions(+), 129 deletions(-) delete mode 100644 phpunit/tests/fonts/font-library/wpFontLibrary/getMimeTypes.php diff --git a/lib/compat/wordpress-6.5/fonts/class-wp-font-library.php b/lib/compat/wordpress-6.5/fonts/class-wp-font-library.php index b09e2c5a7b0f3..dec2421cb94df 100644 --- a/lib/compat/wordpress-6.5/fonts/class-wp-font-library.php +++ b/lib/compat/wordpress-6.5/fonts/class-wp-font-library.php @@ -18,29 +18,6 @@ */ class WP_Font_Library { - /** - * Provide the expected mime-type value for font files per-PHP release. Due to differences in the values returned these values differ between PHP versions. - * - * This is necessary until a collection of valid mime-types per-file extension can be provided to 'upload_mimes' filter. - * - * @since 6.5.0 - * - * @param array $php_version_id The version of PHP to provide mime types for. The default is the current PHP version. - * - * @return Array A collection of mime types keyed by file extension. - */ - public static function get_expected_font_mime_types_per_php_version( $php_version_id = PHP_VERSION_ID ) { - - $php_7_ttf_mime_type = $php_version_id >= 70300 ? 'application/font-sfnt' : 'application/x-font-ttf'; - - return array( - 'otf' => 'application/vnd.ms-opentype', - 'ttf' => $php_version_id >= 70400 ? 'font/sfnt' : $php_7_ttf_mime_type, - 'woff' => $php_version_id >= 80100 ? 'font/woff' : 'application/font-woff', - 'woff2' => $php_version_id >= 80100 ? 'font/woff2' : 'application/font-woff2', - ); - } - /** * Font collections. * @@ -142,17 +119,5 @@ public static function get_font_collection( $slug ) { } return new WP_Error( 'font_collection_not_found', 'Font collection not found.' ); } - - /** - * Sets the allowed mime types for fonts. - * - * @since 6.5.0 - * - * @param array $mime_types List of allowed mime types. - * @return array Modified upload directory. - */ - public static function set_allowed_mime_types( $mime_types ) { - return array_merge( $mime_types, self::get_expected_font_mime_types_per_php_version() ); - } } } diff --git a/lib/compat/wordpress-6.5/fonts/class-wp-font-utils.php b/lib/compat/wordpress-6.5/fonts/class-wp-font-utils.php index 2f810640c55f0..b75ce0ec41cf6 100644 --- a/lib/compat/wordpress-6.5/fonts/class-wp-font-utils.php +++ b/lib/compat/wordpress-6.5/fonts/class-wp-font-utils.php @@ -213,5 +213,25 @@ private static function apply_sanitizer( $value, $sanitizer ) { } return call_user_func( $sanitizer, $value ); } + + /** + * Provide the expected mime-type value for font files per-PHP release. Due to differences in the values returned these values differ between PHP versions. + * + * This is necessary until a collection of valid mime-types per-file extension can be provided to 'upload_mimes' filter. + * + * @since 6.5.0 + * + * @return Array A collection of mime types keyed by file extension. + */ + public static function get_allowed_font_mime_types() { + $php_7_ttf_mime_type = PHP_VERSION_ID >= 70300 ? 'application/font-sfnt' : 'application/x-font-ttf'; + + return array( + 'otf' => 'application/vnd.ms-opentype', + 'ttf' => PHP_VERSION_ID >= 70400 ? 'font/sfnt' : $php_7_ttf_mime_type, + 'woff' => PHP_VERSION_ID >= 80100 ? 'font/woff' : 'application/font-woff', + 'woff2' => PHP_VERSION_ID >= 80100 ? 'font/woff2' : 'application/font-woff2', + ); + } } } diff --git a/lib/compat/wordpress-6.5/fonts/class-wp-rest-font-faces-controller.php b/lib/compat/wordpress-6.5/fonts/class-wp-rest-font-faces-controller.php index 22a843e7e69ed..60f04e9ff23df 100644 --- a/lib/compat/wordpress-6.5/fonts/class-wp-rest-font-faces-controller.php +++ b/lib/compat/wordpress-6.5/fonts/class-wp-rest-font-faces-controller.php @@ -847,7 +847,7 @@ protected function sanitize_src( $value ) { * @return array Array containing uploaded file attributes on success, or error on failure. */ protected function handle_font_file_upload( $file ) { - add_filter( 'upload_mimes', array( 'WP_Font_Library', 'set_allowed_mime_types' ) ); + add_filter( 'upload_mimes', array( 'WP_Font_Utils', 'get_allowed_font_mime_types' ) ); add_filter( 'upload_dir', 'wp_get_font_dir' ); $overrides = array( @@ -861,13 +861,13 @@ protected function handle_font_file_upload( $file ) { // See wp_check_filetype_and_ext(). 'test_type' => true, // Only allow uploading font files for this request. - 'mimes' => WP_Font_Library::get_expected_font_mime_types_per_php_version(), + 'mimes' => WP_Font_Utils::get_allowed_font_mime_types(), ); $uploaded_file = wp_handle_upload( $file, $overrides ); remove_filter( 'upload_dir', 'wp_get_font_dir' ); - remove_filter( 'upload_mimes', array( 'WP_Font_Library', 'set_allowed_mime_types' ) ); + remove_filter( 'upload_mimes', array( 'WP_Font_Utils', 'get_allowed_font_mime_types' ) ); return $uploaded_file; } diff --git a/phpunit/tests/fonts/font-library/fontLibraryHooks.php b/phpunit/tests/fonts/font-library/fontLibraryHooks.php index 550f3992c9f73..85d631ecaa45f 100644 --- a/phpunit/tests/fonts/font-library/fontLibraryHooks.php +++ b/phpunit/tests/fonts/font-library/fontLibraryHooks.php @@ -73,7 +73,7 @@ protected function upload_font_file( $font_filename ) { // @core-merge Use `DIR_TESTDATA` instead of `GUTENBERG_DIR_TESTDATA`. $font_file_path = GUTENBERG_DIR_TESTDATA . 'fonts/' . $font_filename; - add_filter( 'upload_mimes', array( 'WP_Font_Library', 'set_allowed_mime_types' ) ); + add_filter( 'upload_mimes', array( 'WP_Font_Utils', 'get_allowed_font_mime_types' ) ); add_filter( 'upload_dir', 'wp_get_font_dir' ); $font_file = wp_upload_bits( $font_filename, @@ -81,7 +81,7 @@ protected function upload_font_file( $font_filename ) { file_get_contents( $font_file_path ) ); remove_filter( 'upload_dir', 'wp_get_font_dir' ); - remove_filter( 'upload_mimes', array( 'WP_Font_Library', 'set_allowed_mime_types' ) ); + remove_filter( 'upload_mimes', array( 'WP_Font_Utils', 'get_allowed_font_mime_types' ) ); return $font_file; } diff --git a/phpunit/tests/fonts/font-library/wpFontLibrary/getMimeTypes.php b/phpunit/tests/fonts/font-library/wpFontLibrary/getMimeTypes.php deleted file mode 100644 index 4fb97e754611f..0000000000000 --- a/phpunit/tests/fonts/font-library/wpFontLibrary/getMimeTypes.php +++ /dev/null @@ -1,89 +0,0 @@ -assertSame( $expected, $mimes ); - } - - /** - * Data provider. - * - * @return array - */ - public function data_should_supply_correct_mime_type_for_php_version() { - return array( - 'version 7.2' => array( - 'php_version_id' => 70200, - 'expected' => array( - 'otf' => 'application/vnd.ms-opentype', - 'ttf' => 'application/x-font-ttf', - 'woff' => 'application/font-woff', - 'woff2' => 'application/font-woff2', - ), - ), - 'version 7.3' => array( - 'php_version_id' => 70300, - 'expected' => array( - 'otf' => 'application/vnd.ms-opentype', - 'ttf' => 'application/font-sfnt', - 'woff' => 'application/font-woff', - 'woff2' => 'application/font-woff2', - ), - ), - 'version 7.4' => array( - 'php_version_id' => 70400, - 'expected' => array( - 'otf' => 'application/vnd.ms-opentype', - 'ttf' => 'font/sfnt', - 'woff' => 'application/font-woff', - 'woff2' => 'application/font-woff2', - ), - ), - 'version 8.0' => array( - 'php_version_id' => 80000, - 'expected' => array( - 'otf' => 'application/vnd.ms-opentype', - 'ttf' => 'font/sfnt', - 'woff' => 'application/font-woff', - 'woff2' => 'application/font-woff2', - ), - ), - 'version 8.1' => array( - 'php_version_id' => 80100, - 'expected' => array( - 'otf' => 'application/vnd.ms-opentype', - 'ttf' => 'font/sfnt', - 'woff' => 'font/woff', - 'woff2' => 'font/woff2', - ), - ), - 'version 8.2' => array( - 'php_version_id' => 80200, - 'expected' => array( - 'otf' => 'application/vnd.ms-opentype', - 'ttf' => 'font/sfnt', - 'woff' => 'font/woff', - 'woff2' => 'font/woff2', - ), - ), - ); - } -} From b1c7ec2dbef7020138462cc98951283db37fdf56 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Mon, 5 Feb 2024 13:57:21 +0100 Subject: [PATCH 03/41] Font Library: Refactor as a singleton (#58669) Co-authored-by: youknowriad Co-authored-by: matiasbenedetto --- .../fonts/class-wp-font-library.php | 53 ++++++++++++++----- ...ss-wp-rest-font-collections-controller.php | 4 +- lib/compat/wordpress-6.5/fonts/fonts.php | 4 +- .../fonts/font-library/wpFontLibrary/base.php | 9 ++-- .../wpFontLibrary/getFontCollection.php | 4 +- .../wpFontLibrary/getFontCollections.php | 6 +-- .../wpFontLibrary/registerFontCollection.php | 6 +-- .../unregisterFontCollection.php | 16 +++--- 8 files changed, 63 insertions(+), 39 deletions(-) diff --git a/lib/compat/wordpress-6.5/fonts/class-wp-font-library.php b/lib/compat/wordpress-6.5/fonts/class-wp-font-library.php index dec2421cb94df..9bf90ca1d9dd8 100644 --- a/lib/compat/wordpress-6.5/fonts/class-wp-font-library.php +++ b/lib/compat/wordpress-6.5/fonts/class-wp-font-library.php @@ -25,7 +25,15 @@ class WP_Font_Library { * * @var array */ - private static $collections = array(); + private $collections = array(); + + /** + * Container for the main instance of the class. + * + * @since 6.5.0 + * @var WP_Font_Library|null + */ + private static $instance = null; /** * Register a new font collection. @@ -39,10 +47,10 @@ class WP_Font_Library { * @return WP_Font_Collection|WP_Error A font collection if it was registered successfully, * or WP_Error object on failure. */ - public static function register_font_collection( $slug, $data_or_file ) { + public function register_font_collection( $slug, $data_or_file ) { $new_collection = new WP_Font_Collection( $slug, $data_or_file ); - if ( self::is_collection_registered( $new_collection->slug ) ) { + if ( $this->is_collection_registered( $new_collection->slug ) ) { $error_message = sprintf( /* translators: %s: Font collection slug. */ __( 'Font collection with slug: "%s" is already registered.', 'gutenberg' ), @@ -55,7 +63,7 @@ public static function register_font_collection( $slug, $data_or_file ) { ); return new WP_Error( 'font_collection_registration_error', $error_message ); } - self::$collections[ $new_collection->slug ] = $new_collection; + $this->collections[ $new_collection->slug ] = $new_collection; return $new_collection; } @@ -67,8 +75,8 @@ public static function register_font_collection( $slug, $data_or_file ) { * @param string $slug Font collection slug. * @return bool True if the font collection was unregistered successfully and false otherwise. */ - public static function unregister_font_collection( $slug ) { - if ( ! self::is_collection_registered( $slug ) ) { + public function unregister_font_collection( $slug ) { + if ( ! $this->is_collection_registered( $slug ) ) { _doing_it_wrong( __METHOD__, /* translators: %s: Font collection slug. */ @@ -77,7 +85,7 @@ public static function unregister_font_collection( $slug ) { ); return false; } - unset( self::$collections[ $slug ] ); + unset( $this->collections[ $slug ] ); return true; } @@ -89,8 +97,8 @@ public static function unregister_font_collection( $slug ) { * @param string $slug Font collection slug. * @return bool True if the font collection is registered and false otherwise. */ - private static function is_collection_registered( $slug ) { - return array_key_exists( $slug, self::$collections ); + private function is_collection_registered( $slug ) { + return array_key_exists( $slug, $this->collections ); } /** @@ -100,8 +108,8 @@ private static function is_collection_registered( $slug ) { * * @return array List of font collections. */ - public static function get_font_collections() { - return self::$collections; + public function get_font_collections() { + return $this->collections; } /** @@ -113,11 +121,28 @@ public static function get_font_collections() { * @return WP_Font_Collection|WP_Error Font collection object, * or WP_Error object if the font collection doesn't exist. */ - public static function get_font_collection( $slug ) { - if ( array_key_exists( $slug, self::$collections ) ) { - return self::$collections[ $slug ]; + public function get_font_collection( $slug ) { + if ( array_key_exists( $slug, $this->collections ) ) { + return $this->collections[ $slug ]; } return new WP_Error( 'font_collection_not_found', 'Font collection not found.' ); } + + /** + * Utility method to retrieve the main instance of the class. + * + * The instance will be created if it does not exist yet. + * + * @since 6.5.0 + * + * @return WP_Font_Library The main instance. + */ + public static function get_instance() { + if ( null === self::$instance ) { + self::$instance = new self(); + } + + return self::$instance; + } } } diff --git a/lib/compat/wordpress-6.5/fonts/class-wp-rest-font-collections-controller.php b/lib/compat/wordpress-6.5/fonts/class-wp-rest-font-collections-controller.php index 372f798f0d2bf..06a073d426abc 100644 --- a/lib/compat/wordpress-6.5/fonts/class-wp-rest-font-collections-controller.php +++ b/lib/compat/wordpress-6.5/fonts/class-wp-rest-font-collections-controller.php @@ -74,7 +74,7 @@ public function register_routes() { * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function get_items( $request ) { - $collections_all = WP_Font_Library::get_font_collections(); + $collections_all = WP_Font_Library::get_instance()->get_font_collections(); $page = $request['page']; $per_page = $request['per_page']; @@ -142,7 +142,7 @@ public function get_items( $request ) { */ public function get_item( $request ) { $slug = $request->get_param( 'slug' ); - $collection = WP_Font_Library::get_font_collection( $slug ); + $collection = WP_Font_Library::get_instance()->get_font_collection( $slug ); // If the collection doesn't exist returns a 404. if ( is_wp_error( $collection ) ) { diff --git a/lib/compat/wordpress-6.5/fonts/fonts.php b/lib/compat/wordpress-6.5/fonts/fonts.php index 1258eaacd99c6..e6913b0d8a57e 100644 --- a/lib/compat/wordpress-6.5/fonts/fonts.php +++ b/lib/compat/wordpress-6.5/fonts/fonts.php @@ -131,7 +131,7 @@ function gutenberg_init_font_library() { * successfully, or WP_Error object on failure. */ function wp_register_font_collection( $slug, $data_or_file ) { - return WP_Font_Library::register_font_collection( $slug, $data_or_file ); + return WP_Font_Library::get_instance()->register_font_collection( $slug, $data_or_file ); } } @@ -145,7 +145,7 @@ function wp_register_font_collection( $slug, $data_or_file ) { * @return bool True if the font collection was unregistered successfully, else false. */ function wp_unregister_font_collection( $slug ) { - return WP_Font_Library::unregister_font_collection( $slug ); + return WP_Font_Library::get_instance()->unregister_font_collection( $slug ); } } diff --git a/phpunit/tests/fonts/font-library/wpFontLibrary/base.php b/phpunit/tests/fonts/font-library/wpFontLibrary/base.php index e8d970f5b3d39..135329e5add73 100644 --- a/phpunit/tests/fonts/font-library/wpFontLibrary/base.php +++ b/phpunit/tests/fonts/font-library/wpFontLibrary/base.php @@ -7,11 +7,10 @@ */ abstract class WP_Font_Library_UnitTestCase extends WP_UnitTestCase { public function reset_font_collections() { - // Resets the private static property WP_Font_Library::$collections to empty array. - $reflection = new ReflectionClass( 'WP_Font_Library' ); - $property = $reflection->getProperty( 'collections' ); - $property->setAccessible( true ); - $property->setValue( array() ); + $collections = WP_Font_Library::get_instance()->get_font_collections(); + foreach ( $collections as $slug => $collection ) { + WP_Font_Library::get_instance()->unregister_font_collection( $slug ); + } } public function set_up() { diff --git a/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollection.php b/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollection.php index 2c246de80b8b9..675efe81aec59 100644 --- a/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollection.php +++ b/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollection.php @@ -19,12 +19,12 @@ public function test_should_get_font_collection() { ); wp_register_font_collection( 'my-font-collection', $mock_collection_data ); - $font_collection = WP_Font_Library::get_font_collection( 'my-font-collection' ); + $font_collection = WP_Font_Library::get_instance()->get_font_collection( 'my-font-collection' ); $this->assertInstanceOf( 'WP_Font_Collection', $font_collection ); } public function test_should_get_no_font_collection_if_the_slug_is_not_registered() { - $font_collection = WP_Font_Library::get_font_collection( 'not-registered-font-collection' ); + $font_collection = WP_Font_Library::get_instance()->get_font_collection( 'not-registered-font-collection' ); $this->assertWPError( $font_collection ); } } diff --git a/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollections.php b/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollections.php index 394c977799a06..f5ca6389b8ff5 100644 --- a/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollections.php +++ b/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollections.php @@ -12,7 +12,7 @@ */ class Tests_Fonts_WpFontLibrary_GetFontCollections extends WP_Font_Library_UnitTestCase { public function test_should_get_an_empty_list() { - $font_collections = WP_Font_Library::get_font_collections(); + $font_collections = WP_Font_Library::get_instance()->get_font_collections(); $this->assertEmpty( $font_collections, 'Should return an empty array.' ); } @@ -23,9 +23,9 @@ public function test_should_get_mock_font_collection() { 'font_families' => array( 'mock' ), ); - WP_Font_Library::register_font_collection( 'my-font-collection', $my_font_collection_config ); + WP_Font_Library::get_instance()->register_font_collection( 'my-font-collection', $my_font_collection_config ); - $font_collections = WP_Font_Library::get_font_collections(); + $font_collections = WP_Font_Library::get_instance()->get_font_collections(); $this->assertNotEmpty( $font_collections, 'Should return an array of font collections.' ); $this->assertCount( 1, $font_collections, 'Should return an array with one font collection.' ); $this->assertArrayHasKey( 'my-font-collection', $font_collections, 'The array should have the key of the registered font collection id.' ); diff --git a/phpunit/tests/fonts/font-library/wpFontLibrary/registerFontCollection.php b/phpunit/tests/fonts/font-library/wpFontLibrary/registerFontCollection.php index 24529d0dd090b..d3b0f126e2e7e 100644 --- a/phpunit/tests/fonts/font-library/wpFontLibrary/registerFontCollection.php +++ b/phpunit/tests/fonts/font-library/wpFontLibrary/registerFontCollection.php @@ -17,7 +17,7 @@ public function test_should_register_font_collection() { 'font_families' => array( 'mock' ), ); - $collection = WP_Font_Library::register_font_collection( 'my-collection', $config ); + $collection = WP_Font_Library::get_instance()->register_font_collection( 'my-collection', $config ); $this->assertInstanceOf( 'WP_Font_Collection', $collection ); } @@ -28,13 +28,13 @@ public function test_should_return_error_if_slug_is_repeated() { ); // Register first collection. - $collection1 = WP_Font_Library::register_font_collection( 'my-collection-1', $mock_collection_data ); + $collection1 = WP_Font_Library::get_instance()->register_font_collection( 'my-collection-1', $mock_collection_data ); $this->assertInstanceOf( 'WP_Font_Collection', $collection1, 'A collection should be registered.' ); // Expects a _doing_it_wrong notice. $this->setExpectedIncorrectUsage( 'WP_Font_Library::register_font_collection' ); // Try to register a second collection with same slug. - WP_Font_Library::register_font_collection( 'my-collection-1', $mock_collection_data ); + WP_Font_Library::get_instance()->register_font_collection( 'my-collection-1', $mock_collection_data ); } } diff --git a/phpunit/tests/fonts/font-library/wpFontLibrary/unregisterFontCollection.php b/phpunit/tests/fonts/font-library/wpFontLibrary/unregisterFontCollection.php index 657d3bb07aaaf..ddb0fa91c1d60 100644 --- a/phpunit/tests/fonts/font-library/wpFontLibrary/unregisterFontCollection.php +++ b/phpunit/tests/fonts/font-library/wpFontLibrary/unregisterFontCollection.php @@ -19,18 +19,18 @@ public function test_should_unregister_font_collection() { ); // Registers two mock font collections. - WP_Font_Library::register_font_collection( 'mock-font-collection-1', $mock_collection_data ); - WP_Font_Library::register_font_collection( 'mock-font-collection-2', $mock_collection_data ); + WP_Font_Library::get_instance()->register_font_collection( 'mock-font-collection-1', $mock_collection_data ); + WP_Font_Library::get_instance()->register_font_collection( 'mock-font-collection-2', $mock_collection_data ); // Unregister mock font collection. - WP_Font_Library::unregister_font_collection( 'mock-font-collection-1' ); - $collections = WP_Font_Library::get_font_collections(); + WP_Font_Library::get_instance()->unregister_font_collection( 'mock-font-collection-1' ); + $collections = WP_Font_Library::get_instance()->get_font_collections(); $this->assertArrayNotHasKey( 'mock-font-collection-1', $collections, 'Font collection was not unregistered.' ); $this->assertArrayHasKey( 'mock-font-collection-2', $collections, 'Font collection was unregistered by mistake.' ); // Unregisters remaining mock font collection. - WP_Font_Library::unregister_font_collection( 'mock-font-collection-2' ); - $collections = WP_Font_Library::get_font_collections(); + WP_Font_Library::get_instance()->unregister_font_collection( 'mock-font-collection-2' ); + $collections = WP_Font_Library::get_instance()->get_font_collections(); $this->assertArrayNotHasKey( 'mock-font-collection-2', $collections, 'Mock font collection was not unregistered.' ); // Checks that all font collections were unregistered. @@ -39,8 +39,8 @@ public function test_should_unregister_font_collection() { public function unregister_non_existing_collection() { // Unregisters non-existing font collection. - WP_Font_Library::unregister_font_collection( 'non-existing-collection' ); - $collections = WP_Font_Library::get_font_collections(); + WP_Font_Library::get_instance()->unregister_font_collection( 'non-existing-collection' ); + $collections = WP_Font_Library::get_instance()->get_font_collections(); $this->assertEmpty( $collections, 'No collections should be registered.' ); } } From 2c7cfbe3586a3139391de54778fc679d32dab13c Mon Sep 17 00:00:00 2001 From: Gutenberg Repository Automation Date: Mon, 5 Feb 2024 13:40:46 +0000 Subject: [PATCH 04/41] Bump plugin version to 17.6.1 --- gutenberg.php | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gutenberg.php b/gutenberg.php index 1036f6f0cca2b..6c6f7f26c6ee9 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.3 * Requires PHP: 7.0 - * Version: 17.6.0 + * Version: 17.6.1 * Author: Gutenberg Team * Text Domain: gutenberg * diff --git a/package-lock.json b/package-lock.json index e79fd2b453408..525fa158bb76f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gutenberg", - "version": "17.6.0", + "version": "17.6.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "gutenberg", - "version": "17.6.0", + "version": "17.6.1", "hasInstallScript": true, "license": "GPL-2.0-or-later", "dependencies": { diff --git a/package.json b/package.json index df97ac16f590d..e1efb1835656e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "17.6.0", + "version": "17.6.1", "private": true, "description": "A new WordPress editor experience.", "author": "The WordPress Contributors", From 995a1eb5db53a3ea8f1f321776a36a420a33cd7b Mon Sep 17 00:00:00 2001 From: Gutenberg Repository Automation Date: Mon, 5 Feb 2024 13:51:26 +0000 Subject: [PATCH 05/41] Update Changelog for 17.6.1 --- changelog.txt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/changelog.txt b/changelog.txt index dcccd8d335abf..55ae8dfcb811b 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,22 @@ == Changelog == += 17.6.1 = + + +## Changelog + +### Font Library + +- Make the API compatible with the upcoming Core patch. ([58671](https://github.com/WordPress/gutenberg/pull/58671)) + + +## Contributors + +The following contributors merged PRs in this release: + +@youknowriad + + = 17.6.0 = ## Changelog From 2cc3c3f71b7f24d249e4c35dd9ea89d918799c8f Mon Sep 17 00:00:00 2001 From: Nick Diego Date: Mon, 5 Feb 2024 07:52:57 -0600 Subject: [PATCH 06/41] Docs: Copy edits and list formatting for main Block Editor Handbook readme (#58652) * Copy edits and list formatting. * Update heading. --- docs/README.md | 59 ++++++++++++++++++++++++-------------------------- 1 file changed, 28 insertions(+), 31 deletions(-) diff --git a/docs/README.md b/docs/README.md index 94f640ae46bfc..bbe3f8e0290e7 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,60 +2,57 @@ Welcome to the Block Editor Handbook. -The [**Block Editor**](https://wordpress.org/gutenberg/) is a modern and up-to-date paradigm for WordPress site building and publishing. It uses a modular system of **Blocks** to compose and format content and is designed to create rich and flexible layouts for websites and digital products. +The [**Block Editor**](https://wordpress.org/gutenberg/) is a modern paradigm for WordPress site building and publishing. It uses a modular system of **blocks** to compose and format content and is designed to create rich and flexible layouts for websites and digital products. -The editor consists of several primary elements, as shown in the following figure: +The Block Editor consists of several primary elements, as shown in the following figure: -![Quick view of the block editor](https://raw.githubusercontent.com/WordPress/gutenberg/trunk/docs/assets/overview-block-editor-2023.png) +![Quick view of the Block Editor](https://raw.githubusercontent.com/WordPress/gutenberg/trunk/docs/assets/overview-block-editor-2023.png) -The elements highlighted in the figure are: +The elements highlighted are: -1. **Inserter**: A panel for inserting blocks into the content canvas -2. **Content canvas**: The content editor, which holds content created with blocks -3. **Settings Sidebar**: A sidebar panel for configuring a block’s settings (among other things) +1. **Inserter:** A panel for inserting blocks into the content canvas +2. **Content canvas:** The content editor, which holds content created with blocks +3. **Settings Sidebar:** A sidebar panel for configuring a block’s settings when selected or the settings of the post -Through the Block editor, you create content modularly using Blocks. There are many [core blocks](https://developer.wordpress.org/block-editor/reference-guides/core-blocks/) ready to be used, and you can also [create your own custom block](https://developer.wordpress.org/block-editor/getting-started/create-block/). +Through the Block Editor, you create content modularly using blocks. Many [blocks](https://developer.wordpress.org/block-editor/reference-guides/core-blocks/) are available in WordPress by default, and you can also [create your own](https://developer.wordpress.org/block-editor/getting-started/create-block/). -A [Block](https://developer.wordpress.org/block-editor/explanations/architecture/key-concepts/#blocks) is a discrete element such as a Paragraph, Heading, Media, or Embed. Each block is treated as a separate element with individual editing and format controls. When all these components are pieced together, they make up the content that is then [stored in the WordPress database](https://developer.wordpress.org/block-editor/explanations/architecture/data-flow/#serialization-and-parsing). +A [block](https://developer.wordpress.org/block-editor/explanations/architecture/key-concepts/#blocks) is a discrete element such as a Paragraph, Heading, Media, or Embed. Each block is treated as a separate element with individual editing and format controls. When all these components are pieced together, they make up the content of the page or post, which is then [stored in the WordPress database](https://developer.wordpress.org/block-editor/explanations/architecture/data-flow/#serialization-and-parsing). The Block Editor is the result of the work done on the [**Gutenberg project**](https://developer.wordpress.org/block-editor/getting-started/faq/#what-is-gutenberg), which aims to revolutionize the WordPress editing experience. -Besides offering an [enhanced editing experience](https://wordpress.org/gutenberg/) through visual content creation tools, the Block Editor is also a powerful developer platform with a [rich feature set of APIs](https://developer.wordpress.org/block-editor/reference-guides/) that allow it to be manipulated and extended many different ways. +Besides offering an [enhanced editing experience](https://wordpress.org/gutenberg/) through visual content creation tools, the Block Editor is also a powerful developer platform with a [rich feature set of APIs](https://developer.wordpress.org/block-editor/reference-guides/) that allow it to be manipulated and extended in countless ways. ## Navigating this handbook -This handbook is focused on block development and is divided into five sections, each serving a different purpose. +This handbook is focused on block development and is divided into five major sections. +- **[Getting Started](https://developer.wordpress.org/block-editor/getting-started/):** For those just starting out with block development, this is where you can get set up with a [development environment](https://developer.wordpress.org/block-editor/getting-started/devenv/) and learn the [fundamentals of block development](https://developer.wordpress.org/block-editor/getting-started/fundamentals/). Its [Quick Start Guide](https://developer.wordpress.org/block-editor/getting-started/quick-start-guide/) and [Tutorial: Build your first block](https://developer.wordpress.org/block-editor/getting-started/tutorial/) are great places to start learning block development. -- [**Getting Started**](https://developer.wordpress.org/block-editor/getting-started/) - For those just starting out with block development, this is where you can get set up with a [development environment](https://developer.wordpress.org/block-editor/getting-started/devenv/) and learn the [fundamentals of block development](https://developer.wordpress.org/block-editor/getting-started/fundamentals/). Its [Quick Start Guide](https://developer.wordpress.org/block-editor/getting-started/quick-start-guide/) and [Tutorial: Build your first block](https://developer.wordpress.org/block-editor/getting-started/tutorial/) are probably the best places to start learning block development. +- **[How-to Guides](https://developer.wordpress.org/block-editor/how-to-guides/):** Here, you can build on what you learned in the Getting Started section and learn how to solve particular problems. You will also find tutorials and example code to reuse in your own projects, such as [working with WordPress data](https://developer.wordpress.org/block-editor/how-to-guides/data-basics/) or [Curating the Editor Experience](https://developer.wordpress.org/block-editor/how-to-guides/curating-the-editor-experience/). +- **[Reference Guides](https://developer.wordpress.org/block-editor/reference-guides/):** This section is the heart of the handbook and is where you can get down to the nitty-gritty and look up the details of the particular API you’re working with or need information on. Among other things, the [Block API Reference](https://developer.wordpress.org/block-editor/reference-guides/block-api/) covers most of what you will want to do with a block, and each [component](https://developer.wordpress.org/block-editor/reference-guides/components/) and [package](https://developer.wordpress.org/block-editor/reference-guides/packages/) is also documented here. _Components are also documented via [Storybook](https://wordpress.github.io/gutenberg/?path=/story/docs-introduction--page)._ -- [**How-to Guides**](https://developer.wordpress.org/block-editor/how-to-guides/) - Here, you can build on what you learned in the Getting Started section and learn how to solve particular problems you might encounter. You can also get tutorials and example code that you can reuse for projects such as [working with WordPress’ data](https://developer.wordpress.org/block-editor/how-to-guides/data-basics/) or [Curating the Editor Experience](https://developer.wordpress.org/block-editor/how-to-guides/curating-the-editor-experience/). +- **[Explanations](https://developer.wordpress.org/block-editor/explanations/):** This section enables you to go deeper and reinforce your practical knowledge with a theoretical understanding of the [Architecture](https://developer.wordpress.org/block-editor/explanations/architecture/) of the Block Editor. - -- [**Reference Guides**](https://developer.wordpress.org/block-editor/reference-guides/) - This section is the heart of the handbook and is where you can get down to the nitty-gritty and look up the details of the particular API you’re working with or need information on. Among other things, the [Block API Reference](https://developer.wordpress.org/block-editor/reference-guides/block-api/) covers most of what you will want to do with a block, and each [component](https://developer.wordpress.org/block-editor/reference-guides/components/) and [package](https://developer.wordpress.org/block-editor/reference-guides/packages/) is also documented here. _Components are also documented via [Storybook](https://wordpress.github.io/gutenberg/?path=/story/docs-introduction--page)._ - -- [**Explanations**](https://developer.wordpress.org/block-editor/explanations/) - This section enables you to go deeper and reinforce your practical knowledge with a theoretical understanding of the [Architecture](https://developer.wordpress.org/block-editor/explanations/architecture/) of the block editor. - -- [**Contributor Guide**](https://developer.wordpress.org/block-editor/contributors/) - Gutenberg is open source software, and anyone is welcome to contribute to the project. This section details how to contribute and can help you choose in which way you want to contribute, whether with [code](https://developer.wordpress.org/block-editor/contributors/code/), [design](https://developer.wordpress.org/block-editor/contributors/design/), [documentation](https://developer.wordpress.org/block-editor/contributors/documentation/), or in some other way. +- **[Contributor Guide](https://developer.wordpress.org/block-editor/contributors/):** Gutenberg is open-source software, and everyone is welcome to contribute to the project. This section details how to contribute, whether with [code](https://developer.wordpress.org/block-editor/contributors/code/), [design](https://developer.wordpress.org/block-editor/contributors/design/), [documentation](https://developer.wordpress.org/block-editor/contributors/documentation/), or in some other way. ## Further resources This handbook should be considered the canonical resource for all things related to block development. However, there are other resources that can help you. -- [**WordPress Developer Blog**](https://developer.wordpress.org/news/) - An ever-growing resource of technical articles covering specific topics related to block development and a wide variety of use cases. The blog is also an excellent way to [keep up with the latest developments in WordPress](https://developer.wordpress.org/news/tag/roundup/). -- [**Learn WordPress**](https://learn.wordpress.org/) - The WordPress hub for learning resources where you can find courses like [Introduction to Block Development: Build your first custom block](https://learn.wordpress.org/course/introduction-to-block-development-build-your-first-custom-block/), [Converting a Shortcode to a Block](https://learn.wordpress.org/course/converting-a-shortcode-to-a-block/) or [Using the WordPress Data Layer](https://learn.wordpress.org/course/using-the-wordpress-data-layer/) -- [**WordPress.tv**](https://wordpress.tv/) - A hub of WordPress-related videos (from talks at WordCamps to recordings of online workshops) curated and moderated by the WordPress.org community. You’re sure to find something to aid your learning about [block development](https://wordpress.tv/?s=block%20development&sort=newest) or the [Block Editor](https://wordpress.tv/?s=block%20editor&sort=relevance) here. -- [**Gutenberg repository**](https://github.com/WordPress/gutenberg/) - Development of the block editor project is carried out in this GitHub repository. It contains the code of interesting packages such as [`block-library`](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src) (core blocks) or [`components`](https://github.com/WordPress/gutenberg/tree/trunk/packages/components) (common UI elements). _The [block-development-examples](https://github.com/WordPress/block-development-examples) repository is another useful reference._ -- [**End User Documentation**](https://wordpress.org/documentation/) - This documentation site is targeted to the end user (not developers), where you can also find documentation about the [Block Editor](https://wordpress.org/documentation/category/block-editor/) and [working with blocks](https://wordpress.org/documentation/article/work-with-blocks/). +- **[WordPress Developer Blog](https://developer.wordpress.org/news/):** An ever-growing resource of technical articles covering specific topics related to block development and a wide variety of use cases. The blog is also an excellent way to [keep up with the latest developments in WordPress](https://developer.wordpress.org/news/tag/roundup/). +- **[Learn WordPress](https://learn.wordpress.org/):** The WordPress hub for learning resources where you can find courses like [Introduction to Block Development: Build your first custom block](https://learn.wordpress.org/course/introduction-to-block-development-build-your-first-custom-block/), [Converting a Shortcode to a Block](https://learn.wordpress.org/course/converting-a-shortcode-to-a-block/), or [Using the WordPress Data Layer](https://learn.wordpress.org/course/using-the-wordpress-data-layer/) +- **[WordPress.tv](https://wordpress.tv/):** A hub of WordPress-related videos (from talks at WordCamps to recordings of online workshops) curated and moderated by the WordPress community. You’re sure to find something to aid your learning about [block development](https://wordpress.tv/?s=block%20development&sort=newest) or the [Block Editor](https://wordpress.tv/?s=block%20editor&sort=relevance) here. +- **[Gutenberg repository](https://github.com/WordPress/gutenberg/):** Development of the Block Editor takes place on GitHub. The repository contains the code of interesting packages such as [`block-library`](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src) (core blocks) or [`components`](https://github.com/WordPress/gutenberg/tree/trunk/packages/components) (common UI elements). _The [block-development-examples](https://github.com/WordPress/block-development-examples) repository is another useful reference._ +- **[End User Documentation](https://wordpress.org/documentation/):** This documentation site is targeted to the end user (not developers), where you can also find documentation about the [Block Editor](https://wordpress.org/documentation/category/block-editor/) and [working with blocks](https://wordpress.org/documentation/article/work-with-blocks/). ## Are you in the right place? -[This handbook](https://developer.wordpress.org/block-editor) is targeted at those seeking to develop for the block editor, but several other handbooks exist for WordPress developers under [developer.wordpress.org](http://developer.wordpress.org/): +The Block Editor Handbook is designed for those looking to create and develop for the Block Editor. However, it's important to note that there are multiple other handbooks available within the [Developer Resources](http://developer.wordpress.org/) that you may find beneficial: -- [/themes](https://developer.wordpress.org/themes) - Theme Handbook -- [/plugins](https://developer.wordpress.org/plugins) - Plugin Handbook -- [/apis](https://developer.wordpress.org/apis) - Common APIs Handbook -- [/advanced-administration](https://developer.wordpress.org/advanced-administration) - WP Advanced Administration Handbook -- [/rest-api](https://developer.wordpress.org/rest-api/) - REST API Handbook -- [/coding-standards](https://developer.wordpress.org/coding-standards) - Best practices for WordPress developers +- [Theme Handbook](https://developer.wordpress.org/themes) +- [Plugin Handbook](https://developer.wordpress.org/plugins) +- [Common APIs Handbook](https://developer.wordpress.org/apis) +- [Advanced Administration Handbook](https://developer.wordpress.org/advanced-administration) +- [REST API Handbook](https://developer.wordpress.org/rest-api/) +- [Coding Standards Handbook](https://developer.wordpress.org/coding-standards) From 280084ed188c6d18db4300c53f2ec17ca50f2749 Mon Sep 17 00:00:00 2001 From: Andrew Hayward Date: Mon, 5 Feb 2024 14:16:20 +0000 Subject: [PATCH 07/41] Removing Reakit as a dependency (#58631) Co-authored-by: andrewhayward Co-authored-by: ciampo Co-authored-by: mirka <0mirka00@git.wordpress.org> Co-authored-by: diegohaz --- .eslintrc.js | 5 - package-lock.json | 111 ------------------ packages/components/CHANGELOG.md | 1 + packages/components/package.json | 1 - packages/components/src/composite/v2.ts | 4 +- .../src/context/wordpress-component.ts | 2 +- packages/components/src/divider/README.md | 2 +- .../src/utils/hooks/use-update-effect.js | 2 +- .../src/components/page-patterns/grid-item.js | 2 - 9 files changed, 6 insertions(+), 124 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 01f8a506addc7..67671070aa2a7 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -46,11 +46,6 @@ const restrictedImports = [ name: 'lodash', message: 'Please use native functionality instead.', }, - { - name: 'reakit', - message: - 'Please use Reakit API through `@wordpress/components` instead.', - }, { name: '@ariakit/react', message: diff --git a/package-lock.json b/package-lock.json index 525fa158bb76f..34c3bc5cc4184 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7334,15 +7334,6 @@ "integrity": "sha512-C16M+IYz0rgRhWZdCmK+h58JMv8vijAA61gmz2rspCSwKwzBebpdcsiUmwrtJRdphuY30i6BSLEOP8ppbNLyLg==", "dev": true }, - "node_modules/@popperjs/core": { - "version": "2.11.7", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.7.tgz", - "integrity": "sha512-Cr4OjIkipTtcXKjAsm8agyleBuDHvxzeBoa1v543lbv1YaIwQjESsVcmjiWiPEbC1FIeHOG/Op9kdCmAmiS3Kw==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" - } - }, "node_modules/@preact/signals-core": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@preact/signals-core/-/signals-core-1.4.0.tgz", @@ -20640,11 +20631,6 @@ "node": ">= 0.8" } }, - "node_modules/body-scroll-lock": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/body-scroll-lock/-/body-scroll-lock-3.1.5.tgz", - "integrity": "sha512-Yi1Xaml0EvNA0OYWxXiYNqY24AfWkbA6w5vxE7GWxtKfzIbZM+Qw+aSmkgsbWzbHiy/RCSkUZBplVxTA+E4jJg==" - }, "node_modules/bonjour-service": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.1.1.tgz", @@ -45465,58 +45451,6 @@ "resolved": "https://registry.npmjs.org/readline/-/readline-1.3.0.tgz", "integrity": "sha512-k2d6ACCkiNYz222Fs/iNze30rRJ1iIicW7JuX/7/cozvih6YCkFZH+J6mAFDVgv0dRBaAyr4jDqC95R2y4IADg==" }, - "node_modules/reakit": { - "version": "1.3.11", - "resolved": "https://registry.npmjs.org/reakit/-/reakit-1.3.11.tgz", - "integrity": "sha512-mYxw2z0fsJNOQKAEn5FJCPTU3rcrY33YZ/HzoWqZX0G7FwySp1wkCYW79WhuYMNIUFQ8s3Baob1RtsEywmZSig==", - "dependencies": { - "@popperjs/core": "^2.5.4", - "body-scroll-lock": "^3.1.5", - "reakit-system": "^0.15.2", - "reakit-utils": "^0.15.2", - "reakit-warning": "^0.6.2" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ariakit" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0", - "react-dom": "^16.8.0 || ^17.0.0" - } - }, - "node_modules/reakit-system": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/reakit-system/-/reakit-system-0.15.2.tgz", - "integrity": "sha512-TvRthEz0DmD0rcJkGamMYx+bATwnGNWJpe/lc8UV2Js8nnPvkaxrHk5fX9cVASFrWbaIyegZHCWUBfxr30bmmA==", - "dependencies": { - "reakit-utils": "^0.15.2" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0", - "react-dom": "^16.8.0 || ^17.0.0" - } - }, - "node_modules/reakit-utils": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/reakit-utils/-/reakit-utils-0.15.2.tgz", - "integrity": "sha512-i/RYkq+W6hvfFmXw5QW7zvfJJT/K8a4qZ0hjA79T61JAFPGt23DsfxwyBbyK91GZrJ9HMrXFVXWMovsKBc1qEQ==", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0", - "react-dom": "^16.8.0 || ^17.0.0" - } - }, - "node_modules/reakit-warning": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/reakit-warning/-/reakit-warning-0.6.2.tgz", - "integrity": "sha512-z/3fvuc46DJyD3nJAUOto6inz2EbSQTjvI/KBQDqxwB0y02HDyeP8IWOJxvkuAUGkWpeSx+H3QWQFSNiPcHtmw==", - "dependencies": { - "reakit-utils": "^0.15.2" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0" - } - }, "node_modules/reassure": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/reassure/-/reassure-0.7.1.tgz", @@ -54097,7 +54031,6 @@ "path-to-regexp": "^6.2.1", "re-resizable": "^6.4.0", "react-colorful": "^5.3.1", - "reakit": "^1.3.11", "remove-accents": "^0.5.0", "use-lilius": "^2.0.1", "uuid": "^9.0.1", @@ -61276,11 +61209,6 @@ "integrity": "sha512-C16M+IYz0rgRhWZdCmK+h58JMv8vijAA61gmz2rspCSwKwzBebpdcsiUmwrtJRdphuY30i6BSLEOP8ppbNLyLg==", "dev": true }, - "@popperjs/core": { - "version": "2.11.7", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.7.tgz", - "integrity": "sha512-Cr4OjIkipTtcXKjAsm8agyleBuDHvxzeBoa1v543lbv1YaIwQjESsVcmjiWiPEbC1FIeHOG/Op9kdCmAmiS3Kw==" - }, "@preact/signals-core": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@preact/signals-core/-/signals-core-1.4.0.tgz", @@ -69207,7 +69135,6 @@ "path-to-regexp": "^6.2.1", "re-resizable": "^6.4.0", "react-colorful": "^5.3.1", - "reakit": "^1.3.11", "remove-accents": "^0.5.0", "use-lilius": "^2.0.1", "uuid": "^9.0.1", @@ -72555,11 +72482,6 @@ } } }, - "body-scroll-lock": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/body-scroll-lock/-/body-scroll-lock-3.1.5.tgz", - "integrity": "sha512-Yi1Xaml0EvNA0OYWxXiYNqY24AfWkbA6w5vxE7GWxtKfzIbZM+Qw+aSmkgsbWzbHiy/RCSkUZBplVxTA+E4jJg==" - }, "bonjour-service": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.1.1.tgz", @@ -91554,39 +91476,6 @@ "resolved": "https://registry.npmjs.org/readline/-/readline-1.3.0.tgz", "integrity": "sha512-k2d6ACCkiNYz222Fs/iNze30rRJ1iIicW7JuX/7/cozvih6YCkFZH+J6mAFDVgv0dRBaAyr4jDqC95R2y4IADg==" }, - "reakit": { - "version": "1.3.11", - "resolved": "https://registry.npmjs.org/reakit/-/reakit-1.3.11.tgz", - "integrity": "sha512-mYxw2z0fsJNOQKAEn5FJCPTU3rcrY33YZ/HzoWqZX0G7FwySp1wkCYW79WhuYMNIUFQ8s3Baob1RtsEywmZSig==", - "requires": { - "@popperjs/core": "^2.5.4", - "body-scroll-lock": "^3.1.5", - "reakit-system": "^0.15.2", - "reakit-utils": "^0.15.2", - "reakit-warning": "^0.6.2" - } - }, - "reakit-system": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/reakit-system/-/reakit-system-0.15.2.tgz", - "integrity": "sha512-TvRthEz0DmD0rcJkGamMYx+bATwnGNWJpe/lc8UV2Js8nnPvkaxrHk5fX9cVASFrWbaIyegZHCWUBfxr30bmmA==", - "requires": { - "reakit-utils": "^0.15.2" - } - }, - "reakit-utils": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/reakit-utils/-/reakit-utils-0.15.2.tgz", - "integrity": "sha512-i/RYkq+W6hvfFmXw5QW7zvfJJT/K8a4qZ0hjA79T61JAFPGt23DsfxwyBbyK91GZrJ9HMrXFVXWMovsKBc1qEQ==" - }, - "reakit-warning": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/reakit-warning/-/reakit-warning-0.6.2.tgz", - "integrity": "sha512-z/3fvuc46DJyD3nJAUOto6inz2EbSQTjvI/KBQDqxwB0y02HDyeP8IWOJxvkuAUGkWpeSx+H3QWQFSNiPcHtmw==", - "requires": { - "reakit-utils": "^0.15.2" - } - }, "reassure": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/reassure/-/reassure-0.7.1.tgz", diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 73b2482587ba3..03411bfca9a72 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -33,6 +33,7 @@ ### Internal - `Composite`: Removing Reakit `Composite` implementation ([#58620](https://github.com/WordPress/gutenberg/pull/58620)). +- Removing Reakit as a dependency of the components package ([#58631](https://github.com/WordPress/gutenberg/pull/58631)). ## 25.16.0 (2024-01-24) diff --git a/packages/components/package.json b/packages/components/package.json index 071fa44727b1d..eb8574e56a46f 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -75,7 +75,6 @@ "path-to-regexp": "^6.2.1", "re-resizable": "^6.4.0", "react-colorful": "^5.3.1", - "reakit": "^1.3.11", "remove-accents": "^0.5.0", "use-lilius": "^2.0.1", "uuid": "^9.0.1", diff --git a/packages/components/src/composite/v2.ts b/packages/components/src/composite/v2.ts index 5e3e8c13fd05e..38d3f628d368b 100644 --- a/packages/components/src/composite/v2.ts +++ b/packages/components/src/composite/v2.ts @@ -1,4 +1,4 @@ -// Until we migrate away from Reakit, the 'current' -// Ariakit implementation is considered a v2. +// Although we have migrated away from Reakit, the 'current' +// Ariakit implementation is still considered a v2. export * from './current'; diff --git a/packages/components/src/context/wordpress-component.ts b/packages/components/src/context/wordpress-component.ts index 03c796bbbc3e4..6e88ed4efb094 100644 --- a/packages/components/src/context/wordpress-component.ts +++ b/packages/components/src/context/wordpress-component.ts @@ -3,7 +3,7 @@ */ import type * as React from 'react'; -// Based on https://github.com/reakit/reakit/blob/master/packages/reakit-utils/src/types.ts +// Based on https://github.com/ariakit/ariakit/blob/reakit/packages/reakit-utils/src/types.ts export type WordPressComponentProps< /** Prop types. */ P, diff --git a/packages/components/src/divider/README.md b/packages/components/src/divider/README.md index c28b6b6b060a0..c81026b7b5587 100644 --- a/packages/components/src/divider/README.md +++ b/packages/components/src/divider/README.md @@ -55,4 +55,4 @@ Divider's orientation. When using inside a flex container, you may need to make ### Inherited props -`Divider` also inherits all of the [`Separator` props](https://reakit.io/docs/separator/). +`Divider` also inherits all of the [`Separator` props](https://ariakit.org/reference/separator#optional-props). diff --git a/packages/components/src/utils/hooks/use-update-effect.js b/packages/components/src/utils/hooks/use-update-effect.js index ab96caf579975..361bce5c9c288 100644 --- a/packages/components/src/utils/hooks/use-update-effect.js +++ b/packages/components/src/utils/hooks/use-update-effect.js @@ -6,7 +6,7 @@ import { useRef, useEffect } from '@wordpress/element'; /** * A `React.useEffect` that will not run on the first render. * Source: - * https://github.com/reakit/reakit/blob/HEAD/packages/reakit-utils/src/useUpdateEffect.ts + * https://github.com/ariakit/ariakit/blob/reakit/packages/reakit-utils/src/useUpdateEffect.ts * * @param {import('react').EffectCallback} effect * @param {import('react').DependencyList} deps diff --git a/packages/edit-site/src/components/page-patterns/grid-item.js b/packages/edit-site/src/components/page-patterns/grid-item.js index 8d2cbaf7806b4..0c1b162dac99d 100644 --- a/packages/edit-site/src/components/page-patterns/grid-item.js +++ b/packages/edit-site/src/components/page-patterns/grid-item.js @@ -171,8 +171,6 @@ function GridItem( { categoryId, item, ...props } ) {
  • - ); -} - -export default BackButton; diff --git a/packages/edit-site/src/components/block-editor/site-editor-canvas.js b/packages/edit-site/src/components/block-editor/site-editor-canvas.js index e946068ea1d84..f1e4a5fa3f2c6 100644 --- a/packages/edit-site/src/components/block-editor/site-editor-canvas.js +++ b/packages/edit-site/src/components/block-editor/site-editor-canvas.js @@ -11,7 +11,6 @@ import { useViewportMatch, useResizeObserver } from '@wordpress/compose'; /** * Internal dependencies */ -import BackButton from './back-button'; import ResizableEditor from './resizable-editor'; import EditorCanvas from './editor-canvas'; import EditorCanvasContainer from '../editor-canvas-container'; @@ -20,6 +19,7 @@ import { store as editSiteStore } from '../../store'; import { FOCUSABLE_ENTITIES, NAVIGATION_POST_TYPE, + TEMPLATE_POST_TYPE, } from '../../utils/constants'; import { unlock } from '../../lock-unlock'; import { privateApis as routerPrivateApis } from '@wordpress/router'; @@ -54,7 +54,9 @@ export default function SiteEditorCanvas() { isFocusMode && ! isViewMode && // Disable resizing in mobile viewport. - ! isMobileViewport; + ! isMobileViewport && + // Disable resizing when editing a template in focus mode. + templateType !== TEMPLATE_POST_TYPE; const isTemplateTypeNavigation = templateType === NAVIGATION_POST_TYPE; const isNavigationFocusMode = isTemplateTypeNavigation && isFocusMode; @@ -74,7 +76,6 @@ export default function SiteEditorCanvas() { 'is-view-mode': isViewMode, } ) } > - { + const isFocusMode = + location.params.focusMode || + FOCUSABLE_ENTITIES.includes( location.params.postType ); + return isFocusMode ? () => history.back() : undefined; + }, [ location.params.focusMode, location.params.postType, history ] ); + return goBack; +} + export function useSpecificEditorSettings() { const getPostLinkProps = usePostLinkProps(); const { templateSlug, canvasMode, settings, postWithTemplate } = useSelect( @@ -118,6 +133,7 @@ export function useSpecificEditorSettings() { ); const archiveLabels = useArchiveLabel( templateSlug ); const defaultRenderingMode = postWithTemplate ? 'template-locked' : 'all'; + const goBack = useGoBack(); const defaultEditorSettings = useMemo( () => { return { ...settings, @@ -127,6 +143,7 @@ export function useSpecificEditorSettings() { focusMode: canvasMode !== 'view', defaultRenderingMode, getPostLinkProps, + goBack, // I wonder if they should be set in the post editor too __experimentalArchiveTitleTypeLabel: archiveLabels.archiveTypeLabel, __experimentalArchiveTitleNameLabel: archiveLabels.archiveNameLabel, @@ -136,6 +153,7 @@ export function useSpecificEditorSettings() { canvasMode, defaultRenderingMode, getPostLinkProps, + goBack, archiveLabels.archiveTypeLabel, archiveLabels.archiveNameLabel, ] ); diff --git a/test/e2e/specs/site-editor/pages.spec.js b/test/e2e/specs/site-editor/pages.spec.js index 9c582c5edb397..717fcfe0a39aa 100644 --- a/test/e2e/specs/site-editor/pages.spec.js +++ b/test/e2e/specs/site-editor/pages.spec.js @@ -150,7 +150,7 @@ test.describe( 'Pages', () => { // Go back to page editing focus. await page - .getByRole( 'region', { name: 'Editor content' } ) + .getByRole( 'region', { name: 'Editor top bar' } ) .getByRole( 'button', { name: 'Back' } ) .click(); From 4ea44317dd31f1264fc98473a5a16278ce9ee4b7 Mon Sep 17 00:00:00 2001 From: Andrei Draganescu Date: Tue, 6 Feb 2024 04:54:35 +0200 Subject: [PATCH 29/41] Add default restoration of UI when exiting distraction free mode (#58455) * remove duplicate pref setter, show breadcrumbs if dfm is off, always restore toolbar to non fixed * dispatch the toggle action for writing menu items * add handleToggling prop to the pref toggle component for toggle handlers that also toggle, fixedbug with undo link in snackbar --- .../components/header/writing-menu/index.js | 19 +++------------ packages/edit-post/src/store/actions.js | 23 ++++++++++++++++--- .../edit-site/src/components/editor/index.js | 5 +++- .../header-edit-mode/more-menu/index.js | 22 ++++-------------- packages/edit-site/src/store/actions.js | 23 ++++++++++++++++--- .../preference-toggle-menu-item/index.js | 5 +++- 6 files changed, 56 insertions(+), 41 deletions(-) diff --git a/packages/edit-post/src/components/header/writing-menu/index.js b/packages/edit-post/src/components/header/writing-menu/index.js index 754e65af5200d..54ad4104dccf1 100644 --- a/packages/edit-post/src/components/header/writing-menu/index.js +++ b/packages/edit-post/src/components/header/writing-menu/index.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { useDispatch, useRegistry } from '@wordpress/data'; +import { useDispatch } from '@wordpress/data'; import { MenuGroup } from '@wordpress/components'; import { __, _x } from '@wordpress/i18n'; import { useViewportMatch } from '@wordpress/compose'; @@ -10,7 +10,6 @@ import { PreferenceToggleMenuItem, store as preferencesStore, } from '@wordpress/preferences'; -import { store as editorStore } from '@wordpress/editor'; /** * Internal dependencies @@ -18,21 +17,8 @@ import { store as editorStore } from '@wordpress/editor'; import { store as postEditorStore } from '../../../store'; function WritingMenu() { - const registry = useRegistry(); - - const { closeGeneralSidebar } = useDispatch( postEditorStore ); const { set: setPreference } = useDispatch( preferencesStore ); - const { setIsInserterOpened, setIsListViewOpened } = - useDispatch( editorStore ); - - const toggleDistractionFree = () => { - registry.batch( () => { - setPreference( 'core', 'fixedToolbar', true ); - setIsInserterOpened( false ); - setIsListViewOpened( false ); - closeGeneralSidebar(); - } ); - }; + const { toggleDistractionFree } = useDispatch( postEditorStore ); const turnOffDistractionFree = () => { setPreference( 'core', 'distractionFree', false ); @@ -59,6 +45,7 @@ function WritingMenu() { { registry @@ -568,9 +573,21 @@ export const toggleDistractionFree = { label: __( 'Undo' ), onClick: () => { - registry - .dispatch( preferencesStore ) - .toggle( 'core', 'distractionFree' ); + registry.batch( () => { + registry + .dispatch( preferencesStore ) + .set( + 'core', + 'fixedToolbar', + isDistractionFree ? true : false + ); + registry + .dispatch( preferencesStore ) + .toggle( + 'core', + 'distractionFree' + ); + } ); }, }, ], diff --git a/packages/edit-site/src/components/editor/index.js b/packages/edit-site/src/components/editor/index.js index 402004c603518..440775f9e5d48 100644 --- a/packages/edit-site/src/components/editor/index.js +++ b/packages/edit-site/src/components/editor/index.js @@ -106,6 +106,7 @@ export default function Editor( { isLoading } ) { isRightSidebarOpen, isInserterOpen, isListViewOpen, + isDistractionFree, showIconLabels, showBlockBreadcrumbs, postTypeLabel, @@ -140,6 +141,7 @@ export default function Editor( { isLoading } ) { isRightSidebarOpen: getActiveComplementaryArea( editSiteStore.name ), + isDistractionFree: get( 'core', 'distractionFree' ), showBlockBreadcrumbs: get( 'core', 'showBlockBreadcrumbs' ), showIconLabels: get( 'core', 'showIconLabels' ), postTypeLabel: getPostTypeLabel(), @@ -150,6 +152,7 @@ export default function Editor( { isLoading } ) { const isEditMode = canvasMode === 'edit'; const showVisualEditor = isViewMode || editorMode === 'visual'; const shouldShowBlockBreadcrumbs = + ! isDistractionFree && showBlockBreadcrumbs && isEditMode && showVisualEditor && @@ -210,7 +213,7 @@ export default function Editor( { isLoading } ) { { isEditMode && } { return select( coreStore ).getCurrentTheme().is_block_theme; }, [] ); - const toggleDistractionFree = () => { - registry.batch( () => { - setPreference( 'core', 'fixedToolbar', true ); - setIsInserterOpened( false ); - setIsListViewOpened( false ); - closeGeneralSidebar(); - } ); - }; + const { toggleDistractionFree } = useDispatch( editSiteStore ); const turnOffDistractionFree = () => { setPreference( 'core', 'distractionFree', false ); @@ -90,9 +77,10 @@ export default function MoreMenu( { showIconLabels } ) { { registry @@ -586,9 +591,21 @@ export const toggleDistractionFree = { label: __( 'Undo' ), onClick: () => { - registry - .dispatch( preferencesStore ) - .toggle( 'core', 'distractionFree' ); + registry.batch( () => { + registry + .dispatch( preferencesStore ) + .set( + 'core', + 'fixedToolbar', + isDistractionFree ? true : false + ); + registry + .dispatch( preferencesStore ) + .toggle( + 'core', + 'distractionFree' + ); + } ); }, }, ], diff --git a/packages/preferences/src/components/preference-toggle-menu-item/index.js b/packages/preferences/src/components/preference-toggle-menu-item/index.js index 9d2b8d353e944..c5110463c9800 100644 --- a/packages/preferences/src/components/preference-toggle-menu-item/index.js +++ b/packages/preferences/src/components/preference-toggle-menu-item/index.js @@ -20,6 +20,7 @@ export default function PreferenceToggleMenuItem( { messageActivated, messageDeactivated, shortcut, + handleToggling = true, onToggle = () => null, disabled = false, } ) { @@ -56,7 +57,9 @@ export default function PreferenceToggleMenuItem( { isSelected={ isActive } onClick={ () => { onToggle(); - toggle( scope, name ); + if ( handleToggling ) { + toggle( scope, name ); + } speakMessage(); } } role="menuitemcheckbox" From 8afe850e85b2751527487b70cdf6001c3e385a70 Mon Sep 17 00:00:00 2001 From: Shreyash Date: Tue, 6 Feb 2024 08:50:14 +0530 Subject: [PATCH 30/41] fix: innerBlocks schema description in block.json (#58649) * Fix innerBlocks schema description in block.json (#58381) * docs: Update innerBlocks description in block.json Co-authored-by: Aki Hamano <54422211+t-hamano@users.noreply.github.com> * docs: Update innerBlocks description in block.json --------- Unlinked contributors: shreyash3087, MHRSRoni. Co-authored-by: t-hamano Co-authored-by: ocean90 --- schemas/json/block.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/schemas/json/block.json b/schemas/json/block.json index 9c1e89b8a601a..7234f36a5e0ec 100644 --- a/schemas/json/block.json +++ b/schemas/json/block.json @@ -745,7 +745,7 @@ }, "innerBlocks": { "type": "array", - "description": "Set the inner blocks that should be used within the block example. The blocks should be defined as a nested array like this: \n\n [ [ 'core/heading', { content: 'This is an Example' }, [] ] ]\n\n Where each block itself is an array that contains the block name, the block attributes, and the blocks inner blocks." + "description": "Set the inner blocks that should be used within the block example. The blocks should be defined as a nested array like this:\n\n[ { \"name\": \"core/heading\", \"attributes\": { \"content\": \"This is an Example\" } } ]\n\nWhere each block itself is an object that contains the block name, the block attributes, and the blocks inner blocks." } } }, From be540431d515cf75921ee54c07a07a98244601c0 Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Tue, 6 Feb 2024 08:31:46 +0400 Subject: [PATCH 31/41] Migrate remaining 'pattern block' e2e tests to Playwright (#58486) * Migrate remaining 'pattern block' e2e tests to Playwright * Remove old test files Co-authored-by: Mamaduka Co-authored-by: t-hamano --- .../__snapshots__/pattern-blocks.test.js.snap | 11 - .../editor/various/pattern-blocks.test.js | 289 ----------------- .../e2e/specs/editor/various/patterns.spec.js | 298 ++++++++++++++++++ 3 files changed, 298 insertions(+), 300 deletions(-) delete mode 100644 packages/e2e-tests/specs/editor/various/__snapshots__/pattern-blocks.test.js.snap delete mode 100644 packages/e2e-tests/specs/editor/various/pattern-blocks.test.js diff --git a/packages/e2e-tests/specs/editor/various/__snapshots__/pattern-blocks.test.js.snap b/packages/e2e-tests/specs/editor/various/__snapshots__/pattern-blocks.test.js.snap deleted file mode 100644 index 325deae67bbfa..0000000000000 --- a/packages/e2e-tests/specs/editor/various/__snapshots__/pattern-blocks.test.js.snap +++ /dev/null @@ -1,11 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Pattern blocks can be created from multiselection and converted back to regular blocks 1`] = ` -" -

    Hello there!

    - - - -

    Second paragraph

    -" -`; diff --git a/packages/e2e-tests/specs/editor/various/pattern-blocks.test.js b/packages/e2e-tests/specs/editor/various/pattern-blocks.test.js deleted file mode 100644 index 32d5952f57259..0000000000000 --- a/packages/e2e-tests/specs/editor/various/pattern-blocks.test.js +++ /dev/null @@ -1,289 +0,0 @@ -/** - * WordPress dependencies - */ -import { - clickMenuItem, - insertBlock, - insertPattern, - createNewPost, - clickBlockToolbarButton, - pressKeyWithModifier, - getEditedPostContent, - trashAllPosts, - visitAdminPage, - toggleGlobalBlockInserter, - openDocumentSettingsSidebar, - saveDraft, - createReusableBlock, - canvas, -} from '@wordpress/e2e-test-utils'; - -const patternBlockNameInputSelector = - '.patterns-menu-items__convert-modal .components-text-control__input'; -const patternBlockInspectorNameSelector = - '.block-editor-block-inspector h2.block-editor-block-card__title'; -const syncToggleSelectorChecked = - '.patterns-menu-items__convert-modal .components-form-toggle.is-checked'; - -const clearAllBlocks = async () => { - // Remove all blocks from the post so that we're working with a clean slate. - await page.evaluate( () => { - const blocks = wp.data.select( 'core/block-editor' ).getBlocks(); - const clientIds = blocks.map( ( block ) => block.clientId ); - wp.data.dispatch( 'core/block-editor' ).removeBlocks( clientIds ); - } ); -}; - -describe( 'Pattern blocks', () => { - afterAll( async () => { - await trashAllPosts( 'wp_block' ); - } ); - - beforeEach( async () => { - await createNewPost(); - } ); - - it( 'can be created, inserted, and converted to a regular block.', async () => { - await createReusableBlock( 'Hello there!', 'Greeting block' ); - await clearAllBlocks(); - - // Insert the reusable block we created above. - await insertPattern( 'Greeting block' ); - - // Check that its content is up to date. - const text = await canvas().$eval( - '.block-editor-block-list__block[data-type="core/block"] p', - ( element ) => element.innerText - ); - expect( text ).toMatch( 'Hello there!' ); - - await clearAllBlocks(); - - // Insert the reusable block we edited above. - await insertPattern( 'Greeting block' ); - - // Convert block to a regular block. - await clickBlockToolbarButton( 'Options' ); - await clickMenuItem( 'Detach' ); - - // Check that we have a paragraph block on the page. - const paragraphBlock = await canvas().$( - '.block-editor-block-list__block[data-type="core/paragraph"]' - ); - expect( paragraphBlock ).not.toBeNull(); - - // Check that its content is up to date. - const paragraphContent = await canvas().$eval( - '.block-editor-block-list__block[data-type="core/paragraph"]', - ( element ) => element.innerText - ); - expect( paragraphContent ).toMatch( 'Hello there!' ); - } ); - - it( 'can be inserted after refresh', async () => { - await createReusableBlock( 'Awesome Paragraph', 'Awesome block' ); - - // Step 2. Create new post. - await createNewPost(); - - // Step 3. Insert the block created in Step 1. - await insertPattern( 'Awesome block' ); - - // Check the title. - await openDocumentSettingsSidebar(); - const title = await page.$eval( - patternBlockInspectorNameSelector, - ( element ) => element.textContent - ); - expect( title ).toBe( 'Awesome block' ); - } ); - - it( 'can be created from multiselection and converted back to regular blocks', async () => { - await createNewPost(); - - // Insert a Two paragraphs block. - await insertBlock( 'Paragraph' ); - await page.keyboard.type( 'Hello there!' ); - await page.keyboard.press( 'Enter' ); - await page.keyboard.type( 'Second paragraph' ); - - // Select all the blocks. - await pressKeyWithModifier( 'primary', 'a' ); - await pressKeyWithModifier( 'primary', 'a' ); - - // Convert block to a reusable block. - await clickBlockToolbarButton( 'Options' ); - await clickMenuItem( 'Create pattern' ); - - // Set title. - const nameInput = await page.waitForSelector( - patternBlockNameInputSelector - ); - await nameInput.click(); - await page.keyboard.type( 'Multi-selection reusable block' ); - await page.waitForSelector( syncToggleSelectorChecked ); - await page.keyboard.press( 'Enter' ); - - // Wait for creation to finish. - await page.waitForXPath( - '//*[contains(@class, "components-snackbar")]/*[contains(text(),"pattern created:")]' - ); - - await clearAllBlocks(); - - // Insert the reusable block we edited above. - await insertPattern( 'Multi-selection reusable block' ); - - // Convert block to a regular block. - await clickBlockToolbarButton( 'Options' ); - await clickMenuItem( 'Detach' ); - - // Check that we have two paragraph blocks on the page. - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'will not break the editor if empty', async () => { - await createReusableBlock( - 'Awesome Paragraph', - 'Random reusable block' - ); - await clearAllBlocks(); - await insertPattern( 'Random reusable block' ); - - await visitAdminPage( 'edit.php', [ 'post_type=wp_block' ] ); - - const [ editButton ] = await page.$x( - `//a[contains(@aria-label, 'Random reusable block')]` - ); - await editButton.click(); - - await page.waitForNavigation(); - await page.waitForSelector( 'iframe[name="editor-canvas"]' ); - - // Click the block to give it focus. - const blockSelector = 'p[data-title="Paragraph"]'; - await canvas().waitForSelector( blockSelector ); - await canvas().click( blockSelector ); - - // Delete the block, leaving the reusable block empty. - await clickBlockToolbarButton( 'Options' ); - const deleteButton = await page.waitForXPath( - '//button/span[text()="Delete"]' - ); - deleteButton.click(); - - // Wait for the Update button to become enabled. - const publishButtonSelector = '.editor-post-publish-button__button'; - await page.waitForSelector( - publishButtonSelector + '[aria-disabled="false"]' - ); - - // Save the reusable block. - await page.click( publishButtonSelector ); - await page.waitForXPath( - '//*[contains(@class, "components-snackbar")]/*[text()="Pattern updated."]' - ); - - await createNewPost(); - - await toggleGlobalBlockInserter(); - - expect( console ).not.toHaveErrored(); - } ); - - it( 'Should show a proper message when the reusable block is missing', async () => { - // Insert a non-existant reusable block. - await page.evaluate( () => { - const { createBlock } = window.wp.blocks; - const { dispatch } = window.wp.data; - dispatch( 'core/block-editor' ).resetBlocks( [ - createBlock( 'core/block', { ref: 123456 } ), - ] ); - } ); - - await canvas().waitForXPath( - '//*[contains(@class, "block-editor-warning")]/*[text()="Block has been deleted or is unavailable."]' - ); - - // This happens when the 404 is returned. - expect( console ).toHaveErrored(); - } ); - - it( 'should be able to insert a reusable block twice', async () => { - await createReusableBlock( - 'Awesome Paragraph', - 'Duplicated reusable block' - ); - await clearAllBlocks(); - await insertPattern( 'Duplicated reusable block' ); - await insertPattern( 'Duplicated reusable block' ); - await saveDraft(); - await page.reload(); - await page.waitForSelector( 'iframe[name="editor-canvas"]' ); - - // Wait for the paragraph to be loaded. - await canvas().waitForSelector( - '.block-editor-block-list__block[data-type="core/paragraph"]' - ); - // The first click selects the reusable block wrapper. - // The second click selects the actual paragraph block. - await canvas().click( '.wp-block-block' ); - await canvas().focus( - '.block-editor-block-list__block[data-type="core/paragraph"]' - ); - await pressKeyWithModifier( 'primary', 'a' ); - await page.keyboard.press( 'End' ); - await page.keyboard.type( ' modified' ); - - // Wait for async mode to dispatch the update. - // eslint-disable-next-line no-restricted-syntax - await page.waitForTimeout( 1000 ); - - // Check that the content of the second reusable block has been updated. - const reusableBlocks = await page.$$( '.wp-block-block' ); - await Promise.all( - reusableBlocks.map( async ( paragraph ) => { - const content = await paragraph.$eval( - 'p', - ( element ) => element.textContent - ); - expect( content ).toEqual( 'Awesome Paragraph modified' ); - } ) - ); - } ); - - // Test for regressions of https://github.com/WordPress/gutenberg/issues/27243. - it( 'should allow a block with styles to be converted to a reusable block', async () => { - // Insert a quote and reload the page. - insertBlock( 'Quote' ); - await saveDraft(); - await page.reload(); - await page.waitForSelector( 'iframe[name="editor-canvas"]' ); - - // The quote block should have a visible preview in the sidebar for this test to be valid. - const quoteBlock = await canvas().waitForSelector( - '.block-editor-block-list__block[aria-label="Block: Quote"]' - ); - // Select the quote block. - await quoteBlock.focus(); - await openDocumentSettingsSidebar(); - await page.waitForXPath( - '//*[@role="region"][@aria-label="Editor settings"]//button[.="Styles"]' - ); - - // Convert to reusable. - await clickBlockToolbarButton( 'Options' ); - await clickMenuItem( 'Create pattern' ); - const nameInput = await page.waitForSelector( - patternBlockNameInputSelector - ); - await nameInput.click(); - await page.keyboard.type( 'Block with styles' ); - await page.waitForSelector( syncToggleSelectorChecked ); - await page.keyboard.press( 'Enter' ); - const reusableBlock = await canvas().waitForSelector( - '.block-editor-block-list__block[aria-label="Block: Pattern"]' - ); - expect( reusableBlock ).toBeTruthy(); - } ); -} ); diff --git a/test/e2e/specs/editor/various/patterns.spec.js b/test/e2e/specs/editor/various/patterns.spec.js index 745f5a5aa417b..a16772ea8e70d 100644 --- a/test/e2e/specs/editor/various/patterns.spec.js +++ b/test/e2e/specs/editor/various/patterns.spec.js @@ -321,4 +321,302 @@ test.describe( 'Synced pattern', () => { .poll( editor.getBlocks ) .toMatchObject( [ expectedParagraphBlock ] ); } ); + + test( 'can be created, inserted, and converted to a regular block', async ( { + editor, + requestUtils, + } ) => { + const { id } = await requestUtils.createBlock( { + title: 'Greeting block', + content: + '\n

    Hello there!

    \n', + status: 'publish', + } ); + + await editor.insertBlock( { + name: 'core/block', + attributes: { ref: id }, + } ); + + const expectedParagraphBlock = { + name: 'core/paragraph', + attributes: { content: 'Hello there!' }, + }; + + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/block', + attributes: { ref: id }, + innerBlocks: [ expectedParagraphBlock ], + }, + ] ); + + await editor.selectBlocks( + editor.canvas.getByRole( 'document', { name: 'Block: Pattern' } ) + ); + await editor.clickBlockOptionsMenuItem( 'Detach' ); + + await expect + .poll( editor.getBlocks ) + .toMatchObject( [ expectedParagraphBlock ] ); + } ); + + test( 'can be inserted after refresh', async ( { + admin, + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { content: 'Awesome Paragraph' }, + } ); + await editor.clickBlockOptionsMenuItem( 'Create pattern' ); + + const createPatternDialog = page.getByRole( 'dialog', { + name: 'Create pattern', + } ); + await createPatternDialog + .getByRole( 'textbox', { name: 'Name' } ) + .fill( 'Awesome block' ); + await createPatternDialog + .getByRole( 'checkbox', { name: 'Synced' } ) + .setChecked( true ); + await createPatternDialog + .getByRole( 'button', { name: 'Create' } ) + .click(); + + await admin.createNewPost(); + await editor.canvas + .getByRole( 'button', { name: 'Add default block' } ) + .click(); + await page.keyboard.type( '/Awesome block' ); + await page.getByRole( 'option', { name: 'Awesome block' } ).click(); + + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/block', + attributes: { ref: expect.any( Number ) }, + innerBlocks: [ + { + name: 'core/paragraph', + attributes: { content: 'Awesome Paragraph' }, + }, + ], + }, + ] ); + } ); + + test( 'can be created from multiselection and converted back to regular blocks', async ( { + editor, + pageUtils, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { content: 'Hello there!' }, + } ); + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { content: 'Second paragraph' }, + } ); + + await pageUtils.pressKeys( 'primary+a', { times: 2 } ); + await editor.clickBlockOptionsMenuItem( 'Create pattern' ); + + const createPatternDialog = editor.page.getByRole( 'dialog', { + name: 'Create pattern', + } ); + await createPatternDialog + .getByRole( 'textbox', { name: 'Name' } ) + .fill( 'Multi-selection reusable block' ); + await createPatternDialog + .getByRole( 'checkbox', { name: 'Synced' } ) + .setChecked( true ); + await createPatternDialog + .getByRole( 'button', { name: 'Create' } ) + .click(); + + const expectedParagraphBlocks = [ + { + name: 'core/paragraph', + attributes: { content: 'Hello there!' }, + }, + { + name: 'core/paragraph', + attributes: { content: 'Second paragraph' }, + }, + ]; + + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/block', + attributes: { ref: expect.any( Number ) }, + innerBlocks: expectedParagraphBlocks, + }, + ] ); + + await editor.selectBlocks( + editor.canvas.getByRole( 'document', { name: 'Block: Pattern' } ) + ); + await editor.clickBlockOptionsMenuItem( 'Detach' ); + + await expect + .poll( editor.getBlocks ) + .toMatchObject( expectedParagraphBlocks ); + } ); + + // Check for regressions of https://github.com/WordPress/gutenberg/pull/26484. + test( 'will not break the editor if empty', async ( { + editor, + page, + requestUtils, + } ) => { + const { id } = await requestUtils.createBlock( { + title: 'Awesome empty', + content: '', + status: 'publish', + } ); + + let hasError = false; + page.on( 'console', ( msg ) => { + if ( msg.type() === 'error' ) hasError = true; + } ); + + // Need to reload the page to make pattern available in the store. + await page.reload(); + await editor.insertBlock( { + name: 'core/block', + attributes: { ref: id }, + } ); + + await page + .getByRole( 'button', { name: 'Toggle block inserter' } ) + .click(); + await page + .getByRole( 'searchbox', { + name: 'Search for blocks and patterns', + } ) + .fill( 'Awesome empty' ); + + await expect( + page + .getByRole( 'listbox', { name: 'Block patterns' } ) + .getByRole( 'option', { + name: 'Awesome empty', + } ) + ).toBeVisible(); + expect( hasError ).toBe( false ); + } ); + + test( 'should show a proper message when the reusable block is missing', async ( { + editor, + } ) => { + // Insert a non-existant reusable block. + await editor.insertBlock( { + name: 'core/block', + attributes: { ref: 123456 }, + } ); + + await expect( + editor.canvas.getByRole( 'document', { name: 'Block: Pattern' } ) + ).toContainText( 'Block has been deleted or is unavailable.' ); + } ); + + test( 'should be able to insert a reusable block twice', async ( { + editor, + page, + pageUtils, + requestUtils, + } ) => { + const { id } = await requestUtils.createBlock( { + title: 'Duplicated reusable block', + content: + '\n

    Awesome Paragraph

    \n', + status: 'publish', + } ); + await editor.insertBlock( { + name: 'core/block', + attributes: { ref: id }, + } ); + await editor.insertBlock( { + name: 'core/block', + attributes: { ref: id }, + } ); + + await editor.saveDraft(); + await editor.selectBlocks( + editor.canvas + .getByRole( 'document', { name: 'Block: Pattern' } ) + .first() + ); + await editor.showBlockToolbar(); + await page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'link', { name: 'Edit original' } ) + .click(); + + await editor.canvas + .getByRole( 'document', { name: 'Block: Paragraph' } ) + .click(); + await pageUtils.pressKeys( 'primary+a' ); + await page.keyboard.press( 'ArrowRight' ); + await page.keyboard.type( ' modified' ); + + const editorTopBar = page.getByRole( 'region', { + name: 'Editor top bar', + } ); + + await editorTopBar.getByRole( 'button', { name: 'Update' } ).click(); + await page + .getByRole( 'button', { name: 'Dismiss this notice' } ) + .filter( { hasText: 'Pattern updated.' } ) + .click(); + await editorTopBar.getByRole( 'button', { name: 'Back' } ).click(); + + await expect( + editor.canvas + .getByRole( 'document', { name: 'Block: Paragraph' } ) + .filter( { hasText: 'Awesome Paragraph modified' } ) + ).toHaveCount( 2 ); + } ); + + // Check for regressions of https://github.com/WordPress/gutenberg/issues/27243. + test( 'should allow a block with styles to be converted to a reusable block', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { name: 'core/quote' } ); + await editor.saveDraft(); + await page.reload(); + + await editor.openDocumentSettingsSidebar(); + await editor.selectBlocks( + editor.canvas.getByRole( 'document', { name: 'Block: Quote' } ) + ); + + // The quote block should have a visible preview in the sidebar for this test to be valid. + await expect( + page + .getByRole( 'region', { name: 'Editor settings' } ) + .getByRole( 'button', { name: 'Styles', exact: true } ) + ).toBeVisible(); + + await editor.clickBlockOptionsMenuItem( 'Create pattern' ); + + const createPatternDialog = editor.page.getByRole( 'dialog', { + name: 'Create pattern', + } ); + await createPatternDialog + .getByRole( 'textbox', { name: 'Name' } ) + .fill( 'Block with styles' ); + await createPatternDialog + .getByRole( 'checkbox', { name: 'Synced' } ) + .setChecked( true ); + await createPatternDialog + .getByRole( 'button', { name: 'Create' } ) + .click(); + + await expect( + editor.canvas.getByRole( 'document', { name: 'Block: Pattern' } ) + ).toBeVisible(); + } ); } ); From d8104f0de1a4290506e923dc089a993ca1d128c8 Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Tue, 6 Feb 2024 09:00:43 +0400 Subject: [PATCH 32/41] useOnBlockDrop: Fix the Gallery block check (#58711) Co-authored-by: Mamaduka Co-authored-by: andrewserong --- .../src/components/use-on-block-drop/index.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/block-editor/src/components/use-on-block-drop/index.js b/packages/block-editor/src/components/use-on-block-drop/index.js index 212afd7aa96ee..58ce615436d8e 100644 --- a/packages/block-editor/src/components/use-on-block-drop/index.js +++ b/packages/block-editor/src/components/use-on-block-drop/index.js @@ -234,7 +234,7 @@ export default function useOnBlockDrop( getBlock, isGroupable, } = useSelect( blockEditorStore ); - const { getBlockType, getGroupingBlockName } = useSelect( blocksStore ); + const { getGroupingBlockName } = useSelect( blocksStore ); const { insertBlocks, moveBlocksToPosition, @@ -283,7 +283,10 @@ export default function useOnBlockDrop( return block.name === 'core/image'; } ); - const galleryBlock = !! getBlockType( 'core/gallery' ); + const galleryBlock = canInsertBlockType( + 'core/gallery', + targetRootClientId + ); const wrappedBlocks = createBlock( areAllImages && galleryBlock @@ -292,7 +295,8 @@ export default function useOnBlockDrop( { layout: { type: 'flex', - flexWrap: areAllImages ? null : 'nowrap', + flexWrap: + areAllImages && galleryBlock ? null : 'nowrap', }, }, groupInnerBlocks @@ -319,11 +323,12 @@ export default function useOnBlockDrop( getBlockOrder, targetRootClientId, targetBlockIndex, + isGroupable, operation, replaceBlocks, getBlock, nearestSide, - getBlockType, + canInsertBlockType, getGroupingBlockName, insertBlocks, ] From 15644c02ae81ac7dfcbae1384beb04ec5ec2f825 Mon Sep 17 00:00:00 2001 From: Ramon Date: Tue, 6 Feb 2024 16:33:50 +1100 Subject: [PATCH 33/41] Global styles: update return values from getGlobalStylesChanges() (#58707) * Leave it up to the consuming component as to how to present the changes. This includes adding a period at the end of any sentences. * Period. Co-authored-by: ramonjd Co-authored-by: andrewserong --- .../global-styles/get-global-styles-changes.js | 10 ++-------- .../global-styles/test/get-global-styles-changes.js | 6 +++--- .../screen-revisions/revisions-buttons.js | 2 +- .../entities-saved-states/entity-type-list.js | 2 +- .../site-editor/user-global-styles-revisions.spec.js | 2 +- 5 files changed, 8 insertions(+), 14 deletions(-) diff --git a/packages/block-editor/src/components/global-styles/get-global-styles-changes.js b/packages/block-editor/src/components/global-styles/get-global-styles-changes.js index 942d709e6a268..9bbd11fb7d797 100644 --- a/packages/block-editor/src/components/global-styles/get-global-styles-changes.js +++ b/packages/block-editor/src/components/global-styles/get-global-styles-changes.js @@ -50,13 +50,7 @@ function getTranslation( key ) { if ( keyArray?.[ 0 ] === 'blocks' ) { const blockName = getBlockNames()?.[ keyArray[ 1 ] ]; - return blockName - ? sprintf( - // translators: %s: block name. - __( '%s block' ), - blockName - ) - : keyArray[ 1 ]; + return blockName || keyArray[ 1 ]; } if ( keyArray?.[ 0 ] === 'elements' ) { @@ -200,7 +194,7 @@ export default function getGlobalStylesChanges( next, previous, options = {} ) { const deleteCount = changesLength - maxResults; const andMoreText = sprintf( // translators: %d: number of global styles changes that are not displayed in the UI. - _n( '…and %d more change.', '…and %d more changes.', deleteCount ), + _n( '…and %d more change', '…and %d more changes', deleteCount ), deleteCount ); changes.splice( maxResults, deleteCount, andMoreText ); diff --git a/packages/block-editor/src/components/global-styles/test/get-global-styles-changes.js b/packages/block-editor/src/components/global-styles/test/get-global-styles-changes.js index 0c53336c87e02..2e7a68dab1f7b 100644 --- a/packages/block-editor/src/components/global-styles/test/get-global-styles-changes.js +++ b/packages/block-editor/src/components/global-styles/test/get-global-styles-changes.js @@ -171,7 +171,7 @@ describe( 'getGlobalStylesChanges', () => { expect( resultA ).toEqual( [ 'Colors', 'Typography', - 'Test pumpkin flowers block', + 'Test pumpkin flowers', 'H3 element', 'Caption element', 'H6 element', @@ -191,8 +191,8 @@ describe( 'getGlobalStylesChanges', () => { expect( resultA ).toEqual( [ 'Colors', 'Typography', - 'Test pumpkin flowers block', - '…and 5 more changes.', + 'Test pumpkin flowers', + '…and 5 more changes', ] ); } ); diff --git a/packages/edit-site/src/components/global-styles/screen-revisions/revisions-buttons.js b/packages/edit-site/src/components/global-styles/screen-revisions/revisions-buttons.js index dcdc98fefebb6..07d479251add9 100644 --- a/packages/edit-site/src/components/global-styles/screen-revisions/revisions-buttons.js +++ b/packages/edit-site/src/components/global-styles/screen-revisions/revisions-buttons.js @@ -36,7 +36,7 @@ function ChangesSummary( { revision, previousRevision } ) { data-testid="global-styles-revision-changes" className="edit-site-global-styles-screen-revisions__changes" > - { changes.join( ', ' ) } + { changes.join( ', ' ) }. ); } diff --git a/packages/editor/src/components/entities-saved-states/entity-type-list.js b/packages/editor/src/components/entities-saved-states/entity-type-list.js index d422c2ae9bfdb..cb050a370c4e3 100644 --- a/packages/editor/src/components/entities-saved-states/entity-type-list.js +++ b/packages/editor/src/components/entities-saved-states/entity-type-list.js @@ -59,7 +59,7 @@ function GlobalStylesDescription( { record } ) {

    { __( 'Changes made to:' ) }

    - { globalStylesChanges.join( ', ' ) } + { globalStylesChanges.join( ', ' ) }. ) : null; } diff --git a/test/e2e/specs/site-editor/user-global-styles-revisions.spec.js b/test/e2e/specs/site-editor/user-global-styles-revisions.spec.js index 9cb8ba53461e0..c8a87ecbeb69e 100644 --- a/test/e2e/specs/site-editor/user-global-styles-revisions.spec.js +++ b/test/e2e/specs/site-editor/user-global-styles-revisions.spec.js @@ -57,7 +57,7 @@ test.describe( 'Style Revisions', () => { // Shows changes made in the revision. await expect( page.getByTestId( 'global-styles-revision-changes' ) - ).toHaveText( 'Colors' ); + ).toHaveText( 'Colors.' ); // There should be 2 revisions not including the reset to theme defaults button. await expect( revisionButtons ).toHaveCount( From 77878eabe77e80e3391c3980e02153c029004909 Mon Sep 17 00:00:00 2001 From: Nik Tsekouras Date: Tue, 6 Feb 2024 08:30:41 +0200 Subject: [PATCH 34/41] Fix image link preset suggestions arrow key navigation (#58615) Co-authored-by: ntsekouras Co-authored-by: artemiomorales Co-authored-by: mirka <0mirka00@git.wordpress.org> Co-authored-by: richtabor Co-authored-by: annezazu Co-authored-by: afercia --- .../src/components/url-popover/image-url-input-ui.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/block-editor/src/components/url-popover/image-url-input-ui.js b/packages/block-editor/src/components/url-popover/image-url-input-ui.js index f45770004f045..dca8db868bbb5 100644 --- a/packages/block-editor/src/components/url-popover/image-url-input-ui.js +++ b/packages/block-editor/src/components/url-popover/image-url-input-ui.js @@ -6,11 +6,11 @@ import { useRef, useEffect, useState } from '@wordpress/element'; import { focus } from '@wordpress/dom'; import { ToolbarButton, + NavigableMenu, Button, MenuItem, ToggleControl, TextControl, - MenuGroup, __experimentalVStack as VStack, } from '@wordpress/components'; import { @@ -270,7 +270,7 @@ const ImageURLInputUI = ( { } additionalControls={ showLinkEditor && ( - + { getLinkDestinations().map( ( link ) => ( ) } - + ) } offset={ 13 } From 28b78b5c76c92030984b54321d6f9f1d14ab1487 Mon Sep 17 00:00:00 2001 From: Kai Hao Date: Tue, 6 Feb 2024 15:51:43 +0800 Subject: [PATCH 35/41] Support button's link settings for Pattern Overrides (#58587) Co-authored-by: kevin940726 Co-authored-by: talldan --- lib/compat/wordpress-6.5/blocks.php | 2 +- packages/block-library/src/block/edit.js | 9 +- packages/patterns/src/constants.js | 2 + .../editor/various/pattern-overrides.spec.js | 111 ++++++++++++++++++ 4 files changed, 120 insertions(+), 4 deletions(-) diff --git a/lib/compat/wordpress-6.5/blocks.php b/lib/compat/wordpress-6.5/blocks.php index 637ebaf6483be..1ca5865e64dc5 100644 --- a/lib/compat/wordpress-6.5/blocks.php +++ b/lib/compat/wordpress-6.5/blocks.php @@ -163,7 +163,7 @@ function gutenberg_process_block_bindings( $block_content, $block, $block_instan 'core/paragraph' => array( 'content' ), 'core/heading' => array( 'content' ), 'core/image' => array( 'url', 'title', 'alt' ), - 'core/button' => array( 'url', 'text' ), + 'core/button' => array( 'url', 'text', 'linkTarget', 'rel' ), ); // If the block doesn't have the bindings property or isn't one of the allowed block types, return. diff --git a/packages/block-library/src/block/edit.js b/packages/block-library/src/block/edit.js index 9c1e81de04e17..1ad40055b7ee7 100644 --- a/packages/block-library/src/block/edit.js +++ b/packages/block-library/src/block/edit.js @@ -147,10 +147,13 @@ function getContentValuesFromInnerBlocks( blocks, defaultValues ) { defaultValues[ blockId ][ attributeKey ] ) { content[ blockId ] ??= { values: {} }; - // TODO: We need a way to represent `undefined` in the serialized overrides. - // Also see: https://github.com/WordPress/gutenberg/pull/57249#discussion_r1452987871 content[ blockId ].values[ attributeKey ] = - block.attributes[ attributeKey ]; + block.attributes[ attributeKey ] === undefined + ? // TODO: We use an empty string to represent undefined for now until + // we support a richer format for overrides and the block binding API. + // Currently only the `linkTarget` attribute of `core/button` is affected. + '' + : block.attributes[ attributeKey ]; } } } diff --git a/packages/patterns/src/constants.js b/packages/patterns/src/constants.js index 5fa9ec44f4e3a..109044946a3fc 100644 --- a/packages/patterns/src/constants.js +++ b/packages/patterns/src/constants.js @@ -27,6 +27,8 @@ export const PARTIAL_SYNCING_SUPPORTED_BLOCKS = { 'core/button': { text: __( 'Text' ), url: __( 'URL' ), + linkTarget: __( 'Link Target' ), + rel: __( 'Link Relationship' ), }, 'core/image': { url: __( 'URL' ), diff --git a/test/e2e/specs/editor/various/pattern-overrides.spec.js b/test/e2e/specs/editor/various/pattern-overrides.spec.js index ae70f575fbb28..a655439b241ea 100644 --- a/test/e2e/specs/editor/various/pattern-overrides.spec.js +++ b/test/e2e/specs/editor/various/pattern-overrides.spec.js @@ -263,4 +263,115 @@ test.describe( 'Pattern Overrides', () => { }, ] ); } ); + + test( "handles button's link settings", async ( { + page, + admin, + requestUtils, + editor, + context, + } ) => { + const buttonId = 'button-id'; + const { id } = await requestUtils.createBlock( { + title: 'Button with target', + content: ` +
    + +
    +`, + status: 'publish', + } ); + + await admin.createNewPost(); + + await editor.insertBlock( { + name: 'core/block', + attributes: { ref: id }, + } ); + + // Focus the button, open the link popup. + await editor.canvas + .getByRole( 'document', { name: 'Block: Button' } ) + .getByRole( 'textbox', { name: 'Button text' } ) + .focus(); + await expect( + page.getByRole( 'link', { name: 'wp.org' } ) + ).toContainText( 'opens in a new tab' ); + + // The link popup doesn't have a role which is a bit unfortunate. + // These are the buttons in the link popup. + const advancedPanel = page.getByRole( 'button', { + name: 'Advanced', + exact: true, + } ); + const editLinkButton = page.getByRole( 'button', { + name: 'Edit', + exact: true, + } ); + const saveLinkButton = page.getByRole( 'button', { + name: 'Save', + exact: true, + } ); + + await editLinkButton.click(); + if ( + ( await advancedPanel.getAttribute( 'aria-expanded' ) ) === 'false' + ) { + await advancedPanel.click(); + } + + const openInNewTabCheckbox = page.getByRole( 'checkbox', { + name: 'Open in new tab', + } ); + const markAsNoFollowCheckbox = page.getByRole( 'checkbox', { + name: 'Mark as nofollow', + } ); + // Both checkboxes are checked. + await expect( openInNewTabCheckbox ).toBeChecked(); + await expect( markAsNoFollowCheckbox ).toBeChecked(); + + // Check only the "open in new tab" checkbox. + await markAsNoFollowCheckbox.setChecked( false ); + await saveLinkButton.click(); + + const postId = await editor.publishPost(); + const previewPage = await context.newPage(); + await previewPage.goto( `/?p=${ postId }` ); + const buttonLink = previewPage.getByRole( 'link', { name: 'Button' } ); + + await expect( buttonLink ).toHaveAttribute( 'target', '_blank' ); + await expect( buttonLink ).toHaveAttribute( + 'rel', + 'noreferrer noopener' + ); + + // Uncheck both checkboxes. + await editLinkButton.click(); + await openInNewTabCheckbox.setChecked( false ); + await saveLinkButton.click(); + + // Update the post. + const updateButton = page + .getByRole( 'region', { name: 'Editor top bar' } ) + .getByRole( 'button', { name: 'Update' } ); + await updateButton.click(); + await expect( updateButton ).toBeDisabled(); + + await previewPage.reload(); + await expect( buttonLink ).toHaveAttribute( 'target', '' ); + await expect( buttonLink ).toHaveAttribute( 'rel', '' ); + + // Check only the "mark as nofollow" checkbox. + await editLinkButton.click(); + await markAsNoFollowCheckbox.setChecked( true ); + await saveLinkButton.click(); + + // Update the post. + await updateButton.click(); + await expect( updateButton ).toBeDisabled(); + + await previewPage.reload(); + await expect( buttonLink ).toHaveAttribute( 'target', '' ); + await expect( buttonLink ).toHaveAttribute( 'rel', /^\s*nofollow\s*$/ ); + } ); } ); From 87658842da8067167e857f5988ce1d156b60ece5 Mon Sep 17 00:00:00 2001 From: Mario Santos <34552881+SantosGuillamot@users.noreply.github.com> Date: Tue, 6 Feb 2024 09:45:18 +0100 Subject: [PATCH 36/41] Block Bindings: Add tests for the frontend and polish the existing ones (#58676) * Add editor `updatePost` helper in e2e tests * Update default url custom field value * Test the frontend shows the custom fields values * Use `toHaveText` assertion * Use `toHaveAttribute` assertion * Inline variables and primitives * Define blocks inside `inserBlock` instead of variables * Nest locators to clarify the scopes * Use `toHaveAttribute` for `contenteditable` * Use `toHaveText` in buttons * Move `updatePost` helper to bindings test * Narrow down update locator --- packages/e2e-tests/plugins/block-bindings.php | 2 +- .../editor/various/block-bindings.spec.js | 1225 ++++++++++++----- 2 files changed, 861 insertions(+), 366 deletions(-) diff --git a/packages/e2e-tests/plugins/block-bindings.php b/packages/e2e-tests/plugins/block-bindings.php index d61f00f9c4387..c686b40006a06 100644 --- a/packages/e2e-tests/plugins/block-bindings.php +++ b/packages/e2e-tests/plugins/block-bindings.php @@ -29,7 +29,7 @@ function gutenberg_test_block_bindings_register_custom_fields() { 'show_in_rest' => true, 'type' => 'string', 'single' => true, - 'default' => '', + 'default' => '#url-custom-field', ) ); } diff --git a/test/e2e/specs/editor/various/block-bindings.spec.js b/test/e2e/specs/editor/various/block-bindings.spec.js index ded951ba937a8..67b8946aa815b 100644 --- a/test/e2e/specs/editor/various/block-bindings.spec.js +++ b/test/e2e/specs/editor/various/block-bindings.spec.js @@ -8,124 +8,8 @@ const path = require( 'path' ); const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); test.describe( 'Block bindings', () => { - const variables = { - customFields: { - textValue: 'Value of the text_custom_field', - textKey: 'text_custom_field', - urlValue: '', - urlKey: 'url_custom_field', - }, - labels: { - align: 'Align text', - bold: 'Bold', - imageReplace: 'Replace', - imageAlt: 'Alternative text', - imageTitle: 'Title attribute', - }, - blocks: { - paragraph: { - name: 'core/paragraph', - attributes: { - content: 'p', - metadata: { - bindings: { - content: { - source: 'core/post-meta', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - }, - heading: { - name: 'core/heading', - attributes: { - content: 'h', - metadata: { - bindings: { - content: { - source: 'core/post-meta', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - }, - buttons: { - textOnly: { - name: 'core/buttons', - innerBlocks: [ - { - name: 'core/button', - attributes: { - text: 'b', - url: 'https://www.wordpress.org/', - metadata: { - bindings: { - text: { - source: 'core/post-meta', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - }, - ], - }, - urlOnly: { - name: 'core/buttons', - innerBlocks: [ - { - name: 'core/button', - attributes: { - text: 'b', - url: 'https://www.wordpress.org/', - metadata: { - bindings: { - url: { - source: 'core/post-meta', - args: { key: 'url_custom_field' }, - }, - }, - }, - }, - }, - ], - }, - multipleAttrs: { - name: 'core/buttons', - innerBlocks: [ - { - name: 'core/button', - attributes: { - text: 'b', - url: 'https://www.wordpress.org/', - metadata: { - bindings: { - text: { - source: 'core/post-meta', - args: { key: 'text_custom_field' }, - }, - url: { - source: 'core/post-meta', - args: { key: 'url_custom_field' }, - }, - }, - }, - }, - }, - ], - }, - }, - images: { - urlOnly: {}, - altOnly: {}, - titleOnly: {}, - multipleAttrs: {}, - }, - }, - placeholderSrc: '', - }; + let imagePlaceholderSrc; + let imageCustomFieldSrc; test.beforeAll( async ( { requestUtils } ) => { await requestUtils.activateTheme( 'emptytheme' ); await requestUtils.activatePlugin( 'gutenberg-test-block-bindings' ); @@ -133,76 +17,7 @@ test.describe( 'Block bindings', () => { const placeholderMedia = await requestUtils.uploadMedia( path.join( './test/e2e/assets', '10x10_e2e_test_image_z9T8jK.png' ) ); - variables.placeholderSrc = placeholderMedia.source_url; - // Init image blocks. - variables.blocks.images.urlOnly = { - name: 'core/image', - attributes: { - url: variables.placeholderSrc, - alt: 'default alt value', - title: 'default title value', - metadata: { - bindings: { - url: { - source: 'core/post-meta', - args: { key: 'url_custom_field' }, - }, - }, - }, - }, - }; - variables.blocks.images.altOnly = { - name: 'core/image', - attributes: { - url: variables.placeholderSrc, - alt: 'default alt value', - title: 'default title value', - metadata: { - bindings: { - alt: { - source: 'core/post-meta', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - }; - variables.blocks.images.titleOnly = { - name: 'core/image', - attributes: { - url: variables.placeholderSrc, - alt: 'default alt value', - title: 'default title value', - metadata: { - bindings: { - title: { - source: 'core/post-meta', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - }; - variables.blocks.images.multipleAttrs = { - name: 'core/image', - attributes: { - url: variables.placeholderSrc, - alt: 'default alt value', - title: 'default title value', - metadata: { - bindings: { - url: { - source: 'core/post-meta', - args: { key: 'url_custom_field' }, - }, - alt: { - source: 'core/post-meta', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - }; + imagePlaceholderSrc = placeholderMedia.source_url; } ); test.afterEach( async ( { requestUtils } ) => { @@ -215,6 +30,12 @@ test.describe( 'Block bindings', () => { await requestUtils.deactivatePlugin( 'gutenberg-test-block-bindings' ); } ); + test.use( { + BlockBindingsUtils: async ( { editor, page, pageUtils }, use ) => { + await use( new BlockBindingsUtils( { editor, page, pageUtils } ) ); + }, + } ); + test.describe( 'Template context', () => { test.beforeEach( async ( { admin, editor } ) => { await admin.visitSiteEditor( { @@ -229,13 +50,25 @@ test.describe( 'Block bindings', () => { test( 'Should show the value of the custom field', async ( { editor, } ) => { - await editor.insertBlock( variables.blocks.paragraph ); + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: 'paragraph default content', + metadata: { + bindings: { + content: { + source: 'core/post-meta', + args: { key: 'text_custom_field' }, + }, + }, + }, + }, + } ); const paragraphBlock = editor.canvas.getByRole( 'document', { name: 'Block: Paragraph', } ); - const paragraphContent = await paragraphBlock.textContent(); - expect( paragraphContent ).toBe( - variables.customFields.textKey + await expect( paragraphBlock ).toHaveText( + 'text_custom_field' ); } ); @@ -243,7 +76,20 @@ test.describe( 'Block bindings', () => { editor, page, } ) => { - await editor.insertBlock( variables.blocks.paragraph ); + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: 'paragraph default content', + metadata: { + bindings: { + content: { + source: 'core/post-meta', + args: { key: 'text_custom_field' }, + }, + }, + }, + }, + } ); const paragraphBlock = editor.canvas.getByRole( 'document', { name: 'Block: Paragraph', } ); @@ -251,22 +97,25 @@ test.describe( 'Block bindings', () => { // Alignment controls exist. await expect( - page.getByRole( 'button', { - name: variables.labels.align, - } ) + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { name: 'Align text' } ) ).toBeVisible(); // Format controls don't exist. await expect( - page.getByRole( 'button', { - name: variables.labels.bold, - } ) + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { + name: 'Bold', + } ) ).toBeHidden(); // Paragraph is not editable. - const isContentEditable = - await paragraphBlock.getAttribute( 'contenteditable' ); - expect( isContentEditable ).toBe( 'false' ); + await expect( paragraphBlock ).toHaveAttribute( + 'contenteditable', + 'false' + ); } ); } ); @@ -274,19 +123,44 @@ test.describe( 'Block bindings', () => { test( 'Should show the key of the custom field', async ( { editor, } ) => { - await editor.insertBlock( variables.blocks.heading ); + await editor.insertBlock( { + name: 'core/heading', + attributes: { + content: 'heading default content', + metadata: { + bindings: { + content: { + source: 'core/post-meta', + args: { key: 'text_custom_field' }, + }, + }, + }, + }, + } ); const headingBlock = editor.canvas.getByRole( 'document', { name: 'Block: Heading', } ); - const headingContent = await headingBlock.textContent(); - expect( headingContent ).toBe( variables.customFields.textKey ); + await expect( headingBlock ).toHaveText( 'text_custom_field' ); } ); test( 'Should lock the appropriate controls', async ( { editor, page, } ) => { - await editor.insertBlock( variables.blocks.heading ); + await editor.insertBlock( { + name: 'core/heading', + attributes: { + content: 'heading default content', + metadata: { + bindings: { + content: { + source: 'core/post-meta', + args: { key: 'text_custom_field' }, + }, + }, + }, + }, + } ); const headingBlock = editor.canvas.getByRole( 'document', { name: 'Block: Heading', } ); @@ -294,22 +168,25 @@ test.describe( 'Block bindings', () => { // Alignment controls exist. await expect( - page.getByRole( 'button', { - name: variables.labels.align, - } ) + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { name: 'Align text' } ) ).toBeVisible(); // Format controls don't exist. await expect( - page.getByRole( 'button', { - name: variables.labels.bold, - } ) + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { + name: 'Bold', + } ) ).toBeHidden(); // Heading is not editable. - const isContentEditable = - await headingBlock.getAttribute( 'contenteditable' ); - expect( isContentEditable ).toBe( 'false' ); + await expect( headingBlock ).toHaveAttribute( + 'contenteditable', + 'false' + ); } ); } ); @@ -317,45 +194,86 @@ test.describe( 'Block bindings', () => { test( 'Should show the key of the custom field when text is bound', async ( { editor, } ) => { - await editor.insertBlock( variables.blocks.buttons.textOnly ); + await editor.insertBlock( { + name: 'core/buttons', + innerBlocks: [ + { + name: 'core/button', + attributes: { + text: 'button default text', + url: '#default-url', + metadata: { + bindings: { + text: { + source: 'core/post-meta', + args: { key: 'text_custom_field' }, + }, + }, + }, + }, + }, + ], + } ); const buttonBlock = editor.canvas.getByRole( 'document', { name: 'Block: Button', exact: true, } ); - const buttonText = await buttonBlock.textContent(); - expect( buttonText ).toBe( variables.customFields.textKey ); + await expect( buttonBlock ).toHaveText( 'text_custom_field' ); } ); test( 'Should lock text controls when text is bound', async ( { editor, page, } ) => { - await editor.insertBlock( variables.blocks.buttons.textOnly ); - const buttonBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Button', - exact: true, + await editor.insertBlock( { + name: 'core/buttons', + innerBlocks: [ + { + name: 'core/button', + attributes: { + text: 'button default text', + url: '#default-url', + metadata: { + bindings: { + text: { + source: 'core/post-meta', + args: { key: 'text_custom_field' }, + }, + }, + }, + }, + }, + ], } ); + const buttonBlock = editor.canvas + .getByRole( 'document', { + name: 'Block: Button', + exact: true, + } ) + .locator( 'div' ); await buttonBlock.click(); // Alignment controls exist. await expect( - page.getByRole( 'button', { - name: variables.labels.align, - } ) + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { name: 'Align text' } ) ).toBeVisible(); // Format controls don't exist. await expect( - page.getByRole( 'button', { - name: variables.labels.bold, - } ) + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { + name: 'Bold', + } ) ).toBeHidden(); // Button is not editable. - const isContentEditable = await buttonBlock - .locator( 'div' ) - .getAttribute( 'contenteditable' ); - expect( isContentEditable ).toBe( 'false' ); + await expect( buttonBlock ).toHaveAttribute( + 'contenteditable', + 'false' + ); // Link controls exist. await expect( @@ -369,25 +287,48 @@ test.describe( 'Block bindings', () => { editor, page, } ) => { - await editor.insertBlock( variables.blocks.buttons.urlOnly ); - const buttonBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Button', - exact: true, + await editor.insertBlock( { + name: 'core/buttons', + innerBlocks: [ + { + name: 'core/button', + attributes: { + text: 'button default text', + url: '#default-url', + metadata: { + bindings: { + url: { + source: 'core/post-meta', + args: { key: 'url_custom_field' }, + }, + }, + }, + }, + }, + ], } ); + const buttonBlock = editor.canvas + .getByRole( 'document', { + name: 'Block: Button', + exact: true, + } ) + .locator( 'div' ); await buttonBlock.click(); // Format controls exist. await expect( - page.getByRole( 'button', { - name: variables.labels.bold, - } ) + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { + name: 'Bold', + } ) ).toBeVisible(); // Button is editable. - const isContentEditable = await buttonBlock - .locator( 'div' ) - .getAttribute( 'contenteditable' ); - expect( isContentEditable ).toBe( 'true' ); + await expect( buttonBlock ).toHaveAttribute( + 'contenteditable', + 'true' + ); // Link controls don't exist. await expect( @@ -406,34 +347,59 @@ test.describe( 'Block bindings', () => { editor, page, } ) => { - await editor.insertBlock( - variables.blocks.buttons.multipleAttrs - ); - const buttonBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Button', - exact: true, + await editor.insertBlock( { + name: 'core/buttons', + innerBlocks: [ + { + name: 'core/button', + attributes: { + text: 'button default text', + url: '#default-url', + metadata: { + bindings: { + text: { + source: 'core/post-meta', + args: { key: 'text_custom_field' }, + }, + url: { + source: 'core/post-meta', + args: { key: 'url_custom_field' }, + }, + }, + }, + }, + }, + ], } ); + const buttonBlock = editor.canvas + .getByRole( 'document', { + name: 'Block: Button', + exact: true, + } ) + .locator( 'div' ); await buttonBlock.click(); // Alignment controls are visible. await expect( - page.getByRole( 'button', { - name: variables.labels.align, - } ) + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { name: 'Align text' } ) ).toBeVisible(); // Format controls don't exist. await expect( - page.getByRole( 'button', { - name: variables.labels.bold, - } ) + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { + name: 'Bold', + } ) ).toBeHidden(); // Button is not editable. - const isContentEditable = await buttonBlock - .locator( 'div' ) - .getAttribute( 'contenteditable' ); - expect( isContentEditable ).toBe( 'false' ); + await expect( buttonBlock ).toHaveAttribute( + 'contenteditable', + 'false' + ); // Link controls don't exist. await expect( @@ -466,7 +432,22 @@ test.describe( 'Block bindings', () => { test( 'Should NOT show the upload form when url is bound', async ( { editor, } ) => { - await editor.insertBlock( variables.blocks.images.urlOnly ); + await editor.insertBlock( { + name: 'core/image', + attributes: { + url: imagePlaceholderSrc, + alt: 'default alt value', + title: 'default title value', + metadata: { + bindings: { + url: { + source: 'core/post-meta', + args: { key: 'url_custom_field' }, + }, + }, + }, + }, + } ); const imageBlock = editor.canvas.getByRole( 'document', { name: 'Block: Image', } ); @@ -480,7 +461,22 @@ test.describe( 'Block bindings', () => { editor, page, } ) => { - await editor.insertBlock( variables.blocks.images.urlOnly ); + await editor.insertBlock( { + name: 'core/image', + attributes: { + url: imagePlaceholderSrc, + alt: 'default alt value', + title: 'default title value', + metadata: { + bindings: { + url: { + source: 'core/post-meta', + args: { key: 'url_custom_field' }, + }, + }, + }, + }, + } ); const imageBlock = editor.canvas.getByRole( 'document', { name: 'Block: Image', } ); @@ -488,9 +484,11 @@ test.describe( 'Block bindings', () => { // Replace controls don't exist. await expect( - page.getByRole( 'button', { - name: variables.labels.imageReplace, - } ) + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { + name: 'Replace', + } ) ).toBeHidden(); // Image placeholder doesn't show the upload button. @@ -500,20 +498,30 @@ test.describe( 'Block bindings', () => { // Alt textarea is enabled and with the original value. await expect( - page.getByLabel( variables.labels.imageAlt ) + page + .getByRole( 'tabpanel', { name: 'Block' } ) + .getByLabel( 'Alternative text' ) ).toBeEnabled(); const altValue = await page - .getByLabel( variables.labels.imageAlt ) + .getByRole( 'tabpanel', { name: 'Block' } ) + .getByLabel( 'Alternative text' ) .inputValue(); expect( altValue ).toBe( 'default alt value' ); // Title input is enabled and with the original value. - await page.getByRole( 'button', { name: 'Advanced' } ).click(); + await page + .getByRole( 'tabpanel', { name: 'Settings' } ) + .getByRole( 'button', { name: 'Advanced' } ) + .click(); + await expect( - page.getByLabel( variables.labels.imageTitle ) + page + .getByRole( 'tabpanel', { name: 'Block' } ) + .getByLabel( 'Title attribute' ) ).toBeEnabled(); const titleValue = await page - .getByLabel( variables.labels.imageTitle ) + .getByRole( 'tabpanel', { name: 'Block' } ) + .getByLabel( 'Title attribute' ) .inputValue(); expect( titleValue ).toBe( 'default title value' ); } ); @@ -522,7 +530,22 @@ test.describe( 'Block bindings', () => { editor, page, } ) => { - await editor.insertBlock( variables.blocks.images.altOnly ); + await editor.insertBlock( { + name: 'core/image', + attributes: { + url: imagePlaceholderSrc, + alt: 'default alt value', + title: 'default title value', + metadata: { + bindings: { + alt: { + source: 'core/post-meta', + args: { key: 'text_custom_field' }, + }, + }, + }, + }, + } ); const imageBlock = editor.canvas.getByRole( 'document', { name: 'Block: Image', } ); @@ -530,27 +553,38 @@ test.describe( 'Block bindings', () => { // Replace controls exist. await expect( - page.getByRole( 'button', { - name: variables.labels.imageReplace, - } ) + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { + name: 'Replace', + } ) ).toBeVisible(); // Alt textarea is disabled and with the custom field value. await expect( - page.getByLabel( variables.labels.imageAlt ) + page + .getByRole( 'tabpanel', { name: 'Block' } ) + .getByLabel( 'Alternative text' ) ).toBeDisabled(); const altValue = await page - .getByLabel( variables.labels.imageAlt ) + .getByRole( 'tabpanel', { name: 'Block' } ) + .getByLabel( 'Alternative text' ) .inputValue(); - expect( altValue ).toBe( variables.customFields.textKey ); + expect( altValue ).toBe( 'text_custom_field' ); // Title input is enabled and with the original value. - await page.getByRole( 'button', { name: 'Advanced' } ).click(); + await page + .getByRole( 'tabpanel', { name: 'Settings' } ) + .getByRole( 'button', { name: 'Advanced' } ) + .click(); await expect( - page.getByLabel( variables.labels.imageTitle ) + page + .getByRole( 'tabpanel', { name: 'Block' } ) + .getByLabel( 'Title attribute' ) ).toBeEnabled(); const titleValue = await page - .getByLabel( variables.labels.imageTitle ) + .getByRole( 'tabpanel', { name: 'Block' } ) + .getByLabel( 'Title attribute' ) .inputValue(); expect( titleValue ).toBe( 'default title value' ); } ); @@ -559,7 +593,22 @@ test.describe( 'Block bindings', () => { editor, page, } ) => { - await editor.insertBlock( variables.blocks.images.titleOnly ); + await editor.insertBlock( { + name: 'core/image', + attributes: { + url: imagePlaceholderSrc, + alt: 'default alt value', + title: 'default title value', + metadata: { + bindings: { + title: { + source: 'core/post-meta', + args: { key: 'text_custom_field' }, + }, + }, + }, + }, + } ); const imageBlock = editor.canvas.getByRole( 'document', { name: 'Block: Image', } ); @@ -567,38 +616,66 @@ test.describe( 'Block bindings', () => { // Replace controls exist. await expect( - page.getByRole( 'button', { - name: variables.labels.imageReplace, - } ) + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { + name: 'Replace', + } ) ).toBeVisible(); // Alt textarea is enabled and with the original value. await expect( - page.getByLabel( variables.labels.imageAlt ) + page + .getByRole( 'tabpanel', { name: 'Block' } ) + .getByLabel( 'Alternative text' ) ).toBeEnabled(); const altValue = await page - .getByLabel( variables.labels.imageAlt ) + .getByRole( 'tabpanel', { name: 'Block' } ) + .getByLabel( 'Alternative text' ) .inputValue(); expect( altValue ).toBe( 'default alt value' ); // Title input is disabled and with the custom field value. - await page.getByRole( 'button', { name: 'Advanced' } ).click(); + await page + .getByRole( 'tabpanel', { name: 'Settings' } ) + .getByRole( 'button', { name: 'Advanced' } ) + .click(); await expect( - page.getByLabel( variables.labels.imageTitle ) + page + .getByRole( 'tabpanel', { name: 'Block' } ) + .getByLabel( 'Title attribute' ) ).toBeDisabled(); const titleValue = await page - .getByLabel( variables.labels.imageTitle ) + .getByRole( 'tabpanel', { name: 'Block' } ) + .getByLabel( 'Title attribute' ) .inputValue(); - expect( titleValue ).toBe( variables.customFields.textKey ); + expect( titleValue ).toBe( 'text_custom_field' ); } ); test( 'Multiple bindings should lock the appropriate controls', async ( { editor, page, } ) => { - await editor.insertBlock( - variables.blocks.images.multipleAttrs - ); + await editor.insertBlock( { + name: 'core/image', + attributes: { + url: imagePlaceholderSrc, + alt: 'default alt value', + title: 'default title value', + metadata: { + bindings: { + url: { + source: 'core/post-meta', + args: { key: 'url_custom_field' }, + }, + alt: { + source: 'core/post-meta', + args: { key: 'text_custom_field' }, + }, + }, + }, + }, + } ); const imageBlock = editor.canvas.getByRole( 'document', { name: 'Block: Image', } ); @@ -606,9 +683,11 @@ test.describe( 'Block bindings', () => { // Replace controls don't exist. await expect( - page.getByRole( 'button', { - name: variables.labels.imageReplace, - } ) + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { + name: 'Replace', + } ) ).toBeHidden(); // Image placeholder doesn't show the upload button. @@ -618,20 +697,29 @@ test.describe( 'Block bindings', () => { // Alt textarea is disabled and with the custom field value. await expect( - page.getByLabel( variables.labels.imageAlt ) + page + .getByRole( 'tabpanel', { name: 'Block' } ) + .getByLabel( 'Alternative text' ) ).toBeDisabled(); const altValue = await page - .getByLabel( variables.labels.imageAlt ) + .getByRole( 'tabpanel', { name: 'Block' } ) + .getByLabel( 'Alternative text' ) .inputValue(); - expect( altValue ).toBe( variables.customFields.textKey ); + expect( altValue ).toBe( 'text_custom_field' ); // Title input is enabled and with the original value. - await page.getByRole( 'button', { name: 'Advanced' } ).click(); + await page + .getByRole( 'tabpanel', { name: 'Settings' } ) + .getByRole( 'button', { name: 'Advanced' } ) + .click(); await expect( - page.getByLabel( variables.labels.imageTitle ) + page + .getByRole( 'tabpanel', { name: 'Block' } ) + .getByLabel( 'Title attribute' ) ).toBeEnabled(); const titleValue = await page - .getByLabel( variables.labels.imageTitle ) + .getByRole( 'tabpanel', { name: 'Block' } ) + .getByLabel( 'Title attribute' ) .inputValue(); expect( titleValue ).toBe( 'default title value' ); } ); @@ -645,23 +733,51 @@ test.describe( 'Block bindings', () => { test.describe( 'Paragraph', () => { test( 'Should show the value of the custom field when exists', async ( { editor, + page, + BlockBindingsUtils, } ) => { - await editor.insertBlock( variables.blocks.paragraph ); + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: 'paragraph default content', + metadata: { + bindings: { + content: { + source: 'core/post-meta', + args: { key: 'text_custom_field' }, + }, + }, + }, + }, + } ); const paragraphBlock = editor.canvas.getByRole( 'document', { name: 'Block: Paragraph', } ); - const paragraphContent = await paragraphBlock.textContent(); - expect( paragraphContent ).toBe( - variables.customFields.textValue + await expect( paragraphBlock ).toHaveText( + 'Value of the text_custom_field' ); // Paragraph is not editable. - const isContentEditable = - await paragraphBlock.getAttribute( 'contenteditable' ); - expect( isContentEditable ).toBe( 'false' ); + await expect( paragraphBlock ).toHaveAttribute( + 'contenteditable', + 'false' + ); + + // Check the frontend shows the value of the custom field. + await BlockBindingsUtils.setId( 'paragraph-binding' ); + const postId = await editor.publishPost(); + await page.goto( `/?p=${ postId }` ); + await expect( + page.locator( '#paragraph-binding' ) + ).toBeVisible(); + await expect( page.locator( '#paragraph-binding' ) ).toHaveText( + 'Value of the text_custom_field' + ); } ); test( "Should show the value of the key when custom field doesn't exists", async ( { editor, + page, + BlockBindingsUtils, } ) => { await editor.insertBlock( { name: 'core/paragraph', @@ -680,51 +796,210 @@ test.describe( 'Block bindings', () => { const paragraphBlock = editor.canvas.getByRole( 'document', { name: 'Block: Paragraph', } ); - const paragraphContent = await paragraphBlock.textContent(); - expect( paragraphContent ).toBe( 'non_existing_custom_field' ); + await expect( paragraphBlock ).toHaveText( + 'non_existing_custom_field' + ); // Paragraph is not editable. - const isContentEditable = - await paragraphBlock.getAttribute( 'contenteditable' ); - expect( isContentEditable ).toBe( 'false' ); + await expect( paragraphBlock ).toHaveAttribute( + 'contenteditable', + 'false' + ); + + // Check the frontend doesn't show the content. + await BlockBindingsUtils.setId( 'paragraph-binding' ); + const postId = await editor.publishPost(); + await page.goto( `/?p=${ postId }` ); + await expect( + page.locator( '#paragraph-binding' ) + ).toBeHidden(); } ); } ); test( 'Heading - should show the value of the custom field', async ( { editor, + page, + BlockBindingsUtils, } ) => { - await editor.insertBlock( variables.blocks.heading ); + await editor.insertBlock( { + name: 'core/heading', + attributes: { + content: 'heading default content', + metadata: { + bindings: { + content: { + source: 'core/post-meta', + args: { key: 'text_custom_field' }, + }, + }, + }, + }, + } ); const headingBlock = editor.canvas.getByRole( 'document', { name: 'Block: Heading', } ); - const headingContent = await headingBlock.textContent(); - expect( headingContent ).toBe( variables.customFields.textValue ); + await expect( headingBlock ).toHaveText( + 'Value of the text_custom_field' + ); // Heading is not editable. - const isContentEditable = - await headingBlock.getAttribute( 'contenteditable' ); - expect( isContentEditable ).toBe( 'false' ); + await expect( headingBlock ).toHaveAttribute( + 'contenteditable', + 'false' + ); + + // Check the frontend shows the value of the custom field. + await BlockBindingsUtils.setId( 'heading-binding' ); + const postId = await editor.publishPost(); + await page.goto( `/?p=${ postId }` ); + await expect( page.locator( '#heading-binding' ) ).toBeVisible(); + await expect( page.locator( '#heading-binding' ) ).toHaveText( + 'Value of the text_custom_field' + ); } ); - test( 'Button - should show the value of the custom field when text is bound', async ( { - editor, - } ) => { - await editor.insertBlock( variables.blocks.buttons.textOnly ); - const buttonBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Button', - exact: true, + test.describe( 'Button', () => { + test( 'Should show the value of the custom field when text is bound', async ( { + editor, + page, + BlockBindingsUtils, + } ) => { + await editor.insertBlock( { + name: 'core/buttons', + innerBlocks: [ + { + name: 'core/button', + attributes: { + text: 'button default text', + url: '#default-url', + metadata: { + bindings: { + text: { + source: 'core/post-meta', + args: { key: 'text_custom_field' }, + }, + }, + }, + }, + }, + ], + } ); + const buttonBlock = editor.canvas + .getByRole( 'document', { + name: 'Block: Button', + exact: true, + } ) + .locator( 'div' ); + await buttonBlock.click(); + await expect( buttonBlock ).toHaveText( + 'Value of the text_custom_field' + ); + + // Button is not editable. + await expect( buttonBlock ).toHaveAttribute( + 'contenteditable', + 'false' + ); + + // Check the frontend shows the value of the custom field. + await BlockBindingsUtils.setId( 'button-text-binding' ); + const postId = await editor.publishPost(); + await page.goto( `/?p=${ postId }` ); + const buttonDom = page.locator( '#button-text-binding a' ); + await expect( buttonDom ).toBeVisible(); + await expect( buttonDom ).toHaveText( + 'Value of the text_custom_field' + ); + await expect( buttonDom ).toHaveAttribute( + 'href', + '#default-url' + ); + } ); + + test( 'Should use the value of the custom field when url is bound', async ( { + editor, + page, + BlockBindingsUtils, + } ) => { + await editor.insertBlock( { + name: 'core/buttons', + innerBlocks: [ + { + name: 'core/button', + attributes: { + text: 'button default text', + url: '#default-url', + metadata: { + bindings: { + url: { + source: 'core/post-meta', + args: { key: 'url_custom_field' }, + }, + }, + }, + }, + }, + ], + } ); + + // Check the frontend shows the original value of the custom field. + await BlockBindingsUtils.setId( 'button-url-binding' ); + const postId = await editor.publishPost(); + await page.goto( `/?p=${ postId }` ); + const buttonDom = page.locator( '#button-url-binding a' ); + await expect( buttonDom ).toBeVisible(); + await expect( buttonDom ).toHaveText( 'button default text' ); + await expect( buttonDom ).toHaveAttribute( + 'href', + '#url-custom-field' + ); + } ); + + test( 'Should use the values of the custom fields when text and url are bound', async ( { + editor, + page, + BlockBindingsUtils, + } ) => { + await editor.insertBlock( { + name: 'core/buttons', + innerBlocks: [ + { + name: 'core/button', + attributes: { + text: 'button default text', + url: '#default-url', + metadata: { + bindings: { + text: { + source: 'core/post-meta', + args: { key: 'text_custom_field' }, + }, + url: { + source: 'core/post-meta', + args: { key: 'url_custom_field' }, + }, + }, + }, + }, + }, + ], + } ); + + // Check the frontend uses the values of the custom fields. + await BlockBindingsUtils.setId( 'button-multiple-bindings' ); + const postId = await editor.publishPost(); + await page.goto( `/?p=${ postId }` ); + const buttonDom = page.locator( '#button-multiple-bindings a' ); + await expect( buttonDom ).toBeVisible(); + await expect( buttonDom ).toHaveText( + 'Value of the text_custom_field' + ); + await expect( buttonDom ).toHaveAttribute( + 'href', + '#url-custom-field' + ); } ); - await buttonBlock.click(); - const buttonText = await buttonBlock.textContent(); - expect( buttonText ).toBe( variables.customFields.textValue ); - - // Button is not editable. - const isContentEditable = await buttonBlock - .locator( 'div' ) - .getAttribute( 'contenteditable' ); - expect( isContentEditable ).toBe( 'false' ); } ); test.describe( 'Image', () => { - let customFieldSrc; test.beforeAll( async ( { requestUtils } ) => { const customFieldMedia = await requestUtils.uploadMedia( path.join( @@ -732,7 +1007,7 @@ test.describe( 'Block bindings', () => { '1024x768_e2e_test_image_size.jpeg' ) ); - customFieldSrc = customFieldMedia.source_url; + imageCustomFieldSrc = customFieldMedia.source_url; } ); test.beforeEach( async ( { editor, page, requestUtils } ) => { @@ -742,7 +1017,7 @@ test.describe( 'Block bindings', () => { path: '/wp/v2/posts/' + postId, data: { meta: { - url_custom_field: customFieldSrc, + url_custom_field: imageCustomFieldSrc, }, }, } ); @@ -750,22 +1025,76 @@ test.describe( 'Block bindings', () => { } ); test( 'Should show the value of the custom field when url is bound', async ( { editor, + page, + BlockBindingsUtils, } ) => { - await editor.insertBlock( variables.blocks.images.urlOnly ); + await editor.insertBlock( { + name: 'core/image', + attributes: { + url: imagePlaceholderSrc, + alt: 'default alt value', + title: 'default title value', + metadata: { + bindings: { + url: { + source: 'core/post-meta', + args: { key: 'url_custom_field' }, + }, + }, + }, + }, + } ); const imageBlockImg = editor.canvas .getByRole( 'document', { name: 'Block: Image', } ) .locator( 'img' ); - const imageSrc = await imageBlockImg.getAttribute( 'src' ); - expect( imageSrc ).toBe( customFieldSrc ); + await expect( imageBlockImg ).toHaveAttribute( + 'src', + imageCustomFieldSrc + ); + + // Check the frontend uses the value of the custom field. + await BlockBindingsUtils.setId( 'image-url-binding' ); + const postId = await BlockBindingsUtils.updatePost(); + await page.goto( `/?p=${ postId }` ); + const imageDom = page.locator( '#image-url-binding img' ); + await expect( imageDom ).toBeVisible(); + await expect( imageDom ).toHaveAttribute( + 'src', + imageCustomFieldSrc + ); + await expect( imageDom ).toHaveAttribute( + 'alt', + 'default alt value' + ); + await expect( imageDom ).toHaveAttribute( + 'title', + 'default title value' + ); } ); test( 'Should show value of the custom field in the alt textarea when alt is bound', async ( { editor, page, + BlockBindingsUtils, } ) => { - await editor.insertBlock( variables.blocks.images.altOnly ); + await editor.insertBlock( { + name: 'core/image', + attributes: { + url: imagePlaceholderSrc, + alt: 'default alt value', + title: 'default title value', + metadata: { + bindings: { + alt: { + source: 'core/post-meta', + args: { key: 'text_custom_field' }, + }, + }, + }, + }, + } ); const imageBlockImg = editor.canvas .getByRole( 'document', { name: 'Block: Image', @@ -774,24 +1103,64 @@ test.describe( 'Block bindings', () => { await imageBlockImg.click(); // Image src is the placeholder. - const imageSrc = await imageBlockImg.getAttribute( 'src' ); - expect( imageSrc ).toBe( variables.placeholderSrc ); + await expect( imageBlockImg ).toHaveAttribute( + 'src', + imagePlaceholderSrc + ); // Alt textarea is disabled and with the custom field value. await expect( - page.getByLabel( variables.labels.imageAlt ) + page + .getByRole( 'tabpanel', { name: 'Block' } ) + .getByLabel( 'Alternative text' ) ).toBeDisabled(); const altValue = await page - .getByLabel( variables.labels.imageAlt ) + .getByRole( 'tabpanel', { name: 'Block' } ) + .getByLabel( 'Alternative text' ) .inputValue(); - expect( altValue ).toBe( variables.customFields.textValue ); + expect( altValue ).toBe( 'Value of the text_custom_field' ); + + // Check the frontend uses the value of the custom field. + await BlockBindingsUtils.setId( 'image-alt-binding' ); + const postId = await BlockBindingsUtils.updatePost(); + await page.goto( `/?p=${ postId }` ); + const imageDom = page.locator( '#image-alt-binding img' ); + await expect( imageDom ).toBeVisible(); + await expect( imageDom ).toHaveAttribute( + 'src', + imagePlaceholderSrc + ); + await expect( imageDom ).toHaveAttribute( + 'alt', + 'Value of the text_custom_field' + ); + await expect( imageDom ).toHaveAttribute( + 'title', + 'default title value' + ); } ); test( 'Should show value of the custom field in the title input when title is bound', async ( { editor, page, + BlockBindingsUtils, } ) => { - await editor.insertBlock( variables.blocks.images.titleOnly ); + await editor.insertBlock( { + name: 'core/image', + attributes: { + url: imagePlaceholderSrc, + alt: 'default alt value', + title: 'default title value', + metadata: { + bindings: { + title: { + source: 'core/post-meta', + args: { key: 'text_custom_field' }, + }, + }, + }, + }, + } ); const imageBlockImg = editor.canvas .getByRole( 'document', { name: 'Block: Image', @@ -800,27 +1169,78 @@ test.describe( 'Block bindings', () => { await imageBlockImg.click(); // Image src is the placeholder. - const imageSrc = await imageBlockImg.getAttribute( 'src' ); - expect( imageSrc ).toBe( variables.placeholderSrc ); + await expect( imageBlockImg ).toHaveAttribute( + 'src', + imagePlaceholderSrc + ); // Title input is disabled and with the custom field value. - await page.getByRole( 'button', { name: 'Advanced' } ).click(); + const advancedButton = page + .getByRole( 'tabpanel', { name: 'Block' } ) + .getByRole( 'button', { + name: 'Advanced', + } ); + const isAdvancedPanelOpen = + await advancedButton.getAttribute( 'aria-expanded' ); + if ( isAdvancedPanelOpen === 'false' ) { + await advancedButton.click(); + } await expect( - page.getByLabel( variables.labels.imageTitle ) + page + .getByRole( 'tabpanel', { name: 'Block' } ) + .getByLabel( 'Title attribute' ) ).toBeDisabled(); const titleValue = await page - .getByLabel( variables.labels.imageTitle ) + .getByRole( 'tabpanel', { name: 'Block' } ) + .getByLabel( 'Title attribute' ) .inputValue(); - expect( titleValue ).toBe( variables.customFields.textValue ); + expect( titleValue ).toBe( 'Value of the text_custom_field' ); + + // Check the frontend uses the value of the custom field. + await BlockBindingsUtils.setId( 'image-title-binding' ); + const postId = await BlockBindingsUtils.updatePost(); + await page.goto( `/?p=${ postId }` ); + const imageDom = page.locator( '#image-title-binding img' ); + await expect( imageDom ).toBeVisible(); + await expect( imageDom ).toHaveAttribute( + 'src', + imagePlaceholderSrc + ); + await expect( imageDom ).toHaveAttribute( + 'alt', + 'default alt value' + ); + await expect( imageDom ).toHaveAttribute( + 'title', + 'Value of the text_custom_field' + ); } ); test( 'Multiple bindings should show the value of the custom fields', async ( { editor, page, + BlockBindingsUtils, } ) => { - await editor.insertBlock( - variables.blocks.images.multipleAttrs - ); + await editor.insertBlock( { + name: 'core/image', + attributes: { + url: imagePlaceholderSrc, + alt: 'default alt value', + title: 'default title value', + metadata: { + bindings: { + url: { + source: 'core/post-meta', + args: { key: 'url_custom_field' }, + }, + alt: { + source: 'core/post-meta', + args: { key: 'text_custom_field' }, + }, + }, + }, + }, + } ); const imageBlockImg = editor.canvas .getByRole( 'document', { name: 'Block: Image', @@ -829,28 +1249,103 @@ test.describe( 'Block bindings', () => { await imageBlockImg.click(); // Image src is the custom field value. - const imageSrc = await imageBlockImg.getAttribute( 'src' ); - expect( imageSrc ).toBe( customFieldSrc ); + await expect( imageBlockImg ).toHaveAttribute( + 'src', + imageCustomFieldSrc + ); // Alt textarea is disabled and with the custom field value. await expect( - page.getByLabel( variables.labels.imageAlt ) + page + .getByRole( 'tabpanel', { name: 'Block' } ) + .getByLabel( 'Alternative text' ) ).toBeDisabled(); const altValue = await page - .getByLabel( variables.labels.imageAlt ) + .getByRole( 'tabpanel', { name: 'Block' } ) + .getByLabel( 'Alternative text' ) .inputValue(); - expect( altValue ).toBe( variables.customFields.textValue ); + expect( altValue ).toBe( 'Value of the text_custom_field' ); // Title input is enabled and with the original value. - await page.getByRole( 'button', { name: 'Advanced' } ).click(); + const advancedButton = page + .getByRole( 'tabpanel', { name: 'Block' } ) + .getByRole( 'button', { + name: 'Advanced', + } ); + const isAdvancedPanelOpen = + await advancedButton.getAttribute( 'aria-expanded' ); + if ( isAdvancedPanelOpen === 'false' ) { + await advancedButton.click(); + } await expect( - page.getByLabel( variables.labels.imageTitle ) + page + .getByRole( 'tabpanel', { name: 'Block' } ) + .getByLabel( 'Title attribute' ) ).toBeEnabled(); const titleValue = await page - .getByLabel( variables.labels.imageTitle ) + .getByRole( 'tabpanel', { name: 'Block' } ) + .getByLabel( 'Title attribute' ) .inputValue(); expect( titleValue ).toBe( 'default title value' ); + + // Check the frontend uses the values of the custom fields. + await BlockBindingsUtils.setId( 'image-multiple-bindings' ); + const postId = await BlockBindingsUtils.updatePost(); + await page.goto( `/?p=${ postId }` ); + const imageDom = page.locator( '#image-multiple-bindings img' ); + await expect( imageDom ).toBeVisible(); + await expect( imageDom ).toHaveAttribute( + 'src', + imageCustomFieldSrc + ); + await expect( imageDom ).toHaveAttribute( + 'alt', + 'Value of the text_custom_field' + ); + await expect( imageDom ).toHaveAttribute( + 'title', + 'default title value' + ); } ); } ); } ); } ); + +class BlockBindingsUtils { + constructor( { page } ) { + this.page = page; + } + + // Helper to add an anchor/id to be able to locate the block in the frontend. + async setId( testId ) { + const isAdvancedPanelOpen = await this.page + .getByRole( 'tabpanel', { name: 'Block' } ) + .getByRole( 'button', { name: 'Advanced' } ) + .getAttribute( 'aria-expanded' ); + if ( isAdvancedPanelOpen === 'false' ) { + await this.page + .getByRole( 'tabpanel', { name: 'Block' } ) + .getByRole( 'button', { name: 'Advanced' } ) + .click(); + } + await this.page + .getByRole( 'tabpanel', { name: 'Block' } ) + .getByLabel( 'HTML anchor' ) + .fill( testId ); + } + + // Helper to update the post. + async updatePost() { + await this.page + .getByRole( 'region', { name: 'Editor top bar' } ) + .getByRole( 'button', { name: 'Update' } ) + .click(); + await this.page + .getByRole( 'button', { name: 'Dismiss this notice' } ) + .filter( { hasText: 'updated' } ) + .waitFor(); + const postId = new URL( this.page.url() ).searchParams.get( 'post' ); + + return typeof postId === 'string' ? parseInt( postId, 10 ) : null; + } +} From a7742b89cf5163bfbd5e4bfdefbb037d3b7dfbd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= <583546+oandregal@users.noreply.github.com> Date: Tue, 6 Feb 2024 10:04:01 +0100 Subject: [PATCH 37/41] DataViews: remove test artifact (status filter was set as primary) (#58682) --- packages/edit-site/src/components/page-pages/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/edit-site/src/components/page-pages/index.js b/packages/edit-site/src/components/page-pages/index.js index 82d8c72e24420..73dd87eeab5a5 100644 --- a/packages/edit-site/src/components/page-pages/index.js +++ b/packages/edit-site/src/components/page-pages/index.js @@ -332,7 +332,6 @@ export default function PagePages() { enableSorting: false, filterBy: { operators: [ OPERATOR_IN ], - isPrimary: true, }, }, { From 769b1e71681609cec577530a594301817d74c4b0 Mon Sep 17 00:00:00 2001 From: Mario Santos <34552881+SantosGuillamot@users.noreply.github.com> Date: Tue, 6 Feb 2024 10:04:11 +0100 Subject: [PATCH 38/41] Block Bindings: Backport block bindings refactor from WordPress core (#58683) * Change folder of block bindings sources * Update registry * Add `get_block_bindings_source` method * Update bindings processing * Add comment in block bindings registry * Do not escape the attribute passed to `$processor->set_attribute()` --- .../block-bindings/block-bindings.php | 24 +++++-- .../class-wp-block-bindings-registry.php | 12 ++-- .../pattern.php => pattern-overrides.php} | 17 ++++- .../{sources => }/post-meta.php | 18 +++-- lib/compat/wordpress-6.5/blocks.php | 66 ++++++++++--------- lib/load.php | 4 +- 6 files changed, 92 insertions(+), 49 deletions(-) rename lib/compat/wordpress-6.5/block-bindings/{sources/pattern.php => pattern-overrides.php} (61%) rename lib/compat/wordpress-6.5/block-bindings/{sources => }/post-meta.php (64%) diff --git a/lib/compat/wordpress-6.5/block-bindings/block-bindings.php b/lib/compat/wordpress-6.5/block-bindings/block-bindings.php index 3da4ecc185ab5..87518d6afc5d3 100644 --- a/lib/compat/wordpress-6.5/block-bindings/block-bindings.php +++ b/lib/compat/wordpress-6.5/block-bindings/block-bindings.php @@ -19,8 +19,10 @@ * * @since 6.5.0 * - * @param string $source_name The name of the source. - * @param array $source_properties { + * @param string $source_name The name of the source. It must be a string containing a namespace prefix, i.e. + * `my-plugin/my-custom-source`. It must only contain lowercase alphanumeric + * characters, the forward slash `/` and dashes. + * @param array $source_properties { * The array of arguments that are used to register a source. * * @type string $label The label of the source. @@ -39,7 +41,7 @@ * @return array|false Source when the registration was successful, or `false` on failure. */ if ( ! function_exists( 'register_block_bindings_source' ) ) { - function register_block_bindings_source( $source_name, array $source_properties ) { + function register_block_bindings_source( string $source_name, array $source_properties ) { return WP_Block_Bindings_Registry::get_instance()->register( $source_name, $source_properties ); } } @@ -53,7 +55,7 @@ function register_block_bindings_source( $source_name, array $source_properties * @return array|false The unregistred block bindings source on success and `false` otherwise. */ if ( ! function_exists( 'unregister_block_bindings_source' ) ) { - function unregister_block_bindings_source( $source_name ) { + function unregister_block_bindings_source( string $source_name ) { return WP_Block_Bindings_Registry::get_instance()->unregister( $source_name ); } } @@ -70,3 +72,17 @@ function get_all_registered_block_bindings_sources() { return WP_Block_Bindings_Registry::get_instance()->get_all_registered(); } } + +/** + * Retrieves a registered block bindings source. + * + * @since 6.5.0 + * + * @param string $source_name The name of the source. + * @return array|null The registered block bindings source, or `null` if it is not registered. + */ +if ( ! function_exists( 'get_block_bindings_source' ) ) { + function get_block_bindings_source( string $source_name ) { + return WP_Block_Bindings_Registry::get_instance()->get_registered( $source_name ); + } +} diff --git a/lib/compat/wordpress-6.5/block-bindings/class-wp-block-bindings-registry.php b/lib/compat/wordpress-6.5/block-bindings/class-wp-block-bindings-registry.php index b6f17dd53af81..5a5322bec4409 100644 --- a/lib/compat/wordpress-6.5/block-bindings/class-wp-block-bindings-registry.php +++ b/lib/compat/wordpress-6.5/block-bindings/class-wp-block-bindings-registry.php @@ -44,7 +44,9 @@ final class WP_Block_Bindings_Registry { * * @since 6.5.0 * - * @param string $source_name The name of the source. + * @param string $source_name The name of the source. It must be a string containing a namespace prefix, i.e. + * `my-plugin/my-custom-source`. It must only contain lowercase alphanumeric + * characters, the forward slash `/` and dashes. * @param array $source_properties { * The array of arguments that are used to register a source. * @@ -57,13 +59,13 @@ final class WP_Block_Bindings_Registry { * used to look up the override value, * i.e. {"key": "foo"}. * - @param WP_Block $block_instance The block instance. - * - @param string $attribute_name The name of an attribute . + * - @param string $attribute_name The name of the target attribute . * The callback has a mixed return type; it may return a string to override * the block's original value, null, false to remove an attribute, etc. * } * @return array|false Source when the registration was successful, or `false` on failure. */ - public function register( $source_name, array $source_properties ) { + public function register( string $source_name, array $source_properties ) { if ( ! is_string( $source_name ) ) { _doing_it_wrong( __METHOD__, @@ -120,7 +122,7 @@ public function register( $source_name, array $source_properties ) { * @param string $source_name Block bindings source name including namespace. * @return array|false The unregistred block bindings source on success and `false` otherwise. */ - public function unregister( $source_name ) { + public function unregister( string $source_name ) { if ( ! $this->is_registered( $source_name ) ) { _doing_it_wrong( __METHOD__, @@ -156,7 +158,7 @@ public function get_all_registered() { * @param string $source_name The name of the source. * @return array|null The registered block bindings source, or `null` if it is not registered. */ - public function get_registered( $source_name ) { + public function get_registered( string $source_name ) { if ( ! $this->is_registered( $source_name ) ) { return null; } diff --git a/lib/compat/wordpress-6.5/block-bindings/sources/pattern.php b/lib/compat/wordpress-6.5/block-bindings/pattern-overrides.php similarity index 61% rename from lib/compat/wordpress-6.5/block-bindings/sources/pattern.php rename to lib/compat/wordpress-6.5/block-bindings/pattern-overrides.php index 9168cfc785b55..0e6d3fc94eeaa 100644 --- a/lib/compat/wordpress-6.5/block-bindings/sources/pattern.php +++ b/lib/compat/wordpress-6.5/block-bindings/pattern-overrides.php @@ -1,17 +1,30 @@ "foo" ). + * @param WP_Block $block_instance The block instance. + * @param string $attribute_name The name of the target attribute. + * @return mixed The value computed for the source. + */ function gutenberg_block_bindings_pattern_overrides_callback( $source_attrs, $block_instance, $attribute_name ) { - if ( ! _wp_array_get( $block_instance->attributes, array( 'metadata', 'id' ), false ) ) { + if ( empty( $block_instance->attributes['metadata']['id'] ) ) { return null; } $block_id = $block_instance->attributes['metadata']['id']; return _wp_array_get( $block_instance->context, array( 'pattern/overrides', $block_id, 'values', $attribute_name ), null ); } +/** + * Registers Pattern Overrides source in the Block Bindings registry. + */ function gutenberg_register_block_bindings_pattern_overrides_source() { // Override the "core/pattern-overrides" source from core. if ( array_key_exists( 'core/pattern-overrides', get_all_registered_block_bindings_sources() ) ) { diff --git a/lib/compat/wordpress-6.5/block-bindings/sources/post-meta.php b/lib/compat/wordpress-6.5/block-bindings/post-meta.php similarity index 64% rename from lib/compat/wordpress-6.5/block-bindings/sources/post-meta.php rename to lib/compat/wordpress-6.5/block-bindings/post-meta.php index e9b84108e52a3..655abd982663b 100644 --- a/lib/compat/wordpress-6.5/block-bindings/sources/post-meta.php +++ b/lib/compat/wordpress-6.5/block-bindings/post-meta.php @@ -1,9 +1,17 @@ "foo" ). + * @return mixed The value computed for the source. + */ function gutenberg_block_bindings_post_meta_callback( $source_attrs ) { if ( ! isset( $source_attrs['key'] ) ) { return null; @@ -17,16 +25,18 @@ function gutenberg_block_bindings_post_meta_callback( $source_attrs ) { $post_id = get_the_ID(); } - // If a post isn't public, we need to prevent - // unauthorized users from accessing the post meta. + // If a post isn't public, we need to prevent unauthorized users from accessing the post meta. $post = get_post( $post_id ); - if ( ( $post && 'publish' !== $post->post_status && ! current_user_can( 'read_post', $post_id ) ) || post_password_required( $post_id ) ) { + if ( ( ! is_post_publicly_viewable( $post ) && ! current_user_can( 'read_post', $post_id ) ) || post_password_required( $post ) ) { return null; } return get_post_meta( $post_id, $source_attrs['key'], true ); } +/** + * Registers Post Meta source in the block bindings registry. + */ function gutenberg_register_block_bindings_post_meta_source() { // Override the "core/post-meta" source from core. if ( array_key_exists( 'core/post-meta', get_all_registered_block_bindings_sources() ) ) { diff --git a/lib/compat/wordpress-6.5/blocks.php b/lib/compat/wordpress-6.5/blocks.php index 1ca5865e64dc5..ba83a30f52962 100644 --- a/lib/compat/wordpress-6.5/blocks.php +++ b/lib/compat/wordpress-6.5/blocks.php @@ -47,29 +47,29 @@ function gutenberg_register_metadata_attribute( $args ) { add_filter( 'register_block_type_args', 'gutenberg_register_metadata_attribute' ); /** - * Replaces the HTML content of a block based on the provided source value. + * Depending on the block attribute name, replace its value in the HTML based on the value provided. * - * @param string $block_content Block Content. - * @param string $block_name The name of the block to process. - * @param string $block_attr The attribute of the block we want to process. - * @param string $source_value The value used to replace the HTML. + * @param string $block_content Block Content. + * @param string $block_name The name of the block to process. + * @param string $attribute_name The attribute name to replace. + * @param mixed $source_value The value used to replace in the HTML. * @return string The modified block content. */ -function gutenberg_block_bindings_replace_html( $block_content, $block_name, $block_attr, $source_value ) { +function gutenberg_block_bindings_replace_html( $block_content, $block_name, string $attribute_name, $source_value ) { $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block_name ); - if ( null === $block_type ) { - return; + if ( ! isset( $block_type->attributes[ $attribute_name ] ) ) { + return $block_content; } // Depending on the attribute source, the processing will be different. - switch ( $block_type->attributes[ $block_attr ]['source'] ) { + switch ( $block_type->attributes[ $attribute_name ]['source'] ) { case 'html': case 'rich-text': $block_reader = new WP_HTML_Tag_Processor( $block_content ); // TODO: Support for CSS selectors whenever they are ready in the HTML API. // In the meantime, support comma-separated selectors by exploding them into an array. - $selectors = explode( ',', $block_type->attributes[ $block_attr ]['selector'] ); + $selectors = explode( ',', $block_type->attributes[ $attribute_name ]['selector'] ); // Add a bookmark to the first tag to be able to iterate over the selectors. $block_reader->next_tag(); $block_reader->set_bookmark( 'iterate-selectors' ); @@ -133,12 +133,12 @@ function gutenberg_block_bindings_replace_html( $block_content, $block_name, $bl if ( ! $amended_content->next_tag( array( // TODO: build the query from CSS selector. - 'tag_name' => $block_type->attributes[ $block_attr ]['selector'], + 'tag_name' => $block_type->attributes[ $attribute_name ]['selector'], ) ) ) { return $block_content; } - $amended_content->set_attribute( $block_type->attributes[ $block_attr ]['attribute'], esc_attr( $source_value ) ); + $amended_content->set_attribute( $block_type->attributes[ $attribute_name ]['attribute'], $source_value ); return $amended_content->get_updated_html(); break; @@ -153,10 +153,10 @@ function gutenberg_block_bindings_replace_html( $block_content, $block_name, $bl * Process the block bindings attribute. * * @param string $block_content Block Content. - * @param array $block Block attributes. + * @param array $parsed_block The full block, including name and attributes. * @param WP_Block $block_instance The block instance. */ -function gutenberg_process_block_bindings( $block_content, $block, $block_instance ) { +function gutenberg_process_block_bindings( $block_content, $parsed_block, $block_instance ) { // Allowed blocks that support block bindings. // TODO: Look for a mechanism to opt-in for this. Maybe adding a property to block attributes? $allowed_blocks = array( @@ -167,7 +167,11 @@ function gutenberg_process_block_bindings( $block_content, $block, $block_instan ); // If the block doesn't have the bindings property or isn't one of the allowed block types, return. - if ( ! isset( $block['attrs']['metadata']['bindings'] ) || ! isset( $allowed_blocks[ $block_instance->name ] ) ) { + if ( + ! isset( $allowed_blocks[ $block_instance->name ] ) || + empty( $parsed_block['attrs']['metadata']['bindings'] ) || + ! is_array( $parsed_block['attrs']['metadata']['bindings'] ) + ) { return $block_content; } @@ -186,34 +190,32 @@ function gutenberg_process_block_bindings( $block_content, $block, $block_instan * } */ - $block_bindings_sources = get_all_registered_block_bindings_sources(); $modified_block_content = $block_content; - foreach ( $block['attrs']['metadata']['bindings'] as $binding_attribute => $binding_source ) { - // If the attribute is not in the list, process next attribute. - if ( ! in_array( $binding_attribute, $allowed_blocks[ $block_instance->name ], true ) ) { + foreach ( $parsed_block['attrs']['metadata']['bindings'] as $attribute_name => $block_binding ) { + // If the attribute is not in the allowed list, process next attribute. + if ( ! in_array( $attribute_name, $allowed_blocks[ $block_instance->name ], true ) ) { continue; } // If no source is provided, or that source is not registered, process next attribute. - if ( ! isset( $binding_source['source'] ) || ! is_string( $binding_source['source'] ) || ! isset( $block_bindings_sources[ $binding_source['source'] ] ) ) { + if ( ! isset( $block_binding['source'] ) || ! is_string( $block_binding['source'] ) ) { continue; } - $source_callback = $block_bindings_sources[ $binding_source['source'] ]['get_value_callback']; - // Get the value based on the source. - if ( ! isset( $binding_source['args'] ) ) { - $source_args = array(); - } else { - $source_args = $binding_source['args']; - } - $source_value = $source_callback( $source_args, $block_instance, $binding_attribute ); - // If the value is null, process next attribute. - if ( is_null( $source_value ) ) { + $block_binding_source = get_block_bindings_source( $block_binding['source'] ); + if ( null === $block_binding_source ) { continue; } - // Process the HTML based on the block and the attribute. - $modified_block_content = gutenberg_block_bindings_replace_html( $modified_block_content, $block_instance->name, $binding_attribute, $source_value ); + $source_callback = $block_binding_source['get_value_callback']; + $source_args = ! empty( $block_binding['args'] ) && is_array( $block_binding['args'] ) ? $block_binding['args'] : array(); + $source_value = call_user_func_array( $source_callback, array( $source_args, $block_instance, $attribute_name ) ); + + // If the value is not null, process the HTML based on the block and the attribute. + if ( ! is_null( $source_value ) ) { + $modified_block_content = gutenberg_block_bindings_replace_html( $modified_block_content, $block_instance->name, $attribute_name, $source_value ); + } } + return $modified_block_content; } diff --git a/lib/load.php b/lib/load.php index 5fd9df15d987e..3d9213ce6e93a 100644 --- a/lib/load.php +++ b/lib/load.php @@ -117,8 +117,8 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/compat/wordpress-6.5/block-bindings/class-wp-block-bindings-registry.php'; } require __DIR__ . '/compat/wordpress-6.5/block-bindings/block-bindings.php'; -require __DIR__ . '/compat/wordpress-6.5/block-bindings/sources/post-meta.php'; -require __DIR__ . '/compat/wordpress-6.5/block-bindings/sources/pattern.php'; +require __DIR__ . '/compat/wordpress-6.5/block-bindings/post-meta.php'; +require __DIR__ . '/compat/wordpress-6.5/block-bindings/pattern-overrides.php'; require __DIR__ . '/compat/wordpress-6.5/script-loader.php'; // Experimental features. From 519824a7195b4a5ec20e28c8712440388c2aa024 Mon Sep 17 00:00:00 2001 From: Brooke <35543432+brookewp@users.noreply.github.com> Date: Tue, 6 Feb 2024 01:17:27 -0800 Subject: [PATCH 39/41] CustomSelect: add tests for new features (#58583) * CustomSelect: add tests for new features * Update tests based on PR feedback * Test against controlled vs uncontrolled and group multiselect tests * Remove span and add additional assertion for option * Update changelog --- packages/components/CHANGELOG.md | 3 +- .../custom-select-control-v2/test/index.tsx | 219 ++++++++++++++++++ 2 files changed, 221 insertions(+), 1 deletion(-) create mode 100644 packages/components/src/custom-select-control-v2/test/index.tsx diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 72e22bbe0c117..5b9aaeea25a2c 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -25,7 +25,7 @@ - `Tabs`: improve controlled mode focus handling and keyboard navigation ([#57696](https://github.com/WordPress/gutenberg/pull/57696)). - `Tabs`: prevent internal focus from updating too early ([#58625](https://github.com/WordPress/gutenberg/pull/58625)). - Expand theming support in the `COLORS` variable object ([#58097](https://github.com/WordPress/gutenberg/pull/58097)). -- `CustomSelect`: disable `virtualFocus` to fix issue for screenreaders ([#58585](https://github.com/WordPress/gutenberg/pull/58585)) +- `CustomSelect`: disable `virtualFocus` to fix issue for screenreaders ([#58585](https://github.com/WordPress/gutenberg/pull/58585)). ### Enhancements @@ -35,6 +35,7 @@ - `Composite`: Removing Reakit `Composite` implementation ([#58620](https://github.com/WordPress/gutenberg/pull/58620)). - Removing Reakit as a dependency of the components package ([#58631](https://github.com/WordPress/gutenberg/pull/58631)). +- `CustomSelect`: add unit tests ([#58583](https://github.com/WordPress/gutenberg/pull/58583)). ## 25.16.0 (2024-01-24) diff --git a/packages/components/src/custom-select-control-v2/test/index.tsx b/packages/components/src/custom-select-control-v2/test/index.tsx new file mode 100644 index 0000000000000..a707a56e4d724 --- /dev/null +++ b/packages/components/src/custom-select-control-v2/test/index.tsx @@ -0,0 +1,219 @@ +/** + * External dependencies + */ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +/** + * WordPress dependencies + */ +import { useState } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { CustomSelect, CustomSelectItem } from '..'; +import type { CustomSelectProps } from '../types'; + +const ControlledCustomSelect = ( props: CustomSelectProps ) => { + const [ value, setValue ] = useState< string | string[] >(); + return ( + { + setValue( nextValue ); + props.onChange?.( nextValue ); + } } + value={ value } + /> + ); +}; + +describe.each( [ + [ 'uncontrolled', CustomSelect ], + [ 'controlled', ControlledCustomSelect ], +] )( 'CustomSelect %s', ( ...modeAndComponent ) => { + const [ , Component ] = modeAndComponent; + + describe( 'Multiple selection', () => { + it( 'Should be able to select multiple items when provided an array', async () => { + const user = userEvent.setup(); + const onChangeMock = jest.fn(); + + // initial selection as defaultValue + const defaultValues = [ + 'incandescent glow', + 'ultraviolet morning light', + ]; + + render( + + { [ + 'aurora borealis green', + 'flamingo pink sunrise', + 'incandescent glow', + 'rose blush', + 'ultraviolet morning light', + ].map( ( item ) => ( + + { item } + + ) ) } + + ); + + const currentSelectedItem = screen.getByRole( 'combobox', { + expanded: false, + } ); + + // ensure more than one item is selected due to defaultValues + expect( currentSelectedItem ).toHaveTextContent( + `${ defaultValues.length } items selected` + ); + + await user.click( currentSelectedItem ); + + expect( screen.getByRole( 'listbox' ) ).toHaveAttribute( + 'aria-multiselectable' + ); + + // ensure defaultValues are selected in list of items + defaultValues.forEach( ( value ) => + expect( + screen.getByRole( 'option', { + name: value, + selected: true, + } ) + ).toBeVisible() + ); + + // name of next selection + const nextSelectionName = 'rose blush'; + + // element for next selection + const nextSelection = screen.getByRole( 'option', { + name: nextSelectionName, + } ); + + // click next selection to add another item to current selection + await user.click( nextSelection ); + + // updated array containing defaultValues + the item just selected + const updatedSelection = defaultValues.concat( nextSelectionName ); + + expect( onChangeMock ).toHaveBeenCalledWith( updatedSelection ); + + expect( nextSelection ).toHaveAttribute( 'aria-selected' ); + + // expect increased array length for current selection + expect( currentSelectedItem ).toHaveTextContent( + `${ updatedSelection.length } items selected` + ); + } ); + + it( 'Should be able to deselect items when provided an array', async () => { + const user = userEvent.setup(); + + // initial selection as defaultValue + const defaultValues = [ + 'aurora borealis green', + 'incandescent glow', + 'key lime green', + 'rose blush', + 'ultraviolet morning light', + ]; + + render( + + { defaultValues.map( ( item ) => ( + + { item } + + ) ) } + + ); + + const currentSelectedItem = screen.getByRole( 'combobox', { + expanded: false, + } ); + + await user.click( currentSelectedItem ); + + // Array containing items to deselect + const nextSelection = [ + 'aurora borealis green', + 'rose blush', + 'incandescent glow', + ]; + + // Deselect some items by clicking them to ensure that changes + // are reflected correctly + await Promise.all( + nextSelection.map( async ( value ) => { + await user.click( + screen.getByRole( 'option', { name: value } ) + ); + expect( + screen.getByRole( 'option', { + name: value, + selected: false, + } ) + ).toBeVisible(); + } ) + ); + + // expect different array length from defaultValues due to deselecting items + expect( currentSelectedItem ).toHaveTextContent( + `${ + defaultValues.length - nextSelection.length + } items selected` + ); + } ); + } ); + + it( 'Should allow rendering a custom value when using `renderSelectedValue`', async () => { + const user = userEvent.setup(); + + const renderValue = ( value: string | string[] ) => { + return {; + }; + + render( + + + { renderValue( 'april-29' ) } + + + { renderValue( 'july-9' ) } + + + ); + + const currentSelectedItem = screen.getByRole( 'combobox', { + expanded: false, + } ); + + expect( currentSelectedItem ).toBeVisible(); + + // expect that the initial selection renders an image + expect( currentSelectedItem ).toContainElement( + screen.getByRole( 'img', { name: 'april-29' } ) + ); + + expect( + screen.queryByRole( 'img', { name: 'july-9' } ) + ).not.toBeInTheDocument(); + + await user.click( currentSelectedItem ); + + // expect that the other image is only visible after opening popover with options + expect( screen.getByRole( 'img', { name: 'july-9' } ) ).toBeVisible(); + expect( + screen.getByRole( 'option', { name: 'july-9' } ) + ).toBeVisible(); + } ); +} ); From 1fb5110da6444424202bcfe4d8c611aaa9594ba7 Mon Sep 17 00:00:00 2001 From: Kai Hao Date: Tue, 6 Feb 2024 18:12:49 +0800 Subject: [PATCH 40/41] Fix nested pattern overrides and disable editing inner pattern (#58541) Co-authored-by: kevin940726 Co-authored-by: talldan Co-authored-by: glendaviesnz * Fix nested pattern overrides and disable editing inner pattern * Address code reviews --- packages/block-library/src/block/edit.js | 79 +++++++----- .../src/editor/get-blocks.ts | 19 ++- .../editor/various/pattern-overrides.spec.js | 119 ++++++++++++++++++ 3 files changed, 186 insertions(+), 31 deletions(-) diff --git a/packages/block-library/src/block/edit.js b/packages/block-library/src/block/edit.js index 1ad40055b7ee7..f8359c9889312 100644 --- a/packages/block-library/src/block/edit.js +++ b/packages/block-library/src/block/edit.js @@ -33,6 +33,7 @@ import { parse, cloneBlock } from '@wordpress/blocks'; /** * Internal dependencies */ +import { name as patternBlockName } from './index'; import { unlock } from '../lock-unlock'; const { useLayoutClasses } = unlock( blockEditorPrivateApis ); @@ -134,6 +135,7 @@ function getContentValuesFromInnerBlocks( blocks, defaultValues ) { /** @type {Record}>} */ const content = {}; for ( const block of blocks ) { + if ( block.name === patternBlockName ) continue; Object.assign( content, getContentValuesFromInnerBlocks( block.innerBlocks, defaultValues ) @@ -166,7 +168,13 @@ function setBlockEditMode( setEditMode, blocks, mode ) { mode || ( hasOverridableAttributes( block ) ? 'contentOnly' : 'disabled' ); setEditMode( block.clientId, editMode ); - setBlockEditMode( setEditMode, block.innerBlocks, mode ); + + setBlockEditMode( + setEditMode, + block.innerBlocks, + // Disable editing for nested patterns. + block.name === patternBlockName ? 'disabled' : mode + ); } ); } @@ -200,28 +208,34 @@ export default function ReusableBlockEdit( { } = useDispatch( blockEditorStore ); const { syncDerivedUpdates } = unlock( useDispatch( blockEditorStore ) ); - const { innerBlocks, userCanEdit, getBlockEditingMode, getPostLinkProps } = - useSelect( - ( select ) => { - const { canUser } = select( coreStore ); - const { - getBlocks, - getBlockEditingMode: editingMode, - getSettings, - } = select( blockEditorStore ); - const blocks = getBlocks( patternClientId ); - const canEdit = canUser( 'update', 'blocks', ref ); - - // For editing link to the site editor if the theme and user permissions support it. - return { - innerBlocks: blocks, - userCanEdit: canEdit, - getBlockEditingMode: editingMode, - getPostLinkProps: getSettings().getPostLinkProps, - }; - }, - [ patternClientId, ref ] - ); + const { + innerBlocks, + userCanEdit, + getBlockEditingMode, + getPostLinkProps, + editingMode, + } = useSelect( + ( select ) => { + const { canUser } = select( coreStore ); + const { + getBlocks, + getSettings, + getBlockEditingMode: _getBlockEditingMode, + } = select( blockEditorStore ); + const blocks = getBlocks( patternClientId ); + const canEdit = canUser( 'update', 'blocks', ref ); + + // For editing link to the site editor if the theme and user permissions support it. + return { + innerBlocks: blocks, + userCanEdit: canEdit, + getBlockEditingMode: _getBlockEditingMode, + getPostLinkProps: getSettings().getPostLinkProps, + editingMode: _getBlockEditingMode( patternClientId ), + }; + }, + [ patternClientId, ref ] + ); const editOriginalProps = getPostLinkProps ? getPostLinkProps( { @@ -230,10 +244,15 @@ export default function ReusableBlockEdit( { } ) : {}; - useEffect( - () => setBlockEditMode( setBlockEditingMode, innerBlocks ), - [ innerBlocks, setBlockEditingMode ] - ); + // Sync the editing mode of the pattern block with the inner blocks. + useEffect( () => { + setBlockEditMode( + setBlockEditingMode, + innerBlocks, + // Disable editing if the pattern itself is disabled. + editingMode === 'disabled' ? 'disabled' : undefined + ); + }, [ editingMode, innerBlocks, setBlockEditingMode ] ); const canOverrideBlocks = useMemo( () => hasOverridableBlocks( innerBlocks ), @@ -253,7 +272,8 @@ export default function ReusableBlockEdit( { // Apply the initial overrides from the pattern block to the inner blocks. useEffect( () => { defaultContent.current = {}; - const editingMode = getBlockEditingMode( patternClientId ); + const originalEditingMode = getBlockEditingMode( patternClientId ); + // Replace the contents of the blocks with the overrides. registry.batch( () => { setBlockEditingMode( patternClientId, 'default' ); syncDerivedUpdates( () => { @@ -266,7 +286,7 @@ export default function ReusableBlockEdit( { ) ); } ); - setBlockEditingMode( patternClientId, editingMode ); + setBlockEditingMode( patternClientId, originalEditingMode ); } ); }, [ __unstableMarkNextChangeAsNotPersistent, @@ -323,7 +343,6 @@ export default function ReusableBlockEdit( { }, [ syncDerivedUpdates, patternClientId, registry, setAttributes ] ); const handleEditOriginal = ( event ) => { - setBlockEditMode( setBlockEditingMode, innerBlocks, 'default' ); editOriginalProps.onClick( event ); }; diff --git a/packages/e2e-test-utils-playwright/src/editor/get-blocks.ts b/packages/e2e-test-utils-playwright/src/editor/get-blocks.ts index 72fc2c7ed44ac..36e4e23e587f9 100644 --- a/packages/e2e-test-utils-playwright/src/editor/get-blocks.ts +++ b/packages/e2e-test-utils-playwright/src/editor/get-blocks.ts @@ -29,11 +29,28 @@ export async function getBlocks( return await this.page.evaluate( ( [ _full, _clientId ] ) => { + // Serialize serializable attributes of blocks. + function serializeAttributes( + attributes: Record< string, unknown > + ) { + return Object.fromEntries( + Object.entries( attributes ).map( ( [ key, value ] ) => { + // Serialize RichTextData to string. + if ( + value instanceof window.wp.richText.RichTextData + ) { + return [ key, ( value as string ).toString() ]; + } + return [ key, value ]; + } ) + ); + } + // Remove other unpredictable properties like clientId from blocks for testing purposes. function recursivelyTransformBlocks( blocks: Block[] ): Block[] { return blocks.map( ( block ) => ( { name: block.name, - attributes: block.attributes, + attributes: serializeAttributes( block.attributes ), innerBlocks: recursivelyTransformBlocks( block.innerBlocks ), diff --git a/test/e2e/specs/editor/various/pattern-overrides.spec.js b/test/e2e/specs/editor/various/pattern-overrides.spec.js index a655439b241ea..c5cb9d2599170 100644 --- a/test/e2e/specs/editor/various/pattern-overrides.spec.js +++ b/test/e2e/specs/editor/various/pattern-overrides.spec.js @@ -374,4 +374,123 @@ test.describe( 'Pattern Overrides', () => { await expect( buttonLink ).toHaveAttribute( 'target', '' ); await expect( buttonLink ).toHaveAttribute( 'rel', /^\s*nofollow\s*$/ ); } ); + + test( 'disables editing of nested patterns', async ( { + page, + admin, + requestUtils, + editor, + } ) => { + const paragraphId = 'paragraph-id'; + const headingId = 'heading-id'; + const innerPattern = await requestUtils.createBlock( { + title: 'Inner Pattern', + content: ` +

    Inner paragraph

    +`, + status: 'publish', + } ); + const outerPattern = await requestUtils.createBlock( { + title: 'Outer Pattern', + content: ` +

    Outer heading

    + +`, + status: 'publish', + } ); + + await admin.createNewPost(); + + await editor.insertBlock( { + name: 'core/block', + attributes: { ref: outerPattern.id }, + } ); + + // Make an edit to the outer pattern heading. + await editor.canvas + .getByRole( 'document', { name: 'Block: Heading' } ) + .fill( 'Outer heading (edited)' ); + + const postId = await editor.publishPost(); + + // Check it renders correctly. + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/block', + attributes: { + ref: outerPattern.id, + content: { + [ headingId ]: { + values: { content: 'Outer heading (edited)' }, + }, + }, + }, + innerBlocks: [ + { + name: 'core/heading', + attributes: { content: 'Outer heading (edited)' }, + }, + { + name: 'core/block', + attributes: { + ref: innerPattern.id, + content: { + [ paragraphId ]: { + values: { + content: 'Inner paragraph (edited)', + }, + }, + }, + }, + innerBlocks: [ + { + name: 'core/paragraph', + attributes: { + content: 'Inner paragraph (edited)', + }, + }, + ], + }, + ], + }, + ] ); + + await expect( + editor.canvas.getByRole( 'document', { + name: 'Block: Paragraph', + includeHidden: true, + } ), + 'The inner paragraph should not be editable' + ).toHaveAttribute( 'inert', 'true' ); + + // Edit the outer pattern. + await editor.selectBlocks( + editor.canvas + .getByRole( 'document', { name: 'Block: Pattern' } ) + .first() + ); + await editor.showBlockToolbar(); + await page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'link', { name: 'Edit original' } ) + .click(); + + // The inner paragraph should be editable in the pattern focus mode. + await expect( + editor.canvas.getByRole( 'document', { + name: 'Block: Paragraph', + } ), + 'The inner paragraph should not be editable' + ).not.toHaveAttribute( 'inert', 'true' ); + + // Visit the post on the frontend. + await page.goto( `/?p=${ postId }` ); + + await expect( page.getByRole( 'heading', { level: 2 } ) ).toHaveText( + 'Outer heading (edited)' + ); + await expect( + page.getByText( 'Inner paragraph (edited)' ) + ).toBeVisible(); + } ); } ); From 8d94c3bd7af977d998466b56bd773f9b19de8d03 Mon Sep 17 00:00:00 2001 From: Joen A <1204802+jasmussen@users.noreply.github.com> Date: Tue, 6 Feb 2024 11:18:11 +0100 Subject: [PATCH 41/41] Try: Remove shadow preset overflow. (#58663) --- .../block-editor/src/components/global-styles/style.scss | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/block-editor/src/components/global-styles/style.scss b/packages/block-editor/src/components/global-styles/style.scss index 46429ea5c4776..693f1cee762be 100644 --- a/packages/block-editor/src/components/global-styles/style.scss +++ b/packages/block-editor/src/components/global-styles/style.scss @@ -23,8 +23,7 @@ // wrapper to clip the shadow beyond 6px .block-editor-global-styles-effects-panel__shadow-indicator-wrapper { - padding: 6px; - overflow: hidden; + padding: $grid-unit-15 * 0.5; display: flex; align-items: center; justify-content: center; @@ -38,8 +37,8 @@ cursor: pointer; padding: 0; - height: 24px; - width: 24px; + height: $button-size-small; + width: $button-size-small; } .block-editor-global-styles-advanced-panel__custom-css-input textarea {