diff --git a/docs/getting-started/devenv/README.md b/docs/getting-started/devenv/README.md index c891490437d431..47113c84d78dac 100644 --- a/docs/getting-started/devenv/README.md +++ b/docs/getting-started/devenv/README.md @@ -48,3 +48,10 @@ Refer to the [Get started with `wp-env`](/docs/getting-started/devenv/get-starte
Throughout the Handbook, you may also see references to wp-now. This is a lightweight tool powered by WordPress Playground that streamlines setting up a simple local WordPress environment. While still experimental, this tool is great for quickly testing WordPress releases, plugins, and themes.
+ +This list is not exhaustive, but here are several additional options to choose from if you prefer not to use `wp-env`: + +- [Local](https://localwp.com/) +- [XAMPP](https://www.apachefriends.org/) +- [MAMP](https://www.mamp.info/en/mamp/mac/) +- [Varying Vagrant Vagrants](https://varyingvagrantvagrants.org/) (VVV) diff --git a/docs/getting-started/fundamentals/README.md b/docs/getting-started/fundamentals/README.md index 799ff89aa39419..0683e55d7edf76 100644 --- a/docs/getting-started/fundamentals/README.md +++ b/docs/getting-started/fundamentals/README.md @@ -9,5 +9,6 @@ In this section, you will learn: 1. [**Registration of a block**](https://developer.wordpress.org/block-editor/getting-started/fundamentals/registration-of-a-block) - How a block is registered in both the server and the client. 1. [**Block wrapper**](https://developer.wordpress.org/block-editor/getting-started/fundamentals/block-wrapper) - How to set proper attributes to the block's markup wrapper. 1. [**The block in the Editor**](https://developer.wordpress.org/block-editor/getting-started/fundamentals/block-in-the-editor) - The block as a React component loaded in the Block Editor and its possibilities. -1. [**Markup representation of a block**](https://developer.wordpress.org/block-editor/getting-started/fundamentals/markup-representation-block) - How blocks are represented in the DB or in templates. +1. [**Markup representation of a block**](https://developer.wordpress.org/block-editor/getting-started/fundamentals/markup-representation-block) - How blocks are represented in the database, theme templates, or patterns. +1. [**Static or Dynamic rendering of a block**](https://developer.wordpress.org/block-editor/getting-started/fundamentals/static-dynamic-rendering) - How blocks can generate their output for the front end dynamically or statically. 1. [**Javascript in the Block Editor**](https://developer.wordpress.org/block-editor/getting-started/fundamentals/javascript-in-the-block-editor) - How to work with Javascript for the Block Editor. \ No newline at end of file diff --git a/docs/getting-started/fundamentals/markup-representation-block.md b/docs/getting-started/fundamentals/markup-representation-block.md index 20289f8f228ce8..506d0feb8d3d17 100644 --- a/docs/getting-started/fundamentals/markup-representation-block.md +++ b/docs/getting-started/fundamentals/markup-representation-block.md @@ -23,7 +23,7 @@ The [markup representation of a block is parsed for the Block Editor](https://de Whenever a block is saved, the `save` function, defined when the [block is registered in the client](https://developer.wordpress.org/block-editor/getting-started/fundamentals/registration-of-a-block/#registration-of-the-block-with-javascript-client-side), is called to return the markup that will be saved into the database within the block delimiter's comment. If `save` is `null` (common case for blocks with dynamic rendering), only a single line block delimiter's comment is stored, along with any attributes The Post Editor checks that the markup created by the `save` function is identical to the block's markup saved to the database: -- If there are any differences, the Post Editor trigger a **block validation error**. +- If there are any differences, the Post Editor triggers a [block validation error](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#validation). - Block validation errors usually happen when a block’s `save` function is updated to change the markup produced by the block. - A block developer can mitigate these issues by adding a [**block deprecation**](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-deprecation/) to register the change in the block. diff --git a/docs/getting-started/fundamentals/static-dynamic-rendering.md b/docs/getting-started/fundamentals/static-dynamic-rendering.md new file mode 100644 index 00000000000000..34d5432850c45e --- /dev/null +++ b/docs/getting-started/fundamentals/static-dynamic-rendering.md @@ -0,0 +1,170 @@ +# Static or Dynamic rendering of a block + +The block's markup returned on the front end can be dynamically generated on the server when the block is requested from the client (dynamic blocks) or statically generated when the block is saved in the Block Editor (static blocks). + +
+The post Static vs. dynamic blocks: What’s the difference? provides a great introduction to static and dynamic blocks. +
+ +## Static rendering + +![Blocks with static rendering diagram](https://developer.wordpress.org/files/2024/01/static-rendering.png) + +Blocks are considered "static" when they have "static rendering", this is when their output for the front end is statically generated when saved to the database, as returned by their `save` functions. + +Blocks have static rendering **when no dynamic rendering method has been defined (or is available) for the block**. In this case, the output for the front end will be taken from the [markup representation of the block in the database](https://developer.wordpress.org/block-editor/getting-started/fundamentals/markup-representation-block/) that is returned by its `save` function when the block is saved in the Block Editor. This type is block is often called a "static block". + +### How to define static rendering for a block + +The `save` function, which can be defined when [registering a block on the client](https://developer.wordpress.org/block-editor/getting-started/fundamentals/registration-of-a-block/#registration-of-the-block-with-javascript-client-side), determines the markup of the block that will be stored in the database when the content is saved and eventually returned to the front end when there's a request. This markup is stored wrapped up in [unique block delimiters](https://developer.wordpress.org/block-editor/getting-started/fundamentals/markup-representation-block/) but only the markup inside these block indicators is returned as the markup to be rendered for the block on the front end. + +To define static rendering for a block we define a `save` function for the block without any dynamic rendering method. + +
Example of static rendering of the preformatted core block +
+For example, the following save function of the preformatted core block... + +```js +import { RichText, useBlockProps } from '@wordpress/block-editor'; + +export default function save( { attributes } ) { + const { content } = attributes; + + return ( +
+			
+		
+ ); +} +``` + +...generates the following markup representation of the block when `attributes.content` has the value `"This is some preformatted text"`... + +```html + +
This is some preformatted text
+ +``` + +...and it will return the following markup for the block to the front end when there's a request. + +```html +
This is some preformatted text
+``` + +
+ +
+ +Blocks with dynamic rendering can also define a markup representation of the block (via the `save` function) which can be processed in the server before returning the markup to the front end. If no dynamic rendering method is found, any markup representation of the block in the database will be returned to the front end. + +
+The markup stored for a block can be modified before it gets rendered on the front end via hooks such as render_block or via $render_callback. +
+ +Some examples of core blocks with static rendering are: +- [`separator`](https://github.com/WordPress/gutenberg/blob/trunk/packages/block-library/src/separator) (see its [`save`](https://github.com/WordPress/gutenberg/blob/trunk/packages/block-library/src/separator/save.js) function) +- [`spacer`](https://github.com/WordPress/gutenberg/blob/trunk/packages/block-library/src/spacer) (see its [`save`](https://github.com/WordPress/gutenberg/blob/trunk/packages/block-library/src/spacer/save.js) function). +- [`button`](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/button) (see its [`save`](https://github.com/WordPress/gutenberg/blob/trunk/packages/block-library/src/button/save.js) function). + + +## Dynamic rendering + +Blocks with dynamic rendering are blocks that **build their structure and content on the fly when the block is requested from the client**. This type of block is often called a "dynamic block". + +![Blocks with dynamic rendering diagram](https://developer.wordpress.org/files/2024/01/dynamic-rendering.png) + +There are some common use cases for dynamic blocks: + +1. **Blocks where content should change even if a post has not been updated**. An example is the [`latest-posts` core block](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/latest-posts), which will update its content on request time, everywhere it is used after a new post is published. +2. **Blocks where updates to the markup should be immediately shown on the front end of the website**. For example, if you update the structure of a block by adding a new class, adding an HTML element, or changing the layout in any other way, using a dynamic block ensures those changes are applied immediately on all occurrences of that block across the site. If a dynamic block is not used then when block code is updated, Gutenberg's [validation process](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#validation) generally applies, causing users to see the validation message: "This block appears to have been modified externally". + +### How to define dynamic rendering for a block + +A block can define dynamic rendering in two main ways: +1. Via the `render_callback` argument that can be passed to the [`register_block_type()` function](https://developer.wordpress.org/block-editor/getting-started/fundamentals/registration-of-a-block/#registration-of-the-block-with-php-server-side). +1. Via a separate PHP file (usually named `render.php`) which path can be defined at the [`render` property of the `block.json`](https://developer.wordpress.org/block-editor/getting-started/fundamentals/block-json/#files-for-the-blocks-behavior-output-or-style). + +Both of these ways to define the block's dynamic rendering receive the following data: + - `$attributes` - The array of attributes for this block. + - `$content` - Rendered block output (markup of the block as stored in the database). + - `$block` - The instance of the [WP_Block](https://developer.wordpress.org/reference/classes/wp_block/) class that represents the block being rendered ([metadata of the block](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/)). + +
Example of dynamic rendering of the site-title core block +
+ +For example, the [`site-title`](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/site-title) core block with the following function registered as [`render_callback`](https://github.com/WordPress/gutenberg/blob/trunk/packages/block-library/src/site-title/index.php)... + + +```php +function render_block_core_site_title( $attributes ) { + $site_title = get_bloginfo( 'name' ); + if ( ! $site_title ) { + return; + } + + $tag_name = 'h1'; + $classes = empty( $attributes['textAlign'] ) ? '' : "has-text-align-{$attributes['textAlign']}"; + if ( isset( $attributes['style']['elements']['link']['color']['text'] ) ) { + $classes .= ' has-link-color'; + } + + if ( isset( $attributes['level'] ) ) { + $tag_name = 0 === $attributes['level'] ? 'p' : 'h' . (int) $attributes['level']; + } + + if ( $attributes['isLink'] ) { + $aria_current = is_home() || ( is_front_page() && 'page' === get_option( 'show_on_front' ) ) ? ' aria-current="page"' : ''; + $link_target = ! empty( $attributes['linkTarget'] ) ? $attributes['linkTarget'] : '_self'; + + $site_title = sprintf( + '%4$s', + esc_url( home_url() ), + esc_attr( $link_target ), + $aria_current, + esc_html( $site_title ) + ); + } + $wrapper_attributes = get_block_wrapper_attributes( array( 'class' => trim( $classes ) ) ); + + return sprintf( + '<%1$s %2$s>%3$s', + $tag_name, + $wrapper_attributes, + // already pre-escaped if it is a link. + $attributes['isLink'] ? $site_title : esc_html( $site_title ) + ); +} +``` + +... generates the following markup representation of the block in the database (as [there's no `save` function defined for this block](https://github.com/WordPress/gutenberg/blob/trunk/packages/block-library/src/site-title/index.js))... + +```html + +``` + +...and it could generate the following markup for the block to the front end when there's a request (depending on the specific values on the server at request time). + +``` +

My WordPress Website

+``` + +
+
+ +### HTML representation of dynamic blocks in the database (`save`) + +For dynamic blocks, the `save` callback function can return just `null`, which tells the editor to save only the block delimiter comment (along with any existing [block attributes](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-attributes/)) to the database. These attributes are then passed into the server-side rendering callback, which will determine how to display the block on the front end of your site. **When `save` is `null`, the Block Editor will skip the [block markup validation process](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#validation)**, avoiding issues with frequently changing markup. + +Blocks with dynamic rendering can also save an HTML representation of the block as a backup. If you provide a server-side rendering callback, the HTML representing the block in the database will be replaced with the output of your callback, but will be rendered if your block is deactivated (the plugin that registers the block is uninstalled) or your render callback is removed. + +In some cases, the block saves an HTML representation of the block and uses a dynamic rendering to fine-tune this markup if some conditions are met. Some examples of core blocks using this approach are: +- The [`cover`](https://github.com/WordPress/gutenberg/blob/trunk/packages/block-library/src/cover) block saves a [full HTML representation of the block in the database](https://github.com/WordPress/gutenberg/blob/trunk/packages/block-library/src/cover/save.js). This markup is processed via a [`render_callback`](https://github.com/WordPress/gutenberg/blob/22741661998834e69db74ad863705ee2ce97b446/packages/block-library/src/cover/index.php#L74) when requested to do some PHP magic that dynamically [injects the featured image if the "use featured image" setting is enabled](https://github.com/WordPress/gutenberg/blob/22741661998834e69db74ad863705ee2ce97b446/packages/block-library/src/cover/index.php#L16). +- The [`image`](https://github.com/WordPress/gutenberg/blob/trunk/packages/block-library/src/image) block also saves [its HTML representation in the database](https://github.com/WordPress/gutenberg/blob/trunk/packages/block-library/src/image/save.js) and processes it via a [`render_callback`](https://github.com/WordPress/gutenberg/blob/22741661998834e69db74ad863705ee2ce97b446/packages/block-library/src/image/index.php#L363) when requested to [add some attributes to the markup](https://github.com/WordPress/gutenberg/blob/22741661998834e69db74ad863705ee2ce97b446/packages/block-library/src/image/index.php#L18) if some conditions are met. + +If you are using [InnerBlocks](https://developer.wordpress.org/block-editor/how-to-guides/block-tutorial/nested-blocks-inner-blocks/) in a dynamic block, you will need to save the `InnerBlocks` in the `save` callback function using ``. + +## Additional Resources + +- [Static vs. dynamic blocks: What’s the difference?](https://developer.wordpress.org/news/2023/02/27/static-vs-dynamic-blocks-whats-the-difference/) +- [Block deprecation – a tutorial](https://developer.wordpress.org/news/2023/03/10/block-deprecation-a-tutorial/) \ No newline at end of file diff --git a/docs/getting-started/quick-start-guide.md b/docs/getting-started/quick-start-guide.md index c6f22ce219136c..736a56c006c9e1 100644 --- a/docs/getting-started/quick-start-guide.md +++ b/docs/getting-started/quick-start-guide.md @@ -41,7 +41,7 @@ When you are finished making changes, run the `npm run build` command. This opti You can use any local WordPress development environment to test your new block, but the scaffolded plugin includes configuration for `wp-env`. You must have [Docker](https://www.docker.com/products/docker-desktop) already installed and running on your machine, but if you do, run the `npx wp-env start` command. -Once the script finishes running, you can access the local environment at: `http://localhost:8888`. Log into the WordPress dashboard using username `admin` and password `password`. The plugin will already be installed and activated. Open the Editor or Site Editor, and insert the Copyright Date Block as you would any other block. +Once the script finishes running, you can access the local environment at: http://localhost:8888. Log into the WordPress dashboard using username `admin` and password `password`. The plugin will already be installed and activated. Open the Editor or Site Editor, and insert the Copyright Date Block as you would any other block. Visit the [Getting started](https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-wp-env/) guide to learn more about `wp-env`. diff --git a/docs/manifest.json b/docs/manifest.json index 5629675c0b57e3..67b8fac99f7137 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -95,6 +95,12 @@ "markdown_source": "../docs/getting-started/fundamentals/markup-representation-block.md", "parent": "fundamentals" }, + { + "title": "Static or Dynamic rendering of a block", + "slug": "static-dynamic-rendering", + "markdown_source": "../docs/getting-started/fundamentals/static-dynamic-rendering.md", + "parent": "fundamentals" + }, { "title": "Working with Javascript for the Block Editor", "slug": "javascript-in-the-block-editor", diff --git a/docs/reference-guides/data/data-core-block-editor.md b/docs/reference-guides/data/data-core-block-editor.md index 38a93552bcbef2..7b0bd386daaf48 100644 --- a/docs/reference-guides/data/data-core-block-editor.md +++ b/docs/reference-guides/data/data-core-block-editor.md @@ -588,18 +588,6 @@ _Properties_ - _isDisabled_ `boolean`: Whether or not the user should be prevented from inserting this item. - _frecency_ `number`: Heuristic that combines frequency and recency. -### getLastFocus - -Returns the element of the last element that had focus when focus left the editor canvas. - -_Parameters_ - -- _state_ `Object`: Block editor state. - -_Returns_ - -- `Object`: Element. - ### getLastMultiSelectedBlockClientId Returns the client ID of the last block in the multi-selection set, or null if there is no multi-selection. @@ -1663,18 +1651,6 @@ _Parameters_ - _clientId_ `string`: The block's clientId. - _hasControlledInnerBlocks_ `boolean`: True if the block's inner blocks are controlled. -### setLastFocus - -Action that sets the element that had focus when focus leaves the editor canvas. - -_Parameters_ - -- _lastFocus_ `Object`: The last focused element. - -_Returns_ - -- `Object`: Action object. - ### setNavigationMode Action that enables or disables the navigation mode. diff --git a/docs/toc.json b/docs/toc.json index 49110f8bed9579..849de991c78080 100644 --- a/docs/toc.json +++ b/docs/toc.json @@ -42,6 +42,9 @@ { "docs/getting-started/fundamentals/markup-representation-block.md": [] }, + { + "docs/getting-started/fundamentals/static-dynamic-rendering.md": [] + }, { "docs/getting-started/fundamentals/javascript-in-the-block-editor.md": [] } diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index af750fa0599795..aa8de83df9597b 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -1021,8 +1021,7 @@ protected static function get_blocks_metadata() { if ( ! empty( $block_type->styles ) ) { $style_selectors = array(); foreach ( $block_type->styles as $style ) { - // The style variation classname is duplicated in the selector to ensure that it overrides core block styles. - $style_selectors[ $style['name'] ] = static::append_to_selector( '.is-style-' . $style['name'] . '.is-style-' . $style['name'], static::$blocks_metadata[ $block_name ]['selector'] ); + $style_selectors[ $style['name'] ] = static::append_to_selector( '.is-style-' . $style['name'], static::$blocks_metadata[ $block_name ]['selector'] ); } static::$blocks_metadata[ $block_name ]['styleVariations'] = $style_selectors; } diff --git a/lib/experimental/blocks.php b/lib/experimental/blocks.php index 88e46b478389d2..d4bb6c9b4586eb 100644 --- a/lib/experimental/blocks.php +++ b/lib/experimental/blocks.php @@ -150,7 +150,7 @@ function gutenberg_render_block_connections( $block_content, $block, $block_inst continue; } - $custom_value = $connection_sources[ $attribute_value['source'] ]( $block_instance ); + $custom_value = $connection_sources[ $attribute_value['source'] ]( $block_instance, $attribute_name ); } else { // If the attribute does not specify the name of the custom field, skip it. if ( ! isset( $attribute_value['value'] ) ) { diff --git a/lib/experimental/connection-sources/index.php b/lib/experimental/connection-sources/index.php index bf89ba177b6e94..4f9e06cb13b945 100644 --- a/lib/experimental/connection-sources/index.php +++ b/lib/experimental/connection-sources/index.php @@ -12,8 +12,12 @@ // if it doesn't, `get_post_meta()` will just return an empty string. return get_post_meta( $block_instance->context['postId'], $meta_field, true ); }, - 'pattern_attributes' => function ( $block_instance ) { + 'pattern_attributes' => function ( $block_instance, $attribute_name ) { $block_id = $block_instance->attributes['metadata']['id']; - return _wp_array_get( $block_instance->context, array( 'pattern/overrides', $block_id ), false ); + return _wp_array_get( + $block_instance->context, + array( 'pattern/overrides', $block_id, $attribute_name ), + false + ); }, ); diff --git a/lib/experimental/fonts/font-library/class-wp-font-family.php b/lib/experimental/fonts/font-library/class-wp-font-family.php index a4204dfe1fa2c7..e47cf0afdac1de 100644 --- a/lib/experimental/fonts/font-library/class-wp-font-family.php +++ b/lib/experimental/fonts/font-library/class-wp-font-family.php @@ -599,9 +599,9 @@ private function create_or_update_font_post() { */ public function install( $files = null ) { add_filter( 'upload_mimes', array( 'WP_Font_Library', 'set_allowed_mime_types' ) ); - add_filter( 'upload_dir', array( 'WP_Font_Library', 'set_upload_dir' ) ); + add_filter( 'upload_dir', array( 'WP_Font_Library', 'fonts_dir' ) ); $were_assets_written = $this->download_or_move_font_faces( $files ); - remove_filter( 'upload_dir', array( 'WP_Font_Library', 'set_upload_dir' ) ); + remove_filter( 'upload_dir', array( 'WP_Font_Library', 'fonts_dir' ) ); remove_filter( 'upload_mimes', array( 'WP_Font_Library', 'set_allowed_mime_types' ) ); if ( ! $were_assets_written ) { diff --git a/lib/experimental/fonts/font-library/class-wp-font-library.php b/lib/experimental/fonts/font-library/class-wp-font-library.php index 9320a554e510c7..99de81e0bd74a3 100644 --- a/lib/experimental/fonts/font-library/class-wp-font-library.php +++ b/lib/experimental/fonts/font-library/class-wp-font-library.php @@ -63,15 +63,57 @@ public static function get_expected_font_mime_types_per_php_version( $php_versio */ public static function register_font_collection( $config ) { $new_collection = new WP_Font_Collection( $config ); - - if ( isset( self::$collections[ $config['id'] ] ) ) { - return new WP_Error( 'font_collection_registration_error', 'Font collection already registered.' ); + if ( self::is_collection_registered( $config['id'] ) ) { + $error_message = sprintf( + /* translators: %s: Font collection id. */ + __( 'Font collection with id: "%s" is already registered.', 'default' ), + $config['id'] + ); + _doing_it_wrong( + __METHOD__, + $error_message, + '6.5.0' + ); + return new WP_Error( 'font_collection_registration_error', $error_message ); } - self::$collections[ $config['id'] ] = $new_collection; return $new_collection; } + /** + * Unregisters a previously registered font collection. + * + * @since 6.5.0 + * + * @param string $collection_id Font collection ID. + * @return bool True if the font collection was unregistered successfully and false otherwise. + */ + public static function unregister_font_collection( $collection_id ) { + if ( ! self::is_collection_registered( $collection_id ) ) { + _doing_it_wrong( + __METHOD__, + /* translators: %s: Font collection id. */ + sprintf( __( 'Font collection "%s" not found.', 'default' ), $collection_id ), + '6.5.0' + ); + return false; + } + unset( self::$collections[ $collection_id ] ); + return true; + } + + /** + * Checks if a font collection is registered. + * + * @since 6.5.0 + * + * @param string $collection_id Font collection ID. + * @return bool True if the font collection is registered and false otherwise. + */ + private static function is_collection_registered( $collection_id ) { + return array_key_exists( $collection_id, self::$collections ); + } + /** * Gets all the font collections available. * @@ -99,40 +141,72 @@ public static function get_font_collection( $id ) { } /** - * Gets the upload directory for fonts. + * Returns an array containing the current fonts upload directory's path and URL. * * @since 6.5.0 * - * @return string Path of the upload directory for fonts. + * @param array $defaults { + * Array of information about the upload directory. + * + * @type string $path Base directory and subdirectory or full path to the fonts upload directory. + * @type string $url Base URL and subdirectory or absolute URL to the fonts upload directory. + * @type string $subdir Subdirectory + * @type string $basedir Path without subdir. + * @type string $baseurl URL path without subdir. + * @type string|false $error False or error message. + * } + * + * @return array $defaults { + * Array of information about the upload directory. + * + * @type string $path Base directory and subdirectory or full path to the fonts upload directory. + * @type string $url Base URL and subdirectory or absolute URL to the fonts upload directory. + * @type string $subdir Subdirectory + * @type string $basedir Path without subdir. + * @type string $baseurl URL path without subdir. + * @type string|false $error False or error message. + * } */ - public static function get_fonts_dir() { - return path_join( WP_CONTENT_DIR, 'fonts' ); + public static function fonts_dir( $defaults = array() ) { + $site_path = self::get_multi_site_dir(); + + // Sets the defaults. + $defaults['path'] = path_join( WP_CONTENT_DIR, 'fonts' ) . $site_path; + $defaults['url'] = untrailingslashit( content_url( 'fonts' ) ) . $site_path; + $defaults['subdir'] = ''; + $defaults['basedir'] = path_join( WP_CONTENT_DIR, 'fonts' ) . $site_path; + $defaults['baseurl'] = untrailingslashit( content_url( 'fonts' ) ) . $site_path; + $defaults['error'] = false; + + // Filters the fonts directory data. + return apply_filters( 'fonts_dir', $defaults ); } /** - * Sets the upload directory for fonts. + * Gets the Site dir for fonts, using the blog ID if multi-site, empty otherwise. * * @since 6.5.0 * - * @param array $defaults { - * Default upload directory. + * @return string Site dir path. + */ + private static function get_multi_site_dir() { + $font_sub_dir = ''; + if ( is_multisite() && ! ( is_main_network() && is_main_site() ) ) { + $font_sub_dir = '/sites/' . get_current_blog_id(); + } + return $font_sub_dir; + } + + /** + * Gets the upload directory for fonts. * - * @type string $path Path to the directory. - * @type string $url URL for the directory. - * @type string $subdir Sub-directory of the directory. - * @type string $basedir Base directory. - * @type string $baseurl Base URL. - * } - * @return array Modified upload directory. + * @since 6.5.0 + * + * @return string Path of the upload directory for fonts. */ - public static function set_upload_dir( $defaults ) { - $defaults['basedir'] = WP_CONTENT_DIR; - $defaults['baseurl'] = content_url(); - $defaults['subdir'] = '/fonts'; - $defaults['path'] = self::get_fonts_dir(); - $defaults['url'] = $defaults['baseurl'] . '/fonts'; - - return $defaults; + public static function get_fonts_dir() { + $fonts_dir_settings = self::fonts_dir(); + return $fonts_dir_settings['path']; } /** diff --git a/lib/experimental/fonts/font-library/class-wp-rest-font-families-controller.php b/lib/experimental/fonts/font-library/class-wp-rest-font-families-controller.php index c92a0d2697f315..0147d80b7bde94 100644 --- a/lib/experimental/fonts/font-library/class-wp-rest-font-families-controller.php +++ b/lib/experimental/fonts/font-library/class-wp-rest-font-families-controller.php @@ -44,8 +44,7 @@ public function register_routes() { array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), - 'permission_callback' => function () { - return true;}, + 'permission_callback' => array( $this, 'update_font_library_permissions_check' ), ), ) ); @@ -59,7 +58,7 @@ public function register_routes() { 'callback' => array( $this, 'install_fonts' ), 'permission_callback' => array( $this, 'update_font_library_permissions_check' ), 'args' => array( - 'font_families' => array( + 'font_family_settings' => array( 'required' => true, 'type' => 'string', 'validate_callback' => array( $this, 'validate_install_font_families' ), @@ -92,85 +91,61 @@ public function register_routes() { * @param array $files Files to install. * @return array $error_messages Array of error messages. */ - private function get_validation_errors( $font_families, $files ) { + private function get_validation_errors( $font_family_settings, $files ) { $error_messages = array(); - if ( ! is_array( $font_families ) ) { - $error_messages[] = __( 'font_families should be an array of font families.', 'gutenberg' ); + if ( ! is_array( $font_family_settings ) ) { + $error_messages[] = __( 'font_family_settings should be a font family definition.', 'gutenberg' ); return $error_messages; } - // Checks if there is at least one font family. - if ( count( $font_families ) < 1 ) { - $error_messages[] = __( 'font_families should have at least one font family definition.', 'gutenberg' ); + if ( + ! isset( $font_family_settings['slug'] ) || + ! isset( $font_family_settings['name'] ) || + ! isset( $font_family_settings['fontFamily'] ) + ) { + $error_messages[] = __( 'Font family should have slug, name and fontFamily properties defined.', 'gutenberg' ); + return $error_messages; } - for ( $family_index = 0; $family_index < count( $font_families ); $family_index++ ) { - $font_family = $font_families[ $family_index ]; - - if ( - ! isset( $font_family['slug'] ) || - ! isset( $font_family['name'] ) || - ! isset( $font_family['fontFamily'] ) - ) { - $error_messages[] = sprintf( - // translators: 1: font family index. - __( 'Font family [%s] should have slug, name and fontFamily properties defined.', 'gutenberg' ), - $family_index - ); + if ( isset( $font_family_settings['fontFace'] ) ) { + if ( ! is_array( $font_family_settings['fontFace'] ) ) { + $error_messages[] = __( 'Font family should have fontFace property defined as an array.', 'gutenberg' ); } - if ( isset( $font_family['fontFace'] ) ) { - if ( ! is_array( $font_family['fontFace'] ) ) { - $error_messages[] = sprintf( - // translators: 1: font family index. - __( 'Font family [%s] should have fontFace property defined as an array.', 'gutenberg' ), - $family_index - ); - continue; - } + if ( count( $font_family_settings['fontFace'] ) < 1 ) { + $error_messages[] = __( 'Font family should have at least one font face definition.', 'gutenberg' ); + } - if ( count( $font_family['fontFace'] ) < 1 ) { - $error_messages[] = sprintf( - // translators: 1: font family index. - __( 'Font family [%s] should have at least one font face definition.', 'gutenberg' ), - $family_index - ); - } + if ( ! empty( $font_family_settings['fontFace'] ) ) { + for ( $face_index = 0; $face_index < count( $font_family_settings['fontFace'] ); $face_index++ ) { - if ( ! empty( $font_family['fontFace'] ) ) { - for ( $face_index = 0; $face_index < count( $font_family['fontFace'] ); $face_index++ ) { + $font_face = $font_family_settings['fontFace'][ $face_index ]; + if ( ! isset( $font_face['fontWeight'] ) || ! isset( $font_face['fontStyle'] ) ) { + $error_messages[] = sprintf( + // translators: font face index. + __( 'Font family Font face [%1$s] should have fontWeight and fontStyle properties defined.', 'gutenberg' ), + $face_index + ); + } - $font_face = $font_family['fontFace'][ $face_index ]; - if ( ! isset( $font_face['fontWeight'] ) || ! isset( $font_face['fontStyle'] ) ) { - $error_messages[] = sprintf( - // translators: 1: font family index, 2: font face index. - __( 'Font family [%1$s] Font face [%2$s] should have fontWeight and fontStyle properties defined.', 'gutenberg' ), - $family_index, - $face_index - ); - } + if ( isset( $font_face['downloadFromUrl'] ) && isset( $font_face['uploadedFile'] ) ) { + $error_messages[] = sprintf( + // translators: font face index. + __( 'Font family Font face [%1$s] should have only one of the downloadFromUrl or uploadedFile properties defined and not both.', 'gutenberg' ), + $face_index + ); + } - if ( isset( $font_face['downloadFromUrl'] ) && isset( $font_face['uploadedFile'] ) ) { + if ( isset( $font_face['uploadedFile'] ) ) { + if ( ! isset( $files[ $font_face['uploadedFile'] ] ) ) { $error_messages[] = sprintf( - // translators: 1: font family index, 2: font face index. - __( 'Font family [%1$s] Font face [%2$s] should have only one of the downloadFromUrl or uploadedFile properties defined and not both.', 'gutenberg' ), - $family_index, + // translators: font face index. + __( 'Font family Font face [%1$s] file is not defined in the request files.', 'gutenberg' ), $face_index ); } - - if ( isset( $font_face['uploadedFile'] ) ) { - if ( ! isset( $files[ $font_face['uploadedFile'] ] ) ) { - $error_messages[] = sprintf( - // translators: 1: font family index, 2: font face index. - __( 'Font family [%1$s] Font face [%2$s] file is not defined in the request files.', 'gutenberg' ), - $family_index, - $face_index - ); - } - } } } } @@ -189,9 +164,9 @@ private function get_validation_errors( $font_families, $files ) { * @return true|WP_Error True if the parameter is valid, WP_Error otherwise. */ public function validate_install_font_families( $param, $request ) { - $font_families = json_decode( $param, true ); - $files = $request->get_file_params(); - $error_messages = $this->get_validation_errors( $font_families, $files ); + $font_family_settings = json_decode( $param, true ); + $files = $request->get_file_params(); + $error_messages = $this->get_validation_errors( $font_family_settings, $files ); if ( empty( $error_messages ) ) { return true; @@ -327,17 +302,15 @@ private function has_write_permission() { * * @since 6.5.0 * - * @param array[] $font_families Font families to install. + * @param array[] $font_family_settings Font family definition. * @return bool Whether the request needs write permissions. */ - private function needs_write_permission( $font_families ) { - foreach ( $font_families as $font ) { - if ( isset( $font['fontFace'] ) ) { - foreach ( $font['fontFace'] as $face ) { - // If the font is being downloaded from a URL or uploaded, it needs write permissions. - if ( isset( $face['downloadFromUrl'] ) || isset( $face['uploadedFile'] ) ) { - return true; - } + private function needs_write_permission( $font_family_settings ) { + if ( isset( $font_family_settings['fontFace'] ) ) { + foreach ( $font_family_settings['fontFace'] as $face ) { + // If the font is being downloaded from a URL or uploaded, it needs write permissions. + if ( isset( $face['downloadFromUrl'] ) || isset( $face['uploadedFile'] ) ) { + return true; } } } @@ -358,20 +331,20 @@ private function needs_write_permission( $font_families ) { */ public function install_fonts( $request ) { // Get new fonts to install. - $fonts_param = $request->get_param( 'font_families' ); + $font_family_settings = $request->get_param( 'font_family_settings' ); /* * As this is receiving form data, the font families are encoded as a string. * The form data is used because local fonts need to use that format to * attach the files in the request. */ - $fonts_to_install = json_decode( $fonts_param, true ); + $font_family_settings = json_decode( $font_family_settings, true ); $successes = array(); $errors = array(); $response_status = 200; - if ( empty( $fonts_to_install ) ) { + if ( empty( $font_family_settings ) ) { $errors[] = new WP_Error( 'no_fonts_to_install', __( 'No fonts to install', 'gutenberg' ) @@ -379,7 +352,7 @@ public function install_fonts( $request ) { $response_status = 400; } - if ( $this->needs_write_permission( $fonts_to_install ) ) { + if ( $this->needs_write_permission( $font_family_settings ) ) { $upload_dir = WP_Font_Library::get_fonts_dir(); if ( ! $this->has_upload_directory() ) { if ( ! wp_mkdir_p( $upload_dir ) ) { @@ -415,15 +388,13 @@ public function install_fonts( $request ) { } // Get uploaded files (used when installing local fonts). - $files = $request->get_file_params(); - foreach ( $fonts_to_install as $font_data ) { - $font = new WP_Font_Family( $font_data ); - $result = $font->install( $files ); - if ( is_wp_error( $result ) ) { - $errors[] = $result; - } else { - $successes[] = $result; - } + $files = $request->get_file_params(); + $font = new WP_Font_Family( $font_family_settings ); + $result = $font->install( $files ); + if ( is_wp_error( $result ) ) { + $errors[] = $result; + } else { + $successes[] = $result; } $data = array( diff --git a/lib/experimental/fonts/font-library/font-library.php b/lib/experimental/fonts/font-library/font-library.php index 709f63e9126cbc..711a6bb40c282b 100644 --- a/lib/experimental/fonts/font-library/font-library.php +++ b/lib/experimental/fonts/font-library/font-library.php @@ -60,6 +60,19 @@ function wp_register_font_collection( $config ) { } } +if ( ! function_exists( 'wp_unregister_font_collection' ) ) { + /** + * Unregisters a font collection from the Font Library. + * + * @since 6.5.0 + * + * @param string $collection_id The font collection ID. + */ + function wp_unregister_font_collection( $collection_id ) { + WP_Font_Library::unregister_font_collection( $collection_id ); + } + +} $default_font_collection = array( 'id' => 'default-font-collection', diff --git a/lib/experimental/interactivity-api/class-wp-directive-processor.php b/lib/experimental/interactivity-api/class-wp-directive-processor.php index bb70068aa9482b..3b8a38f973815d 100644 --- a/lib/experimental/interactivity-api/class-wp-directive-processor.php +++ b/lib/experimental/interactivity-api/class-wp-directive-processor.php @@ -35,7 +35,11 @@ class WP_Directive_Processor extends Gutenberg_HTML_Tag_Processor_6_5 { * @param array $block The block to add. */ public static function mark_root_block( $block ) { - self::$root_block = md5( serialize( $block ) ); + if ( null !== $block['blockName'] ) { + self::$root_block = $block['blockName'] . md5( serialize( $block ) ); + } else { + self::$root_block = md5( serialize( $block ) ); + } } /** @@ -52,6 +56,14 @@ public static function unmark_root_block() { * @return bool True if block is a root block, false otherwise. */ public static function is_marked_as_root_block( $block ) { + // If self::$root_block is null, is impossible that any block has been marked as root. + if ( is_null( self::$root_block ) ) { + return false; + } + // Blocks whose blockName is null are specifically intended to convey - "this is a freeform HTML block." + if ( null !== $block['blockName'] ) { + return str_contains( self::$root_block, $block['blockName'] ) && $block['blockName'] . md5( serialize( $block ) ) === self::$root_block; + } return md5( serialize( $block ) ) === self::$root_block; } @@ -256,4 +268,43 @@ public static function is_html_void_element( $tag_name ) { public static function parse_attribute_name( $name ) { return explode( '--', $name, 2 ); } + + /** + * Parse and extract the namespace and path from the given value. + * + * If the value contains a JSON instead of a path, the function parses it + * and returns the resulting array. + * + * @param string $value Passed value. + * @param string $ns Namespace fallback. + * @return array The resulting array + */ + public static function parse_attribute_value( $value, $ns = null ) { + $matches = array(); + $has_ns = preg_match( '/^([\w\-_\/]+)::(.+)$/', $value, $matches ); + + /* + * Overwrite both `$ns` and `$value` variables if `$value` explicitly + * contains a namespace. + */ + if ( $has_ns ) { + list( , $ns, $value ) = $matches; + } + + /* + * Try to decode `$value` as a JSON object. If it works, `$value` is + * replaced with the resulting array. The original string is preserved + * otherwise. + * + * Note that `json_decode` returns `null` both for an invalid JSON or + * the `'null'` string (a valid JSON). In the latter case, `$value` is + * replaced with `null`. + */ + $data = json_decode( $value, true ); + if ( null !== $data || 'null' === trim( $value ) ) { + $value = $data; + } + + return array( $ns, $value ); + } } diff --git a/lib/experimental/interactivity-api/class-wp-interactivity-initial-state.php b/lib/experimental/interactivity-api/class-wp-interactivity-initial-state.php new file mode 100644 index 00000000000000..15e57edfa4a6a2 --- /dev/null +++ b/lib/experimental/interactivity-api/class-wp-interactivity-initial-state.php @@ -0,0 +1,82 @@ +%s', + wp_json_encode( self::$initial_state, JSON_HEX_TAG | JSON_HEX_AMP ) + ); + } +} diff --git a/lib/experimental/interactivity-api/class-wp-interactivity-store.php b/lib/experimental/interactivity-api/class-wp-interactivity-store.php deleted file mode 100644 index c53701b14e8aff..00000000000000 --- a/lib/experimental/interactivity-api/class-wp-interactivity-store.php +++ /dev/null @@ -1,69 +0,0 @@ -%s', - wp_json_encode( self::$store, JSON_HEX_TAG | JSON_HEX_AMP ) - ); - } -} diff --git a/lib/experimental/interactivity-api/directive-processing.php b/lib/experimental/interactivity-api/directive-processing.php index 075d31d577634c..b49ee538390ff1 100644 --- a/lib/experimental/interactivity-api/directive-processing.php +++ b/lib/experimental/interactivity-api/directive-processing.php @@ -43,12 +43,13 @@ function gutenberg_process_directives_in_root_blocks( $block_content, $block ) { $parsed_blocks = parse_blocks( $block_content ); $context = new WP_Directive_Context(); $processed_content = ''; + $namespace_stack = array(); foreach ( $parsed_blocks as $parsed_block ) { if ( 'core/interactivity-wrapper' === $parsed_block['blockName'] ) { - $processed_content .= gutenberg_process_interactive_block( $parsed_block, $context ); + $processed_content .= gutenberg_process_interactive_block( $parsed_block, $context, $namespace_stack ); } elseif ( 'core/non-interactivity-wrapper' === $parsed_block['blockName'] ) { - $processed_content .= gutenberg_process_non_interactive_block( $parsed_block, $context ); + $processed_content .= gutenberg_process_non_interactive_block( $parsed_block, $context, $namespace_stack ); } else { $processed_content .= $parsed_block['innerHTML']; } @@ -118,10 +119,11 @@ function gutenberg_mark_block_interactivity( $block_content, $block, $block_inst * * @param array $interactive_block The interactive block to process. * @param WP_Directive_Context $context The context to use when processing. + * @param array $namespace_stack Stack of namespackes passed by reference. * * @return string The processed HTML. */ -function gutenberg_process_interactive_block( $interactive_block, $context ) { +function gutenberg_process_interactive_block( $interactive_block, $context, &$namespace_stack ) { $block_index = 0; $content = ''; $interactive_inner_blocks = array(); @@ -137,7 +139,7 @@ function gutenberg_process_interactive_block( $interactive_block, $context ) { } } - return gutenberg_process_interactive_html( $content, $context, $interactive_inner_blocks ); + return gutenberg_process_interactive_html( $content, $context, $interactive_inner_blocks, $namespace_stack ); } /** @@ -147,10 +149,11 @@ function gutenberg_process_interactive_block( $interactive_block, $context ) { * * @param array $non_interactive_block The non-interactive block to process. * @param WP_Directive_Context $context The context to use when processing. + * @param array $namespace_stack Stack of namespackes passed by reference. * * @return string The processed HTML. */ -function gutenberg_process_non_interactive_block( $non_interactive_block, $context ) { +function gutenberg_process_non_interactive_block( $non_interactive_block, $context, &$namespace_stack ) { $block_index = 0; $content = ''; foreach ( $non_interactive_block['innerContent'] as $inner_content ) { @@ -164,9 +167,9 @@ function gutenberg_process_non_interactive_block( $non_interactive_block, $conte $inner_block = $non_interactive_block['innerBlocks'][ $block_index++ ]; if ( 'core/interactivity-wrapper' === $inner_block['blockName'] ) { - $content .= gutenberg_process_interactive_block( $inner_block, $context ); + $content .= gutenberg_process_interactive_block( $inner_block, $context, $namespace_stack ); } elseif ( 'core/non-interactivity-wrapper' === $inner_block['blockName'] ) { - $content .= gutenberg_process_non_interactive_block( $inner_block, $context ); + $content .= gutenberg_process_non_interactive_block( $inner_block, $context, $namespace_stack ); } } } @@ -184,16 +187,18 @@ function gutenberg_process_non_interactive_block( $non_interactive_block, $conte * @param string $html The HTML to process. * @param mixed $context The context to use when processing. * @param array $inner_blocks The inner blocks to process. + * @param array $namespace_stack Stack of namespackes passed by reference. * * @return string The processed HTML. */ -function gutenberg_process_interactive_html( $html, $context, $inner_blocks = array() ) { +function gutenberg_process_interactive_html( $html, $context, $inner_blocks = array(), &$namespace_stack = array() ) { static $directives = array( - 'data-wp-context' => 'gutenberg_interactivity_process_wp_context', - 'data-wp-bind' => 'gutenberg_interactivity_process_wp_bind', - 'data-wp-class' => 'gutenberg_interactivity_process_wp_class', - 'data-wp-style' => 'gutenberg_interactivity_process_wp_style', - 'data-wp-text' => 'gutenberg_interactivity_process_wp_text', + 'data-wp-interactive' => 'gutenberg_interactivity_process_wp_interactive', + 'data-wp-context' => 'gutenberg_interactivity_process_wp_context', + 'data-wp-bind' => 'gutenberg_interactivity_process_wp_bind', + 'data-wp-class' => 'gutenberg_interactivity_process_wp_class', + 'data-wp-style' => 'gutenberg_interactivity_process_wp_style', + 'data-wp-text' => 'gutenberg_interactivity_process_wp_text', ); $tags = new WP_Directive_Processor( $html ); @@ -207,9 +212,9 @@ function gutenberg_process_interactive_html( $html, $context, $inner_blocks = ar // Processes the inner blocks. if ( str_contains( $tag_name, 'WP-INNER-BLOCKS' ) && ! empty( $inner_blocks ) && ! $tags->is_tag_closer() ) { if ( 'core/interactivity-wrapper' === $inner_blocks[ $inner_blocks_index ]['blockName'] ) { - $inner_processed_blocks[ strtolower( $tag_name ) ] = gutenberg_process_interactive_block( $inner_blocks[ $inner_blocks_index++ ], $context ); + $inner_processed_blocks[ strtolower( $tag_name ) ] = gutenberg_process_interactive_block( $inner_blocks[ $inner_blocks_index++ ], $context, $namespace_stack ); } elseif ( 'core/non-interactivity-wrapper' === $inner_blocks[ $inner_blocks_index ]['blockName'] ) { - $inner_processed_blocks[ strtolower( $tag_name ) ] = gutenberg_process_non_interactive_block( $inner_blocks[ $inner_blocks_index++ ], $context ); + $inner_processed_blocks[ strtolower( $tag_name ) ] = gutenberg_process_non_interactive_block( $inner_blocks[ $inner_blocks_index++ ], $context, $namespace_stack ); } } if ( $tags->is_tag_closer() ) { @@ -270,7 +275,15 @@ function gutenberg_process_interactive_html( $html, $context, $inner_blocks = ar ); foreach ( $sorted_attrs as $attribute ) { - call_user_func( $directives[ $attribute ], $tags, $context ); + call_user_func_array( + $directives[ $attribute ], + array( + $tags, + $context, + end( $namespace_stack ), + &$namespace_stack, + ) + ); } } @@ -290,17 +303,25 @@ function gutenberg_process_interactive_html( $html, $context, $inner_blocks = ar } /** - * Resolves the reference using the store and the context from the provided - * path. + * Resolves the passed reference from the store and the context under the given + * namespace. * - * @param string $path Path. + * A reference could be either a single path or a namespace followed by a path, + * separated by two colons, i.e, `namespace::path.to.prop`. If the reference + * contains a namespace, that namespace overrides the one passed as argument. + * + * @param string $reference Reference value. + * @param string $ns Inherited namespace. * @param array $context Context data. - * @return mixed + * @return mixed Resolved value. */ -function gutenberg_interactivity_evaluate_reference( $path, array $context = array() ) { - $store = array_merge( - WP_Interactivity_Store::get_data(), - array( 'context' => $context ) +function gutenberg_interactivity_evaluate_reference( $reference, $ns, array $context = array() ) { + // Extract the namespace from the reference (if present). + list( $ns, $path ) = WP_Directive_Processor::parse_attribute_value( $reference, $ns ); + + $store = array( + 'state' => WP_Interactivity_Initial_State::get_state( $ns ), + 'context' => $context[ $ns ] ?? array(), ); /* @@ -329,7 +350,12 @@ function gutenberg_interactivity_evaluate_reference( $path, array $context = arr * E.g., "file" is an string and a "callable" (the "file" function exists). */ if ( $current instanceof Closure ) { - $current = call_user_func( $current, $store ); + /* + * TODO: Figure out a way to implement derived state without having to + * pass the store as argument: + * + * $current = call_user_func( $current ); + */ } // Returns the opposite if it has a negator operator (!). diff --git a/lib/experimental/interactivity-api/directives/wp-bind.php b/lib/experimental/interactivity-api/directives/wp-bind.php index 54be4a9faeb7d2..57d2e5deb23ab4 100644 --- a/lib/experimental/interactivity-api/directives/wp-bind.php +++ b/lib/experimental/interactivity-api/directives/wp-bind.php @@ -11,8 +11,9 @@ * * @param WP_Directive_Processor $tags Tags. * @param WP_Directive_Context $context Directive context. + * @param string $ns Namespace. */ -function gutenberg_interactivity_process_wp_bind( $tags, $context ) { +function gutenberg_interactivity_process_wp_bind( $tags, $context, $ns ) { if ( $tags->is_tag_closer() ) { return; } @@ -25,8 +26,8 @@ function gutenberg_interactivity_process_wp_bind( $tags, $context ) { continue; } - $expr = $tags->get_attribute( $attr ); - $value = gutenberg_interactivity_evaluate_reference( $expr, $context->get_context() ); + $reference = $tags->get_attribute( $attr ); + $value = gutenberg_interactivity_evaluate_reference( $reference, $ns, $context->get_context() ); $tags->set_attribute( $bound_attr, $value ); } } diff --git a/lib/experimental/interactivity-api/directives/wp-class.php b/lib/experimental/interactivity-api/directives/wp-class.php index 741cc75b42c60e..ef91835be86fc1 100644 --- a/lib/experimental/interactivity-api/directives/wp-class.php +++ b/lib/experimental/interactivity-api/directives/wp-class.php @@ -11,8 +11,9 @@ * * @param WP_Directive_Processor $tags Tags. * @param WP_Directive_Context $context Directive context. + * @param string $ns Namespace. */ -function gutenberg_interactivity_process_wp_class( $tags, $context ) { +function gutenberg_interactivity_process_wp_class( $tags, $context, $ns ) { if ( $tags->is_tag_closer() ) { return; } @@ -25,8 +26,8 @@ function gutenberg_interactivity_process_wp_class( $tags, $context ) { continue; } - $expr = $tags->get_attribute( $attr ); - $add_class = gutenberg_interactivity_evaluate_reference( $expr, $context->get_context() ); + $reference = $tags->get_attribute( $attr ); + $add_class = gutenberg_interactivity_evaluate_reference( $reference, $ns, $context->get_context() ); if ( $add_class ) { $tags->add_class( $class_name ); } else { diff --git a/lib/experimental/interactivity-api/directives/wp-context.php b/lib/experimental/interactivity-api/directives/wp-context.php index 7d92b0ac7b0c67..b41b47c86c78c3 100644 --- a/lib/experimental/interactivity-api/directives/wp-context.php +++ b/lib/experimental/interactivity-api/directives/wp-context.php @@ -10,19 +10,21 @@ * * @param WP_Directive_Processor $tags Tags. * @param WP_Directive_Context $context Directive context. + * @param string $ns Namespace. */ -function gutenberg_interactivity_process_wp_context( $tags, $context ) { +function gutenberg_interactivity_process_wp_context( $tags, $context, $ns ) { if ( $tags->is_tag_closer() ) { $context->rewind_context(); return; } - $value = $tags->get_attribute( 'data-wp-context' ); + $attr_value = $tags->get_attribute( 'data-wp-context' ); - $new_context = json_decode( - is_string( $value ) && ! empty( $value ) ? $value : '{}', - true - ); + //Separate namespace and value from the context directive attribute. + list( $ns, $data ) = is_string( $attr_value ) && ! empty( $attr_value ) + ? WP_Directive_Processor::parse_attribute_value( $attr_value, $ns ) + : array( $ns, null ); - $context->set_context( $new_context ?? array() ); + // Add parsed data to the context under the corresponding namespace. + $context->set_context( array( $ns => is_array( $data ) ? $data : array() ) ); } diff --git a/lib/experimental/interactivity-api/directives/wp-interactive.php b/lib/experimental/interactivity-api/directives/wp-interactive.php new file mode 100644 index 00000000000000..9f3471a8b4e6a9 --- /dev/null +++ b/lib/experimental/interactivity-api/directives/wp-interactive.php @@ -0,0 +1,44 @@ +is_tag_closer() ) { + array_pop( $ns_stack ); + return; + } + + /* + * Decode the data-wp-interactive attribute. In the case it is not a valid + * JSON string, NULL is stored in `$island_data`. + */ + $island = $tags->get_attribute( 'data-wp-interactive' ); + $island_data = is_string( $island ) && ! empty( $island ) + ? json_decode( $island, true ) + : null; + + /* + * Push the newly defined namespace, or the current one if the island + * definition was invalid or does not contain a namespace. + * + * This is done because the function pops out the current namespace from the + * stack whenever it finds an island's closing tag, independently of whether + * the island definition was correct or it contained a valid namespace. + */ + $ns_stack[] = isset( $island_data ) && $island_data['namespace'] + ? $island_data['namespace'] + : $ns; +} diff --git a/lib/experimental/interactivity-api/directives/wp-style.php b/lib/experimental/interactivity-api/directives/wp-style.php index e5d7b269ace7cf..16432e57282606 100644 --- a/lib/experimental/interactivity-api/directives/wp-style.php +++ b/lib/experimental/interactivity-api/directives/wp-style.php @@ -11,8 +11,9 @@ * * @param WP_Directive_Processor $tags Tags. * @param WP_Directive_Context $context Directive context. + * @param string $ns Namespace. */ -function gutenberg_interactivity_process_wp_style( $tags, $context ) { +function gutenberg_interactivity_process_wp_style( $tags, $context, $ns ) { if ( $tags->is_tag_closer() ) { return; } @@ -25,8 +26,8 @@ function gutenberg_interactivity_process_wp_style( $tags, $context ) { continue; } - $expr = $tags->get_attribute( $attr ); - $style_value = gutenberg_interactivity_evaluate_reference( $expr, $context->get_context() ); + $reference = $tags->get_attribute( $attr ); + $style_value = gutenberg_interactivity_evaluate_reference( $reference, $ns, $context->get_context() ); if ( $style_value ) { $style_attr = $tags->get_attribute( 'style' ) ?? ''; $style_attr = gutenberg_interactivity_set_style( $style_attr, $style_name, $style_value ); diff --git a/lib/experimental/interactivity-api/directives/wp-text.php b/lib/experimental/interactivity-api/directives/wp-text.php index b0cfc98a74e702..c4c5bb27a31e10 100644 --- a/lib/experimental/interactivity-api/directives/wp-text.php +++ b/lib/experimental/interactivity-api/directives/wp-text.php @@ -11,8 +11,9 @@ * * @param WP_Directive_Processor $tags Tags. * @param WP_Directive_Context $context Directive context. + * @param string $ns Namespace. */ -function gutenberg_interactivity_process_wp_text( $tags, $context ) { +function gutenberg_interactivity_process_wp_text( $tags, $context, $ns ) { if ( $tags->is_tag_closer() ) { return; } @@ -22,6 +23,6 @@ function gutenberg_interactivity_process_wp_text( $tags, $context ) { return; } - $text = gutenberg_interactivity_evaluate_reference( $value, $context->get_context() ); + $text = gutenberg_interactivity_evaluate_reference( $value, $ns, $context->get_context() ); $tags->set_inner_html( esc_html( $text ) ); } diff --git a/lib/experimental/interactivity-api/initial-state.php b/lib/experimental/interactivity-api/initial-state.php new file mode 100644 index 00000000000000..a38d0da631f3c4 --- /dev/null +++ b/lib/experimental/interactivity-api/initial-state.php @@ -0,0 +1,29 @@ + gutenberg_url( '/build/modules/importmap-polyfill.min.js' ), - 'defer' => true, - ) - ); - } + $test = 'HTMLScriptElement.supports && HTMLScriptElement.supports("importmap")'; + $src = gutenberg_url( '/build/modules/importmap-polyfill.min.js' ); + + echo ( + // Test presence of feature... + '' + ); } /** @@ -273,4 +279,192 @@ function gutenberg_dequeue_module( $module_identifier ) { add_action( $modules_position, array( 'Gutenberg_Modules', 'print_module_preloads' ) ); // Prints the script that loads the import map polyfill in the footer. -add_action( 'wp_footer', array( 'Gutenberg_Modules', 'print_import_map_polyfill' ), 11 ); +add_action( 'wp_head', array( 'Gutenberg_Modules', 'print_import_map_polyfill' ), 11 ); + +/** + * Add module fields from block metadata to WP_Block_Type settings. + * + * This filter allows us to register modules from block metadata and attach additional fields to + * WP_Block_Type instances. + * + * @param array $settings Array of determined settings for registering a block type. + * @param array $metadata Metadata provided for registering a block type. + */ +function gutenberg_filter_block_type_metadata_settings_register_modules( $settings, $metadata = null ) { + $module_fields = array( + 'viewModule' => 'view_module_ids', + ); + foreach ( $module_fields as $metadata_field_name => $settings_field_name ) { + if ( ! empty( $settings[ $metadata_field_name ] ) ) { + $metadata[ $metadata_field_name ] = $settings[ $metadata_field_name ]; + } + if ( ! empty( $metadata[ $metadata_field_name ] ) ) { + $modules = $metadata[ $metadata_field_name ]; + $processed_modules = array(); + if ( is_array( $modules ) ) { + for ( $index = 0; $index < count( $modules ); $index++ ) { + $processed_modules[] = gutenberg_register_block_module_id( + $metadata, + $metadata_field_name, + $index + ); + } + } else { + $processed_modules[] = gutenberg_register_block_module_id( + $metadata, + $metadata_field_name + ); + } + $settings[ $settings_field_name ] = $processed_modules; + } + } + + return $settings; +} + +add_filter( 'block_type_metadata_settings', 'gutenberg_filter_block_type_metadata_settings_register_modules', 10, 2 ); + +/** + * Enqueue modules associated with the block. + * + * @param string $block_content The block content. + * @param array $block The full block, including name and attributes. + * @param WP_Block $instance The block instance. + */ +function gutenberg_filter_render_block_enqueue_view_modules( $block_content, $parsed_block, $block_instance ) { + $block_type = $block_instance->block_type; + + if ( ! empty( $block_type->view_module_ids ) ) { + foreach ( $block_type->view_module_ids as $module_id ) { + gutenberg_enqueue_module( $module_id ); + } + } + + return $block_content; +} + +add_filter( 'render_block', 'gutenberg_filter_render_block_enqueue_view_modules', 10, 3 ); + +/** + * Finds a module ID for the selected block metadata field. It detects + * when a path to file was provided and finds a corresponding asset file + * with details necessary to register the module under an automatically + * generated module ID. + * + * This is analogous to the `register_block_script_handle` in WordPress Core. + * + * @param array $metadata Block metadata. + * @param string $field_name Field name to pick from metadata. + * @param int $index Optional. Index of the script to register when multiple items passed. + * Default 0. + * @return string Module ID. + */ +function gutenberg_register_block_module_id( $metadata, $field_name, $index = 0 ) { + if ( empty( $metadata[ $field_name ] ) ) { + return false; + } + + $module_id = $metadata[ $field_name ]; + if ( is_array( $module_id ) ) { + if ( empty( $module_id[ $index ] ) ) { + return false; + } + $module_id = $module_id[ $index ]; + } + + $module_path = remove_block_asset_path_prefix( $module_id ); + if ( $module_id === $module_path ) { + return $module_id; + } + + $path = dirname( $metadata['file'] ); + $module_asset_raw_path = $path . '/' . substr_replace( $module_path, '.asset.php', - strlen( '.js' ) ); + $module_id = gutenberg_generate_block_asset_module_id( $metadata['name'], $field_name, $index ); + $module_asset_path = wp_normalize_path( realpath( $module_asset_raw_path ) ); + + if ( empty( $module_asset_path ) ) { + _doing_it_wrong( + __FUNCTION__, + sprintf( + // This string is from WordPress Core. See `register_block_script_handle`. + // Translators: This is a translation from WordPress Core (default). No need to translate. + __( 'The asset file (%1$s) for the "%2$s" defined in "%3$s" block definition is missing.', 'default' ), + $module_asset_raw_path, + $field_name, + $metadata['name'] + ), + '6.5.0' + ); + return false; + } + + $module_path_norm = wp_normalize_path( realpath( $path . '/' . $module_path ) ); + $module_uri = get_block_asset_url( $module_path_norm ); + $module_asset = require $module_asset_path; + $module_dependencies = isset( $module_asset['dependencies'] ) ? $module_asset['dependencies'] : array(); + + gutenberg_register_module( + $module_id, + $module_uri, + $module_dependencies, + isset( $module_asset['version'] ) ? $module_asset['version'] : false + ); + + return $module_id; +} + +/** + * Generates the module ID for an asset based on the name of the block + * and the field name provided. + * + * This is analogous to the `generate_block_asset_handle` in WordPress Core. + * + * @param string $block_name Name of the block. + * @param string $field_name Name of the metadata field. + * @param int $index Optional. Index of the asset when multiple items passed. + * Default 0. + * @return string Generated module ID for the block's field. + */ +function gutenberg_generate_block_asset_module_id( $block_name, $field_name, $index = 0 ) { + if ( str_starts_with( $block_name, 'core/' ) ) { + $asset_handle = str_replace( 'core/', 'wp-block-', $block_name ); + if ( str_starts_with( $field_name, 'editor' ) ) { + $asset_handle .= '-editor'; + } + if ( str_starts_with( $field_name, 'view' ) ) { + $asset_handle .= '-view'; + } + if ( $index > 0 ) { + $asset_handle .= '-' . ( $index + 1 ); + } + return $asset_handle; + } + + $field_mappings = array( + 'viewModule' => 'view-module', + ); + $asset_handle = str_replace( '/', '-', $block_name ) . + '-' . $field_mappings[ $field_name ]; + if ( $index > 0 ) { + $asset_handle .= '-' . ( $index + 1 ); + } + return $asset_handle; +} + +function gutenberg_register_view_module_ids_rest_field() { + register_rest_field( + 'block-type', + 'view_module_ids', + array( + 'get_callback' => function ( $item ) { + $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $item['name'] ); + if ( isset( $block_type->view_module_ids ) ) { + return $block_type->view_module_ids; + } + return array(); + }, + ) + ); +} + +add_action( 'rest_api_init', 'gutenberg_register_view_module_ids_rest_field' ); diff --git a/lib/load.php b/lib/load.php index 1aa55f7581c272..d413334227ee73 100644 --- a/lib/load.php +++ b/lib/load.php @@ -117,8 +117,8 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/experimental/disable-tinymce.php'; } -require __DIR__ . '/experimental/interactivity-api/class-wp-interactivity-store.php'; -require __DIR__ . '/experimental/interactivity-api/store.php'; +require __DIR__ . '/experimental/interactivity-api/class-wp-interactivity-initial-state.php'; +require __DIR__ . '/experimental/interactivity-api/initial-state.php'; require __DIR__ . '/experimental/interactivity-api/modules.php'; require __DIR__ . '/experimental/interactivity-api/class-wp-directive-processor.php'; require __DIR__ . '/experimental/interactivity-api/class-wp-directive-context.php'; @@ -128,6 +128,7 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/experimental/interactivity-api/directives/wp-class.php'; require __DIR__ . '/experimental/interactivity-api/directives/wp-style.php'; require __DIR__ . '/experimental/interactivity-api/directives/wp-text.php'; +require __DIR__ . '/experimental/interactivity-api/directives/wp-interactive.php'; require __DIR__ . '/experimental/modules/class-gutenberg-modules.php'; diff --git a/package-lock.json b/package-lock.json index 326c7ac7bf80c0..96a14ec8eeb50e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gutenberg", - "version": "17.4.1", + "version": "17.5.0-rc.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "gutenberg", - "version": "17.4.1", + "version": "17.5.0-rc.1", "hasInstallScript": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -86,7 +86,7 @@ "devDependencies": { "@actions/core": "1.9.1", "@actions/github": "5.0.0", - "@ariakit/test": "^0.3.5", + "@ariakit/test": "^0.3.7", "@babel/core": "7.16.0", "@babel/plugin-proposal-export-namespace-from": "7.18.9", "@babel/plugin-syntax-jsx": "7.16.0", @@ -212,7 +212,7 @@ "node-fetch": "2.6.1", "node-watch": "0.7.0", "npm-run-all": "4.1.5", - "patch-package": "6.2.2", + "patch-package": "8.0.0", "postcss": "8.4.16", "postcss-loader": "6.2.1", "prettier": "npm:wp-prettier@3.0.3", @@ -1628,13 +1628,48 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@ariakit/core": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.3.10.tgz", + "integrity": "sha512-AcN+GSoVXuUOzKx5d3xPL3YsEHevh4PIO6QIt/mg/nRX1XQ6cvxQEiAjO/BJQm+/MVl7/VbuGBoTFjr0tPU6NQ==" + }, + "node_modules/@ariakit/react": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@ariakit/react/-/react-0.3.12.tgz", + "integrity": "sha512-HxKMZZhWSkwwS/Sh9OdWyuNKQ2tjDAIQIy2KVI7IRa8ZQ6ze/4g3YLUHbfCxO7oDupXHfXaeZ4hWx8lP7l1U/g==", + "dependencies": { + "@ariakit/react-core": "0.3.12" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ariakit" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + } + }, + "node_modules/@ariakit/react-core": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@ariakit/react-core/-/react-core-0.3.12.tgz", + "integrity": "sha512-w6P1A7TYb1fKUe9QbwaoTOWofl13g7TEuXdV4JyefJCQL1e9HQdEw9UL67I8aXRo8/cFHH94/z0N37t8hw5Ogg==", + "dependencies": { + "@ariakit/core": "0.3.10", + "@floating-ui/dom": "^1.0.0", + "use-sync-external-store": "^1.2.0" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + } + }, "node_modules/@ariakit/test": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@ariakit/test/-/test-0.3.5.tgz", - "integrity": "sha512-7UCQBnJZ88JptkEnAXT7iSgtxEZiFwqdkKtxLCXDssTOJNatbFsnq0Jow324y41jGfAE2n4Lf5qY2FsZUPf9XQ==", + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/@ariakit/test/-/test-0.3.7.tgz", + "integrity": "sha512-rOa9pJA0ZfPPSI4SkDX41CsBcvxs6BmxgzFEElZWZo/uBBqtnr8ZL4oe5HySeZKEAHRH86XDqfxFISkhV76m5g==", "dev": true, "dependencies": { - "@ariakit/core": "0.3.8", + "@ariakit/core": "0.3.10", "@testing-library/dom": "^8.0.0 || ^9.0.0" }, "peerDependencies": { @@ -1650,12 +1685,6 @@ } } }, - "node_modules/@ariakit/test/node_modules/@ariakit/core": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.3.8.tgz", - "integrity": "sha512-LlSCwbyyozMX4ZEobpYGcv1LFqOdBTdTYPZw3lAVgLcFSNivsazi3NkKM9qNWNIu00MS+xTa0+RuIcuWAjlB2Q==", - "dev": true - }, "node_modules/@aw-web-design/x-default-browser": { "version": "1.4.126", "resolved": "https://registry.npmjs.org/@aw-web-design/x-default-browser/-/x-default-browser-1.4.126.tgz", @@ -19711,6 +19740,15 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "dev": true }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/atob": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", @@ -21203,13 +21241,14 @@ } }, "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", "dev": true, "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -24631,6 +24670,20 @@ "node": ">=10" } }, + "node_modules/define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/define-lazy-prop": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", @@ -28068,108 +28121,12 @@ } }, "node_modules/find-yarn-workspace-root": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-1.2.1.tgz", - "integrity": "sha512-dVtfb0WuQG+8Ag2uWkbG79hOUzEsRrhBzgfn86g2sJPkzmcpGdghbNTfUKGTxymFrY/tLIodDzLoW9nOJ4FY8Q==", - "dev": true, - "dependencies": { - "fs-extra": "^4.0.3", - "micromatch": "^3.1.4" - } - }, - "node_modules/find-yarn-workspace-root/node_modules/braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "dependencies": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/find-yarn-workspace-root/node_modules/braces/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "dev": true, - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/find-yarn-workspace-root/node_modules/fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", - "dev": true, - "dependencies": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/find-yarn-workspace-root/node_modules/fill-range/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "dev": true, - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/find-yarn-workspace-root/node_modules/fs-extra": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz", - "integrity": "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - }, - "node_modules/find-yarn-workspace-root/node_modules/micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz", + "integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==", "dev": true, "dependencies": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" + "micromatch": "^4.0.2" } }, "node_modules/flat": { @@ -28603,9 +28560,12 @@ "dev": true }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/function.prototype.name": { "version": "1.1.5", @@ -33323,12 +33283,36 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, + "node_modules/json-stable-stringify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.1.0.tgz", + "integrity": "sha512-zfA+5SuwYN2VWqN1/5HZaDzQKLJHaBVMZIIM+wuYjdptkaQsqzDdqjqf+lZZJUuJq1aanHiY8LhH8LmH+qBYJA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "isarray": "^2.0.5", + "jsonify": "^0.0.1", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "node_modules/json-stable-stringify/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, "node_modules/json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", @@ -33366,6 +33350,15 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz", + "integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/jsonparse": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", @@ -42337,85 +42330,78 @@ } }, "node_modules/patch-package": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-6.2.2.tgz", - "integrity": "sha512-YqScVYkVcClUY0v8fF0kWOjDYopzIM8e3bj/RU1DPeEF14+dCGm6UeOYm4jvCyxqIEQ5/eJzmbWfDWnUleFNMg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.0.tgz", + "integrity": "sha512-da8BVIhzjtgScwDJ2TtKsfT5JFWz1hYoBl9rUQ1f38MC2HwnEIkK8VN3dKMKcP7P7bvvgzNDbfNHtx3MsQb5vA==", "dev": true, "dependencies": { "@yarnpkg/lockfile": "^1.1.0", - "chalk": "^2.4.2", - "cross-spawn": "^6.0.5", - "find-yarn-workspace-root": "^1.2.1", - "fs-extra": "^7.0.1", - "is-ci": "^2.0.0", + "chalk": "^4.1.2", + "ci-info": "^3.7.0", + "cross-spawn": "^7.0.3", + "find-yarn-workspace-root": "^2.0.0", + "fs-extra": "^9.0.0", + "json-stable-stringify": "^1.0.2", "klaw-sync": "^6.0.0", - "minimist": "^1.2.0", + "minimist": "^1.2.6", + "open": "^7.4.2", "rimraf": "^2.6.3", - "semver": "^5.6.0", + "semver": "^7.5.3", "slash": "^2.0.0", - "tmp": "^0.0.33" + "tmp": "^0.0.33", + "yaml": "^2.2.2" }, "bin": { "patch-package": "index.js" }, "engines": { + "node": ">=14", "npm": ">5" } }, - "node_modules/patch-package/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/patch-package/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">=4" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/patch-package/node_modules/cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "dev": true, "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" }, "engines": { - "node": ">=4.8" + "node": ">= 8" } }, "node_modules/patch-package/node_modules/fs-extra": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", - "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", "dev": true, "dependencies": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" }, "engines": { - "node": ">=6 <7 || >=8" + "node": ">=10" } }, "node_modules/patch-package/node_modules/glob": { @@ -42438,6 +42424,64 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/patch-package/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/patch-package/node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/patch-package/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/patch-package/node_modules/open": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/patch-package/node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/patch-package/node_modules/rimraf": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", @@ -42450,13 +42494,25 @@ "rimraf": "bin.js" } }, - "node_modules/patch-package/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "node_modules/patch-package/node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, - "bin": { - "semver": "bin/semver" + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/patch-package/node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" } }, "node_modules/patch-package/node_modules/slash": { @@ -42468,6 +42524,51 @@ "node": ">=6" } }, + "node_modules/patch-package/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/patch-package/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/patch-package/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/patch-package/node_modules/yaml": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", + "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", + "dev": true, + "engines": { + "node": ">= 14" + } + }, "node_modules/path-browserify": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", @@ -47215,6 +47316,21 @@ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" }, + "node_modules/set-function-length": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", + "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.1", + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/set-value": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", @@ -54157,7 +54273,7 @@ "version": "25.14.0", "license": "GPL-2.0-or-later", "dependencies": { - "@ariakit/react": "^0.3.10", + "@ariakit/react": "^0.3.12", "@babel/runtime": "^7.16.0", "@emotion/cache": "^11.7.1", "@emotion/css": "^11.7.1", @@ -54216,41 +54332,6 @@ "react-dom": "^18.0.0" } }, - "packages/components/node_modules/@ariakit/core": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.3.8.tgz", - "integrity": "sha512-LlSCwbyyozMX4ZEobpYGcv1LFqOdBTdTYPZw3lAVgLcFSNivsazi3NkKM9qNWNIu00MS+xTa0+RuIcuWAjlB2Q==" - }, - "packages/components/node_modules/@ariakit/react": { - "version": "0.3.10", - "resolved": "https://registry.npmjs.org/@ariakit/react/-/react-0.3.10.tgz", - "integrity": "sha512-XRY69IOm8Oy+HSPoaspcVLAhLo3ToLhhJKSLK1voTAZtSzu5kUeUf4nUPxTzYFsvirKORZgOLAeNwuo1gPr61g==", - "dependencies": { - "@ariakit/react-core": "0.3.10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ariakit" - }, - "peerDependencies": { - "react": "^17.0.0 || ^18.0.0", - "react-dom": "^17.0.0 || ^18.0.0" - } - }, - "packages/components/node_modules/@ariakit/react-core": { - "version": "0.3.10", - "resolved": "https://registry.npmjs.org/@ariakit/react-core/-/react-core-0.3.10.tgz", - "integrity": "sha512-CzSffcNlOyS2xuy21UB6fgJXi5LriJ9JrTSJzcgJmE+P9/WfQlplJC3L75d8O2yKgaGPeFnQ0hhDA6ItsI98eQ==", - "dependencies": { - "@ariakit/core": "0.3.8", - "@floating-ui/dom": "^1.0.0", - "use-sync-external-store": "^1.2.0" - }, - "peerDependencies": { - "react": "^17.0.0 || ^18.0.0", - "react-dom": "^17.0.0 || ^18.0.0" - } - }, "packages/components/node_modules/@floating-ui/react-dom": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.1.tgz", @@ -55121,6 +55202,7 @@ "@wordpress/html-entities": "file:../html-entities", "@wordpress/i18n": "file:../i18n", "@wordpress/icons": "file:../icons", + "@wordpress/private-apis": "file:../private-apis", "@wordpress/rich-text": "file:../rich-text", "@wordpress/url": "file:../url" }, @@ -57304,22 +57386,37 @@ } } }, + "@ariakit/core": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.3.10.tgz", + "integrity": "sha512-AcN+GSoVXuUOzKx5d3xPL3YsEHevh4PIO6QIt/mg/nRX1XQ6cvxQEiAjO/BJQm+/MVl7/VbuGBoTFjr0tPU6NQ==" + }, + "@ariakit/react": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@ariakit/react/-/react-0.3.12.tgz", + "integrity": "sha512-HxKMZZhWSkwwS/Sh9OdWyuNKQ2tjDAIQIy2KVI7IRa8ZQ6ze/4g3YLUHbfCxO7oDupXHfXaeZ4hWx8lP7l1U/g==", + "requires": { + "@ariakit/react-core": "0.3.12" + } + }, + "@ariakit/react-core": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@ariakit/react-core/-/react-core-0.3.12.tgz", + "integrity": "sha512-w6P1A7TYb1fKUe9QbwaoTOWofl13g7TEuXdV4JyefJCQL1e9HQdEw9UL67I8aXRo8/cFHH94/z0N37t8hw5Ogg==", + "requires": { + "@ariakit/core": "0.3.10", + "@floating-ui/dom": "^1.0.0", + "use-sync-external-store": "^1.2.0" + } + }, "@ariakit/test": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@ariakit/test/-/test-0.3.5.tgz", - "integrity": "sha512-7UCQBnJZ88JptkEnAXT7iSgtxEZiFwqdkKtxLCXDssTOJNatbFsnq0Jow324y41jGfAE2n4Lf5qY2FsZUPf9XQ==", + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/@ariakit/test/-/test-0.3.7.tgz", + "integrity": "sha512-rOa9pJA0ZfPPSI4SkDX41CsBcvxs6BmxgzFEElZWZo/uBBqtnr8ZL4oe5HySeZKEAHRH86XDqfxFISkhV76m5g==", "dev": true, "requires": { - "@ariakit/core": "0.3.8", + "@ariakit/core": "0.3.10", "@testing-library/dom": "^8.0.0 || ^9.0.0" - }, - "dependencies": { - "@ariakit/core": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.3.8.tgz", - "integrity": "sha512-LlSCwbyyozMX4ZEobpYGcv1LFqOdBTdTYPZw3lAVgLcFSNivsazi3NkKM9qNWNIu00MS+xTa0+RuIcuWAjlB2Q==", - "dev": true - } } }, "@aw-web-design/x-default-browser": { @@ -69275,7 +69372,7 @@ "@wordpress/components": { "version": "file:packages/components", "requires": { - "@ariakit/react": "^0.3.10", + "@ariakit/react": "^0.3.12", "@babel/runtime": "^7.16.0", "@emotion/cache": "^11.7.1", "@emotion/css": "^11.7.1", @@ -69327,29 +69424,6 @@ "valtio": "1.7.0" }, "dependencies": { - "@ariakit/core": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.3.8.tgz", - "integrity": "sha512-LlSCwbyyozMX4ZEobpYGcv1LFqOdBTdTYPZw3lAVgLcFSNivsazi3NkKM9qNWNIu00MS+xTa0+RuIcuWAjlB2Q==" - }, - "@ariakit/react": { - "version": "0.3.10", - "resolved": "https://registry.npmjs.org/@ariakit/react/-/react-0.3.10.tgz", - "integrity": "sha512-XRY69IOm8Oy+HSPoaspcVLAhLo3ToLhhJKSLK1voTAZtSzu5kUeUf4nUPxTzYFsvirKORZgOLAeNwuo1gPr61g==", - "requires": { - "@ariakit/react-core": "0.3.10" - } - }, - "@ariakit/react-core": { - "version": "0.3.10", - "resolved": "https://registry.npmjs.org/@ariakit/react-core/-/react-core-0.3.10.tgz", - "integrity": "sha512-CzSffcNlOyS2xuy21UB6fgJXi5LriJ9JrTSJzcgJmE+P9/WfQlplJC3L75d8O2yKgaGPeFnQ0hhDA6ItsI98eQ==", - "requires": { - "@ariakit/core": "0.3.8", - "@floating-ui/dom": "^1.0.0", - "use-sync-external-store": "^1.2.0" - } - }, "@floating-ui/react-dom": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.1.tgz", @@ -69972,6 +70046,7 @@ "@wordpress/html-entities": "file:../html-entities", "@wordpress/i18n": "file:../i18n", "@wordpress/icons": "file:../icons", + "@wordpress/private-apis": "file:../private-apis", "@wordpress/rich-text": "file:../rich-text", "@wordpress/url": "file:../url" } @@ -71916,6 +71991,12 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "dev": true }, + "at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true + }, "atob": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", @@ -73080,13 +73161,14 @@ } }, "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", "dev": true, "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" } }, "caller-callsite": { @@ -75685,6 +75767,17 @@ "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", "dev": true }, + "define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "dev": true, + "requires": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + } + }, "define-lazy-prop": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", @@ -78348,99 +78441,12 @@ } }, "find-yarn-workspace-root": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-1.2.1.tgz", - "integrity": "sha512-dVtfb0WuQG+8Ag2uWkbG79hOUzEsRrhBzgfn86g2sJPkzmcpGdghbNTfUKGTxymFrY/tLIodDzLoW9nOJ4FY8Q==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz", + "integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==", "dev": true, "requires": { - "fs-extra": "^4.0.3", - "micromatch": "^3.1.4" - }, - "dependencies": { - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "fs-extra": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz", - "integrity": "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - } + "micromatch": "^4.0.2" } }, "flat": { @@ -78771,9 +78777,9 @@ } }, "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" }, "function.prototype.name": { "version": "1.1.5", @@ -82297,6 +82303,26 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, + "json-stable-stringify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.1.0.tgz", + "integrity": "sha512-zfA+5SuwYN2VWqN1/5HZaDzQKLJHaBVMZIIM+wuYjdptkaQsqzDdqjqf+lZZJUuJq1aanHiY8LhH8LmH+qBYJA==", + "dev": true, + "requires": { + "call-bind": "^1.0.5", + "isarray": "^2.0.5", + "jsonify": "^0.0.1", + "object-keys": "^1.1.1" + }, + "dependencies": { + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + } + } + }, "json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -82334,6 +82360,12 @@ "graceful-fs": "^4.1.6" } }, + "jsonify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz", + "integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==", + "dev": true + }, "jsonparse": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", @@ -89307,67 +89339,59 @@ "integrity": "sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==" }, "patch-package": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-6.2.2.tgz", - "integrity": "sha512-YqScVYkVcClUY0v8fF0kWOjDYopzIM8e3bj/RU1DPeEF14+dCGm6UeOYm4jvCyxqIEQ5/eJzmbWfDWnUleFNMg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.0.tgz", + "integrity": "sha512-da8BVIhzjtgScwDJ2TtKsfT5JFWz1hYoBl9rUQ1f38MC2HwnEIkK8VN3dKMKcP7P7bvvgzNDbfNHtx3MsQb5vA==", "dev": true, "requires": { "@yarnpkg/lockfile": "^1.1.0", - "chalk": "^2.4.2", - "cross-spawn": "^6.0.5", - "find-yarn-workspace-root": "^1.2.1", - "fs-extra": "^7.0.1", - "is-ci": "^2.0.0", + "chalk": "^4.1.2", + "ci-info": "^3.7.0", + "cross-spawn": "^7.0.3", + "find-yarn-workspace-root": "^2.0.0", + "fs-extra": "^9.0.0", + "json-stable-stringify": "^1.0.2", "klaw-sync": "^6.0.0", - "minimist": "^1.2.0", + "minimist": "^1.2.6", + "open": "^7.4.2", "rimraf": "^2.6.3", - "semver": "^5.6.0", + "semver": "^7.5.3", "slash": "^2.0.0", - "tmp": "^0.0.33" + "tmp": "^0.0.33", + "yaml": "^2.2.2" }, "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" } }, "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "dev": true, "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" } }, "fs-extra": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", - "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", "dev": true, "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" } }, "glob": { @@ -89384,6 +89408,47 @@ "path-is-absolute": "^1.0.0" } }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "requires": { + "is-docker": "^2.0.0" + } + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "open": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "dev": true, + "requires": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + } + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, "rimraf": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", @@ -89393,10 +89458,19 @@ "glob": "^7.1.3" } }, - "semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true }, "slash": { @@ -89404,6 +89478,36 @@ "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "yaml": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", + "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", + "dev": true } } }, @@ -92997,6 +93101,18 @@ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" }, + "set-function-length": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", + "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "dev": true, + "requires": { + "define-data-property": "^1.1.1", + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + } + }, "set-value": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", diff --git a/package.json b/package.json index 5f99726146658e..684f35d408d3c2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "17.4.1", + "version": "17.5.0-rc.1", "private": true, "description": "A new WordPress editor experience.", "author": "The WordPress Contributors", @@ -98,7 +98,7 @@ "devDependencies": { "@actions/core": "1.9.1", "@actions/github": "5.0.0", - "@ariakit/test": "^0.3.5", + "@ariakit/test": "^0.3.7", "@babel/core": "7.16.0", "@babel/plugin-proposal-export-namespace-from": "7.18.9", "@babel/plugin-syntax-jsx": "7.16.0", @@ -224,7 +224,7 @@ "node-fetch": "2.6.1", "node-watch": "0.7.0", "npm-run-all": "4.1.5", - "patch-package": "6.2.2", + "patch-package": "8.0.0", "postcss": "8.4.16", "postcss-loader": "6.2.1", "prettier": "npm:wp-prettier@3.0.3", diff --git a/packages/block-editor/src/components/global-styles/dimensions-panel.js b/packages/block-editor/src/components/global-styles/dimensions-panel.js index 47b5bd329725a7..47e50aa515e3c6 100644 --- a/packages/block-editor/src/components/global-styles/dimensions-panel.js +++ b/packages/block-editor/src/components/global-styles/dimensions-panel.js @@ -603,7 +603,7 @@ export default function DimensionsPanel( { { showMinHeightControl && ( diff --git a/packages/block-editor/src/components/height-control/README.md b/packages/block-editor/src/components/height-control/README.md index 67b52f1d56f9b2..9be1741e8cdd8e 100644 --- a/packages/block-editor/src/components/height-control/README.md +++ b/packages/block-editor/src/components/height-control/README.md @@ -43,7 +43,7 @@ A callback function that handles the application of the height value. - **Type:** `String` - **Default:** `'Height'` -A label for the height control. This is useful when using the height control for a feature that is controlled in the same way as height, but requires a different label. For example, "Min. height". +A label for the height control. This is useful when using the height control for a feature that is controlled in the same way as height, but requires a different label. For example, "Minimum height". ## Related components diff --git a/packages/block-editor/src/components/height-control/index.js b/packages/block-editor/src/components/height-control/index.js index e1797522497447..23738378b69983 100644 --- a/packages/block-editor/src/components/height-control/index.js +++ b/packages/block-editor/src/components/height-control/index.js @@ -157,6 +157,8 @@ export default function HeightControl( { onUnitChange={ handleUnitChange } min={ 0 } size={ '__unstable-large' } + label={ label } + hideLabelFromVision /> @@ -175,6 +177,8 @@ export default function HeightControl( { withInputField={ false } onChange={ handleSliderChange } __nextHasNoMarginBottom + label={ label } + hideLabelFromVision /> diff --git a/packages/block-editor/src/components/index.native.js b/packages/block-editor/src/components/index.native.js index 45451908a34472..21a9b1114ce5fa 100644 --- a/packages/block-editor/src/components/index.native.js +++ b/packages/block-editor/src/components/index.native.js @@ -45,13 +45,13 @@ export { MEDIA_TYPE_AUDIO, MEDIA_TYPE_ANY, } from './media-upload/constants'; +export { default as MediaUploadProgress } from './media-upload-progress'; export { - default as MediaUploadProgress, MEDIA_UPLOAD_STATE_UPLOADING, MEDIA_UPLOAD_STATE_SUCCEEDED, MEDIA_UPLOAD_STATE_FAILED, MEDIA_UPLOAD_STATE_RESET, -} from './media-upload-progress'; +} from './media-upload-progress/constants'; export { default as BlockMediaUpdateProgress } from './block-media-update-progress'; export { default as URLInput } from './url-input'; export { default as BlockInvalidWarning } from './block-list/block-invalid-warning'; diff --git a/packages/block-editor/src/components/media-upload-progress/constants.js b/packages/block-editor/src/components/media-upload-progress/constants.js new file mode 100644 index 00000000000000..4003cd30e44c53 --- /dev/null +++ b/packages/block-editor/src/components/media-upload-progress/constants.js @@ -0,0 +1,6 @@ +export const MEDIA_UPLOAD_STATE_IDLE = 0; +export const MEDIA_UPLOAD_STATE_UPLOADING = 1; +export const MEDIA_UPLOAD_STATE_SUCCEEDED = 2; +export const MEDIA_UPLOAD_STATE_FAILED = 3; +export const MEDIA_UPLOAD_STATE_RESET = 4; +export const MEDIA_UPLOAD_STATE_PAUSED = 11; diff --git a/packages/block-editor/src/components/media-upload-progress/index.native.js b/packages/block-editor/src/components/media-upload-progress/index.native.js index b64b08eec09d8f..cb5a25d0bb8669 100644 --- a/packages/block-editor/src/components/media-upload-progress/index.native.js +++ b/packages/block-editor/src/components/media-upload-progress/index.native.js @@ -15,23 +15,28 @@ import { subscribeMediaUpload } from '@wordpress/react-native-bridge'; * Internal dependencies */ import styles from './styles.scss'; - -export const MEDIA_UPLOAD_STATE_UPLOADING = 1; -export const MEDIA_UPLOAD_STATE_SUCCEEDED = 2; -export const MEDIA_UPLOAD_STATE_FAILED = 3; -export const MEDIA_UPLOAD_STATE_RESET = 4; +import { + MEDIA_UPLOAD_STATE_IDLE, + MEDIA_UPLOAD_STATE_UPLOADING, + MEDIA_UPLOAD_STATE_SUCCEEDED, + MEDIA_UPLOAD_STATE_FAILED, + MEDIA_UPLOAD_STATE_RESET, + MEDIA_UPLOAD_STATE_PAUSED, +} from './constants'; export class MediaUploadProgress extends Component { constructor( props ) { super( props ); this.state = { + uploadState: MEDIA_UPLOAD_STATE_IDLE, progress: 0, isUploadInProgress: false, isUploadFailed: false, }; this.mediaUpload = this.mediaUpload.bind( this ); + this.getRetryMessage = this.getRetryMessage.bind( this ); } componentDidMount() { @@ -45,7 +50,11 @@ export class MediaUploadProgress extends Component { mediaUpload( payload ) { const { mediaId } = this.props; - if ( payload.mediaId !== mediaId ) { + if ( + payload.mediaId !== mediaId || + ( payload.state === this.state.uploadState && + payload.progress === this.state.progress ) + ) { return; } @@ -56,6 +65,9 @@ export class MediaUploadProgress extends Component { case MEDIA_UPLOAD_STATE_SUCCEEDED: this.finishMediaUploadWithSuccess( payload ); break; + case MEDIA_UPLOAD_STATE_PAUSED: + this.finishMediaUploadWithPause( payload ); + break; case MEDIA_UPLOAD_STATE_FAILED: this.finishMediaUploadWithFailure( payload ); break; @@ -68,6 +80,7 @@ export class MediaUploadProgress extends Component { updateMediaProgress( payload ) { this.setState( { progress: payload.progress, + uploadState: payload.state, isUploadInProgress: true, isUploadFailed: false, } ); @@ -77,21 +90,48 @@ export class MediaUploadProgress extends Component { } finishMediaUploadWithSuccess( payload ) { - this.setState( { isUploadInProgress: false } ); + this.setState( { + uploadState: payload.state, + isUploadInProgress: false, + } ); if ( this.props.onFinishMediaUploadWithSuccess ) { this.props.onFinishMediaUploadWithSuccess( payload ); } } + finishMediaUploadWithPause( payload ) { + if ( ! this.props.enablePausedUploads ) { + this.finishMediaUploadWithFailure( payload ); + return; + } + + this.setState( { + uploadState: payload.state, + isUploadInProgress: true, + isUploadFailed: false, + } ); + if ( this.props.onFinishMediaUploadWithFailure ) { + this.props.onFinishMediaUploadWithFailure( payload ); + } + } + finishMediaUploadWithFailure( payload ) { - this.setState( { isUploadInProgress: false, isUploadFailed: true } ); + this.setState( { + uploadState: payload.state, + isUploadInProgress: false, + isUploadFailed: true, + } ); if ( this.props.onFinishMediaUploadWithFailure ) { this.props.onFinishMediaUploadWithFailure( payload ); } } mediaUploadStateReset( payload ) { - this.setState( { isUploadInProgress: false, isUploadFailed: false } ); + this.setState( { + uploadState: payload.state, + isUploadInProgress: false, + isUploadFailed: false, + } ); if ( this.props.onMediaUploadStateReset ) { this.props.onMediaUploadStateReset( payload ); } @@ -115,15 +155,24 @@ export class MediaUploadProgress extends Component { } } + getRetryMessage() { + if ( + this.state.uploadState === MEDIA_UPLOAD_STATE_PAUSED && + this.props.enablePausedUploads + ) { + return __( 'Waiting for connection' ); + } + + // eslint-disable-next-line @wordpress/i18n-no-collapsible-whitespace + return __( 'Failed to insert media.\nTap for more info.' ); + } + render() { const { renderContent = () => null } = this.props; - const { isUploadInProgress, isUploadFailed } = this.state; + const { isUploadInProgress, isUploadFailed, uploadState } = this.state; const showSpinner = this.state.isUploadInProgress; const progress = this.state.progress * 100; - // eslint-disable-next-line @wordpress/i18n-no-collapsible-whitespace - const retryMessage = __( - 'Failed to insert media.\nTap for more info.' - ); + const retryMessage = this.getRetryMessage(); const progressBarStyle = [ styles.progressBar, @@ -149,6 +198,9 @@ export class MediaUploadProgress extends Component { ) } { renderContent( { + isUploadPaused: + uploadState === MEDIA_UPLOAD_STATE_PAUSED && + this.props.enablePausedUploads, isUploadInProgress, isUploadFailed, retryMessage, diff --git a/packages/block-editor/src/components/media-upload-progress/test/index.native.js b/packages/block-editor/src/components/media-upload-progress/test/index.native.js index 1185c9c35a8682..e5a6b460d94ef5 100644 --- a/packages/block-editor/src/components/media-upload-progress/test/index.native.js +++ b/packages/block-editor/src/components/media-upload-progress/test/index.native.js @@ -14,13 +14,13 @@ import { /** * Internal dependencies */ +import { MediaUploadProgress } from '../'; import { - MediaUploadProgress, MEDIA_UPLOAD_STATE_UPLOADING, MEDIA_UPLOAD_STATE_SUCCEEDED, MEDIA_UPLOAD_STATE_FAILED, MEDIA_UPLOAD_STATE_RESET, -} from '../'; +} from '../constants'; let uploadCallBack; subscribeMediaUpload.mockImplementation( ( callback ) => { diff --git a/packages/block-editor/src/components/navigable-toolbar/index.js b/packages/block-editor/src/components/navigable-toolbar/index.js index e97efb2a4b3910..b5212497b287e3 100644 --- a/packages/block-editor/src/components/navigable-toolbar/index.js +++ b/packages/block-editor/src/components/navigable-toolbar/index.js @@ -19,6 +19,7 @@ import { ESCAPE } from '@wordpress/keycodes'; * Internal dependencies */ import { store as blockEditorStore } from '../../store'; +import { unlock } from '../../lock-unlock'; function hasOnlyToolbarItem( elements ) { const dataProp = 'toolbarItem'; @@ -169,7 +170,7 @@ function useToolbarFocus( { }; }, [ initialIndex, initialFocusOnMount, onIndexChange, toolbarRef ] ); - const { getLastFocus } = useSelect( blockEditorStore ); + const { getLastFocus } = unlock( useSelect( blockEditorStore ) ); /** * Handles returning focus to the block editor canvas when pressing escape. */ diff --git a/packages/block-editor/src/components/rich-text/native/index.native.js b/packages/block-editor/src/components/rich-text/native/index.native.js index f83d03ece47983..1f536011b35b6f 100644 --- a/packages/block-editor/src/components/rich-text/native/index.native.js +++ b/packages/block-editor/src/components/rich-text/native/index.native.js @@ -52,10 +52,6 @@ import { getFormatColors } from './get-format-colors'; import styles from './style.scss'; import ToolbarButtonWithOptions from './toolbar-button-with-options'; -const unescapeSpaces = ( text ) => { - return text.replace( / | /gi, ' ' ); -}; - // The flattened color palettes array is memoized to ensure that the same array instance is // returned for the colors palettes. This value might be used as a prop, so having the same // instance will prevent unnecessary re-renders of the RichText component. @@ -318,7 +314,7 @@ export class RichText extends Component { } const contentWithoutRootTag = this.removeRootTagsProducedByAztec( - unescapeSpaces( event.nativeEvent.text ) + event.nativeEvent.text ); // On iOS, onChange can be triggered after selection changes, even though there are no content changes. if ( contentWithoutRootTag === this.value.toString() ) { @@ -333,7 +329,7 @@ export class RichText extends Component { onTextUpdate( event ) { const contentWithoutRootTag = this.removeRootTagsProducedByAztec( - unescapeSpaces( event.nativeEvent.text ) + event.nativeEvent.text ); this.debounceCreateUndoLevel(); @@ -660,7 +656,7 @@ export class RichText extends Component { // Check and dicsard stray event, where the text and selection is equal to the ones already cached. const contentWithoutRootTag = this.removeRootTagsProducedByAztec( - unescapeSpaces( event.nativeEvent.text ) + event.nativeEvent.text ); if ( contentWithoutRootTag === this.value.toString() && diff --git a/packages/block-editor/src/components/rich-text/native/test/__snapshots__/index.native.js.snap b/packages/block-editor/src/components/rich-text/native/test/__snapshots__/index.native.js.snap index c9d3d62e40ce9d..84e9b467132714 100644 --- a/packages/block-editor/src/components/rich-text/native/test/__snapshots__/index.native.js.snap +++ b/packages/block-editor/src/components/rich-text/native/test/__snapshots__/index.native.js.snap @@ -1,12 +1,12 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[` Font Size renders component with style and font size 1`] = ` +exports[` when applying the font size renders component with style and font size 1`] = ` "

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed imperdiet ut nibh vitae ornare. Sed auctor nec augue at blandit.

" `; -exports[` Font Size should update the font size when style prop with font size property is provided 1`] = ` +exports[` when applying the font size should update the font size when style prop with font size property is provided 1`] = ` Font Size should update the font size when style prop with `; -exports[` Font Size should update the font size with decimals when style prop with font size property is provided 1`] = ` +exports[` when applying the font size should update the font size with decimals when style prop with font size property is provided 1`] = ` ', () => { } ); } ); - describe( 'when changes arrive from Aztec', () => { + describe( 'when the value changes', () => { it( 'should avoid updating attributes when values are equal', async () => { const handleChange = jest.fn(); - const defaultEmptyValue = new RichTextData(); - const screen = render( + const defaultEmptyValue = RichTextData.empty(); + render( ', () => { expect( handleChange ).not.toHaveBeenCalled(); } ); + + it( 'should preserve non-breaking space HTML entity', () => { + const onChange = jest.fn(); + const onSelectionChange = jest.fn(); + // The initial value is created using an HTML element to preserve + // the HTML entity. + const initialValue = RichTextData.fromHTMLElement( + __unstableCreateElement( document, ' ' ) + ); + render( + + ); + + // Trigger selection event with same text value as initial. + fireEvent( + screen.getByLabelText( /Text input/ ), + 'onSelectionChange', + 0, + 0, + initialValue.toString(), + { + nativeEvent: { + eventCount: 0, + target: undefined, + text: initialValue.toString(), + }, + } + ); + + expect( onChange ).not.toHaveBeenCalled(); + expect( onSelectionChange ).toHaveBeenCalled(); + } ); } ); - describe( 'Font Size', () => { + describe( 'when applying the font size', () => { it( 'should display rich text at the DEFAULT font size.', () => { // Arrange. const expectedFontSize = 16; @@ -259,7 +301,7 @@ describe( '', () => { const fontSize = '10'; const style = { fontSize: '12' }; // Act. - const screen = render( ); + render( ); screen.update( ); // Assert. expect( screen.toJSON() ).toMatchSnapshot(); @@ -281,7 +323,7 @@ describe( '', () => { const fontSize = '10'; const style = { fontSize: '12.56px' }; // Act. - const screen = render( ); + render( ); screen.update( ); // Assert. expect( screen.toJSON() ).toMatchSnapshot(); diff --git a/packages/block-editor/src/components/url-input/index.js b/packages/block-editor/src/components/url-input/index.js index 1451397ce68e5f..947c39abfd0c7d 100644 --- a/packages/block-editor/src/components/url-input/index.js +++ b/packages/block-editor/src/components/url-input/index.js @@ -66,7 +66,6 @@ class URLInput extends Component { this.state = { suggestions: [], showSuggestions: false, - isUpdatingSuggestions: false, suggestionsValue: null, selectedSuggestion: null, suggestionsListboxId: '', @@ -102,11 +101,7 @@ class URLInput extends Component { } // Update suggestions when the value changes. - if ( - prevProps.value !== value && - ! this.props.disableSuggestions && - ! this.state.isUpdatingSuggestions - ) { + if ( prevProps.value !== value && ! this.props.disableSuggestions ) { if ( value?.length ) { // If the new value is not empty we need to update with suggestions for it. this.updateSuggestions( value ); @@ -183,7 +178,6 @@ class URLInput extends Component { } this.setState( { - isUpdatingSuggestions: true, selectedSuggestion: null, loading: true, } ); @@ -203,7 +197,6 @@ class URLInput extends Component { this.setState( { suggestions, - isUpdatingSuggestions: false, suggestionsValue: value, loading: false, showSuggestions: !! suggestions.length, @@ -235,9 +228,15 @@ class URLInput extends Component { } this.setState( { - isUpdatingSuggestions: false, loading: false, } ); + } ) + .finally( () => { + // If this is the current promise then reset the reference + // to allow for checking if a new request is made. + if ( this.suggestionsRequest === request ) { + this.suggestionsRequest = null; + } } ); // Note that this assignment is handled *before* the async search request @@ -255,11 +254,12 @@ class URLInput extends Component { // When opening the link editor, if there's a value present, we want to load the suggestions pane with the results for this input search value // Don't re-run the suggestions on focus if there are already suggestions present (prevents searching again when tabbing between the input and buttons) + // or there is already a request in progress. if ( value && ! disableSuggestions && - ! this.state.isUpdatingSuggestions && - ! ( suggestions && suggestions.length ) + ! ( suggestions && suggestions.length ) && + this.suggestionsRequest === null ) { // Ensure the suggestions are updated with the current input value. this.updateSuggestions( value ); diff --git a/packages/block-editor/src/components/writing-flow/use-tab-nav.js b/packages/block-editor/src/components/writing-flow/use-tab-nav.js index b1fb1800a53ea2..bfc64dde071533 100644 --- a/packages/block-editor/src/components/writing-flow/use-tab-nav.js +++ b/packages/block-editor/src/components/writing-flow/use-tab-nav.js @@ -12,6 +12,7 @@ import { useRef } from '@wordpress/element'; */ import { store as blockEditorStore } from '../../store'; import { isInSameBlock, isInsideRootBlock } from '../../utils/dom'; +import { unlock } from '../../lock-unlock'; export default function useTabNav() { const container = useRef(); @@ -20,16 +21,15 @@ export default function useTabNav() { const { hasMultiSelection, getSelectedBlockClientId, getBlockCount } = useSelect( blockEditorStore ); - const { setNavigationMode, setLastFocus } = useDispatch( blockEditorStore ); + const { setNavigationMode, setLastFocus } = unlock( + useDispatch( blockEditorStore ) + ); const isNavigationMode = useSelect( ( select ) => select( blockEditorStore ).isNavigationMode(), [] ); - const lastFocus = useSelect( - ( select ) => select( blockEditorStore ).getLastFocus(), - [] - ); + const { getLastFocus } = unlock( useSelect( blockEditorStore ) ); // Don't allow tabbing to this element in Navigation mode. const focusCaptureTabIndex = ! isNavigationMode ? '0' : undefined; @@ -45,7 +45,7 @@ export default function useTabNav() { } else if ( hasMultiSelection() ) { container.current.focus(); } else if ( getSelectedBlockClientId() ) { - lastFocus.current.focus(); + getLastFocus()?.current.focus(); } else { setNavigationMode( true ); @@ -163,7 +163,7 @@ export default function useTabNav() { } function onFocusOut( event ) { - setLastFocus( { ...lastFocus, current: event.target } ); + setLastFocus( { ...getLastFocus(), current: event.target } ); const { ownerDocument } = node; diff --git a/packages/block-editor/src/store/actions.js b/packages/block-editor/src/store/actions.js index b21436161cb8c3..da9beb0ba73a95 100644 --- a/packages/block-editor/src/store/actions.js +++ b/packages/block-editor/src/store/actions.js @@ -1919,18 +1919,3 @@ export function unsetBlockEditingMode( clientId = '' ) { clientId, }; } - -/** - * Action that sets the element that had focus when focus leaves the editor canvas. - * - * @param {Object} lastFocus The last focused element. - * - * - * @return {Object} Action object. - */ -export function setLastFocus( lastFocus = null ) { - return { - type: 'LAST_FOCUS', - lastFocus, - }; -} diff --git a/packages/block-editor/src/store/private-actions.js b/packages/block-editor/src/store/private-actions.js index 48c5d15d469be4..43c392bc7ce8cf 100644 --- a/packages/block-editor/src/store/private-actions.js +++ b/packages/block-editor/src/store/private-actions.js @@ -323,3 +323,18 @@ export function syncDerivedUpdates( callback ) { } ); }; } + +/** + * Action that sets the element that had focus when focus leaves the editor canvas. + * + * @param {Object} lastFocus The last focused element. + * + * + * @return {Object} Action object. + */ +export function setLastFocus( lastFocus = null ) { + return { + type: 'LAST_FOCUS', + lastFocus, + }; +} diff --git a/packages/block-editor/src/store/private-selectors.js b/packages/block-editor/src/store/private-selectors.js index 98a75122f47245..d31a710fd94fe3 100644 --- a/packages/block-editor/src/store/private-selectors.js +++ b/packages/block-editor/src/store/private-selectors.js @@ -281,3 +281,14 @@ export const hasAllowedPatterns = createSelector( ), ] ); + +/** + * Returns the element of the last element that had focus when focus left the editor canvas. + * + * @param {Object} state Block editor state. + * + * @return {Object} Element. + */ +export function getLastFocus( state ) { + return state.lastFocus; +} diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 94eebd32837a53..55d157c6927a2d 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -2946,14 +2946,3 @@ export const isGroupable = createRegistrySelector( ); } ); - -/** - * Returns the element of the last element that had focus when focus left the editor canvas. - * - * @param {Object} state Block editor state. - * - * @return {Object} Element. - */ -export function getLastFocus( state ) { - return state.lastFocus; -} diff --git a/packages/block-library/src/audio/test/__snapshots__/edit.native.js.snap b/packages/block-library/src/audio/test/__snapshots__/edit.native.js.snap index dca3f782efc676..4cf28f7063ad31 100644 --- a/packages/block-library/src/audio/test/__snapshots__/edit.native.js.snap +++ b/packages/block-library/src/audio/test/__snapshots__/edit.native.js.snap @@ -532,3 +532,15 @@ exports[`Audio block renders placeholder without crashing 1`] = ` `; + +exports[`Audio block should enable autoplay setting 1`] = ` +" +
+" +`; + +exports[`Audio block should enable loop setting 1`] = ` +" +
+" +`; diff --git a/packages/block-library/src/audio/test/edit.native.js b/packages/block-library/src/audio/test/edit.native.js index c191fd2fff7989..7296d595d7aaab 100644 --- a/packages/block-library/src/audio/test/edit.native.js +++ b/packages/block-library/src/audio/test/edit.native.js @@ -5,7 +5,10 @@ import { addBlock, dismissModal, fireEvent, + getBlock, + getEditorHtml, initializeEditor, + openBlockSettings, render, screen, setupCoreBlocks, @@ -31,6 +34,10 @@ jest.unmock( '@wordpress/react-native-aztec' ); const MEDIA_UPLOAD_STATE_FAILED = 3; +const AUDIO_BLOCK = ` +
+`; + let uploadCallBack; subscribeMediaUpload.mockImplementation( ( callback ) => { uploadCallBack = callback; @@ -100,4 +107,26 @@ describe( 'Audio block', () => { screen.getByText( 'Invalid URL. Audio file not found.' ) ).toBeVisible(); } ); + + it( 'should enable autoplay setting', async () => { + await initializeEditor( { initialHtml: AUDIO_BLOCK } ); + + const audioBlock = getBlock( screen, 'Audio' ); + fireEvent.press( audioBlock ); + await openBlockSettings( screen ); + + fireEvent.press( screen.getByText( 'Autoplay' ) ); + expect( getEditorHtml() ).toMatchSnapshot(); + } ); + + it( 'should enable loop setting', async () => { + await initializeEditor( { initialHtml: AUDIO_BLOCK } ); + + const audioBlock = getBlock( screen, 'Audio' ); + fireEvent.press( audioBlock ); + await openBlockSettings( screen ); + + fireEvent.press( screen.getByText( 'Loop' ) ); + expect( getEditorHtml() ).toMatchSnapshot(); + } ); } ); diff --git a/packages/block-library/src/block/edit.js b/packages/block-library/src/block/edit.js index 607f073323d996..57db2d166f9f99 100644 --- a/packages/block-library/src/block/edit.js +++ b/packages/block-library/src/block/edit.js @@ -97,9 +97,12 @@ function applyInitialOverrides( blocks, overrides = {}, defaultValues ) { const attributes = getPartiallySyncedAttributes( block ); const newAttributes = { ...block.attributes }; for ( const attributeKey of attributes ) { - defaultValues[ blockId ] = block.attributes[ attributeKey ]; + defaultValues[ blockId ] ??= {}; + defaultValues[ blockId ][ attributeKey ] = + block.attributes[ attributeKey ]; if ( overrides[ blockId ] ) { - newAttributes[ attributeKey ] = overrides[ blockId ]; + newAttributes[ attributeKey ] = + overrides[ blockId ][ attributeKey ]; } } return { @@ -111,7 +114,7 @@ function applyInitialOverrides( blocks, overrides = {}, defaultValues ) { } function getOverridesFromBlocks( blocks, defaultValues ) { - /** @type {Record} */ + /** @type {Record>} */ const overrides = {}; for ( const block of blocks ) { Object.assign( @@ -123,9 +126,12 @@ function getOverridesFromBlocks( blocks, defaultValues ) { const attributes = getPartiallySyncedAttributes( block ); for ( const attributeKey of attributes ) { if ( - block.attributes[ attributeKey ] !== defaultValues[ blockId ] + block.attributes[ attributeKey ] !== + defaultValues[ blockId ][ attributeKey ] ) { - overrides[ blockId ] = block.attributes[ attributeKey ]; + overrides[ blockId ] ??= {}; + overrides[ blockId ][ attributeKey ] = + block.attributes[ attributeKey ]; } } } @@ -254,6 +260,7 @@ export default function ReusableBlockEdit( { } ); const innerBlocksProps = useInnerBlocksProps( blockProps, { + templateLock: 'all', layout, renderAppender: innerBlocks?.length ? undefined diff --git a/packages/block-library/src/button/style.scss b/packages/block-library/src/button/style.scss index d9efc928c5b1c7..f441152107973f 100644 --- a/packages/block-library/src/button/style.scss +++ b/packages/block-library/src/button/style.scss @@ -98,19 +98,19 @@ $blocks-block__margin: 0.5em; border-radius: 0 !important; } -.wp-block-button.is-style-outline > .wp-block-button__link, -.wp-block-button .wp-block-button__link.is-style-outline { +.wp-block-button:where(.is-style-outline) > .wp-block-button__link, +.wp-block-button .wp-block-button__link:where(.is-style-outline) { border: 2px solid currentColor; padding: 0.667em 1.333em; } -.wp-block-button.is-style-outline > .wp-block-button__link:not(.has-text-color), -.wp-block-button .wp-block-button__link.is-style-outline:not(.has-text-color) { +.wp-block-button:where(.is-style-outline) > .wp-block-button__link:not(.has-text-color), +.wp-block-button .wp-block-button__link:where(.is-style-outline):not(.has-text-color) { color: currentColor; } -.wp-block-button.is-style-outline > .wp-block-button__link:not(.has-background), -.wp-block-button .wp-block-button__link.is-style-outline:not(.has-background) { +.wp-block-button:where(.is-style-outline) > .wp-block-button__link:not(.has-background), +.wp-block-button .wp-block-button__link:where(.is-style-outline):not(.has-background) { background-color: transparent; // background-image is required to overwrite a gradient background background-image: none; diff --git a/packages/block-library/src/buttons/test/__snapshots__/edit.native.js.snap b/packages/block-library/src/buttons/test/__snapshots__/edit.native.js.snap index 1a55c807225d9d..f04eacee4b91c1 100644 --- a/packages/block-library/src/buttons/test/__snapshots__/edit.native.js.snap +++ b/packages/block-library/src/buttons/test/__snapshots__/edit.native.js.snap @@ -6,6 +6,12 @@ exports[`Buttons block color customization sets a background color 1`] = ` " `; +exports[`Buttons block color customization sets a custom gradient background color 1`] = ` +" +
+" +`; + exports[`Buttons block color customization sets a gradient background color 1`] = ` "
diff --git a/packages/block-library/src/buttons/test/edit.native.js b/packages/block-library/src/buttons/test/edit.native.js index f393a31c7330ad..af2ffe762e6a36 100644 --- a/packages/block-library/src/buttons/test/edit.native.js +++ b/packages/block-library/src/buttons/test/edit.native.js @@ -10,6 +10,7 @@ import { initializeEditor, triggerBlockListLayout, typeInRichText, + openBlockSettings, waitFor, } from 'test/helpers'; @@ -391,5 +392,53 @@ describe( 'Buttons block', () => { // Assert expect( getEditorHtml() ).toMatchSnapshot(); } ); + + it( 'sets a custom gradient background color', async () => { + // Arrange + const screen = await initializeEditor(); + await addBlock( screen, 'Buttons' ); + + // Act + const buttonsBlock = getBlock( screen, 'Buttons' ); + fireEvent.press( buttonsBlock ); + + // Trigger onLayout for the list + await triggerBlockListLayout( buttonsBlock ); + + const buttonBlock = await getBlock( screen, 'Button' ); + fireEvent.press( buttonBlock ); + + // Open Block Settings. + await openBlockSettings( screen ); + + // Open Text color settings + fireEvent.press( screen.getByLabelText( 'Background, Default' ) ); + + // Tap on the gradient segment + fireEvent.press( screen.getByLabelText( 'Gradient' ) ); + + // Tap one gradient color + fireEvent.press( + screen.getByLabelText( 'Light green cyan to vivid green cyan' ) + ); + + // Tap on Customize Gradient + fireEvent.press( screen.getByLabelText( /Customize Gradient/ ) ); + + // Change the current angle + fireEvent.press( screen.getByText( '135', { hidden: true } ) ); + const angleTextInput = screen.getByDisplayValue( '135', { + hidden: true, + } ); + fireEvent.changeText( angleTextInput, '200' ); + + // Go back to the settings list. + fireEvent.press( await screen.findByLabelText( 'Go back' ) ); + + // Assert + const customButton = await screen.findByText( 'CUSTOM' ); + expect( customButton ).toBeVisible(); + expect( getEditorHtml() ).toMatchSnapshot(); + } ); } ); } ); diff --git a/packages/block-library/src/cover/edit.native.js b/packages/block-library/src/cover/edit.native.js index 81ff43128b1a35..989c5ec3a0d332 100644 --- a/packages/block-library/src/cover/edit.native.js +++ b/packages/block-library/src/cover/edit.native.js @@ -538,6 +538,7 @@ const Cover = ( { { ( { shouldEnableBottomSheetScroll } ) => ( diff --git a/packages/block-library/src/media-text/media-container.native.js b/packages/block-library/src/media-text/media-container.native.js index dbc30dcf23e7aa..ec6e3999807c8e 100644 --- a/packages/block-library/src/media-text/media-container.native.js +++ b/packages/block-library/src/media-text/media-container.native.js @@ -170,7 +170,7 @@ class MediaContainer extends Component { mediaWidth, shouldStack, } = this.props; - const { isUploadFailed, retryMessage } = params; + const { isUploadFailed, isUploadPaused, retryMessage } = params; const focalPointValues = ! focalPoint ? IMAGE_DEFAULT_FOCAL_POINT : focalPoint; @@ -203,6 +203,7 @@ class MediaContainer extends Component { focalPoint={ imageFill && focalPointValues } isSelected={ isMediaSelected } isUploadFailed={ isUploadFailed } + isUploadPaused={ isUploadPaused } isUploadInProgress={ isUploadInProgress } onSelectMediaUploadOption={ this.onSelectMediaUploadOption @@ -340,6 +341,7 @@ class MediaContainer extends Component { { getMediaOptions() } style. - margin: 0; - .wp-block[data-align="left"] > &, .wp-block[data-align="right"] > &, .wp-block[data-align="center"] > & { diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 8805736c2e4409..a8dd57900cfae1 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -24,6 +24,7 @@ ### Enhancements - Update `ariakit` to version `0.3.10` ([#57325](https://github.com/WordPress/gutenberg/pull/57325)). +- Update `@ariakit/react` to version `0.3.12` and @ariakit/test to version `0.3.7` ([#57547](https://github.com/WordPress/gutenberg/pull/57547)). - `DropdownMenuV2`: do not collapse suffix width ([#57238](https://github.com/WordPress/gutenberg/pull/57238)). - `DateTimePicker`: Adjustment of the dot position on DayButton and expansion of the button area. ([#55502](https://github.com/WordPress/gutenberg/pull/55502)). - `Modal`: Improve application of body class names ([#55430](https://github.com/WordPress/gutenberg/pull/55430)). @@ -31,6 +32,7 @@ - `InputControl`, `NumberControl`, `UnitControl`, `SelectControl`, `TreeSelect`: Add `compact` size variant ([#57398](https://github.com/WordPress/gutenberg/pull/57398)). - `ToggleGroupControl`: Update button size in large variant to be 32px ([#57338](https://github.com/WordPress/gutenberg/pull/57338)). - `Tooltip`: improve unit tests ([#57345](https://github.com/WordPress/gutenberg/pull/57345)). +- `Tooltip`: no-op when nested inside other `Tooltip` components ([#57202](https://github.com/WordPress/gutenberg/pull/57202)). ### Experimental diff --git a/packages/components/package.json b/packages/components/package.json index 885c1e455fea40..cd440998b93230 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -30,7 +30,7 @@ ], "types": "build-types", "dependencies": { - "@ariakit/react": "^0.3.10", + "@ariakit/react": "^0.3.12", "@babel/runtime": "^7.16.0", "@emotion/cache": "^11.7.1", "@emotion/css": "^11.7.1", diff --git a/packages/components/src/alignment-matrix-control/test/index.tsx b/packages/components/src/alignment-matrix-control/test/index.tsx index 6836bc7e45f95c..a820b69b26c8ff 100644 --- a/packages/components/src/alignment-matrix-control/test/index.tsx +++ b/packages/components/src/alignment-matrix-control/test/index.tsx @@ -2,7 +2,7 @@ * External dependencies */ import { render, screen, waitFor, within } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; +import { press, click } from '@ariakit/test'; /** * Internal dependencies @@ -37,11 +37,9 @@ describe( 'AlignmentMatrixControl', () => { } ); it( 'should be centered by default', async () => { - const user = userEvent.setup(); - await renderAndInitCompositeStore( ); - await user.tab(); + await press.Tab(); expect( getCell( 'center center' ) ).toHaveFocus(); } ); @@ -60,7 +58,6 @@ describe( 'AlignmentMatrixControl', () => { 'bottom center', 'bottom right', ] )( '%s', async ( alignment ) => { - const user = userEvent.setup(); const spy = jest.fn(); await renderAndInitCompositeStore( @@ -72,14 +69,13 @@ describe( 'AlignmentMatrixControl', () => { const cell = getCell( alignment ); - await user.click( cell ); + await click( cell ); expect( cell ).toHaveFocus(); expect( spy ).toHaveBeenCalledWith( alignment ); } ); it( 'unless already focused', async () => { - const user = userEvent.setup(); const spy = jest.fn(); await renderAndInitCompositeStore( @@ -91,7 +87,7 @@ describe( 'AlignmentMatrixControl', () => { const cell = getCell( 'center center' ); - await user.click( cell ); + await click( cell ); expect( cell ).toHaveFocus(); expect( spy ).not.toHaveBeenCalled(); @@ -106,16 +102,15 @@ describe( 'AlignmentMatrixControl', () => { [ 'ArrowLeft', 'center left' ], [ 'ArrowDown', 'bottom center' ], [ 'ArrowRight', 'center right' ], - ] )( '%s', async ( keyRef, cellRef ) => { - const user = userEvent.setup(); + ] as const )( '%s', async ( keyRef, cellRef ) => { const spy = jest.fn(); await renderAndInitCompositeStore( ); - await user.tab(); - await user.keyboard( `[${ keyRef }]` ); + await press.Tab(); + await press[ keyRef ](); expect( getCell( cellRef ) ).toHaveFocus(); expect( spy ).toHaveBeenCalledWith( cellRef ); @@ -128,8 +123,7 @@ describe( 'AlignmentMatrixControl', () => { [ 'ArrowLeft', 'top left' ], [ 'ArrowDown', 'bottom right' ], [ 'ArrowRight', 'bottom right' ], - ] )( '%s', async ( keyRef, cellRef ) => { - const user = userEvent.setup(); + ] as const )( '%s', async ( keyRef, cellRef ) => { const spy = jest.fn(); await renderAndInitCompositeStore( @@ -137,8 +131,8 @@ describe( 'AlignmentMatrixControl', () => { ); const cell = getCell( cellRef ); - await user.click( cell ); - await user.keyboard( `[${ keyRef }]` ); + await click( cell ); + await press[ keyRef ](); expect( cell ).toHaveFocus(); expect( spy ).toHaveBeenCalledWith( cellRef ); diff --git a/packages/components/src/color-palette/index.native.js b/packages/components/src/color-palette/index.native.js index 51a61785df9afe..a3d4175b31ac92 100644 --- a/packages/components/src/color-palette/index.native.js +++ b/packages/components/src/color-palette/index.native.js @@ -33,7 +33,7 @@ let scrollPosition = 0; let customIndicatorWidth = 0; function ColorPalette( { - enableCustomColor = true, + enableCustomColor = false, setColor, activeColor, isGradientColor, @@ -62,24 +62,35 @@ function ColorPalette( { const scale = useRef( new Animated.Value( 1 ) ).current; const opacity = useRef( new Animated.Value( 1 ) ).current; - const defaultColors = [ + const mergedColors = [ ...new Set( ( defaultSettings.colors ?? [] ).map( ( { color } ) => color ) ), ]; - const mergedColors = [ + const mergedGradients = [ + ...new Set( + ( defaultSettings.gradients ?? [] ).map( + ( { gradient } ) => gradient + ) + ), + ]; + const allAvailableColors = [ ...new Set( ( defaultSettings.allColors ?? [] ).map( ( { color } ) => color ) ), ]; - const defaultGradientColors = [ + const allAvailableGradients = [ ...new Set( - ( defaultSettings.gradients ?? [] ).map( + ( defaultSettings.allGradients ?? [] ).map( ( { gradient } ) => gradient ) ), ]; - const colors = isGradientSegment ? defaultGradientColors : defaultColors; + + const colors = isGradientSegment ? mergedGradients : mergedColors; + const allColors = isGradientSegment + ? allAvailableGradients + : allAvailableColors; const customIndicatorColor = isGradientSegment ? activeColor @@ -110,7 +121,7 @@ function ColorPalette( { function isSelectedCustom() { const isWithinColors = - activeColor && mergedColors && mergedColors.includes( activeColor ); + activeColor && allColors?.includes( activeColor ); if ( enableCustomColor && activeColor ) { if ( isGradientSegment ) { return isGradientColor && ! isWithinColors; diff --git a/packages/components/src/index.native.js b/packages/components/src/index.native.js index 3958f359ca3079..1421316d27b400 100644 --- a/packages/components/src/index.native.js +++ b/packages/components/src/index.native.js @@ -107,7 +107,8 @@ export { default as LinkSettings } from './mobile/link-settings'; export { default as LinkSettingsScreen } from './mobile/link-settings/link-settings-screen'; export { default as LinkSettingsNavigation } from './mobile/link-settings/link-settings-navigation'; export { default as SegmentedControl } from './mobile/segmented-control'; -export { default as Image, IMAGE_DEFAULT_FOCAL_POINT } from './mobile/image'; +export { default as Image } from './mobile/image'; +export { IMAGE_DEFAULT_FOCAL_POINT } from './mobile/image/constants'; export { default as ImageEditingButton } from './mobile/image/image-editing-button'; export { setClipboard, getClipboard } from './mobile/clipboard'; export { default as AudioPlayer } from './mobile/audio-player'; diff --git a/packages/components/src/mobile/color-settings/palette.screen.native.js b/packages/components/src/mobile/color-settings/palette.screen.native.js index bc7187fd092b8c..fcf03f9ecd4483 100644 --- a/packages/components/src/mobile/color-settings/palette.screen.native.js +++ b/packages/components/src/mobile/color-settings/palette.screen.native.js @@ -29,7 +29,6 @@ import { colorsUtils } from './utils'; import styles from './style.scss'; const HIT_SLOP = { top: 8, bottom: 8, left: 8, right: 8 }; -const THEME_PALETTE_NAME = 'Theme'; const PaletteScreen = () => { const route = useRoute(); @@ -48,7 +47,6 @@ const PaletteScreen = () => { const [ currentValue, setCurrentValue ] = useState( colorValue ); const isGradientColor = isGradient( currentValue ); const selectedSegmentIndex = isGradientColor ? 1 : 0; - const allAvailableColors = useMobileGlobalStylesColors(); const [ currentSegment, setCurrentSegment ] = useState( segments[ selectedSegmentIndex ] @@ -57,6 +55,10 @@ const PaletteScreen = () => { const currentSegmentColors = ! isGradientSegment ? defaultSettings.colors : defaultSettings.gradients; + const allAvailableColors = useMobileGlobalStylesColors(); + const allAvailableGradients = currentSegmentColors + .flatMap( ( { gradients } ) => gradients ) + .filter( Boolean ); const horizontalSeparatorStyle = usePreferredColorSchemeStyle( styles.horizontalSeparator, @@ -184,10 +186,10 @@ const PaletteScreen = () => { colors: palette.colors, gradients: palette.gradients, allColors: allAvailableColors, + allGradients: allAvailableGradients, }; - const enableCustomColor = - ! isGradientSegment && - palette.name === THEME_PALETTE_NAME; + // Limit to show the custom indicator to the first available palette + const enableCustomColor = paletteKey === 0; return ( { + let icon; let iconStyle; switch ( iconType ) { case ICON_TYPE.RETRY: - return ( - - ); + icon = retryIcon || SvgIconRetry; + iconStyle = iconRetryStyles; + break; + case ICON_TYPE.OFFLINE: + icon = offline; + iconStyle = iconOfflineStyles; + break; case ICON_TYPE.PLACEHOLDER: + icon = image; iconStyle = iconPlaceholderStyles; break; case ICON_TYPE.UPLOAD: + icon = image; iconStyle = iconUploadStyles; break; } @@ -130,6 +134,31 @@ const ImageComponent = ( { styles.iconUploadDark ); + const iconOfflineStyles = usePreferredColorSchemeStyle( + styles.iconOffline, + styles.iconOfflineDark + ); + + const retryIconStyles = usePreferredColorSchemeStyle( + styles.retryIcon, + styles.retryIconDark + ); + + const iconRetryStyles = usePreferredColorSchemeStyle( + styles.iconRetry, + styles.iconRetryDark + ); + + const retryContainerStyles = usePreferredColorSchemeStyle( + styles.retryContainer, + styles.retryContainerDark + ); + + const uploadFailedTextStyles = usePreferredColorSchemeStyle( + styles.uploadFailedText, + styles.uploadFailedTextDark + ); + const placeholderStyles = [ usePreferredColorSchemeStyle( styles.imageContainerUpload, @@ -216,9 +245,11 @@ const ImageComponent = ( { > { isSelected && highlightSelected && - ! ( isUploadInProgress || isUploadFailed ) && ( - - ) } + ! ( + isUploadInProgress || + isUploadFailed || + isUploadPaused + ) && } { ! imageData ? ( @@ -239,22 +270,24 @@ const ImageComponent = ( { ) } - { isUploadFailed && retryMessage && ( + { ( isUploadFailed || isUploadPaused ) && retryMessage && ( - { getIcon( ICON_TYPE.RETRY ) } + { isUploadPaused + ? getIcon( ICON_TYPE.OFFLINE ) + : getIcon( ICON_TYPE.RETRY ) } - + { retryMessage } @@ -265,7 +298,11 @@ const ImageComponent = ( { ) } diff --git a/packages/components/src/mobile/image/style.native.scss b/packages/components/src/mobile/image/style.native.scss index f6deb3655f3699..040a8e507667e8 100644 --- a/packages/components/src/mobile/image/style.native.scss +++ b/packages/components/src/mobile/image/style.native.scss @@ -21,10 +21,23 @@ } .retryIcon { - width: 80px; - height: 80px; - justify-content: center; - align-items: center; + background-color: $black; + border-radius: 200px; + padding: 8px; +} + +.retryIconDark { + background-color: $white; +} + +.iconOffline { + fill: $white; + width: 24px; + height: 24px; +} + +.iconOfflineDark { + fill: $black; } .customRetryIcon { @@ -33,9 +46,13 @@ } .iconRetry { - fill: #fff; - width: 100%; - height: 100%; + fill: $white; + width: 24px; + height: 24px; +} + +.iconRetryDark { + fill: $black; } .iconPlaceholder { @@ -90,12 +107,17 @@ } .uploadFailedText { - color: #fff; + color: $black; + font-weight: bold; font-size: 14; margin-top: 5; text-align: center; } +.uploadFailedTextDark { + color: $white; +} + .editContainer { width: 44px; height: 44px; @@ -116,7 +138,7 @@ } .iconCustomise { - fill: #fff; + fill: $white; position: absolute; top: 7px; left: 7px; @@ -124,6 +146,10 @@ .retryContainer { flex: 1; + background-color: "rgba(255, 255, 255, 0.8)"; +} + +.retryContainerDark { background-color: "rgba(0, 0, 0, 0.5)"; } diff --git a/packages/components/src/toggle-group-control/test/index.tsx b/packages/components/src/toggle-group-control/test/index.tsx index b54b5764d4e0ff..99a2dd8a00421c 100644 --- a/packages/components/src/toggle-group-control/test/index.tsx +++ b/packages/components/src/toggle-group-control/test/index.tsx @@ -2,7 +2,7 @@ * External dependencies */ import { render, screen, waitFor } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; +import { press, click, hover, sleep } from '@ariakit/test'; /** * WordPress dependencies @@ -19,8 +19,13 @@ import { ToggleGroupControlOption, ToggleGroupControlOptionIcon, } from '../index'; +import { TOOLTIP_DELAY } from '../../tooltip'; import type { ToggleGroupControlProps } from '../types'; -import cleanupTooltip from '../../tooltip/test/utils'; + +const hoverOutside = async () => { + await hover( document.body ); + await hover( document.body, { clientX: 10, clientY: 10 } ); +}; const ControlledToggleGroupControl = ( { value: valueProp, @@ -113,7 +118,6 @@ describe.each( [ } ); } ); it( 'should call onChange with proper value', async () => { - const user = userEvent.setup(); const mockOnChange = jest.fn(); render( @@ -126,13 +130,12 @@ describe.each( [ ); - await user.click( screen.getByRole( 'radio', { name: 'R' } ) ); + await click( screen.getByRole( 'radio', { name: 'R' } ) ); expect( mockOnChange ).toHaveBeenCalledWith( 'rigas' ); } ); it( 'should render tooltip where `showTooltip` === `true`', async () => { - const user = userEvent.setup(); render( { optionsWithTooltip } @@ -143,19 +146,26 @@ describe.each( [ 'Click for Delicious Gnocchi' ); - await user.hover( firstRadio ); + await hover( firstRadio ); - const tooltip = await screen.findByText( - 'Click for Delicious Gnocchi' - ); + const tooltip = await screen.findByRole( 'tooltip', { + name: 'Click for Delicious Gnocchi', + } ); await waitFor( () => expect( tooltip ).toBeVisible() ); - await cleanupTooltip( user ); + // hover outside of radio + await hoverOutside(); + + // Tooltip should hide + expect( + screen.queryByRole( 'tooltip', { + name: 'Click for Delicious Gnocchi', + } ) + ).not.toBeInTheDocument(); } ); it( 'should not render tooltip', async () => { - const user = userEvent.setup(); render( { optionsWithTooltip } @@ -166,19 +176,24 @@ describe.each( [ 'Click for Sumptuous Caponata' ); - await user.hover( secondRadio ); + await hover( secondRadio ); - await waitFor( () => - expect( - screen.queryByText( 'Click for Sumptuous Caponata' ) - ).not.toBeInTheDocument() - ); + // Tooltip shouldn't show + expect( + screen.queryByText( 'Click for Sumptuous Caponata' ) + ).not.toBeInTheDocument(); + + // Advance time by default delay + await sleep( TOOLTIP_DELAY ); + + // Tooltip shouldn't show. + expect( + screen.queryByText( 'Click for Sumptuous Caponata' ) + ).not.toBeInTheDocument(); } ); if ( mode === 'controlled' ) { it( 'should reset values correctly', async () => { - const user = userEvent.setup(); - render( { options } @@ -188,25 +203,23 @@ describe.each( [ const rigasOption = screen.getByRole( 'radio', { name: 'R' } ); const jackOption = screen.getByRole( 'radio', { name: 'J' } ); - await user.click( rigasOption ); + await click( rigasOption ); expect( jackOption ).not.toBeChecked(); expect( rigasOption ).toBeChecked(); - await user.keyboard( '[ArrowRight]' ); + await press.ArrowRight(); expect( rigasOption ).not.toBeChecked(); expect( jackOption ).toBeChecked(); - await user.click( screen.getByRole( 'button', { name: 'Reset' } ) ); + await click( screen.getByRole( 'button', { name: 'Reset' } ) ); expect( rigasOption ).not.toBeChecked(); expect( jackOption ).not.toBeChecked(); } ); it( 'should update correctly when triggered by external updates', async () => { - const user = userEvent.setup(); - render( { it( 'should not be deselectable', async () => { const mockOnChange = jest.fn(); - const user = userEvent.setup(); render( { - const user = userEvent.setup(); - render( - - { options } - + <> + + { options } + + + ); const rigas = screen.getByRole( 'radio', { name: 'R', } ); - await user.tab(); + await press.Tab(); expect( rigas ).toHaveFocus(); - await user.tab(); + await press.Tab(); + // When in controlled mode, there is an additional "Reset" button. const expectedFocusTarget = mode === 'uncontrolled' - ? rigas.ownerDocument.body + ? screen.getByRole( 'button', { + name: 'After ToggleGroupControl', + } ) : screen.getByRole( 'button', { name: 'Reset' } ); expect( expectedFocusTarget ).toHaveFocus(); @@ -301,7 +317,6 @@ describe.each( [ describe( 'isDeselectable = true', () => { it( 'should be deselectable', async () => { const mockOnChange = jest.fn(); - const user = userEvent.setup(); render( ); - await user.click( + await click( screen.getByRole( 'button', { name: 'R', pressed: true, @@ -323,7 +338,7 @@ describe.each( [ expect( mockOnChange ).toHaveBeenCalledTimes( 1 ); expect( mockOnChange ).toHaveBeenLastCalledWith( undefined ); - await user.click( + await click( screen.getByRole( 'button', { name: 'R', pressed: false, @@ -334,15 +349,13 @@ describe.each( [ } ); it( 'should tab to the next option button', async () => { - const user = userEvent.setup(); - render( { options } ); - await user.tab(); + await press.Tab(); expect( screen.getByRole( 'button', { name: 'R', @@ -350,7 +363,7 @@ describe.each( [ } ) ).toHaveFocus(); - await user.tab(); + await press.Tab(); expect( screen.getByRole( 'button', { name: 'J', @@ -359,7 +372,7 @@ describe.each( [ ).toHaveFocus(); // Focus should not move with arrow keys - await user.keyboard( '{ArrowLeft}' ); + await press.ArrowLeft(); expect( screen.getByRole( 'button', { name: 'J', diff --git a/packages/components/src/tooltip/README.md b/packages/components/src/tooltip/README.md index 9b214e8fc6b00e..ef2cd35d25543e 100644 --- a/packages/components/src/tooltip/README.md +++ b/packages/components/src/tooltip/README.md @@ -16,6 +16,10 @@ const MyTooltip = () => ( ); ``` +### Nested tooltips + +In case one or more `Tooltip` components are rendered inside another `Tooltip` component, only the tooltip associated to the outermost `Tooltip` component will be rendered in the browser and shown to the user appropriately. The rest of the nested `Tooltip` components will simply no-op and pass-through their anchor. + ## Props The component accepts the following props: diff --git a/packages/components/src/tooltip/index.tsx b/packages/components/src/tooltip/index.tsx index 817d6d18812ee4..1e652d9a42dbb4 100644 --- a/packages/components/src/tooltip/index.tsx +++ b/packages/components/src/tooltip/index.tsx @@ -8,22 +8,37 @@ import * as Ariakit from '@ariakit/react'; * WordPress dependencies */ import { useInstanceId } from '@wordpress/compose'; -import { Children } from '@wordpress/element'; +import { Children, cloneElement } from '@wordpress/element'; import deprecated from '@wordpress/deprecated'; /** * Internal dependencies */ -import type { TooltipProps } from './types'; +import type { TooltipProps, TooltipInternalContext } from './types'; import Shortcut from '../shortcut'; import { positionToPlacement } from '../popover/utils'; +import { + contextConnect, + useContextSystem, + ContextSystemProvider, +} from '../context'; +import type { WordPressComponentProps } from '../context'; /** * Time over anchor to wait before showing tooltip */ export const TOOLTIP_DELAY = 700; -function Tooltip( props: TooltipProps ) { +const CONTEXT_VALUE = { + Tooltip: { + isNestedInTooltip: true, + }, +}; + +function UnconnectedTooltip( + props: WordPressComponentProps< TooltipProps, 'div', false >, + ref: React.ForwardedRef< any > +) { const { children, delay = TOOLTIP_DELAY, @@ -32,7 +47,15 @@ function Tooltip( props: TooltipProps ) { position, shortcut, text, - } = props; + + // From Internal Context system + isNestedInTooltip, + + ...restProps + } = useContextSystem< typeof props & TooltipInternalContext >( + props, + 'Tooltip' + ); const baseId = useInstanceId( Tooltip, 'tooltip' ); const describedById = text || shortcut ? baseId : undefined; @@ -43,7 +66,7 @@ function Tooltip( props: TooltipProps ) { if ( 'development' === process.env.NODE_ENV ) { // eslint-disable-next-line no-console console.error( - 'Tooltip should be called with only a single child element.' + 'wp-components.Tooltip should be called with only a single child element.' ); } } @@ -64,24 +87,37 @@ function Tooltip( props: TooltipProps ) { } computedPlacement = computedPlacement || 'bottom'; - const tooltipStore = Ariakit.useTooltipStore( { + // Removing the `Ariakit` namespace from the hook name allows ESLint to + // properly identify the hook, and apply the correct linting rules. + const useAriakitTooltipStore = Ariakit.useTooltipStore; + const tooltipStore = useAriakitTooltipStore( { placement: computedPlacement, showTimeout: delay, } ); + if ( isNestedInTooltip ) { + return isOnlyChild + ? cloneElement( children, { + ...restProps, + ref, + } ) + : children; + } + return ( - <> + { isOnlyChild ? undefined : children } { isOnlyChild && ( text || shortcut ) && ( ) } - + ); } +export const Tooltip = contextConnect( UnconnectedTooltip, 'Tooltip' ); + export default Tooltip; diff --git a/packages/components/src/tooltip/stories/index.story.tsx b/packages/components/src/tooltip/stories/index.story.tsx index 760f3dcc23e2fd..b006bc03aced96 100644 --- a/packages/components/src/tooltip/stories/index.story.tsx +++ b/packages/components/src/tooltip/stories/index.story.tsx @@ -30,7 +30,7 @@ const meta: Meta< typeof Tooltip > = { 'bottom right', ], }, - shortcut: { control: { type: 'text' } }, + shortcut: { control: { type: 'object' } }, }, parameters: { controls: { expanded: true }, @@ -57,3 +57,20 @@ KeyboardShortcut.args = { ariaLabel: shortcutAriaLabel.primaryShift( ',' ), }, }; + +/** + * In case one or more `Tooltip` components are rendered inside another + * `Tooltip` component, only the tooltip associated to the outermost `Tooltip` + * component will be rendered in the browser and shown to the user + * appropriately. The rest of the nested `Tooltip` components will simply no-op + * and pass-through their anchor. + */ +export const Nested: StoryFn< typeof Tooltip > = Template.bind( {} ); +Nested.args = { + children: ( + + + + ), + text: 'Outer tooltip text', +}; diff --git a/packages/components/src/tooltip/test/index.tsx b/packages/components/src/tooltip/test/index.tsx index cbe144cfa53d4d..ed6f7b5f7b4a14 100644 --- a/packages/components/src/tooltip/test/index.tsx +++ b/packages/components/src/tooltip/test/index.tsx @@ -436,4 +436,50 @@ describe( 'Tooltip', () => { await waitExpectTooltipToHide(); } ); } ); + + describe( 'nested', () => { + it( 'should render the outer tooltip and ignore nested tooltips', async () => { + render( + + + + + + + + ); + + // Hover the anchor. Only the outer tooltip should show. + await hover( + screen.getByRole( 'button', { + name: 'Tooltip anchor', + } ) + ); + + await waitFor( () => + expect( + screen.getByRole( 'tooltip', { name: 'Outer tooltip' } ) + ).toBeVisible() + ); + expect( + screen.queryByRole( 'tooltip', { name: 'Middle tooltip' } ) + ).not.toBeInTheDocument(); + expect( + screen.queryByRole( 'tooltip', { name: 'Inner tooltip' } ) + ).not.toBeInTheDocument(); + expect( + screen.getByRole( 'button', { + description: 'Outer tooltip', + } ) + ).toBeVisible(); + + // Hover outside of the anchor, tooltip should hide + await hoverOutside(); + await waitFor( () => + expect( + screen.queryByRole( 'tooltip', { name: 'Outer tooltip' } ) + ).not.toBeInTheDocument() + ); + } ); + } ); } ); diff --git a/packages/components/src/tooltip/types.ts b/packages/components/src/tooltip/types.ts index 8708ae7005f5b3..3d28a1a0e96c67 100644 --- a/packages/components/src/tooltip/types.ts +++ b/packages/components/src/tooltip/types.ts @@ -59,3 +59,7 @@ export type TooltipProps = { */ text?: string; }; + +export type TooltipInternalContext = { + isNestedInTooltip?: boolean; +}; diff --git a/packages/create-block-interactive-template/CHANGELOG.md b/packages/create-block-interactive-template/CHANGELOG.md index 47a8aec6c92a31..159c65e9ada19c 100644 --- a/packages/create-block-interactive-template/CHANGELOG.md +++ b/packages/create-block-interactive-template/CHANGELOG.md @@ -2,20 +2,24 @@ ## Unreleased +### Enhancement + +- Update the template to use `viewModule` in block.json ([#57712](https://github.com/WordPress/gutenberg/pull/57712)). + ## 1.11.0 (2023-12-13) -- Add all files to the generated plugin zip. [#56943](https://github.com/WordPress/gutenberg/pull/56943) -- Prevent crash when Gutenberg plugin is not installed. [#56941](https://github.com/WordPress/gutenberg/pull/56941) +- Add all files to the generated plugin zip ([#56943](https://github.com/WordPress/gutenberg/pull/56943)). +- Prevent crash when Gutenberg plugin is not installed ([#56941](https://github.com/WordPress/gutenberg/pull/56941)). ## 1.10.1 (2023-12-07) -- Update template to use modules instead of scripts. [#56694](https://github.com/WordPress/gutenberg/pull/56694) +- Update template to use modules instead of scripts ([#56694](https://github.com/WordPress/gutenberg/pull/56694)). ## 1.10.0 (2023-11-29) ### Enhancement -- Update `view.js` and `render.php` templates to the new `store()` API. [#56613](https://github.com/WordPress/gutenberg/pull/56613) +- Update `view.js` and `render.php` templates to the new `store()` API ([#56613](https://github.com/WordPress/gutenberg/pull/56613)). ## 1.9.0 (2023-11-16) @@ -35,4 +39,4 @@ ### Enhancement -- Moves the `example` property into block.json by leveraging changes to create-block to now support `example`. [#52801](https://github.com/WordPress/gutenberg/pull/52801) +- Moves the `example` property into block.json by leveraging changes to create-block to now support `example` ([#52801](https://github.com/WordPress/gutenberg/pull/52801)). diff --git a/packages/create-block-interactive-template/README.md b/packages/create-block-interactive-template/README.md index cc0530c0630549..adf3cab6594cc9 100644 --- a/packages/create-block-interactive-template/README.md +++ b/packages/create-block-interactive-template/README.md @@ -10,6 +10,8 @@ This block template can be used by running the following command: npx @wordpress/create-block --template @wordpress/create-block-interactive-template ``` +It requires Gutenberg 17.5 or higher. + ## Contributing to this package This is an individual package that's part of the Gutenberg project. The project is organized as a monorepo. It's made up of multiple self-contained software packages, each with a specific purpose. The packages in this monorepo are published to [npm](https://www.npmjs.com/) and used by [WordPress](https://make.wordpress.org/core/) as well as other software projects. diff --git a/packages/create-block-interactive-template/block-templates/render.php.mustache b/packages/create-block-interactive-template/block-templates/render.php.mustache index 0f6883a9362407..960da619f790a4 100644 --- a/packages/create-block-interactive-template/block-templates/render.php.mustache +++ b/packages/create-block-interactive-template/block-templates/render.php.mustache @@ -13,11 +13,6 @@ // Generate unique id for aria-controls. $unique_id = wp_unique_id( 'p-' ); - -// Enqueue the view file. -if (function_exists('gutenberg_enqueue_module')) { - gutenberg_enqueue_module( '{{namespace}}-view' ); -} ?>
!! value ) diff --git a/packages/create-block/lib/scaffold.js b/packages/create-block/lib/scaffold.js index 49d3cbf794777a..bd9ba0396b75e3 100644 --- a/packages/create-block/lib/scaffold.js +++ b/packages/create-block/lib/scaffold.js @@ -44,6 +44,7 @@ module.exports = async ( editorStyle, style, render, + viewModule, viewScript, variantVars, customPackageJSON, @@ -84,6 +85,7 @@ module.exports = async ( editorStyle, style, render, + viewModule, viewScript, variantVars, customPackageJSON, diff --git a/packages/dataviews/README.md b/packages/dataviews/README.md index 558c509139a666..86e4e3e38c3dfc 100644 --- a/packages/dataviews/README.md +++ b/packages/dataviews/README.md @@ -236,7 +236,8 @@ Array of operations that can be performed upon each record. Each action is an ob - `isLoading`: whether the data is loading. `false` by default. - `supportedLayouts`: array of layouts supported. By default, all are: `table`, `grid`, `list`. - `deferredRendering`: whether the items should be rendered asynchronously. Useful when there's a field that takes a lot of time (e.g.: previews). `false` by default. -- `onSelectionChange`: callback that returns the selected items. So far, only the `list` view implements this. +- `onSelectionChange`: callback that signals the user selected one of more items, and takes them as parameter. So far, only the `list` view implements it. +- `onDetailsChange`: callback that signals the user triggered the details for one of more items, and takes them as paremeter. So far, only the `list` view implements it. ## Contributing to this package diff --git a/packages/dataviews/src/dataviews.js b/packages/dataviews/src/dataviews.js index 91ea942ee6d22a..61837e4f8fc964 100644 --- a/packages/dataviews/src/dataviews.js +++ b/packages/dataviews/src/dataviews.js @@ -17,6 +17,7 @@ import Search from './search'; import { VIEW_LAYOUTS } from './constants'; const defaultGetItemId = ( item ) => item.id; +const defaultOnSelectionChange = () => {}; export default function DataViews( { view, @@ -30,7 +31,8 @@ export default function DataViews( { isLoading = false, paginationInfo, supportedLayouts, - onSelectionChange, + onSelectionChange = defaultOnSelectionChange, + onDetailsChange = null, deferredRendering = false, } ) { const [ selection, setSelection ] = useState( [] ); @@ -57,7 +59,7 @@ export default function DataViews( { { search && ( @@ -89,6 +91,7 @@ export default function DataViews( { getItemId={ getItemId } isLoading={ isLoading } onSelectionChange={ onSetSelection } + onDetailsChange={ onDetailsChange } selection={ selection } deferredRendering={ deferredRendering } /> diff --git a/packages/dataviews/src/dropdown-menu-helper.js b/packages/dataviews/src/dropdown-menu-helper.js index 0c473c50cd0c10..ce0ace8f61e551 100644 --- a/packages/dataviews/src/dropdown-menu-helper.js +++ b/packages/dataviews/src/dropdown-menu-helper.js @@ -48,7 +48,7 @@ export const DropdownMenuRadioItemCustom = forwardRef( ) : ( ) diff --git a/packages/dataviews/src/pagination.js b/packages/dataviews/src/pagination.js index 21aeda8a602a1a..2c9cade42d89b7 100644 --- a/packages/dataviews/src/pagination.js +++ b/packages/dataviews/src/pagination.js @@ -23,8 +23,8 @@ const Pagination = memo( function Pagination( { totalPages !== 1 && ( diff --git a/packages/dataviews/src/style.scss b/packages/dataviews/src/style.scss index edf1500d2cc5ad..80630050b68efb 100644 --- a/packages/dataviews/src/style.scss +++ b/packages/dataviews/src/style.scss @@ -10,7 +10,7 @@ } } -.dataviews__filters-view-actions { +.dataviews-filters__view-actions { padding: $grid-unit-15 $grid-unit-40; .components-search-control { flex-grow: 1; @@ -55,7 +55,7 @@ margin: $grid-unit-40 0 $grid-unit-20; } -.dataviews-table-view { +.dataviews-view-table { width: 100%; text-indent: 0; border-color: inherit; @@ -85,9 +85,18 @@ tr { border-bottom: 1px solid $gray-100; + .dataviews-view-table-header-button { + gap: $grid-unit-05; + } + td:first-child, th:first-child { padding-left: $grid-unit-40; + + .dataviews-view-table-header-button, + .dataviews-view-table-header { + margin-left: - #{$grid-unit-10}; + } } td:last-child, @@ -112,18 +121,27 @@ th { position: sticky; top: -1px; - background-color: lighten($gray-100, 4%); + background-color: $white; box-shadow: inset 0 -#{$border-width} 0 $gray-100; - border-top: 1px solid $gray-100; - padding-top: $grid-unit-05; - padding-bottom: $grid-unit-05; + padding-top: $grid-unit-10; + padding-bottom: $grid-unit-10; z-index: 1; + font-size: 11px; + text-transform: uppercase; + font-weight: 500; + padding-left: $grid-unit-05; } } - .dataviews-table-header-button { - padding: 0; - gap: $grid-unit-05; + .dataviews-view-table-header-button { + padding: $grid-unit-05 $grid-unit-10; + font-size: 11px; + text-transform: uppercase; + font-weight: 500; + + &:not(:hover) { + color: $gray-900; + } span { speak: none; @@ -133,9 +151,13 @@ } } } + + .dataviews-view-table-header { + padding-left: $grid-unit-05; + } } -.dataviews-grid-view { +.dataviews-view-grid { margin-bottom: $grid-unit-30; grid-template-columns: repeat(2, minmax(0, 1fr)) !important; padding: 0 $grid-unit-40; @@ -149,10 +171,22 @@ } .dataviews-view-grid__card { - h3 { // Todo: A better way to target this - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + .dataviews-view-grid__primary-field { + .dataviews-view-grid__title-field { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: block; + font-size: $default-font-size; + width: 100%; + } + + .dataviews-view-grid__title-field a, + button.dataviews-view-grid__title-field { + font-weight: 500; + color: $gray-900; + text-decoration: none; + } } } @@ -173,12 +207,6 @@ .dataviews-view-grid__primary-field { min-height: $grid-unit-30; - - a { - color: $gray-900; - text-decoration: none; - font-weight: 500; - } } .dataviews-view-grid__fields { @@ -197,29 +225,71 @@ } } -.dataviews-list-view { +.dataviews-view-list { margin: 0; + padding: $grid-unit-10; li { - border-bottom: $border-width solid $gray-100; margin: 0; - &:first-child { - border-top: $border-width solid $gray-100; + + .dataviews-view-list__item-wrapper { + position: relative; + padding-right: $grid-unit-30; + border-radius: $grid-unit-05; + + &::after { + position: absolute; + content: ""; + top: 100%; + left: $grid-unit-30; + right: $grid-unit-30; + background: $gray-100; + height: 1px; + } } - &:last-child { - border-bottom: 0; + + &:not(.is-selected):hover { + color: var(--wp-admin-theme-color); + + .dataviews-view-list__fields { + color: var(--wp-admin-theme-color); + } } } - .dataviews-list-view__item { - padding: $grid-unit-15 $grid-unit-40; - cursor: default; - &:focus, - &:hover { - background-color: lighten($gray-100, 3%); + li.is-selected, + li.is-selected:focus-within { + .dataviews-view-list__item-wrapper { + background-color: var(--wp-admin-theme-color); + color: $white; + + .dataviews-view-list__fields, + .components-button { + color: $white; + } + + &::after { + background: transparent; + } } + } + + .dataviews-view-list__item { + padding: $grid-unit-15 0 $grid-unit-15 $grid-unit-30; + width: 100%; + cursor: pointer; &:focus { - box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); + &::before { + position: absolute; + content: ""; + top: -1px; + right: -1px; + bottom: -1px; + left: -1px; + box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); + z-index: -1; + border-radius: $grid-unit-05; + } } h3 { overflow: hidden; @@ -228,22 +298,12 @@ } } - .dataviews-list-view__item-selected, - .dataviews-list-view__item-selected:hover { - background-color: $gray-100; - - &:focus { - box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); - } - } - - .dataviews-list-view__media-wrapper { + .dataviews-view-list__media-wrapper { min-width: $grid-unit-40; height: $grid-unit-40; border-radius: $grid-unit-05; overflow: hidden; position: relative; - margin-top: $grid-unit-05; &::after { content: ""; @@ -257,19 +317,19 @@ } } - .dataviews-list-view__media-placeholder { + .dataviews-view-list__media-placeholder { min-width: $grid-unit-40; height: $grid-unit-40; background-color: $gray-200; } - .dataviews-list-view__fields { + .dataviews-view-list__fields { color: $gray-700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - .dataviews-list-view__field { + .dataviews-view-list__field { margin-right: $grid-unit-15; &:last-child { @@ -277,6 +337,31 @@ } } } + + & + .dataviews-pagination { + justify-content: space-between; + } + + .dataviews-view-list__details-button { + align-self: center; + opacity: 0; + } + + li.is-selected, + li:hover, + li:focus-within { + .dataviews-view-list__details-button { + opacity: 1; + } + } + + li.is-selected { + .dataviews-view-list__details-button { + &:focus { + box-shadow: 0 0 0 var(--wp-admin-border-width-focus) currentColor; + } + } + } } .dataviews-action-modal { @@ -288,7 +373,7 @@ padding: 0 $grid-unit-40; } -.dataviews__filters-custom-menu-radio-item-prefix { +.dataviews-filters__custom-menu-radio-item-prefix { display: block; width: 24px; } diff --git a/packages/dataviews/src/view-grid.js b/packages/dataviews/src/view-grid.js index 5ac4d1d42d5739..7c18d31dccd193 100644 --- a/packages/dataviews/src/view-grid.js +++ b/packages/dataviews/src/view-grid.js @@ -42,7 +42,7 @@ export default function ViewGrid( { gap={ 8 } columns={ 2 } alignment="top" - className="dataviews-grid-view" + className="dataviews-view-grid" > { usedData.map( ( item ) => ( +
    { usedData.map( ( item ) => { return ( -
  • -
    onSelectionChange( [ item ] ) } - > - -
    - { mediaField?.render( { item } ) || ( -
    - ) } -
    - +
  • + +
    onSelectionChange( [ item ] ) } + > + +
    + { mediaField?.render( { item } ) || ( +
    + ) } +
    { primaryField?.render( { item } ) } -
    +
    { visibleFields.map( ( field ) => { return ( { field.render( { item, @@ -89,8 +92,19 @@ export default function ViewList( {
    - -
    +
    + { onDetailsChange && ( +
  • ); } ) } diff --git a/packages/dataviews/src/view-table.js b/packages/dataviews/src/view-table.js index 083931bb5203ec..dc76572e30494e 100644 --- a/packages/dataviews/src/view-table.js +++ b/packages/dataviews/src/view-table.js @@ -90,9 +90,9 @@ const HeaderMenu = forwardRef( function HeaderMenu( trigger={ - - - - - + + { area === value && ( + + ) } + + + + ) + ) } + + + + + + + + ); } diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/context.js b/packages/edit-site/src/components/global-styles/font-library-modal/context.js index 58b8621adcf0c3..e0749845788d60 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/context.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/context.js @@ -14,7 +14,7 @@ import { * Internal dependencies */ import { - fetchInstallFonts, + fetchInstallFont, fetchUninstallFonts, fetchFontCollections, fetchFontCollection, @@ -26,7 +26,7 @@ import { mergeFontFamilies, loadFontFaceInBrowser, getDisplaySrcFromFontFace, - makeFormDataFromFontFamilies, + makeFormDataFromFontFamily, } from './utils'; import { toggleFont } from './utils/toggleFont'; import getIntersectingFontFaces from './utils/get-intersecting-font-faces'; @@ -192,19 +192,19 @@ function FontLibraryProvider( { children } ) { return getActivatedFontsOutline( source )[ slug ] || []; }; - async function installFonts( fonts ) { + async function installFont( font ) { setIsInstalling( true ); try { // Prepare formData to install. - const formData = makeFormDataFromFontFamilies( fonts ); + const formData = makeFormDataFromFontFamily( font ); // Install the fonts (upload the font files to the server and create the post in the database). - const response = await fetchInstallFonts( formData ); + const response = await fetchInstallFont( formData ); const fontsInstalled = response?.successes || []; // Get intersecting font faces between the fonts we tried to installed and the fonts that were installed // (to avoid activating a non installed font). const fontToBeActivated = getIntersectingFontFaces( fontsInstalled, - fonts + [ font ] ); // Activate the font families (add the font families to the global styles). activateCustomFontFamilies( fontToBeActivated ); @@ -358,7 +358,7 @@ function FontLibraryProvider( { children } ) { isFontActivated, getFontFacesActivated, loadFontFaceAsset, - installFonts, + installFont, uninstallFont, toggleActivateFont, getAvailableFontsOutline, diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js b/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js index 062ad232e3ca98..fc39e2e0096531 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js @@ -54,7 +54,7 @@ function FontCollection( { id } ) { const [ renderConfirmDialog, setRenderConfirmDialog ] = useState( requiresPermission && ! getGoogleFontsPermissionFromStorage() ); - const { collections, getFontCollection, installFonts } = + const { collections, getFontCollection, installFont } = useContext( FontLibraryContext ); const selectedCollection = collections.find( ( collection ) => collection.id === id @@ -92,6 +92,11 @@ function FontCollection( { id } ) { setNotice( null ); }, [ id ] ); + useEffect( () => { + // If the selected fonts change, reset the selected fonts to install + setFontsToInstall( [] ); + }, [ selectedFont ] ); + // Reset notice after 5 seconds useEffect( () => { if ( notice && notice?.duration !== 0 ) { @@ -149,7 +154,7 @@ function FontCollection( { id } ) { }; const handleInstall = async () => { - const response = await installFonts( fontsToInstall ); + const response = await installFont( fontsToInstall[ 0 ] ); const installNotice = getNoticeFromInstallResponse( response ); setNotice( installNotice ); resetFontsToInstall(); diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/local-fonts.js b/packages/edit-site/src/components/global-styles/font-library-modal/local-fonts.js index 9612f8be52f5ee..d4221b420cb613 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/local-fonts.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/local-fonts.js @@ -11,6 +11,7 @@ import { FormFileUpload, Notice, FlexItem, + privateApis as componentsPrivateApis, } from '@wordpress/components'; import { useContext, useState, useEffect } from '@wordpress/element'; @@ -23,10 +24,14 @@ import { Font } from '../../../../lib/lib-font.browser'; import makeFamiliesFromFaces from './utils/make-families-from-faces'; import { loadFontFaceInBrowser } from './utils'; import { getNoticeFromInstallResponse } from './utils/get-notice-from-response'; +import { unlock } from '../../../lock-unlock'; + +const { ProgressBar } = unlock( componentsPrivateApis ); function LocalFonts() { - const { installFonts } = useContext( FontLibraryContext ); + const { installFont } = useContext( FontLibraryContext ); const [ notice, setNotice ] = useState( null ); + const [ isUploading, setIsUploading ] = useState( false ); const supportedFormats = ALLOWED_FILE_EXTENSIONS.slice( 0, -1 ) .map( ( extension ) => `.${ extension }` ) @@ -58,6 +63,7 @@ function LocalFonts() { */ const handleFilesUpload = ( files ) => { setNotice( null ); + setIsUploading( true ); const uniqueFilenames = new Set(); const selectedFiles = [ ...files ]; const allowedFiles = selectedFiles.filter( ( file ) => { @@ -147,9 +153,21 @@ function LocalFonts() { */ const handleInstall = async ( fontFaces ) => { const fontFamilies = makeFamiliesFromFaces( fontFaces ); - const response = await installFonts( fontFamilies ); + + if ( fontFamilies.length > 1 ) { + setNotice( { + type: 'error', + message: __( + 'Variants from only one font family can be uploaded at a time.' + ), + } ); + return; + } + + const response = await installFont( fontFamilies[ 0 ] ); const installNotice = getNoticeFromInstallResponse( response ); setNotice( installNotice ); + setIsUploading( false ); }; return ( @@ -157,31 +175,28 @@ function LocalFonts() { - `.${ ext }` - ).join( ',' ) } - multiple={ true } - onChange={ onFilesUpload } - render={ ( { openFileDialog } ) => ( - - ) } - /> - { notice && ( + { ! isUploading && ( + `.${ ext }` + ).join( ',' ) } + multiple={ true } + onChange={ onFilesUpload } + render={ ( { openFileDialog } ) => ( + + ) } + /> + ) } + { isUploading && ( - - - { notice.message } - +
    + +
    ) } @@ -194,6 +209,18 @@ function LocalFonts() { supportedFormats ) } + { ! isUploading && notice && ( + + + + { notice.message } + + + ) }
    ); diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/resolvers.js b/packages/edit-site/src/components/global-styles/font-library-modal/resolvers.js index 0ab4a7ba742247..2e7f413a6fa45b 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/resolvers.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/resolvers.js @@ -7,7 +7,7 @@ */ import apiFetch from '@wordpress/api-fetch'; -export async function fetchInstallFonts( data ) { +export async function fetchInstallFont( data ) { const config = { path: '/wp/v2/font-families', method: 'POST', diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/style.scss b/packages/edit-site/src/components/global-styles/font-library-modal/style.scss index cf7de98d6fbbb1..d026563d3b73ea 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/style.scss +++ b/packages/edit-site/src/components/global-styles/font-library-modal/style.scss @@ -94,6 +94,9 @@ justify-content: center; height: 250px; width: 100%; +} + +button.font-library-modal__upload-area { background-color: #f0f0f0; } diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js b/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js index 69db09d49a0cea..2874dd446efb45 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js @@ -130,16 +130,21 @@ export function getDisplaySrcFromFontFace( input, urlPrefix ) { return src; } -export function makeFormDataFromFontFamilies( fontFamilies ) { +export function makeFormDataFromFontFamily( fontFamily ) { const formData = new FormData(); - const newFontFamilies = fontFamilies.map( ( family, familyIndex ) => { - const { kebabCase } = unlock( componentsPrivateApis ); - family.slug = kebabCase( family.slug ); - if ( family?.fontFace ) { - family.fontFace = family.fontFace.map( ( face, faceIndex ) => { + const { kebabCase } = unlock( componentsPrivateApis ); + + const newFontFamily = { + ...fontFamily, + slug: kebabCase( fontFamily.slug ), + }; + + if ( newFontFamily?.fontFace ) { + const newFontFaces = newFontFamily.fontFace.map( + ( face, faceIndex ) => { if ( face.file ) { // Slugified file name because the it might contain spaces or characters treated differently on the server. - const fileId = `file-${ familyIndex }-${ faceIndex }`; + const fileId = `file-${ faceIndex }`; // Add the files to the formData formData.append( fileId, face.file, face.file.name ); // remove the file object from the face object the file is referenced by the uploadedFile key @@ -151,10 +156,11 @@ export function makeFormDataFromFontFamilies( fontFamilies ) { return newFace; } return face; - } ); - } - return family; - } ); - formData.append( 'font_families', JSON.stringify( newFontFamilies ) ); + } + ); + newFontFamily.fontFace = newFontFaces; + } + + formData.append( 'font_family_settings', JSON.stringify( newFontFamily ) ); return formData; } diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/makeFormDataFromFontFamilies.spec.js b/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/makeFormDataFromFontFamilies.spec.js deleted file mode 100644 index 4adae7889cc5e5..00000000000000 --- a/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/makeFormDataFromFontFamilies.spec.js +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Internal dependencies - */ -import { makeFormDataFromFontFamilies } from '../index'; - -/* global File */ - -describe( 'makeFormDataFromFontFamilies', () => { - it( 'should process fontFamilies and return FormData', () => { - const mockFontFamilies = [ - { - slug: 'bebas', - name: 'Bebas', - fontFamily: 'Bebas', - fontFace: [ - { - file: new File( [ 'content' ], 'test-font1.woff2' ), - fontWeight: '500', - fontStyle: 'normal', - }, - { - file: new File( [ 'content' ], 'test-font2.woff2' ), - fontWeight: '400', - fontStyle: 'normal', - }, - ], - }, - ]; - - const formData = makeFormDataFromFontFamilies( mockFontFamilies ); - - expect( formData instanceof FormData ).toBeTruthy(); - - // Check if files are added correctly - expect( formData.get( 'file-0-0' ).name ).toBe( 'test-font1.woff2' ); - expect( formData.get( 'file-0-1' ).name ).toBe( 'test-font2.woff2' ); - - // Check if 'fontFamilies' key in FormData is correct - const expectedFontFamilies = [ - { - fontFace: [ - { - fontWeight: '500', - fontStyle: 'normal', - uploadedFile: 'file-0-0', - }, - { - fontWeight: '400', - fontStyle: 'normal', - uploadedFile: 'file-0-1', - }, - ], - slug: 'bebas', - name: 'Bebas', - fontFamily: 'Bebas', - }, - ]; - expect( JSON.parse( formData.get( 'font_families' ) ) ).toEqual( - expectedFontFamilies - ); - } ); -} ); diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/makeFormDataFromFontFamily.spec.js b/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/makeFormDataFromFontFamily.spec.js new file mode 100644 index 00000000000000..9f38903c89759b --- /dev/null +++ b/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/makeFormDataFromFontFamily.spec.js @@ -0,0 +1,58 @@ +/** + * Internal dependencies + */ +import { makeFormDataFromFontFamily } from '../index'; + +/* global File */ + +describe( 'makeFormDataFromFontFamily', () => { + it( 'should process fontFamilies and return FormData', () => { + const mockFontFamily = { + slug: 'bebas', + name: 'Bebas', + fontFamily: 'Bebas', + fontFace: [ + { + file: new File( [ 'content' ], 'test-font1.woff2' ), + fontWeight: '500', + fontStyle: 'normal', + }, + { + file: new File( [ 'content' ], 'test-font2.woff2' ), + fontWeight: '400', + fontStyle: 'normal', + }, + ], + }; + + const formData = makeFormDataFromFontFamily( mockFontFamily ); + + expect( formData instanceof FormData ).toBeTruthy(); + + // Check if files are added correctly + expect( formData.get( 'file-0' ).name ).toBe( 'test-font1.woff2' ); + expect( formData.get( 'file-1' ).name ).toBe( 'test-font2.woff2' ); + + // Check if 'fontFamilies' key in FormData is correct + const expectedFontFamily = { + fontFace: [ + { + fontWeight: '500', + fontStyle: 'normal', + uploadedFile: 'file-0', + }, + { + fontWeight: '400', + fontStyle: 'normal', + uploadedFile: 'file-1', + }, + ], + slug: 'bebas', + name: 'Bebas', + fontFamily: 'Bebas', + }; + expect( JSON.parse( formData.get( 'font_family_settings' ) ) ).toEqual( + expectedFontFamily + ); + } ); +} ); diff --git a/packages/edit-site/src/components/list/style.scss b/packages/edit-site/src/components/list/style.scss index 9d6e0f0f1d6e24..cfc65252c8e685 100644 --- a/packages/edit-site/src/components/list/style.scss +++ b/packages/edit-site/src/components/list/style.scss @@ -186,8 +186,3 @@ display: block; color: $gray-700; } - -.edit-site-list-title__customized-info { - font-size: $default-font-size; - font-weight: 500; -} diff --git a/packages/edit-site/src/components/page-main/index.js b/packages/edit-site/src/components/page-main/index.js index 4a4a235c1922bd..7cc2025ac2c941 100644 --- a/packages/edit-site/src/components/page-main/index.js +++ b/packages/edit-site/src/components/page-main/index.js @@ -30,7 +30,7 @@ export default function PageMain() { ) : ( ); - } else if ( window?.__experimentalAdminViews && path === '/pages' ) { + } else if ( window?.__experimentalAdminViews && path === '/page' ) { return ; } diff --git a/packages/edit-site/src/components/page-pages/index.js b/packages/edit-site/src/components/page-pages/index.js index 164b4daf8603c0..45368c11833f39 100644 --- a/packages/edit-site/src/components/page-pages/index.js +++ b/packages/edit-site/src/components/page-pages/index.js @@ -4,6 +4,7 @@ import { __experimentalView as View, __experimentalVStack as VStack, + Button, } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { useEntityRecords, store as coreStore } from '@wordpress/core-data'; @@ -20,7 +21,10 @@ import { ENTER, SPACE } from '@wordpress/keycodes'; */ import Page from '../page'; import Link from '../routes/link'; -import { default as DEFAULT_VIEWS } from '../sidebar-dataviews/default-views'; +import { + DEFAULT_VIEWS, + DEFAULT_CONFIG_PER_VIEW_TYPE, +} from '../sidebar-dataviews/default-views'; import { ENUMERATION_TYPE, LAYOUT_GRID, @@ -39,22 +43,12 @@ import { useEditPostAction, } from '../actions'; import PostPreview from '../post-preview'; +import AddNewPageModal from '../add-new-page'; import Media from '../media'; import { unlock } from '../../lock-unlock'; const { useLocation, useHistory } = unlock( routerPrivateApis ); const EMPTY_ARRAY = []; -const defaultConfigPerViewType = { - [ LAYOUT_TABLE ]: {}, - [ LAYOUT_GRID ]: { - mediaField: 'featured-image', - primaryField: 'title', - }, - [ LAYOUT_LIST ]: { - primaryField: 'title', - mediaField: 'featured-image', - }, -}; function useView( type ) { const { @@ -138,6 +132,18 @@ export default function PagePages() { [ setPageId ] ); + const onDetailsChange = useCallback( + ( items ) => { + if ( !! postType && items?.length === 1 ) { + history.push( { + postId: items[ 0 ].id, + postType, + } ); + } + }, + [ history, postType ] + ); + const queryArgs = useMemo( () => { const filters = {}; view.filters.forEach( ( filter ) => { @@ -222,7 +228,7 @@ export default function PagePages() { { [ LAYOUT_TABLE, LAYOUT_GRID ].includes( view.type @@ -309,7 +315,7 @@ export default function PagePages() { newView = { ...newView, layout: { - ...defaultConfigPerViewType[ newView.type ], + ...DEFAULT_CONFIG_PER_VIEW_TYPE[ newView.type ], }, }; } @@ -319,6 +325,29 @@ export default function PagePages() { [ view.type, setView ] ); + const [ showAddPageModal, setShowAddPageModal ] = useState( false ); + const openModal = useCallback( () => { + if ( ! showAddPageModal ) { + setShowAddPageModal( true ); + } + }, [ showAddPageModal ] ); + const closeModal = useCallback( () => { + if ( showAddPageModal ) { + setShowAddPageModal( false ); + } + }, [ showAddPageModal ] ); + const handleNewPage = useCallback( + ( { type, id } ) => { + history.push( { + postId: id, + postType: type, + canvas: 'edit', + } ); + closeModal(); + }, + [ history ] + ); + // TODO: we need to handle properly `data={ data || EMPTY_ARRAY }` for when `isLoading`. return ( <> @@ -329,6 +358,19 @@ export default function PagePages() { : null } title={ __( 'Pages' ) } + actions={ + <> + + { showAddPageModal && ( + + ) } + + } > { view.type === LAYOUT_LIST && ( diff --git a/packages/edit-site/src/components/page-pages/style.scss b/packages/edit-site/src/components/page-pages/style.scss index 35ac8273dc555a..c2d2cc25529c4a 100644 --- a/packages/edit-site/src/components/page-pages/style.scss +++ b/packages/edit-site/src/components/page-pages/style.scss @@ -1,11 +1,6 @@ .edit-site-page-pages__featured-image { border-radius: $grid-unit-05; - width: $grid-unit-40; - height: $grid-unit-40; -} - - -.edit-site-page-pages__list-view-title-field { - font-size: $default-font-size; - font-weight: 500; + width: $grid-unit-50; + height: $grid-unit-50; + display: block; } diff --git a/packages/edit-site/src/components/page-patterns/dataviews-pattern-actions.js b/packages/edit-site/src/components/page-patterns/dataviews-pattern-actions.js index e49238495b5f3c..bf5210beb49fbf 100644 --- a/packages/edit-site/src/components/page-patterns/dataviews-pattern-actions.js +++ b/packages/edit-site/src/components/page-patterns/dataviews-pattern-actions.js @@ -8,7 +8,7 @@ import { paramCase as kebabCase } from 'change-case'; */ import { getQueryArgs } from '@wordpress/url'; import { downloadBlob } from '@wordpress/blob'; -import { __, sprintf } from '@wordpress/i18n'; +import { __, _x, sprintf } from '@wordpress/i18n'; import { Button, TextControl, @@ -35,6 +35,7 @@ import { TEMPLATE_PART_POST_TYPE, PATTERN_DEFAULT_CATEGORY, } from '../../utils/constants'; +import { CreateTemplatePartModalContents } from '../create-template-part-modal'; const { useHistory } = unlock( routerPrivateApis ); const { CreatePatternModalContents, useDuplicatePatternProps } = @@ -250,9 +251,9 @@ export const resetAction = { export const duplicatePatternAction = { id: 'duplicate-pattern', - label: __( 'Duplicate' ), + label: _x( 'Duplicate', 'action label' ), isEligible: ( item ) => item.type !== TEMPLATE_PART_POST_TYPE, - modalHeader: __( 'Duplicate pattern' ), + modalHeader: _x( 'Duplicate pattern', 'action label' ), RenderModal: ( { item, closeModal } ) => { const { categoryId = PATTERN_DEFAULT_CATEGORY } = getQueryArgs( window.location.href @@ -275,9 +276,54 @@ export const duplicatePatternAction = { return ( ); }, }; + +export const duplicateTemplatePartAction = { + id: 'duplicate-template-part', + label: _x( 'Duplicate', 'action label' ), + isEligible: ( item ) => item.type === TEMPLATE_PART_POST_TYPE, + modalHeader: _x( 'Duplicate template part', 'action label' ), + RenderModal: ( { item, closeModal } ) => { + const { createSuccessNotice } = useDispatch( noticesStore ); + const { categoryId = PATTERN_DEFAULT_CATEGORY } = getQueryArgs( + window.location.href + ); + const history = useHistory(); + async function onTemplatePartSuccess( templatePart ) { + createSuccessNotice( + sprintf( + // translators: %s: The new template part's title e.g. 'Call to action (copy)'. + __( '"%s" duplicated.' ), + item.title + ), + { type: 'snackbar', id: 'edit-site-patterns-success' } + ); + history.push( { + postType: TEMPLATE_PART_POST_TYPE, + postId: templatePart?.id, + categoryType: TEMPLATE_PART_POST_TYPE, + categoryId, + } ); + closeModal(); + } + return ( + + ); + }, +}; diff --git a/packages/edit-site/src/components/page-patterns/dataviews-patterns.js b/packages/edit-site/src/components/page-patterns/dataviews-patterns.js index f7e86b8e2e23b1..ad474d882cfcf6 100644 --- a/packages/edit-site/src/components/page-patterns/dataviews-patterns.js +++ b/packages/edit-site/src/components/page-patterns/dataviews-patterns.js @@ -55,6 +55,7 @@ import { resetAction, deleteAction, duplicatePatternAction, + duplicateTemplatePartAction, } from './dataviews-pattern-actions'; import usePatternSettings from './use-pattern-settings'; import { unlock } from '../../lock-unlock'; @@ -198,7 +199,9 @@ function Title( { item, categoryId } ) { ) } { item.type === PATTERN_TYPES.theme ? ( - item.title + + { item.title } + ) : ( @@ -318,6 +322,7 @@ export default function DataviewsPatterns() { () => [ renameAction, duplicatePatternAction, + duplicateTemplatePartAction, exportJSONaction, resetAction, deleteAction, diff --git a/packages/edit-site/src/components/page-patterns/style.scss b/packages/edit-site/src/components/page-patterns/style.scss index cce14e8067122d..dd3c52ef08c1a3 100644 --- a/packages/edit-site/src/components/page-patterns/style.scss +++ b/packages/edit-site/src/components/page-patterns/style.scss @@ -225,7 +225,7 @@ } /** - * DataViews patterns styles + * DataViews patterns styles. * TODO: when this becomes stable, consolidate styles with the above. */ .edit-site-page-patterns-dataviews { @@ -256,8 +256,6 @@ } } -// TODO: this is duplicated from `patterns-menu-items__convert-modal` styles, -// except for the `z-index`. Need to check if this is still needed. .dataviews-action-modal__duplicate-pattern { // Fix the modal width to prevent added categories from stretching the modal. [role="dialog"] > [role="document"] { @@ -283,3 +281,11 @@ max-height: $grid-unit-60 * 2; // Adjust to not cover the save button, showing three items. } } + +.dataviews-action-modal__duplicate-template-part { + .components-modal__frame { + @include break-small { + max-width: 500px; + } + } +} diff --git a/packages/edit-site/src/components/page-templates/index.js b/packages/edit-site/src/components/page-templates/index.js index 1aaf1e153d0c50..c0e0289311db6a 100644 --- a/packages/edit-site/src/components/page-templates/index.js +++ b/packages/edit-site/src/components/page-templates/index.js @@ -103,7 +103,7 @@ function TemplateTitle( { item, viewType } ) { return ( - + ), }, + { + name: 'blocks', + tabLabel: __( 'Blocks' ), + content: ( + <> + + + + + ), + }, ]; if ( ! isModalActive ) { return null; diff --git a/packages/edit-site/src/components/sidebar-dataviews/add-new-view.js b/packages/edit-site/src/components/sidebar-dataviews/add-new-view.js index d84d85faf4d60a..4bf7a173525c90 100644 --- a/packages/edit-site/src/components/sidebar-dataviews/add-new-view.js +++ b/packages/edit-site/src/components/sidebar-dataviews/add-new-view.js @@ -19,7 +19,7 @@ import { privateApis as routerPrivateApis } from '@wordpress/router'; * Internal dependencies */ import SidebarNavigationItem from '../sidebar-navigation-item'; -import DEFAULT_VIEWS from './default-views'; +import { DEFAULT_VIEWS } from './default-views'; import { unlock } from '../../lock-unlock'; const { useHistory, useLocation } = unlock( routerPrivateApis ); diff --git a/packages/edit-site/src/components/sidebar-dataviews/default-views.js b/packages/edit-site/src/components/sidebar-dataviews/default-views.js index 11652286e62d8d..fe9f046f31972f 100644 --- a/packages/edit-site/src/components/sidebar-dataviews/default-views.js +++ b/packages/edit-site/src/components/sidebar-dataviews/default-views.js @@ -7,10 +7,27 @@ import { trash } from '@wordpress/icons'; /** * Internal dependencies */ -import { LAYOUT_TABLE, OPERATOR_IN } from '../../utils/constants'; +import { + LAYOUT_LIST, + LAYOUT_TABLE, + LAYOUT_GRID, + OPERATOR_IN, +} from '../../utils/constants'; + +export const DEFAULT_CONFIG_PER_VIEW_TYPE = { + [ LAYOUT_TABLE ]: {}, + [ LAYOUT_GRID ]: { + mediaField: 'featured-image', + primaryField: 'title', + }, + [ LAYOUT_LIST ]: { + primaryField: 'title', + mediaField: 'featured-image', + }, +}; const DEFAULT_PAGE_BASE = { - type: LAYOUT_TABLE, + type: LAYOUT_LIST, search: '', filters: [], page: 1, @@ -22,10 +39,12 @@ const DEFAULT_PAGE_BASE = { // All fields are visible by default, so it's // better to keep track of the hidden ones. hiddenFields: [ 'date', 'featured-image' ], - layout: {}, + layout: { + ...DEFAULT_CONFIG_PER_VIEW_TYPE[ LAYOUT_LIST ], + }, }; -const DEFAULT_VIEWS = { +export const DEFAULT_VIEWS = { page: [ { title: __( 'All' ), @@ -55,5 +74,3 @@ const DEFAULT_VIEWS = { }, ], }; - -export default DEFAULT_VIEWS; diff --git a/packages/edit-site/src/components/sidebar-dataviews/index.js b/packages/edit-site/src/components/sidebar-dataviews/index.js index 9e4534ab342745..9748600907e331 100644 --- a/packages/edit-site/src/components/sidebar-dataviews/index.js +++ b/packages/edit-site/src/components/sidebar-dataviews/index.js @@ -8,14 +8,14 @@ import { privateApis as routerPrivateApis } from '@wordpress/router'; * Internal dependencies */ -import { default as DEFAULT_VIEWS } from './default-views'; +import { DEFAULT_VIEWS } from './default-views'; import { unlock } from '../../lock-unlock'; const { useLocation } = unlock( routerPrivateApis ); import DataViewItem from './dataview-item'; import CustomDataViewsList from './custom-dataviews-list'; const PATH_TO_TYPE = { - '/pages': 'page', + '/page': 'page', }; export default function DataViewsSidebarContent() { diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-page/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-page/index.js index 110bc920fb0a9f..7df1aaa3ba9084 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-page/index.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-page/index.js @@ -31,7 +31,7 @@ import SidebarNavigationScreenDetailsFooter from '../sidebar-navigation-screen-d const { useHistory } = unlock( routerPrivateApis ); -export default function SidebarNavigationScreenPage() { +export default function SidebarNavigationScreenPage( { backPath } ) { const { setCanvasMode } = unlock( useDispatch( editSiteStore ) ); const history = useHistory(); const { @@ -88,6 +88,7 @@ export default function SidebarNavigationScreenPage() { return record ? ( { + const linkInfo = useLink( + { + postType, + postId, + }, + { + backPath: '/page', + } + ); + return ; +}; + +export default function SidebarNavigationScreenPagesDataViews() { + const { records: templateRecords } = useEntityRecords( + 'postType', + TEMPLATE_POST_TYPE, + { + per_page: -1, + } + ); + const templates = useMemo( + () => + templateRecords?.filter( ( { slug } ) => + [ '404', 'search' ].includes( slug ) + ), + [ templateRecords ] + ); + + return ( + } + footer={ + + { templates?.map( ( item ) => ( + + + { decodeEntities( + item.title?.rendered || __( '(no title)' ) + ) } + + + ) ) } + + } + /> + ); +} diff --git a/packages/edit-site/src/components/sidebar/index.js b/packages/edit-site/src/components/sidebar/index.js index 3fa1280d59f427..73c6aea7e328c5 100644 --- a/packages/edit-site/src/components/sidebar/index.js +++ b/packages/edit-site/src/components/sidebar/index.js @@ -7,7 +7,6 @@ import classNames from 'classnames'; * WordPress dependencies */ import { memo, useRef } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; import { __experimentalNavigatorProvider as NavigatorProvider, __experimentalNavigatorScreen as NavigatorScreen, @@ -32,9 +31,8 @@ import SidebarNavigationScreenTemplatesBrowse from '../sidebar-navigation-screen import SaveHub from '../save-hub'; import { unlock } from '../../lock-unlock'; import SidebarNavigationScreenPages from '../sidebar-navigation-screen-pages'; +import SidebarNavigationScreenPagesDataViews from '../sidebar-navigation-screen-pages-dataviews'; import SidebarNavigationScreenPage from '../sidebar-navigation-screen-page'; -import SidebarNavigationScreen from '../sidebar-navigation-screen'; -import DataViewsSidebarContent from '../sidebar-dataviews'; const { useLocation } = unlock( routerPrivateApis ); @@ -68,20 +66,15 @@ function SidebarScreens() { - + { window?.__experimentalAdminViews ? ( + + ) : ( + + ) } - { window?.__experimentalAdminViews && ( - - } - /> - - ) } diff --git a/packages/edit-site/src/components/style-book/index.js b/packages/edit-site/src/components/style-book/index.js index d3e0cb3531ae99..7ecfdc3506cf8a 100644 --- a/packages/edit-site/src/components/style-book/index.js +++ b/packages/edit-site/src/components/style-book/index.js @@ -8,7 +8,6 @@ import classnames from 'classnames'; */ import { Disabled, - TabPanel, privateApis as componentsPrivateApis, } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; @@ -48,6 +47,7 @@ const { CompositeV2: Composite, CompositeItemV2: CompositeItem, useCompositeStoreV2: useCompositeStore, + Tabs, } = unlock( componentsPrivateApis ); // The content area of the Style Book is rendered within an iframe so that global styles @@ -253,22 +253,37 @@ function StyleBook( { > { resizeObserver } { showTabs ? ( - - { ( tab ) => ( - - ) } - +
    + + + { tabs.map( ( tab ) => ( + + { tab.title } + + ) ) } + + { tabs.map( ( tab ) => ( + + + + ) ) } + +
    ) : ( { +export default function InserterSidebar() { + const { insertionPoint, showMostUsedBlocks } = useSelect( ( select ) => { const { getInsertionPoint } = unlock( select( editorStore ) ); + const { get } = select( preferencesStore ); return { insertionPoint: getInsertionPoint(), + showMostUsedBlocks: get( 'core', 'mostUsedBlocks' ), }; }, [] ); const { setIsInserterOpened } = useDispatch( editorStore ); diff --git a/packages/editor/src/components/post-visibility/check.js b/packages/editor/src/components/post-visibility/check.js index 4bf9bd03772da6..116db0f546de2b 100644 --- a/packages/editor/src/components/post-visibility/check.js +++ b/packages/editor/src/components/post-visibility/check.js @@ -1,26 +1,21 @@ /** * WordPress dependencies */ -import { compose } from '@wordpress/compose'; -import { withSelect } from '@wordpress/data'; +import { useSelect } from '@wordpress/data'; /** * Internal dependencies */ import { store as editorStore } from '../../store'; -export function PostVisibilityCheck( { hasPublishAction, render } ) { - const canEdit = hasPublishAction; +export default function PostVisibilityCheck( { render } ) { + const canEdit = useSelect( ( select ) => { + return ( + select( editorStore ).getCurrentPost()._links?.[ + 'wp:action-publish' + ] ?? false + ); + } ); + return render( { canEdit } ); } - -export default compose( [ - withSelect( ( select ) => { - const { getCurrentPost, getCurrentPostType } = select( editorStore ); - return { - hasPublishAction: - getCurrentPost()._links?.[ 'wp:action-publish' ] ?? false, - postType: getCurrentPostType(), - }; - } ), -] )( PostVisibilityCheck ); diff --git a/packages/editor/src/components/post-visibility/test/check.js b/packages/editor/src/components/post-visibility/test/check.js index 8ec0c2df04ec90..828e876cceb102 100644 --- a/packages/editor/src/components/post-visibility/test/check.js +++ b/packages/editor/src/components/post-visibility/test/check.js @@ -3,32 +3,43 @@ */ import { render, screen } from '@testing-library/react'; +/** + * WordPress dependencies + */ +import { useSelect } from '@wordpress/data'; + +jest.mock( '@wordpress/data/src/components/use-select', () => jest.fn() ); + /** * Internal dependencies */ -import { PostVisibilityCheck } from '../check'; +import PostVisibilityCheck from '../check'; + +function setupMockSelect( hasPublishAction ) { + useSelect.mockImplementation( ( mapSelect ) => { + return mapSelect( () => ( { + getCurrentPost: () => ( { + _links: { + 'wp:action-publish': hasPublishAction, + }, + } ), + } ) ); + } ); +} describe( 'PostVisibilityCheck', () => { const renderProp = ( { canEdit } ) => ( canEdit ? 'yes' : 'no' ); it( "should not render the edit link if the user doesn't have the right capability", () => { - render( - - ); + setupMockSelect( false ); + render( ); expect( screen.queryByText( 'yes' ) ).not.toBeInTheDocument(); expect( screen.getByText( 'no' ) ).toBeVisible(); } ); it( 'should render if the user has the correct capability', () => { - render( - - ); + setupMockSelect( true ); + render( ); expect( screen.queryByText( 'no' ) ).not.toBeInTheDocument(); expect( screen.getByText( 'yes' ) ).toBeVisible(); } ); diff --git a/packages/format-library/package.json b/packages/format-library/package.json index d2ea9062cf79e3..b71addc176e7c1 100644 --- a/packages/format-library/package.json +++ b/packages/format-library/package.json @@ -35,6 +35,7 @@ "@wordpress/html-entities": "file:../html-entities", "@wordpress/i18n": "file:../i18n", "@wordpress/icons": "file:../icons", + "@wordpress/private-apis": "file:../private-apis", "@wordpress/rich-text": "file:../rich-text", "@wordpress/url": "file:../url" }, diff --git a/packages/format-library/src/lock-unlock.js b/packages/format-library/src/lock-unlock.js new file mode 100644 index 00000000000000..f7512caa4b746f --- /dev/null +++ b/packages/format-library/src/lock-unlock.js @@ -0,0 +1,10 @@ +/** + * WordPress dependencies + */ +import { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '@wordpress/private-apis'; + +export const { lock, unlock } = + __dangerousOptInToUnstableAPIsOnlyForCoreModules( + 'I know using unstable features means my theme or plugin will inevitably break in the next version of WordPress.', + '@wordpress/format-library' + ); diff --git a/packages/format-library/src/text-color/inline.js b/packages/format-library/src/text-color/inline.js index f71c7766222582..98ced2cca01e29 100644 --- a/packages/format-library/src/text-color/inline.js +++ b/packages/format-library/src/text-color/inline.js @@ -17,13 +17,24 @@ import { store as blockEditorStore, useCachedTruthy, } from '@wordpress/block-editor'; -import { Popover, TabPanel } from '@wordpress/components'; +import { + Popover, + privateApis as componentsPrivateApis, +} from '@wordpress/components'; import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ import { textColor as settings, transparentValue } from './index'; +import { unlock } from '../lock-unlock'; + +const { Tabs } = unlock( componentsPrivateApis ); + +const TABS = [ + { name: 'color', title: __( 'Text' ) }, + { name: 'backgroundColor', title: __( 'Background' ) }, +]; function parseCSS( css = '' ) { return css.split( ';' ).reduce( ( accumulator, rule ) => { @@ -155,30 +166,32 @@ export default function InlineColorUI( { return ( - - { ( tab ) => ( - - ) } - + + + { TABS.map( ( tab ) => ( + + { tab.title } + + ) ) } + + { TABS.map( ( tab ) => ( + + + + ) ) } + ); } diff --git a/packages/format-library/src/text-color/style.scss b/packages/format-library/src/text-color/style.scss index 121ba8da756cf5..439af6db38d0cf 100644 --- a/packages/format-library/src/text-color/style.scss +++ b/packages/format-library/src/text-color/style.scss @@ -1,23 +1,6 @@ -.components-inline-color-popover { +.format-library__inline-color-popover { - .components-popover__content { - .components-tab-panel__tab-content { - padding: 16px; - } - - .components-color-palette { - margin-top: 0.6rem; - } - - .components-base-control__title { - display: block; - margin-bottom: 16px; - font-weight: 600; - color: #191e23; - } - - .component-color-indicator { - vertical-align: text-bottom; - } + [role="tabpanel"] { + padding: 16px; } } diff --git a/packages/icons/src/library/offline.js b/packages/icons/src/library/offline.js index f0daa1aaeb79ee..444d3667f297e8 100644 --- a/packages/icons/src/library/offline.js +++ b/packages/icons/src/library/offline.js @@ -4,13 +4,7 @@ import { SVG, Path } from '@wordpress/primitives'; const offline = ( - + **Note** -> The Interactivity API recently switched from [using modules instead of scripts in the frontend](https://github.com/WordPress/gutenberg/pull/56143). Therefore, in order to test this scaffolded block, you will need to add the following line to the `package.json` file of the generated plugin: - -```json -"files": [ - "src/view.js" -] -``` -> This should be updated in the [scripts package](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-scripts/) soon. - - - #### 2. Generate the build When the plugin folder is generated, we should launch the build process to get the final version of the interactive block that can be used from WordPress. @@ -61,7 +49,7 @@ At this point you should be able to insert the "My First Interactive Block" bloc ## Requirements of the Interactivity API -To start working with the Interactivity API you'll need to have a [proper WordPress development environment for blocks](https://developer.wordpress.org/block-editor/getting-started/devenv/) and some specific code in your block, which should include: +To start working with the Interactivity API you'll need to have a [proper WordPress development environment for blocks](https://developer.wordpress.org/block-editor/getting-started/devenv/) and some specific code in your block, which should include: #### A local WordPress installation @@ -71,7 +59,7 @@ To get quickly started, [`wp-now`](https://www.npmjs.com/package/@wp-now/wp-now) #### Latest vesion of Gutenberg -The Interactivity API is currently only available as an experimental feature from Gutenberg 17.2, so you'll need to have Gutenberg 17.2 or higher version installed and activated in your WordPress installation. +The Interactivity API is currently only available as an experimental feature from Gutenberg, so you'll need to have Gutenberg 17.5 or higher version installed and activated in your WordPress installation. #### Node.js diff --git a/packages/patterns/src/components/create-pattern-modal.js b/packages/patterns/src/components/create-pattern-modal.js index e69390b9997521..137c14222ced34 100644 --- a/packages/patterns/src/components/create-pattern-modal.js +++ b/packages/patterns/src/components/create-pattern-modal.js @@ -77,24 +77,27 @@ export function CreatePatternModalContents( { const categoryMap = useMemo( () => { // Merge the user and core pattern categories and remove any duplicates. const uniqueCategories = new Map(); - [ ...userPatternCategories, ...corePatternCategories ].forEach( - ( category ) => { - if ( - ! uniqueCategories.has( category.label ) && - // There are two core categories with `Post` label so explicitly remove the one with - // the `query` slug to avoid any confusion. - category.name !== 'query' - ) { - // We need to store the name separately as this is used as the slug in the - // taxonomy and may vary from the label. - uniqueCategories.set( category.label, { - label: category.label, - value: category.label, - name: category.name, - } ); - } + userPatternCategories.forEach( ( category ) => { + uniqueCategories.set( category.label.toLowerCase(), { + label: category.label, + name: category.name, + id: category.id, + } ); + } ); + + corePatternCategories.forEach( ( category ) => { + if ( + ! uniqueCategories.has( category.label.toLowerCase() ) && + // There are two core categories with `Post` label so explicitly remove the one with + // the `query` slug to avoid any confusion. + category.name !== 'query' + ) { + uniqueCategories.set( category.label.toLowerCase(), { + label: category.label, + name: category.name, + } ); } - ); + } ); return uniqueCategories; }, [ userPatternCategories, corePatternCategories ] ); @@ -140,9 +143,13 @@ export function CreatePatternModalContents( { */ async function findOrCreateTerm( term ) { try { - // We need to match any existing term to the correct slug to prevent duplicates, eg. - // the core `Headers` category uses the singular `header` as the slug. - const existingTerm = categoryMap.get( term ); + const existingTerm = categoryMap.get( term.toLowerCase() ); + if ( existingTerm && existingTerm.id ) { + return existingTerm.id; + } + // If we have an existing core category we need to match the new user category to the + // correct slug rather than autogenerating it to prevent duplicates, eg. the core `Headers` + // category uses the singular `header` as the slug. const termData = existingTerm ? { name: existingTerm.label, slug: existingTerm.name } : { name: term }; diff --git a/packages/preferences-persistence/src/migrations/preferences-package-data/convert-editor-settings.js b/packages/preferences-persistence/src/migrations/preferences-package-data/convert-editor-settings.js index b1940ef1b10813..84542937563acd 100644 --- a/packages/preferences-persistence/src/migrations/preferences-package-data/convert-editor-settings.js +++ b/packages/preferences-persistence/src/migrations/preferences-package-data/convert-editor-settings.js @@ -11,6 +11,7 @@ export default function convertEditorSettings( data ) { 'focusMode', 'inactivePanels', 'keepCaretInsideBlock', + 'mostUsedBlocks', 'openPanels', 'showBlockBreadcrumbs', 'showIconLabels', diff --git a/packages/private-apis/src/implementation.js b/packages/private-apis/src/implementation.js index a7da5bc9726554..619478cf76386d 100644 --- a/packages/private-apis/src/implementation.js +++ b/packages/private-apis/src/implementation.js @@ -24,6 +24,7 @@ const CORE_MODULES_USING_PRIVATE_APIS = [ '@wordpress/edit-site', '@wordpress/edit-widgets', '@wordpress/editor', + '@wordpress/format-library', '@wordpress/patterns', '@wordpress/reusable-blocks', '@wordpress/router', diff --git a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/GutenbergBridgeJS2Parent.java b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/GutenbergBridgeJS2Parent.java index c1dc4bab896b3a..62848ad68f12e4 100644 --- a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/GutenbergBridgeJS2Parent.java +++ b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/GutenbergBridgeJS2Parent.java @@ -32,6 +32,7 @@ interface MediaUploadEventEmitter { void onMediaFileUploadProgress(int mediaId, float progress); void onMediaFileUploadSucceeded(int mediaId, String mediaUrl, int serverId, WritableNativeMap metadata); void onMediaFileUploadFailed(int mediaId); + void onMediaFileUploadPaused(int mediaId); } interface MediaSaveEventEmitter { diff --git a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/DeferredEventEmitter.java b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/DeferredEventEmitter.java index fe83bc8a14b540..3bbc8fe7429532 100644 --- a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/DeferredEventEmitter.java +++ b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/DeferredEventEmitter.java @@ -31,6 +31,7 @@ public interface JSEventEmitter { private static final int MEDIA_UPLOAD_STATE_SUCCEEDED = 2; private static final int MEDIA_UPLOAD_STATE_FAILED = 3; private static final int MEDIA_UPLOAD_STATE_RESET = 4; + private static final int MEDIA_UPLOAD_STATE_PAUSED = 11; private static final int MEDIA_SAVE_STATE_SAVING = 5; private static final int MEDIA_SAVE_STATE_SUCCEEDED = 6; @@ -180,6 +181,11 @@ public void onMediaFileUploadFailed(int mediaId) { setMediaFileUploadDataInJS(MEDIA_UPLOAD_STATE_FAILED, mediaId, null, 0); } + @Override + public void onMediaFileUploadPaused(int mediaId) { + setMediaFileUploadDataInJS(MEDIA_UPLOAD_STATE_PAUSED, mediaId, null, 0); + } + // Media file save events emitter @Override public void onSaveMediaFileClear(String mediaId) { diff --git a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java index 8bffc6b28c00da..6098f15a6927b1 100644 --- a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java +++ b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java @@ -1147,6 +1147,10 @@ public void mediaFileUploadFailed(final int mediaId) { mDeferredEventEmitter.onMediaFileUploadFailed(mediaId); } + public void mediaFileUploadPaused(final int mediaId) { + mDeferredEventEmitter.onMediaFileUploadPaused(mediaId); + } + public void mediaFileUploadSucceeded(final int mediaId, final String mediaUrl, final int serverMediaId, final WritableNativeMap metadata) { mDeferredEventEmitter.onMediaFileUploadSucceeded(mediaId, mediaUrl, serverMediaId, metadata); diff --git a/packages/react-native-bridge/ios/Gutenberg.swift b/packages/react-native-bridge/ios/Gutenberg.swift index de0d1b513f00dc..1a3c0479646796 100644 --- a/packages/react-native-bridge/ios/Gutenberg.swift +++ b/packages/react-native-bridge/ios/Gutenberg.swift @@ -296,6 +296,7 @@ extension Gutenberg { case succeeded = 2 case failed = 3 case reset = 4 + case paused = 11 } public enum MediaSaveState: Int, MediaState { diff --git a/packages/react-native-editor/CHANGELOG.md b/packages/react-native-editor/CHANGELOG.md index ed3f7b5e961eb6..a62838b066bee4 100644 --- a/packages/react-native-editor/CHANGELOG.md +++ b/packages/react-native-editor/CHANGELOG.md @@ -10,6 +10,8 @@ For each user feature we should also add a importance categorization label to i --> ## Unreleased +- [**] Image block media uploads display a custom error message when there is no internet connection [#56937] +- [*] Fix missing custom color indicator for custom gradients [#57605] ## 1.110.0 - [*] [internal] Move InserterButton from components package to block-editor package [#56494] diff --git a/packages/scripts/CHANGELOG.md b/packages/scripts/CHANGELOG.md index f6fb412377f7ef..a917cd119c17a1 100644 --- a/packages/scripts/CHANGELOG.md +++ b/packages/scripts/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### New Features + +- Add experimental support for `viewModule` field in block.json for `build` and `start` scripts ([#57461](https://github.com/WordPress/gutenberg/pull/57461)). + ### Breaking Changes - Drop support for Node.js versions < 18. diff --git a/packages/scripts/README.md b/packages/scripts/README.md index 42a9c563ea77f7..e4a2be8d9a3fae 100644 --- a/packages/scripts/README.md +++ b/packages/scripts/README.md @@ -108,6 +108,11 @@ This script automatically use the optimized config but sometimes you may want to - `--webpack-src-dir` – Allows customization of the source code directory. Default is `src`. - `--output-path` – Allows customization of the output directory. Default is `build`. +Experimental support for the block.json `viewModule` field is available via the +`--experimental-modules` option. With this option enabled, script and module fields will all be +compiled. The `viewModule` field is analogous to the `viewScript` field, but will compile a module +and should be registered in WordPress using the Modules API. + #### Advanced information This script uses [webpack](https://webpack.js.org/) behind the scenes. It’ll look for a webpack config in the top-level directory of your package and will use it if it finds one. If none is found, it’ll use the default config provided by `@wordpress/scripts` packages. Learn more in the [Advanced Usage](#advanced-usage) section. @@ -391,6 +396,11 @@ This script automatically use the optimized config but sometimes you may want to - `--webpack-src-dir` – Allows customization of the source code directory. Default is `src`. - `--output-path` – Allows customization of the output directory. Default is `build`. +Experimental support for the block.json `viewModule` field is available via the +`--experimental-modules` option. With this option enabled, script and module fields will all be +compiled. The `viewModule` field is analogous to the `viewScript` field, but will compile a module +and should be registered in WordPress using the Modules API. + #### Advanced information This script uses [webpack](https://webpack.js.org/) behind the scenes. It’ll look for a webpack config in the top-level directory of your package and will use it if it finds one. If none is found, it’ll use the default config provided by `@wordpress/scripts` packages. Learn more in the [Advanced Usage](#advanced-usage) section. @@ -723,8 +733,8 @@ module.exports = { If you follow this approach, please, be aware that: -- You should keep using the `wp-scripts` commands (`start` and `build`). Do not use `webpack` directly. -- Future versions of this package may change what webpack and Babel plugins we bundle, default configs, etc. Should those changes be necessary, they will be registered in the [package’s CHANGELOG](https://github.com/WordPress/gutenberg/blob/HEAD/packages/scripts/CHANGELOG.md), so make sure to read it before upgrading. +- You should keep using the `wp-scripts` commands (`start` and `build`). Do not use `webpack` directly. +- Future versions of this package may change what webpack and Babel plugins we bundle, default configs, etc. Should those changes be necessary, they will be registered in the [package’s CHANGELOG](https://github.com/WordPress/gutenberg/blob/HEAD/packages/scripts/CHANGELOG.md), so make sure to read it before upgrading. ## Contributing to this package diff --git a/packages/scripts/config/webpack.config.js b/packages/scripts/config/webpack.config.js index 1e060d0e142c91..57bd258d325393 100644 --- a/packages/scripts/config/webpack.config.js +++ b/packages/scripts/config/webpack.config.js @@ -4,7 +4,7 @@ const { BundleAnalyzerPlugin } = require( 'webpack-bundle-analyzer' ); const { CleanWebpackPlugin } = require( 'clean-webpack-plugin' ); const CopyWebpackPlugin = require( 'copy-webpack-plugin' ); -const { DefinePlugin } = require( 'webpack' ); +const webpack = require( 'webpack' ); const browserslist = require( 'browserslist' ); const MiniCSSExtractPlugin = require( 'mini-css-extract-plugin' ); const { basename, dirname, resolve } = require( 'path' ); @@ -30,6 +30,9 @@ const { getWordPressSrcDirectory, getWebpackEntryPoints, getRenderPropPaths, + getAsBooleanFromENV, + getBlockJsonModuleFields, + getBlockJsonScriptFields, } = require( '../utils' ); const isProduction = process.env.NODE_ENV === 'production'; @@ -39,6 +42,9 @@ if ( ! browserslist.findConfig( '.' ) ) { target += ':' + fromConfigRoot( '.browserslistrc' ); } const hasReactFastRefresh = hasArgInCLI( '--hot' ) && ! isProduction; +const hasExperimentalModulesFlag = getAsBooleanFromENV( + 'WP_EXPERIMENTAL_MODULES' +); /** * The plugin recomputes the render paths once on each compilation. It is necessary to avoid repeating processing @@ -110,10 +116,10 @@ const cssLoaders = [ }, ]; -const config = { +/** @type {webpack.Configuration} */ +const baseConfig = { mode, target, - entry: getWebpackEntryPoints, output: { filename: '[name].js', path: resolve( process.cwd(), 'build' ), @@ -165,7 +171,7 @@ const config = { module: { rules: [ { - test: /\.(j|t)sx?$/, + test: /\.m?(j|t)sx?$/, exclude: /node_modules/, use: [ { @@ -245,21 +251,72 @@ const config = { }, ], }, + stats: { + children: false, + }, +}; + +// WP_DEVTOOL global variable controls how source maps are generated. +// See: https://webpack.js.org/configuration/devtool/#devtool. +if ( process.env.WP_DEVTOOL ) { + baseConfig.devtool = process.env.WP_DEVTOOL; +} + +if ( ! isProduction ) { + // Set default sourcemap mode if it wasn't set by WP_DEVTOOL. + baseConfig.devtool = baseConfig.devtool || 'source-map'; +} + +// Add source-map-loader if devtool is set, whether in dev mode or not. +if ( baseConfig.devtool ) { + baseConfig.module.rules.unshift( { + test: /\.(j|t)sx?$/, + exclude: [ /node_modules/ ], + use: require.resolve( 'source-map-loader' ), + enforce: 'pre', + } ); +} + +/** @type {webpack.Configuration} */ +const scriptConfig = { + ...baseConfig, + + entry: getWebpackEntryPoints( 'script' ), + + devServer: isProduction + ? undefined + : { + devMiddleware: { + writeToDisk: true, + }, + allowedHosts: 'auto', + host: 'localhost', + port: 8887, + proxy: { + '/build': { + pathRewrite: { + '^/build': '', + }, + }, + }, + }, + plugins: [ - new DefinePlugin( { + new webpack.DefinePlugin( { // Inject the `SCRIPT_DEBUG` global, used for development features flagging. SCRIPT_DEBUG: ! isProduction, } ), - // During rebuilds, all webpack assets that are not used anymore will be - // removed automatically. There is an exception added in watch mode for - // fonts and images. It is a known limitations: - // https://github.com/johnagan/clean-webpack-plugin/issues/159 - new CleanWebpackPlugin( { - cleanAfterEveryBuildPatterns: [ '!fonts/**', '!images/**' ], - // Prevent it from deleting webpack assets during builds that have - // multiple configurations returned in the webpack config. - cleanStaleWebpackAssets: false, - } ), + + // If we run a modules build, the 2 compilations can "clean" each other's output + // Prevent the cleaning from happening + ! hasExperimentalModulesFlag && + new CleanWebpackPlugin( { + cleanAfterEveryBuildPatterns: [ '!fonts/**', '!images/**' ], + // Prevent it from deleting webpack assets during builds that have + // multiple configurations returned in the webpack config. + cleanStaleWebpackAssets: false, + } ), + new RenderPathsPlugin(), new CopyWebpackPlugin( { patterns: [ @@ -269,27 +326,33 @@ const config = { noErrorOnMissing: true, transform( content, absoluteFrom ) { const convertExtension = ( path ) => { - return path.replace( /\.(j|t)sx?$/, '.js' ); + return path.replace( /\.m?(j|t)sx?$/, '.js' ); }; if ( basename( absoluteFrom ) === 'block.json' ) { const blockJson = JSON.parse( content.toString() ); - [ 'viewScript', 'script', 'editorScript' ].forEach( - ( key ) => { - if ( Array.isArray( blockJson[ key ] ) ) { - blockJson[ key ] = - blockJson[ key ].map( - convertExtension - ); - } else if ( - typeof blockJson[ key ] === 'string' - ) { - blockJson[ key ] = convertExtension( - blockJson[ key ] - ); + + [ + getBlockJsonScriptFields( blockJson ), + getBlockJsonModuleFields( blockJson ), + ].forEach( ( fields ) => { + if ( fields ) { + for ( const [ + key, + value, + ] of Object.entries( fields ) ) { + if ( Array.isArray( value ) ) { + blockJson[ key ] = + value.map( convertExtension ); + } else if ( + typeof value === 'string' + ) { + blockJson[ key ] = + convertExtension( value ); + } } } - ); + } ); return JSON.stringify( blockJson, null, 2 ); } @@ -317,52 +380,59 @@ const config = { process.env.WP_BUNDLE_ANALYZER && new BundleAnalyzerPlugin(), // MiniCSSExtractPlugin to extract the CSS thats gets imported into JavaScript. new MiniCSSExtractPlugin( { filename: '[name].css' } ), - // React Fast Refresh. - hasReactFastRefresh && new ReactRefreshWebpackPlugin(), // WP_NO_EXTERNALS global variable controls whether scripts' assets get // generated, and the default externals set. ! process.env.WP_NO_EXTERNALS && new DependencyExtractionWebpackPlugin(), ].filter( Boolean ), - stats: { - children: false, - }, }; -// WP_DEVTOOL global variable controls how source maps are generated. -// See: https://webpack.js.org/configuration/devtool/#devtool. -if ( process.env.WP_DEVTOOL ) { - config.devtool = process.env.WP_DEVTOOL; -} +if ( hasExperimentalModulesFlag ) { + /** @type {webpack.Configuration} */ + const moduleConfig = { + ...baseConfig, -if ( ! isProduction ) { - // Set default sourcemap mode if it wasn't set by WP_DEVTOOL. - config.devtool = config.devtool || 'source-map'; - config.devServer = { - devMiddleware: { - writeToDisk: true, + entry: getWebpackEntryPoints( 'module' ), + + experiments: { + ...baseConfig.experiments, + outputModule: true, }, - allowedHosts: 'auto', - host: 'localhost', - port: 8887, - proxy: { - '/build': { - pathRewrite: { - '^/build': '', - }, + + output: { + ...baseConfig.output, + module: true, + chunkFormat: 'module', + environment: { + ...baseConfig.output.environment, + module: true, + }, + library: { + ...baseConfig.output.library, + type: 'module', }, }, + + plugins: [ + new webpack.DefinePlugin( { + // Inject the `SCRIPT_DEBUG` global, used for development features flagging. + SCRIPT_DEBUG: ! isProduction, + } ), + // The WP_BUNDLE_ANALYZER global variable enables a utility that represents + // bundle content as a convenient interactive zoomable treemap. + process.env.WP_BUNDLE_ANALYZER && new BundleAnalyzerPlugin(), + // MiniCSSExtractPlugin to extract the CSS thats gets imported into JavaScript. + new MiniCSSExtractPlugin( { filename: '[name].css' } ), + // React Fast Refresh. + hasReactFastRefresh && new ReactRefreshWebpackPlugin(), + // WP_NO_EXTERNALS global variable controls whether scripts' assets get + // generated, and the default externals set. + ! process.env.WP_NO_EXTERNALS && + new DependencyExtractionWebpackPlugin(), + ].filter( Boolean ), }; -} -// Add source-map-loader if devtool is set, whether in dev mode or not. -if ( config.devtool ) { - config.module.rules.unshift( { - test: /\.(j|t)sx?$/, - exclude: [ /node_modules/ ], - use: require.resolve( 'source-map-loader' ), - enforce: 'pre', - } ); + module.exports = [ scriptConfig, moduleConfig ]; +} else { + module.exports = scriptConfig; } - -module.exports = config; diff --git a/packages/scripts/scripts/build.js b/packages/scripts/scripts/build.js index 714038fd80ee4e..0eef2afb451bfc 100644 --- a/packages/scripts/scripts/build.js +++ b/packages/scripts/scripts/build.js @@ -12,6 +12,10 @@ const EXIT_ERROR_CODE = 1; process.env.NODE_ENV = process.env.NODE_ENV || 'production'; +if ( hasArgInCLI( '--experimental-modules' ) ) { + process.env.WP_EXPERIMENTAL_MODULES = true; +} + if ( hasArgInCLI( '--webpack-no-externals' ) ) { process.env.WP_NO_EXTERNALS = true; } diff --git a/packages/scripts/scripts/start.js b/packages/scripts/scripts/start.js index cf29709f3eff15..6296192ef302b1 100644 --- a/packages/scripts/scripts/start.js +++ b/packages/scripts/scripts/start.js @@ -10,6 +10,10 @@ const { sync: resolveBin } = require( 'resolve-bin' ); const { getArgFromCLI, getWebpackArgs, hasArgInCLI } = require( '../utils' ); const EXIT_ERROR_CODE = 1; +if ( hasArgInCLI( '--experimental-modules' ) ) { + process.env.WP_EXPERIMENTAL_MODULES = true; +} + if ( hasArgInCLI( '--webpack-no-externals' ) ) { process.env.WP_NO_EXTERNALS = true; } diff --git a/packages/scripts/utils/block-json.js b/packages/scripts/utils/block-json.js new file mode 100644 index 00000000000000..892cc63c889e50 --- /dev/null +++ b/packages/scripts/utils/block-json.js @@ -0,0 +1,41 @@ +const moduleFields = new Set( [ 'viewModule' ] ); +const scriptFields = new Set( [ 'viewScript', 'script', 'editorScript' ] ); + +/** + * @param {Object} blockJson + * @return {null|Record} Fields + */ +function getBlockJsonModuleFields( blockJson ) { + let result = null; + for ( const field of moduleFields ) { + if ( Object.hasOwn( blockJson, field ) ) { + if ( ! result ) { + result = {}; + } + result[ field ] = blockJson[ field ]; + } + } + return result; +} + +/** + * @param {Object} blockJson + * @return {null|Record} Fields + */ +function getBlockJsonScriptFields( blockJson ) { + let result = null; + for ( const field of scriptFields ) { + if ( Object.hasOwn( blockJson, field ) ) { + if ( ! result ) { + result = {}; + } + result[ field ] = blockJson[ field ]; + } + } + return result; +} + +module.exports = { + getBlockJsonModuleFields, + getBlockJsonScriptFields, +}; diff --git a/packages/scripts/utils/config.js b/packages/scripts/utils/config.js index e4e42255f95dd3..8b1bbb1ca50590 100644 --- a/packages/scripts/utils/config.js +++ b/packages/scripts/utils/config.js @@ -17,6 +17,10 @@ const { } = require( './cli' ); const { fromConfigRoot, fromProjectRoot, hasProjectFile } = require( './file' ); const { hasPackageProp } = require( './package' ); +const { + getBlockJsonModuleFields, + getBlockJsonScriptFields, +} = require( './block-json' ); const { log } = console; // See https://babeljs.io/docs/en/config-files#configuration-file-types. @@ -108,7 +112,10 @@ const hasPostCSSConfig = () => */ const getWebpackArgs = () => { // Gets all args from CLI without those prefixed with `--webpack`. - let webpackArgs = getArgsFromCLI( [ '--webpack' ] ); + let webpackArgs = getArgsFromCLI( [ + '--experimental-modules', + '--webpack', + ] ); const hasWebpackOutputOption = hasArgInCLI( '-o' ) || hasArgInCLI( '--output' ); @@ -186,104 +193,52 @@ function getWordPressSrcDirectory() { * * @see https://webpack.js.org/concepts/entry-points/ * - * @return {Object} The list of entry points. + * @param {'script' | 'module'} buildType */ -function getWebpackEntryPoints() { - // 1. Handles the legacy format for entry points when explicitly provided with the `process.env.WP_ENTRY`. - if ( process.env.WP_ENTRY ) { - return JSON.parse( process.env.WP_ENTRY ); - } +function getWebpackEntryPoints( buildType ) { + /** + * @return {Object} The list of entry points. + */ + return () => { + // 1. Handles the legacy format for entry points when explicitly provided with the `process.env.WP_ENTRY`. + if ( process.env.WP_ENTRY ) { + return buildType === 'script' + ? JSON.parse( process.env.WP_ENTRY ) + : {}; + } - // Continue only if the source directory exists. - if ( ! hasProjectFile( getWordPressSrcDirectory() ) ) { - log( - chalk.yellow( - `Source directory "${ getWordPressSrcDirectory() }" was not found. Please confirm there is a "src" directory in the root or the value passed to --webpack-src-dir is correct.` - ) - ); - return {}; - } + // Continue only if the source directory exists. + if ( ! hasProjectFile( getWordPressSrcDirectory() ) ) { + log( + chalk.yellow( + `Source directory "${ getWordPressSrcDirectory() }" was not found. Please confirm there is a "src" directory in the root or the value passed to --webpack-src-dir is correct.` + ) + ); + return {}; + } - // 2. Checks whether any block metadata files can be detected in the defined source directory. - // It scans all discovered files looking for JavaScript assets and converts them to entry points. - const blockMetadataFiles = glob( '**/block.json', { - absolute: true, - cwd: fromProjectRoot( getWordPressSrcDirectory() ), - } ); + // 2. Checks whether any block metadata files can be detected in the defined source directory. + // It scans all discovered files looking for JavaScript assets and converts them to entry points. + const blockMetadataFiles = glob( '**/block.json', { + absolute: true, + cwd: fromProjectRoot( getWordPressSrcDirectory() ), + } ); + + if ( blockMetadataFiles.length > 0 ) { + const srcDirectory = fromProjectRoot( + getWordPressSrcDirectory() + sep + ); + + const entryPoints = {}; - if ( blockMetadataFiles.length > 0 ) { - const srcDirectory = fromProjectRoot( - getWordPressSrcDirectory() + sep - ); - const entryPoints = blockMetadataFiles.reduce( - ( accumulator, blockMetadataFile ) => { + for ( const blockMetadataFile of blockMetadataFiles ) { + const fileContents = readFileSync( blockMetadataFile ); + let parsedBlockJson; // wrapping in try/catch in case the file is malformed // this happens especially when new block.json files are added // at which point they are completely empty and therefore not valid JSON try { - const { editorScript, script, viewScript } = JSON.parse( - readFileSync( blockMetadataFile ) - ); - [ editorScript, script, viewScript ] - .flat() - .filter( - ( value ) => value && value.startsWith( 'file:' ) - ) - .forEach( ( value ) => { - // Removes the `file:` prefix. - const filepath = join( - dirname( blockMetadataFile ), - value.replace( 'file:', '' ) - ); - - // Takes the path without the file extension, and relative to the defined source directory. - if ( ! filepath.startsWith( srcDirectory ) ) { - log( - chalk.yellow( - `Skipping "${ value.replace( - 'file:', - '' - ) }" listed in "${ blockMetadataFile.replace( - fromProjectRoot( sep ), - '' - ) }". File is located outside of the "${ getWordPressSrcDirectory() }" directory.` - ) - ); - return; - } - const entryName = filepath - .replace( extname( filepath ), '' ) - .replace( srcDirectory, '' ) - .replace( /\\/g, '/' ); - - // Detects the proper file extension used in the defined source directory. - const [ entryFilepath ] = glob( - `${ entryName }.[jt]s?(x)`, - { - absolute: true, - cwd: fromProjectRoot( - getWordPressSrcDirectory() - ), - } - ); - - if ( ! entryFilepath ) { - log( - chalk.yellow( - `Skipping "${ value.replace( - 'file:', - '' - ) }" listed in "${ blockMetadataFile.replace( - fromProjectRoot( sep ), - '' - ) }". File does not exist in the "${ getWordPressSrcDirectory() }" directory.` - ) - ); - return; - } - accumulator[ entryName ] = entryFilepath; - } ); - return accumulator; + parsedBlockJson = JSON.parse( fileContents ); } catch ( error ) { chalk.yellow( `Skipping "${ blockMetadataFile.replace( @@ -291,35 +246,105 @@ function getWebpackEntryPoints() { '' ) }" due to malformed JSON.` ); - return accumulator; } - }, - {} - ); - if ( Object.keys( entryPoints ).length > 0 ) { - return entryPoints; + const fields = + buildType === 'script' + ? getBlockJsonScriptFields( parsedBlockJson ) + : getBlockJsonModuleFields( parsedBlockJson ); + + if ( ! fields ) { + continue; + } + + for ( const value of Object.values( fields ).flat() ) { + if ( ! value.startsWith( 'file:' ) ) { + continue; + } + + // Removes the `file:` prefix. + const filepath = join( + dirname( blockMetadataFile ), + value.replace( 'file:', '' ) + ); + + // Takes the path without the file extension, and relative to the defined source directory. + if ( ! filepath.startsWith( srcDirectory ) ) { + log( + chalk.yellow( + `Skipping "${ value.replace( + 'file:', + '' + ) }" listed in "${ blockMetadataFile.replace( + fromProjectRoot( sep ), + '' + ) }". File is located outside of the "${ getWordPressSrcDirectory() }" directory.` + ) + ); + return; + } + const entryName = filepath + .replace( extname( filepath ), '' ) + .replace( srcDirectory, '' ) + .replace( /\\/g, '/' ); + + // Detects the proper file extension used in the defined source directory. + const [ entryFilepath ] = glob( + `${ entryName }.?(m)[jt]s?(x)`, + { + absolute: true, + cwd: fromProjectRoot( getWordPressSrcDirectory() ), + } + ); + + if ( ! entryFilepath ) { + log( + chalk.yellow( + `Skipping "${ value.replace( + 'file:', + '' + ) }" listed in "${ blockMetadataFile.replace( + fromProjectRoot( sep ), + '' + ) }". File does not exist in the "${ getWordPressSrcDirectory() }" directory.` + ) + ); + return; + } + entryPoints[ entryName ] = entryFilepath; + } + } + + if ( Object.keys( entryPoints ).length > 0 ) { + return entryPoints; + } } - } - // 3. Checks whether a standard file name can be detected in the defined source directory, - // and converts the discovered file to entry point. - const [ entryFile ] = glob( 'index.[jt]s?(x)', { - absolute: true, - cwd: fromProjectRoot( getWordPressSrcDirectory() ), - } ); + // Don't do any further processing if this is a module build. + // This only respects *module block.json fields. + if ( buildType === 'module' ) { + return {}; + } - if ( ! entryFile ) { - log( - chalk.yellow( - `No entry file discovered in the "${ getWordPressSrcDirectory() }" directory.` - ) - ); - return {}; - } + // 3. Checks whether a standard file name can be detected in the defined source directory, + // and converts the discovered file to entry point. + const [ entryFile ] = glob( 'index.[jt]s?(x)', { + absolute: true, + cwd: fromProjectRoot( getWordPressSrcDirectory() ), + } ); + + if ( ! entryFile ) { + log( + chalk.yellow( + `No entry file discovered in the "${ getWordPressSrcDirectory() }" directory.` + ) + ); + return {}; + } - return { - index: entryFile, + return { + index: entryFile, + }; }; } diff --git a/packages/scripts/utils/index.js b/packages/scripts/utils/index.js index ae93160381df44..148895ecbc4edf 100644 --- a/packages/scripts/utils/index.js +++ b/packages/scripts/utils/index.js @@ -25,6 +25,10 @@ const { } = require( './config' ); const { fromProjectRoot, fromConfigRoot, hasProjectFile } = require( './file' ); const { getPackageProp, hasPackageProp } = require( './package' ); +const { + getBlockJsonModuleFields, + getBlockJsonScriptFields, +} = require( './block-json' ); module.exports = { fromProjectRoot, @@ -40,6 +44,8 @@ module.exports = { getWordPressSrcDirectory, getWebpackEntryPoints, getRenderPropPaths, + getBlockJsonModuleFields, + getBlockJsonScriptFields, hasArgInCLI, hasBabelConfig, hasCssnanoConfig, diff --git a/patches/react-native+0.71.11.patch b/patches/react-native+0.71.11+001+initial.patch similarity index 100% rename from patches/react-native+0.71.11.patch rename to patches/react-native+0.71.11+001+initial.patch diff --git a/patches/react-native+0.71.11+002+boost-podspec.patch b/patches/react-native+0.71.11+002+boost-podspec.patch new file mode 100644 index 00000000000000..ad785b77a47fcc --- /dev/null +++ b/patches/react-native+0.71.11+002+boost-podspec.patch @@ -0,0 +1,13 @@ +diff --git a/node_modules/react-native/third-party-podspecs/boost.podspec b/node_modules/react-native/third-party-podspecs/boost.podspec +index 3d9331c..bbbb738 100644 +--- a/node_modules/react-native/third-party-podspecs/boost.podspec ++++ b/node_modules/react-native/third-party-podspecs/boost.podspec +@@ -10,7 +10,7 @@ Pod::Spec.new do |spec| + spec.homepage = 'http://www.boost.org' + spec.summary = 'Boost provides free peer-reviewed portable C++ source libraries.' + spec.authors = 'Rene Rivera' +- spec.source = { :http => 'https://boostorg.jfrog.io/artifactory/main/release/1.76.0/source/boost_1_76_0.tar.bz2', ++ spec.source = { :http => 'https://archives.boost.io/release/1.76.0/source/boost_1_76_0.tar.bz2', + :sha256 => 'f0397ba6e982c4450f27bf32a2a83292aba035b827a5623a14636ea583318c41' } + + # Pinning to the same version as React.podspec. diff --git a/phpunit/experimental/interactivity-api/class-wp-interactivity-initial-state-test.php b/phpunit/experimental/interactivity-api/class-wp-interactivity-initial-state-test.php new file mode 100644 index 00000000000000..a95c3482ec80d1 --- /dev/null +++ b/phpunit/experimental/interactivity-api/class-wp-interactivity-initial-state-test.php @@ -0,0 +1,115 @@ +assertEmpty( WP_Interactivity_Initial_State::get_data() ); + } + + public function test_initial_state_can_be_merged() { + $state = array( + 'a' => 1, + 'b' => 2, + 'nested' => array( + 'c' => 3, + ), + ); + WP_Interactivity_Initial_State::merge_state( 'core', $state ); + $this->assertSame( $state, WP_Interactivity_Initial_State::get_state( 'core' ) ); + } + + public function test_initial_state_can_be_extended() { + WP_Interactivity_Initial_State::merge_state( 'core', array( 'a' => 1 ) ); + WP_Interactivity_Initial_State::merge_state( 'core', array( 'b' => 2 ) ); + WP_Interactivity_Initial_State::merge_state( 'custom', array( 'c' => 3 ) ); + $this->assertSame( + array( + 'core' => array( + 'a' => 1, + 'b' => 2, + ), + 'custom' => array( + 'c' => 3, + ), + ), + WP_Interactivity_Initial_State::get_data() + ); + } + + public function test_initial_state_existing_props_should_be_overwritten() { + WP_Interactivity_Initial_State::merge_state( 'core', array( 'a' => 1 ) ); + WP_Interactivity_Initial_State::merge_state( 'core', array( 'a' => 'overwritten' ) ); + $this->assertSame( + array( + 'core' => array( + 'a' => 'overwritten', + ), + ), + WP_Interactivity_Initial_State::get_data() + ); + } + + public function test_initial_state_existing_indexed_arrays_should_be_replaced() { + WP_Interactivity_Initial_State::merge_state( 'core', array( 'a' => array( 1, 2 ) ) ); + WP_Interactivity_Initial_State::merge_state( 'core', array( 'a' => array( 3, 4 ) ) ); + $this->assertSame( + array( + 'core' => array( + 'a' => array( 3, 4 ), + ), + ), + WP_Interactivity_Initial_State::get_data() + ); + } + + public function test_initial_state_should_be_correctly_rendered() { + WP_Interactivity_Initial_State::merge_state( 'core', array( 'a' => 1 ) ); + WP_Interactivity_Initial_State::merge_state( 'core', array( 'b' => 2 ) ); + WP_Interactivity_Initial_State::merge_state( 'custom', array( 'c' => 3 ) ); + + ob_start(); + WP_Interactivity_Initial_State::render(); + $rendered = ob_get_clean(); + $this->assertSame( + '', + $rendered + ); + } + + public function test_initial_state_should_also_escape_tags_and_amps() { + WP_Interactivity_Initial_State::merge_state( + 'test', + array( + 'amps' => 'http://site.test/?foo=1&baz=2&bar=3', + 'tags' => 'Do not do this: + + + + + + '; + + $html = do_blocks( $post_content ); + $tags = new WP_HTML_Tag_Processor( $html ); + + $tags->next_tag( array( 'class_name' => 'bind-state' ) ); + $this->assertSame( 'state', $tags->get_attribute( 'data-value' ) ); + + $tags->next_tag( array( 'class_name' => 'bind-context' ) ); + $this->assertSame( 'context', $tags->get_attribute( 'data-value' ) ); + } + + public function test_namespace_should_be_inherited_from_same_element() { + /* + * This function call should be done inside block render functions. We + * run it here instead just for conveninence. + */ + wp_initial_state( 'test-2', array( 'text' => 'state-2' ) ); + + $post_content = ' + + + + + + + '; + + $html = do_blocks( $post_content ); + $tags = new WP_HTML_Tag_Processor( $html ); + + $tags->next_tag( array( 'class_name' => 'bind-state' ) ); + $this->assertSame( 'state-2', $tags->get_attribute( 'data-value' ) ); + + $tags->next_tag( array( 'class_name' => 'bind-context' ) ); + $this->assertSame( 'context-2', $tags->get_attribute( 'data-value' ) ); + } + + public function test_namespace_should_not_leak_from_descendant() { + /* + * This function call should be done inside block render functions. We + * run it here instead just for conveninence. + */ + wp_initial_state( 'test-1', array( 'text' => 'state-1' ) ); + wp_initial_state( 'test-2', array( 'text' => 'state-2' ) ); + + $post_content = ' + + + + + + '; + + $html = do_blocks( $post_content ); + $tags = new WP_HTML_Tag_Processor( $html ); + + $tags->next_tag( array( 'class_name' => 'target' ) ); + $this->assertSame( 'state-1', $tags->get_attribute( 'data-state' ) ); + $this->assertSame( 'context-1', $tags->get_attribute( 'data-context' ) ); + } + + public function test_namespace_should_not_leak_from_sibling() { + /* + * This function call should be done inside block render functions. We + * run it here instead just for conveninence. + */ + wp_initial_state( 'test-1', array( 'text' => 'state-1' ) ); + wp_initial_state( 'test-2', array( 'text' => 'state-2' ) ); + + $post_content = ' + + + + + + + '; + + $html = do_blocks( $post_content ); + $tags = new WP_HTML_Tag_Processor( $html ); + + $tags->next_tag( array( 'class_name' => 'target' ) ); + $this->assertSame( 'state-1', $tags->get_attribute( 'data-from-state' ) ); + $this->assertSame( 'context-1', $tags->get_attribute( 'data-from-context' ) ); + } + + public function test_namespace_can_be_overwritten_in_directives() { + /* + * This function call should be done inside block render functions. We + * run it here instead just for conveninence. + */ + wp_initial_state( 'test-1', array( 'text' => 'state-1' ) ); + wp_initial_state( 'test-2', array( 'text' => 'state-2' ) ); + + $post_content = ' + + + + + + '; + + $html = do_blocks( $post_content ); + $tags = new WP_HTML_Tag_Processor( $html ); + + $tags->next_tag( array( 'class_name' => 'inherited-ns' ) ); + $this->assertSame( 'state-1', $tags->get_attribute( 'data-value' ) ); + + $tags->next_tag( array( 'class_name' => 'custom-ns' ) ); + $this->assertSame( 'state-2', $tags->get_attribute( 'data-value' ) ); + + $tags->next_tag( array( 'class_name' => 'mixed-ns' ) ); + $this->assertSame( 'state-1', $tags->get_attribute( 'data-inherited-ns' ) ); + $this->assertSame( 'state-2', $tags->get_attribute( 'data-custom-ns' ) ); + } } diff --git a/phpunit/experimental/interactivity-api/directives/wp-bind-test.php b/phpunit/experimental/interactivity-api/directives/wp-bind-test.php index bfb4c428cd9466..8fe212bb8ed93a 100644 --- a/phpunit/experimental/interactivity-api/directives/wp-bind-test.php +++ b/phpunit/experimental/interactivity-api/directives/wp-bind-test.php @@ -14,16 +14,17 @@ */ class Tests_Directives_WpBind extends WP_UnitTestCase { public function test_directive_sets_attribute() { - $markup = ''; + $markup = ''; $tags = new WP_HTML_Tag_Processor( $markup ); $tags->next_tag(); $context_before = new WP_Directive_Context( array( 'myblock' => array( 'imageSource' => './wordpress.png' ) ) ); $context = $context_before; - gutenberg_interactivity_process_wp_bind( $tags, $context ); + $directive_ns = 'myblock'; + gutenberg_interactivity_process_wp_bind( $tags, $context, $directive_ns ); $this->assertSame( - '', + '', $tags->get_updated_html() ); $this->assertSame( './wordpress.png', $tags->get_attribute( 'src' ) ); @@ -31,13 +32,14 @@ public function test_directive_sets_attribute() { } public function test_directive_ignores_empty_bound_attribute() { - $markup = ''; + $markup = ''; $tags = new WP_HTML_Tag_Processor( $markup ); $tags->next_tag(); $context_before = new WP_Directive_Context( array( 'myblock' => array( 'imageSource' => './wordpress.png' ) ) ); $context = $context_before; - gutenberg_interactivity_process_wp_bind( $tags, $context ); + $directive_ns = 'myblock'; + gutenberg_interactivity_process_wp_bind( $tags, $context, $directive_ns ); $this->assertSame( $markup, $tags->get_updated_html() ); $this->assertNull( $tags->get_attribute( 'src' ) ); diff --git a/phpunit/experimental/interactivity-api/directives/wp-class-test.php b/phpunit/experimental/interactivity-api/directives/wp-class-test.php index 419546c6d9ef8b..f40486647ff8b8 100644 --- a/phpunit/experimental/interactivity-api/directives/wp-class-test.php +++ b/phpunit/experimental/interactivity-api/directives/wp-class-test.php @@ -14,16 +14,17 @@ */ class Tests_Directives_WpClass extends WP_UnitTestCase { public function test_directive_adds_class() { - $markup = '
    Test
    '; + $markup = '
    Test
    '; $tags = new WP_HTML_Tag_Processor( $markup ); $tags->next_tag(); $context_before = new WP_Directive_Context( array( 'myblock' => array( 'isRed' => true ) ) ); $context = $context_before; - gutenberg_interactivity_process_wp_class( $tags, $context ); + $directive_ns = 'myblock'; + gutenberg_interactivity_process_wp_class( $tags, $context, $directive_ns ); $this->assertSame( - '
    Test
    ', + '
    Test
    ', $tags->get_updated_html() ); $this->assertStringContainsString( 'red', $tags->get_attribute( 'class' ) ); @@ -31,16 +32,17 @@ public function test_directive_adds_class() { } public function test_directive_removes_class() { - $markup = '
    Test
    '; + $markup = '
    Test
    '; $tags = new WP_HTML_Tag_Processor( $markup ); $tags->next_tag(); $context_before = new WP_Directive_Context( array( 'myblock' => array( 'isBlue' => false ) ) ); $context = $context_before; - gutenberg_interactivity_process_wp_class( $tags, $context ); + $directive_ns = 'myblock'; + gutenberg_interactivity_process_wp_class( $tags, $context, $directive_ns ); $this->assertSame( - '
    Test
    ', + '
    Test
    ', $tags->get_updated_html() ); $this->assertStringNotContainsString( 'blue', $tags->get_attribute( 'class' ) ); @@ -48,17 +50,18 @@ public function test_directive_removes_class() { } public function test_directive_removes_empty_class_attribute() { - $markup = '
    Test
    '; + $markup = '
    Test
    '; $tags = new WP_HTML_Tag_Processor( $markup ); $tags->next_tag(); $context_before = new WP_Directive_Context( array( 'myblock' => array( 'isBlue' => false ) ) ); $context = $context_before; - gutenberg_interactivity_process_wp_class( $tags, $context ); + $directive_ns = 'myblock'; + gutenberg_interactivity_process_wp_class( $tags, $context, $directive_ns ); $this->assertSame( // WP_HTML_Tag_Processor has a TODO note to prune whitespace after classname removal. - '
    Test
    ', + '
    Test
    ', $tags->get_updated_html() ); $this->assertNull( $tags->get_attribute( 'class' ) ); @@ -66,16 +69,17 @@ public function test_directive_removes_empty_class_attribute() { } public function test_directive_does_not_remove_non_existant_class() { - $markup = '
    Test
    '; + $markup = '
    Test
    '; $tags = new WP_HTML_Tag_Processor( $markup ); $tags->next_tag(); $context_before = new WP_Directive_Context( array( 'myblock' => array( 'isBlue' => false ) ) ); $context = $context_before; - gutenberg_interactivity_process_wp_class( $tags, $context ); + $directive_ns = 'myblock'; + gutenberg_interactivity_process_wp_class( $tags, $context, $directive_ns ); $this->assertSame( - '
    Test
    ', + '
    Test
    ', $tags->get_updated_html() ); $this->assertSame( 'green red', $tags->get_attribute( 'class' ) ); @@ -83,13 +87,14 @@ public function test_directive_does_not_remove_non_existant_class() { } public function test_directive_ignores_empty_class_name() { - $markup = '
    Test
    '; + $markup = '
    Test
    '; $tags = new WP_HTML_Tag_Processor( $markup ); $tags->next_tag(); $context_before = new WP_Directive_Context( array( 'myblock' => array( 'isRed' => true ) ) ); $context = $context_before; - gutenberg_interactivity_process_wp_class( $tags, $context ); + $directive_ns = 'myblock'; + gutenberg_interactivity_process_wp_class( $tags, $context, $directive_ns ); $this->assertSame( $markup, $tags->get_updated_html() ); $this->assertStringNotContainsString( 'red', $tags->get_attribute( 'class' ) ); diff --git a/phpunit/experimental/interactivity-api/directives/wp-context-test.php b/phpunit/experimental/interactivity-api/directives/wp-context-test.php index 1277b016848cce..788feec95fe7c5 100644 --- a/phpunit/experimental/interactivity-api/directives/wp-context-test.php +++ b/phpunit/experimental/interactivity-api/directives/wp-context-test.php @@ -21,11 +21,12 @@ public function test_directive_merges_context_correctly_upon_wp_context_attribut ) ); - $markup = '
    '; + $ns = 'myblock'; + $markup = '
    '; $tags = new WP_HTML_Tag_Processor( $markup ); $tags->next_tag(); - gutenberg_interactivity_process_wp_context( $tags, $context ); + gutenberg_interactivity_process_wp_context( $tags, $context, $ns ); $this->assertSame( array( @@ -38,39 +39,39 @@ public function test_directive_merges_context_correctly_upon_wp_context_attribut public function test_directive_resets_context_correctly_upon_closing_tag() { $context = new WP_Directive_Context( - array( 'my-key' => 'original-value' ) + array( 'myblock' => array( 'my-key' => 'original-value' ) ) ); $context->set_context( - array( 'my-key' => 'new-value' ) + array( 'myblock' => array( 'my-key' => 'new-value' ) ) ); $markup = '
    '; $tags = new WP_HTML_Tag_Processor( $markup ); $tags->next_tag( array( 'tag_closers' => 'visit' ) ); - gutenberg_interactivity_process_wp_context( $tags, $context ); + gutenberg_interactivity_process_wp_context( $tags, $context, 'myblock' ); $this->assertSame( array( 'my-key' => 'original-value' ), - $context->get_context() + $context->get_context()['myblock'] ); } public function test_directive_doesnt_throw_on_malformed_context_objects() { $context = new WP_Directive_Context( - array( 'my-key' => 'some-value' ) + array( 'myblock' => array( 'my-key' => 'some-value' ) ) ); $markup = '
    '; $tags = new WP_HTML_Tag_Processor( $markup ); $tags->next_tag(); - gutenberg_interactivity_process_wp_context( $tags, $context ); + gutenberg_interactivity_process_wp_context( $tags, $context, 'myblock' ); $this->assertSame( array( 'my-key' => 'some-value' ), - $context->get_context() + $context->get_context()['myblock'] ); } @@ -87,36 +88,36 @@ public function test_directive_keeps_working_after_malformed_context_objects() { // Parent div. $tags->next_tag( array( 'tag_closers' => 'visit' ) ); - gutenberg_interactivity_process_wp_context( $tags, $context ); + gutenberg_interactivity_process_wp_context( $tags, $context, 'myblock' ); $this->assertSame( array( 'my-key' => 'some-value' ), - $context->get_context() + $context->get_context()['myblock'] ); // Children div. $tags->next_tag( array( 'tag_closers' => 'visit' ) ); - gutenberg_interactivity_process_wp_context( $tags, $context ); + gutenberg_interactivity_process_wp_context( $tags, $context, 'myblock' ); // Still the same context. $this->assertSame( array( 'my-key' => 'some-value' ), - $context->get_context() + $context->get_context()['myblock'] ); // Closing children div. $tags->next_tag( array( 'tag_closers' => 'visit' ) ); - gutenberg_interactivity_process_wp_context( $tags, $context ); + gutenberg_interactivity_process_wp_context( $tags, $context, 'myblock' ); // Still the same context. $this->assertSame( array( 'my-key' => 'some-value' ), - $context->get_context() + $context->get_context()['myblock'] ); // Closing parent div. $tags->next_tag( array( 'tag_closers' => 'visit' ) ); - gutenberg_interactivity_process_wp_context( $tags, $context ); + gutenberg_interactivity_process_wp_context( $tags, $context, 'myblock' ); // Now the context is empty. $this->assertSame( @@ -138,36 +139,36 @@ public function test_directive_keeps_working_with_a_directive_without_value() { // Parent div. $tags->next_tag( array( 'tag_closers' => 'visit' ) ); - gutenberg_interactivity_process_wp_context( $tags, $context ); + gutenberg_interactivity_process_wp_context( $tags, $context, 'myblock' ); $this->assertSame( array( 'my-key' => 'some-value' ), - $context->get_context() + $context->get_context()['myblock'] ); // Children div. $tags->next_tag( array( 'tag_closers' => 'visit' ) ); - gutenberg_interactivity_process_wp_context( $tags, $context ); + gutenberg_interactivity_process_wp_context( $tags, $context, 'myblock' ); // Still the same context. $this->assertSame( array( 'my-key' => 'some-value' ), - $context->get_context() + $context->get_context()['myblock'] ); // Closing children div. $tags->next_tag( array( 'tag_closers' => 'visit' ) ); - gutenberg_interactivity_process_wp_context( $tags, $context ); + gutenberg_interactivity_process_wp_context( $tags, $context, 'myblock' ); // Still the same context. $this->assertSame( array( 'my-key' => 'some-value' ), - $context->get_context() + $context->get_context()['myblock'] ); // Closing parent div. $tags->next_tag( array( 'tag_closers' => 'visit' ) ); - gutenberg_interactivity_process_wp_context( $tags, $context ); + gutenberg_interactivity_process_wp_context( $tags, $context, 'myblock' ); // Now the context is empty. $this->assertSame( @@ -189,36 +190,36 @@ public function test_directive_keeps_working_with_an_empty_directive() { // Parent div. $tags->next_tag( array( 'tag_closers' => 'visit' ) ); - gutenberg_interactivity_process_wp_context( $tags, $context ); + gutenberg_interactivity_process_wp_context( $tags, $context, 'myblock' ); $this->assertSame( array( 'my-key' => 'some-value' ), - $context->get_context() + $context->get_context()['myblock'] ); // Children div. $tags->next_tag( array( 'tag_closers' => 'visit' ) ); - gutenberg_interactivity_process_wp_context( $tags, $context ); + gutenberg_interactivity_process_wp_context( $tags, $context, 'myblock' ); // Still the same context. $this->assertSame( array( 'my-key' => 'some-value' ), - $context->get_context() + $context->get_context()['myblock'] ); // Closing children div. $tags->next_tag( array( 'tag_closers' => 'visit' ) ); - gutenberg_interactivity_process_wp_context( $tags, $context ); + gutenberg_interactivity_process_wp_context( $tags, $context, 'myblock' ); // Still the same context. $this->assertSame( array( 'my-key' => 'some-value' ), - $context->get_context() + $context->get_context()['myblock'] ); // Closing parent div. $tags->next_tag( array( 'tag_closers' => 'visit' ) ); - gutenberg_interactivity_process_wp_context( $tags, $context ); + gutenberg_interactivity_process_wp_context( $tags, $context, 'myblock' ); // Now the context is empty. $this->assertSame( diff --git a/phpunit/experimental/interactivity-api/directives/wp-style-test.php b/phpunit/experimental/interactivity-api/directives/wp-style-test.php index 51468bd8a28141..9625803ebca78f 100644 --- a/phpunit/experimental/interactivity-api/directives/wp-style-test.php +++ b/phpunit/experimental/interactivity-api/directives/wp-style-test.php @@ -18,9 +18,9 @@ public function test_directive_adds_style() { $tags = new WP_HTML_Tag_Processor( $markup ); $tags->next_tag(); - $context_before = new WP_Directive_Context( array( 'color' => 'green' ) ); + $context_before = new WP_Directive_Context( array( 'myblock' => array( 'color' => 'green' ) ) ); $context = $context_before; - gutenberg_interactivity_process_wp_style( $tags, $context ); + gutenberg_interactivity_process_wp_style( $tags, $context, 'myblock' ); $this->assertSame( '
    Test
    ', @@ -35,9 +35,9 @@ public function test_directive_ignores_empty_style() { $tags = new WP_HTML_Tag_Processor( $markup ); $tags->next_tag(); - $context_before = new WP_Directive_Context( array( 'color' => 'green' ) ); + $context_before = new WP_Directive_Context( array( 'myblock' => array( 'color' => 'green' ) ) ); $context = $context_before; - gutenberg_interactivity_process_wp_style( $tags, $context ); + gutenberg_interactivity_process_wp_style( $tags, $context, 'myblock' ); $this->assertSame( $markup, $tags->get_updated_html() ); $this->assertStringNotContainsString( 'color: green;', $tags->get_attribute( 'style' ) ); @@ -49,9 +49,9 @@ public function test_directive_works_without_style_attribute() { $tags = new WP_HTML_Tag_Processor( $markup ); $tags->next_tag(); - $context_before = new WP_Directive_Context( array( 'color' => 'green' ) ); + $context_before = new WP_Directive_Context( array( 'myblock' => array( 'color' => 'green' ) ) ); $context = $context_before; - gutenberg_interactivity_process_wp_style( $tags, $context ); + gutenberg_interactivity_process_wp_style( $tags, $context, 'myblock' ); $this->assertSame( '
    Test
    ', diff --git a/phpunit/experimental/interactivity-api/directives/wp-text-test.php b/phpunit/experimental/interactivity-api/directives/wp-text-test.php index 81d2d0f370a64b..9c889a3f0eb68f 100644 --- a/phpunit/experimental/interactivity-api/directives/wp-text-test.php +++ b/phpunit/experimental/interactivity-api/directives/wp-text-test.php @@ -14,31 +14,31 @@ */ class Tests_Directives_WpText extends WP_UnitTestCase { public function test_directive_sets_inner_html_based_on_attribute_value_and_escapes_html() { - $markup = '
    '; + $markup = '
    '; $tags = new WP_Directive_Processor( $markup ); $tags->next_tag(); $context_before = new WP_Directive_Context( array( 'myblock' => array( 'someText' => 'The HTML tag
    produces a line break.' ) ) ); $context = clone $context_before; - gutenberg_interactivity_process_wp_text( $tags, $context ); + gutenberg_interactivity_process_wp_text( $tags, $context, 'myblock' ); - $expected_markup = '
    The HTML tag <br> produces a line break.
    '; + $expected_markup = '
    The HTML tag <br> produces a line break.
    '; $this->assertSame( $expected_markup, $tags->get_updated_html() ); $this->assertSame( $context_before->get_context(), $context->get_context(), 'data-wp-text directive changed context' ); } public function test_directive_overwrites_inner_html_based_on_attribute_value() { - $markup = '
    Lorem ipsum dolor sit.
    '; + $markup = '
    Lorem ipsum dolor sit.
    '; $tags = new WP_Directive_Processor( $markup ); $tags->next_tag(); $context_before = new WP_Directive_Context( array( 'myblock' => array( 'someText' => 'Honi soit qui mal y pense.' ) ) ); $context = clone $context_before; - gutenberg_interactivity_process_wp_text( $tags, $context ); + gutenberg_interactivity_process_wp_text( $tags, $context, 'myblock' ); - $expected_markup = '
    Honi soit qui mal y pense.
    '; + $expected_markup = '
    Honi soit qui mal y pense.
    '; $this->assertSame( $expected_markup, $tags->get_updated_html() ); $this->assertSame( $context_before->get_context(), $context->get_context(), 'data-wp-text directive changed context' ); } diff --git a/phpunit/tests/fonts/font-library/wpFontLibrary/base.php b/phpunit/tests/fonts/font-library/wpFontLibrary/base.php new file mode 100644 index 00000000000000..e8d970f5b3d393 --- /dev/null +++ b/phpunit/tests/fonts/font-library/wpFontLibrary/base.php @@ -0,0 +1,26 @@ +getProperty( 'collections' ); + $property->setAccessible( true ); + $property->setValue( array() ); + } + + public function set_up() { + parent::set_up(); + $this->reset_font_collections(); + } + + public function tear_down() { + parent::tear_down(); + $this->reset_font_collections(); + } +} diff --git a/phpunit/tests/fonts/font-library/wpFontLibrary/fontsDir.php b/phpunit/tests/fonts/font-library/wpFontLibrary/fontsDir.php new file mode 100644 index 00000000000000..9926bb74090888 --- /dev/null +++ b/phpunit/tests/fonts/font-library/wpFontLibrary/fontsDir.php @@ -0,0 +1,70 @@ +dir_defaults = array( + 'path' => path_join( WP_CONTENT_DIR, 'fonts' ), + 'url' => content_url( 'fonts' ), + 'subdir' => '', + 'basedir' => path_join( WP_CONTENT_DIR, 'fonts' ), + 'baseurl' => content_url( 'fonts' ), + 'error' => false, + ); + } + + public function test_fonts_dir() { + $fonts_dir = WP_Font_Library::fonts_dir(); + $this->assertEquals( $fonts_dir, $this->dir_defaults ); + } + + public function test_fonts_dir_with_filter() { + // Define a callback function to pass to the filter. + function set_new_values( $defaults ) { + $defaults['path'] = '/custom-path/fonts/my-custom-subdir'; + $defaults['url'] = 'http://example.com/custom-path/fonts/my-custom-subdir'; + $defaults['subdir'] = 'my-custom-subdir'; + $defaults['basedir'] = '/custom-path/fonts'; + $defaults['baseurl'] = 'http://example.com/custom-path/fonts'; + $defaults['error'] = false; + return $defaults; + } + + // Add the filter. + add_filter( 'fonts_dir', 'set_new_values' ); + + // Gets the fonts dir. + $fonts_dir = WP_Font_Library::fonts_dir(); + + $expected = array( + 'path' => '/custom-path/fonts/my-custom-subdir', + 'url' => 'http://example.com/custom-path/fonts/my-custom-subdir', + 'subdir' => 'my-custom-subdir', + 'basedir' => '/custom-path/fonts', + 'baseurl' => 'http://example.com/custom-path/fonts', + 'error' => false, + ); + + $this->assertEquals( $fonts_dir, $expected, 'The fonts_dir() method should return the expected values.' ); + + // Remove the filter. + remove_filter( 'fonts_dir', 'set_new_values' ); + + // Gets the fonts dir. + $fonts_dir = WP_Font_Library::fonts_dir(); + + $this->assertEquals( $fonts_dir, $this->dir_defaults, 'The fonts_dir() method should return the default values.' ); + } +} diff --git a/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollection.php b/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollection.php index bfdb7258fa11aa..00d5ca2dcb2e73 100644 --- a/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollection.php +++ b/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollection.php @@ -10,20 +10,16 @@ * * @covers WP_Font_Library::get_font_collection */ -class Tests_Fonts_WpFontLibrary_GetFontCollection extends WP_UnitTestCase { +class Tests_Fonts_WpFontLibrary_GetFontCollection extends WP_Font_Library_UnitTestCase { - public static function set_up_before_class() { + public function test_should_get_font_collection() { $my_font_collection_config = array( 'id' => 'my-font-collection', 'name' => 'My Font Collection', 'description' => 'Demo about how to a font collection to your WordPress Font Library.', 'src' => path_join( __DIR__, 'my-font-collection-data.json' ), ); - wp_register_font_collection( $my_font_collection_config ); - } - - public function test_should_get_font_collection() { $font_collection = WP_Font_Library::get_font_collection( 'my-font-collection' ); $this->assertInstanceOf( 'WP_Font_Collection', $font_collection ); } diff --git a/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollections.php b/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollections.php index 97e66e64e87161..40eacba8e18c56 100644 --- a/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollections.php +++ b/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollections.php @@ -10,11 +10,13 @@ * * @covers WP_Font_Library::get_font_collections */ -class Tests_Fonts_WpFontLibrary_GetFontCollections extends WP_UnitTestCase { - - public static function set_up_before_class() { - $font_library = new WP_Font_Library(); +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(); + $this->assertEmpty( $font_collections, 'Should return an empty array.' ); + } + public function test_should_get_mock_font_collection() { $my_font_collection_config = array( 'id' => 'my-font-collection', 'name' => 'My Font Collection', @@ -22,23 +24,11 @@ public static function set_up_before_class() { 'src' => path_join( __DIR__, 'my-font-collection-data.json' ), ); - $font_library::register_font_collection( $my_font_collection_config ); - } - - public function test_should_get_the_default_font_collection() { - $font_collections = WP_Font_Library::get_font_collections(); - $this->assertArrayHasKey( 'default-font-collection', $font_collections, 'Default Google Fonts collection should be registered' ); - $this->assertInstanceOf( 'WP_Font_Collection', $font_collections['default-font-collection'], 'The value of the array $font_collections[id] should be an instance of WP_Font_Collection class.' ); - } + WP_Font_Library::register_font_collection( $my_font_collection_config ); - public function test_should_get_the_right_number_of_collections() { $font_collections = WP_Font_Library::get_font_collections(); $this->assertNotEmpty( $font_collections, 'Sould return an array of font collections.' ); - $this->assertCount( 2, $font_collections, 'Should return an array with one font collection.' ); - } - - public function test_should_get_mock_font_collection() { - $font_collections = WP_Font_Library::get_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.' ); $this->assertInstanceOf( 'WP_Font_Collection', $font_collections['my-font-collection'], 'The value of the array $font_collections[id] should be an instance of WP_Font_Collection class.' ); } diff --git a/phpunit/tests/fonts/font-library/wpFontLibrary/getFontsDir.php b/phpunit/tests/fonts/font-library/wpFontLibrary/getFontsDir.php deleted file mode 100644 index 4bbafc55a2147c..00000000000000 --- a/phpunit/tests/fonts/font-library/wpFontLibrary/getFontsDir.php +++ /dev/null @@ -1,18 +0,0 @@ -assertStringEndsWith( '/wp-content/fonts', WP_Font_Library::get_fonts_dir() ); - } -} diff --git a/phpunit/tests/fonts/font-library/wpFontLibrary/getMimeTypes.php b/phpunit/tests/fonts/font-library/wpFontLibrary/getMimeTypes.php index 708134af69e92a..485587060f16a1 100644 --- a/phpunit/tests/fonts/font-library/wpFontLibrary/getMimeTypes.php +++ b/phpunit/tests/fonts/font-library/wpFontLibrary/getMimeTypes.php @@ -10,7 +10,7 @@ * * @covers WP_Font_Family_Utils::get_expected_font_mime_types_per_php_version */ -class Tests_Fonts_WpFontsFamilyUtils_GetMimeTypes extends WP_UnitTestCase { +class Tests_Fonts_WpFontsFamilyUtils_GetMimeTypes extends WP_Font_Library_UnitTestCase { /** * diff --git a/phpunit/tests/fonts/font-library/wpFontLibrary/registerFontCollection.php b/phpunit/tests/fonts/font-library/wpFontLibrary/registerFontCollection.php index 6bc5fbb8161cee..2569830f6bf2aa 100644 --- a/phpunit/tests/fonts/font-library/wpFontLibrary/registerFontCollection.php +++ b/phpunit/tests/fonts/font-library/wpFontLibrary/registerFontCollection.php @@ -10,7 +10,7 @@ * * @covers WP_Font_Library::register_font_collection */ -class Tests_Fonts_WpFontLibrary_RegisterFontCollection extends WP_UnitTestCase { +class Tests_Fonts_WpFontLibrary_RegisterFontCollection extends WP_Font_Library_UnitTestCase { public function test_should_register_font_collection() { $config = array( @@ -70,8 +70,10 @@ public function test_should_return_error_if_id_is_repeated() { $collection1 = WP_Font_Library::register_font_collection( $config1 ); $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 id. $collection2 = WP_Font_Library::register_font_collection( $config2 ); - $this->assertWPError( $collection2, 'Second collection with the same id should fail.' ); + $this->assertWPError( $collection2, 'A WP_Error should be returned.' ); } } diff --git a/phpunit/tests/fonts/font-library/wpFontLibrary/setUploadDir.php b/phpunit/tests/fonts/font-library/wpFontLibrary/setUploadDir.php deleted file mode 100644 index daa4c84aad9004..00000000000000 --- a/phpunit/tests/fonts/font-library/wpFontLibrary/setUploadDir.php +++ /dev/null @@ -1,32 +0,0 @@ - '/abc', - 'basedir' => '/any/path', - 'baseurl' => 'http://example.com/an/arbitrary/url', - 'path' => '/any/path/abc', - 'url' => 'http://example.com/an/arbitrary/url/abc', - ); - $expected = array( - 'subdir' => '/fonts', - 'basedir' => WP_CONTENT_DIR, - 'baseurl' => content_url(), - 'path' => path_join( WP_CONTENT_DIR, 'fonts' ), - 'url' => content_url() . '/fonts', - ); - $this->assertSame( $expected, WP_Font_Library::set_upload_dir( $defaults ) ); - } -} diff --git a/phpunit/tests/fonts/font-library/wpFontLibrary/unregisterFontCollection.php b/phpunit/tests/fonts/font-library/wpFontLibrary/unregisterFontCollection.php new file mode 100644 index 00000000000000..e6e16956814fb4 --- /dev/null +++ b/phpunit/tests/fonts/font-library/wpFontLibrary/unregisterFontCollection.php @@ -0,0 +1,54 @@ + 'mock-font-collection-1', + 'name' => 'Mock Collection to be unregistered', + 'description' => 'A mock font collection to be unregistered.', + 'src' => 'my-collection-data.json', + ); + WP_Font_Library::register_font_collection( $config ); + + $config = array( + 'id' => 'mock-font-collection-2', + 'name' => 'Mock Collection', + 'description' => 'A mock font collection.', + 'src' => 'my-mock-data.json', + ); + WP_Font_Library::register_font_collection( $config ); + + // Unregister mock font collection. + WP_Font_Library::unregister_font_collection( 'mock-font-collection-1' ); + $collections = WP_Font_Library::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(); + $this->assertArrayNotHasKey( 'mock-font-collection-2', $collections, 'Mock font collection was not unregistered.' ); + + // Checks that all font collections were unregistered. + $this->assertEmpty( $collections, 'Font collections were not unregistered.' ); + } + + 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(); + $this->assertEmpty( $collections, 'Should not be registered collections.' ); + } +} diff --git a/phpunit/tests/fonts/font-library/wpRestFontFamiliesController/installFonts.php b/phpunit/tests/fonts/font-library/wpRestFontFamiliesController/installFonts.php index d35022306f4e6f..98c1cb6e13fe5c 100644 --- a/phpunit/tests/fonts/font-library/wpRestFontFamiliesController/installFonts.php +++ b/phpunit/tests/fonts/font-library/wpRestFontFamiliesController/installFonts.php @@ -21,10 +21,10 @@ class Tests_Fonts_WPRESTFontFamiliesController_InstallFonts extends WP_REST_Font * @param array $files Font files to install. * @param array $expected_response Expected response data. */ - public function test_install_fonts( $font_families, $files, $expected_response ) { - $install_request = new WP_REST_Request( 'POST', '/wp/v2/font-families' ); - $font_families_json = json_encode( $font_families ); - $install_request->set_param( 'font_families', $font_families_json ); + public function test_install_fonts( $font_family_settings, $files, $expected_response ) { + $install_request = new WP_REST_Request( 'POST', '/wp/v2/font-families' ); + $font_family_json = json_encode( $font_family_settings ); + $install_request->set_param( 'font_family_settings', $font_family_json ); $install_request->set_file_params( $files ); $response = rest_get_server()->dispatch( $install_request ); $data = $response->get_data(); @@ -68,38 +68,22 @@ public function data_install_fonts() { return array( 'google_fonts_to_download' => array( - 'font_families' => array( - array( - 'fontFamily' => 'Piazzolla', - 'slug' => 'piazzolla', - 'name' => 'Piazzolla', - 'fontFace' => array( - array( - 'fontFamily' => 'Piazzolla', - 'fontStyle' => 'normal', - 'fontWeight' => '400', - 'src' => 'http://fonts.gstatic.com/s/piazzolla/v33/N0b72SlTPu5rIkWIZjVgI-TckS03oGpPETyEJ88Rbvi0_TzOzKcQhZqx3gX9BRy5m5M.ttf', - 'downloadFromUrl' => 'http://fonts.gstatic.com/s/piazzolla/v33/N0b72SlTPu5rIkWIZjVgI-TckS03oGpPETyEJ88Rbvi0_TzOzKcQhZqx3gX9BRy5m5M.ttf', - ), - ), - ), - array( - 'fontFamily' => 'Montserrat', - 'slug' => 'montserrat', - 'name' => 'Montserrat', - 'fontFace' => array( - array( - 'fontFamily' => 'Montserrat', - 'fontStyle' => 'normal', - 'fontWeight' => '100', - 'src' => 'http://fonts.gstatic.com/s/montserrat/v25/JTUHjIg1_i6t8kCHKm4532VJOt5-QNFgpCtr6Uw-Y3tcoqK5.ttf', - 'downloadFromUrl' => 'http://fonts.gstatic.com/s/montserrat/v25/JTUHjIg1_i6t8kCHKm4532VJOt5-QNFgpCtr6Uw-Y3tcoqK5.ttf', - ), + 'font_family_settings' => array( + 'fontFamily' => 'Piazzolla', + 'slug' => 'piazzolla', + 'name' => 'Piazzolla', + 'fontFace' => array( + array( + 'fontFamily' => 'Piazzolla', + 'fontStyle' => 'normal', + 'fontWeight' => '400', + 'src' => 'http://fonts.gstatic.com/s/piazzolla/v33/N0b72SlTPu5rIkWIZjVgI-TckS03oGpPETyEJ88Rbvi0_TzOzKcQhZqx3gX9BRy5m5M.ttf', + 'downloadFromUrl' => 'http://fonts.gstatic.com/s/piazzolla/v33/N0b72SlTPu5rIkWIZjVgI-TckS03oGpPETyEJ88Rbvi0_TzOzKcQhZqx3gX9BRy5m5M.ttf', ), ), ), - 'files' => array(), - 'expected_response' => array( + 'files' => array(), + 'expected_response' => array( 'successes' => array( array( 'fontFamily' => 'Piazzolla', @@ -114,55 +98,27 @@ public function data_install_fonts() { ), ), ), - array( - 'fontFamily' => 'Montserrat', - 'slug' => 'montserrat', - 'name' => 'Montserrat', - 'fontFace' => array( - array( - 'fontFamily' => 'Montserrat', - 'fontStyle' => 'normal', - 'fontWeight' => '100', - 'src' => '/wp-content/fonts/montserrat_normal_100.ttf', - ), - ), - ), ), 'errors' => array(), ), ), 'google_fonts_to_use_as_is' => array( - 'font_families' => array( - array( - 'fontFamily' => 'Piazzolla', - 'slug' => 'piazzolla', - 'name' => 'Piazzolla', - 'fontFace' => array( - array( - 'fontFamily' => 'Piazzolla', - 'fontStyle' => 'normal', - 'fontWeight' => '400', - 'src' => 'http://fonts.gstatic.com/s/piazzolla/v33/N0b72SlTPu5rIkWIZjVgI-TckS03oGpPETyEJ88Rbvi0_TzOzKcQhZqx3gX9BRy5m5M.ttf', - ), - ), - ), - array( - 'fontFamily' => 'Montserrat', - 'slug' => 'montserrat', - 'name' => 'Montserrat', - 'fontFace' => array( - array( - 'fontFamily' => 'Montserrat', - 'fontStyle' => 'normal', - 'fontWeight' => '100', - 'src' => 'http://fonts.gstatic.com/s/montserrat/v25/JTUHjIg1_i6t8kCHKm4532VJOt5-QNFgpCtr6Uw-Y3tcoqK5.ttf', - ), + 'font_family_settings' => array( + 'fontFamily' => 'Piazzolla', + 'slug' => 'piazzolla', + 'name' => 'Piazzolla', + 'fontFace' => array( + array( + 'fontFamily' => 'Piazzolla', + 'fontStyle' => 'normal', + 'fontWeight' => '400', + 'src' => 'http://fonts.gstatic.com/s/piazzolla/v33/N0b72SlTPu5rIkWIZjVgI-TckS03oGpPETyEJ88Rbvi0_TzOzKcQhZqx3gX9BRy5m5M.ttf', ), ), ), - 'files' => array(), - 'expected_response' => array( + 'files' => array(), + 'expected_response' => array( 'successes' => array( array( 'fontFamily' => 'Piazzolla', @@ -177,35 +133,19 @@ public function data_install_fonts() { ), ), ), - array( - 'fontFamily' => 'Montserrat', - 'slug' => 'montserrat', - 'name' => 'Montserrat', - 'fontFace' => array( - array( - 'fontFamily' => 'Montserrat', - 'fontStyle' => 'normal', - 'fontWeight' => '100', - 'src' => 'http://fonts.gstatic.com/s/montserrat/v25/JTUHjIg1_i6t8kCHKm4532VJOt5-QNFgpCtr6Uw-Y3tcoqK5.ttf', - - ), - ), - ), ), 'errors' => array(), ), ), 'fonts_without_font_faces' => array( - 'font_families' => array( - array( - 'fontFamily' => 'Arial', - 'slug' => 'arial', - 'name' => 'Arial', - ), + 'font_family_settings' => array( + 'fontFamily' => 'Arial', + 'slug' => 'arial', + 'name' => 'Arial', ), - 'files' => array(), - 'expected_response' => array( + 'files' => array(), + 'expected_response' => array( 'successes' => array( array( 'fontFamily' => 'Arial', @@ -218,35 +158,20 @@ public function data_install_fonts() { ), 'fonts_with_local_fonts_assets' => array( - 'font_families' => array( - array( - 'fontFamily' => 'Piazzolla', - 'slug' => 'piazzolla', - 'name' => 'Piazzolla', - 'fontFace' => array( - array( - 'fontFamily' => 'Piazzolla', - 'fontStyle' => 'normal', - 'fontWeight' => '400', - 'uploadedFile' => 'files0', - ), - ), - ), - array( - 'fontFamily' => 'Montserrat', - 'slug' => 'montserrat', - 'name' => 'Montserrat', - 'fontFace' => array( - array( - 'fontFamily' => 'Montserrat', - 'fontStyle' => 'normal', - 'fontWeight' => '100', - 'uploadedFile' => 'files1', - ), + 'font_family_settings' => array( + 'fontFamily' => 'Piazzolla', + 'slug' => 'piazzolla', + 'name' => 'Piazzolla', + 'fontFace' => array( + array( + 'fontFamily' => 'Piazzolla', + 'fontStyle' => 'normal', + 'fontWeight' => '400', + 'uploadedFile' => 'files0', ), ), ), - 'files' => array( + 'files' => array( 'files0' => array( 'name' => 'piazzola1.ttf', 'type' => 'font/ttf', @@ -262,7 +187,7 @@ public function data_install_fonts() { 'size' => 123, ), ), - 'expected_response' => array( + 'expected_response' => array( 'successes' => array( array( 'fontFamily' => 'Piazzolla', @@ -277,20 +202,6 @@ public function data_install_fonts() { ), ), ), - array( - 'fontFamily' => 'Montserrat', - 'slug' => 'montserrat', - 'name' => 'Montserrat', - 'fontFace' => array( - array( - 'fontFamily' => 'Montserrat', - 'fontStyle' => 'normal', - 'fontWeight' => '100', - 'src' => '/wp-content/fonts/montserrat_normal_100.ttf', - ), - ), - ), - ), 'errors' => array(), ), @@ -325,15 +236,15 @@ public function data_install_with_improper_inputs() { return array( 'not a font families array' => array( - 'font_families' => 'This is not an array', + 'font_family_settings' => 'This is not an array', ), 'empty array' => array( - 'font_families' => array(), + 'font_family_settings' => array(), ), 'without slug' => array( - 'font_families' => array( + 'font_family_settings' => array( array( 'fontFamily' => 'Piazzolla', 'name' => 'Piazzolla', @@ -342,63 +253,55 @@ public function data_install_with_improper_inputs() { ), 'with improper font face property' => array( - 'font_families' => array( - array( - 'fontFamily' => 'Piazzolla', - 'name' => 'Piazzolla', - 'slug' => 'piazzolla', - 'fontFace' => 'This is not an array', - ), + 'font_family_settings' => array( + 'fontFamily' => 'Piazzolla', + 'name' => 'Piazzolla', + 'slug' => 'piazzolla', + 'fontFace' => 'This is not an array', ), ), 'with empty font face property' => array( - 'font_families' => array( - array( - 'fontFamily' => 'Piazzolla', - 'name' => 'Piazzolla', - 'slug' => 'piazzolla', - 'fontFace' => array(), - ), + 'font_family_settings' => array( + 'fontFamily' => 'Piazzolla', + 'name' => 'Piazzolla', + 'slug' => 'piazzolla', + 'fontFace' => array(), ), ), 'fontface referencing uploaded file without uploaded files' => array( - 'font_families' => array( - array( - 'fontFamily' => 'Piazzolla', - 'name' => 'Piazzolla', - 'slug' => 'piazzolla', - 'fontFace' => array( - array( - 'fontFamily' => 'Piazzolla', - 'fontStyle' => 'normal', - 'fontWeight' => '400', - 'uploadedFile' => 'files0', - ), + 'font_family_settings' => array( + 'fontFamily' => 'Piazzolla', + 'name' => 'Piazzolla', + 'slug' => 'piazzolla', + 'fontFace' => array( + array( + 'fontFamily' => 'Piazzolla', + 'fontStyle' => 'normal', + 'fontWeight' => '400', + 'uploadedFile' => 'files0', ), ), ), - 'files' => array(), + 'files' => array(), ), 'fontface referencing uploaded file without uploaded files' => array( - 'font_families' => array( - array( - 'fontFamily' => 'Piazzolla', - 'name' => 'Piazzolla', - 'slug' => 'piazzolla', - 'fontFace' => array( - array( - 'fontFamily' => 'Piazzolla', - 'fontStyle' => 'normal', - 'fontWeight' => '400', - 'uploadedFile' => 'files666', - ), + 'font_family_settings' => array( + 'fontFamily' => 'Piazzolla', + 'name' => 'Piazzolla', + 'slug' => 'piazzolla', + 'fontFace' => array( + array( + 'fontFamily' => 'Piazzolla', + 'fontStyle' => 'normal', + 'fontWeight' => '400', + 'uploadedFile' => 'files666', ), ), ), - 'files' => array( + 'files' => array( 'files0' => array( 'name' => 'piazzola1.ttf', 'type' => 'font/ttf', @@ -410,20 +313,18 @@ public function data_install_with_improper_inputs() { ), 'fontface with incompatible properties (downloadFromUrl and uploadedFile together)' => array( - 'font_families' => array( - array( - 'fontFamily' => 'Piazzolla', - 'slug' => 'piazzolla', - 'name' => 'Piazzolla', - 'fontFace' => array( - array( - 'fontFamily' => 'Piazzolla', - 'fontStyle' => 'normal', - 'fontWeight' => '400', - 'src' => 'http://fonts.gstatic.com/s/piazzolla/v33/N0b72SlTPu5rIkWIZjVgI-TckS03oGpPETyEJ88Rbvi0_TzOzKcQhZqx3gX9BRy5m5M.ttf', - 'downloadFromUrl' => 'http://fonts.gstatic.com/s/piazzolla/v33/N0b72SlTPu5rIkWIZjVgI-TckS03oGpPETyEJ88Rbvi0_TzOzKcQhZqx3gX9BRy5m5M.ttf', - 'uploadedFile' => 'files0', - ), + 'font_family_settings' => array( + 'fontFamily' => 'Piazzolla', + 'slug' => 'piazzolla', + 'name' => 'Piazzolla', + 'fontFace' => array( + array( + 'fontFamily' => 'Piazzolla', + 'fontStyle' => 'normal', + 'fontWeight' => '400', + 'src' => 'http://fonts.gstatic.com/s/piazzolla/v33/N0b72SlTPu5rIkWIZjVgI-TckS03oGpPETyEJ88Rbvi0_TzOzKcQhZqx3gX9BRy5m5M.ttf', + 'downloadFromUrl' => 'http://fonts.gstatic.com/s/piazzolla/v33/N0b72SlTPu5rIkWIZjVgI-TckS03oGpPETyEJ88Rbvi0_TzOzKcQhZqx3gX9BRy5m5M.ttf', + 'uploadedFile' => 'files0', ), ), ), diff --git a/test/e2e/specs/editor/blocks/image.spec.js b/test/e2e/specs/editor/blocks/image.spec.js index adeabc860c8342..9080a6dc194021 100644 --- a/test/e2e/specs/editor/blocks/image.spec.js +++ b/test/e2e/specs/editor/blocks/image.spec.js @@ -696,7 +696,9 @@ test.describe( 'Image', () => { await expect( linkDom ).toHaveAttribute( 'href', url ); } ); - test( 'should upload external image', async ( { editor } ) => { + test( 'should upload external image to media library', async ( { + editor, + } ) => { await editor.insertBlock( { name: 'core/image', attributes: { @@ -704,7 +706,7 @@ test.describe( 'Image', () => { }, } ); - await editor.clickBlockToolbarButton( 'Upload external image' ); + await editor.clickBlockToolbarButton( 'Upload image to media library' ); const imageBlock = editor.canvas.locator( 'role=document[name="Block: Image"i]' diff --git a/packages/e2e-tests/specs/editor/various/core-settings.test.js b/test/e2e/specs/editor/various/core-settings.spec.js similarity index 58% rename from packages/e2e-tests/specs/editor/various/core-settings.test.js rename to test/e2e/specs/editor/various/core-settings.spec.js index 0eb98a2de050b4..9dddc273e6b16e 100644 --- a/packages/e2e-tests/specs/editor/various/core-settings.test.js +++ b/test/e2e/specs/editor/various/core-settings.spec.js @@ -1,10 +1,10 @@ /** * WordPress dependencies */ -import { visitAdminPage } from '@wordpress/e2e-test-utils'; +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); -async function getOptionsValues( selector ) { - await visitAdminPage( 'options.php' ); +async function getOptionsValues( selector, admin, page ) { + await admin.visitAdminPage( 'options.php' ); return page.evaluate( ( theSelector ) => { const inputs = Array.from( document.querySelectorAll( theSelector ) ); return inputs.reduce( ( memo, input ) => { @@ -16,22 +16,32 @@ async function getOptionsValues( selector ) { // It might make sense to include a similar test in WP core (or move this one over). // See discussion here: https://github.com/WordPress/gutenberg/pull/32797#issuecomment-864192088. -describe( 'Settings', () => { - test( 'Regression: updating a specific option will only change its value and will not corrupt others', async () => { +test.describe( 'Settings', () => { + test( 'Regression: updating a specific option will only change its value and will not corrupt others', async ( { + page, + admin, + } ) => { // We won't select the option that we updated and will also remove some // _transient options that seem to change at every update. const optionsInputsSelector = 'form#all-options table.form-table input:not([id*="_transient"]):not([id="blogdescription"])'; - const optionsBefore = await getOptionsValues( optionsInputsSelector ); - - await visitAdminPage( 'options-general.php' ); - await page.type( - 'input#blogdescription', - 'Just another Gutenberg site' + const optionsBefore = await getOptionsValues( + optionsInputsSelector, + admin, + page ); - await page.click( 'input#submit' ); - const optionsAfter = await getOptionsValues( optionsInputsSelector ); + await admin.visitAdminPage( 'options-general.php' ); + await page + .getByRole( 'textbox', { name: 'Tagline' } ) + .fill( 'Just another Gutenberg site' ); + await page.getByRole( 'button', { name: 'Save Changes' } ).click(); + + const optionsAfter = await getOptionsValues( + optionsInputsSelector, + admin, + page + ); Object.entries( optionsBefore ).forEach( ( optionBefore ) => { const [ id ] = optionBefore; diff --git a/test/e2e/specs/editor/various/dropdown-menu.spec.js b/test/e2e/specs/editor/various/dropdown-menu.spec.js new file mode 100644 index 00000000000000..916ef3447d80a4 --- /dev/null +++ b/test/e2e/specs/editor/various/dropdown-menu.spec.js @@ -0,0 +1,62 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'Dropdown Menu', () => { + test.beforeEach( async ( { admin } ) => { + await admin.createNewPost(); + } ); + + test( 'keyboard navigiation', async ( { page, pageUtils } ) => { + await page + .getByRole( 'region', { name: 'Editor top bar' } ) + .getByRole( 'button', { name: 'Options' } ) + .click(); + const menuItems = page.locator( + '[role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"]' + ); + const totalItems = await menuItems.count(); + + // Catch any issues with the selector, which could cause a false positive test result. + expect( totalItems ).toBeGreaterThan( 0 ); + + await test.step( 'allows navigation through each item using arrow keys', async () => { + // Expect the first menu item to be focused. + await expect( menuItems.first() ).toBeFocused(); + + // Arrow down to the last item. + await pageUtils.pressKeys( 'ArrowDown', { times: totalItems - 1 } ); + await expect( menuItems.last() ).toBeFocused(); + + // Arrow back up to the first item. + await pageUtils.pressKeys( 'ArrowUp', { times: totalItems - 1 } ); + await expect( menuItems.first() ).toBeFocused(); + } ); + + await test.step( 'loops to the beginning and end when navigating past the boundaries of the menu', async () => { + // Expect the first menu item to be focused. + await expect( menuItems.first() ).toBeFocused(); + + // Arrow up to the last item. + await page.keyboard.press( 'ArrowUp' ); + await expect( menuItems.last() ).toBeFocused(); + + // Arrow back down to the first item. + await page.keyboard.press( 'ArrowDown' ); + await expect( menuItems.first() ).toBeFocused(); + } ); + + await test.step( 'ignores arrow key navigation that is orthogonal to the orientation of the menu, but stays open', async () => { + // Expect the first menu item to be focused. + await expect( menuItems.first() ).toBeFocused(); + + // Press left and right keys an arbitrary (but > 1) number of times. + await pageUtils.pressKeys( 'ArrowLeft', { times: 5 } ); + await pageUtils.pressKeys( 'ArrowRight', { times: 5 } ); + + // Expect the first menu item to still be focused. + await expect( menuItems.first() ).toBeFocused(); + } ); + } ); +} ); diff --git a/test/e2e/specs/editor/various/taxonomies.spec.js b/test/e2e/specs/editor/various/taxonomies.spec.js new file mode 100644 index 00000000000000..efd8c9c6ee7fe0 --- /dev/null +++ b/test/e2e/specs/editor/various/taxonomies.spec.js @@ -0,0 +1,136 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +function generateRandomNumber() { + return Math.round( 1 + Math.random() * ( Number.MAX_SAFE_INTEGER - 1 ) ); +} + +test.describe( 'Taxonomies', () => { + test.beforeEach( async ( { admin, editor } ) => { + await admin.createNewPost(); + await editor.openDocumentSettingsSidebar(); + } ); + + test( 'should be able to open the categories panel and create a new main category', async ( { + editor, + page, + } ) => { + // Open the Document -> Categories panel. + const panelToggle = page.getByRole( 'button', { + name: 'Categories', + } ); + + if ( + ( await panelToggle.getAttribute( 'aria-expanded' ) ) === 'false' + ) { + await panelToggle.click(); + } + + await page + .getByRole( 'button', { + name: 'Add New Category', + expanded: false, + } ) + .click(); + await page + .getByRole( 'textbox', { name: 'New Category Name' } ) + .fill( 'z rand category 1' ); + await page.keyboard.press( 'Enter' ); + + const categories = page.getByRole( 'group', { name: 'Categories' } ); + const selectedCategories = categories.getByRole( 'checkbox', { + checked: true, + } ); + const newCategory = categories.getByRole( 'checkbox', { + name: 'z rand category 1', + } ); + + await expect( selectedCategories ).toHaveCount( 1 ); + await expect( newCategory ).toBeChecked(); + + await editor.canvas + .getByRole( 'textbox', { name: 'Add title' } ) + .fill( 'Hello World' ); + await editor.publishPost(); + await page.reload(); + + // The category selection was persisted after the publish process. + await expect( selectedCategories ).toHaveCount( 1 ); + await expect( newCategory ).toBeChecked(); + } ); + + test( 'should be able to open the tags panel and create a new tag', async ( { + editor, + page, + } ) => { + // Open the Document -> Tags panel. + const panelToggle = page.getByRole( 'button', { + name: 'Tags', + } ); + + if ( + ( await panelToggle.getAttribute( 'aria-expanded' ) ) === 'false' + ) { + await panelToggle.click(); + } + + const tagName = 'tag-' + generateRandomNumber(); + const tags = page.locator( '.components-form-token-field__token-text' ); + + await page + .getByRole( 'combobox', { name: 'Add New Tag' } ) + .fill( tagName ); + await page.keyboard.press( 'Enter' ); + + await expect( tags ).toHaveCount( 1 ); + await expect( tags ).toContainText( tagName ); + + await editor.canvas + .getByRole( 'textbox', { name: 'Add title' } ) + .fill( 'Hello World' ); + await editor.publishPost(); + await page.reload(); + + await expect( tags ).toHaveCount( 1 ); + await expect( tags ).toContainText( tagName ); + } ); + + // See: https://github.com/WordPress/gutenberg/pull/21693. + test( `should be able to create a new tag with ' on the name`, async ( { + editor, + page, + } ) => { + // Open the Document -> Tags panel. + const panelToggle = page.getByRole( 'button', { + name: 'Tags', + } ); + + if ( + ( await panelToggle.getAttribute( 'aria-expanded' ) ) === 'false' + ) { + await panelToggle.click(); + } + + const tagName = "tag'-" + generateRandomNumber(); + const tags = page.locator( '.components-form-token-field__token-text' ); + + await page + .getByRole( 'combobox', { name: 'Add New Tag' } ) + .fill( tagName ); + await page.keyboard.press( 'Enter' ); + + await expect( tags ).toHaveCount( 1 ); + await expect( tags ).toContainText( tagName ); + + await editor.canvas + .getByRole( 'textbox', { name: 'Add title' } ) + .fill( 'Hello World' ); + await editor.publishPost(); + await page.reload(); + + await expect( tags ).toHaveCount( 1 ); + await expect( tags ).toContainText( tagName ); + } ); +} );